Открыть на GitHub

Стратегия E-Friday Session

Общее описание

Стратегия E-Friday повторяет классического эксперта MetaTrader, который торгует только по пятницам. Алгоритм анализирует предыдущую дневную свечу и в заданный час пятницы открывает позицию. Направление противоположно движению предыдущего дня: если свеча была медвежьей (закрылась ниже открытия) — совершается покупка, если свеча бычья — совершается продажа. Позиция ведётся внутри дня: её можно закрывать автоматически в заданный час либо по защитным ордерам.

Правила торговли

  1. Подписываемся на дневные свечи (по умолчанию 1 день), чтобы получить данные об открытии и закрытии предыдущей сессии.
  2. В пятницу отслеживаем внутридневные свечи (по умолчанию 1 минута), чтобы определить нужный час входа.
  3. С началом первой свечи выбранного часа:
    • Покупаем, если предыдущий день закрылся ниже открытия.
    • Продаём, если предыдущий день закрылся выше открытия.
    • Пропускаем сигнал, если свеча-дожи (открытие равно закрытию).
  4. При достижении заданного часа закрытия позиция может быть автоматически закрыта.
  5. Управление сделкой осуществляется через стоп-лосс, тейк-профит и опциональный трейлинг-стоп, полностью повторяющий логику оригинального эксперта (активация по прибыли и минимальный шаг подтягивания).

Особенности реализации

  • Используется высокоуровневый API StockSharp: отдельные подписки на дневные и внутридневные свечи.
  • Параметры риска в «пунктах» из MQL переводятся в абсолютные ценовые смещения с учётом минимального шага цены инструмента.
  • Трейлинг-стоп ведётся в коде: на каждой завершённой свече проверяются экстремумы и при необходимости позиция закрывается рыночной заявкой.
  • Ведётся учёт состояния, чтобы по пятницам открывалась не более одной позиции.
  • Поддерживаются длинные и короткие позиции; как и в оригинале, стратегия рассчитана на один инструмент на экземпляр.

Параметры

Имя Описание Значение по умолчанию
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.
  • Стратегия рассчитана на одну сделку в пятницу. Для торговли несколькими инструментами запустите отдельные экземпляры стратегии.

Отличия от оригинального эксперта

  • Решения принимаются по завершённым свечам, тогда как оригинал проверял условия на каждом тике.
  • Защитные ордера исполняются рыночными заявками при достижении уровней внутри свечи, поэтому возможны небольшие расхождения во времени выхода.
  • Все параметры оформлены через систему StrategyParam, что позволяет использовать оптимизацию и интеграцию с UI StockSharp.

Уточнение исполнения: вход проверяется на первой завершённой внутридневной свече с часом открытия HourOpen, а не непосредственно на первом тике часа. Обязательно наличие завершённой дневной свечи четверга. За пятницу допустима одна попытка входа, даже если позиция ранее закрыта защитой. Временной выход, стоп, тейк и трейлинг проверяются по экстремумам завершённых внутридневных свечей и исполняются рыночными заявками. Трейлинг переносится только после достижения шага; максимум текущей свечи не используется для фиктивного исполнения стопа в её прошлом.

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;
		}
	}
}