GitHub で見る

共鳴ハンターの戦略

概要

Resonance Hunter 戦略は、MetaTrader エキスパート アドバイザー Exp_ResonanceHunter の StockSharp 移植版です。スロットごとに 3 つの相関通貨ペアを監視し、それらの Stochastic オシレーターの同期の勢いを探します。オシレーターが同じ方向に共鳴する場合、戦略はプライマリシンボルでポジションをオープンし、セカンダリシンボルと確認シンボルがフィルターとして機能します。主要商品が勢いを失うか、設定されたストップロスに達するとすぐに取引は終了します。

3 つのスロットが事前に構成されています。

  1. EURUSD は確認として EURJPY および USDJPY と取引されます。
  2. GBPUSDはGBPJPYおよびUSDJPYと取引されます。
  3. AUDUSDはAUDJPYおよびUSDJPYと取引されます。

各スロットは個別に有効または無効にすることができ、独自のタイムフレームとインジケーターのパラメーターを使用できます。

パラメーター

すべてのパラメータはスロット (スロット 1 ~ 3) ごとにグループ化されています。すべてのグループは次の設定を共有します。

パラメータ 説明
{Slot} Enabled スロットの取引を可能にします。
{Slot} Primary ストラテジーによって取引され、出口シグナルに使用される商品。
{Slot} Secondary 共鳴チェックに参加する 2 番目の機器。
{Slot} Confirmation 共振チェックに使用される 3 番目の機器。
{Slot} Candle Type 3 つの商品すべてに適用される時間枠 (デフォルト = 1 時間)。
{Slot} K Period Stochastic %K ルックバック。
{Slot} D Period %D の平滑化期間。
{Slot} Slowing %K の追加の平滑化。
{Slot} Volume ロット単位での注文量。既存の逆エクスポージャーは相殺されます。
{Slot} Stop Loss MetaTrader スタイルのストップロス距離 (ポイント単位)。保護停止を無効にするには、0 に設定します。

取引ロジック

  1. 設定された各金融商品について、選択されたパラメータを含む StochasticOscillator が完了したローソク足で計算されます。
  2. 3 つの金融商品の最新のローソク足が同じ始値を共有すると、その差 %K - %D が評価されます。
    • 正の差は上向きインパルス (Up) をマークし、負の差は下向きインパルス (Down) をマークします。
    • 元のインジケーターからの追加の一貫性ルールは、各ペアの大きさを比較することによってインパルスを調整します。
  3. ロングエントリーは、3 つのインパルスがすべて上向きの場合に生成されます。 3 つのインパルスがすべて下向きの場合、短いエントリが表示されます。
  4. 新しい注文を送信する前に、プライマリシンボルが反対のインパルスを示している場合、ストラテジーは既存のポジションを閉じます(インジケーターの UpStop/DnStop バッファーを反映します)。
  5. ポジションを入力した後、最新の終値と {Slot} Stop Loss の距離を使用して保護ストップ価格が計算されます。新しいプライマリローソク足ごとにストップがチェックされ、違反した場合はポジションが直ちに閉じられます。

注文は BuyMarket/SellMarket を通じてルーティングされます。プライマリシンボルの既存のエクスポージャはネッティングされるため、必要に応じて戦略を直接反転できます。

注意事項

  • この戦略では、各スロット内の 3 つの金融商品の同期されたローソク足データが必要です。 1 つのシンボルが遅れている場合、バーのタイムスタンプが揃うまで信号は延期されます。
  • ストップレベルは、MetaTrader の動作と一致するように、ストラテジー内でエミュレートされます (実際のストップ注文は送信されません)。
  • デフォルトのパラメータ値は元のエキスパート アドバイザを再現しますが、Param インターフェースを通じて最適化できます。
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Resonance Hunter" MetaTrader expert.
/// Uses multiple Stochastic oscillators on different periods to find
/// resonance (all pointing same direction) for entry signals.
/// </summary>
public class ResonanceHunterStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastKPeriod;
	private readonly StrategyParam<int> _slowKPeriod;
	private readonly StrategyParam<int> _dPeriod;

	// Manual stochastic: track highest high and lowest low over K periods
	private readonly decimal[] _highs1 = new decimal[100];
	private readonly decimal[] _lows1 = new decimal[100];
	private readonly decimal[] _highs2 = new decimal[100];
	private readonly decimal[] _lows2 = new decimal[100];
	private int _barCount;

	private decimal? _prevFastK;
	private decimal? _prevSlowK;
	private decimal? _prevFastD;
	private decimal? _prevSlowD;

	// Simple smoothing queues for %D
	private readonly decimal[] _fastKHistory = new decimal[3];
	private readonly decimal[] _slowKHistory = new decimal[3];
	private int _fastKCount;
	private int _slowKCount;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastKPeriod
	{
		get => _fastKPeriod.Value;
		set => _fastKPeriod.Value = value;
	}

	public int SlowKPeriod
	{
		get => _slowKPeriod.Value;
		set => _slowKPeriod.Value = value;
	}

	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	public ResonanceHunterStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_fastKPeriod = Param(nameof(FastKPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Fast K Period", "Fast stochastic K period", "Indicators");

		_slowKPeriod = Param(nameof(SlowKPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("Slow K Period", "Slow stochastic K period", "Indicators");

		_dPeriod = Param(nameof(DPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("D Period", "Smoothing period for %D line", "Indicators");
	}

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

		_barCount = 0;
		_prevFastK = null;
		_prevSlowK = null;
		_prevFastD = null;
		_prevSlowD = null;
		_fastKCount = 0;
		_slowKCount = 0;

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

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

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

		var idx = _barCount % 100;
		_highs1[idx] = candle.HighPrice;
		_lows1[idx] = candle.LowPrice;
		_highs2[idx] = candle.HighPrice;
		_lows2[idx] = candle.LowPrice;
		_barCount++;

		if (_barCount < SlowKPeriod)
			return;

		// Calculate fast stochastic %K
		var fastK = CalculateStochasticK(_highs1, _lows1, candle.ClosePrice, FastKPeriod);
		// Calculate slow stochastic %K
		var slowK = CalculateStochasticK(_highs2, _lows2, candle.ClosePrice, SlowKPeriod);

		if (fastK == null || slowK == null)
			return;

		// Calculate %D as SMA of %K
		var fastD = AddToSmoothing(_fastKHistory, ref _fastKCount, fastK.Value, DPeriod);
		var slowD = AddToSmoothing(_slowKHistory, ref _slowKCount, slowK.Value, DPeriod);

		if (fastD == null || slowD == null || _prevFastK == null || _prevSlowK == null || _prevFastD == null || _prevSlowD == null)
		{
			_prevFastK = fastK;
			_prevSlowK = slowK;
			_prevFastD = fastD;
			_prevSlowD = slowD;
			return;
		}

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Resonance buy: both stochastics cross above their D lines
		var fastBullCross = _prevFastK.Value < _prevFastD.Value && fastK.Value > fastD.Value;
		var slowBullCross = _prevSlowK.Value < _prevSlowD.Value && slowK.Value > slowD.Value;
		var bothOversold = fastK.Value < 30 && slowK.Value < 30;

		// Resonance sell: both stochastics cross below their D lines
		var fastBearCross = _prevFastK.Value > _prevFastD.Value && fastK.Value < fastD.Value;
		var slowBearCross = _prevSlowK.Value > _prevSlowD.Value && slowK.Value < slowD.Value;
		var bothOverbought = fastK.Value > 70 && slowK.Value > 70;

		// Buy when both signals confirm or fast crosses with slow already bullish
		var buySignal = (fastBullCross && (slowBullCross || slowK.Value > slowD.Value)) && bothOversold;
		// Sell when both signals confirm or fast crosses with slow already bearish
		var sellSignal = (fastBearCross && (slowBearCross || slowK.Value < slowD.Value)) && bothOverbought;

		if (buySignal)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		else if (sellSignal)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevFastK = fastK;
		_prevSlowK = slowK;
		_prevFastD = fastD;
		_prevSlowD = slowD;
	}

	private decimal? CalculateStochasticK(decimal[] highs, decimal[] lows, decimal close, int period)
	{
		if (_barCount < period)
			return null;

		var hh = decimal.MinValue;
		var ll = decimal.MaxValue;

		for (var i = 0; i < period; i++)
		{
			var idx = (_barCount - 1 - i) % 100;
			if (idx < 0) idx += 100;
			if (highs[idx] > hh) hh = highs[idx];
			if (lows[idx] < ll) ll = lows[idx];
		}

		var range = hh - ll;
		if (range <= 0)
			return 50m;

		return (close - ll) / range * 100m;
	}

	private static decimal? AddToSmoothing(decimal[] history, ref int count, decimal value, int period)
	{
		var idx = count % history.Length;
		history[idx] = value;
		count++;

		if (count < period)
			return null;

		var sum = 0m;
		var n = Math.Min(period, history.Length);
		for (var i = 0; i < n; i++)
		{
			var j = (count - 1 - i) % history.Length;
			if (j < 0) j += history.Length;
			sum += history[j];
		}

		return sum / n;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		Array.Clear(_highs1);
		Array.Clear(_lows1);
		Array.Clear(_highs2);
		Array.Clear(_lows2);
		Array.Clear(_fastKHistory);
		Array.Clear(_slowKHistory);
		_barCount = 0;
		_prevFastK = null;
		_prevSlowK = null;
		_prevFastD = null;
		_prevSlowD = null;
		_fastKCount = 0;
		_slowKCount = 0;

		base.OnReseted();
	}
}