Esta estrategia traslada el MetaTrader 4 asesor experto original BalanceDrawdownInMT4 al API de alto nivel de StockSharp. El EA abre inmediatamente una única posición larga y mide continuamente la reducción de la cuenta en relación con el saldo máximo alcanzado desde que comenzó la sesión.
Lógica de trading
Cuando comienza la estrategia, llama a StartProtection para armar niveles administrados de stop-loss y take-profit que imitan las entradas de MQL expresadas en puntos de precio.
En la primera vela finalizada (plazo predeterminado: 1 minuto), la estrategia verifica si una posición está abierta. Si no existe exposición, envía una orden de compra de mercado utilizando el Volume configurado.
Después de cada vela terminada, actualiza la métrica de reducción:
La estrategia rastrea el saldo máximo alcanzado como StartBalance + PnL realizado.
El capital actual es igual a StartBalance + PnL realizado + PnL no realizado, donde el PnL no realizado se deriva del último precio de cierre de vela, el precio de entrada promedio y el PriceStep/StepPrice del instrumento.
La reducción es la disminución porcentual desde el saldo máximo almacenado hasta el capital actual. El valor se registra con un mensaje informativo en cada actualización.
El algoritmo nunca abre posiciones adicionales ni invierte. Una vez que se establece la posición inicial, permanece activa hasta que se detiene, se activa la toma de ganancias o el usuario interviene manualmente.
Parámetros
Parámetro
Predeterminado
Descripción
StartBalance
1000
Saldo de referencia utilizado al calcular el capital máximo y el porcentaje de reducción.
Volume
0.01
Volumen neto (en unidades de instrumentos) de la orden de compra inicial en el mercado.
StopLossPoints
300
Distancia desde el precio de entrada hasta el stop de protección, medida en puntos de precio. Un valor de 0 desactiva la parada.
TakeProfitPoints
400
Distancia desde el precio de entrada al objetivo de protección, medida en puntos de precio. Un valor de 0 deshabilita el objetivo.
CandleType
1m período de tiempo
Marco de tiempo que impulsa las actualizaciones periódicas de la reducción y la verificación de entrada inicial.
Notas de implementación
El contador de reducción utiliza el PnL realizado de la estrategia (PnL) combinado con el PnL no realizado estimado a partir de las diferencias de precios, lo que coincide con la lógica del saldo corriente que se encuentra en la versión MT4.
Si PriceStep o StepPrice no está disponible para la seguridad, el cálculo de PnL no realizado devuelve cero de forma segura, lo que evita errores de división por cero.
Volume se valida para garantizar un valor positivo antes de la operación inicial; de lo contrario, se registra una advertencia y la estrategia permanece estable.
DrawdownPercent expone la última lectura de reducción para que otros módulos (paneles de control, controladores de riesgos) puedan extraer el valor mediante programación.
Consejos de uso
Establezca StartBalance en el saldo real de la cuenta (o el saldo al comienzo de la sesión de negociación) para obtener estadísticas de reducción significativas.
Mantenga las velas predeterminadas de 1 minuto para actualizaciones oportunas o elija un tipo de vela sintética más rápida si necesita una precisión cercana al tick.
Debido a que esta estrategia mantiene intencionalmente una única posición larga, combínela con controles de riesgo manuales o automatización externa si necesita volver a ingresar después de alcanzar una parada o un objetivo.
Pruebe siempre en un simulador para confirmar que el corredor proporciona PriceStep y StepPrice para que la conversión PnL no realizada coincida con las expectativas.
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()