Ver en GitHub

Estrategia de niveles MartingalaEA-5 (StockSharp)

La Estrategia de niveles MartingaleEA-5 es una adaptación directa del MetaTrader asesor experto de 5 "Niveles MartinaleEA-5" al API de alto nivel de StockSharp. El sistema supervisa una posición existente y construye una cuadrícula de promedio de cinco pasos cada vez que el mercado se mueve en contra. Toda la lógica se ejecuta en velas terminadas, lo que mantiene el comportamiento reproducible tanto en pruebas históricas como en operaciones reales.

Lógica de trading

  1. Seguimiento de la exposición existente: la estrategia espera que esté presente una posición inicial larga o corta. Puede abrir la primera operación manualmente o mediante cualquier otra estrategia.
  2. Detección de movimientos adversos: en cada vela completada, la estrategia mide qué tan lejos se ha alejado el precio actual de la entrada con el peor precio del grupo activo (la posición larga más alta o la posición corta más baja).
  3. Martingale adiciones: si la pérdida flotante en el grupo es negativa y el movimiento adverso excede las distancias acumuladas configuradas, la estrategia envía órdenes de mercado adicionales. Cada pedido adicional multiplica el anterior por VolumeMultiplier. Se pueden configurar hasta cinco niveles; el parámetro MaxAdditions limita cuántos de ellos se utilizan realmente.
  4. Objetivo de pérdidas y ganancias: mientras un grupo está abierto, la estrategia suma continuamente el PnL no realizado para esa dirección. Una vez que el total alcanza TakeProfitCurrency o cae por debajo de StopLossCurrency, todas las órdenes de ese lado se cierran con una orden de mercado y los contadores de martingala se reinician.
  5. Normalización de volumen: cada volumen de pedido pasa por los VolumeStep, MinVolume y MaxVolume del instrumento para evitar el envío de cantidades no ejecutables.

Parámetros

Nombre Descripción Predeterminado
EnableMartingale Activa o desactiva la lógica de promediación y liquidación. true
VolumeMultiplier Factor aplicado al volumen del pedido anterior al agregar un nuevo nivel. 2.0
MaxAdditions Número máximo de pasos de martingala por dirección (hasta cinco). 4
Level1DistancePips Distancia adversa inicial (en pips) antes de abrir la segunda orden. 300
Level2DistancePips Distancia adicional requerida para el tercer pedido. 400
Level3DistancePips Distancia adicional requerida para el cuarto orden. 500
Level4DistancePips Distancia adicional requerida para el quinto pedido. 600
Level5DistancePips Distancia adicional requerida para el sexto orden (si está permitido). 700
TakeProfitCurrency Beneficio no realizado (moneda de la cuenta) que cierra todo el grupo. 200
StopLossCurrency Pérdida no realizada (moneda de la cuenta) que obliga a una salida de emergencia. -500
CandleType Marco de tiempo utilizado para las evaluaciones (velas predeterminadas de 1 minuto). TimeFrame(1m)

Conversión de pips: cada distancia se multiplica por el paso del precio del instrumento (PriceStep o MinPriceStep). Para los símbolos cotizados en pips fraccionarios, ajuste los valores en consecuencia.

Notas y recomendaciones

  • La implementación refleja el EA original, incluida la suposición de que solo una cesta direccional está activa a la vez. Abrir posiciones simultáneamente en ambas direcciones hará que cada lado se gestione de forma independiente.
  • Debido a que la estrategia reacciona solo al cierre de la vela, elija un período de tiempo que coincida con la capacidad de respuesta deseada. Los plazos más bajos emulan más fielmente el comportamiento a nivel de tick.
  • Las técnicas Martingale amplifican el riesgo. Realice siempre una prueba retrospectiva con modelos realistas de deslizamiento y comisión y defina niveles de parada conservadores antes de habilitar la estrategia en los mercados reales.
  • La estrategia aún no crea un puerto Python. Solo se incluye la implementación de alto nivel de C# según lo solicitado.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Martingale averaging strategy converted from "MartingaleEA-5 Levels".
/// Opens initial position on simple momentum, then averages down with
/// increasing lot sizes up to 5 levels. Closes when floating profit
/// reaches target or stop threshold.
/// </summary>
public class MartingaleEa5LevelsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<int> _maxAdditions;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _stopLossPercent;

	private SimpleMovingAverage _sma;
	private decimal? _prevClose;
	private decimal? _prevMa;

	private readonly List<(decimal price, decimal vol)> _entries = new();
	private int _additions;
	private decimal _lastVolume;
	private Sides? _activeSide;
	private int _candleCount;
	private int _lastOrderCandle;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	public int MaxAdditions
	{
		get => _maxAdditions.Value;
		set => _maxAdditions.Value = value;
	}

	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	public MartingaleEa5LevelsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "SMA period for entry signal", "Indicators");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Multiplier for each martingale level", "Money Management");

		_maxAdditions = Param(nameof(MaxAdditions), 4)
			.SetDisplay("Max Additions", "Maximum martingale additions", "Money Management");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Floating profit % to close group", "Risk");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Floating loss % to close group", "Risk");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = default;
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_sma = new SimpleMovingAverage { Length = MaPeriod };
		_prevClose = null;
		_prevMa = null;
		_entries.Clear();
		_additions = 0;
		_lastVolume = 0;
		_activeSide = null;
		_candleCount = 0;
		_lastOrderCandle = 0;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_sma, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _sma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal smaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_sma.IsFormed)
		{
			_prevClose = candle.ClosePrice;
			_prevMa = smaValue;
			return;
		}

		_candleCount++;

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Cooldown: allow at most one order per 100 candles
		var cooldownPassed = (_candleCount - _lastOrderCandle) >= 100;

		// Check martingale closure first
		if (_entries.Count > 0)
		{
			var floatingPnl = CalculateFloatingPnl(close);
			var totalCost = CalculateTotalCost();

			if (totalCost > 0)
			{
				var pnlPercent = floatingPnl / totalCost * 100m;

				if (cooldownPassed && (pnlPercent >= TakeProfitPercent || pnlPercent <= -StopLossPercent))
				{
					// Close entire position
					if (Position > 0)
						SellMarket(Position);
					else if (Position < 0)
						BuyMarket(Math.Abs(Position));

					_lastOrderCandle = _candleCount;
					_entries.Clear();
					_additions = 0;
					_lastVolume = 0;
					_activeSide = null;

					_prevClose = close;
					_prevMa = smaValue;
					return;
				}
			}

			// Check for martingale additions
			if (cooldownPassed && _additions < MaxAdditions)
			{
				var avgPrice = CalculateAvgPrice();
				var adversePercent = _activeSide == Sides.Buy
					? (avgPrice - close) / avgPrice * 100m
					: (close - avgPrice) / avgPrice * 100m;

				// Add at each 0.3% adverse move beyond previous level
				var threshold = 0.3m * (_additions + 1);
				if (adversePercent >= threshold)
				{
					var nextVol = _lastVolume * VolumeMultiplier;
					if (nextVol < 1) nextVol = 1;

					if (_activeSide == Sides.Buy)
					{
						BuyMarket(nextVol);
						_entries.Add((close, nextVol));
					}
					else
					{
						SellMarket(nextVol);
						_entries.Add((close, nextVol));
					}

					_lastVolume = nextVol;
					_additions++;
					_lastOrderCandle = _candleCount;
				}
			}
		}

		// Initial entry signal: MA crossover
		if (cooldownPassed && _prevClose != null && _prevMa != null && _activeSide == null)
		{
			var buySignal = _prevClose.Value < _prevMa.Value && close > smaValue;
			var sellSignal = _prevClose.Value > _prevMa.Value && close < smaValue;

			if (buySignal)
			{
				BuyMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Buy;
				_lastOrderCandle = _candleCount;
			}
			else if (sellSignal)
			{
				SellMarket(volume);
				_entries.Clear();
				_entries.Add((close, volume));
				_additions = 0;
				_lastVolume = volume;
				_activeSide = Sides.Sell;
				_lastOrderCandle = _candleCount;
			}
		}

		_prevClose = close;
		_prevMa = smaValue;
	}

	private decimal CalculateFloatingPnl(decimal currentPrice)
	{
		var pnl = 0m;
		foreach (var (price, vol) in _entries)
		{
			if (_activeSide == Sides.Buy)
				pnl += (currentPrice - price) * vol;
			else
				pnl += (price - currentPrice) * vol;
		}
		return pnl;
	}

	private decimal CalculateTotalCost()
	{
		var cost = 0m;
		foreach (var (price, vol) in _entries)
			cost += price * vol;
		return cost;
	}

	private decimal CalculateAvgPrice()
	{
		var totalVol = 0m;
		var totalCost = 0m;
		foreach (var (price, vol) in _entries)
		{
			totalVol += vol;
			totalCost += price * vol;
		}
		return totalVol > 0 ? totalCost / totalVol : 0;
	}
}