Ver en GitHub

Martingale Trade Simulator Strategy

Overview

MartingaleTradeSimulatorStrategy recreates the "Martingale Trade Simulator" expert advisor from MetaTrader inside the StockSharp framework. The strategy is a manual trading panel that lets a trader send immediate market orders, apply martingale-style averaging, and manage trailing protection without scripting additional automation. It reacts to parameter switches in real time, making it suitable for Strategy Tester experiments just like the original MQL robot.

How it works

Manual market buttons

  • Buy and Sell parameters act as virtual buttons. When either parameter is set to true, the strategy sends a market order with volume Order Volume and then automatically resets the parameter to false.
  • No pending orders are used — the strategy works entirely with market executions, mirroring the simulator behavior inside MetaTrader's visual tester.

Martingale averaging

  • Enabling Enable Martingale allows the panel to place averaging orders when the Martingale parameter is toggled to true.
  • The strategy checks the active position:
    • Long position: If the current ask price is at least Martingale Step (points) below the lowest filled buy price, a new buy order is sent.
    • Short position: If the current bid price is at least Martingale Step (points) above the highest filled sell price, a new sell order is issued.
  • Each averaging order volume equals Order Volume × Martingale Multiplier^N, where N is the number of consecutive entries in the current direction.
  • When martingale is active, the take-profit target is recalculated to the weighted average entry price plus/minus Martingale TP Offset (points) to cover accumulated drawdown.

Trailing stop module

  • Enable Trailing activates a protective trailing stop that follows the most recent best price.
  • The trailing stop starts at Trailing Stop (points) away from the market price and moves forward only after price improves by at least Trailing Step (points).
  • If the market price crosses the trailing level, the strategy immediately closes the entire position with an opposing market order.

Stop-loss and take-profit

  • Stop Loss (points) and Take Profit (points) reproduce the basic risk controls from the original expert advisor.
  • For long positions the stop is placed below the average entry price, while the take-profit sits above. For short positions both levels are mirrored.
  • Protective exits are executed with market orders, so the strategy stays compatible with any connector supported by StockSharp.

Parameters

Parameter Description Default
Order Volume Base size for manual market orders. 1
Stop Loss (points) Distance to the protective stop. Zero disables the stop-loss. 500
Take Profit (points) Distance to the protective target. Zero disables the take-profit. 500
Enable Trailing Turns the trailing stop module on/off. true
Trailing Stop (points) Distance between price and trailing stop. 50
Trailing Step (points) Minimal favorable move required to advance the trailing stop. 20
Enable Martingale Allows averaging orders controlled by the Martingale button. true
Martingale Multiplier Volume multiplier used for each additional averaging trade. 1.2
Martingale Step (points) Required adverse movement before an averaging order is allowed. 150
Martingale TP Offset (points) Additional offset applied to the averaged take-profit level. 50
Buy Set to true to send a market buy order (auto-resets). false
Sell Set to true to send a market sell order (auto-resets). false
Martingale Set to true to evaluate and place an averaging order (auto-resets). false

Usage tips

  1. Attach the strategy to an instrument, set Order Volume, and start it in tester or live mode.
  2. Use the Buy / Sell toggles to simulate button clicks from the MetaTrader panel.
  3. After the first trade, trigger the Martingale toggle whenever the price moves against the position. The strategy verifies the price distance and increases the volume if conditions are met.
  4. Adjust trailing and risk parameters to replicate the original EA's behavior or to experiment with alternative settings.

Notes

  • The strategy relies on Level1 data (best bid/ask and last trade) to evaluate market conditions.
  • All comments inside the C# code are in English, keeping consistency with the repository guidelines.
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;
	}
}