Стратегия KDJ Expert Advisor
Обзор
Стратегия полностью повторяет MetaTrader 5 эксперт "KDJ Expert Advisor" от senlin ge. Используется осциллятор KDJ — модифицированный стохастик с двойным сглаживанием линии %K. Алгоритм отслеживает разницу между линиями %K и %D (в оригинале — буфер KDC/J) и открывает только одну позицию, когда обнаруживает разворот импульса. Каждая сделка сразу получает фиксированные уровни стоп-лосса и тейк-профита, задаваемые в пунктах и автоматически переводимые в абсолютное расстояние по цене.
Реализация построена на высокоуровневом API StockSharp: подключение свечной подписки, использование встроенного индикатора Stochastic с параметрами, идентичными MQL5-версии, и автоматический подбор pip-значения для инструментов с 3 или 5 знаками после запятой.
Логика индикатора
Расчёт KDJ состоит из трёх шагов:
- RSV — вычисление Raw Stochastic Value за
KDJ Length последних свечей.
- %K — усреднение последних
Smooth %K значений RSV.
- %D — усреднение последних
Smooth %D значений %K.
Далее стратегия анализирует величину K - D и изменение наклона %K, чтобы определить моменты разворота.
Правила входа
Новые сделки открываются только при отсутствии текущей позиции. Сигналы проверяются на закрывшихся свечах:
- Покупка, если выполняется одно из условий:
K - D пересекает нулевой уровень снизу вверх;
K - D уже положительное, а %K растёт (K_current > K_previous).
- Продажа, если выполняется одно из условий:
K - D пересекает ноль сверху вниз;
K - D уже отрицательное, а %K падает (K_current < K_previous).
Такой набор логических условий соответствует оригинальному MQL5 коду, обеспечивая совпадение точек входа.
Управление риском
- Для каждой сделки выставляются защитный стоп и тейк, расстояние задаётся в пунктах. Значение 0 отключает соответствующий уровень.
- Стратегия не усредняется и не наращивает позицию: после входа она ждёт, пока защитные ордера или ручное вмешательство закроют позицию.
Параметры
| Параметр |
Описание |
Значение по умолчанию |
| Candle Type |
Тип/таймфрейм свечей для расчётов. |
Свечи 15 минут |
| KDJ Length |
Длина окна для RSV. |
30 |
| Smooth %K |
Количество RSV для сглаживания %K. |
3 |
| Smooth %D |
Количество %K для сглаживания %D. |
6 |
| Stop Loss (pips) |
Стоп-лосс в пунктах. 0 — отключено. |
25 |
| Take Profit (pips) |
Тейк-профит в пунктах. 0 — отключено. |
45 |
| Order Volume |
Объём рыночной заявки. |
1 |
Каждый параметр имеет диапазоны оптимизации, повторяющие настройки оригинального советника.
Рекомендации по использованию
- Настройте соединение и выберите инструмент в тестере или в рабочем подключении.
- Установите
Candle Type в соответствии с таймфреймом, который использовался в MetaTrader.
- При необходимости запустите оптимизацию параметров KDJ, стоп-лосса, тейк-профита или объёма.
- Запустите стратегию — сигналы рассчитываются только на завершённых свечах.
- На графике автоматически отображаются свечи, индикатор KDJ и совершённые сделки для визуального контроля.
Отличия от оригинального EA
- Используется встроенный индикатор
Stochastic, поэтому не требуется отдельный файл индикатора.
- Управление стоп-лоссом и тейк-профитом реализовано через
StartProtection, который отправляет рыночные ордера при срабатывании уровней.
- Объём сделок задаётся фиксированным параметром вместо алгоритма
MoneyFixedMargin, что упрощает пример и концентрируется на торговой логике.
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()