Открыть на GitHub

Стратегия ATR Step Trader

Обзор

ATR Step Trader — это порт советника MetaTrader5 atrTrader.mq5. Стратегия сочетает фильтрацию тренда по двум скользящим средним и работу с диапазонами, построенными на Average True Range (ATR). Реализация для StockSharp повторяет событийную модель исходного советника: обрабатываются только закрытые свечи, тренд должен подтверждаться серией из MomentumPeriod баров, а все расстояния выражаются в кратных величинах ATR, чтобы адаптироваться к волатильности инструмента.

Индикаторы и данные

  • Простые скользящие средние (SMA). Быстрая и медленная SMA (FastPeriod и SlowPeriod) формируют основной тренд-фильтр. Обе рассчитываются по свечам выбранного таймфрейма.
  • Average True Range. Индикатор AverageTrueRange с периодом AtrPeriod переводит волатильность в ценовые расстояния. Входы, пирамидинг и стопы строятся именно на этих расстояниях.
  • Каналы максимумов/минимумов. Индикаторы Highest и Lowest отслеживают экстремумы последних MomentumPeriod свечей и заменяют вызовы iHighest/iLowest из MQL.
  • Таймфрейм. По умолчанию используется часовое окно (TimeSpan.FromHours(1)), что соответствует режиму PERIOD_CURRENT исходного эксперта. Параметр CandleType позволяет выбрать иной интервал.

Логика входа

  1. Дождаться закрытия свечи. Незавершённые бары игнорируются, как и в исходном OnTick с проверкой iTime.
  2. Обновить счётчики серии. Быстрый сигнал увеличивает бычью серию, когда быстрая SMA выше медленной; медвежья серия растёт, когда быстрая SMA ниже медленной. При равенстве значения противоположный счётчик сбрасывается.
  3. Когда бычья серия достигает MomentumPeriod, проверить, остаётся ли цена закрытия ниже последнего максимума минимум на StepMultiplier * ATR. Если да — открыть длинную позицию по рынку.
  4. Когда медвежья серия достигает MomentumPeriod, проверить, остаётся ли цена закрытия выше последнего минимума минимум на StepMultiplier * ATR. Если да — открыть короткую позицию по рынку.
  5. Первый вход инициализирует вспомогательные переменные: стратегия запоминает минимальную и максимальную цену входа для текущего направления и сразу выставляет волатильностный стоп (StepMultiplier * StopMultiplier * ATR). Эти значения используются при дальнейшем пирамидинге.

Управление позицией

  • Пирамидинг. Пока количество сделок меньше PyramidLimit, добавляется ещё один лот при отклонении цены на ± StepsMultiplier * ATR от сохранённых экстремумов. Это копирует логику параметра Steps в исходнике и работает как в сторону прибыли, так и против тренда.
  • Стопы. Начальный стоп устанавливается на расстоянии StepMultiplier * StopMultiplier * ATR от цены входа. Когда ступеней набрано максимум, стоп подтягивается к цене закрытия на расстояние StepMultiplier * ATR, что имитирует обновление стопов в MQL при трёх позициях.
  • Выход при неблагоприятном движении. Если цена пробивает край пирамиды на StepsMultiplier * ATR, стратегия немедленно закрывает все позиции данного направления рыночным ордером.
  • Сброс состояния. После полного выхода серия счётчиков и стоповые уровни обнуляются — для повторного входа нужно новое трендовое подтверждение.

Параметры

Группа Параметр Описание Значение по умолчанию
Trend Filter FastPeriod Период быстрой SMA. 70
Trend Filter SlowPeriod Период медленной SMA. 180
Trend Filter MomentumPeriod Количество последовательных свечей для подтверждения тренда. 50
Volatility AtrPeriod Окно ATR для расчёта расстояний. 100
Entry Logic StepMultiplier Кратность ATR для первичного пробоя. 4
Entry Logic StepsMultiplier Кратность ATR между слоями пирамиды. 2
Risk Management StopMultiplier Дополнительный множитель для первоначального стопа. 3
Position Sizing PyramidLimit Максимальное число входов в одном направлении. 3
Trading TradeVolume Объём рыночного ордера. 1
General CandleType Используемый тип свечей (таймфрейм). TimeFrame(1h)

Практические замечания

  • Размер позиции контролируется через свойство Volume стратегии. Перед запуском установите TradeVolume в соответствии с лотом вашего инструмента.
  • В примере используются рыночные заявки, аналогичные вызовам CTrade.Buy/Sell в MT5. При низкой ликвидности можно заменить их на лимитные или стоповые ордера.
  • Переменные экстремумов повторяют логику h_price и l_price исходника и необходимы для решения, когда добавлять или закрывать ступени пирамиды.
  • В оригинале стопы задавались на уровне каждой позиции. В портированной версии стоп контролируется на уровне всей стратегии, что приводит к одновременному закрытию всех слоёв и упрощает работу с заявками.
  • Перед реальной торговлей протестируйте стратегию в симуляторе. Несмотря на адаптацию к волатильности через ATR, гэпы и проскальзывание могут увеличить фактический риск.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-step ATR trend strategy converted from the "atrTrader" MQL5 expert advisor.
/// Filters trends with a dual moving-average stack, opens breakouts, and pyramids positions using ATR distances.
/// </summary>
public class AtrStepTraderStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<int> _momentumPeriod;
	private readonly StrategyParam<int> _pyramidLimit;
	private readonly StrategyParam<decimal> _stepMultiplier;
	private readonly StrategyParam<decimal> _stepsMultiplier;
	private readonly StrategyParam<decimal> _stopMultiplier;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private int _bullishStreak;
	private int _bearishStreak;
	private decimal? _previousSlow;
	private decimal? _longEntryHigh;
	private decimal? _longEntryLow;
	private decimal? _shortEntryHigh;
	private decimal? _shortEntryLow;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;

	/// <summary>
	/// Fast moving average length.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average length.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// ATR calculation period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Number of consecutive bars that must confirm the trend direction.
	/// </summary>
	public int MomentumPeriod
	{
		get => _momentumPeriod.Value;
		set => _momentumPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of stacked entries per direction.
	/// </summary>
	public int PyramidLimit
	{
		get => _pyramidLimit.Value;
		set => _pyramidLimit.Value = value;
	}

	/// <summary>
	/// ATR multiple used for breakout gating.
	/// </summary>
	public decimal StepMultiplier
	{
		get => _stepMultiplier.Value;
		set => _stepMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiple used for pyramiding distance checks.
	/// </summary>
	public decimal StepsMultiplier
	{
		get => _stepsMultiplier.Value;
		set => _stepsMultiplier.Value = value;
	}

	/// <summary>
	/// Additional multiplier that widens the protective stop distance.
	/// </summary>
	public decimal StopMultiplier
	{
		get => _stopMultiplier.Value;
		set => _stopMultiplier.Value = value;
	}

	/// <summary>
	/// Base order volume for market entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="AtrStepTraderStrategy"/>.
	/// </summary>
	public AtrStepTraderStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 70)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Length of the fast moving average", "Trend Filter")
			
			.SetOptimize(50, 100, 10);

		_slowPeriod = Param(nameof(SlowPeriod), 180)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Length of the slow moving average", "Trend Filter")
			
			.SetOptimize(120, 240, 20);

		_atrPeriod = Param(nameof(AtrPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR window used for distance calculations", "Volatility")
			
			.SetOptimize(50, 150, 10);

		_momentumPeriod = Param(nameof(MomentumPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Bars", "Number of consecutive bars required for trend confirmation", "Trend Filter")
			
			.SetOptimize(30, 80, 5);

		_pyramidLimit = Param(nameof(PyramidLimit), 3)
			.SetGreaterThanZero()
			.SetDisplay("Pyramid Limit", "Maximum number of entries per direction", "Position Sizing")
			
			.SetOptimize(2, 4, 1);

		_stepMultiplier = Param(nameof(StepMultiplier), 4m)
			.SetGreaterThanZero()
			.SetDisplay("Step Multiplier", "ATR multiple for breakout validation", "Entry Logic")
			
			.SetOptimize(2m, 6m, 1m);

		_stepsMultiplier = Param(nameof(StepsMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Steps Multiplier", "ATR multiple for add-on spacing", "Entry Logic")
			
			.SetOptimize(1m, 3m, 0.5m);

		_stopMultiplier = Param(nameof(StopMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Multiplier", "Extra multiplier applied on top of the step distance", "Risk Management")
			
			.SetOptimize(2m, 4m, 0.5m);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Base order size for market entries", "Trading")
			
			.SetOptimize(0.5m, 2m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for processing", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_bullishStreak = 0;
		_bearishStreak = 0;
		_previousSlow = null;
		_longEntryHigh = null;
		_longEntryLow = null;
		_shortEntryHigh = null;
		_shortEntryLow = null;
		_longStopPrice = null;
		_shortStopPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;

		var fastMa = new SimpleMovingAverage { Length = FastPeriod };
		var slowMa = new SimpleMovingAverage { Length = SlowPeriod };
		var atr = new AverageTrueRange { Length = AtrPeriod };
		var highest = new Highest { Length = MomentumPeriod };
		var lowest = new Lowest { Length = MomentumPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastMa, slowMa, atr, highest, lowest, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, fastMa);
			DrawIndicator(area, slowMa);
			DrawIndicator(area, atr);
			DrawIndicator(area, highest);
			DrawIndicator(area, lowest);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal atrValue, decimal highest, decimal lowest)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (atrValue <= 0m)
			return;

		UpdateMomentumCounters(fastValue, slowValue);

		var price = candle.ClosePrice;
		var previousSlow = _previousSlow;
		_previousSlow = slowValue;

		var volume = Volume;
		if (volume <= 0m)
			volume = 1m;

		var netPosition = Position;
		var longCount = netPosition > 0m ? (int)Math.Round(netPosition / volume, MidpointRounding.AwayFromZero) : 0;
		var shortCount = netPosition < 0m ? (int)Math.Round(-netPosition / volume, MidpointRounding.AwayFromZero) : 0;

		if (longCount == 0 && shortCount == 0)
		{
			if (previousSlow.HasValue && slowValue > 0m)
			{
				var bullishReady = _bullishStreak >= MomentumPeriod && price > previousSlow.Value;
				if (bullishReady)
				{
					BuyMarket(Volume);
					longCount = 1;
					_longEntryHigh = price;
					_longEntryLow = price;
					_longStopPrice = price - StepMultiplier * StopMultiplier * atrValue;
				}
			}

			if (longCount == 0 && previousSlow.HasValue && slowValue > 0m)
			{
				var bearishReady = _bearishStreak >= MomentumPeriod && price < previousSlow.Value;
				if (bearishReady)
				{
					SellMarket(Volume);
					shortCount = 1;
					_shortEntryHigh = price;
					_shortEntryLow = price;
					_shortStopPrice = price + StepMultiplier * StopMultiplier * atrValue;
				}
			}
		}
		else if (longCount > 0 && shortCount == 0)
		{
			ManageLongPosition(ref longCount, price, atrValue);
		}
		else if (shortCount > 0 && longCount == 0)
		{
			ManageShortPosition(ref shortCount, price, atrValue);
		}
	}

	private void UpdateMomentumCounters(decimal fastValue, decimal slowValue)
	{
		if (fastValue > slowValue)
		{
			_bullishStreak++;
			_bearishStreak = 0;
		}
		else if (fastValue < slowValue)
		{
			_bearishStreak++;
			_bullishStreak = 0;
		}
		else
		{
			_bullishStreak++;
			_bearishStreak++;
		}
	}

	private void ManageLongPosition(ref int longCount, decimal price, decimal atrValue)
	{
		if (_longEntryHigh is not decimal high || _longEntryLow is not decimal low)
			return;

		var stepsDistance = StepsMultiplier * atrValue;
		var stepDistance = StepMultiplier * atrValue;

		if (_longStopPrice.HasValue && price <= _longStopPrice.Value)
		{
			SellMarket(Position);
			longCount = 0;
			ResetLongState();
			return;
		}

		if (longCount < PyramidLimit)
		{
			if (price >= high + stepsDistance || price <= low - stepsDistance)
			{
				BuyMarket(Volume);
				longCount++;
				_longEntryHigh = Math.Max(high, price);
				_longEntryLow = Math.Min(low, price);
				UpdateLongStopAfterEntry(price, atrValue);
				return;
			}
		}

		if (price <= low - stepsDistance)
		{
			SellMarket(Position);
			longCount = 0;
			ResetLongState();
			return;
		}

		if (longCount >= PyramidLimit)
		{
			var tightened = price - stepDistance;
			if (!_longStopPrice.HasValue || tightened > _longStopPrice.Value)
				_longStopPrice = tightened;
		}
	}

	private void ManageShortPosition(ref int shortCount, decimal price, decimal atrValue)
	{
		if (_shortEntryHigh is not decimal high || _shortEntryLow is not decimal low)
			return;

		var stepsDistance = StepsMultiplier * atrValue;
		var stepDistance = StepMultiplier * atrValue;

		if (_shortStopPrice.HasValue && price >= _shortStopPrice.Value)
		{
			BuyMarket(Math.Abs(Position));
			shortCount = 0;
			ResetShortState();
			return;
		}

		if (shortCount < PyramidLimit)
		{
			if (price <= low - stepsDistance || price >= high + stepsDistance)
			{
				SellMarket(Volume);
				shortCount++;
				_shortEntryHigh = Math.Max(high, price);
				_shortEntryLow = Math.Min(low, price);
				UpdateShortStopAfterEntry(price, atrValue);
				return;
			}
		}

		if (price >= high + stepsDistance)
		{
			BuyMarket(Math.Abs(Position));
			shortCount = 0;
			ResetShortState();
			return;
		}

		if (shortCount >= PyramidLimit)
		{
			var tightened = price + stepDistance;
			if (!_shortStopPrice.HasValue || tightened < _shortStopPrice.Value)
				_shortStopPrice = tightened;
		}
	}

	private void UpdateLongStopAfterEntry(decimal entryPrice, decimal atrValue)
	{
		var stop = entryPrice - StepMultiplier * StopMultiplier * atrValue;
		if (!_longStopPrice.HasValue || stop > _longStopPrice.Value)
			_longStopPrice = stop;
	}

	private void UpdateShortStopAfterEntry(decimal entryPrice, decimal atrValue)
	{
		var stop = entryPrice + StepMultiplier * StopMultiplier * atrValue;
		if (!_shortStopPrice.HasValue || stop < _shortStopPrice.Value)
			_shortStopPrice = stop;
	}

	private void ResetLongState()
	{
		_longEntryHigh = null;
		_longEntryLow = null;
		_longStopPrice = null;
		_bullishStreak = 0;
	}

	private void ResetShortState()
	{
		_shortEntryHigh = null;
		_shortEntryLow = null;
		_shortStopPrice = null;
		_bearishStreak = 0;
	}
}