ColorMetroDuplexStrategy 戦略
概要
ColorMetroDuplexStrategy は、MetaTrader 5のエキスパート Exp_ColorMETRO_Duplex をC#に変換したものです。元のロボットはColorMETROインジケーターの2つの独立したインスタンスを使用して、ロングとショートのトレーディングモジュールを管理します。各モジュールは独自のローソク足サブスクリプションで動作し、ColorMETROインジケーターが生成する2つのステップ式RSIエンベロープを評価し、高速と低速のエンベロープがクロスしたときにオプションでポジションを開閉します。
StockSharpバージョンは両モジュールを維持し、同じシグナル評価ルールを再現しながら、ローソク足サブスクリプション、注文管理、インジケーターバインディングにはハイレベルAPIを使用します。MT5のiCustom実装を模倣するカスタム ColorMetroIndicator が含まれており、ColorMETROの高速・低速バンドと内部RSI値を公開します。
動作原理
- 2つの
SignalModule インスタンスが作成されます — Long と Short — それぞれ独自のローソク足シリーズ、ColorMETRO設定、トレード管理オプションを持ちます。
- 戦略が開始されると、各モジュールは設定された時間軸をサブスクライブし、
SubscribeCandles(...).BindEx(...) を通じて ColorMetroIndicator をバインドします。
- 確定したローソク足ごとにインジケーターが生成するもの:
- 高速ColorMETROバンド(高速RSIエンベロープ)。
- 低速ColorMETROバンド(低速RSIエンベロープ)。
- 基礎となるRSI値(参照のみに使用)。
- モジュールはインジケーターの履歴を保存し、設定された
SignalBar シフトを使用して最後の2つの値を評価します(MT5の CopyBuffer ロジックに対応)。
- トレードルール:
- ロングモジュール
- 開く: 前のバーで高速バンドが低速バンドより上にあり、現在は以下または等しい。
- 閉じる: 前のバーで低速バンドが高速バンドより上にあった。
- ショートモジュール
- 開く: 前のバーで高速バンドが低速バンドより下にあり、現在は以上または等しい。
- 閉じる: 前のバーで低速バンドが高速バンドより下にあった。
- 注文は
BuyMarket / SellMarket 経由でルーティングされます。現在のネットポジションが考慮されます — 反対のトレードは新しいポジションを開く前に既存のエクスポージャーを解消します。
パラメーター
各モジュールは専用のパラメーターグループを公開します。デフォルト値はMT5エキスパートを反映します。
共有市場パラメーター
- Long_Volume、Short_Volume — 新規エントリーに使用するトレードサイズ(ロット)。
- Long_OpenAllowed、Short_OpenAllowed — モジュールのトレード開始を有効または無効にします。
- Long_CloseAllowed、Short_CloseAllowed — 自動エグジットを有効または無効にします。
- Long_MarginMode、Short_MarginMode — 互換性のために保持された資金管理モード(このポートでは効果なし)。
- Long_StopLoss、Long_TakeProfit、Long_Deviation、Short_StopLoss、Short_TakeProfit、Short_Deviation — ドキュメント用として予約済み;ストップとスリッページ制御はこのバージョンでは自動化されていません。
- Long_Magic、Short_Magic — 参照用として保存された元のMT5マジックナンバー。
インジケーターパラメーター
- Long_CandleType、Short_CandleType — 各ColorMETROモジュールの時間軸。
- Long_PeriodRSI、Short_PeriodRSI — ColorMETROアルゴリズム内で使用されるRSI長。
- Long_StepSizeFast、Short_StepSizeFast — 高速エンベロープのステップ(RSIポイント単位)。
- Long_StepSizeSlow、Short_StepSizeSlow — 低速エンベロープのステップ。
- Long_SignalBar、Short_SignalBar — インジケーターバッファ読み取り時のバーシフト(MT5の
SignalBar 入力と同一)。
- Long_AppliedPrice、Short_AppliedPrice — RSI計算の価格ソース(デフォルトは終値)。
MT5との違い
- ポジションモデル — StockSharp戦略はネットポジションで動作します。元のエキスパートはマジックナンバーで個別ポジションを保管していましたが、ポートは反対側を開く前に現在のエクスポージャーを解消します。
- 資金管理 — マージンモードと偏差設定はパラメーターとして保持されますが、自動的には適用されません。サイズを制御するには
Volume 入力を使用してください。
- ストップロス / テイクプロフィット — MT5エキスパートは各注文に保護ストップを配置していました。StockSharpバージョンは距離をパラメーターとして参照用に保持しますが、実際のストップ注文が必要な場合は別途実装する必要があります。
- 時間レベル制御 — MT5コードはグローバル変数を使用して、シグナル時間ごとに1つのトレードのみを保証していました。StockSharpでは確定したローソク足ごとに一度処理し、重複エントリーを防ぐためにネットポジションチェックに依存します。
注意事項
- カスタム
ColorMetroIndicator はMT5ロジックを再現し、ステップ式RSIエンベロープとトレンドメモリを含みます。チャートやデバッグ用に高速/低速バンドと内部RSIを公開します。
- コード内のコメントは、ポーティングの決定を明確にし、さらなるカスタマイズを支援するために意図的に詳細に記述されています。
- ストップロスまたはテイクプロフィットの自動化を有効にするには、StockSharpのリスクコントロールを使用して保護注文を配置するよう
SignalModule.ProcessModule を拡張してください。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Conversion of the MT5 expert "Exp_ColorMETRO_Duplex".
/// Uses RSI with step-based envelopes (fast/slow) to generate long/short signals.
/// </summary>
public class ColorMetroDuplexStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _fastStep;
private readonly StrategyParam<int> _slowStep;
private readonly StrategyParam<int> _signalCooldownBars;
// fast envelope state
private decimal? _fastMin, _fastMax;
private int _fastTrend;
private decimal? _prevFastBand;
// slow envelope state
private decimal? _slowMin, _slowMax;
private int _slowTrend;
private decimal? _prevSlowBand;
private int _cooldownRemaining;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }
public int FastStep { get => _fastStep.Value; set => _fastStep.Value = value; }
public int SlowStep { get => _slowStep.Value; set => _slowStep.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public ColorMetroDuplexStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 7)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI lookback", "Indicator");
_fastStep = Param(nameof(FastStep), 8)
.SetGreaterThanZero()
.SetDisplay("Fast Step", "Step size for fast envelope", "Indicator");
_slowStep = Param(nameof(SlowStep), 24)
.SetGreaterThanZero()
.SetDisplay("Slow Step", "Step size for slow envelope", "Indicator");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between reversals", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMin = _fastMax = null;
_slowMin = _slowMax = null;
_fastTrend = _slowTrend = 0;
_prevFastBand = _prevSlowBand = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMin = _fastMax = null;
_slowMin = _slowMax = null;
_fastTrend = _slowTrend = 0;
_prevFastBand = _prevSlowBand = null;
_cooldownRemaining = 0;
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiVal)
{
if (candle.State != CandleStates.Finished)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var fStep = (decimal)FastStep;
var sStep = (decimal)SlowStep;
// fast envelope
var fastMinCand = rsiVal - 2m * fStep;
var fastMaxCand = rsiVal + 2m * fStep;
if (_fastMin == null || _fastMax == null)
{
_fastMin = fastMinCand;
_fastMax = fastMaxCand;
_fastTrend = 0;
_slowMin = rsiVal - 2m * sStep;
_slowMax = rsiVal + 2m * sStep;
_slowTrend = 0;
return;
}
// fast trend
if (rsiVal > _fastMax) _fastTrend = 1;
else if (rsiVal < _fastMin) _fastTrend = -1;
if (_fastTrend > 0 && fastMinCand < _fastMin) fastMinCand = _fastMin.Value;
else if (_fastTrend < 0 && fastMaxCand > _fastMax) fastMaxCand = _fastMax.Value;
// slow envelope
var slowMinCand = rsiVal - 2m * sStep;
var slowMaxCand = rsiVal + 2m * sStep;
if (rsiVal > _slowMax) _slowTrend = 1;
else if (rsiVal < _slowMin) _slowTrend = -1;
if (_slowTrend > 0 && slowMinCand < _slowMin) slowMinCand = _slowMin.Value;
else if (_slowTrend < 0 && slowMaxCand > _slowMax) slowMaxCand = _slowMax.Value;
// compute band values
decimal? fastBand = null;
if (_fastTrend > 0) fastBand = fastMinCand + fStep;
else if (_fastTrend < 0) fastBand = fastMaxCand - fStep;
decimal? slowBand = null;
if (_slowTrend > 0) slowBand = slowMinCand + sStep;
else if (_slowTrend < 0) slowBand = slowMaxCand - sStep;
_fastMin = fastMinCand;
_fastMax = fastMaxCand;
_slowMin = slowMinCand;
_slowMax = slowMaxCand;
if (fastBand == null || slowBand == null)
{
_prevFastBand = fastBand;
_prevSlowBand = slowBand;
return;
}
if (_prevFastBand == null || _prevSlowBand == null)
{
_prevFastBand = fastBand;
_prevSlowBand = slowBand;
return;
}
var up = fastBand.Value;
var down = slowBand.Value;
var prevUp = _prevFastBand.Value;
var prevDown = _prevSlowBand.Value;
_prevFastBand = fastBand;
_prevSlowBand = slowBand;
// Long signal: fast crosses below slow (up crosses down downward)
var longOpen = prevUp > prevDown && up <= down;
// Short signal: fast crosses above slow (up crosses down upward)
var shortOpen = prevUp < prevDown && up >= down;
if (_cooldownRemaining == 0 && longOpen && Position == 0)
{
BuyMarket();
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && shortOpen && Position == 0)
{
SellMarket();
_cooldownRemaining = SignalCooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class color_metro_duplex_strategy(Strategy):
def __init__(self):
super(color_metro_duplex_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._rsi_period = self.Param("RsiPeriod", 7) \
.SetDisplay("RSI Period", "RSI lookback", "Indicator")
self._fast_step = self.Param("FastStep", 8) \
.SetDisplay("Fast Step", "Step size for fast envelope", "Indicator")
self._slow_step = self.Param("SlowStep", 24) \
.SetDisplay("Slow Step", "Step size for slow envelope", "Indicator")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4) \
.SetDisplay("Signal Cooldown", "Bars to wait between reversals", "Trading")
self._fast_min = None
self._fast_max = None
self._fast_trend = 0
self._prev_fast_band = None
self._slow_min = None
self._slow_max = None
self._slow_trend = 0
self._prev_slow_band = None
self._cooldown_remaining = 0
@property
def CandleType(self):
return self._candle_type.Value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@property
def FastStep(self):
return self._fast_step.Value
@property
def SlowStep(self):
return self._slow_step.Value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
def OnReseted(self):
super(color_metro_duplex_strategy, self).OnReseted()
self._fast_min = None
self._fast_max = None
self._fast_trend = 0
self._prev_fast_band = None
self._slow_min = None
self._slow_max = None
self._slow_trend = 0
self._prev_slow_band = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(color_metro_duplex_strategy, self).OnStarted2(time)
self._fast_min = None
self._fast_max = None
self._fast_trend = 0
self._prev_fast_band = None
self._slow_min = None
self._slow_max = None
self._slow_trend = 0
self._prev_slow_band = None
self._cooldown_remaining = 0
rsi = RelativeStrengthIndex()
rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(rsi, self._on_process).Start()
self.StartProtection(Unit(2, UnitTypes.Percent), Unit(1, UnitTypes.Percent))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def _on_process(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
rv = float(rsi_value)
f_step = float(self.FastStep)
s_step = float(self.SlowStep)
fast_min_cand = rv - 2.0 * f_step
fast_max_cand = rv + 2.0 * f_step
if self._fast_min is None or self._fast_max is None:
self._fast_min = fast_min_cand
self._fast_max = fast_max_cand
self._fast_trend = 0
self._slow_min = rv - 2.0 * s_step
self._slow_max = rv + 2.0 * s_step
self._slow_trend = 0
return
if rv > self._fast_max:
self._fast_trend = 1
elif rv < self._fast_min:
self._fast_trend = -1
if self._fast_trend > 0 and fast_min_cand < self._fast_min:
fast_min_cand = self._fast_min
elif self._fast_trend < 0 and fast_max_cand > self._fast_max:
fast_max_cand = self._fast_max
slow_min_cand = rv - 2.0 * s_step
slow_max_cand = rv + 2.0 * s_step
if rv > self._slow_max:
self._slow_trend = 1
elif rv < self._slow_min:
self._slow_trend = -1
if self._slow_trend > 0 and slow_min_cand < self._slow_min:
slow_min_cand = self._slow_min
elif self._slow_trend < 0 and slow_max_cand > self._slow_max:
slow_max_cand = self._slow_max
fast_band = None
if self._fast_trend > 0:
fast_band = fast_min_cand + f_step
elif self._fast_trend < 0:
fast_band = fast_max_cand - f_step
slow_band = None
if self._slow_trend > 0:
slow_band = slow_min_cand + s_step
elif self._slow_trend < 0:
slow_band = slow_max_cand - s_step
self._fast_min = fast_min_cand
self._fast_max = fast_max_cand
self._slow_min = slow_min_cand
self._slow_max = slow_max_cand
if fast_band is None or slow_band is None:
self._prev_fast_band = fast_band
self._prev_slow_band = slow_band
return
if self._prev_fast_band is None or self._prev_slow_band is None:
self._prev_fast_band = fast_band
self._prev_slow_band = slow_band
return
up = fast_band
down = slow_band
prev_up = self._prev_fast_band
prev_down = self._prev_slow_band
self._prev_fast_band = fast_band
self._prev_slow_band = slow_band
long_open = prev_up > prev_down and up <= down
short_open = prev_up < prev_down and up >= down
if self._cooldown_remaining == 0 and long_open and self.Position == 0:
self.BuyMarket()
self._cooldown_remaining = self.SignalCooldownBars
elif self._cooldown_remaining == 0 and short_open and self.Position == 0:
self.SellMarket()
self._cooldown_remaining = self.SignalCooldownBars
def CreateClone(self):
return color_metro_duplex_strategy()