GitHub で見る

リスク管理 ATR 戦略

概要

リスク管理 ATR 戦略は、MetaTrader の 5 専門家 ATR のボラティリティ に基づくリスク管理 EA の StockSharp の変換です。オリジナルの EA は、口座残高と平均トゥルー レンジ (ATR) によって測定される現在の市場のボラティリティに応じてポジションを自動的に調整することに焦点を当てていました。 StockSharp ポートも同じ哲学を維持しています。10 期間の単純移動平均が 20 期間の単純移動平均を上回った場合にのみロング ポジションをオープンし、保護ストップでの潜在的な損失が設定されたリスク パーセンテージと一致するようにすべてのエントリー サイズが計算されます。

変換は高レベルの StockSharp API に従います。インジケーターの計算は、インジケーターを直接呼び出すのではなく、ローソク足のサブスクリプションにアタッチされた AverageTrueRange および SimpleMovingAverage コンポーネントに依存します。取引管理は StockSharp ヘルパー メソッドを再利用し、各約定後に保護ストップをキャンセルして再作成するため、ネット ポジションとストップ注文は常に一致します。

取引ロジック

  1. CandleType で定義されたタイムフレームを購読し、時期尚早な決定を避けるために完全に閉じたローソク足を待ちます。
  2. 14 期間の ATR と 2 つの単純移動平均 (長さ 10 と 20) にサブスクリプション データをフィードします。
  3. 高速移動平均が低速移動平均を上回って終了し、オープンポジションがない場合、選択したリスクモデルに基づいてポジションサイズを計算し、成行買い注文を送信します。
  4. 約定するたびに、ストップロス距離を計算します。ATR * AtrMultiplier、または UseAtrStopLoss が無効な場合は固定の価格ステップ数のいずれかです。
  5. ストップ価格を最も近いティックに切り捨て、現在のポジション サイズで SellStop 注文を出します。新しいストップが登録される前に、以前のストップはキャンセルされます。
  6. ストップ注文が実行され、ポジションがゼロに戻ると、ストラテジーは内部状態をクリアし、次のクロスオーバーに備えます。

リスク管理

  • RiskPercentage は、1 回の取引でポートフォリオ価値のどれだけが失われるかを決定します。この戦略は、Portfolio.CurrentValue (またはフォールバックとして BeginValue) を読み取り、それにパーセンテージを乗じて、許容される金銭的リスクを取得します。
  • 許容リスクをストップロス距離で割って取引量を求めます。出来高の四捨五入では、商品の出来高ステップ、最小および最大の制約が尊重されるため、生成された注文は取引所で有効なままになります。
  • RiskPercentage0 に設定されている場合、戦略は自動保護停止を維持しながら、デフォルトの Volume プロパティ (デフォルトでは 1 ロット) に戻ります。

パラメーター

名前 種類 デフォルト 説明
CandleType DataType 1分間の時間枠 戦略によって処理されたプライマリーキャンドルシリーズ。
AtrPeriod int 14 ATR インジケーターを滑らかにするために使用されるローソク足の数。
AtrMultiplier decimal 2.0 ストップロス距離を導出するために、ATR 値に適用される乗数。
RiskPercentage decimal 1.0 各取引でリスクが生じるポートフォリオ価値の割合。固定ボリュームを使用するには、ゼロに設定します。
UseAtrStopLoss bool true 有効にすると、ストップは ATR * AtrMultiplier に配置されます。それ以外の場合は、固定距離が使用されます。
FixedStopLossPoints int 50 ATR ベースのプレースメントが無効になるたびに、保護ストップに使用される価格ステップの数。

オリジナルの EA との違い

  • StockSharp はネット ポジションで動作するため、変換では成行買い注文のみが送信されます。出口は保護装置 SellStop を介して発生します。これにより、停止後に常にフラットになる EA の動作が再現されます。
  • MetaTrader は、ティック サイズの _Point 定数を公開します。金融商品がティック仕様を提供しない場合、ポートは Security.PriceStep をクエリし、単一の通貨単位にフォールバックします。
  • ポジションのサイジングは、StockSharp のボリューム フィルター (VolumeStepMinVolumeMaxVolume) を考慮して、生成された注文サイズをオーダーブックが確実に受け入れるようにします。
  • インジケーターの処理は、同期 iMA/iATR 呼び出しではなく、Subscription.Bind(...) を通じてイベント駆動型です。

使い方のヒント

  • 接続されたポートフォリオが正しい CurrentValue をレポートしていることを確認してください。そうしないと、リスクベースのポジションサイジングがゼロボリュームに戻ってしまいます。
  • Volume プロパティは引き続きセーフティ ネットとして機能します。 ATR の計算に関係なくロットサイズを固定したい場合は、戦略を開始する前に RiskPercentage をゼロに設定し、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;
	}
}