Открыть на GitHub

Стратегия Martingale Trade Simulator

Общее описание

MartingaleTradeSimulatorStrategy переносит советник «Martingale Trade Simulator» из MetaTrader в экосистему StockSharp. Стратегия выступает в роли ручной панели управления: позволяет мгновенно отправлять рыночные заявки, запускать мартингейловое усреднение одной кнопкой и управлять трейлинг-стопом без дополнительного кода. Все переключатели реализованы через параметры и реагируют в режиме реального времени, что делает стратегию удобной для визуального тестирования.

Логика работы

Ручные рыночные заявки

  • Параметры Buy и Sell имитируют кнопки панели. При установке значения true стратегия отправляет рыночный ордер объёмом Order Volume и затем автоматически сбрасывает флаг обратно в false.
  • Используются только рыночные ордера, что полностью повторяет поведение исходного советника в визуальном тестере MetaTrader.

Мартингейл-усреднение

  • Опция Enable Martingale позволяет задействовать усреднение. Для проверки условий необходимо переключить параметр Martingale в true — после обработки он сбрасывается.
  • В зависимости от текущей позиции выполняется проверка:
    • Длинная позиция: если текущая цена Ask опустилась минимум на Martingale Step (points) ниже самой дешёвой покупки, отправляется дополнительный рыночный buy-ордер.
    • Короткая позиция: если текущая цена Bid поднялась минимум на Martingale Step (points) выше самой дорогой продажи, отправляется рыночный sell-ордер.
  • Объём каждой новой ступени равен Order Volume × (Martingale Multiplier)^N, где N — число последовательных входов в текущем направлении.
  • После активации мартингейла стратегия пересчитывает уровень тейк-профита относительно средней цены позиции и добавляет смещение Martingale TP Offset (points) для покрытия просадки.

Трейлинг-стоп

  • Параметр Enable Trailing включает автоматический трейлинг-стоп.
  • Стоп устанавливается на расстоянии Trailing Stop (points) от текущей цены и сдвигается только тогда, когда цена проходит вперёд не менее чем на Trailing Step (points).
  • При касании трейлинг-уровня позиция закрывается встречной рыночной заявкой целиком.

Стоп-лосс и тейк-профит

  • Параметры Stop Loss (points) и Take Profit (points) повторяют базовые защитные уровни оригинального советника.
  • Для лонгов стоп размещается ниже средней цены входа, тейк — выше; для шортов уровни зеркальны.
  • Защитные выходы исполняются рыночными заявками, что сохраняет совместимость с любыми коннекторами StockSharp.

Параметры

Параметр Описание Значение по умолчанию
Order Volume Базовый объём для ручных рыночных ордеров. 1
Stop Loss (points) Расстояние до стоп-лосса (0 — отключить). 500
Take Profit (points) Расстояние до тейк-профита (0 — отключить). 500
Enable Trailing Включить / выключить трейлинг-стоп. true
Trailing Stop (points) Расстояние от цены до трейлинг-стопа. 50
Trailing Step (points) Минимальный шаг продвижения трейлинга. 20
Enable Martingale Разрешить мартингейл по кнопке Martingale. true
Martingale Multiplier Множитель объёма для каждой ступени усреднения. 1.2
Martingale Step (points) Требуемое неблагоприятное движение цены перед усреднением. 150
Martingale TP Offset (points) Дополнительное смещение тейк-профита при мартингейле. 50
Buy Установите true, чтобы отправить рыночный buy-ордер (автосброс). false
Sell Установите true, чтобы отправить рыночный sell-ордер (автосброс). false
Martingale Установите true, чтобы выполнить проверку и при необходимости усредниться (автосброс). false

Рекомендации по использованию

  1. Выберите инструмент, задайте Order Volume и запустите стратегию в тестере или на реальном подключении.
  2. Для имитации ручных действий устанавливайте Buy или Sell в true — стратегия немедленно отправит рыночный ордер.
  3. Когда цена движется против открытой позиции, переключите Martingale в true, чтобы стратегия проверила условия и добавила усредняющий ордер с повышенным объёмом.
  4. Настраивайте трейлинг и защитные уровни, чтобы полностью повторить механику оригинального советника либо протестировать альтернативные параметры.

Примечания

  • Стратегия использует данные Level1 (лучшие цены и последняя сделка) для оценки ситуации на рынке.
  • Все комментарии в исходном коде написаны на английском языке в соответствии с требованиями репозитория.
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;
	}
}