在 GitHub 上查看

Martingale Breakout 策略

概览

Martingale Breakout Strategy 是 MetaTrader 智能交易系统 MartinGaleBreakout.mq5 的 StockSharp 移植版本。策略等待 异常放大的突破 K 线,只在突破方向开出一笔市价单。原始 EA 通过“魔术号”区分仓位,而在 StockSharp 中同样的逻辑由 策略自身上下文提供,因此行为完全一致。

系统依赖两个核心思想:

  1. 突破检测:对每根完结 K 线计算区间(High - Low),并与前十根 K 线的平均区间比较。如果当前区间大于平均值的 三倍,同时 K 线实体占据整体区间的一半以上,则认为出现突破信号。
  2. 马丁格尔式恢复:持续跟踪浮动盈亏。一旦浮盈亏突破设定的亏损阈值,立即平掉全部仓位,并把下一次的盈利目标 提高到足以弥补刚才的亏损;当扩大的目标被实现后,各个阈值恢复到初始状态。

移植版本保留了所有资金管理参数:可用于保证金的账户比例、以余额百分比表示的止盈/止损以及恢复阶段放大止盈距离的 乘数。

交易逻辑

  1. 订阅指定类型的 K 线,并仅处理状态为 Finished 的蜡烛。
  2. 维护一个包含最近十个区间值的环形缓冲区,用作异常检测的参考平均值。
  3. 根据多头和空头的平均持仓价计算浮动盈亏;当浮盈亏达到获利目标或跌破止损阈值时,立即平仓并重置恢复状态。
  4. 只要策略已经持仓或连接状态不允许交易,就跳过新信号。
  5. 出现看涨突破时,根据当前目标收益计算下单手数,使预期利润等于目标。若处于恢复阶段,止盈距离会乘以 RecoveryPointsMultiplier,对应原 EA 中的 TP_Points_Multiplier
  6. 检查手数是否满足交易所限制(最小值、最大值和步长),并确认所需保证金没有超过允许使用的余额份额以及可用资金。 条件满足后发送买入市价单。
  7. 看跌突破时执行相同流程,改为发送卖出市价单。

该流程完整复现了原始 MetaTrader 策略的行为,包括触发止损后的恢复模式。

参数

参数 说明 默认值
TakeProfitPoints 入场价到止盈价之间的价格步数。 50
BalancePercentAvailable 单笔交易可占用的账户余额百分比。 50
TakeProfitPercentOfBalance 以余额百分比表示的目标盈利。 0.1
StopLossPercentOfBalance 以余额百分比表示的止损幅度。 10
RecoveryStartFraction 启动恢复模式前使用的止损比例。 0.1
RecoveryPointsMultiplier 恢复阶段用于放大止盈距离的乘数。 1
CandleType 用于生成信号的蜡烛数据类型(时间框架、Tick 蜡烛等)。 15 分钟蜡烛

其他说明

  • 手数计算遵循 CalcLotWithTP 的思路:根据目标盈利和预期价格波动得到所需手数,然后按照合约的手数步长归一化。
  • 保证金校验与 MQL 版本中的 CheckVolumeValue 以及余额比例限制保持一致,若所需保证金超过允许范围或账户可用资金, 则放弃下单。
  • 在强制平仓前会取消所有挂单,以匹配原脚本 CloseAllOrders 的行为。
  • 区间缓冲区仅保存十个值,与原程序遍历 iHigh/iLow 的结果等价,不需要额外的历史数据。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that detects abnormally large candles and enters in the breakout direction.
/// Uses a simple martingale recovery: after a losing trade, the next entry is taken more aggressively.
/// </summary>
public class MartingaleBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _lookback;
	private readonly StrategyParam<decimal> _breakoutMultiplier;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<DataType> _candleType;

	private readonly decimal[] _rangeBuffer = new decimal[10];
	private int _rangeBufferCount;
	private int _rangeBufferIndex;
	private decimal _rangeBufferSum;

	private decimal _entryPrice;
	private Sides? _entrySide;
	private bool _lastWasLoss;

	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	public decimal BreakoutMultiplier
	{
		get => _breakoutMultiplier.Value;
		set => _breakoutMultiplier.Value = value;
	}

	public decimal TakeProfitPct
	{
		get => _takeProfitPct.Value;
		set => _takeProfitPct.Value = value;
	}

	public decimal StopLossPct
	{
		get => _stopLossPct.Value;
		set => _stopLossPct.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public MartingaleBreakoutStrategy()
	{
		_lookback = Param(nameof(Lookback), 10)
			.SetDisplay("Lookback", "Number of candles for average range", "General");

		_breakoutMultiplier = Param(nameof(BreakoutMultiplier), 3m)
			.SetDisplay("Breakout Mult", "Multiplier above avg range for breakout", "General");

		_takeProfitPct = Param(nameof(TakeProfitPct), 1m)
			.SetDisplay("Take Profit %", "Take profit as percentage of entry price", "Trading");

		_stopLossPct = Param(nameof(StopLossPct), 0.5m)
			.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle type", "General");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_rangeBufferCount = 0;
		_rangeBufferIndex = 0;
		_rangeBufferSum = 0m;
		_entryPrice = 0m;
		_entrySide = null;
		_lastWasLoss = false;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		var closePrice = candle.ClosePrice;

		// Check exit conditions first
		if (Position != 0 && _entryPrice > 0)
		{
			var tp = _lastWasLoss ? TakeProfitPct * 1.5m : TakeProfitPct;
			var sl = StopLossPct;

			if (_entrySide == Sides.Buy)
			{
				var pnlPct = (closePrice - _entryPrice) / _entryPrice * 100m;
				if (pnlPct >= tp || pnlPct <= -sl)
				{
					_lastWasLoss = pnlPct < 0;
					SellMarket();
					_entryPrice = 0;
					_entrySide = null;
					UpdateRangeStatistics(candle);
					return;
				}
			}
			else if (_entrySide == Sides.Sell)
			{
				var pnlPct = (_entryPrice - closePrice) / _entryPrice * 100m;
				if (pnlPct >= tp || pnlPct <= -sl)
				{
					_lastWasLoss = pnlPct < 0;
					BuyMarket();
					_entryPrice = 0;
					_entrySide = null;
					UpdateRangeStatistics(candle);
					return;
				}
			}
		}

		// Entry logic - only when flat
		if (Position == 0)
		{
			var range = candle.HighPrice - candle.LowPrice;

			if (_rangeBufferCount >= _rangeBuffer.Length)
			{
				var avgRange = _rangeBufferSum / _rangeBuffer.Length;

				if (range > avgRange * BreakoutMultiplier)
				{
					var body = candle.ClosePrice - candle.OpenPrice;

					if (body > 0 && body > range * 0.4m)
					{
						// Bullish breakout
						BuyMarket();
						_entryPrice = closePrice;
						_entrySide = Sides.Buy;
					}
					else if (body < 0 && Math.Abs(body) > range * 0.4m)
					{
						// Bearish breakout
						SellMarket();
						_entryPrice = closePrice;
						_entrySide = Sides.Sell;
					}
				}
			}
		}

		UpdateRangeStatistics(candle);
	}

	private void UpdateRangeStatistics(ICandleMessage candle)
	{
		var range = candle.HighPrice - candle.LowPrice;

		if (_rangeBufferCount < _rangeBuffer.Length)
		{
			_rangeBuffer[_rangeBufferIndex] = range;
			_rangeBufferSum += range;
			_rangeBufferCount++;
			_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
			return;
		}

		_rangeBufferSum -= _rangeBuffer[_rangeBufferIndex];
		_rangeBuffer[_rangeBufferIndex] = range;
		_rangeBufferSum += range;
		_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		Array.Clear(_rangeBuffer);
		_rangeBufferCount = 0;
		_rangeBufferIndex = 0;
		_rangeBufferSum = 0m;
		_entryPrice = 0m;
		_entrySide = null;
		_lastWasLoss = false;

		base.OnReseted();
	}
}