GitHub で見る

Martin Martingale戦略

概要

この戦略は、現在の価格周辺にヘッジ型マーチンゲールグリッドを構築することで、MQLのオリジナル「Martin」エキスパートアドバイザーの動作を再現します。バスケット全体の累積利益が設定されたターゲットに達するまで、各反転時に取引量を倍増させながら、ロングとショートのポジションを継続的に交互に切り替えます。ローソク足は意思決定ロジックのドライバーとしてのみ使用され、実際の約定はStockSharpの高レベルAPIが公開する成行注文とストップ注文に依存します。

動作原理

  1. 起動時に、戦略はEntryOffsetPointsStepPointsパラメーターを絶対価格距離に変換するためにインストゥルメントのPriceStepを読み取ります。価格ステップが欠如している場合、値1が仮定されます。
  2. オープンポジションも有効なマーチンゲールサイクルもない場合、戦略は最後のクローズ価格周辺に買いストップと売りストップを配置します。オフセットはEntryOffsetPoints * PriceStepで、元のMQLコードで使用される10ポイント距離と一致します。
  3. いずれかのストップ注文が約定すると、反対側の保留中の注文がキャンセルされます。約定がマーチンゲールシーケンスの最初のトレードを定義します。戦略はその価格、量、方向を保存し、内部レベルカウンターを1に設定します。
  4. 後続の各ローソク足クローズ時に、現在のクローズ価格が最後に約定した注文の価格と比較されます。市場がその注文に対して少なくともmartingaleLevel * StepPoints * PriceStep動いた場合、前のトレードの2倍の量で反対方向に成行注文が送信されます。最終トレード情報は各実行後に更新されます。
  5. 未実現利益はPnL + Position * (closePrice - PositionPrice)として評価されます。この集計利益がProfitTargetパラメーターを超えると、戦略はバスケット内のすべてのポジションをフラット化するためにCloseAll()を送信し、残りのすべての注文をキャンセルして、新しいストップ注文のペアを配置できるようにサイクルをリセットします。
  6. すべてのポジションが手動でクローズされると、同じリセットが自動的に行われます。内部カウンターがクリアされ、次のローソク足で新しいストップ注文が作成されます。

このワークフローは、元のエキスパートアドバイザーの交互買い/売りロジックを反映しながら、実装をStockSharpの高レベルAPI内に完全に保ちます。

パラメーター

  • StepPoints – 次の平均化注文の反転閾値を計算するために使用される価格ステップ数。デフォルト10で最適化可能。
  • EntryOffsetPoints – 初期買い/売りストップ注文のオフセット(価格ステップ)。MQLバージョンと同様にデフォルト10ポイント。
  • ProfitTarget – マーチンゲールバスケット全体をクローズするために必要な絶対通貨利益。実現・未実現PnLの合計がこの値を超えると、すべてのポジションが清算されます。
  • CandleType – 戦略ロジックを駆動するためのローソク足サブスクリプション。デフォルトは1分足ですが、市場がサポートする任意のDataTypeを選択できます。

基本取引サイズは戦略のVolumeプロパティから取得されます。各新しい反転は、古典的なマーチンゲール方式でこのベースを2の累乗で乗算します。

実用的な注意事項

  • ブローカーの最小ロットサイズに合わせてVolumeを常に設定してください。倍増スキームにより急速にエクスポージャーが増加するため、リスク制限は外部で適用する必要があります。
  • 注文配置はローソク足クローズによって駆動されるため、ローソク足内の急速な価格変動により、ティックベースのMQLバージョンよりもわずかに遅いエントリーが引き起こされる可能性があります。ただし、ストップ注文はエントリー価格を元のロジックと一致させ続けます。
  • 戦略はデフォルトチャートエリアに価格ローソク足と自己トレードを描画し、視覚的な追跡を容易にします。
  • 自動ストップロスは使用されません。唯一の退出条件はProfitTargetであるため、大きな不利なトレンドのリスクをコントロールするためにインストゥルメントと時間軸を慎重に選択する必要があります。

MQLエキスパートとの違い

  • StockSharpはネットポジションを使用するため、各反転は単一のトレードで以前のエクスポージャーをクローズし、新しいものをオープンする成行注文で実行されます。バスケットの累積PnLはヘッジ実装と同一のままです。
  • ティック毎のロジックは、推奨される高レベルAPI使用法に準拠するために、シグナル評価のためのローソク足クローズに置き換えられました。
  • 部分的な約定を複数回処理するのを避けるために注文識別子が追跡され、量の倍増ロジックが一貫したままであることが保証されます。

これらの変更により、取引動作はソース戦略に忠実に保ちながら、StockSharpフレームワークに適応しています。

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _stepPoints;
	private readonly StrategyParam<int> _entryOffsetPoints;
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<int> _maxLevel;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _stepSize;
	private decimal _entryOffset;
	private decimal _lastTradePrice;
	private decimal _lastTradeVolume;
	private int _martingaleLevel;
	private Sides? _lastTradeSide;
	private bool _isClosing;
	private decimal? _initialPrice;

	/// <summary>
	/// Distance in points that defines when the next reversal is triggered.
	/// </summary>
	public int StepPoints
	{
		get => _stepPoints.Value;
		set => _stepPoints.Value = value;
	}

	/// <summary>
	/// Offset in points for the initial breakout entry.
	/// </summary>
	public int EntryOffsetPoints
	{
		get => _entryOffsetPoints.Value;
		set => _entryOffsetPoints.Value = value;
	}

	/// <summary>
	/// Aggregated profit required to close the entire martingale cycle.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum martingale doubling level before resetting.
	/// </summary>
	public int MaxLevel
	{
		get => _maxLevel.Value;
		set => _maxLevel.Value = value;
	}

	/// <summary>
	/// Candle type used to monitor the price.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
	/// </summary>
	public MartinMartingaleStrategy()
	{
		_stepPoints = Param(nameof(StepPoints), 10)
			.SetGreaterThanZero()
			.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
			;

		_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
			.SetGreaterThanZero()
			.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
			;

		_profitTarget = Param(nameof(ProfitTarget), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
			;

		_maxLevel = Param(nameof(MaxLevel), 5)
			.SetGreaterThanZero()
			.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetCycle();
		_isClosing = false;
		_initialPrice = null;
		_stepSize = 0;
		_entryOffset = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		UpdateStepSettings();

		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;

		UpdateStepSettings();

		if (_stepSize <= 0m || Volume <= 0m)
			return;

		var price = candle.ClosePrice;

		// If closing, flatten and wait
		if (_isClosing)
		{
			if (Position == 0)
			{
				_isClosing = false;
				ResetCycle();
			}
			return;
		}

		// If flat after a cycle, reset
		if (Position == 0 && _martingaleLevel > 0)
		{
			ResetCycle();
		}

		// Check profit target
		if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
		{
			_isClosing = true;
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();
			return;
		}

		// Max level reached -> close and reset
		if (_martingaleLevel >= MaxLevel && Position != 0)
		{
			_isClosing = true;
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();
			return;
		}

		// Initial entry: wait for breakout from first candle
		if (_martingaleLevel == 0 && Position == 0)
		{
			if (!_initialPrice.HasValue)
			{
				_initialPrice = price;
				return;
			}

			if (_entryOffset <= 0m)
				return;

			if (price >= _initialPrice.Value + _entryOffset)
			{
				BuyMarket();
				_lastTradePrice = price;
				_lastTradeVolume = Volume;
				_lastTradeSide = Sides.Buy;
				_martingaleLevel = 1;
				_initialPrice = null;
			}
			else if (price <= _initialPrice.Value - _entryOffset)
			{
				SellMarket();
				_lastTradePrice = price;
				_lastTradeVolume = Volume;
				_lastTradeSide = Sides.Sell;
				_martingaleLevel = 1;
				_initialPrice = null;
			}

			return;
		}

		if (_lastTradeSide is null || _martingaleLevel == 0)
			return;

		var threshold = _stepSize;

		if (_lastTradeSide == Sides.Buy)
		{
			if (price <= _lastTradePrice - threshold)
			{
				var nextVolume = _lastTradeVolume * 2m;
				var totalVolume = nextVolume + Math.Abs(Position);
				SellMarket();
				_lastTradePrice = price;
				_lastTradeVolume = nextVolume;
				_lastTradeSide = Sides.Sell;
				_martingaleLevel++;
			}
		}
		else
		{
			if (price >= _lastTradePrice + threshold)
			{
				var nextVolume = _lastTradeVolume * 2m;
				var totalVolume = nextVolume + Math.Abs(Position);
				BuyMarket();
				_lastTradePrice = price;
				_lastTradeVolume = nextVolume;
				_lastTradeSide = Sides.Buy;
				_martingaleLevel++;
			}
		}
	}

	private void UpdateStepSettings()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
		{
			priceStep = 1m;
		}

		_stepSize = StepPoints * priceStep;
		_entryOffset = EntryOffsetPoints * priceStep;
	}

	private void ResetCycle()
	{
		_martingaleLevel = 0;
		_lastTradePrice = 0m;
		_lastTradeVolume = 0m;
		_lastTradeSide = null;
	}
}