Ver no GitHub

Martingale Estratégia de simulador comercial

Visão geral

MartingaleTradeSimulatorStrategy recria o consultor especialista "Martingale Trade Simulator" de MetaTrader dentro da estrutura StockSharp. A estratégia é um painel de negociação manual que permite ao trader enviar ordens de mercado imediatas, aplicar a média no estilo martingale e gerenciar a proteção de rastreamento sem scripts de automação adicional. Ele reage às mudanças de parâmetros em tempo real, tornando-o adequado para experimentos do Strategy Tester, assim como o robô MQL original.

Como funciona

Botões manuais de mercado

  • Os parâmetros Buy e Sell atuam como botões virtuais. Quando qualquer parâmetro é definido como true, a estratégia envia uma ordem de mercado com volume Order Volume e então redefine automaticamente o parâmetro para false.
  • Nenhuma ordem pendente é usada — a estratégia funciona inteiramente com execuções de mercado, refletindo o comportamento do simulador dentro do testador visual de MetaTrader.

Média de Martingale

  • Ativar Enable Martingale permite que o painel faça pedidos de média quando o parâmetro Martingale é alternado para true.
  • A estratégia verifica a posição ativa:
    • Posição longa: Se o preço de venda atual estiver pelo menos Martingale Step (points) abaixo do preço de compra mais baixo preenchido, uma nova ordem de compra será enviada.
    • Posição curta: Se o preço de compra atual estiver pelo menos Martingale Step (points) acima do preço de venda preenchido mais alto, uma nova ordem de venda será emitida.
  • Cada volume médio de pedidos é igual a Order Volume × Martingale Multiplier^N, onde N é o número de entradas consecutivas na direção atual.
  • Quando o martingale está ativo, a meta de lucro é recalculada para o preço de entrada médio ponderado mais/menos Martingale TP Offset (points) para cobrir o rebaixamento acumulado.

Módulo de parada móvel

  • Enable Trailing ativa um trailing stop de proteção que segue o melhor preço mais recente.
  • O trailing stop começa a Trailing Stop (points) de distância do preço de mercado e avança somente depois que o preço melhora em pelo menos Trailing Step (points).
  • Se o preço de mercado ultrapassar o nível final, a estratégia fecha imediatamente toda a posição com uma ordem de mercado oposta.

Stop-loss e take-profit

  • Stop Loss (points) e Take Profit (points) reproduzem os controles básicos de risco do consultor especialista original.
  • Para posições longas, o stop é colocado abaixo do preço médio de entrada, enquanto o take-profit fica acima. Para posições curtas, ambos os níveis são espelhados.
  • As saídas protetoras são executadas com ordens de mercado, portanto a estratégia permanece compatível com qualquer conector suportado por StockSharp.

Parâmetros

Parâmetro Descrição Padrão
Order Volume Tamanho base para ordens manuais de mercado. 1
Stop Loss (points) Distância até a parada de proteção. Zero desativa o stop loss. 500
Take Profit (points) Distância até o alvo protetor. Zero desativa o take-profit. 500
Enable Trailing Liga/desliga o módulo de parada móvel. true
Trailing Stop (points) Distância entre o preço e o trailing stop. 50
Trailing Step (points) Movimento favorável mínimo necessário para avançar o trailing stop. 20
Enable Martingale Permite calcular a média de pedidos controlados pelo botão Martingale. true
Martingale Multiplier Multiplicador de volume usado para cada negociação média adicional. 1.2
Martingale Step (points) Movimento adverso necessário antes que uma ordem de média seja permitida. 150
Martingale TP Offset (points) Compensação adicional aplicada ao nível médio de take-profit. 50
Buy Defina como true para enviar uma ordem de compra de mercado (redefinições automáticas). false
Sell Defina como true para enviar uma ordem de venda a mercado (redefinições automáticas). false
Martingale Defina como true para avaliar e colocar uma ordem média (redefinições automáticas). false

Dicas de uso

  1. Anexe a estratégia a um instrumento, defina Order Volume e inicie-a no modo testador ou ao vivo.
  2. Use os alternadores Buy / Sell para simular cliques de botão no painel MetaTrader.
  3. Após a primeira negociação, acione a alternância Martingale sempre que o preço se mover contra a posição. A estratégia verifica a distância do preço e aumenta o volume se as condições forem atendidas.
  4. Ajuste os parâmetros de trilha e risco para replicar o comportamento do EA original ou para experimentar configurações alternativas.

Notas

  • A estratégia depende dos dados do Nível 1 (melhor oferta/venda e última negociação) para avaliar as condições do mercado.
  • Todos os comentários dentro do código C# estão em inglês, mantendo a consistência com as diretrizes do repositório.
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;
	}
}