GitHub で見る

ファルハド・クラブ戦略

概要

ファルハド クラブ戦略は、MetaTrader エキスパート アドバイザー FarhadCrab1.mq4 の StockSharp の高レベル移植です。オリジナルの EA は、GBP/JPY、GBP/USD、EUR/USD の M1 タイムフレーム向けに設計された高速スキャルピング システムです。この変換は、日中移動平均フィルターと日次トレンドのセーフティ ネットおよび自動出口管理を組み合わせることにより、C# で取引ロジックを再作成します。

この戦略は、典型的な価格に基づいて計算された 9 期間 EMA とローソク足の始値に基づいて計算された 9 期間 SMA を通じて現在のタイムフレームを分析します。同時に、毎日のローソク足から構築された 55 期間の平滑移動平均 (SMMA) を追跡します。オープンポジションがないときに短期フィルターが十分な上昇の勢いを示すと、ロングトレードがトリガーされます。逆に、日中高値がオープンの SMA を下回っている場合、ショートトレードが開始されます。日次 SMMA は保護オーバーレイとして機能します。価格を下からクロスするとすべてのロング取引が終了し、上からクロスするとショート ポジションが閉じられます。

出口管理は、ピップ単位で構成可能なテイクプロフィットレベルとロングポジションとショートポジションの独立したトレーリングストップを備えたオリジナルの EA の動作を再現します。トレーリング ロジックは、市場が設定された距離だけ進んだ後でのみストップを移動することで、MetaTrader の実装に従います。この戦略は、保留中の逆指値注文ではなく成行注文を通じてポジションを決済するため、高レベルの API イベント フローと互換性があります。

主な特長

  • 指標セットは EA と同じです – 一般的な価格では 9 期間 EMA、始値では 9 期間 SMA、トレンドの方向性については日次 55 期間 SMMA。
  • マルチタイムフレーム データ処理 – 取引タイムフレームと毎日のローソク足を同時にサブスクライブし、StockSharp が手動でバッファリングせずに必要なインジケーターを計算できるようにします。
  • 設定可能なエグジット – 元の外部入力と同様に、対称的なテイクプロフィットディスタンス (ロング/ショート) とピップ単位で表現されるトレーリングストップ。
  • 日次安全スイッチ – 日次 SMMA が日次終値を上回った場合にロングを閉じ、下回った場合にショートを閉じるという EA のルールを再現します。
  • 組み込みの保護 – 起動時に StartProtection() を 1 回呼び出して、フレームワークのベスト プラクティスに従って位置を保護します。

パラメーター

パラメータ 説明 デフォルト
OrderVolume 新しい成行注文に適用される取引量。 0.1
LongTakeProfitPips ロングポジションのテイクプロフィット距離(ピップ単位で測定)。 10
ShortTakeProfitPips ショートポジションのテイクプロフィット距離(ピップ単位で測定)。 10
LongTrailingStopPips ロングトレードのトレーリングストップ距離。ゼロに設定すると、トレーリングは無効になります。 8
ShortTrailingStopPips ショートトレードのトレーリングストップ距離。ゼロに設定すると、トレーリングは無効になります。 8
DailyMaPeriod 保護的出口に使用される日次平滑移動平均の長さ。 55
CandleType 戦略の計算を推進する主な時間枠。デフォルトは 1 分のローソク足です。 1m

すべてのパラメータは StrategyParam<T> を通じて公開され、必要に応じて最適化可能としてマークされるため、StockSharp オプティマイザーを介して調整できます。

取引ルール

  1. ロングエントリー: 現在のローソク足の安値が標準価格の 9 期間 EMA を上回っており、アクティブなポジションがない場合は、ロング取引を開始します。
  2. ショートエントリー: 現在のローソク足の高値が始値の 9 期間 SMA を下回っており、アクティブなポジションがない場合は、ショート取引を開始します。
  3. 日次保護出口 (ロング): 日次 SMMA が以前は前の終値を下回っていたときに日次終値を上回った場合は、ロングポジションを閉じます。
  4. 日次保護出口 (ショート): 日次 SMMA が前回の終値を上回っていたにもかかわらず、日次終値を下回った場合は、ショート ポジションを閉じます。
  5. テイクプロフィット: 設定されたピップ目標に達したらポジションをクローズします。
  6. トレーリングストップ: ポジションがトレーリング距離を獲得した後、リトレースメント距離を監視して利益を確定し、価格がその量だけ後退したときに決済します。

実装メモ

  • このコードは高レベルの SubscribeCandles().Bind(...) 呼び出しのみに依存しており、手動のインジケーター バッファーを排除し、プロジェクトのガイドライン内に留まります。
  • ピップは、3 桁および 5 桁の相場に対する通常の MetaTrader スタイルの調整を使用して、商品の PriceStep から計算されます。これにより、EA のポイントベースのパラメータと動作の一貫性が保たれます。
  • ストップロスとテイクプロフィットの管理は、指値/ストップ注文を登録するのではなく、条件が満たされたときにポジションを決済することによって内部的に実行されます。このアプローチは、StockSharp での非同期注文実行との互換性を維持しながら、元のスクリプトにある瞬間的な終了と一致します。
  • この戦略は、OnReseted 内の状態をリセットし、最適化が実行され、繰り返し起動が白紙の状態から開始されるようにします。

使用のヒント

  • オリジナルの EA は、M1 タイムフレームのボラティリティの高いポンドとユーロのペアに合わせて調整されました。同じ時間枠と商品を適用した場合にも同様の結果が期待できますが、パラメーターは異なるボラティリティプロファイルに対応するために公開されています。
  • システムは一度に 1 つのポジションのみを保持するため、複雑なポジションピラミッド化を行わずに簡単なバックテストやライブ実行に適しています。
  • トレーリングストップは、トレンドが滑らかな商品でより効果的になります。レンジ相場では、後続距離を短縮するか、利益確定のエグジットのみに依存することを検討してください。
  • 毎日の SMMA 出口は主要なリスク管理として機能します。スイング指向のセットアップの場合、DailyMaPeriod を増やすと、長期フィルターの反応性が低くなります。

検証可能な計算規則: 確定したM1足ごとに typical=(high+low+close)/3 を計算します。typical の EMA(9) は最初の9本の平均で初期化し、その後 EMA=(8*EMA+2*typical)/10 と更新します。別の SMA(9) は Open を使います。ポジションも未約定注文もなければ low > EMA で買い、それ以外で high < SMA(Open) なら売ります。日足 SMMA(55) は確定した55本の日足終値で初期化し、その後 (54*SMMA+close)/55 に従います。日足終値が不利方向に交差すると決済します。利確とトレーリングは実約定価格に基づき確定M1足で判定し、新しいトレーリング水準は次の足から適用します。DailyMaPeriod を縮小したテストは売買動作のみを検証し、標準の55日設定を確認したことにはなりません。バックテストは将来の収益を保証しません。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

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

/// <summary>
/// Farhad Crab uses EMA(9) on typical M1 prices, SMA(9) of M1 opens,
/// and daily SMMA protection with direction-specific take/trailing exits.
/// </summary>
public class FarhadCrabStrategy : Strategy
{
	private readonly StrategyParam<int> _dailyMaPeriod;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _longTakeProfitPips;
	private readonly StrategyParam<decimal> _shortTakeProfitPips;
	private readonly StrategyParam<decimal> _longTrailingStopPips;
	private readonly StrategyParam<decimal> _shortTrailingStopPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _typicalSeed = new();
	private readonly Queue<decimal> _opens = new();
	private readonly Queue<decimal> _dailySeed = new();
	private decimal? _emaTypical;
	private decimal? _dailySmma;
	private decimal? _priorSmma;
	private decimal? _priorDailyClose;
	private decimal? _entryPrice;
	private decimal? _trailingLevel;
	private decimal _bestPrice;
	private Order _pendingOrder;
	private Order _entryOrder;

	public int DailyMaPeriod { get => _dailyMaPeriod.Value; set => _dailyMaPeriod.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal LongTakeProfitPips { get => _longTakeProfitPips.Value; set => _longTakeProfitPips.Value = value; }
	public decimal ShortTakeProfitPips { get => _shortTakeProfitPips.Value; set => _shortTakeProfitPips.Value = value; }
	public decimal LongTrailingStopPips { get => _longTrailingStopPips.Value; set => _longTrailingStopPips.Value = value; }
	public decimal ShortTrailingStopPips { get => _shortTrailingStopPips.Value; set => _shortTrailingStopPips.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public FarhadCrabStrategy()
	{
		_dailyMaPeriod = Param(nameof(DailyMaPeriod), 55).SetGreaterThanZero();
		_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
		_longTakeProfitPips = Param(nameof(LongTakeProfitPips), 10m).SetNotNegative();
		_shortTakeProfitPips = Param(nameof(ShortTakeProfitPips), 10m).SetNotNegative();
		_longTrailingStopPips = Param(nameof(LongTrailingStopPips), 8m).SetNotNegative();
		_shortTrailingStopPips = Param(nameof(ShortTrailingStopPips), 8m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, TimeSpan.FromDays(1).TimeFrame())];

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_typicalSeed.Clear();
		_opens.Clear();
		_dailySeed.Clear();
		_emaTypical = null;
		_dailySmma = null;
		_priorSmma = null;
		_priorDailyClose = null;
		_entryPrice = null;
		_trailingLevel = null;
		_bestPrice = 0m;
		_pendingOrder = null;
		_entryOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		if ((Security?.PriceStep ?? 0m) <= 0m)
			throw new InvalidOperationException("Farhad Crab requires a positive instrument PriceStep.");
		SubscribeCandles(TimeSpan.FromDays(1).TimeFrame()).Bind(ProcessDailyCandle).Start();
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private decimal PipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return Security?.Decimals is 3 or 5 ? step * 10m : step;
	}

	private bool Pending => _pendingOrder is not null &&
		_pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);

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

		var priorSmma = _dailySmma;
		var priorClose = _priorDailyClose;
		if (_dailySmma is null)
		{
			_dailySeed.Enqueue(candle.ClosePrice);
			if (_dailySeed.Count == DailyMaPeriod)
				_dailySmma = _dailySeed.Average();
		}
		else
			_dailySmma = (_dailySmma.Value * (DailyMaPeriod - 1) + candle.ClosePrice) / DailyMaPeriod;

		_priorSmma = priorSmma;
		_priorDailyClose = candle.ClosePrice;

		if (Pending || Position == 0m || priorSmma is null || priorClose is null || _dailySmma is null)
			return;

		if (Position > 0m && priorSmma < priorClose && _dailySmma > candle.ClosePrice ||
			Position < 0m && priorSmma > priorClose && _dailySmma < candle.ClosePrice)
			Exit("Farhad Crab daily safety exit");
	}

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

		var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		if (_emaTypical is null)
		{
			_typicalSeed.Enqueue(typical);
			if (_typicalSeed.Count == 9)
				_emaTypical = _typicalSeed.Average();
		}
		else
			_emaTypical = (_emaTypical.Value * 8m + typical * 2m) / 10m;

		_opens.Enqueue(candle.OpenPrice);
		while (_opens.Count > 9)
			_opens.Dequeue();

		if (Pending)
			return;
		if (Position != 0m)
		{
			HandleRisk(candle);
			return;
		}
		if (_emaTypical is null || _opens.Count < 9)
			return;

		if (candle.LowPrice > _emaTypical.Value)
			Enter(Sides.Buy);
		else if (candle.HighPrice < _opens.Average())
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = OrderVolume;
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(min, volume);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(max, volume);
		if (volume > 0m)
			Submit(side, volume, "Farhad Crab entry");
	}

	private void HandleRisk(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
			return;
		var positive = Position > 0m;
		var pip = PipSize();
		var target = (positive ? LongTakeProfitPips : ShortTakeProfitPips) * pip;
		if (target > 0m && (positive ? candle.HighPrice >= entry + target :
			candle.LowPrice <= entry - target))
		{
			Exit("Farhad Crab take profit");
			return;
		}

		if (_trailingLevel is decimal level && (positive ? candle.LowPrice <= level :
			candle.HighPrice >= level))
		{
			Exit("Farhad Crab trailing stop");
			return;
		}

		var distance = (positive ? LongTrailingStopPips : ShortTrailingStopPips) * pip;
		if (distance <= 0m)
			return;

		_bestPrice = positive ? Math.Max(_bestPrice, candle.HighPrice) :
			Math.Min(_bestPrice, candle.LowPrice);
		var move = positive ? _bestPrice - entry : entry - _bestPrice;
		if (move < distance)
			return;
		var next = positive ? _bestPrice - distance : _bestPrice + distance;
		if (_trailingLevel is null || (positive ? next > _trailingLevel.Value : next < _trailingLevel.Value))
			_trailingLevel = next;
	}

	private void Exit(string reason)
	{
		if (!Pending && Position != 0m)
			Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), reason);
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		if (comment == "Farhad Crab entry")
			_entryOrder = _pendingOrder;
		RegisterOrder(_pendingOrder);
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Order == _entryOrder && trade.Trade is not null)
		{
			_entryPrice = trade.Trade.Price;
			_bestPrice = _entryPrice.Value;
			_trailingLevel = null;
		}
		else if (Position == 0m)
		{
			_entryPrice = null;
			_trailingLevel = null;
			_bestPrice = 0m;
		}
	}
}