Открыть на GitHub

Стратегия Risk Management ATR

Обзор

Стратегия Risk Management ATR — это порт MetaTrader 5 эксперта Risk Management EA Based on ATR Volatility на платформу StockSharp. Оригинальный робот автоматически рассчитывал объём позиции исходя из баланса счёта и текущей волатильности, измеряемой индикатором Average True Range (ATR). В версии для StockSharp сохраняется тот же подход: стратегия открывает только длинные позиции при пересечении быстрой (10) и медленной (20) простых скользящих средних, причём объём каждой сделки подбирается так, чтобы возможный убыток по стоп-лоссу соответствовал заданному проценту риска.

Перенос выполнен с использованием высокоуровневого API StockSharp. Индикаторы подключены через SubscribeCandles(...).Bind(...), что исключает прямые вызовы iATR и iMA. Управление сделкой реализовано через штатные методы BuyMarket и SellStop: после каждого исполнения защитный стоп снимается и перевыставляется с актуальным объёмом, поэтому нет рассогласования между позицией и защитной заявкой.

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

  1. Подписаться на свечи типа CandleType и обрабатывать только завершённые (Finished) свечи.
  2. Рассчитывать 14-периодный ATR и две простые скользящие средние с периодами 10 и 20 на тех же свечах.
  3. Если быстрая SMA закрылась выше медленной и чистая позиция равна нулю, вычислить размер сделки с учётом выбранного риска и отправить рыночную заявку на покупку.
  4. После исполнения определить расстояние до стоп-лосса: ATR * AtrMultiplier, если включён режим ATR, либо фиксированное число шагов цены при выключенном UseAtrStopLoss.
  5. Округлить цену стопа вниз до ближайшего шага и выставить SellStop на текущий объём позиции, предварительно отменив прежний стоп.
  6. Когда защитный стоп срабатывает и позиция закрывается, стратегия очищает внутреннее состояние и ждёт следующего сигнала пересечения скользящих.

Управление рисками

  • Параметр RiskPercentage задаёт долю стоимости портфеля, которую можно потерять в одной сделке. Стратегия берёт Portfolio.CurrentValue (или BeginValue в качестве резервного варианта) и умножает на процент риска, чтобы получить допустимую сумму убытка.
  • Допустимый убыток делится на расстояние до стопа — так получается объём заявки. Затем объём приводится к сетке торгового инструмента с учётом VolumeStep, MinVolume и MaxVolume.
  • Если RiskPercentage равен нулю, стратегия использует значение Volume (по умолчанию 1 лот), но защитный стоп всё равно создаётся автоматически.

Параметры

Имя Тип Значение по умолчанию Описание
CandleType DataType Таймфрейм 1 минута Основная серия свечей, используемая стратегией.
AtrPeriod int 14 Количество свечей в расчёте ATR.
AtrMultiplier decimal 2.0 Множитель ATR для определения стоп-лосса.
RiskPercentage decimal 1.0 Процент капитала, находящийся под риском в каждой сделке. 0 — фиксированный объём.
UseAtrStopLoss bool true Включает режим стоп-лосса, зависящего от ATR.
FixedStopLossPoints int 50 Количество шагов цены для фиксированного стопа, если ATR-режим отключён.

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

  • В StockSharp используется нетто-позиция, поэтому стратегия отправляет только рыночные покупки, а выход из позиции осуществляется через защитный SellStop, что соответствует поведению MT5-версии после срабатывания стопа.
  • Константа _Point, присутствующая в MetaTrader, заменена на Security.PriceStep. Если шаг цены недоступен, используется значение 1m как резерв.
  • Расчёт объёма учитывает торговые ограничения площадки (VolumeStep, MinVolume, MaxVolume), чтобы избежать отклонённых заявок.
  • Обработка индикаторов выполняется событийно через механизм Bind, а не синхронными запросами к индикаторам, как в MQL.

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

  • Убедитесь, что подключённый портфель возвращает корректное значение CurrentValue; без него риск-менеджмент не сможет посчитать объём и не будет торговать.
  • Чтобы торговать фиксированным объёмом, установите RiskPercentage в ноль и заранее задайте желаемое Volume.
  • Добавьте стратегию на график, чтобы видеть свечи, обе скользящие и исполненные сделки — так проще проверить корректность сигналов и размещения стопов.
  • На более волатильных инструментах увеличьте AtrMultiplier или переключитесь на фиксированный стоп через FixedStopLossPoints, чтобы адаптировать стратегию к рыночным условиям.

Индикаторы

  • AverageTrueRange с периодом AtrPeriod.
  • SimpleMovingAverage с периодом 10 (быстрая линия).
  • SimpleMovingAverage с периодом 20 (медленная линия).
namespace StockSharp.Samples.Strategies;

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;

public class RiskManagementAtrStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<bool> _useAtrStopLoss;
	private readonly StrategyParam<int> _fixedStopLossPoints;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;

	private AverageTrueRange _atr;
	private SimpleMovingAverage _fastMovingAverage;
	private SimpleMovingAverage _slowMovingAverage;

	private decimal? _lastAtrValue;
	private Order _stopLossOrder;
	private decimal _priceStep;
	private decimal? _virtualStopPrice;

	public RiskManagementAtrStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe processed by the strategy.", "General");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR period", "Number of candles used to smooth the ATR volatility measure.", "Indicator");

		_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("ATR multiplier", "Distance multiplier applied to the ATR for stop-loss placement.", "Risk");

		_riskPercentage = Param(nameof(RiskPercentage), 1m)
			.SetNotNegative()
			.SetDisplay("Risk %", "Percentage of portfolio value risked on every trade.", "Risk");

		_useAtrStopLoss = Param(nameof(UseAtrStopLoss), true)
			.SetDisplay("Use ATR stop", "Switch between ATR-based and fixed-distance stop-loss modes.", "Risk");

		_fixedStopLossPoints = Param(nameof(FixedStopLossPoints), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fixed stop (points)", "Stop-loss distance expressed in price steps when ATR mode is disabled.", "Risk");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA period", "Length of the fast moving average used for signals.", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA period", "Length of the slow moving average used for signals.", "Indicators");
	}

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

	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public bool UseAtrStopLoss
	{
		get => _useAtrStopLoss.Value;
		set => _useAtrStopLoss.Value = value;
	}

	public int FixedStopLossPoints
	{
		get => _fixedStopLossPoints.Value;
		set => _fixedStopLossPoints.Value = value;
	}

	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

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

		_atr = null;
		_fastMovingAverage = null;
		_slowMovingAverage = null;
		_lastAtrValue = null;
		_stopLossOrder = null;
		_priceStep = 0m;
		_virtualStopPrice = null;
	}

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

		Volume = Volume > 0m ? Volume : 1m; // Provide a default lot size when no risk-based sizing is used

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
			_priceStep = 1m; // Fallback to a single currency unit when the instrument does not expose a price step

		_atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

		_fastMovingAverage = new SimpleMovingAverage
		{
			Length = FastMaPeriod
		};

		_slowMovingAverage = new SimpleMovingAverage
		{
			Length = SlowMaPeriod
		};

		_lastAtrValue = null;
		CancelStopLossOrder();

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_atr, _fastMovingAverage, _slowMovingAverage, ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _atr);
			DrawIndicator(area, _fastMovingAverage);
			DrawIndicator(area, _slowMovingAverage);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal atrValue, decimal fastMaValue, decimal slowMaValue)
	{
		if (candle.State != CandleStates.Finished)
			return; // Work exclusively with closed candles to avoid premature entries

		_lastAtrValue = atrValue;

		// Check virtual stop-loss
		if (_virtualStopPrice.HasValue && Position > 0m && candle.LowPrice <= _virtualStopPrice.Value)
		{
			SellMarket(Math.Abs(Position));
			_virtualStopPrice = null;
			return;
		}

		if (Position == 0m)
			_virtualStopPrice = null;

		if (_atr == null || _fastMovingAverage == null || _slowMovingAverage == null)
			return;

		if (!_atr.IsFormed || !_fastMovingAverage.IsFormed || !_slowMovingAverage.IsFormed)
			return; // Ensure all indicators accumulated enough history

		if (fastMaValue <= slowMaValue)
			return; // The simple moving average crossover only buys when the fast average is above the slow one

		if (Position != 0m)
			return; // Mimic the MetaTrader expert: enter only when there is no open position

		var volume = CalculateOrderVolume(atrValue);
		if (volume <= 0m)
			return;

		CancelStopLossOrder();

		BuyMarket(volume);
	}

	private decimal CalculateOrderVolume(decimal atrValue)
	{
		var volume = Volume > 0m ? Volume : 0m;

		var stopDistance = CalculateStopDistance(atrValue);
		if (stopDistance <= 0m)
			return 0m; // Skip trading when the stop distance cannot be computed

		var riskPercent = RiskPercentage;
		if (riskPercent > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (portfolioValue <= 0m)
				return 0m; // Unable to size the trade without a portfolio valuation

			var riskAmount = portfolioValue * riskPercent / 100m;
			if (riskAmount <= 0m)
				return 0m;

			volume = riskAmount / stopDistance;
		}

		volume = RoundVolume(volume);
		volume = ClampVolume(volume);

		return volume > 0m ? volume : 0m;
	}

	private decimal CalculateStopDistance(decimal atrValue)
	{
		if (UseAtrStopLoss)
		{
			if (atrValue <= 0m)
				return 0m;

			var distance = atrValue * AtrMultiplier;
			return distance > 0m ? distance : 0m;
		}

		var steps = FixedStopLossPoints;
		if (steps <= 0)
			return 0m;

		return steps * _priceStep;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
				return step; // Use the minimum tradable lot when the calculated volume is below one step

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private decimal ClampVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
			volume = minVolume.Value;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}

	private decimal AdjustPrice(decimal price)
	{
		if (price <= 0m)
			return 0m;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return Math.Round(price, 4, MidpointRounding.AwayFromZero);

		var steps = Math.Floor(price / step);
		if (steps <= 0m)
			return step; // Never place protective stops at non-positive prices

		return steps * step;
	}

	private void CancelStopLossOrder()
	{
		if (_stopLossOrder == null)
			return;

		if (_stopLossOrder.State == OrderStates.Active)
			CancelOrder(_stopLossOrder);

		_stopLossOrder = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order.Security != Security)
			return;

		if (Position <= 0m)
			CancelStopLossOrder();

		if (trade.Order.Side != Sides.Buy)
			return; // The expert only opens long trades; sell trades come from stop-loss execution

		var atrValue = _lastAtrValue ?? 0m;
		var stopDistance = CalculateStopDistance(atrValue);
		if (stopDistance <= 0m)
			return;

		var stopPrice = trade.Trade.Price - stopDistance;
		stopPrice = AdjustPrice(stopPrice);

		if (stopPrice <= 0m || stopPrice >= trade.Trade.Price)
			return; // Do not place invalid protective stops

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		CancelStopLossOrder();

		// Use virtual stop-loss instead of SellStop order
		_virtualStopPrice = stopPrice;
	}
}