Ver en GitHub

Estrategia Martin Martingale

Descripción general

La estrategia reproduce el comportamiento del Asesor Experto original "Martin" de MQL ejecutando una cuadrícula martingala con cobertura alrededor del precio actual. Alterna continuamente posiciones largas y cortas, duplicando el volumen operado en cada reversión hasta que la ganancia acumulada de toda la cesta alcance el objetivo configurado. Las velas solo se usan como impulsor para la lógica de decisión, mientras que las ejecuciones reales dependen de órdenes de mercado y órdenes stop expuestas por la API de alto nivel de StockSharp.

Cómo funciona

  1. Al iniciarse, la estrategia lee el PriceStep del instrumento para convertir los parámetros EntryOffsetPoints y StepPoints en distancias de precio absolutas. Si el paso de precio no está disponible, se asume el valor 1.
  2. Cuando no hay posición abierta ni ciclo martingala activo, la estrategia coloca una orden stop de compra y una orden stop de venta alrededor del último cierre. Los offsets son EntryOffsetPoints * PriceStep, lo que coincide con la distancia de 10 puntos usada en el código MQL original.
  3. Cuando se ejecuta una de las órdenes stop, la orden pendiente opuesta se cancela. El relleno define el primer trade de la secuencia martingala: la estrategia almacena su precio, volumen y dirección, y establece el contador de nivel interno en 1.
  4. En cada cierre de vela posterior, el precio de cierre actual se compara con el precio del último orden ejecutado. Si el mercado se ha movido contra esa orden al menos martingaleLevel * StepPoints * PriceStep, se envía una orden de mercado en la dirección opuesta con volumen duplicado respecto al trade anterior. La información del último trade se actualiza tras cada ejecución.
  5. La ganancia no realizada se evalúa como PnL + Position * (closePrice - PositionPrice). Cuando esta ganancia agregada supera el parámetro ProfitTarget, la estrategia envía CloseAll() para aplanar cada posición en la cesta, cancela todas las órdenes restantes y reinicia el ciclo para que pueda colocarse un nuevo par de órdenes stop.
  6. El mismo reinicio ocurre automáticamente cuando todas las posiciones se cierran manualmente: los contadores internos se borran y se crearán nuevas órdenes stop en la siguiente vela.

Este flujo de trabajo refleja la lógica de compra/venta alternante del Asesor Experto original mientras mantiene la implementación completamente dentro de la API de alto nivel de StockSharp.

Parámetros

  • StepPoints – número de pasos de precio usados para calcular el umbral de reversión para la siguiente orden de promediado. Por defecto 10 y puede optimizarse.
  • EntryOffsetPoints – offset para las órdenes stop iniciales de compra/venta en pasos de precio. También por defecto 10 puntos como la versión MQL.
  • ProfitTarget – ganancia absoluta en divisa requerida para cerrar toda la cesta martingala. Una vez que el PnL combinado realizado y no realizado supera este valor, todas las posiciones se liquidan.
  • CandleType – suscripción de velas usada para impulsar la lógica de la estrategia. El valor predeterminado es el marco temporal de un minuto, pero puede seleccionarse cualquier DataType soportado por el mercado.

El tamaño base del trade se toma de la propiedad Volume de la estrategia. Cada nueva reversión multiplica esta base por potencias de dos de la manera martingala clásica.

Notas prácticas

  • Siempre configure Volume para que coincida con el tamaño mínimo de lote del broker. El esquema de duplicación incrementa rápidamente la exposición, por lo que los límites de riesgo deben aplicarse externamente.
  • Dado que la colocación de órdenes está impulsada por los cierres de velas, los movimientos de precio rápidos dentro de la vela pueden activar entradas ligeramente más tarde que la versión MQL basada en ticks. Sin embargo, las órdenes stop mantienen los precios de entrada alineados con la lógica original.
  • La estrategia dibuja velas de precio y trades propios en el área de gráfico predeterminada para facilitar el seguimiento visual.
  • No se usa stop-loss automático. La única condición de salida es el ProfitTarget, por lo que el instrumento y el marco temporal deben elegirse cuidadosamente para controlar el riesgo de grandes tendencias adversas.

Diferencias respecto al Experto MQL

  • StockSharp usa posiciones netas, por lo tanto cada reversión se ejecuta con una orden de mercado que cierra la exposición anterior y abre la nueva en un solo trade. El PnL acumulado de la cesta sigue siendo idéntico a la implementación con cobertura.
  • La lógica tick a tick fue reemplazada por cierres de velas para la evaluación de señales con el fin de mantenerse dentro del uso recomendado de la API de alto nivel.
  • Los identificadores de órdenes son rastreados para evitar procesar ejecuciones parciales múltiples veces, asegurando que la lógica de duplicación de volumen permanezca consistente.

Estos cambios mantienen el comportamiento de trading fiel a la estrategia fuente mientras la adaptan al framework de StockSharp.

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>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _stepPoints;
	private readonly StrategyParam<int> _entryOffsetPoints;
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<int> _maxLevel;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _stepSize;
	private decimal _entryOffset;
	private decimal _lastTradePrice;
	private decimal _lastTradeVolume;
	private int _martingaleLevel;
	private Sides? _lastTradeSide;
	private bool _isClosing;
	private decimal? _initialPrice;

	/// <summary>
	/// Distance in points that defines when the next reversal is triggered.
	/// </summary>
	public int StepPoints
	{
		get => _stepPoints.Value;
		set => _stepPoints.Value = value;
	}

	/// <summary>
	/// Offset in points for the initial breakout entry.
	/// </summary>
	public int EntryOffsetPoints
	{
		get => _entryOffsetPoints.Value;
		set => _entryOffsetPoints.Value = value;
	}

	/// <summary>
	/// Aggregated profit required to close the entire martingale cycle.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum martingale doubling level before resetting.
	/// </summary>
	public int MaxLevel
	{
		get => _maxLevel.Value;
		set => _maxLevel.Value = value;
	}

	/// <summary>
	/// Candle type used to monitor the price.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
	/// </summary>
	public MartinMartingaleStrategy()
	{
		_stepPoints = Param(nameof(StepPoints), 10)
			.SetGreaterThanZero()
			.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
			;

		_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
			.SetGreaterThanZero()
			.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
			;

		_profitTarget = Param(nameof(ProfitTarget), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
			;

		_maxLevel = Param(nameof(MaxLevel), 5)
			.SetGreaterThanZero()
			.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetCycle();
		_isClosing = false;
		_initialPrice = null;
		_stepSize = 0;
		_entryOffset = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		UpdateStepSettings();

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

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

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

		UpdateStepSettings();

		if (_stepSize <= 0m || Volume <= 0m)
			return;

		var price = candle.ClosePrice;

		// If closing, flatten and wait
		if (_isClosing)
		{
			if (Position == 0)
			{
				_isClosing = false;
				ResetCycle();
			}
			return;
		}

		// If flat after a cycle, reset
		if (Position == 0 && _martingaleLevel > 0)
		{
			ResetCycle();
		}

		// Check profit target
		if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
		{
			_isClosing = true;
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();
			return;
		}

		// Max level reached -> close and reset
		if (_martingaleLevel >= MaxLevel && Position != 0)
		{
			_isClosing = true;
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();
			return;
		}

		// Initial entry: wait for breakout from first candle
		if (_martingaleLevel == 0 && Position == 0)
		{
			if (!_initialPrice.HasValue)
			{
				_initialPrice = price;
				return;
			}

			if (_entryOffset <= 0m)
				return;

			if (price >= _initialPrice.Value + _entryOffset)
			{
				BuyMarket();
				_lastTradePrice = price;
				_lastTradeVolume = Volume;
				_lastTradeSide = Sides.Buy;
				_martingaleLevel = 1;
				_initialPrice = null;
			}
			else if (price <= _initialPrice.Value - _entryOffset)
			{
				SellMarket();
				_lastTradePrice = price;
				_lastTradeVolume = Volume;
				_lastTradeSide = Sides.Sell;
				_martingaleLevel = 1;
				_initialPrice = null;
			}

			return;
		}

		if (_lastTradeSide is null || _martingaleLevel == 0)
			return;

		var threshold = _stepSize;

		if (_lastTradeSide == Sides.Buy)
		{
			if (price <= _lastTradePrice - threshold)
			{
				var nextVolume = _lastTradeVolume * 2m;
				var totalVolume = nextVolume + Math.Abs(Position);
				SellMarket();
				_lastTradePrice = price;
				_lastTradeVolume = nextVolume;
				_lastTradeSide = Sides.Sell;
				_martingaleLevel++;
			}
		}
		else
		{
			if (price >= _lastTradePrice + threshold)
			{
				var nextVolume = _lastTradeVolume * 2m;
				var totalVolume = nextVolume + Math.Abs(Position);
				BuyMarket();
				_lastTradePrice = price;
				_lastTradeVolume = nextVolume;
				_lastTradeSide = Sides.Buy;
				_martingaleLevel++;
			}
		}
	}

	private void UpdateStepSettings()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
		{
			priceStep = 1m;
		}

		_stepSize = StepPoints * priceStep;
		_entryOffset = EntryOffsetPoints * priceStep;
	}

	private void ResetCycle()
	{
		_martingaleLevel = 0;
		_lastTradePrice = 0m;
		_lastTradeVolume = 0m;
		_lastTradeSide = null;
	}
}