Ver en GitHub

Martingale Estrategia del simulador comercial

Descripción general

MartingaleTradeSimulatorStrategy recrea el asesor experto "Martingale Trade Simulator" de MetaTrader dentro del marco StockSharp. La estrategia es un panel de negociación manual que permite al operador enviar órdenes de mercado inmediatas, aplicar promedios estilo martingala y administrar la protección de seguimiento sin secuencias de comandos de automatización adicional. Reacciona a los cambios de parámetros en tiempo real, lo que lo hace adecuado para experimentos de Strategy Tester al igual que el robot MQL original.

como funciona

Botones de mercado manuales

  • Los parámetros Buy y Sell actúan como botones virtuales. Cuando cualquiera de los parámetros se establece en true, la estrategia envía una orden de mercado con el volumen Order Volume y luego restablece automáticamente el parámetro a false.
  • No se utilizan órdenes pendientes: la estrategia funciona completamente con ejecuciones de mercado, reflejando el comportamiento del simulador dentro del probador visual de MetaTrader.

Martingale promedio

  • Habilitar Enable Martingale permite que el panel realice pedidos promedio cuando el parámetro Martingale se cambia a true.
  • La estrategia comprueba la posición activa:
    • Posición larga: si el precio de venta actual está al menos Martingale Step (points) por debajo del precio de compra más bajo ejecutado, se envía una nueva orden de compra.
    • Posición corta: Si el precio de oferta actual está al menos Martingale Step (points) por encima del precio de venta más alto ejecutado, se emite una nueva orden de venta.
  • Cada volumen de pedido promedio es igual a Order Volume × Martingale Multiplier^N, donde N es el número de entradas consecutivas en la dirección actual.
  • Cuando la martingala está activa, el objetivo de obtención de beneficios se recalcula al precio de entrada medio ponderado más/menos Martingale TP Offset (points) para cubrir la reducción acumulada.

Módulo de parada de seguimiento

  • Enable Trailing activa un trailing stop protector que sigue el mejor precio más reciente.
  • El trailing stop comienza a Trailing Stop (points) del precio de mercado y avanza solo después de que el precio mejora al menos Trailing Step (points).
  • Si el precio de mercado cruza el nivel final, la estrategia cierra inmediatamente toda la posición con una orden de mercado opuesta.

Stop-loss y take-profit

  • Stop Loss (points) y Take Profit (points) reproducen los controles de riesgo básicos del asesor experto original.
  • Para posiciones largas, el stop se sitúa por debajo del precio medio de entrada, mientras que la toma de beneficios se sitúa por encima. Para posiciones cortas, ambos niveles se reflejan.
  • Las salidas protectoras se ejecutan con órdenes de mercado, por lo que la estrategia sigue siendo compatible con cualquier conector compatible con StockSharp.

Parámetros

Parámetro Descripción Predeterminado
Order Volume Tamaño base para órdenes de mercado manuales. 1
Stop Loss (points) Distancia al tope de protección. Zero desactiva el stop-loss. 500
Take Profit (points) Distancia al objetivo protector. Zero desactiva la toma de ganancias. 500
Enable Trailing Enciende/apaga el módulo de trailing stop. true
Trailing Stop (points) Distancia entre el precio y el trailing stop. 50
Trailing Step (points) Se requiere un movimiento mínimo favorable para avanzar el trailing stop. 20
Enable Martingale Permite promediar órdenes controladas por el botón Martingale. true
Martingale Multiplier Multiplicador de volumen utilizado para cada operación promedio adicional. 1.2
Martingale Step (points) Se requiere movimiento adverso antes de que se permita una orden de promediación. 150
Martingale TP Offset (points) Compensación adicional aplicada al nivel promedio de obtención de beneficios. 50
Buy Establezca en true para enviar una orden de compra de mercado (reinicio automático). false
Sell Establezca en true para enviar una orden de venta de mercado (reinicio automático). false
Martingale Establezca en true para evaluar y realizar un pedido promedio (reinicio automático). false

Consejos de uso

  1. Adjunte la estrategia a un instrumento, configure Order Volume e iníciela en modo probador o en vivo.
  2. Utilice los botones Buy / Sell para simular clics en los botones del panel MetaTrader.
  3. Después de la primera operación, active el interruptor Martingale siempre que el precio se mueva en contra de la posición. La estrategia verifica la distancia del precio y aumenta el volumen si se cumplen las condiciones.
  4. Ajuste los parámetros de seguimiento y riesgo para replicar el comportamiento del EA original o experimentar con configuraciones alternativas.

Notas

  • La estrategia se basa en datos de Nivel 1 (mejor oferta/demanda y última operación) para evaluar las condiciones del mercado.
  • Todos los comentarios dentro del código C# están en inglés, manteniendo la coherencia con las pautas del repositorio.
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>
/// Manual martingale simulator that reproduces the "Martingale Trade Simulator" expert advisor.
/// Provides buy/sell buttons, optional martingale averaging and trailing stop automation.
/// </summary>
public class MartingaleTradeSimulatorStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<bool> _enableMartingale;
	private readonly StrategyParam<decimal> _martingaleMultiplier;
	private readonly StrategyParam<decimal> _martingaleStepPoints;
	private readonly StrategyParam<decimal> _martingaleTakeProfitOffset;
	private readonly StrategyParam<bool> _buyRequest;
	private readonly StrategyParam<bool> _sellRequest;
	private readonly StrategyParam<bool> _martingaleRequest;

	private decimal? _lastTradePrice;
	private decimal? _bestBidPrice;
	private decimal? _bestAskPrice;

	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	private decimal? _lowestLongPrice;
	private decimal? _highestShortPrice;
	private decimal? _longTakeProfit;
	private decimal? _shortTakeProfit;

	private int _longEntriesCount;
	private int _shortEntriesCount;
	private decimal _previousPosition;
	private bool _longMartingaleActive;
	private bool _shortMartingaleActive;

	/// <summary>
	/// Volume used for manual market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.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>
	/// Enables the trailing stop automation.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Distance from price to the trailing stop in points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Minimal step required to move the trailing stop in points.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Enables martingale averaging logic.
	/// </summary>
	public bool EnableMartingale
	{
		get => _enableMartingale.Value;
		set => _enableMartingale.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the volume of each martingale order.
	/// </summary>
	public decimal MartingaleMultiplier
	{
		get => _martingaleMultiplier.Value;
		set => _martingaleMultiplier.Value = value;
	}

	/// <summary>
	/// Price step in points before a new martingale order can be placed.
	/// </summary>
	public decimal MartingaleStepPoints
	{
		get => _martingaleStepPoints.Value;
		set => _martingaleStepPoints.Value = value;
	}

	/// <summary>
	/// Offset in points added to the averaged take-profit price.
	/// </summary>
	public decimal MartingaleTakeProfitOffset
	{
		get => _martingaleTakeProfitOffset.Value;
		set => _martingaleTakeProfitOffset.Value = value;
	}

	/// <summary>
	/// Manual trigger for a market buy order.
	/// </summary>
	public bool BuyRequest
	{
		get => _buyRequest.Value;
		set => _buyRequest.Value = value;
	}

	/// <summary>
	/// Manual trigger for a market sell order.
	/// </summary>
	public bool SellRequest
	{
		get => _sellRequest.Value;
		set => _sellRequest.Value = value;
	}

	/// <summary>
	/// Manual trigger for martingale averaging.
	/// </summary>
	public bool MartingaleRequest
	{
		get => _martingaleRequest.Value;
		set => _martingaleRequest.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="MartingaleTradeSimulatorStrategy"/>.
	/// </summary>
	public MartingaleTradeSimulatorStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Base volume for manual market orders.", "Manual Controls");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to protective stop.", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to protective target.", "Risk");

		_enableTrailing = Param(nameof(EnableTrailing), true)
		.SetDisplay("Enable Trailing", "Turn the trailing stop automation on or off.", "Trailing")
		;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Distance of the trailing stop from market price.", "Trailing");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 20m)
		.SetNotNegative()
		.SetDisplay("Trailing Step (points)", "Minimal gain required to move the trailing stop.", "Trailing");

		_enableMartingale = Param(nameof(EnableMartingale), true)
		.SetDisplay("Enable Martingale", "Allow averaging orders using martingale sizing.", "Martingale")
		;

		_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 1.2m)
		.SetGreaterThanZero()
		.SetDisplay("Martingale Multiplier", "Volume multiplier for each averaging order.", "Martingale");

		_martingaleStepPoints = Param(nameof(MartingaleStepPoints), 150m)
		.SetNotNegative()
		.SetDisplay("Martingale Step (points)", "Minimal adverse move before adding a new order.", "Martingale");

		_martingaleTakeProfitOffset = Param(nameof(MartingaleTakeProfitOffset), 50m)
		.SetNotNegative()
		.SetDisplay("Martingale TP Offset (points)", "Extra distance added to averaged take-profit.", "Martingale");

		_buyRequest = Param(nameof(BuyRequest), false)
		.SetDisplay("Buy", "Set to true to send a market buy order.", "Manual Controls")
		;

		_sellRequest = Param(nameof(SellRequest), false)
		.SetDisplay("Sell", "Set to true to send a market sell order.", "Manual Controls")
		;

		_martingaleRequest = Param(nameof(MartingaleRequest), false)
		.SetDisplay("Martingale", "Set to true to evaluate and place an averaging order.", "Manual Controls")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

	private SimpleMovingAverage _smaFast = null!;
	private SimpleMovingAverage _smaSlow = null!;
	private readonly StrategyParam<DataType> _candleType;

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_lastTradePrice = null;
		_bestBidPrice = null;
		_bestAskPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_lowestLongPrice = null;
		_highestShortPrice = null;
		_longTakeProfit = null;
		_shortTakeProfit = null;
		_longEntriesCount = 0;
		_shortEntriesCount = 0;
		_previousPosition = 0m;
		_longMartingaleActive = false;
		_shortMartingaleActive = false;
	}

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

		_smaFast = new SimpleMovingAverage { Length = 10 };
		_smaSlow = new SimpleMovingAverage { Length = 30 };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_smaFast, _smaSlow, ProcessCandle)
			.Start();
	}

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

		_lastTradePrice = candle.ClosePrice;

		if (fast > slow && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(OrderVolume);
		}
		else if (fast < slow && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Position);
			SellMarket(OrderVolume);
		}
	}

	private void ProcessMartingaleCommand()
	{
		if (!MartingaleRequest)
		return;

		MartingaleRequest = false;

		if (!EnableMartingale)
		return;

		if (!IsOnline)
		return;

		if (Security == null || Portfolio == null)
		return;

		var step = GetPriceStep() * MartingaleStepPoints;
		if (step <= 0m)
		return;

		if (Position > 0)
		{
			var ask = GetAskPrice();
			if (ask == null)
			return;

			var referencePrice = _lowestLongPrice ?? _lastTradePrice;
			if (referencePrice == null)
			return;

			if (referencePrice.Value - ask.Value >= step)
			{
				var volume = CalculateNextVolume(true);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_longMartingaleActive = true;
				}
			}
		}
		else if (Position < 0)
		{
			var bid = GetBidPrice();
			if (bid == null)
			return;

			var referencePrice = _highestShortPrice ?? _lastTradePrice;
			if (referencePrice == null)
			return;

			if (bid.Value - referencePrice.Value >= step)
			{
				var volume = CalculateNextVolume(false);
				if (volume > 0m)
				{
					SellMarket(volume);
					_shortMartingaleActive = true;
				}
			}
		}
	}

	private void ManageRisk()
	{
		if (Position == 0)
		{
			_longTrailingStop = null;
			_shortTrailingStop = null;
			return;
		}

		var marketPrice = GetMarketPrice();
		if (marketPrice == null)
		return;

		var step = GetPriceStep();
		var positionPrice = _lastTradePrice;
		if (positionPrice == null)
		return;

		if (Position > 0)
		{
			ApplyLongProtection(marketPrice.Value, positionPrice.Value, step);
		}
		else
		{
			ApplyShortProtection(marketPrice.Value, positionPrice.Value, step);
		}
	}

	private void ApplyLongProtection(decimal marketPrice, decimal positionPrice, decimal priceStep)
	{
		if (StopLossPoints > 0m)
		{
			var stopPrice = positionPrice - StopLossPoints * priceStep;
			if (marketPrice <= stopPrice)
			SellMarket(Math.Abs(Position));
		}

		var takePrice = _longMartingaleActive ? _longTakeProfit : (TakeProfitPoints > 0m ? positionPrice + TakeProfitPoints * priceStep : null);
		if (takePrice != null && marketPrice >= takePrice.Value)
		SellMarket(Math.Abs(Position));

		if (!EnableTrailing || TrailingStopPoints <= 0m)
		{
			_longTrailingStop = null;
			return;
		}

		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		if (_longTrailingStop == null)
		{
			_longTrailingStop = marketPrice - trailingDistance;
		}
		else
		{
			var candidate = marketPrice - trailingDistance;
			if (candidate - _longTrailingStop.Value >= trailingStep)
			_longTrailingStop = candidate;
		}

		if (_longTrailingStop != null && marketPrice <= _longTrailingStop.Value)
		SellMarket(Math.Abs(Position));
	}

	private void ApplyShortProtection(decimal marketPrice, decimal positionPrice, decimal priceStep)
	{
		if (StopLossPoints > 0m)
		{
			var stopPrice = positionPrice + StopLossPoints * priceStep;
			if (marketPrice >= stopPrice)
			BuyMarket(Math.Abs(Position));
		}

		var takePrice = _shortMartingaleActive ? _shortTakeProfit : (TakeProfitPoints > 0m ? positionPrice - TakeProfitPoints * priceStep : null);
		if (takePrice != null && marketPrice <= takePrice.Value)
		BuyMarket(Math.Abs(Position));

		if (!EnableTrailing || TrailingStopPoints <= 0m)
		{
			_shortTrailingStop = null;
			return;
		}

		var trailingDistance = TrailingStopPoints * priceStep;
		var trailingStep = TrailingStepPoints * priceStep;

		if (_shortTrailingStop == null)
		{
			_shortTrailingStop = marketPrice + trailingDistance;
		}
		else
		{
			var candidate = marketPrice + trailingDistance;
			if (_shortTrailingStop.Value - candidate >= trailingStep)
			_shortTrailingStop = candidate;
		}

		if (_shortTrailingStop != null && marketPrice >= _shortTrailingStop.Value)
		BuyMarket(Math.Abs(Position));
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade?.Price;
		if (price is null)
		return;

		if (Position > 0)
		{
			_longMartingaleActive = _longMartingaleActive && Position > 0;
			_shortMartingaleActive = false;
			_shortTrailingStop = null;
			_shortTakeProfit = null;

			if (trade.Order.Side == Sides.Buy)
			{
				_lowestLongPrice = _lowestLongPrice.HasValue ? Math.Min(_lowestLongPrice.Value, price.Value) : price.Value;
				UpdateLongTakeProfit();
			}
			else if (Position <= 0)
			{
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			_shortMartingaleActive = _shortMartingaleActive && Position < 0;
			_longMartingaleActive = false;
			_longTrailingStop = null;
			_longTakeProfit = null;

			if (trade.Order.Side == Sides.Sell)
			{
				_highestShortPrice = _highestShortPrice.HasValue ? Math.Max(_highestShortPrice.Value, price.Value) : price.Value;
				UpdateShortTakeProfit();
			}
			else if (Position >= 0)
			{
				ResetShortState();
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		var delta = Position - _previousPosition;

		if (Position > 0)
		{
			if (_previousPosition <= 0m)
			{
				_longEntriesCount = 1;
			}
			else if (delta > 0m)
			{
				_longEntriesCount++;
			}
			else if (delta < 0m)
			{
				_longEntriesCount = Math.Max(1, _longEntriesCount - 1);
			}

			_shortEntriesCount = 0;
		}
		else if (Position < 0)
		{
			if (_previousPosition >= 0m)
			{
				_shortEntriesCount = 1;
			}
			else if (delta < 0m)
			{
				_shortEntriesCount++;
			}
			else if (delta > 0m)
			{
				_shortEntriesCount = Math.Max(1, _shortEntriesCount - 1);
			}

			_longEntriesCount = 0;
		}
		else
		{
			_longEntriesCount = 0;
			_shortEntriesCount = 0;
		}

		if (Position == 0m)
		{
			ResetLongState();
			ResetShortState();
		}

		_previousPosition = Position;
	}

	private void UpdateLongTakeProfit()
	{
		if (!_longMartingaleActive)
		return;

		var positionPrice = _lastTradePrice;
		if (positionPrice == null)
		return;

		var offset = MartingaleTakeProfitOffset * GetPriceStep();
		_longTakeProfit = positionPrice.Value + offset;
	}

	private void UpdateShortTakeProfit()
	{
		if (!_shortMartingaleActive)
		return;

		var positionPrice = _lastTradePrice;
		if (positionPrice == null)
		return;

		var offset = MartingaleTakeProfitOffset * GetPriceStep();
		_shortTakeProfit = positionPrice.Value - offset;
	}

	private decimal? GetMarketPrice()
	{
		if (_lastTradePrice != null)
		return _lastTradePrice;

		if (_bestBidPrice != null && _bestAskPrice != null)
		return (_bestBidPrice.Value + _bestAskPrice.Value) / 2m;

		return _bestBidPrice ?? _bestAskPrice;
	}

	private decimal? GetBidPrice()
	{
		return _bestBidPrice ?? _lastTradePrice;
	}

	private decimal? GetAskPrice()
	{
		return _bestAskPrice ?? _lastTradePrice;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep;
		return step is null || step == 0m ? 1m : step.Value;
	}

	private decimal CalculateNextVolume(bool isLong)
	{
		var entries = isLong ? _longEntriesCount : _shortEntriesCount;
		var multiplier = MartingaleMultiplier;

		if (multiplier <= 0m)
		return 0m;

		var power = entries;
		var factor = (decimal)Math.Pow((double)multiplier, power);
		return OrderVolume * factor;
	}

	private void ResetLongState()
	{
		_longMartingaleActive = false;
		_longTrailingStop = null;
		_longTakeProfit = null;
		_lowestLongPrice = null;
	}

	private void ResetShortState()
	{
		_shortMartingaleActive = false;
		_shortTrailingStop = null;
		_shortTakeProfit = null;
		_highestShortPrice = null;
	}
}