Стратегия 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: после каждого исполнения защитный стоп снимается и перевыставляется с актуальным объёмом, поэтому нет рассогласования между позицией и защитной заявкой.
Логика торговли
- Подписаться на свечи типа
CandleType и обрабатывать только завершённые (Finished) свечи.
- Рассчитывать 14-периодный ATR и две простые скользящие средние с периодами 10 и 20 на тех же свечах.
- Если быстрая SMA закрылась выше медленной и чистая позиция равна нулю, вычислить размер сделки с учётом выбранного риска и отправить рыночную заявку на покупку.
- После исполнения определить расстояние до стоп-лосса:
ATR * AtrMultiplier, если включён режим ATR, либо фиксированное число шагов цены при выключенном UseAtrStopLoss.
- Округлить цену стопа вниз до ближайшего шага и выставить
SellStop на текущий объём позиции, предварительно отменив прежний стоп.
- Когда защитный стоп срабатывает и позиция закрывается, стратегия очищает внутреннее состояние и ждёт следующего сигнала пересечения скользящих.
Управление рисками
- Параметр
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class risk_management_atr_strategy(Strategy):
"""ATR risk management with MA crossover, buy-only strategy with virtual stop-loss."""
def __init__(self):
super(risk_management_atr_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle type", "Primary timeframe processed by the strategy", "General")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR period", "Number of candles used to smooth the ATR volatility measure", "Indicator")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("ATR multiplier", "Distance multiplier applied to the ATR for stop-loss placement", "Risk")
self._use_atr_stop_loss = self.Param("UseAtrStopLoss", True) \
.SetDisplay("Use ATR stop", "Switch between ATR-based and fixed-distance stop-loss modes", "Risk")
self._fixed_stop_loss_points = self.Param("FixedStopLossPoints", 50) \
.SetGreaterThanZero() \
.SetDisplay("Fixed stop (points)", "Stop-loss distance expressed in price steps when ATR mode is disabled", "Risk")
self._fast_ma_period = self.Param("FastMaPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA period", "Length of the fast moving average used for signals", "Indicators")
self._slow_ma_period = self.Param("SlowMaPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA period", "Length of the slow moving average used for signals", "Indicators")
self._last_atr_value = None
self._price_step = 0.0
self._virtual_stop_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@property
def AtrMultiplier(self):
return self._atr_multiplier.Value
@property
def UseAtrStopLoss(self):
return self._use_atr_stop_loss.Value
@property
def FixedStopLossPoints(self):
return self._fixed_stop_loss_points.Value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
def OnReseted(self):
super(risk_management_atr_strategy, self).OnReseted()
self._last_atr_value = None
self._price_step = 0.0
self._virtual_stop_price = None
def OnStarted2(self, time):
super(risk_management_atr_strategy, self).OnStarted2(time)
self._price_step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
self._price_step = ps
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
fast_ma = SimpleMovingAverage()
fast_ma.Length = self.FastMaPeriod
slow_ma = SimpleMovingAverage()
slow_ma.Length = self.SlowMaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, fast_ma, slow_ma, self._process_candle).Start()
def _process_candle(self, candle, atr_value, fast_ma_value, slow_ma_value):
if candle.State != CandleStates.Finished:
return
atr_v = float(atr_value)
fast_v = float(fast_ma_value)
slow_v = float(slow_ma_value)
self._last_atr_value = atr_v
if self._virtual_stop_price is not None and self.Position > 0 and float(candle.LowPrice) <= self._virtual_stop_price:
self.SellMarket(abs(self.Position))
self._virtual_stop_price = None
return
if self.Position == 0:
self._virtual_stop_price = None
if fast_v <= slow_v:
return
if self.Position != 0:
return
self.BuyMarket()
stop_distance = self._calculate_stop_distance(atr_v)
if stop_distance > 0:
stop_price = float(candle.ClosePrice) - stop_distance
if stop_price > 0 and stop_price < float(candle.ClosePrice):
self._virtual_stop_price = stop_price
def _calculate_stop_distance(self, atr_value):
if self.UseAtrStopLoss:
if atr_value <= 0:
return 0.0
distance = atr_value * float(self.AtrMultiplier)
return distance if distance > 0 else 0.0
else:
steps = self.FixedStopLossPoints
if steps <= 0:
return 0.0
return steps * self._price_step
def CreateClone(self):
return risk_management_atr_strategy()