GitHub で見る

ストップハンター戦略

概要

  • MetaTrader 4 エキスパート アドバイザー Stop Hunter を StockSharp の高レベル戦略フレームワークに移植します。
  • ラウンドナンバーブレイクアウトに焦点を当てます。アルゴリズムは、右端の Zeroes の桁がゼロである価格レベルを常に検索し、それらのしきい値のすぐ内側に逆指値注文を配置します。
  • オリジナルの EA で使用されていた「仮想」リスク管理を再現し、内部でエグジットを監視することで、テイクプロフィットとストップロスのレベルをブローカーから隠蔽します。
  • ソース コードの 2 段階のスケーリング ロジックを実装します。ポジションの最初の部分は最初のターゲットの後でクローズされ、残りは 2 倍の距離を追跡します。

データフローとサブスクリプション

  1. OnStartedレベル 1 データ (SubscribeLevel1().Bind(ProcessLevel1)) をサブスクライブします。最適な買値/売値ストリームのみが必要です。キャンドルやインジケーターは使用されません。
  2. すべての更新では最新の買値と売値が保存され、戦略がオンラインになり取引が許可されると意思決定エンジンがトリガーされます。
  3. オプションのチャート領域は、チャートを有効にしてストラテジーを実行するときに独自の取引を視覚化するために作成されます。

発注ロジック

  • ラウンドレベル検出
    • 商品価格ステップ (Security.PriceStep) を MQL Point の類似物として使用します。
    • ラウンドステップ長を計算します: roundStep = PriceStep * 10^Zeroes
    • 入札額 (Math.Ceiling(bid / roundStep) * roundStep) を上回る次のラウンド数を計算します。
    • アスクがすでにバッファ内にあるときにレベルを調整し、現在のスプレッドに近すぎる注文の送信を回避する元のガードを反映します。
    • LevelB の 1 ラウンドステップ下の下位ラウンド レベル (LevelS) を導出し、入札に対して同じ安全調整を実行します。
  • 保留中の注文
    • 既存の注文が存在せず、ロング取引が有効で、オープンなショート ポジションがない場合は、バイストップLevelB - DistancePoints * PriceStep に配置します。
    • 空売り取引が許可され、買いポジションが存在しない場合、売りストップLevelS + DistancePoints * PriceStep に対称的に配置します。
    • 計算されたラウンド目標が前進するか、価格が 1 ラウンドステップ + DistancePoints * 50 だけ離れるたびに、古い未決注文をキャンセルします。これは、MQL バージョンのクリーンアップ ロジックと一致します。
    • アクティブなスロットの総数 (ポジション + 未決注文) を MaxLongPositions + MaxShortPositions 以内に保ちます。

仮想出口管理

  • 平均エントリー価格と現在のポジション量を追跡します。
  • 2 つの整数アキュムレータ (_takeProfitExtension_stopLossExtension) を使用して、元の隠しバッファを再現します。
    • 最初の利益目標: ビッド/アスクがポジションを有利にして TakeProfitPoints * PriceStep に達したときにポジションの半分をクローズします。
    • 最初の部分エグジットの後、利益と逆指値の両方の距離がさらに TakeProfitPoints/StopLossPoints だけ延長され、「2 番目の取引」ステージがアクティブになります。
    • 最終終了: 2 倍のターゲットに到達したとき、または 2 倍のストップロス距離に達したときに、残りのボリュームを閉じます。
  • BuyMarket または SellMarket を使用して市場でクローズし、ブローカー側のストップロス注文の代わりに市場クローズを発行した EA をミラーリングします。
  • 競合する注文を削除した元のループと同様に、ヘッジを避けるためにポジションがオープンされるたびに反対側の保留中のストップを削除します。

資金管理

  • EA の Call_MM() 関数を再実装します: volume = balance / 100000 * RiskPercent
  • 計算された音量を MinimumVolumeMaximumVolume の間に固定し、楽器の音量ステップ (または MinimumVolume に応じて小数点以下 2/1/0) に四捨五入します。
  • 部分出口では、現在のポジションサイズを再利用して、出来高ステップを考慮しながら半出来高の終値を計算します。

実装メモ

  • StockSharp の高レベル API (BuyStopSellStopBuyMarketSellMarket、レベル 1 バインディング) のみを使用します。直接のコネクタ呼び出しやインジケーターの収集は必要ありません。
  • ResetState() を使用してリセット後の内部状態を維持し、リポジトリのガイドラインに従ってインデントにタブが使用されるようにします。
  • ガード句 (IsFormedAndOnlineAndAllowTrading) は、ストラテジーが完全に初期化される前に注文を送信することを防ぎます。
  • OnOwnTradeReceived は、SecondTrade フラグを更新する前に、正常終了を確認した MQL チェックをミラーリングします。
  • OnOrderChanged は、注文がキャンセルまたは拒否されたときにハンドルが古くならないように参照をクリアします。

MQL バージョンとの違い

  • ネッティング モデル: StockSharp 戦略は単一のネット ポジションで動作します。デフォルトのパラメータは依然として EA (1 つのロングスロットと 1 つのショートスロット) を模倣していますが、複数の同時チケットへのスケールインはネットエクスポージャを超えてサポートされていません。
  • リスク計算では、AccountFreeMargin の代わりに Portfolio.CurrentValue (BeginValue へのフォールバック) を使用し、マルチアセット環境で移植可能な近似値を提供します。
  • 新しい取引が開始されると仮想ストップ/テイクプロフィット距離がきれいにリセットされ、過去の EA コードに存在する累積バグが回避されます。
  • すべてのコメントとドキュメントは英語で書かれていますが、README ファイルには、プロジェクト ガイドラインの要求に応じて戦略がロシア語と中国語で追加的に説明されています。

パラメーター

名前 デフォルト 説明
Zeroes 2 価格がラウンドレベルとみなされるためにはゼロでなければならない右側の数字。
DistancePoints 15 ラウンドレベルとストップエントリーの間のオフセット(価格ポイント単位)。
TakeProfitPoints 15 隠れたテイクプロフィットディスタンス(ポイント単位)。 2段目増築にも再利用。
StopLossPoints 15 ポイント単位の非表示ストップロス距離 (最初のスケールアウト後に 2 倍)。
EnableLongOrders 本当の バイストップの配置を有効にします。
EnableShortOrders 本当の 売りストップの配置を有効にします。
RiskPercent 5 未決注文の規模を決定するために使用される資本の割合。
MinimumVolume 0.1 四捨五入後の最小注文サイズ。
MaximumVolume 30 計算されたボリュームのキャップ。
MaxLongPositions 1 ロング スロットの最大数 (ポジション + 保留中)。
MaxShortPositions 1 ショート スロットの最大数 (ポジション + 保留中)。

使用のヒント

  1. 元のエキスパート アドバイザーが使用した MQL Point の定義と価格ステップが一致する商品を選択します。小数ピップを含む外国為替ペアには通常、Zeroes = 2 が必要です。
  2. ブローカーのティックサイズとボリュームステップを監視します。 MinimumVolume を調整すると、丸めロジックが交換制約と一致するようになります。
  3. エグジットは仮想的なものであるため、ストップロス条件を見逃さないように戦略を常にオンラインに保ってください。取引所側のリスク管理が必要な場合は、StockSharp の StartProtection() との組み合わせを検討してください。
  4. トレーダーがさまざまなチームと共有できるローカライズされた説明については、ロシア語と中国語の README バージョンを確認してください。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Stop Hunter: EMA crossover with ATR trailing stops.
/// </summary>
public class StopHunterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private decimal _bestPrice;

	public StopHunterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetDisplay("Fast EMA Length", "Fast EMA period.", "Indicators");
		_slowEmaLength = Param(nameof(SlowEmaLength), 30)
			.SetDisplay("Slow EMA Length", "Slow EMA period.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
	public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

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

		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _bestPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _bestPrice = 0;
		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, atr, ProcessCandle).Start();
		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close > _bestPrice) _bestPrice = close;
			if (close <= _bestPrice - atrVal * 2m || (fastVal < slowVal && _prevFast >= _prevSlow)) { SellMarket(); _entryPrice = 0; _bestPrice = 0; }
		}
		else if (Position < 0)
		{
			if (close < _bestPrice) _bestPrice = close;
			if (close >= _bestPrice + atrVal * 2m || (fastVal > slowVal && _prevFast <= _prevSlow)) { BuyMarket(); _entryPrice = 0; _bestPrice = 0; }
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow) { _entryPrice = close; _bestPrice = close; BuyMarket(); }
			else if (fastVal < slowVal && _prevFast >= _prevSlow) { _entryPrice = close; _bestPrice = close; SellMarket(); }
		}
		_prevFast = fastVal; _prevSlow = slowVal;
	}
}