在 GitHub 上查看

Resonance Hunter 策略

概述

Resonance Hunter 是 MetaTrader 专家顾问 Exp_ResonanceHunter 的 StockSharp 版本。每个插槽都会跟踪三组相关货币对的 Stochastic 振荡器,当三个振荡器的动能同向共振时,在主交易品种上建立仓位,另外两组品种用来过滤假信号。一旦主品种的动能反转或触及设定的止损,仓位会被立即平掉。

默认提供三个插槽:

  1. 以 EURUSD 为主,EURJPY 与 USDJPY 作为过滤器。
  2. 以 GBPUSD 为主,GBPJPY 与 USDJPY 作为过滤器。
  3. 以 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 时不启用止损。

交易逻辑

  1. 对每个配置的品种使用所选参数计算 StochasticOscillator,只在已完成的 K 线收盘后更新。
  2. 当三只品种的最新 K 线拥有相同的开盘时间时,比较它们的 %K - %D
    • 差值大于 0 视为向上的动能,小于 0 视为向下的动能。
    • 继承原始指标的附加规则,通过比较动能大小来修正信号。
  3. 当三个动能同时向上时产生做多信号;同时向下时产生做空信号。
  4. 在开新仓之前,如果主品种出现相反的动能,策略会先平掉当前仓位(对应指标的 UpStop/DnStop 缓冲区)。
  5. 开仓后根据 {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();
	}
}