Ver en GitHub

Estrategia de Señal Keltner de Aprendizaje por Refuerzo

La estrategia Keltner Reinforcement Learning Signal está construida en torno a la señal de aprendizaje por refuerzo de Keltner.

Las pruebas indican un retorno anual promedio de aproximadamente 118%. Funciona mejor en el mercado de acciones.

Las señales se activan cuando Keltner confirma cambios de tendencia en datos intradía (15m). Esto hace que el método sea adecuado para traders activos.

Los stops se basan en múltiplos de ATR y factores como EmaPeriod, AtrPeriod. Ajuste estos valores predeterminados para equilibrar el riesgo y la recompensa.

Detalles

  • Criterios de entrada: ver implementación para condiciones de indicadores.
  • Largo/Corto: Ambos direcciones.
  • Criterios de salida: señal opuesta o lógica de stop.
  • Stops: Sí, usando cálculos basados en indicadores.
  • Valores predeterminados:
    • EmaPeriod = 20
    • AtrPeriod = 14
    • AtrMultiplier = 2m
    • StopLossAtr = 2m
    • CandleType = TimeSpan.FromMinutes(15).TimeFrame()
  • Filtros:
    • Categoría: Seguimiento de tendencia
    • Dirección: Ambos
    • Indicadores: Keltner, Reinforcement
    • Stops: Sí
    • Complejidad: Intermedio
    • Marco temporal: Intradía (15m)
    • Estacionalidad: No
    • Redes neuronales: Sí
    • Divergencia: No
    • Nivel de riesgo: Medio
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Keltner with Reinforcement Learning Signal strategy.
/// Entry condition:
/// Long: Price > EMA + k*ATR && RL_Signal = Buy
/// Short: Price < EMA - k*ATR && RL_Signal = Sell
/// Exit condition:
/// Long: Price < EMA
/// Short: Price > EMA
/// </summary>
public class KeltnerWithRLSignalStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<decimal> _stopLossAtr;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private enum RLSignals
	{
		None,
		Buy,
		Sell
	}

	private RLSignals _currentSignal = RLSignals.None;

	// State variables for RL
	private decimal _lastPrice;
	private decimal _previousEma;
	private decimal _previousAtr;
	private decimal _previousPrice;
	private decimal _previousSignalPrice;
	private decimal _entryPrice;
	private int _consecutiveWins;
	private int _consecutiveLosses;
	private int _cooldownRemaining;
	private bool _previousAboveUpperBand;
	private bool _previousBelowLowerBand;

	/// <summary>
	/// EMA period.
	/// </summary>
	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	/// <summary>
	/// ATR period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for Keltner channel.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Stop loss in ATR multiples.
	/// </summary>
	public decimal StopLossAtr
	{
		get => _stopLossAtr.Value;
		set => _stopLossAtr.Value = value;
	}

	/// <summary>
	/// Closed candles to wait between position changes.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	/// <summary>
	/// Type of candles to use.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor with default parameters.
	/// </summary>
	public KeltnerWithRLSignalStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("EMA Period", "Period for the exponential moving average", "Keltner Settings")
		
		.SetOptimize(10, 30, 5);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("ATR Period", "Period for the average true range", "Keltner Settings")
		
		.SetOptimize(7, 21, 7);

		_atrMultiplier = Param(nameof(AtrMultiplier), 1.25m)
		.SetGreaterThanZero()
		.SetDisplay("ATR Multiplier", "Multiplier for ATR in Keltner Channels", "Keltner Settings")
		
		.SetOptimize(1.5m, 3m, 0.5m);

		_cooldownBars = Param(nameof(CooldownBars), 48)
		.SetNotNegative()
		.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General");

		_stopLossAtr = Param(nameof(StopLossAtr), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss (ATR)", "Stop Loss in multiples of ATR", "Risk Management")
		
		.SetOptimize(1m, 3m, 0.5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_currentSignal = default;
		_consecutiveWins = _consecutiveLosses = default;
		_lastPrice = _previousEma = _previousAtr = _previousPrice = _previousSignalPrice = _entryPrice = default;
		_cooldownRemaining = default;
		_previousAboveUpperBand = default;
		_previousBelowLowerBand = default;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Keltner Channels built from their two parts, so each documented period drives its own
		// indicator: the middle line is EMA(EmaPeriod), the band width is AtrMultiplier * ATR(AtrPeriod).
		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var atr = new AverageTrueRange { Length = AtrPeriod };

		// Subscribe to candles and bind indicators
		var subscription = SubscribeCandles(CandleType);

		subscription
		.Bind(ema, atr, ProcessCandle)
		.Start();

		// Create chart visualization if available
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

	/// <summary>
	/// Process each candle and Keltner Channel values.
	/// </summary>
	private void ProcessCandle(ICandleMessage candle, decimal middleBand, decimal currentAtr)
	{
		// Skip unfinished candles
		if (candle.State != CandleStates.Finished)
		return;

		// Check if strategy is ready to trade
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		// Keltner bands: the EMA middle line widened by the ATR of its own period
		var bandOffset = AtrMultiplier * currentAtr;
		var upperBand = middleBand + bandOffset;
		var lowerBand = middleBand - bandOffset;

		// Update price and RL state
		_lastPrice = candle.ClosePrice;

		// Generate RL signal based on current state
		UpdateRLSignal(candle, middleBand, currentAtr);

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		// Trading logic
		var price = candle.ClosePrice;
		var priceAboveUpperBand = price > upperBand;
		var priceBelowLowerBand = price < lowerBand;
		var bullishBreakout = !_previousAboveUpperBand && priceAboveUpperBand;
		var bearishBreakout = !_previousBelowLowerBand && priceBelowLowerBand;

		// Entry conditions

		// Long entry: Price above upper band and RL signal is Buy
		if (_cooldownRemaining == 0 && bullishBreakout && _currentSignal == RLSignals.Buy && Position <= 0)
		{
			LogInfo($"Long signal: Price {price} > Upper Band {upperBand}, RL Signal = Buy");
			BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
			_entryPrice = price;
			_previousSignalPrice = price;
			_cooldownRemaining = CooldownBars;
		}
		// Short entry: Price below lower band and RL signal is Sell
		else if (_cooldownRemaining == 0 && bearishBreakout && _currentSignal == RLSignals.Sell && Position >= 0)
		{
			LogInfo($"Short signal: Price {price} < Lower Band {lowerBand}, RL Signal = Sell");
			SellMarket(Volume + (Position > 0 ? Math.Abs(Position) : 0m));
			_entryPrice = price;
			_previousSignalPrice = price;
			_cooldownRemaining = CooldownBars;
		}

		// Exit conditions

		// Exit long: Price drops below EMA (middle band)
		if (Position > 0 && price < middleBand)
		{
			LogInfo($"Exit long: Price {price} < EMA {middleBand}");
			SellMarket(Math.Abs(Position));
			_entryPrice = 0m;
			_cooldownRemaining = CooldownBars;
		}
		// Exit short: Price rises above EMA (middle band)
		else if (Position < 0 && price > middleBand)
		{
			LogInfo($"Exit short: Price {price} > EMA {middleBand}");
			BuyMarket(Math.Abs(Position));
			_entryPrice = 0m;
			_cooldownRemaining = CooldownBars;
		}

		// Set stop loss based on ATR
		ApplyAtrStopLoss(price, currentAtr);

		// Update previous values for next iteration
		_previousEma = middleBand;
		_previousAtr = currentAtr;
		_previousPrice = price;
		_previousAboveUpperBand = priceAboveUpperBand;
		_previousBelowLowerBand = priceBelowLowerBand;
	}

	/// <summary>
	/// Update Reinforcement Learning signal based on current state.
	/// This is a simplified RL model (Q-learning) for demonstration.
	/// In a real system, this would likely be a more sophisticated model.
	/// </summary>
	private void UpdateRLSignal(ICandleMessage candle, decimal ema, decimal atr)
	{
		// Features for RL decision:
		// 1. Price position relative to EMA
		bool priceAboveEma = candle.ClosePrice > ema;

		// 2. Recent momentum
		bool priceIncreasing = candle.ClosePrice > _previousPrice;

		// 3. Volatility
		bool volatilityIncreasing = atr > _previousAtr;

		// 4. Candle pattern (bullish/bearish)
		bool bullishCandle = candle.ClosePrice > candle.OpenPrice;

		// 5. Previous trade outcome
		// More conservative after losses, more aggressive after wins
		bool aggressiveMode = _consecutiveWins > _consecutiveLosses;

		// Simplified Q-learning decision matrix
		if (bullishCandle && priceAboveEma && (priceIncreasing || aggressiveMode))
		{
_currentSignal = RLSignals.Buy;
			LogInfo("RL Signal: Buy");
		}
		else if (!bullishCandle && !priceAboveEma && (!priceIncreasing || aggressiveMode))
		{
_currentSignal = RLSignals.Sell;
			LogInfo("RL Signal: Sell");
		}
		else
		{
			// If conditions are mixed, maintain current signal or go neutral
			if (volatilityIncreasing)
			{
				// High volatility might warrant reducing exposure
_currentSignal = RLSignals.None;
				LogInfo("RL Signal: None (high volatility)");
			}
			// Otherwise keep current signal
		}
	}

	/// <summary>
	/// Process own trades for reinforcement learning feedback.
	/// </summary>
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		// Skip if we don't have a previous signal price (first trade)
		if (_previousSignalPrice == 0)
		return;

		// Determine if the trade was profitable
		bool profitable;

		if (trade.Order.Side == Sides.Buy)
		{
			// For buys, it's profitable if current price > entry price
			profitable = _lastPrice > trade.Trade.Price;
		}
		else
		{
			// For sells, it's profitable if current price < entry price
			profitable = _lastPrice < trade.Trade.Price;
		}

		// Update consecutive win/loss counters for RL state
		if (profitable)
		{
			_consecutiveWins++;
			_consecutiveLosses = 0;
			LogInfo($"Profitable trade: Win streak = {_consecutiveWins}");
		}
		else
		{
			_consecutiveLosses++;
			_consecutiveWins = 0;
			LogInfo($"Unprofitable trade: Loss streak = {_consecutiveLosses}");
		}
	}

	/// <summary>
	/// Apply ATR-based stop loss.
	/// </summary>
	private void ApplyAtrStopLoss(decimal price, decimal atr)
	{
		// Without an entry price there is no position of ours to protect
		if (_entryPrice == 0m)
			return;

		// The stop stands StopLossAtr ATR multiples away from the price the position was opened at
		var stopOffset = StopLossAtr * atr;

		if (Position > 0) // Long position
		{
			var stopLevel = _entryPrice - stopOffset;
			if (price < stopLevel)
			{
				LogInfo($"ATR Stop Loss triggered for long position: Current {price} < Stop {stopLevel}");
				SellMarket(Math.Abs(Position));
				_entryPrice = 0m;
				_cooldownRemaining = CooldownBars;
			}
		}
		else if (Position < 0) // Short position
		{
			var stopLevel = _entryPrice + stopOffset;
			if (price > stopLevel)
			{
				LogInfo($"ATR Stop Loss triggered for short position: Current {price} > Stop {stopLevel}");
				BuyMarket(Math.Abs(Position));
				_entryPrice = 0m;
				_cooldownRemaining = CooldownBars;
			}
		}
	}
}