在 GitHub 上查看

MartinGale 突破策略

概览

MartinGale Breakout Strategy 是从 MetaTrader 4 专家顾问 MartinGaleBreakout 移植到 StockSharp 的策略。策略监控选定周期内的价格区间,一旦发现异常放大的 K 线并出现单边收盘,就顺势开仓。若浮亏超过设定阈值,将进入恢复模式,通过拉大止盈距离来补回之前的亏损,模拟原始 EA 的马丁式资金管理。

策略默认使用 15 分钟 K 线,并持续维护最近 11 根已完成的 K 线。当当前 K 线的区间大于前十根 K 线平均区间的三倍且收盘价位于主导半区时,生成突破信号并在该方向开仓。仓位大小根据目标盈利金额和品种的最小报价单位计算,同时遵守可用资金比例限制。

交易流程

  1. 订阅指定类型的 K 线序列。
  2. 保存最近 11 根已完成的 K 线并计算平均波动区间。
  3. 当满足“上涨突破”条件(区间放大且收阳)时开多单。
  4. 当满足“下跌突破”条件(区间放大且收阴)时开空单。
  5. 开仓前确保没有持仓,且预计占用资金不超过账户余额的限定百分比。
  6. 持仓期间监控浮动盈亏,达到止盈或止损阈值时平仓并重置目标。
  7. 若触发止损:
    • 市价平掉当前仓位。
    • 将止盈金额增加至覆盖亏损。
    • 将止损额度调整为最大允许值并进入恢复模式。
  8. 恢复模式达到新的止盈目标后,恢复默认参数继续交易。

参数

参数 说明 默认值
TakeProfitPoints 基础止盈距离(以最小价格单位计)。 50
BalancePercentageAvailable 单笔交易可使用的账户余额比例上限。 50%
TakeProfitBalancePercent 止盈目标占账户余额的百分比。 0.1%
StopLossBalancePercent 触发恢复模式前允许的最大回撤比例。 10%
StartRecoveryFactor 开启恢复模式时使用的止损比例系数。 0.2
TakeProfitPointsMultiplier 恢复模式下的止盈距离倍数。 1
CandleType 用于计算突破的 K 线类型。 15 分钟

风险控制

  • 根据目标盈利金额、最小跳动价位以及每跳价值计算仓位大小。
  • 仓位会根据交易所的最小、最大以及步进要求进行规范化处理。
  • 估算开仓所需资金,若超过允许的余额比例则放弃入场。
  • 恢复模式只维护单一仓位,通过拉大止盈距离来逐步弥补亏损,避免无限加仓。

提示

  • 策略需要可用的投资组合数据来评估余额,请确保连接到实际或仿真账户。
  • 手续费通过浮动盈亏间接反映,无需单独设置。
  • 全部操作采用市价单实现,不会创建挂单。
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy with martingale-style recovery.
/// Detects abnormally large candles relative to recent history and enters in the breakout direction.
/// After a stop-loss, enters recovery mode with a wider take-profit target.
/// </summary>
public class MartinGaleBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _requiredHistory;
	private readonly StrategyParam<decimal> _breakoutFactor;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<decimal> _recoveryMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _ranges = new();
	private decimal _entryPrice;
	private Sides? _entrySide;
	private bool _recovering;

	public int RequiredHistory
	{
		get => _requiredHistory.Value;
		set => _requiredHistory.Value = value;
	}

	public decimal BreakoutFactor
	{
		get => _breakoutFactor.Value;
		set => _breakoutFactor.Value = value;
	}

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

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

	public decimal RecoveryMultiplier
	{
		get => _recoveryMultiplier.Value;
		set => _recoveryMultiplier.Value = value;
	}

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

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

		_breakoutFactor = Param(nameof(BreakoutFactor), 2.5m)
			.SetDisplay("Breakout Factor", "Multiplier for abnormal candle detection", "General");

		_takeProfitPct = Param(nameof(TakeProfitPct), 0.5m)
			.SetDisplay("TP %", "Take profit percent of entry", "Trading");

		_stopLossPct = Param(nameof(StopLossPct), 0.3m)
			.SetDisplay("SL %", "Stop loss percent of entry", "Trading");

		_recoveryMultiplier = Param(nameof(RecoveryMultiplier), 1.5m)
			.SetDisplay("Recovery Mult", "TP multiplier during recovery", "Trading");

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

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

		_ranges.Clear();
		_entryPrice = 0;
		_entrySide = null;
		_recovering = 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 close = candle.ClosePrice;
		var range = candle.HighPrice - candle.LowPrice;

		// Check exit
		if (Position != 0 && _entryPrice > 0)
		{
			var tpPct = _recovering ? TakeProfitPct * RecoveryMultiplier : TakeProfitPct;

			if (_entrySide == Sides.Buy)
			{
				var pnl = (close - _entryPrice) / _entryPrice * 100m;
				if (pnl >= tpPct || pnl <= -StopLossPct)
				{
					var wasLoss = pnl < 0;
					SellMarket();
					_entryPrice = 0;
					_entrySide = null;
					_recovering = wasLoss;
					AddRange(range);
					return;
				}
			}
			else if (_entrySide == Sides.Sell)
			{
				var pnl = (_entryPrice - close) / _entryPrice * 100m;
				if (pnl >= tpPct || pnl <= -StopLossPct)
				{
					var wasLoss = pnl < 0;
					BuyMarket();
					_entryPrice = 0;
					_entrySide = null;
					_recovering = wasLoss;
					AddRange(range);
					return;
				}
			}
		}

		// Entry - only when flat
		if (Position == 0 && _ranges.Count >= RequiredHistory)
		{
			decimal sum = 0;
			for (int i = 0; i < _ranges.Count; i++)
				sum += _ranges[i];
			var avgRange = sum / _ranges.Count;

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

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

		AddRange(range);
	}

	private void AddRange(decimal range)
	{
		_ranges.Add(range);
		while (_ranges.Count > RequiredHistory)
			_ranges.RemoveAt(0);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_ranges.Clear();
		_entryPrice = 0;
		_entrySide = null;
		_recovering = false;

		base.OnReseted();
	}
}