GitHub で見る

ラッキーシフトリミット戦略

Lucky Shift Limit 戦略は、MetaTrader 4 エキスパート アドバイザー Lucky_acnl6p6j89zn91fa.mq4 を直接変換したものです。最高の買値/売値をリアルタイムで監視し、MetaTrader「ポイント」(pips)単位で測定される突然の急騰に反応します。売り値が設定されたシフト距離だけ上向きに加速すると、戦略は売りによって動きを弱めますが、入札の急激な下落は逆張りの買いを促します。オープンな取引はすべて常に監視され、利益が出た場合、または浮動損失が元の MQ4 ロジックと同じ安全しきい値を超えた場合にクローズされます。

データと実行の要件

  • 市場データ – レベル 1 の相場のみを購読します。ローソク足や市場の厚みは必要ありません。
  • 約定スタイル – エントリーとエグジットは、MetaTrader からの即時の OrderSend コールを模倣する成行注文に依存します。
  • 口座モード – ヘッジ口座とネッティング口座の両方で動作します。ネッティング口座では、この戦略は単一のポジションでエクスポージャーを蓄積し、エグジットモジュールがそれを平坦化します。
  • ボリュームサイジング – デフォルトの注文サイズは Strategy.Volume から取得されますが、ポートフォリオ値が利用可能な場合、ヘルパーは MetaTrader から AccountFreeMargin/10000 をエミュレートします。

パラメーター

名前 デフォルト 説明
Shift points 3 新しい注文をトリガーする、連続するアスク/ビッド間の最小 MetaTrader ポイント数。値を大きくするとノイズが除去され、値を小さくすると反応が速くなります。
Limit points 18 オープントレードで許容される最大の逆方向のエクスカーション。価格がポジションに対してこのポイントだけ変動した場合、取引は強制終了されます。

どちらのパラメータも MetaTrader ポイントで表現され、商品ティック サイズを使用して絶対価格オフセットに内部変換されます。 UI の最適化境界は、MQ4 バージョンの実際の範囲と一致します。

取引ロジック

  1. 初期化
    • Security.PriceStep を使用して、ポイントベースの設定を実際の価格距離に変換します。
    • キャッシュされた入札/売値をリセットし、高レベルの Bind 処理を伴うレベル 1 のサブスクリプションを開始します。
  2. エントリー条件
    • 前の売りと比較して売りが少なくとも Shift points 上昇した場合、戦略はトリガーを説明するログメモとともに成行売り注文 (スパイクをフェードアウト) を送信します。
    • 入札額が前回の入札額と比較して少なくとも同じ距離下がった場合、成行買いが開始されます。
    • 信号は、同時位置の数を制限しなかった元のエキスパートとまったく同じように、連続して複数回発射できます。
  3. 出口管理
    • すべての引用ティックは TryClosePosition() を呼び出します。ロングポジションは、ビッドが平均エントリー(実現利益)を上回っている場合、またはアスクがエントリーよりも Limit points(損失上限)低い場合に、直ちにクローズされます。
    • ショートポジションはこのロジックを反映しており、収益性の高いアスク気配でクローズするか、入札が設定された制限によってエントリーを超えたときにクローズします。
    • すべてのエグジットは成行注文を使用して OrderClose を複製し、ポジションが同じティックでフラット化されることを保証します。
  4. ポジションサイジング
    • 利用可能な場合、ポートフォリオの資本 (equity / 10,000、小数点第 1 位に四捨五入) からデフォルトのボリュームを計算し、MQ4 ヘルパー GetLots() と一致します。
    • 株式データが欠落している場合は、戦略 Volume プロパティにフォールバックします。

実装メモ

  • 高レベルの StockSharp API のみを使用します: SubscribeLevel1().Bind(ProcessLevel1) により、手動の引用リスナーが不要になります。
  • カスタム コレクションは保存されません。以前の買値/売値は、ガイドラインで許可されているように、単純な null 許容変数に保持されます。
  • 損失キャップは商品のティックサイズに連動するため、小数ピップステップを持つエキゾチックシンボルは正しい価格デルタに自動的にマッピングされます。
  • ランタイム中のパラメーターの変更は考慮されます。この戦略では、レベル 1 データが到着したときにしきい値が再計算されます。
  • ログ ステートメントにはすべての開始理由と終了理由が記録されるため、バックテストとライブ診断が簡素化されます。

使い方のヒント

  • ビッド/アスクショックが頻繁に発生する流動性の高いFXペアまたはインデックスに最適です。
  • 追加のストップロスまたはドローダウン制限が必要な場合は、この戦略をポートフォリオレベルの保護 (StartProtection) と組み合わせることを検討してください。
  • ノイズの多いフィードで Shift points を増やして過剰取引を減らすか、減らして超短期の動きをキャプチャします。
  • 論理は本質的に逆張りです。ブレイクアウト動作が必要な場合は、Shift points を十分に高く設定するか、別のフィルター インジケーターと組み合わせてください。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Candle-based reversion strategy that reacts to sudden price jumps (high/low shifts)
/// and enforces a configurable loss cap. Adapted from a Level1 quote-reversion approach
/// to work with candle data for backtesting.
/// </summary>
public class LuckyShiftLimitStrategy : Strategy
{
	private readonly StrategyParam<int> _shiftPoints;
	private readonly StrategyParam<int> _limitPoints;

	private decimal? _previousHigh;
	private decimal? _previousLow;
	private decimal _shiftThreshold;
	private decimal _limitThreshold;
	private decimal _entryPrice;
	private bool _thresholdsReady;
	private int _holdBars;

	/// <summary>
	/// Minimum price shift (as percentage tenths) required to trigger an entry.
	/// </summary>
	public int ShiftPoints
	{
		get => _shiftPoints.Value;
		set => _shiftPoints.Value = value;
	}

	/// <summary>
	/// Maximum adverse excursion (as percentage) tolerated before force-closing losing trades.
	/// </summary>
	public int LimitPoints
	{
		get => _limitPoints.Value;
		set => _limitPoints.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters taken from the original MQ4 expert.
	/// </summary>
	public LuckyShiftLimitStrategy()
	{
		_shiftPoints = Param(nameof(ShiftPoints), 3)
			.SetGreaterThanZero()
			.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")

			.SetOptimize(1, 20, 1);

		_limitPoints = Param(nameof(LimitPoints), 18)
			.SetGreaterThanZero()
			.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")

			.SetOptimize(5, 80, 5);
	}

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

		_previousHigh = null;
		_previousLow = null;
		_shiftThreshold = 0m;
		_limitThreshold = 0m;
		_entryPrice = 0m;
		_thresholdsReady = false;
		_holdBars = 0;
	}

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

		var tf = TimeSpan.FromMinutes(5).TimeFrame();

		SubscribeCandles(tf)
			.Bind(ProcessCandle)
			.Start();
	}

	private void EnsureThresholds(decimal price)
	{
		if (_thresholdsReady)
			return;

		if (price <= 0m)
			return;

		// ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
		_shiftThreshold = price * ShiftPoints * 0.003m;
		_limitThreshold = price * LimitPoints * 0.01m;
		_thresholdsReady = true;
	}

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

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		EnsureThresholds(close);

		if (!_thresholdsReady)
			return;

		// Count hold bars for position management.
		if (Position != 0)
			_holdBars++;

		// Entry logic: detect sudden shifts in high/low between consecutive candles.
		// Only enter when flat.
		if (Position == 0 && _previousHigh is decimal prevHigh && _previousLow is decimal prevLow)
		{
			// High jumped up sharply -> sell on expected reversion
			if (high - prevHigh >= _shiftThreshold)
			{
				SellMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Sell triggered: high shift {high - prevHigh:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
			// Low dropped sharply -> buy on expected rebound
			else if (prevLow - low >= _shiftThreshold)
			{
				BuyMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Buy triggered: low shift {prevLow - low:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
		}

		_previousHigh = high;
		_previousLow = low;

		TryClosePosition(close);
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0)
			return;

		var avgPrice = _entryPrice;

		if (avgPrice <= 0m)
			return;

		// Minimum hold of 5 bars before checking exit.
		if (_holdBars < 5)
			return;

		// Use half of shift threshold as profit target.
		var profitTarget = _shiftThreshold * 0.5m;

		if (Position > 0)
		{
			// Close long on profit or loss cap.
			if (currentPrice - avgPrice >= profitTarget)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && avgPrice - currentPrice >= _limitThreshold)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long on loss cap. Price={currentPrice:0.#####}");
			}
		}
		else if (Position < 0)
		{
			// Close short on profit or loss cap.
			if (avgPrice - currentPrice >= profitTarget)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && currentPrice - avgPrice >= _limitThreshold)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short on loss cap. Price={currentPrice:0.#####}");
			}
		}
	}
}