Estrategia del Asesor Experto KDJ
Descripción general
Esta estrategia replica el MetaTrader 5 «KDJ Expert Advisor» de senlin ge. Opera en un solo símbolo usando señales del oscilador KDJ, una evolución del oscilador estocástico donde la línea %K se suaviza dos veces. La estrategia observa la diferencia entre las líneas %K y %D (a menudo llamada línea J) para identificar reversiones de impulso, abriendo solo una posición a la vez. La gestión de operaciones refleja el asesor experto original: cada operación recibe inmediatamente un stop-loss fijo y take-profit expresados en pips y traducidos a distancia de precio usando la configuración del instrumento.
La implementación usa la API de alto nivel de StockSharp con una suscripción de velas y el indicador Stochastic integrado, configurado para coincidir con los parámetros KDJ de la versión MQL5. El código detecta automáticamente símbolos Forex de 3 o 5 dígitos y ajusta el valor del pip en consecuencia.
Lógica del indicador
El indicador subyacente funciona en tres etapas:
- Cálculo RSV – Para cada vela terminada, calcular el Valor Estocástico Bruto en
KDJ Length velas:
[
RSV = \frac{Close - LowestLow}{HighestHigh - LowestLow} \times 100
]
- Suavizado %K – Promediar los últimos valores
Smooth %K de RSV para obtener la línea %K.
- Suavizado %D – Promediar los últimos valores
Smooth %D de %K para obtener la línea %D.
La estrategia luego analiza K - D (referido como KDC en la fuente original) y la pendiente de %K para detectar reversiones.
Criterios de entrada
Una posición de mercado se abre solo si no hay ninguna posición existente para el símbolo. Las señales se evalúan en velas completadas:
- Compra cuando cualquiera de las siguientes condiciones es verdadera:
K - D cruza por encima de cero (de negativo a positivo); o
K - D está por encima de cero y la línea %K está subiendo (K_current > K_previous).
- Venta cuando cualquiera de las siguientes condiciones es verdadera:
K - D cruza por debajo de cero (de positivo a negativo); o
K - D está por debajo de cero y la línea %K está bajando (K_current < K_previous).
Esto coincide con la estructura booleana del asesor experto MQL5 original, garantizando un timing de operación idéntico.
Gestión de riesgos
- Cada orden ejecutada recibe un stop-loss protector y take-profit, medidos en pips y convertidos a distancia de precio a través del tamaño del tick del instrumento. Un valor de cero deshabilita el tramo de protección correspondiente.
- La estrategia no realiza pirámide ni promedia posiciones. Permanece plana hasta que la posición actual sea cerrada por las órdenes protectoras o por intervención manual.
Parámetros
| Parámetro |
Descripción |
Predeterminado |
| Candle Type |
Tipo de datos/marco temporal de las velas de entrada. |
Marco temporal de 15 minutos |
| KDJ Length |
Número de velas para el cálculo RSV. |
30 |
| Smooth %K |
Número de valores RSV usados para suavizar la línea %K. |
3 |
| Smooth %D |
Número de valores %K usados para suavizar la línea %D. |
6 |
| Stop Loss (pips) |
Distancia del stop-loss protector. Establezca en 0 para deshabilitar. |
25 |
| Take Profit (pips) |
Distancia del take-profit protector. Establezca en 0 para deshabilitar. |
45 |
| Order Volume |
Cantidad enviada con las órdenes de mercado. |
1 |
Todos los parámetros admiten rangos de optimización idénticos a las entradas del asesor experto original.
Notas de uso
- Configure el instrumento y el conector deseados en el tester o en el entorno en vivo.
- Ajuste el tipo de vela para coincidir con el marco temporal del gráfico que desea emular desde MetaTrader.
- Opcionalmente optimice los parámetros KDJ, stop-loss, take-profit o volumen de la orden.
- Inicie la estrategia. Las órdenes se generan solo en velas completamente formadas.
- El gráfico muestra automáticamente velas, el indicador KDJ y las operaciones ejecutadas para confirmación visual.
Diferencias con el EA original
- Usa el indicador
Stochastic de StockSharp con períodos de suavizado para replicar los buffers KDJ de MQL5; no se requiere ningún archivo de indicador externo.
- Las órdenes protectoras se gestionan a través de
StartProtection, que envía salidas de mercado cuando se activan.
- El volumen es un parámetro fijo en lugar del modelo de riesgo
MoneyFixedMargin de MQL5, manteniendo la implementación concisa y enfocada en la lógica de señal.
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>
/// Strategy that replicates the MetaTrader KDJ Expert Advisor logic.
/// Uses the KDJ oscillator to detect momentum reversals and opens a single position with fixed take-profit and stop-loss levels.
/// </summary>
public class KdjExpertAdvisorStrategy : Strategy
{
private readonly StrategyParam<int> _kdjPeriod;
private readonly StrategyParam<int> _smoothK;
private readonly StrategyParam<int> _smoothD;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousK;
private decimal? _previousKdc;
private decimal _pipSize;
/// <summary>
/// Main lookback period used to calculate RSV for the KDJ oscillator.
/// </summary>
public int KdjPeriod
{
get => _kdjPeriod.Value;
set => _kdjPeriod.Value = value;
}
/// <summary>
/// Smoothing period applied to the %K line.
/// </summary>
public int SmoothK
{
get => _smoothK.Value;
set => _smoothK.Value = value;
}
/// <summary>
/// Smoothing period applied to the %D line.
/// </summary>
public int SmoothD
{
get => _smoothD.Value;
set => _smoothD.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Volume applied to every market order.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="KdjExpertAdvisorStrategy"/> class.
/// </summary>
public KdjExpertAdvisorStrategy()
{
_kdjPeriod = Param(nameof(KdjPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("KDJ Length", "Lookback period for KDJ RSV calculation", "KDJ")
.SetOptimize(10, 60, 5);
_smoothK = Param(nameof(SmoothK), 3)
.SetGreaterThanZero()
.SetDisplay("Smooth %K", "Smoothing length for %K", "KDJ")
.SetOptimize(1, 10, 1);
_smoothD = Param(nameof(SmoothD), 6)
.SetGreaterThanZero()
.SetDisplay("Smooth %D", "Smoothing length for %D", "KDJ")
.SetOptimize(1, 15, 1);
_stopLossPips = Param(nameof(StopLossPips), 250)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk")
.SetOptimize(0, 1000, 50);
_takeProfitPips = Param(nameof(TakeProfitPips), 450)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk")
.SetOptimize(0, 1500, 50);
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Quantity used for entries", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for KDJ calculation", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousK = null;
_previousKdc = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var stopLossUnit = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : null;
var takeProfitUnit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : null;
StartProtection(
takeProfit: takeProfitUnit,
stopLoss: stopLossUnit,
useMarketOrders: true);
var kdj = new StochasticOscillator
{
K = { Length = KdjPeriod },
D = { Length = SmoothD }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(kdj, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, kdj);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue kdjValue)
{
if (candle.State != CandleStates.Finished)
return;
var stochastic = (StochasticOscillatorValue)kdjValue;
if (stochastic.K is not decimal k || stochastic.D is not decimal d)
return;
var kdc = k - d;
var buySignal = false;
var sellSignal = false;
if (_previousKdc.HasValue)
{
buySignal |= _previousKdc.Value < 0m && kdc > 0m;
sellSignal |= _previousKdc.Value > 0m && kdc < 0m;
}
if (_previousK.HasValue)
{
buySignal |= kdc > 0m && _previousK.Value < k;
sellSignal |= kdc < 0m && _previousK.Value > k;
}
if (buySignal || sellSignal)
{
if (Position == 0)
{
if (buySignal)
{
LogInfo($"Buy signal at {candle.ClosePrice}: K={k:F2}, D={d:F2}, K-D={kdc:F2}");
BuyMarket();
}
else if (sellSignal)
{
LogInfo($"Sell signal at {candle.ClosePrice}: K={k:F2}, D={d:F2}, K-D={kdc:F2}");
SellMarket();
}
}
}
_previousK = k;
_previousKdc = kdc;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 1m;
var step = security.PriceStep ?? 1m;
var decimals = security.Decimals;
var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
return step * multiplier;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class kdj_expert_advisor_strategy(Strategy):
"""
KDJ Expert Advisor: uses Stochastic Oscillator K/D crossover
for momentum reversals with pip-based SL/TP via StartProtection.
"""
def __init__(self):
super(kdj_expert_advisor_strategy, self).__init__()
self._kdj_period = self.Param("KdjPeriod", 30) \
.SetDisplay("KDJ Length", "Lookback period for KDJ", "KDJ")
self._smooth_d = self.Param("SmoothD", 6) \
.SetDisplay("Smooth %D", "Smoothing for %D", "KDJ")
self._stop_loss_pips = self.Param("StopLossPips", 250) \
.SetDisplay("Stop Loss (pips)", "Stop distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 450) \
.SetDisplay("Take Profit (pips)", "Profit target in pips", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for KDJ", "Data")
self._prev_k = None
self._prev_kdc = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(kdj_expert_advisor_strategy, self).OnReseted()
self._prev_k = None
self._prev_kdc = None
def _calculate_pip_size(self):
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 1.0
decimals = 0
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
multiplier = 10.0 if decimals in (3, 5) else 1.0
return step * multiplier
def OnStarted2(self, time):
super(kdj_expert_advisor_strategy, self).OnStarted2(time)
pip_size = self._calculate_pip_size()
sl_pips = self._stop_loss_pips.Value
tp_pips = self._take_profit_pips.Value
tp_val = Decimal(float(tp_pips) * pip_size) if tp_pips > 0 else Decimal(0)
sl_val = Decimal(float(sl_pips) * pip_size) if sl_pips > 0 else Decimal(0)
tp_unit = Unit(tp_val, UnitTypes.Absolute) if tp_pips > 0 else Unit()
sl_unit = Unit(sl_val, UnitTypes.Absolute) if sl_pips > 0 else Unit()
self.StartProtection(tp_unit, sl_unit)
kdj = StochasticOscillator()
kdj.K.Length = self._kdj_period.Value
kdj.D.Length = self._smooth_d.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(kdj, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, kdj)
self.DrawOwnTrades(area)
def _process_candle(self, candle, kdj_value):
if candle.State != CandleStates.Finished:
return
k = kdj_value.K
d = kdj_value.D
if k is None or d is None:
return
k = float(k)
d = float(d)
kdc = k - d
buy_signal = False
sell_signal = False
if self._prev_kdc is not None:
if self._prev_kdc < 0 and kdc > 0:
buy_signal = True
if self._prev_kdc > 0 and kdc < 0:
sell_signal = True
if self._prev_k is not None:
if kdc > 0 and self._prev_k < k:
buy_signal = True
if kdc < 0 and self._prev_k > k:
sell_signal = True
pos = float(self.Position)
if abs(pos) < 0.0001:
if buy_signal:
self.BuyMarket()
elif sell_signal:
self.SellMarket()
self._prev_k = k
self._prev_kdc = kdc
def CreateClone(self):
return kdj_expert_advisor_strategy()