在 GitHub 上查看

Exp Color TSI Oscillator 策略

概述

  • 将 MetaTrader 5 专家顾问 Exp_ColorTSI-Oscillator 移植到 StockSharp 框架。
  • 重新构建 ColorTSI 指标:对 True Strength Index 进行两层平滑,并加入延迟触发线以及 SmoothAlgorithms.mqh 中提供的全部平滑算法。
  • 当震荡指标相对于延迟触发线出现向上或向下的拐点时产生交易信号,完全复现原策略的“摆动反转”思路。

指标还原

  • ColorTsiAppliedPrice 参数决定输入价格类型(收盘价、开盘价、中价、典型价、Demark 价等)。
  • 价格动量 price[n] - price[n-1] 以及其绝对值采用两级滤波:
    1. 第一级:根据 ColorTsiSmoothingMethod 选择 SmaEmaSmmaLwmaJjmaJurxParmaT3VidyaAma,长度 FirstLength,Jurik 类方法的相位 FirstPhase
    2. 第二级:同样的算法选择,使用 SecondLengthSecondPhase 对第一级输出再次平滑。
  • 振荡值为 TSI = 100 * smoothMomentum / smoothAbsMomentum,当分母为零时跳过该值。
  • 触发线通过将 TSI 延迟 TriggerShift 个柱得到,与 MQL 缓冲区一一对应。
  • 内部历史列表确保 SignalBar 与 MetaTrader 中 CopyBuffer 的索引完全一致(SignalBar 指最近的完整柱,SignalBar + 1 为更早的柱)。

交易规则

  • 仅在 CandleType 指定的已完成蜡烛上运行(默认 4 小时)。
  • TSI[k] 为振荡值、Trigger[k] 为延迟触发线。
  • 多头背景TSI[SignalBar + 1] > Trigger[SignalBar + 1] 表示上一柱动量向上。
    • EnableShortExits 为真,则平掉空头。
    • EnableLongEntries 为真且 TSI[SignalBar] ≤ Trigger[SignalBar],说明回调结束向上拐头,开多头。
  • 空头背景TSI[SignalBar + 1] < Trigger[SignalBar + 1] 表示上一柱动量向下。
    • EnableLongExits 为真,则平掉多头。
    • EnableShortEntries 为真且 TSI[SignalBar] ≥ Trigger[SignalBar],说明顶部转向下,开空头。
  • 信号按照被分析柱的开盘时间加上一个完整周期生成; _lastLongEntryTime / _lastShortEntryTime 防止同一柱重复开仓。
  • 所有操作使用市价单,反向持仓会在开新仓之前被关闭。

参数

参数 说明 默认值
CandleType 用于计算的蜡烛数据类型(任意 DataType)。 4 小时蜡烛
Volume 固定下单量,替代 MQL 中基于资金的 MM 逻辑。 0.1
FirstMethod, FirstLength, FirstPhase 第一级对动量及其绝对值的平滑设置。 SMA, 12, 15
SecondMethod, SecondLength, SecondPhase 第二级平滑设置。 SMA, 12, 15
PriceMode 指标输入价格类型。 Close
SignalBar 评估信号时回看的柱偏移量(1 = 最近一根完整柱)。 1
TriggerShift 触发线的延迟柱数。 1
EnableLongEntries / EnableShortEntries 是否允许开多/开空。 true
EnableLongExits / EnableShortExits 是否允许平多/平空。 true
StopLossPoints 止损距离(以价格点计,乘以 PriceStep 转换)。 1000
TakeProfitPoints 止盈距离(价格点)。 2000

风险控制

  • 原版通过 TradeAlgorithms.mqh 设置止损/止盈;C# 版本使用 StartProtection,并自动将距离转换为 UnitTypes.Point
  • 如果止损或止盈设为 0,则不会放置对应的保护单。
  • 没有实现移动止损或加仓逻辑,与原策略保持一致。

与 MT5 版本的差异

  • MMMMMode 资金管理被固定下单量 Volume 取代,使不同账户环境下的行为更可控。
  • 未模拟 Deviation_ 滑点,因为 StockSharp 的市价单没有该参数。
  • 指标平滑完全用 StockSharp 指标还原(包括通过反射设置 Jurik 的 Phase),因此数值与 MQL 缓冲区一致。
  • 根据要求,不提供 Python 版本。

使用建议

  • 确认所选品种可以提供 CandleType 指定的蜡烛流;标准时间周期可使用 TimeSpan.FromHours(x).TimeFrame()
  • 建议保证 SignalBar ≥ TriggerShift,否则触发线尚未形成会导致信号被跳过。
  • 只有在 IsFormedAndOnlineAndAllowTrading() 为真时才会执行真实交易指令。
  • 图表区域绘制价格和成交,指标内部计算且默认不绘制;如需可自行扩展图表。
  • 若需复刻默认设置,保持两级平滑为 SMA 长度 12,四个开仓/平仓开关均为真,止损/止盈使用默认值。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Color TSI Oscillator strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class ExpColorTsiOscillatorStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public ExpColorTsiOscillatorStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}