Auf GitHub ansehen

Resonanzjäger-Strategie

Überblick

Die Resonance Hunter-Strategie ist die StockSharp-Portierung des MetaTrader-Expertenberaters Exp_ResonanceHunter. Es überwacht drei korrelierte Währungspaare pro Slot und sucht nach synchronem Impuls in ihren Stochastic-Oszillatoren. Wenn die Oszillatoren in die gleiche Richtung schwingen, öffnet die Strategie eine Position auf dem Primärsymbol, während die Sekundär- und Bestätigungssymbole als Filter fungieren. Der Handel wird geschlossen, sobald das führende Instrument an Schwung verliert oder der konfigurierte Stop-Loss erreicht wird.

Drei Steckplätze sind vorkonfiguriert:

  1. EURUSD wurde mit EURJPY und USDJPY als Bestätigungen gehandelt.
  2. GBPUSD wird mit GBPJPY und USDJPY gehandelt.
  3. AUDUSD wird mit AUDJPY und USDJPY gehandelt.

Jeder Slot kann unabhängig aktiviert oder deaktiviert werden und kann seinen eigenen Zeitrahmen und seine eigenen Indikatorparameter verwenden.

Parameter

Alle Parameter sind nach Slot gruppiert (Slot 1–3). Jede Gruppe teilt die folgenden Einstellungen:

Parameter Beschreibung
{Slot} Enabled Ermöglicht den Handel für den Slot.
{Slot} Primary Instrument, das von der Strategie gehandelt und für Ausstiegssignale verwendet wird.
{Slot} Secondary Zweites Instrument, das an der Resonanzprüfung teilnimmt.
{Slot} Confirmation Drittes Instrument zur Resonanzprüfung.
{Slot} Candle Type Auf alle drei Instrumente angewendeter Zeitrahmen (Standard = 1 Stunde).
{Slot} K Period Stochastic %K Lookback.
{Slot} D Period Glättungszeitraum für %D.
{Slot} Slowing Zusätzliche Glättung für %K.
{Slot} Volume Bestellvolumen in Losen. Bestehende Gegenpositionen werden saldiert.
{Slot} Stop Loss Stop-Loss-Distanz im MetaTrader-Stil in Punkten. Auf 0 setzen, um den Schutzstopp zu deaktivieren.

Handelslogik

  1. Für jedes konfigurierte Instrument wird auf abgeschlossene Kerzen ein StochasticOscillator mit den ausgewählten Parametern berechnet.
  2. Sobald die letzten Kerzen der drei Instrumente die gleiche Öffnungszeit haben, werden die Unterschiede %K - %D ausgewertet:
    • Eine positive Differenz markiert einen Aufwärtsimpuls (Up), eine negative Differenz markiert einen Abwärtsimpuls (Down).
    • Zusätzliche Konsistenzregeln des ursprünglichen Indikators passen die Impulse an, indem sie die Größe jedes Paares vergleichen.
  3. Ein Long-Einstieg entsteht, wenn alle drei Impulse nach oben zeigen. Ein kurzer Einstieg entsteht, wenn alle drei Impulse nach unten zeigen.
  4. Vor der Übermittlung neuer Aufträge schließt die Strategie bestehende Positionen, wenn das Primärsymbol einen entgegengesetzten Impuls anzeigt (spiegelt die UpStop/DnStop-Puffer des Indikators wider).
  5. Nach Eingabe einer Position wird ein schützender Stop-Preis anhand des letzten Schlusskurses und der Distanz {Slot} Stop Loss berechnet. Bei jeder neuen Primärkerze wird der Stop überprüft und bei Überschreitung wird die Position sofort geschlossen.

Bestellungen werden über BuyMarket/SellMarket weitergeleitet. Das bestehende Risiko des Primärsymbols wird verrechnet, sodass die Strategie bei Bedarf direkt umgekehrt werden kann.

Notizen

  • Die Strategie erfordert synchronisierte Kerzendaten für die drei Instrumente in jedem Slot. Wenn ein Symbol hinterherhinkt, wird das Signal verschoben, bis die Zeitstempel der Balken übereinstimmen.
  • Stop-Levels werden innerhalb der Strategie emuliert (es werden keine tatsächlichen Stop-Orders gesendet), um dem MetaTrader-Verhalten zu entsprechen.
  • Standardparameterwerte reproduzieren den ursprünglichen Expert Advisor, können jedoch über die Param-Schnittstelle optimiert werden.
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();
	}
}