GitHub で見る

Lucky戦略

Lucky戦略は、最良のビッド価格とアスク価格の間の急速な変化を監視するブレイクアウトスキャルパーです。アスク価格が設定可能な数のpips分上昇すると買い、ビッドが同じ量下落すると売ります。ポジションは利益になり次第すぐにクローズされるか、価格が保護しきい値を超えて不利な方向に動いた場合にクローズされます。

データと執行

  • 市場データ: 最良のビッドとアスクストリームにアクセスするためにレベル1クオートが必要です。
  • 注文タイプ: クオートショックに素早く反応するためにすべてのエントリーと決済に成行注文を使用します。
  • ポジションモード: ヘッジングスタイルの口座向けに設計されていますが、ネットエクスポージャーを累積することでネッティング口座でも機能します。

パラメーター

  • Shift points – 新しい取引をトリガーする連続したクオート間の最小pip距離。高い値はノイズをフィルタリングし、低い値は小さな跳躍にも反応します。
  • Limit points – オープンポジションを強制クローズする前に許容される最大不利移動(pips単位)。銘柄のティックサイズでもスケーリングされます。
  • Reverse mode – 取引方向を反転します。有効にすると、アスクの上昇ショックがショートを開き、ビッドの下降ショックがロングを開きます。

トレーディングロジック

  1. 初期化
    • 銘柄のティックサイズを使用して、ポイントベースのパラメーターを実際の価格距離に変換します。
    • レベル1データを購読し、前回のビッドとアスク価格の内部バッファをリセットします。
  2. エントリー
    • アスクが前回のアスクに対して少なくとも設定されたシフト分増加した場合、戦略はロングを開きます(リバースモードではショート)。
    • ビッドが前回のビッドに対して少なくともシフト分減少した場合、戦略はショートを開きます(リバースモードではロング)。
  3. ボリュームサイジング
    • デフォルトの注文数量は戦略のVolumeプロパティから来ます。
    • ポートフォリオ資産が利用可能な場合、MetaTraderのロジックを模倣して約FreeMargin / 10,000を割り当て、1小数ロットに丸めることで、より大きな口座がより大きなサイズで取引できるようにします。
  4. 決済
    • ロングポジションはビッドが平均エントリー価格を超えるとすぐに、またはアスクが設定されたリミット分エントリーを下回るとクローズされます。
    • ショートポジションはアスクがエントリーを下回るか、ビッドがエントリーをリミット分上回るとクローズされます。

注意事項と使用上のヒント

  • 顕著なクオートジャンプがある高流動性FXペアまたはインデックスCFDで最も効果を発揮します。
  • ライブでテストする際は、ポートフォリオレベルのストップアウトなどの追加リスク管理と組み合わせてください。
  • Reverse modeを有効にすると、他のパラメーターを変更せずにブレイクアウトをフェード戦略に変換できます。
  • 戦略はすべての適格なクオート更新に反応するため、ノイジーなフィードでは受信データのスロットルまたはシフトしきい値の増加を検討してください。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Breakout strategy that reacts to fast price shifts (candle-to-candle high/low jumps)
/// and closes trades on profit target or adverse move (stop loss).
/// </summary>
public class LuckyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _shiftPct;
	private readonly StrategyParam<decimal> _profitPct;
	private readonly StrategyParam<decimal> _stopPct;
	private readonly StrategyParam<bool> _reverse;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _previousHigh;
	private decimal? _previousLow;
	private bool _isReady;

	/// <summary>
	/// Minimum percentage shift in high/low to trigger entry.
	/// </summary>
	public decimal ShiftPct
	{
		get => _shiftPct.Value;
		set => _shiftPct.Value = value;
	}

	/// <summary>
	/// Profit target as percentage of entry price.
	/// </summary>
	public decimal ProfitPct
	{
		get => _profitPct.Value;
		set => _profitPct.Value = value;
	}

	/// <summary>
	/// Stop loss as percentage of entry price.
	/// </summary>
	public decimal StopPct
	{
		get => _stopPct.Value;
		set => _stopPct.Value = value;
	}

	/// <summary>
	/// Switch to invert the trading direction.
	/// </summary>
	public bool Reverse
	{
		get => _reverse.Value;
		set => _reverse.Value = value;
	}

	/// <summary>
	/// Candle type and timeframe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters.
	/// </summary>
	public LuckyStrategy()
	{
		_shiftPct = Param(nameof(ShiftPct), 1.5m)
			.SetDisplay("Shift %", "Minimum percentage shift in high/low to trigger entry", "Trading")
			.SetOptimize(0.5m, 3.0m, 0.5m);

		_profitPct = Param(nameof(ProfitPct), 2.0m)
			.SetDisplay("Profit %", "Profit target as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_stopPct = Param(nameof(StopPct), 3.0m)
			.SetDisplay("Stop %", "Stop loss as percentage of entry price", "Risk management")
			.SetOptimize(1.0m, 5.0m, 0.5m);

		_reverse = Param(nameof(Reverse), false)
			.SetDisplay("Reverse mode", "Invert the direction of new trades", "Trading");

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

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

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

		_previousHigh = null;
		_previousLow = null;
		_entryPrice = 0m;
		_isReady = false;
	}

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

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

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

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

		if (!_isReady)
		{
			_previousHigh = high;
			_previousLow = low;
			_isReady = true;
			return;
		}

		// Try to close existing position first
		TryClosePosition(close);

		// Only open new positions if flat
		if (Position == 0 && _previousHigh.HasValue && _previousLow.HasValue)
		{
			var prevH = _previousHigh.Value;
			var prevL = _previousLow.Value;

			// Check for upward breakout: high moved up sharply relative to previous high
			if (prevH > 0m && (high - prevH) / prevH * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenShort(close);
				else
					OpenLong(close);
			}
			// Check for downward breakdown: low moved down sharply relative to previous low
			else if (prevL > 0m && (prevL - low) / prevL * 100m >= ShiftPct)
			{
				if (Reverse)
					OpenLong(close);
				else
					OpenShort(close);
			}
		}

		_previousHigh = high;
		_previousLow = low;
	}

	private void OpenLong(decimal price)
	{
		BuyMarket(Volume);
		_entryPrice = price;
	}

	private void OpenShort(decimal price)
	{
		SellMarket(Volume);
		_entryPrice = price;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0 || _entryPrice <= 0m)
			return;

		if (Position > 0)
		{
			var pctChange = (currentPrice - _entryPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			var pctChange = (_entryPrice - currentPrice) / _entryPrice * 100m;

			if (pctChange >= ProfitPct || pctChange <= -StopPct)
				BuyMarket(Math.Abs(Position));
		}
	}
}