在 GitHub 上查看

ColorMetroDuplexStrategy

概述

ColorMetroDuplexStrategy 是 MT5 专家顾问 Exp_ColorMETRO_Duplex 的 C# 版本。原始 EA 通过两个独立的 ColorMETRO 指标模块进行多空操作:每个模块订阅自己的 K 线周期,读取指标输出的快慢 RSI 台阶包络线,并在交叉时决定是否开仓或平仓。

移植后的 StockSharp 策略保留了这两个模块,并使用高级 API 处理行情订阅、订单执行和指标绑定。同时实现了自定义的 ColorMetroIndicator,完全复刻 MT5 中的 iCustom 指标逻辑,输出快线、慢线以及内部使用的 RSI 值。

工作流程

  1. 启动时会创建两个 SignalModuleLong(多头模块)和 Short(空头模块),它们具有独立的周期与参数。
  2. 每个模块调用 SubscribeCandles 订阅相应的周期,并使用 BindExColorMetroIndicator 绑定。
  3. 每根收盘的 K 线都会触发一次计算,指标返回:
    • 快速 ColorMETRO 包络线(依据快步长的 RSI 台阶)。
    • 慢速 ColorMETRO 包络线。
    • 指标内部的 RSI 数值(用于参考)。
  4. 模块将最新结果加入历史缓存,并按照 SignalBar 指定的偏移量对比最近两根信号柱,复制 MT5 中 CopyBuffer 的用法。
  5. 交易规则:
    • 多头模块
      • 开仓:前一根信号柱快线在慢线上方,本柱快线下穿或等于慢线。
      • 平仓:前一根信号柱慢线在快线上方。
    • 空头模块
      • 开仓:前一根信号柱快线在慢线下方,本柱快线上穿或等于慢线。
      • 平仓:前一根信号柱慢线在快线下方。
  6. 下单使用 BuyMarket / SellMarket。策略始终检查净头寸,如果需要反向操作,会先平掉已有仓位再建立新仓。

参数说明

每个模块都拥有独立参数组,默认值与 MT5 EA 保持一致。

市场参数

  • Long_VolumeShort_Volume:新开仓的手数。
  • Long_OpenAllowedShort_OpenAllowed:是否允许该模块开仓。
  • Long_CloseAllowedShort_CloseAllowed:是否允许信号自动平仓。
  • Long_MarginModeShort_MarginMode:保留的资金管理模式(本移植中不参与计算)。
  • Long_StopLossLong_TakeProfitLong_Deviation 以及空头对应参数:仅作为文档保留,本版本不会自动设置止损止盈。
  • Long_MagicShort_Magic:原 EA 的魔术号,便于对照。

指标参数

  • Long_CandleTypeShort_CandleType:各模块的 K 线周期。
  • Long_PeriodRSIShort_PeriodRSI:ColorMETRO 内部使用的 RSI 周期。
  • Long_StepSizeFastShort_StepSizeFast:快包络的步长(RSI 点数)。
  • Long_StepSizeSlowShort_StepSizeSlow:慢包络的步长。
  • Long_SignalBarShort_SignalBar:读取指标缓存时的偏移,与 MT5 的 SignalBar 相同。
  • Long_AppliedPriceShort_AppliedPrice:计算 RSI 时使用的价格,默认收盘价。

与 MT5 版本的差异

  • 头寸模型:MT5 可以通过不同魔术号同时持有多空仓位,StockSharp 采用净头寸模式,反手时会先平仓再开仓。
  • 资金管理:保留了 MarginMode、Deviation 等输入,但默认不参与下单计算,仓位大小由 Volume 参数控制。
  • 止损/止盈:原 EA 每次下单都会附带止损止盈。本策略仅记录相应距离,如需自动风控需自行扩展。
  • 时间锁:MT5 使用全局变量避免同一时间重复开仓;在 StockSharp 中我们按收盘柱执行一次逻辑,并通过净头寸判断避免重复信号。

备注

  • ColorMetroIndicator 完全仿真原始算法,包含趋势记忆逻辑,可用于绘图或调试。
  • 代码中提供了详细英文注释,便于进一步修改和优化。
  • 若要加入自动止损、止盈或其他风险控制,可在 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;
		}
	}
}