在 GitHub 上查看

E-Friday 会话策略

概述

E-Friday 策略复刻了经典的 MetaTrader 专家顾问,它只在星期五交易。策略观察上一日的日线蜡烛,并在设定的时间于星期五开盘时入场。方向采取反向思路:若上一日收于开盘价下方(阴线),则在星期五做多;若上一日收于开盘价上方(阳线),则在星期五做空。仓位仅在当日管理,可在预设时间后自动平仓,也可通过保护性止损退出。

交易规则

  1. 订阅日线蜡烛(默认:1 天)以获得前一日的开盘价和收盘价。
  2. 在星期五订阅日内蜡烛(默认:1 分钟)以检测指定的入场小时。
  3. 当出现入场小时的第一根蜡烛时:
    • 若上一日为阴线则做多。
    • 若上一日为阳线则做空。
    • 若上一日开收相等则放弃交易。
  4. 到达设定的平仓小时后,可选择自动平掉当日仓位。
  5. 使用止损、止盈以及可选的跟踪止损来管理仓位,跟踪逻辑包含原始 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;
		}
	}
}