GitHub で見る

マーティンゲイルブレイクアウト戦略

概要

MartinGale Breakout Strategy は、MetaTrader 4 エキスパート アドバイザー MartinGaleBreakout から変換されたブレイクアウト フォロー システムです。オリジナルのロボットは、異常に大きなローソク足を検出した後にポジションに入り、マーチンゲール形式の回復メカニズムを適用して以前の損失を取り戻します。この StockSharp ポートは、キャンドル サブスクリプションと資金管理パラメーターを備えた高レベルの戦略 API を使用して動作を再現します。

この戦略は、設定可能なローソク足シリーズを監視し、範囲が前の 10 バーの平均範囲の 3 倍以上であるローソク足を探します。このようなローソク足が一方向に強く閉じると、戦略はその方向に市場ポジションを開きます。設定可能なしきい値を超える損失でポジションがクローズされた場合、リカバリモードは実現したドローダウンを補うためにテイクプロフィットディスタンスを増加させます。

取引ロジック

  1. 選択したキャンドル シリーズ (デフォルトでは 15 分のキャンドル) を購読します。
  2. 異常なボラティリティを評価するために、直近の 11 本の完成したローソク足を維持します。
  3. 次の場合に強気のブレイクアウトを検出します。
    • 現在のローソク足は、以前の 10 本のローソク足の平均範囲の 3 倍の大きさです。
    • ローソク足はレンジの上半分で終了します。
  4. 対称条件を使用して弱気のブレイクアウトを検出します。
  5. 次の場合、ブレイクアウト方向に市場ポジションをオープンします。
    • 現在他に募集中のポジションはありません。
    • 推定資本エクスポージャが設定された残高パーセンテージを下回っています。
  6. 次の場合にポジションをクローズし、損益目標をリセットします。
    • 変動利益がテイクプロフィットしきい値に達します。
    • 変動損失がストップロスしきい値に達します。
  7. ストップロスが発生したら、リカバリモードに切り替えます。
    • 構成された乗数によってテイクプロフィットディスタンスを増加させます。
    • ストップロス制限を最大許容パーセンテージまで拡張します。
    • 次の目標に到達するまで取引を続け、その後、基本設定にリセットします。

パラメーター

名前 説明 デフォルト
TakeProfitPoints 商品ポイントで表されるベーステイクプロフィットディスタンス。 50
BalancePercentageAvailable 1 回の取引に割り当てることができる口座残高の最大シェア。 50%
TakeProfitBalancePercent 口座残高に対する目標利益の割合。 0.1%
StopLossBalancePercent 回復を引き起こす前の最大ドローダウン。 10%
StartRecoveryFactor リカバリモードをアクティブにする前に使用されるストップロスの一部。 0.2
TakeProfitPointsMultiplier 回復中にテイクプロフィットディスタンスに適用される乗数。 1
CandleType ブレイクアウト計算に使用されるローソク足シリーズ。 15-minute

ポジションサイジングとリスク管理

  • この戦略は、商品のティック サイズとティック値を使用して、設定された金銭的テイクプロフィットを達成するために必要なボリュームを計算します。
  • ボリュームは交換制約 (ステップ、最小、最大) に合わせて正規化されます。
  • 推定資本エクスポージャは、設定された残高パーセンテージを超えてはなりません。
  • リカバリモードでは、ポジションを単一のオープントレードに限定しながら、元のマーチンゲール動作をエミュレートし、損失後のテイクプロフィットターゲットを動的に拡大します。

注意事項

  • この戦略はポートフォリオのバランス情報に依存します。開始する前にポートフォリオ接続を使用して初期化してください。
  • コミッションの処理は、現在のポジションから得られる変動損益に焦点を当て、元の EA を反映しています。
  • 未決注文は使用されず、エントリーとエグジットは成行注文のみで実行されます。
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();
	}
}