Ver en GitHub

Reducción de saldo en la estrategia MT4

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

  1. 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.
  2. 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.
  3. 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;
	}
}