Открыть на GitHub

Стратегия «Two EMA Intraday Filter»

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

Стратегия повторяет логику советника MetaTrader Expert_2EMA_ITF, но реализована на высокоуровневом API StockSharp. В основе лежит пересечение двух экспоненциальных скользящих средних (EMA) и использование индикатора ATR для расчёта уровней входа, стоп-лосса и тейк-профита. Дополнительно встроен внутридневной фильтр, позволяющий исключать отдельные минуты, часы или дни недели.

Логика торговли

  • Рассчитываются значения быстрой и медленной EMA на выбранных свечах.
  • Фиксируется бычий сигнал, когда быстрая EMA пересекает медленную снизу вверх, и медвежий сигнал при обратном пересечении.
  • При бычьем сигнале выставляется отложенный Buy Limit со смещением LimitMultiplier * ATR от медленной EMA (при наличии котировок добавляется текущий спрэд). При медвежьем сигнале выставляется Sell Limit со смещением в противоположную сторону.
  • Для каждого сигнала сохраняются рассчитанные уровни стоп-лосса и тейк-профита, чтобы отправить защитные заявки сразу после исполнения входа.
  • Если отложенная заявка не исполняется за ExpirationBars свечей, она снимается автоматически.
  • Все сигналы проходят через фильтр времени: разрешённые минуты/часы/дни и битовые маски запрещённых периодов.

Используемые индикаторы

  • Fast EMA — быстрый отклик на изменение цены.
  • Slow EMA — определяет направление тренда.
  • Average True Range — оценивает волатильность и масштабирует все ценовые смещения.

Параметры

Параметр Описание Значение по умолчанию
CandleType Тип/таймфрейм свечей для расчётов. Свечи по 30 минут
FastEmaPeriod Период быстрой EMA. 5
SlowEmaPeriod Период медленной EMA (должен быть больше быстрого). 30
AtrPeriod Период ATR. 7
LimitMultiplier Множитель ATR для смещения цены входа. 1.2
StopLossMultiplier Множитель ATR для стоп-лосса. 5
TakeProfitMultiplier Множитель ATR для тейк-профита. 8
ExpirationBars Количество свечей до отмены отложенной заявки. 4
GoodMinuteOfHour Разрешённая минута часа (-1 отключает фильтр). -1
BadMinutesMask Битовая маска запрещённых минут (бит n = 1 — запретить минуту n). 0
GoodHourOfDay Разрешённый час (-1 отключает фильтр). -1
BadHoursMask Битовая маска запрещённых часов. 0
GoodDayOfWeek Разрешённый день недели (-1 отключает фильтр, 0 = воскресенье). -1
BadDaysMask Битовая маска запрещённых дней недели (0 = воскресенье). 0

Управление заявками

  1. Вход — лимитные заявки рассчитываются от медленной EMA с учётом ATR; для покупок добавляется спрэд, если доступны лучшие котировки.
  2. Просрочка — при создании заявки запоминается номер текущей свечи; когда число свечей превысит ExpirationBars, заявка снимается.
  3. Защита — после исполнения входа старые стоп/тейк отменяются и выставляются новые уровни, вычисленные по ATR на момент сигнала. Когда позиция обнуляется, защитные заявки отменяются.

Временной фильтр

  • Единичные разрешения — параметры GoodMinuteOfHour, GoodHourOfDay, GoodDayOfWeek ограничивают торговлю конкретной минутой, часом или днём недели.
  • Битовые маски — параметры BadMinutesMask, BadHoursMask, BadDaysMask позволяют отключить набор минут, часов или дней (например, BadMinutesMask = (1 << 0) | (1 << 30) запрещает 0-ю и 30-ю минуты каждого часа).
  • Совместное применение — сигнал выполняется только если текущее время удовлетворяет разрешённым значениям и не попадает под маски.

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

  • Применяются высокоуровневые методы StockSharp (BuyLimit, SellLimit, SellStop, BuyStop), а не объекты MetaTrader Expert.
  • Компенсация спрэда для покупок берёт значения Security.BestBid/Security.BestAsk; при отсутствии котировок добавка равна нулю.
  • Логика временного фильтра реализована вручную через битовые маски вместо класса CSignalITF.
  • После исполнения входа защитные заявки выставляются немедленно, используя заранее сохранённые уровни стопа и тейка.

Рекомендации по использованию

  • Перед запуском стратегии задайте объём Volume, иначе будет зафиксировано предупреждение и заявки не отправятся.
  • Основные параметры отмечены как пригодные для оптимизации, что облегчает подбор значений.
  • Для фильтра времени используется момент закрытия свечи.
using System;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Two EMA Intraday Filter strategy: EMA crossover.
/// Buys when fast EMA crosses above slow EMA. Sells on cross below.
/// </summary>
public class TwoEmaIntradayFilterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	public TwoEmaIntradayFilterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");

		_fastPeriod = Param(nameof(FastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast EMA period", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 30)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow EMA period", "Indicators");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var fast = new ExponentialMovingAverage { Length = FastPeriod };
		var slow = new ExponentialMovingAverage { Length = SlowPeriod };

		decimal? prevFast = null;
		decimal? prevSlow = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fast, slow, (candle, fastVal, slowVal) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!IsFormedAndOnlineAndAllowTrading())
					return;

				if (prevFast.HasValue && prevSlow.HasValue)
				{
					if (prevFast.Value <= prevSlow.Value && fastVal > slowVal && Position <= 0)
						BuyMarket();
					else if (prevFast.Value >= prevSlow.Value && fastVal < slowVal && Position >= 0)
						SellMarket();
				}

				prevFast = fastVal;
				prevSlow = slowVal;
			})
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fast);
			DrawIndicator(area, slow);
			DrawOwnTrades(area);
		}
	}
}