E-Friday 会话策略
概述
E-Friday 策略复刻了经典的 MetaTrader 专家顾问,它只在星期五交易。策略观察上一日的日线蜡烛,并在设定的时间于星期五开盘时入场。方向采取反向思路:若上一日收于开盘价下方(阴线),则在星期五做多;若上一日收于开盘价上方(阳线),则在星期五做空。仓位仅在当日管理,可在预设时间后自动平仓,也可通过保护性止损退出。
交易规则
- 订阅日线蜡烛(默认:1 天)以获得前一日的开盘价和收盘价。
- 在星期五订阅日内蜡烛(默认:1 分钟)以检测指定的入场小时。
- 当出现入场小时的第一根蜡烛时:
- 若上一日为阴线则做多。
- 若上一日为阳线则做空。
- 若上一日开收相等则放弃交易。
- 到达设定的平仓小时后,可选择自动平掉当日仓位。
- 使用止损、止盈以及可选的跟踪止损来管理仓位,跟踪逻辑包含原始 EA 中的盈利触发和最小移动距离约束。
实现说明
- 使用 StockSharp 的高级蜡烛订阅机制,同时获取日线背景与日内时序信号。
- 将 MQL 版本中的“点数”风险参数换算为基于合约最小价格步长的绝对价格距离。
- 在代码中维护跟踪止损:每根已完成的蜡烛都会更新止损位置,并在价格穿越止损或目标时发送市价单平仓。
- 通过日内状态记录,确保每个星期五仅开一次仓位。
- 支持多空双向,与原始 EA 一样,每个策略实例只操作单一品种。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
Volume |
交易手数或合约数量。 | 0.1 |
StopLossPoints |
止损距离(价格步长单位,0 为关闭)。 | 75 |
TakeProfitPoints |
止盈距离(价格步长单位,0 为关闭)。 | 0 |
HourOpen |
入场小时(0-23)。 | 7 |
UseClosePositions |
是否在设定时间后自动平仓。 | true |
HourClose |
自动平仓的小时(0-23)。 | 19 |
UseTrailing |
是否启用跟踪止损。 | true |
ProfitTrailing |
是否要求利润超过跟踪距离后再启动跟踪。 | true |
TrailingStopPoints |
跟踪止损距离(价格步长单位)。 | 60 |
TrailingStepPoints |
每次收紧跟踪止损所需的额外距离。 | 5 |
IntradayCandleType |
日内信号所用的蜡烛类型(默认 1 分钟)。 | TimeSpan.FromMinutes(1) |
DailyCandleType |
日线背景所用的蜡烛类型(默认 1 天)。 | TimeSpan.FromDays(1) |
使用建议
- 请确保品种的交易时段与星期五的入场小时对齐,以便在市场开盘后及时入场。
- 设定止损和跟踪距离时,请使用与标的最小价格步长相同的“点数”定义,以还原 MetaTrader 的行为。
- 策略设计为每个星期五仅交易一次,如需同时交易多个品种,请为每个品种单独运行一个策略实例。
与原始 EA 的差异
- 策略使用已完成蜡烛的收盘数据,而原始 EA 在每个 tick 上检查时间条件。
- 当蜡烛最高/最低突破目标时,通过市价单平仓,可能与逐 tick 跟踪存在细微差异。
- 参数通过 StockSharp
StrategyParam系统暴露,可用于优化与界面展示。
执行说明: 星期五的信号在开盘小时为 HourOpen 的第一根已完成分钟K线上判断,而非该小时的首个成交价。必须已有完成的星期四日K线。每个星期五仅允许一次入场,即使保护性平仓已经发生。收盘时段、止损、止盈和追踪止损均按已完成分钟K线的高低价判断并通过市价单退出;追踪价位达到设定步长后才更新,不能假定先见到最高价又在同一根K线过去的最低价成交。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Takes at most one contrarian position on Friday at the configured hour,
/// using the previous completed daily candle; manages session and price exits.
/// </summary>
public class EFridaySessionStrategy : Strategy
{
private readonly StrategyParam<int> _hourOpen;
private readonly StrategyParam<int> _hourClose;
private readonly StrategyParam<bool> _useClosePositions;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<bool> _useTrailing;
private readonly StrategyParam<bool> _profitTrailing;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<DataType> _intradayCandleType;
private readonly StrategyParam<DataType> _dailyCandleType;
private DateTime? _lastDailyDate;
private decimal _dailyOpen;
private decimal _dailyClose;
private DateTime? _tradedDate;
private decimal? _entryPrice;
private decimal? _trailingLevel;
private decimal _bestPrice;
private Order _entryOrder;
private Order _exitOrder;
public int HourOpen { get => _hourOpen.Value; set => _hourOpen.Value = value; }
public int HourClose { get => _hourClose.Value; set => _hourClose.Value = value; }
public bool UseClosePositions { get => _useClosePositions.Value; set => _useClosePositions.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public bool UseTrailing { get => _useTrailing.Value; set => _useTrailing.Value = value; }
public bool ProfitTrailing { get => _profitTrailing.Value; set => _profitTrailing.Value = value; }
public decimal TrailingStopPoints { get => _trailingStopPoints.Value; set => _trailingStopPoints.Value = value; }
public decimal TrailingStepPoints { get => _trailingStepPoints.Value; set => _trailingStepPoints.Value = value; }
public DataType IntradayCandleType { get => _intradayCandleType.Value; set => _intradayCandleType.Value = value; }
public DataType DailyCandleType { get => _dailyCandleType.Value; set => _dailyCandleType.Value = value; }
public EFridaySessionStrategy()
{
Volume = 0.1m;
_hourOpen = Param(nameof(HourOpen), 7).SetRange(0, 23);
_hourClose = Param(nameof(HourClose), 19).SetRange(0, 23);
_useClosePositions = Param(nameof(UseClosePositions), true);
_stopLossPoints = Param(nameof(StopLossPoints), 75m).SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m).SetNotNegative();
_useTrailing = Param(nameof(UseTrailing), true);
_profitTrailing = Param(nameof(ProfitTrailing), true);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 60m).SetNotNegative();
_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m).SetNotNegative();
_intradayCandleType = Param(nameof(IntradayCandleType), TimeSpan.FromMinutes(1).TimeFrame());
_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromDays(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, IntradayCandleType), (Security, DailyCandleType)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_lastDailyDate = null;
_dailyOpen = 0m;
_dailyClose = 0m;
_tradedDate = null;
_entryPrice = null;
_trailingLevel = null;
_bestPrice = 0m;
_entryOrder = null;
_exitOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
if ((Security?.PriceStep ?? 0m) <= 0m)
throw new InvalidOperationException("E-Friday requires a positive PriceStep.");
SubscribeCandles(DailyCandleType).Bind(ProcessDailyCandle).Start();
SubscribeCandles(IntradayCandleType).Bind(ProcessIntradayCandle).Start();
}
private void ProcessDailyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_lastDailyDate = candle.OpenTime.Date;
_dailyOpen = candle.OpenPrice;
_dailyClose = candle.ClosePrice;
}
private static bool IsPending(Order order) => order is not null &&
order.State is not (OrderStates.Done or OrderStates.Failed);
private void ProcessIntradayCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position != 0m && !IsPending(_exitOrder))
{
if (UseClosePositions && candle.OpenTime.Hour >= HourClose)
{
ExitPosition("EFriday session close");
return;
}
if (HandleRiskExits(candle))
return;
}
var day = candle.OpenTime.Date;
if (candle.OpenTime.DayOfWeek != DayOfWeek.Friday ||
candle.OpenTime.Hour != HourOpen || _tradedDate == day ||
_lastDailyDate != day.AddDays(-1) ||
Position != 0m || IsPending(_entryOrder) || IsPending(_exitOrder))
return;
if (_dailyClose == _dailyOpen)
return;
var side = _dailyClose < _dailyOpen ? Sides.Buy : Sides.Sell;
var volume = Volume;
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
if (volume <= 0m)
return;
_entryOrder = Market(side, volume, "EFriday entry");
_tradedDate = day;
}
private bool HandleRiskExits(ICandleMessage candle)
{
if (_entryPrice is not decimal entry)
return false;
var point = Security?.PriceStep ?? 0m;
var longPosition = Position > 0m;
var stop = StopLossPoints * point;
var take = TakeProfitPoints * point;
if (stop > 0m && (longPosition ? candle.LowPrice <= entry - stop :
candle.HighPrice >= entry + stop))
{
ExitPosition("EFriday stop loss");
return true;
}
if (take > 0m && (longPosition ? candle.HighPrice >= entry + take :
candle.LowPrice <= entry - take))
{
ExitPosition("EFriday take profit");
return true;
}
// Check the previously established trailing level before using the
// latest bar's extremes: intrabar high/low order is not available.
if (_trailingLevel is decimal oldStop &&
(longPosition ? candle.LowPrice <= oldStop : candle.HighPrice >= oldStop))
{
ExitPosition("EFriday trailing stop");
return true;
}
if (!UseTrailing || TrailingStopPoints <= 0m)
return false;
var distance = TrailingStopPoints * point;
var improvement = TrailingStepPoints * point;
_bestPrice = longPosition ? Math.Max(_bestPrice, candle.HighPrice) :
Math.Min(_bestPrice, candle.LowPrice);
var profit = longPosition ? _bestPrice - entry : entry - _bestPrice;
if (ProfitTrailing && profit < distance)
return false;
var next = longPosition ? _bestPrice - distance : _bestPrice + distance;
if (_trailingLevel is not decimal current ||
(longPosition ? next >= current + improvement && next > current :
next <= current - improvement && next < current))
_trailingLevel = next;
return false;
}
private Order Market(Sides side, decimal volume, string comment)
{
var order = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
if (comment == "EFriday entry")
_entryOrder = order;
else
_exitOrder = order;
RegisterOrder(order);
return order;
}
private void ExitPosition(string comment)
{
if (Position == 0m || IsPending(_exitOrder))
return;
_exitOrder = Market(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), comment);
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order == _entryOrder && trade.Trade is not null)
{
_entryPrice = trade.Trade.Price;
_bestPrice = _entryPrice.Value;
_trailingLevel = null;
}
else if (Position == 0m)
{
_entryPrice = null;
_trailingLevel = null;
_bestPrice = 0m;
_exitOrder = null;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import DateTime, DayOfWeek, Decimal, TimeSpan
from StockSharp.Messages import DataType, CandleStates, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order
from StockSharp.Algo.Strategies import Strategy
class e_friday_session_strategy(Strategy):
"""One contrarian Friday trade based on the previous completed daily candle."""
def __init__(self):
super(e_friday_session_strategy, self).__init__()
self.Volume = Decimal(0.1)
self._hour_open = self.Param("HourOpen", 7).SetRange(0, 23)
self._hour_close = self.Param("HourClose", 19).SetRange(0, 23)
self._use_close_positions = self.Param("UseClosePositions", True)
self._stop_loss_points = self.Param("StopLossPoints", 75.0).SetNotNegative()
self._take_profit_points = self.Param("TakeProfitPoints", 0.0).SetNotNegative()
self._use_trailing = self.Param("UseTrailing", True)
self._profit_trailing = self.Param("ProfitTrailing", True)
self._trailing_stop_points = self.Param("TrailingStopPoints", 60.0).SetNotNegative()
self._trailing_step_points = self.Param("TrailingStepPoints", 5.0).SetNotNegative()
self._intraday_candle_type = self.Param(
"IntradayCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._daily_candle_type = self.Param(
"DailyCandleType", DataType.TimeFrame(TimeSpan.FromDays(1)))
self._clear_state()
def _clear_state(self):
self._last_daily_date = None
self._daily_open = 0.0
self._daily_close = 0.0
self._traded_date = None
self._entry_price = None
self._trailing_level = None
self._best_price = 0.0
self._entry_order = None
self._exit_order = None
def OnReseted(self):
super(e_friday_session_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self._intraday_candle_type.Value),
(self.Security, self._daily_candle_type.Value)]
def OnStarted2(self, time):
super(e_friday_session_strategy, self).OnStarted2(time)
self._clear_state()
if float(self.Security.PriceStep or 0) <= 0:
raise ValueError("E-Friday requires a positive PriceStep.")
self.SubscribeCandles(self._daily_candle_type.Value).Bind(self._process_daily).Start()
self.SubscribeCandles(self._intraday_candle_type.Value).Bind(self._process_intraday).Start()
def _process_daily(self, candle):
if candle.State != CandleStates.Finished:
return
self._last_daily_date = candle.OpenTime.Date
self._daily_open = float(candle.OpenPrice)
self._daily_close = float(candle.ClosePrice)
@staticmethod
def _pending(order):
return order is not None and order.State not in (OrderStates.Done, OrderStates.Failed)
def _process_intraday(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position != 0 and not self._pending(self._exit_order):
if (self._use_close_positions.Value and
candle.OpenTime.Hour >= self._hour_close.Value):
self._exit("EFriday session close")
return
if self._handle_risk(candle):
return
day = candle.OpenTime.Date
if (candle.OpenTime.DayOfWeek != DayOfWeek.Friday or
candle.OpenTime.Hour != self._hour_open.Value or
self._traded_date == day or
self._last_daily_date != day.AddDays(-1) or
self.Position != 0 or
self._pending(self._entry_order) or self._pending(self._exit_order)):
return
if self._daily_open == self._daily_close:
return
side = Sides.Buy if self._daily_close < self._daily_open else Sides.Sell
volume = float(self.Volume)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
if volume <= 0:
return
self._entry_order = self._market(side, volume, "EFriday entry")
self._traded_date = day
def _handle_risk(self, candle):
if self._entry_price is None:
return False
entry = self._entry_price
point = float(self.Security.PriceStep)
long_pos = self.Position > 0
stop = float(self._stop_loss_points.Value) * point
take = float(self._take_profit_points.Value) * point
if stop > 0 and (
float(candle.LowPrice) <= entry - stop if long_pos
else float(candle.HighPrice) >= entry + stop):
self._exit("EFriday stop loss")
return True
if take > 0 and (
float(candle.HighPrice) >= entry + take if long_pos
else float(candle.LowPrice) <= entry - take):
self._exit("EFriday take profit")
return True
old_stop = self._trailing_level
if old_stop is not None and (
float(candle.LowPrice) <= old_stop if long_pos
else float(candle.HighPrice) >= old_stop):
self._exit("EFriday trailing stop")
return True
if not self._use_trailing.Value or self._trailing_stop_points.Value <= 0:
return False
distance = float(self._trailing_stop_points.Value) * point
improvement = float(self._trailing_step_points.Value) * point
self._best_price = (max(self._best_price, float(candle.HighPrice)) if long_pos
else min(self._best_price, float(candle.LowPrice)))
profit = (self._best_price - entry if long_pos else entry - self._best_price)
if self._profit_trailing.Value and profit < distance:
return False
next_stop = (self._best_price - distance if long_pos else
self._best_price + distance)
if old_stop is None or (
next_stop >= old_stop + improvement and next_stop > old_stop if long_pos
else next_stop <= old_stop - improvement and next_stop < old_stop):
self._trailing_level = next_stop
return False
def _market(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
if comment == "EFriday entry":
self._entry_order = order
else:
self._exit_order = order
self.RegisterOrder(order)
return order
def _exit(self, comment):
if self.Position == 0 or self._pending(self._exit_order):
return
self._exit_order = self._market(
Sides.Sell if self.Position > 0 else Sides.Buy,
abs(float(self.Position)), comment)
def OnOwnTradeReceived(self, trade):
super(e_friday_session_strategy, self).OnOwnTradeReceived(trade)
if trade is not None and trade.Order == self._entry_order and trade.Trade is not None:
self._entry_price = float(trade.Trade.Price)
self._best_price = self._entry_price
self._trailing_level = None
elif self.Position == 0:
self._entry_price = None
self._trailing_level = None
self._best_price = 0.0
self._exit_order = None
def CreateClone(self):
return e_friday_session_strategy()