在 GitHub 上查看

风险管理 ATR 策略

概述

风险管理 ATR 策略是 MetaTrader 5 专家顾问 Risk Management EA Based on ATR Volatility 的 StockSharp 版本。原始 EA 的核心思想是根据账户余额和通过平均真实波幅 (ATR) 量化的市场波动率自动计算仓位规模。移植版本保持相同逻辑:当 10 周期简单移动平均线向上突破 20 周期简单移动平均线时开多仓,并让潜在亏损正好等于用户设定的风险百分比。

该策略完全使用 StockSharp 的高级 API。ATR 与 SMA 指标通过蜡烛订阅绑定获得数据,而不是直接调用 MetaTrader 函数。每次成交后都会取消并重新挂出保护性止损委托,从而确保净持仓和止损数量始终一致。

交易逻辑

  1. 订阅 CandleType 指定的时间框架,只处理收盘完成的蜡烛,避免过早下单。
  2. 对订阅数据分别计算 14 周期 ATR、10 周期 SMA 与 20 周期 SMA。
  3. 当快线 SMA 收于慢线之上且当前没有持仓时,按照风险模型计算下单量并发送市价买入单。
  4. 成交后根据 UseAtrStopLoss 选择止损模式:启用时止损距离为 ATR * AtrMultiplier;禁用时使用固定的价格步数。
  5. 将止损价向下取整到最近的最小跳动,并用当前仓位数量挂出 SellStop 保护性卖出止损单。之前的止损会在挂新单前被取消。
  6. 当止损被触发、仓位归零后,策略清空内部状态并等待下一次均线金叉。

风险管理

  • 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 的数量约束(VolumeStepMinVolumeMaxVolume),确保委托在交易所合法。
  • 指标处理通过 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;
	}
}