风险管理 ATR 策略
概述
风险管理 ATR 策略是 MetaTrader 5 专家顾问 Risk Management EA Based on ATR Volatility 的 StockSharp 版本。原始 EA 的核心思想是根据账户余额和通过平均真实波幅 (ATR) 量化的市场波动率自动计算仓位规模。移植版本保持相同逻辑:当 10 周期简单移动平均线向上突破 20 周期简单移动平均线时开多仓,并让潜在亏损正好等于用户设定的风险百分比。
该策略完全使用 StockSharp 的高级 API。ATR 与 SMA 指标通过蜡烛订阅绑定获得数据,而不是直接调用 MetaTrader 函数。每次成交后都会取消并重新挂出保护性止损委托,从而确保净持仓和止损数量始终一致。
交易逻辑
- 订阅
CandleType指定的时间框架,只处理收盘完成的蜡烛,避免过早下单。 - 对订阅数据分别计算 14 周期 ATR、10 周期 SMA 与 20 周期 SMA。
- 当快线 SMA 收于慢线之上且当前没有持仓时,按照风险模型计算下单量并发送市价买入单。
- 成交后根据
UseAtrStopLoss选择止损模式:启用时止损距离为ATR * AtrMultiplier;禁用时使用固定的价格步数。 - 将止损价向下取整到最近的最小跳动,并用当前仓位数量挂出
SellStop保护性卖出止损单。之前的止损会在挂新单前被取消。 - 当止损被触发、仓位归零后,策略清空内部状态并等待下一次均线金叉。
风险管理
RiskPercentage决定每笔交易可承受的最大亏损。策略读取投资组合的Portfolio.CurrentValue(无法获取时退回BeginValue),再乘以风险百分比得到允许亏损金额。- 允许亏损金额除以止损距离得到下单数量。数量会按照交易品种的手数步长、最小/最大交易量自动调整,保证委托有效。
- 当
RiskPercentage设为0时,策略改用固定手数(默认为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 模式时的固定价格步数。 |
与原始 EA 的差异
- StockSharp 使用净头寸模型,因此移植版本只发送市价买单,离场完全依赖保护性
SellStop,结果与原 EA 在止损后持仓归零一致。 - MetaTrader 通过
_Point提供最小跳动。移植版改为读取Security.PriceStep,若缺失则退回到 1 个价格单位。 - 仓位规模计算会遵循 StockSharp 的数量约束(
VolumeStep、MinVolume、MaxVolume),确保委托在交易所合法。 - 指标处理通过
Subscription.Bind(...)的事件机制实现,而非同步调用iMA/iATR。
使用建议
- 请确认连接的投资组合能正确返回
CurrentValue,否则风险模型可能因为无法评估账户价值而不下单。 - 如果希望始终按固定手数交易,可将
RiskPercentage设为 0,并在启动前调整Volume。 - 建议把策略加载到图表,方便同时查看蜡烛、两条移动平均线以及成交记录,以验证入场和止损逻辑。
- 对波动性较高的品种,可以提高
AtrMultiplier以扩大止损距离;或关闭 ATR 止损并通过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()