GitHub で見る

WOC 0.1.2 モメンタム戦略

概要

この戦略は MetaTrader Expert Advisor "WOC.0.1.2" の StockSharp 高レベル版です。Level 1 のベスト bid/ask 更新を受信し、ask 側での素早い価格の連続を探します。ask 価格が限られた時間ウィンドウ内で設定可能な数の連続した高値または安値を印刷すると、戦略はブレイクアウトの方向に成行ポジションを建てます。一度に開けるポジションは 1 つだけで、元のコードの単一ポジション動作を反映します。

データと実行

  • マーケットデータ: Level 1 のベスト bid とベスト ask。アルゴリズムはローソク足やインジケーターを必要としません。
  • 実行: 成行注文。保護的な決済は戦略内で bid/ask 更新を確認することでエミュレートされます。

シグナルロジック

  1. 最新の ask 価格を追跡し、連続した新高値(上昇ストリーク)または新安値(下降ストリーク)が何回印刷されたかを測定します。
  2. 上昇または下降ストリークが SequenceLength に達したとき、ストリークの期間が SequenceTimeoutSeconds 秒以下かどうかを確認します。
  3. 下降ストリークが上昇ストリークより長い場合は売り注文を送信し、そうでなければ買い注文を送信します。この確認は、カウンターが最も高いストリークが方向を定義する元の MetaTrader ロジックを再現します。
  4. 次のシグナルがゼロから始まるように、各エントリー試行後にすべてのストリークカウンターをリセットします。

ポジション管理

  • 初期ストップ: エントリー後、戦略は直ちに現在の bid(ロングの場合)または ask(ショートの場合)から StopLossTicks 価格ステップ離れたストップロス価格を記録します。
  • トレーリングストップ: 価格がトレードの利益方向に TrailingStopTicks 価格ステップ以上移動すると、ストップが現在価格から少なくともトレーリング距離の 2 倍以上離れている限り、最新の bid/ask の TrailingStopTicks 後ろにストップを締めます。これは MQL Expert の 2 ステップトレーリング条件を再現します。
  • 決済実行: 追跡している bid/ask が保存されたストップレベルをクロスすると、成行注文でポジションを決済します。決済後、内部状態は新しいストリークを受け入れるためにリセットされます。

ボリューム管理

2 つのポジションサイジングモードがサポートされています:

  • 固定ロット: LotSize パラメーターを絶対的な注文ボリュームとして使用します。
  • 自動ロット: UseAutoLotSizing を有効にして、口座残高をボリュームティアにマッピングします。残高は Portfolio.CurrentValue から取得し、現在の値が利用できない場合は Portfolio.BeginValue にフォールバックします。
残高(より大きい) ボリューム
0(デフォルト) LotSize
200 0.04
300 0.05
400 0.06
500 0.07
600 0.08
700 0.09
800 0.10
900 0.20
1 000 0.30
2 000 0.40
3 000 0.50
4 000 0.60
5 000 0.70
6 000 0.80
7 000 0.90
8 000 1.00
9 000 2.00
10 000 3.00
11 000 4.00
12 000 5.00
13 000 6.00
14 000 7.00
15 000 8.00
20 000 9.00
30 000 10.00
40 000 11.00
50 000 12.00
60 000 13.00
70 000 14.00
80 000 15.00
90 000 16.00
100 000 17.00
110 000 18.00
120 000 19.00
130 000 20.00

パラメーター

  • StopLossTicks – 価格ステップで測定したストップロス距離。
  • TrailingStopTicks – 価格ステップで測定したトレーリング距離(ゼロにするとトレーリングを無効化)。
  • SequenceLength – トレードに入る前に必要な連続 ask 移動回数。
  • SequenceTimeoutSeconds – ストリークの最大継続時間(秒単位)。
  • LotSize – 自動ロットサイジングが無効な場合に使用する固定注文サイズ。
  • UseAutoLotSizing – 上記の残高ベースのボリュームテーブルを有効にします。

使用上の注意

  • ベスト ask が頻繁に更新される高速な銘柄で最も効果的です。ティックレベルのデータフィードでのテストを検討してください。
  • 戦略は反対方向のポジションを同時に保有しないため、ヘッジ口座が必要です。
  • Security.PriceStep が設定されていることを確認してください。そうでなければ、ストップロスとトレーリングの計算は 1 ティックあたり 1 通貨単位の距離にフォールバックします。
  • 元の MQL 動作を反映して、一度に 1 つのオープンポジションのみがサポートされます。
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>
/// Momentum strategy based on WOC 0.1.2 concept.
/// Detects consecutive candle close runs in one direction and enters on breakout.
/// Uses ATR-based stop loss and trailing stop.
/// </summary>
public class Woc012Strategy : Strategy
{
	private readonly StrategyParam<int> _sequenceLength;
	private readonly StrategyParam<decimal> _stopLossAtrMult;
	private readonly StrategyParam<decimal> _trailingAtrMult;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevClose;
	private int _upCount;
	private int _downCount;
	private decimal _entryPrice;
	private decimal? _stopPrice;

	public int SequenceLength { get => _sequenceLength.Value; set => _sequenceLength.Value = value; }
	public decimal StopLossAtrMult { get => _stopLossAtrMult.Value; set => _stopLossAtrMult.Value = value; }
	public decimal TrailingAtrMult { get => _trailingAtrMult.Value; set => _trailingAtrMult.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public Woc012Strategy()
	{
		_sequenceLength = Param(nameof(SequenceLength), 6)
			.SetGreaterThanZero()
			.SetDisplay("Sequence Length", "Consecutive bars in same direction to trigger entry", "Signals");

		_stopLossAtrMult = Param(nameof(StopLossAtrMult), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL ATR Mult", "Stop loss as ATR multiple", "Risk");

		_trailingAtrMult = Param(nameof(TrailingAtrMult), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Trail ATR Mult", "Trailing stop as ATR multiple", "Risk");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR calculation length", "Risk");

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

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = 0;
		_upCount = 0;
		_downCount = 0;
		_entryPrice = 0;
		_stopPrice = null;
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal atr)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		// Track consecutive direction
		if (_prevClose > 0)
		{
			if (close > _prevClose)
			{
				_upCount++;
				_downCount = 0;
			}
			else if (close < _prevClose)
			{
				_downCount++;
				_upCount = 0;
			}
			else
			{
				_upCount = 0;
				_downCount = 0;
			}
		}

		_prevClose = close;

		// Manage existing position
		if (Position != 0)
		{
			if (Position > 0)
			{
				// Trail up
				var trail = close - TrailingAtrMult * atr;
				if (_stopPrice == null || trail > _stopPrice)
					_stopPrice = trail;

				if (close <= _stopPrice)
				{
					SellMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
			else
			{
				// Trail down
				var trail = close + TrailingAtrMult * atr;
				if (_stopPrice == null || trail < _stopPrice)
					_stopPrice = trail;

				if (close >= _stopPrice)
				{
					BuyMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
		}

		// Entry: consecutive sequence completed
		if (_upCount >= SequenceLength && Position <= 0)
		{
			var vol = Volume + Math.Abs(Position);
			BuyMarket(vol);
			_entryPrice = close;
			_stopPrice = close - StopLossAtrMult * atr;
			_upCount = 0;
		}
		else if (_downCount >= SequenceLength && Position >= 0)
		{
			var vol = Volume + Math.Abs(Position);
			SellMarket(vol);
			_entryPrice = close;
			_stopPrice = close + StopLossAtrMult * atr;
			_downCount = 0;
		}
	}
}