Estratégia do Consultor Especialista KDJ
Visão geral
Esta estratégia replica o MetaTrader 5 «KDJ Expert Advisor» de senlin ge. Ela opera em um único símbolo usando sinais do oscilador KDJ, uma evolução do oscilador estocástico onde a linha %K é suavizada duas vezes. A estratégia observa a diferença entre as linhas %K e %D (frequentemente chamada de linha J) para identificar reversões de momentum, abrindo apenas uma posição por vez. O gerenciamento de operações espelha o expert advisor original: cada operação recebe imediatamente um stop-loss fixo e take-profit expressos em pips e traduzidos em distância de preço usando as configurações do instrumento.
A implementação usa a API de alto nível do StockSharp com uma subscrição de velas e o indicador Stochastic integrado, configurado para corresponder aos parâmetros KDJ da versão MQL5. O código detecta automaticamente símbolos Forex de 3 ou 5 dígitos e ajusta o valor do pip de acordo.
Lógica do indicador
O indicador subjacente funciona em três etapas:
- Cálculo RSV – Para cada vela concluída, calcular o Valor Estocástico Bruto em
KDJ Length velas:
[
RSV = \frac{Close - LowestLow}{HighestHigh - LowestLow} \times 100
]
- Suavização %K – Calcular a média dos últimos valores
Smooth %K de RSV para obter a linha %K.
- Suavização %D – Calcular a média dos últimos valores
Smooth %D de %K para obter a linha %D.
A estratégia então analisa K - D (referido como KDC na fonte original) e a inclinação de %K para detectar reversões.
Critérios de entrada
Uma posição de mercado é aberta apenas se não houver posição existente para o símbolo. Os sinais são avaliados em velas concluídas:
- Compra quando qualquer uma das seguintes condições for verdadeira:
K - D cruza acima de zero (de negativo a positivo); ou
K - D está acima de zero e a linha %K está subindo (K_current > K_previous).
- Venda quando qualquer uma das seguintes condições for verdadeira:
K - D cruza abaixo de zero (de positivo a negativo); ou
K - D está abaixo de zero e a linha %K está caindo (K_current < K_previous).
Isso corresponde à estrutura booleana do expert advisor MQL5 original, garantindo timing de operação idêntico.
Gestão de riscos
- Cada ordem executada recebe um stop-loss protetor e take-profit, medidos em pips e convertidos em distância de preço através do tamanho do tick do instrumento. Um valor de zero desabilita o trecho de proteção correspondente.
- A estratégia não faz pirâmide nem média de posições. Permanece zerada até que a posição atual seja fechada pelas ordens protetoras ou por intervenção manual.
Parâmetros
| Parâmetro |
Descrição |
Padrão |
| Candle Type |
Tipo de dados/período das velas de entrada. |
Período de 15 minutos |
| KDJ Length |
Número de velas para o cálculo RSV. |
30 |
| Smooth %K |
Número de valores RSV usados para suavizar a linha %K. |
3 |
| Smooth %D |
Número de valores %K usados para suavizar a linha %D. |
6 |
| Stop Loss (pips) |
Distância do stop-loss protetor. Defina como 0 para desabilitar. |
25 |
| Take Profit (pips) |
Distância do take-profit protetor. Defina como 0 para desabilitar. |
45 |
| Order Volume |
Quantidade enviada com ordens a mercado. |
1 |
Todos os parâmetros suportam intervalos de otimização idênticos às entradas do expert advisor original.
Notas de uso
- Configure o instrumento e o conector desejados no testador ou no ambiente ao vivo.
- Ajuste o tipo de vela para corresponder ao período do gráfico que deseja emular do MetaTrader.
- Opcionalmente, otimize os parâmetros KDJ, stop-loss, take-profit ou volume de ordem.
- Inicie a estratégia. As ordens são geradas apenas em velas completamente formadas.
- O gráfico exibe automaticamente velas, o indicador KDJ e as operações executadas para confirmação visual.
Diferenças do EA original
- Usa o indicador
Stochastic do StockSharp com períodos de suavização para replicar os buffers KDJ do MQL5; nenhum arquivo de indicador externo é necessário.
- As ordens protetoras são gerenciadas através de
StartProtection, que envia saídas a mercado quando acionadas.
- O volume é um parâmetro fixo em vez do modelo de risco
MoneyFixedMargin do MQL5, mantendo a implementação concisa e focada na lógica de sinal.
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()