在 GitHub 上查看

Martini Martingale 策略

该策略实现对冲型马丁格尔。它在当前价格上下方放置止损单,当市场按指定步长朝不利方向移动时,以双倍数量在相反方向加仓。一旦累计利润超过设定目标,策略将平掉所有仓位。

细节

  • 入场条件:
    • 在距当前价 Step 的位置放置买入止损和卖出止损单。
    • 其中一单成交后,另一单被取消。
  • 仓位管理:
    • 记录最后一笔成交的价格。
    • 当价格相对持仓方向反向移动 Step * orderCount 时,以双倍手数在相反方向市价开单。
  • 出场条件:
    • 当浮动利润达到 ProfitClose 时平掉所有仓位。
  • 多空方向:双向。
  • 止损:仅用于初始入场,无传统止损。
  • 指标:无。
  • 过滤器:无。

参数

  • Step – 绝对价格步长。
  • ProfitClose – 触发平仓的利润阈值。
  • InitialVolume – 初始下单手数。
  • CandleType – 用于价格更新的K线类型。
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;
	}
}