Resonance Hunter 策略
概述
Resonance Hunter 是 MetaTrader 专家顾问 Exp_ResonanceHunter 的 StockSharp 版本。每个插槽都会跟踪三组相关货币对的 Stochastic 振荡器,当三个振荡器的动能同向共振时,在主交易品种上建立仓位,另外两组品种用来过滤假信号。一旦主品种的动能反转或触及设定的止损,仓位会被立即平掉。
默认提供三个插槽:
- 以 EURUSD 为主,EURJPY 与 USDJPY 作为过滤器。
- 以 GBPUSD 为主,GBPJPY 与 USDJPY 作为过滤器。
- 以 AUDUSD 为主,AUDJPY 与 USDJPY 作为过滤器。
每个插槽都可以独立启用,拥有自己的时间框架和指标参数。
参数
所有参数按插槽(Slot 1–3)分组,包含:
| 参数 | 说明 |
|---|---|
{Slot} Enabled |
是否启用该插槽。 |
{Slot} Primary |
实际交易及退出信号所使用的品种。 |
{Slot} Secondary |
共振计算的第二个品种。 |
{Slot} Confirmation |
共振计算的第三个品种。 |
{Slot} Candle Type |
三个品种统一使用的时间框架(默认 1 小时)。 |
{Slot} K Period |
Stochastic %K 的回溯长度。 |
{Slot} D Period |
%D 平滑周期。 |
{Slot} Slowing |
%K 的附加平滑。 |
{Slot} Volume |
下单手数,遇到反向仓位时会自动对冲。 |
{Slot} Stop Loss |
MetaTrader 风格的止损距离(点)。为 0 时不启用止损。 |
交易逻辑
- 对每个配置的品种使用所选参数计算
StochasticOscillator,只在已完成的 K 线收盘后更新。 - 当三只品种的最新 K 线拥有相同的开盘时间时,比较它们的
%K - %D:- 差值大于 0 视为向上的动能,小于 0 视为向下的动能。
- 继承原始指标的附加规则,通过比较动能大小来修正信号。
- 当三个动能同时向上时产生做多信号;同时向下时产生做空信号。
- 在开新仓之前,如果主品种出现相反的动能,策略会先平掉当前仓位(对应指标的
UpStop/DnStop缓冲区)。 - 开仓后根据
{Slot} Stop Loss计算保护价位,每当主品种生成新 K 线时都会检查该价位,触及即平仓。
策略通过 BuyMarket/SellMarket 下单,并会自动净掉主品种上已存在的反向仓位,方便快速反手。
注意事项
- 三个品种的数据需要时间同步,如有一个品种延迟,信号会延后直到时间戳对齐。
- 止损逻辑在策略内部模拟(不会发送真实的止损委托),从而复现 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()