KDJ エキスパートアドバイザー戦略
概要
この戦略はsenlin geによるMetaTrader 5「KDJ Expert Advisor」を再現します。%Kラインが2回平滑化されるストキャスティクスオシレーターの進化形であるKDJオシレーターからのシグナルを使用して単一のシンボルで取引します。戦略は%Kと%Dライン(しばしばJラインと呼ばれる)の差を観察してモメンタム反転を識別し、一度に1つのポジションのみをオープンします。取引管理は元のエキスパートアドバイザーを反映します:各取引はpipsで表現され、インスツルメントの設定を使用して価格距離に変換された固定ストップロスとテイクプロフィットを即座に受け取ります。
実装はローソク足サブスクリプションとMQL5バージョンのKDJパラメーターに一致するよう設定された組み込みのStochasticインジケーターを使用してStockSharpの高レベルAPIを使用します。コードは3桁または5桁のFORXシンボルを自動的に検出し、それに応じてpip値を調整します。
インジケーターロジック
基礎となるインジケーターは3段階で動作します:
- RSV計算 – 各完成したローソク足に対して、
KDJ Lengthローソク足のRaw Stochastic Valueを計算します:
[
RSV = \frac{Close - LowestLow}{HighestHigh - LowestLow} \times 100
]
- %K平滑化 – %Kラインを得るために最後の
Smooth %K個のRSV値を平均します。
- %D平滑化 – %Dラインを得るために最後の
Smooth %D個の%K値を平均します。
戦略は次にK - D(元のソースでKDCと呼ばれる)と%Kの傾きを分析して反転を検出します。
エントリー条件
シンボルに対して既存のポジションがない場合のみ市場ポジションがオープンされます。シグナルは完成したローソク足で評価されます:
- 買いは以下のいずれかの条件が真の場合:
K - Dがゼロを上向きにクロスする(ネガティブからポジティブへ);または
K - Dがゼロより上で、%Kラインが上昇している(K_current > K_previous)。
- 売りは以下のいずれかの条件が真の場合:
K - Dがゼロを下向きにクロスする(ポジティブからネガティブへ);または
K - Dがゼロより下で、%Kラインが下落している(K_current < K_previous)。
これは元のMQL5エキスパートアドバイザーのブール構造に一致し、同一の取引タイミングを保証します。
リスク管理
- 各約定した注文は保護ストップロスとテイクプロフィットを受け取り、pipsで測定され、インスツルメントのティックサイズを通じて価格距離に変換されます。ゼロ値は対応する保護レグを無効にします。
- 戦略はピラミッドや平均コストを行いません。現在のポジションが保護注文または手動介入によって閉じられるまでフラットを維持します。
パラメーター
| パラメーター |
説明 |
デフォルト値 |
| Candle Type |
入力ローソク足のデータタイプ/時間軸。 |
15分時間軸 |
| KDJ Length |
RSV計算のローソク足数。 |
30 |
| Smooth %K |
%Kラインを平滑化するために使用されるRSV値の数。 |
3 |
| Smooth %D |
%Dラインを平滑化するために使用される%K値の数。 |
6 |
| Stop Loss (pips) |
保護ストップロスの距離。無効にするには0に設定。 |
25 |
| Take Profit (pips) |
保護テイクプロフィットの距離。無効にするには0に設定。 |
45 |
| Order Volume |
市場注文で送信される数量。 |
1 |
すべてのパラメーターは元のエキスパートアドバイザーの入力と同一の最適化範囲をサポートします。
使用上の注意
- テスターまたはライブ環境で希望のインスツルメントとコネクターを設定します。
- MetaTraderからエミュレートしたいチャートの時間軸に合わせてローソク足タイプを調整します。
- オプションでKDJパラメーター、ストップロス、テイクプロフィット、または注文ボリュームを最適化します。
- 戦略を開始します。注文は完全に形成されたローソク足でのみ生成されます。
- チャートには視覚的な確認のためにローソク足、KDJインジケーター、および実行された取引が自動的に表示されます。
元のEAとの違い
- MQL5 KDJバッファーを複製するために平滑化期間付きのStockSharpの
Stochasticインジケーターを使用します;外部インジケーターファイルは不要です。
- 保護注文は
StartProtectionを通じて管理され、トリガーされると市場決済を送信します。
- ボリュームはMQL5の
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()