GitHub で見る

Precipice Martin戦略 (C#)

概要

Precipice Martin戦略は、処理された各ローソク足のクローズ時に1つの成行注文を開く機械的なグリッドアプローチです。元のMetaTrader 5エキスパートアドバイザーは、各新しいバーに対称的な買いと売りのポジションを作成し、pipsで表現された静的なストップロスとテイクプロフィットのオフセットを使用して出口を管理しました。損失取引は次の注文サイズをマルチンゲール乗数で増加させ、利益取引はポジションサイズを最小ロットにリセットしました。

このC#ポートはStockSharpの高レベルAPIを使用して同じ高レベルロジックに従います。各完了ローソク足に対して戦略は:

  1. 既存のロングとショートのポジションを更新し、ローソク足の範囲が設定されたストップロスまたはテイクプロフィットレベルを突き破った場合はそれらをクローズします。
  2. フラット時、ソースロボットのデュアルエントリー動作をエミュレートしながら、StockSharpのネットポジション会計と互換性を保ちつつ(両方向が有効な場合)、連続した機会にロングとショートの市場ポジションを交互に開きます。
  3. オプションのマルチンゲールサイジングを適用し、連続した損失取引が設定された乗数によってボリュームを増加させます。
  4. ユーザー定義のpip距離からストップロスとテイクプロフィットのターゲットを計算し、インストゥルメントのティックサイズに基づいて絶対価格オフセットに変換します。

変換ノート

  • 元のEAは、両方のトグルが有効な場合、各新しいバーにロングとショートのポジションを開きました。StockSharpはデフォルトでネットポジションを使用するため、C#バージョンはネットポジションを即座にフラット化するのを避けるために、連続した機会に方向を交互に切り替えます。これにより、時間の経過とともに市場の両側が取引されることが保証されます。
  • ストップロスとテイクプロフィットの管理は、ローソク足の高値/安値が対応するレベルをトリガーしたかどうかを確認することで内部的に実行されます。レベルに達すると、戦略は成行注文を使用してポジションをクローズし、マルチンゲールロジックのために実現利益または損失を記録します。
  • ロット検証は、計算されたボリュームをexchangeのVolumeStepに丸め、最小と最大の境界を強制し、丸められた値がゼロになった場合に注文をキャンセルすることでMQL5のLotCheckルーチンを複製します。
  • マルチンゲールルーチンはCalculateLotを反映します:利益のない出口はすべて次の注文サイズをMartingaleCoefficientで乗算し、利益のある出口は乗数を1にリセットします。

パラメーター

パラメーター 説明
Use Buy ロングポジションの開設を有効にする。
Buy SL/TP (pips) ロング取引のストップロスとテイクプロフィット両方に使用される距離(pips)。値が0の場合、その側の出口が無効になる。
Use Sell ショートポジションの開設を有効にする。
Sell SL/TP (pips) ショート取引のストップロスとテイクプロフィット両方に使用される距離(pips)。
Use Martingale マルチンゲールポジションサイジングを切り替える。無効化されると各注文は最小ロットサイズを使用する。
Martingale Coefficient 利益のない各取引後に最小ロットに適用される乗数。
Candle Type 戦略が処理するローソク足の時間軸。デフォルトでは戦略は1分バーで動作するが、利用可能な任意の時間軸を選択できる。

取引ロジック

  1. Pip サイズ計算 – 戦略はインストゥルメントのティックサイズからpip値を導出します。分数pip(5桁のFXシンボル)で見積もられるインストゥルメントの場合、pipはMT5実装に合わせて10ティックとみなされます。
  2. エントリー選択Use BuyUse Sellの両方が有効な場合、戦略はフラット時にロングとショートのエントリーを交互に行います。1つの方向のみが有効な場合、すべての取引はその方向に従います。エントリーはローソク足が完了し、戦略がオンラインの直後に発動されます。
  3. ストップ/テイクレベル – 取引が開かれると、ストップロスとテイクプロフィットは選択されたpip距離を使用してエントリーを基準とした絶対価格として保存されます。値がゼロの場合、その方向の両方のレベルが無効になります。
  4. 出口処理 – 各完了ローソク足で高値/安値の値が確認されます。安値がロングストップを下回るか、高値がロングターゲットを超えると、ロングポジションがクローズされます。ショートの場合、ロジックは反転されます。出口はそのポジションの最後に記録されたボリュームを使用した成行注文で実行されます。
  5. マルチンゲールサイジング – 次の注文ボリュームは、現在のマルチンゲール乗数で乗算されたインストゥルメントの最小ロットと等しい。損失取引(損益分岐点の結果を含む)は乗数をMartingaleCoefficientで乗算し、利益取引はそれを1にリセットします。注文が送信される前にexchangeステップへのボリューム丸めが適用されます。
  6. 安全チェック – 丸められたボリュームがexchangeの最小ロット未満の場合、注文はスキップされ、元のEAがCheckVolumeで処理した「資金不足」エラーを防ぎます。

使用ガイドライン

  1. MT5で使用するチャート期間に合わせてCandle Typeで希望の時間軸を設定する。
  2. 希望するストップロスとテイクプロフィットの動作に合わせてpip距離を調整する。オフセットは絶対価格であるため、通貨での実際のストップはシンボルに依存することに注意する。
  3. リスク許容度に応じてマルチンゲールサイジングを有効または無効にする。連続した損失後にボリュームが指数関数的に成長するため、保守的な乗数を適用する。
  4. リアルタイムローソク足を提供するインストゥルメントに戦略を展開する。戦略は操作に完了したバーを必要とし、不完全なローソク足では取引しない。
  5. マルチンゲールがアクティブな場合、マージンの使用を監視する。StockSharpバージョンは両側が有効な場合に意図的に方向を交互に切り替えるため、任意の時点で1つのネットポジションのみが開いている。

MT5実装との違い

  • ネットポジション – 交互ロジックは元のアルゴリズムの同時ヘッジエントリーを置き換えます。真のヘッジ口座が必要な場合は、2つの戦略インスタンスを実行できます(1つはUse Buy、もう1つはUse Sell)。
  • 注文配置 – 保護注文はexchangeブックに配置されません。代わりに、戦略がストップまたはテイクレベルが超えられたことを検出すると成行注文で出口が実行されます。
  • 履歴スキャン – MT5スクリプトはすべての取引履歴を各ティックでスキャンすることでマルチンゲール係数を再計算しました。C#バージョンはオーバーヘッドを減らしながら動作を維持するために乗数を増分的に維持します。

リスク免責事項

マルチンゲールベースの戦略は、損失連続中に非常に大きなポジションを生成する可能性があり、口座リスク制限を超える可能性があります。ライブ展開前に常にシミュレートされたデータで戦略をテストし、選択した乗数とpip距離が取引インストゥルメントのボラティリティに適していることを確認してください。

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>
/// Grid style strategy that opens a position on every new bar with optional martingale sizing.
/// </summary>
public class PrecipiceMartinStrategy : Strategy
{
	private readonly StrategyParam<bool> _useBuy;
	private readonly StrategyParam<int> _buyStepPips;
	private readonly StrategyParam<bool> _useSell;
	private readonly StrategyParam<int> _sellStepPips;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<decimal> _martingaleCoefficient;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _martingaleMultiplier;
	private decimal? _longEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortEntryPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal _lastLongVolume;
	private decimal _lastShortVolume;
	private bool _preferLongEntry;

	public bool UseBuy
	{
		get => _useBuy.Value;
		set => _useBuy.Value = value;
	}

	public int BuyStepPips
	{
		get => _buyStepPips.Value;
		set => _buyStepPips.Value = value;
	}

	public bool UseSell
	{
		get => _useSell.Value;
		set => _useSell.Value = value;
	}

	public int SellStepPips
	{
		get => _sellStepPips.Value;
		set => _sellStepPips.Value = value;
	}

	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	public decimal MartingaleCoefficient
	{
		get => _martingaleCoefficient.Value;
		set => _martingaleCoefficient.Value = value;
	}

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

	public PrecipiceMartinStrategy()
	{
		_useBuy = Param(nameof(UseBuy), true)
			.SetDisplay("Use Buy", "Enable opening long positions", "Trading");
		_buyStepPips = Param(nameof(BuyStepPips), 89)
			.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading");
		_useSell = Param(nameof(UseSell), true)
			.SetDisplay("Use Sell", "Enable opening short positions", "Trading");
		_sellStepPips = Param(nameof(SellStepPips), 89)
			.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading");
		_useMartingale = Param(nameof(UseMartingale), true)
			.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing");
		_martingaleCoefficient = Param(nameof(MartingaleCoefficient), 1.6m)
			.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
			.SetGreaterThanZero();
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_martingaleMultiplier = 1m;
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastLongVolume = 0m;
		_lastShortVolume = 0m;
		_preferLongEntry = true;
	}

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

		// Calculate the pip size based on the instrument tick size.
		_pipSize = (Security?.PriceStep ?? 1m) * 10m;
		if (_pipSize <= 0m)
			_pipSize = Security?.PriceStep ?? 1m;
		if (_pipSize <= 0m)
			_pipSize = 1m;

		_martingaleMultiplier = 1m;

		// Subscribe to candle data and process every completed bar.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore unfinished candles because the original strategy trades on bar close.
		if (candle.State != CandleStates.Finished)
			return;

		// Manage exits before looking for new entries.
		var closedLong = TryCloseLong(candle);
		var closedShort = TryCloseShort(candle);

		// Do not open new trades while any position is still active.
		if (Position != 0)
			return;

		// Avoid immediate re-entry for a direction that has just closed on this bar.
		if (closedLong)
			return;
		if (closedShort)
			return;

		if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
			return;

		if (UseBuy && UseSell)
		{
			if (_preferLongEntry)
			{
				if (TryEnterLong(candle))
				{
					_preferLongEntry = false;
					return;
				}

				if (TryEnterShort(candle))
				{
					_preferLongEntry = false;
				}
			}
			else
			{
				if (TryEnterShort(candle))
				{
					_preferLongEntry = true;
					return;
				}

				if (TryEnterLong(candle))
				{
					_preferLongEntry = true;
				}
			}
		}
		else
		{
			if (UseBuy)
			{
				TryEnterLong(candle);
			}

			if (UseSell)
			{
				TryEnterShort(candle);
			}
		}
	}

	private bool TryEnterLong(ICandleMessage candle)
	{
		// Prevent duplicate long entries.
		if (_longEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new long.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		BuyMarket();

		_longEntryPrice = entryPrice;
		_lastLongVolume = volume;

		if (BuyStepPips > 0)
		{
			var offset = BuyStepPips * _pipSize;
			_longStopPrice = entryPrice - offset;
			_longTakePrice = entryPrice + offset;
		}
		else
		{
			_longStopPrice = null;
			_longTakePrice = null;
		}

		return true;
	}

	private bool TryEnterShort(ICandleMessage candle)
	{
		// Prevent duplicate short entries.
		if (_shortEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new short.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		SellMarket();

		_shortEntryPrice = entryPrice;
		_lastShortVolume = volume;

		if (SellStepPips > 0)
		{
			var offset = SellStepPips * _pipSize;
			_shortStopPrice = entryPrice + offset;
			_shortTakePrice = entryPrice - offset;
		}
		else
		{
			_shortStopPrice = null;
			_shortTakePrice = null;
		}

		return true;
	}

	private bool TryCloseLong(ICandleMessage candle)
	{
		if (!_longEntryPrice.HasValue)
			return false;

		var volume = Position;
		if (volume <= 0m)
			volume = _lastLongVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value;
		var takeHit = _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _longStopPrice!.Value : _longTakePrice!.Value;

		SellMarket();

		var pnl = (exitPrice - _longEntryPrice.Value) * volume;
		UpdateMartingale(pnl);

		ResetLongState();
		return true;
	}

	private bool TryCloseShort(ICandleMessage candle)
	{
		if (!_shortEntryPrice.HasValue)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			volume = _lastShortVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value;
		var takeHit = _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _shortStopPrice!.Value : _shortTakePrice!.Value;

		BuyMarket();

		var pnl = (_shortEntryPrice.Value - exitPrice) * volume;
		UpdateMartingale(pnl);

		ResetShortState();
		return true;
	}

	private decimal CalculateOrderVolume()
	{
		var minVolume = Security?.MinVolume ?? Volume;
		if (minVolume <= 0m)
			minVolume = 1m;

		var multiplier = UseMartingale ? _martingaleMultiplier : 1m;
		var volume = minVolume * multiplier;

		return AdjustVolume(volume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			var steps = Math.Truncate(volume / step.Value);
			volume = steps * step.Value;
		}

		var min = Security?.MinVolume;
		if (min.HasValue && min.Value > 0m && volume < min.Value)
			volume = 0m;

		var max = Security?.MaxVolume;
		if (max.HasValue && max.Value > 0m && volume > max.Value)
			volume = max.Value;

		return volume;
	}

	private void UpdateMartingale(decimal realizedPnl)
	{
		if (!UseMartingale)
		{
			_martingaleMultiplier = 1m;
			return;
		}

		// Reset the multiplier after profitable trades and scale up after losses.
		_martingaleMultiplier = realizedPnl > 0m
			? 1m
			: _martingaleMultiplier * MartingaleCoefficient;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_lastLongVolume = 0m;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastShortVolume = 0m;
	}
}