Auf GitHub ansehen

Balance Drawdown in der MT4-Strategie

Diese Strategie portiert den ursprünglichen MetaTrader 4 Expert Advisor BalanceDrawdownInMT4 auf den StockSharp High-Level API. Der EA eröffnet sofort eine einzelne Long-Position und misst kontinuierlich den Kontoverlust im Verhältnis zum seit Beginn der Sitzung erreichten Spitzensaldo.

Handelslogik

  1. Wenn die Strategie startet, ruft sie StartProtection auf, um verwaltete Stop-Loss- und Take-Profit-Levels zu aktivieren, die die in Preispunkten ausgedrückten MQL-Eingaben nachahmen.
  2. Bei der ersten abgeschlossenen Kerze (Standardzeitrahmen: 1 Minute) prüft die Strategie, ob eine Position offen ist. Wenn kein Risiko vorhanden ist, wird eine Market-Buy-Order mit dem konfigurierten Volume übermittelt.
  3. Nach jeder fertigen Kerze wird die Drawdown-Metrik aktualisiert:
    • Die Strategie verfolgt den maximal erreichten Saldo als StartBalance + realisierter PnL.
    • Das aktuelle Eigenkapital entspricht StartBalance + realisierter PnL + nicht realisierter PnL, wobei der nicht realisierte PnL aus dem letzten Schlusskurs der Kerze, dem durchschnittlichen Einstiegspreis und dem PriceStep/StepPrice des Instruments abgeleitet wird.
    • Drawdown ist der prozentuale Rückgang vom gespeicherten Spitzensaldo zum aktuellen Eigenkapital. Der Wert wird bei jeder Aktualisierung mit einer Informationsmeldung protokolliert.

Der Algorithmus eröffnet niemals zusätzliche Positionen oder Umkehrungen. Sobald die Ausgangsposition festgelegt ist, bleibt sie aktiv, bis sie gestoppt wird, der Take-Profit ausgelöst wird oder der Benutzer manuell eingreift.

Parameter

Parameter Standard Beschreibung
StartBalance 1000 Basissaldo, der bei der Berechnung des Spitzenkapitals und des Drawdown-Prozentsatzes verwendet wird.
Volume 0.01 Nettovolumen (in Instrumenteneinheiten) der ursprünglichen Marktkauforder.
StopLossPoints 300 Abstand vom Einstiegspreis bis zum Schutzstopp, gemessen in Preispunkten. Ein Wert von 0 deaktiviert den Stopp.
TakeProfitPoints 400 Abstand vom Einstiegspreis zum Schutzziel, gemessen in Preispunkten. Ein Wert von 0 deaktiviert das Ziel.
CandleType 1m Zeitrahmen Zeitrahmen, der regelmäßige Inanspruchnahmeaktualisierungen und die anfängliche Eingangsprüfung steuert.

Implementierungshinweise

  • Der Drawdown-Zähler verwendet den realisierten PnL (PnL) der Strategie in Kombination mit dem aus Preisunterschieden geschätzten nicht realisierten PnL, was der laufenden Bilanzlogik der MT4-Version entspricht.
  • Wenn PriceStep oder StepPrice für das Wertpapier nicht verfügbar ist, gibt die nicht realisierte PnL-Berechnung sicher Null zurück und verhindert so Fehler bei der Division durch Null.
  • Volume wird validiert, um vor dem ersten Handel einen positiven Wert sicherzustellen; Andernfalls wird eine Warnung protokolliert und die Strategie bleibt unverändert.
  • DrawdownPercent stellt den neuesten Drawdown-Wert zur Verfügung, sodass andere Module (Dashboards, Risikocontroller) den Wert programmgesteuert abrufen können.

Nutzungstipps

  • Setzen Sie StartBalance auf den tatsächlichen Kontostand (oder den Stand zu Beginn der Handelssitzung), um aussagekräftige Drawdown-Statistiken zu erhalten.
  • Behalten Sie die standardmäßigen 1-Minuten-Kerzen für zeitnahe Aktualisierungen bei oder wählen Sie einen schnelleren synthetischen Kerzentyp, wenn Sie nahezu Tick-Präzision benötigen.
  • Da diese Strategie absichtlich eine einzelne Long-Position hält, kombinieren Sie sie mit manuellen Risikokontrollen oder externer Automatisierung, wenn Sie nach dem Erreichen eines Stopps oder Ziels erneut einsteigen müssen.
  • Testen Sie immer auf einem Simulator, um sicherzustellen, dass der Broker PriceStep und StepPrice bereitstellt, sodass die nicht realisierte PnL-Konvertierung den Erwartungen entspricht.
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;
	}
}