Ver en GitHub

Estrategia del cazador de resonancia

Descripción general

La estrategia Resonance Hunter es el StockSharp puerto del MetaTrader asesor experto Exp_ResonanceHunter. Supervisa tres pares de divisas correlacionados por ranura y busca impulso sincrónico en sus osciladores Stochastic. Cuando los osciladores resuenan en la misma dirección, la estrategia abre una posición en el símbolo principal, mientras que los símbolos secundario y de confirmación actúan como filtros. La operación se cierra tan pronto como el instrumento principal pierde impulso o cuando se alcanza el stop loss configurado.

Hay tres ranuras preconfiguradas:

  1. El EURUSD negoció con EURJPY y USDJPY como confirmaciones.
  2. GBPUSD negoció con GBPJPY y USDJPY.
  3. AUDUSD negoció con AUDJPY y USDJPY.

Cada ranura se puede habilitar o deshabilitar de forma independiente y puede usar su propio período de tiempo y parámetros de indicador.

Parámetros

Todos los parámetros están agrupados por ranura (Ranura 1–3). Cada grupo comparte las siguientes configuraciones:

Parámetro Descripción
{Slot} Enabled Permite el comercio de la ranura.
{Slot} Primary Instrumento negociado por la estrategia y utilizado para señales de salida.
{Slot} Secondary Segundo instrumento que participa en la verificación de resonancia.
{Slot} Confirmation Tercer instrumento utilizado en la verificación de resonancia.
{Slot} Candle Type Plazo aplicado a los tres instrumentos (predeterminado = 1 hora).
{Slot} K Period Stochastic %K retrospectiva.
{Slot} D Period Período de suavizado para %D.
{Slot} Slowing Suavizado adicional para %K.
{Slot} Volume Volumen de pedidos en lotes. La exposición opuesta existente se compensa.
{Slot} Stop Loss Distancia de stop-loss estilo MetaTrader en puntos. Establezca en 0 para desactivar la parada de protección.

Lógica de trading

  1. Para cada instrumento configurado, se calcula un StochasticOscillator con los parámetros seleccionados en las velas completadas.
  2. Una vez que las últimas velas de los tres instrumentos comparten el mismo tiempo de apertura, se evalúan las diferencias %K - %D:
    • La diferencia positiva marca un impulso ascendente (Up), la diferencia negativa marca un impulso a la baja (Down).
    • Reglas de consistencia adicionales del indicador original ajustan los impulsos comparando la magnitud de cada par.
  3. Una entrada larga se genera cuando los tres impulsos apuntan hacia arriba. Aparece una entrada corta cuando los tres impulsos apuntan hacia abajo.
  4. Antes de enviar nuevas órdenes, la estrategia cierra las posiciones existentes si el símbolo principal indica un impulso opuesto (refleja los buffers UpStop/DnStop del indicador).
  5. Después de ingresar una posición, se calcula un precio de parada de protección utilizando el último cierre y la distancia {Slot} Stop Loss. En cada nueva vela primaria se comprueba el tope y, si se supera, la posición se cierra inmediatamente.

Los pedidos se enrutan a través de BuyMarket/SellMarket. La exposición existente en el símbolo principal se compensa para que la estrategia pueda revertirse directamente cuando sea necesario.

Notas

  • La estrategia requiere datos de velas sincronizados para los tres instrumentos dentro de cada ranura. Si un símbolo va por detrás, la señal se pospone hasta que las marcas de tiempo de las barras se alineen.
  • Los niveles de stop se emulan dentro de la estrategia (no se envían órdenes de stop reales) para que coincidan con el comportamiento MetaTrader.
  • Los valores de parámetros predeterminados reproducen el asesor experto original, pero se pueden optimizar a través de la interfaz 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();
	}
}