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:
EURUSD wurde mit EURJPY und USDJPY als Bestätigungen gehandelt.
GBPUSD wird mit GBPJPY und USDJPY gehandelt.
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
Für jedes konfigurierte Instrument wird auf abgeschlossene Kerzen ein StochasticOscillator mit den ausgewählten Parametern berechnet.
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.
Ein Long-Einstieg entsteht, wenn alle drei Impulse nach oben zeigen. Ein kurzer Einstieg entsteht, wenn alle drei Impulse nach unten zeigen.
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).
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();
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class resonance_hunter_strategy(Strategy):
def __init__(self):
super(resonance_hunter_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._fast_k_period = self.Param("FastKPeriod", 8)
self._slow_k_period = self.Param("SlowKPeriod", 21)
self._d_period = self.Param("DPeriod", 3)
self._highs = []
self._lows = []
self._bar_count = 0
self._fast_k_history = []
self._slow_k_history = []
self._prev_fast_k = None
self._prev_slow_k = None
self._prev_fast_d = None
self._prev_slow_d = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastKPeriod(self):
return self._fast_k_period.Value
@FastKPeriod.setter
def FastKPeriod(self, value):
self._fast_k_period.Value = value
@property
def SlowKPeriod(self):
return self._slow_k_period.Value
@SlowKPeriod.setter
def SlowKPeriod(self, value):
self._slow_k_period.Value = value
@property
def DPeriod(self):
return self._d_period.Value
@DPeriod.setter
def DPeriod(self, value):
self._d_period.Value = value
def OnReseted(self):
super(resonance_hunter_strategy, self).OnReseted()
self._highs = []
self._lows = []
self._bar_count = 0
self._fast_k_history = []
self._slow_k_history = []
self._prev_fast_k = None
self._prev_slow_k = None
self._prev_fast_d = None
self._prev_slow_d = None
def OnStarted2(self, time):
super(resonance_hunter_strategy, self).OnStarted2(time)
self._highs = []
self._lows = []
self._bar_count = 0
self._fast_k_history = []
self._slow_k_history = []
self._prev_fast_k = None
self._prev_slow_k = None
self._prev_fast_d = None
self._prev_slow_d = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _calc_stochastic_k(self, close, period):
if self._bar_count < period:
return None
highs_slice = self._highs[-period:]
lows_slice = self._lows[-period:]
hh = max(highs_slice)
ll = min(lows_slice)
r = hh - ll
if r <= 0:
return 50.0
return (close - ll) / r * 100.0
def _add_to_smoothing(self, history, value, period):
history.append(value)
while len(history) > period:
history.pop(0)
if len(history) < period:
return None
return sum(history) / len(history)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._highs.append(high)
self._lows.append(low)
self._bar_count += 1
# Keep buffer manageable
max_buf = max(self.SlowKPeriod, self.FastKPeriod) + 10
while len(self._highs) > max_buf:
self._highs.pop(0)
self._lows.pop(0)
if self._bar_count < self.SlowKPeriod:
return
fast_k = self._calc_stochastic_k(close, self.FastKPeriod)
slow_k = self._calc_stochastic_k(close, self.SlowKPeriod)
if fast_k is None or slow_k is None:
return
d_period = self.DPeriod
fast_d = self._add_to_smoothing(self._fast_k_history, fast_k, d_period)
slow_d = self._add_to_smoothing(self._slow_k_history, slow_k, d_period)
if (fast_d is None or slow_d is None or
self._prev_fast_k is None or self._prev_slow_k is None or
self._prev_fast_d is None or self._prev_slow_d is None):
self._prev_fast_k = fast_k
self._prev_slow_k = slow_k
self._prev_fast_d = fast_d
self._prev_slow_d = slow_d
return
# Resonance buy: both stochastics cross above their D lines
fast_bull_cross = self._prev_fast_k < self._prev_fast_d and fast_k > fast_d
slow_bull_cross = self._prev_slow_k < self._prev_slow_d and slow_k > slow_d
both_oversold = fast_k < 30 and slow_k < 30
# Resonance sell: both stochastics cross below their D lines
fast_bear_cross = self._prev_fast_k > self._prev_fast_d and fast_k < fast_d
slow_bear_cross = self._prev_slow_k > self._prev_slow_d and slow_k < slow_d
both_overbought = fast_k > 70 and slow_k > 70
buy_signal = (fast_bull_cross and (slow_bull_cross or slow_k > slow_d)) and both_oversold
sell_signal = (fast_bear_cross and (slow_bear_cross or slow_k < slow_d)) and both_overbought
if buy_signal:
if self.Position <= 0:
self.BuyMarket()
elif sell_signal:
if self.Position >= 0:
self.SellMarket()
self._prev_fast_k = fast_k
self._prev_slow_k = slow_k
self._prev_fast_d = fast_d
self._prev_slow_d = slow_d
def CreateClone(self):
return resonance_hunter_strategy()