共鳴ハンターの戦略
概要
Resonance Hunter 戦略は、MetaTrader エキスパート アドバイザー Exp_ResonanceHunter の StockSharp 移植版です。スロットごとに 3 つの相関通貨ペアを監視し、それらの Stochastic オシレーターの同期の勢いを探します。オシレーターが同じ方向に共鳴する場合、戦略はプライマリシンボルでポジションをオープンし、セカンダリシンボルと確認シンボルがフィルターとして機能します。主要商品が勢いを失うか、設定されたストップロスに達するとすぐに取引は終了します。
3 つのスロットが事前に構成されています。
- EURUSD は確認として EURJPY および USDJPY と取引されます。
- GBPUSDはGBPJPYおよびUSDJPYと取引されます。
- 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 に設定します。 |
取引ロジック
- 設定された各金融商品について、選択されたパラメータを含む
StochasticOscillatorが完了したローソク足で計算されます。 - 3 つの金融商品の最新のローソク足が同じ始値を共有すると、その差
%K - %Dが評価されます。- 正の差は上向きインパルス (
Up) をマークし、負の差は下向きインパルス (Down) をマークします。 - 元のインジケーターからの追加の一貫性ルールは、各ペアの大きさを比較することによってインパルスを調整します。
- 正の差は上向きインパルス (
- ロングエントリーは、3 つのインパルスがすべて上向きの場合に生成されます。 3 つのインパルスがすべて下向きの場合、短いエントリが表示されます。
- 新しい注文を送信する前に、プライマリシンボルが反対のインパルスを示している場合、ストラテジーは既存のポジションを閉じます(インジケーターの
UpStop/DnStopバッファーを反映します)。 - ポジションを入力した後、最新の終値と
{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();
}
}
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()