Стратегия переносит советник MetaTrader 4 BalanceDrawdownInMT4 на высокоуровневый API StockSharp. После запуска она сразу открывает одну длинную позицию и постоянно измеряет просадку счёта относительно максимального достигнутого баланса.
Логика работы
В методе OnStarted вызывается StartProtection, чтобы активировать управляемые стоп-лосс и тейк-профит, заданные в пунктах.
На первой завершённой свече выбранного таймфрейма (по умолчанию 1 минута) стратегия проверяет наличие позиции. Если позиция отсутствует, отправляется рыночная заявка на покупку объёмом Volume.
После каждой завершённой свечи пересчитывается величина просадки:
Максимальный баланс фиксируется как 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class balance_drawdown_in_mt4_strategy(Strategy):
def __init__(self):
super(balance_drawdown_in_mt4_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._start_balance = self.Param("StartBalance", 1000.0)
self._stop_loss_points = self.Param("StopLossPoints", 300.0)
self._take_profit_points = self.Param("TakeProfitPoints", 400.0)
self._entry_cooldown_days = self.Param("EntryCooldownDays", 5)
self._max_balance = 0.0
self._last_entry_date = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartBalance(self):
return self._start_balance.Value
@StartBalance.setter
def StartBalance(self, value):
self._start_balance.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def EntryCooldownDays(self):
return self._entry_cooldown_days.Value
@EntryCooldownDays.setter
def EntryCooldownDays(self, value):
self._entry_cooldown_days.Value = value
def OnReseted(self):
super(balance_drawdown_in_mt4_strategy, self).OnReseted()
self._max_balance = 0.0
self._last_entry_date = None
def OnStarted2(self, time):
super(balance_drawdown_in_mt4_strategy, self).OnStarted2(time)
self._max_balance = float(self.StartBalance)
self._last_entry_date = None
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_date = candle.CloseTime.Date
# Ensure position (long-only, with cooldown)
if self.Position == 0:
if self._last_entry_date is not None:
days_diff = (candle_date - self._last_entry_date).TotalDays
if days_diff < self.EntryCooldownDays:
return
self.BuyMarket()
self._last_entry_date = candle_date
def CreateClone(self):
return balance_drawdown_in_mt4_strategy()