在 GitHub 上查看

Balance Drawdown In MT4 策略

该策略将 MetaTrader 4 的 BalanceDrawdownInMT4 专家顾问迁移到 StockSharp 高级 API。启动后立刻买入一手多单,并持续监控账户权益相对历史峰值的回撤。

交易逻辑

  1. OnStarted 中调用 StartProtection,根据输入的点数设置托管止损与止盈。
  2. 在所选时间框架的第一根收盘 K 线(默认 1 分钟)上检查是否已经持仓。如果账户为空仓,则按 Volume 参数提交市价买单。
  3. 每根收盘 K 线都会刷新回撤指标:
    • 峰值余额记录为 StartBalance + 已实现盈亏 (PnL)。
    • 当前权益 = StartBalance + 已实现 PnL + 未实现 PnL。未实现盈亏通过最新收盘价、平均成交价以及 PriceStep / StepPrice 计算得到。
    • 回撤 = 峰值余额与当前权益之间的百分比跌幅。结果会通过 AddInfoLog 写入日志。

策略不会加仓或反向开仓。初始头寸将一直持有,直到触发止损、止盈或手动平仓。

参数

参数 默认值 说明
StartBalance 1000 计算回撤时使用的起始余额。
Volume 0.01 初始市价买单的交易数量。
StopLossPoints 300 止损距离(点)。0 表示不开启止损。
TakeProfitPoints 400 止盈距离(点)。0 表示不开启止盈。
CandleType 1 分钟 触发回撤计算与持仓检查的蜡烛类型。

实现细节

  • 回撤算法复刻原版 EA:峰值余额来自 StartBalance 加上已实现收益,当前权益再加上使用价格步长折算的浮动盈亏。
  • 若品种未提供 PriceStepStepPrice,未实现盈亏计算会返回 0,以避免除零错误。
  • Volume 小于等于零时,会记录警告并保持空仓,防止生成无效订单。
  • 公开属性 DrawdownPercent,便于监控模块或风险控制逻辑读取当前回撤。

使用建议

  • StartBalance 设置为真实账户余额或当日开盘余额,以获得准确的回撤百分比。
  • 默认的 1 分钟 K 线能够提供稳定的更新频率,如需更快响应可选择更短周期或自定义蜡烛类型。
  • 策略仅持有单向多头,可结合外部策略或手动控制实现重复入场。
  • 建议在仿真环境中充分测试,确认行情源会提供 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;
	}
}