GitHub で見る

MT4戦略における残高ドローダウン

この戦略は、元の MetaTrader 4 エキスパート アドバイザー BalanceDrawdownInMT4 を StockSharp の高レベル API に移植します。 EA はただちに単一のロングポジションをオープンし、セッション開始後に到達したピーク残高と比較して口座ドローダウンを継続的に測定します。

取引ロジック

  1. 戦略が開始されると、価格ポイントで表される MQL 入力を模倣する管理されたストップロスとテイクプロフィットのレベルを設定するために StartProtection を呼び出します。
  2. 最初に終了したローソク足 (デフォルトの時間枠: 1 分) で、ストラテジーはポジションがオープンであるかどうかを確認します。エクスポージャーが存在しない場合は、設定された Volume を使用して成行買い注文を送信します。
  3. キャンドルが完成するたびに、ドローダウンメトリクスが更新されます。
    • この戦略は、StartBalance + 実現損益として達成された最大残高を追跡します。
    • 現在の資産は、StartBalance + 実現損益 + 未実現損益に等しくなります。ここで、未実現損益は、最新のローソク足終値、平均エントリー価格、商品の PriceStep/StepPrice から導出されます。
    • ドローダウンは、保存されたピーク残高から現在の資産までの減少率です。この値は、更新のたびに情報メッセージとともに記録されます。

このアルゴリズムは、追加のポジションをオープンしたり反転したりすることはありません。初期ポジションが確立されると、ストップアウトするか、テイクプロフィットが発動するか、ユーザーが手動で介入するまで、アクティブなままになります。

パラメーター

パラメータ デフォルト 説明
StartBalance 1000 ピークエクイティとドローダウンパーセンテージを計算する際に使用されるベースライン残高。
Volume 0.01 最初の成行買い注文の正味数量(商品単位)。
StopLossPoints 300 価格ポイントで測定されたエントリー価格から保護ストップまでの距離。 0 の値は停止を無効にします。
TakeProfitPoints 400 エントリー価格から保護ターゲットまでの距離。価格ポイントで測定されます。値 0 はターゲットを無効にします。
CandleType 1m 時間枠 定期的なドローダウン更新と最初のエントリーチェックを推進する時間枠。

実装メモ

  • ドローダウン カウンターは、ストラテジーの実現損益 (PnL) と、価格差から推定される未実現損益を組み合わせて使用し、MT4 バージョンにあるランニングバランス ロジックと一致します。
  • PriceStep または StepPrice がセキュリティに利用できない場合、未実現損益計算では安全にゼロが返され、ゼロ除算エラーが防止されます。
  • Volume は、最初の取引の前に正の値であることが保証されるように検証されます。それ以外の場合は、警告がログに記録され、戦略はフラットのままになります。
  • DrawdownPercent は、他のモジュール (ダッシュボード、リスク コントローラー) がプログラムで値を取得できるように、最新のドローダウン読み取り値を公開します。

使用のヒント

  • 意味のあるドローダウン統計を取得するには、StartBalance を実際の口座残高 (または取引セッション開始時の残高) に設定します。
  • タイムリーな更新のためにデフォルトの 1 分ローソク足を維持するか、ニアティック精度が必要な場合はより高速な合成ローソク足タイプを選択してください。
  • この戦略は意図的に単一のロングポジションを保持するため、ストップまたはターゲットに達した後に再エントリーする必要がある場合は、手動のリスク管理または外部の自動化と組み合わせてください。
  • 必ずシミュレーターでテストして、ブローカーが PriceStepStepPrice を供給し、未実現損益換算が期待どおりであることを確認してください。
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Replicates the BalanceDrawdownInMT4 expert advisor: opens a single long position and tracks drawdown from the peak balance.
/// </summary>
public class BalanceDrawdownInMt4Strategy : Strategy
{
	private readonly StrategyParam<decimal> _startBalance;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _entryCooldownDays;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _maxBalance;
	private decimal _lastDrawdown;
	private decimal _lastPrice;
	private DateTime _lastEntryDate;

	/// <summary>
	/// Balance used as the baseline for drawdown calculations.
	/// </summary>
	public decimal StartBalance
	{
		get => _startBalance.Value;
		set => _startBalance.Value = value;
	}


	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum number of days between new entries.
	/// </summary>
	public int EntryCooldownDays
	{
		get => _entryCooldownDays.Value;
		set => _entryCooldownDays.Value = value;
	}

	/// <summary>
	/// Timeframe used to trigger periodic drawdown updates.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Current drawdown percentage relative to the peak balance.
	/// </summary>
	public decimal DrawdownPercent => _lastDrawdown;

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public BalanceDrawdownInMt4Strategy()
	{
		_startBalance = Param(nameof(StartBalance), 1000m)
			.SetDisplay("Start Balance", "Initial balance for drawdown measurement.", "Risk")
			;


		_stopLossPoints = Param(nameof(StopLossPoints), 300m)
			.SetDisplay("Stop-Loss (points)", "Distance from entry price to the protective stop.", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400m)
			.SetDisplay("Take-Profit (points)", "Distance from entry price to the profit target.", "Risk")
			;

		_entryCooldownDays = Param(nameof(EntryCooldownDays), 5)
			.SetGreaterThanZero()
			.SetDisplay("Entry Cooldown Days", "Minimum number of days between new long entries.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe that drives drawdown monitoring.", "General");
	}

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

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

		_maxBalance = 0m;
		_lastDrawdown = 0m;
		_lastPrice = 0m;
		_lastEntryDate = default;
	}

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

		StartProtection(
			stopLoss: new Unit(StopLossPoints * GetPriceStep(), UnitTypes.Absolute),
			takeProfit: new Unit(TakeProfitPoints * GetPriceStep(), UnitTypes.Absolute));

		_maxBalance = StartBalance;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_lastPrice = candle.ClosePrice;

		EnsurePosition(candle.CloseTime);
		UpdateDrawdown();
	}

	private void EnsurePosition(DateTime candleDate)
	{
		if (Position != 0m)
			return;

		if (_lastEntryDate != default && (candleDate.Date - _lastEntryDate.Date).TotalDays < EntryCooldownDays)
			return;

		if (Volume <= 0m)
		{
			LogWarning("Volume parameter must be positive to open the initial trade.");
			return;
		}

		BuyMarket(Volume);
		_lastEntryDate = candleDate.Date;
	}

	private void UpdateDrawdown()
	{
		var balanceWithoutFloating = StartBalance + PnL;
		if (balanceWithoutFloating > _maxBalance)
			_maxBalance = balanceWithoutFloating;

		if (_maxBalance <= 0m)
		{
			_lastDrawdown = 0m;
			return;
		}

		var unrealized = CalculateUnrealizedPnL(_lastPrice);
		var currentBalance = balanceWithoutFloating + unrealized;

		var drawdown = (_maxBalance - currentBalance) / _maxBalance * 100m;
		_lastDrawdown = drawdown > 0m ? drawdown : 0m;

		LogInfo($"Current drawdown: {_lastDrawdown:F2}%.");
	}

	private decimal CalculateUnrealizedPnL(decimal price)
	{
		if (Position == 0m)
			return 0m;

		var step = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
		if (step <= 0m || stepPrice <= 0m)
			return 0m;

		var priceDiff = price - _lastPrice;
		var points = priceDiff / step;

		return points * stepPrice * Position;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 0.0001m;
	}
}