Открыть на GitHub

Стратегия Martini Martingale

Данная стратегия реализует хеджирующий мартингейл. Она выставляет стоп-заявки с обеих сторон от текущей цены и удваивает размер позиции в противоположном направлении при движении рынка против открытой позиции на заданный шаг. Все сделки закрываются, когда накопленная прибыль превышает целевое значение.

Детали

  • Вход:
    • Устанавливаются buy stop выше и sell stop ниже рынка на расстояние Step.
    • При исполнении одной из заявок противоположная отменяется.
  • Управление позицией:
    • Отслеживается цена последней исполненной заявки.
    • Если цена движется против позиции на Step * orderCount, отправляется рыночная заявка в противоположную сторону с двойным объемом.
  • Выход:
    • Все позиции закрываются при достижении прибыли ProfitClose.
  • Длинные/короткие: Да, обе стороны.
  • Стопы: Стоп-заявки используются только для входа; стоп-лосс не применяется.
  • Индикаторы: Нет.
  • Фильтры: Нет.

Параметры

  • Step – шаг цены в абсолютных единицах.
  • ProfitClose – прибыль для закрытия всех позиций.
  • InitialVolume – начальный объем первой сделки.
  • CandleType – тип свечей для обновления цены.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Martingale grid using real conditional entries linked by client-side cancellation.
/// </summary>
public class MartiniMartingaleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _step;
	private readonly StrategyParam<decimal> _profitClose;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<DataType> _candleType;

	private Order _buyStopOrder;
	private Order _sellStopOrder;
	private Order _martingaleOrder;
	private long? _countedOrderId;
	private decimal _lastExecutionPrice;
	private decimal _lastLegVolume;
	private int _orderCount;
	private bool _closingCycle;
	private decimal _cyclePnlBase;

	public decimal Step { get => _step.Value; set => _step.Value = value; }
	public decimal ProfitClose { get => _profitClose.Value; set => _profitClose.Value = value; }
	public decimal InitialVolume { get => _initialVolume.Value; set => _initialVolume.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	private bool IsMartingaleWorking => _martingaleOrder is { State: not (OrderStates.Done or OrderStates.Failed) };

	public MartiniMartingaleStrategy()
	{
		_step = Param(nameof(Step), 10m).SetGreaterThanZero();
		_profitClose = Param(nameof(ProfitClose), 10m).SetGreaterThanZero();
		_initialVolume = Param(nameof(InitialVolume), 0.1m).SetGreaterThanZero();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();
		ResetCycleState();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ResetCycleState();
		// An OHLC-only replay cannot order the two stop activations and a client cancellation.
		// Build the signal bars from real trades so conditional entries use the same tick clock.
		var subscription = new Subscription(CandleType, Security);
		subscription.MarketData.BuildMode = MarketDataBuildModes.Build;
		subscription.MarketData.BuildFrom = DataType.Ticks;
		SubscribeCandles(subscription).Bind(ProcessCandle).Start();
	}

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

		if (Position == 0m)
		{
			if (!_closingCycle && _buyStopOrder is null && _sellStopOrder is null)
				PlaceInitialStops(candle.ClosePrice);

			return;
		}

		if (!_closingCycle && PnL - _cyclePnlBase >= ProfitClose)
		{
			_closingCycle = true;
			CancelInitialStops();
			CloseNetPosition();
			return;
		}

		if (_closingCycle || IsMartingaleWorking || _lastLegVolume <= 0m || _orderCount <= 0)
			return;

		var adverseDistance = Step * _orderCount;

		if (Position > 0m && candle.LowPrice <= _lastExecutionPrice - adverseDistance)
			PlaceMartingale(Sides.Sell, _lastLegVolume * 2m);
		else if (Position < 0m && candle.HighPrice >= _lastExecutionPrice + adverseDistance)
			PlaceMartingale(Sides.Buy, _lastLegVolume * 2m);
	}

	private void PlaceInitialStops(decimal center)
	{
		_cyclePnlBase = PnL;

		_buyStopOrder = CreateStopOrder(Sides.Buy, center + Step, InitialVolume, "Martini initial buy stop");
		_sellStopOrder = CreateStopOrder(Sides.Sell, center - Step, InitialVolume, "Martini initial sell stop");

		RegisterOrder(_buyStopOrder);
		RegisterOrder(_sellStopOrder);
	}

	private Order CreateStopOrder(Sides side, decimal activationPrice, decimal volume, string comment)
		=> new()
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Conditional,
			Condition = new StopOrderCondition { ActivationPrice = activationPrice },
			Side = side,
			Volume = volume,
			Comment = comment,
		};

	private void PlaceMartingale(Sides side, decimal volume)
	{
		volume = NormalizeVolume(volume);
		if (volume <= 0m)
			return;

		_martingaleOrder = side == Sides.Buy ? BuyMarket(volume) : SellMarket(volume);
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Order is null || trade.Trade is null)
			return;

		if (_closingCycle)
		{
			if (Position == 0m)
			{
				_closingCycle = false;
				ResetCycleState(keepPnlBase: false);
			}
			return;
		}

		if (trade.Order.Type == OrderTypes.Conditional)
		{
			if (trade.Order.Side == Sides.Buy)
				CancelIfActive(_sellStopOrder);
			else
				CancelIfActive(_buyStopOrder);

			_buyStopOrder = null;
			_sellStopOrder = null;
		}

		_lastExecutionPrice = trade.Trade.Price;

		// A leg filled in several trades is still one order of its full volume.
		if (_countedOrderId != trade.Order.TransactionId)
		{
			_countedOrderId = trade.Order.TransactionId;
			_lastLegVolume = trade.Order.Volume;
			_orderCount++;
		}
	}

	protected override void OnOrderChanged(Order order)
	{
		base.OnOrderChanged(order);

		if (order is null || order.State != OrderStates.Done)
			return;

		if (_buyStopOrder is not null && order.TransactionId == _buyStopOrder.TransactionId && order.Balance == order.Volume)
			_buyStopOrder = null;

		if (_sellStopOrder is not null && order.TransactionId == _sellStopOrder.TransactionId && order.Balance == order.Volume)
			_sellStopOrder = null;
	}

	private void CancelInitialStops()
	{
		CancelIfActive(_buyStopOrder);
		CancelIfActive(_sellStopOrder);
		_buyStopOrder = null;
		_sellStopOrder = null;
	}

	private void CancelIfActive(Order order)
	{
		if (order is { State: OrderStates.Active })
			CancelOrder(order);
	}

	private void CloseNetPosition()
	{
		if (Position > 0m)
			SellMarket(Math.Abs(Position));
		else if (Position < 0m)
			BuyMarket(Math.Abs(Position));
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = Math.Floor(volume / step) * step;
		return volume;
	}

	private void ResetCycleState(bool keepPnlBase = false)
	{
		_buyStopOrder = null;
		_sellStopOrder = null;
		_martingaleOrder = null;
		_countedOrderId = null;
		_lastExecutionPrice = 0m;
		_lastLegVolume = 0m;
		_orderCount = 0;
		_closingCycle = false;
		if (!keepPnlBase)
			_cyclePnlBase = PnL;
	}
}