Открыть на GitHub

Стратегия Balance Drawdown In MT4

Стратегия переносит советник MetaTrader 4 BalanceDrawdownInMT4 на высокоуровневый API StockSharp. После запуска она сразу открывает одну длинную позицию и постоянно измеряет просадку счёта относительно максимального достигнутого баланса.

Логика работы

  1. В методе OnStarted вызывается StartProtection, чтобы активировать управляемые стоп-лосс и тейк-профит, заданные в пунктах.
  2. На первой завершённой свече выбранного таймфрейма (по умолчанию 1 минута) стратегия проверяет наличие позиции. Если позиция отсутствует, отправляется рыночная заявка на покупку объёмом Volume.
  3. После каждой завершённой свечи пересчитывается величина просадки:
    • Максимальный баланс фиксируется как StartBalance + накопленная реализованная прибыль (PnL).
    • Текущий капитал равен StartBalance + реализованный PnL + нереализованный PnL, где нереализованная прибыль берётся из последней цены закрытия свечи, средней цены входа и параметров инструмента PriceStep/StepPrice.
    • Просадка выражается в процентах падения от максимального баланса до текущего капитала. Значение выводится в журнал через AddInfoLog.

Стратегия больше не открывает дополнительных сделок и не переворачивается. Позиция остаётся открытой до срабатывания стопа, тейк-профита или ручного вмешательства.

Параметры

Параметр Значение по умолчанию Описание
StartBalance 1000 Исходный баланс, относительно которого рассчитываются пик и просадка.
Volume 0.01 Объём первой рыночной покупки (в торговых единицах инструмента).
StopLossPoints 300 Дистанция от цены входа до защитного стопа в пунктах. 0 отключает стоп.
TakeProfitPoints 400 Дистанция до тейк-профита в пунктах. 0 отключает цель.
CandleType Таймфрейм 1 минута Тип свечей, по завершению которых обновляется просадка и выполняется проверка позиции.

Особенности реализации

  • Расчёт просадки повторяет МТ4-логику: накопленный баланс берётся из StartBalance и реализованного PnL, а текущий капитал дополняется плавающей прибылью, посчитанной через шаг цены и стоимость шага.
  • Если для инструмента отсутствуют PriceStep или StepPrice, функция возвращает ноль, чтобы избежать деления на ноль.
  • Перед открытием стартовой сделки проверяется, что Volume положителен; иначе выводится предупреждение, и стратегия остаётся без позиции.
  • Свойство DrawdownPercent предоставляет текущее значение просадки для внешних модулей мониторинга риска.

Рекомендации по использованию

  • Установите StartBalance равным фактическому балансу счёта (или балансу на начало торговой сессии), чтобы получить корректные проценты просадки.
  • Сохраняйте минутный таймфрейм для регулярных обновлений или выберите более быстрый синтетический тип свечей, если требуется больший объём данных.
  • Стратегия держит только длинную позицию, поэтому для повторного входа после закрытия подключите внешние алгоритмы или управляйте вручную.
  • Перед боевым запуском протестируйте алгоритм в симуляторе и убедитесь, что брокер передаёт PriceStep и StepPrice — от этого зависит корректность расчёта нереализованного PnL.
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;
	}
}