View on GitHub

Exp Color TSI Oscillator Strategy

Overview

  • Conversion of the MetaTrader 5 expert advisor Exp_ColorTSI-Oscillator into the StockSharp framework.
  • Reconstructs the ColorTSI oscillator: a double-smoothed True Strength Index with a delayed trigger line and multiple smoothing algorithms taken from SmoothAlgorithms.mqh.
  • Generates trades when the oscillator turns up or down relative to its lagged trigger, replicating the "swing reversal" style used by the original EA.

Indicator reconstruction

  • Applied price is selected through the ColorTsiAppliedPrice option (close, open, median, typical, weighted, Demark, etc.).
  • Price momentum (diff = price[n] - price[n-1]) and its absolute value are smoothed in two stages:
    1. First stage: configurable ColorTsiSmoothingMethod (Sma, Ema, Smma, Lwma, Jjma, Jurx, Parma, T3, Vidya, Ama) with length FirstLength and phase FirstPhase for Jurik-like filters.
    2. Second stage: identical method options with SecondLength/SecondPhase applied to the already-smoothed momentum series.
  • The oscillator output is TSI = 100 * smoothMomentum / smoothAbsMomentum. When the denominator is zero, the value is ignored.
  • A trigger line is obtained by delaying the TSI by TriggerShift bars, mirroring the MetaTrader buffer logic.
  • Historical values are stored so that SignalBar matches MetaTrader's CopyBuffer access pattern (index SignalBar = most recent closed bar examined, SignalBar + 1 = previous bar, etc.).

Trading rules

  • Calculations run on finished candles supplied by CandleType (default: 4-hour time frame).
  • Let TSI[k] be the oscillator value and Trigger[k] be the delayed series.
  • Bullish context: TSI[SignalBar + 1] > Trigger[SignalBar + 1] ⇒ the previous bar showed upward momentum.
    • Close shorts if EnableShortExits is true.
    • Open a long position when EnableLongEntries is true and TSI[SignalBar] ≤ Trigger[SignalBar], signalling an upswing after the pullback.
  • Bearish context: TSI[SignalBar + 1] < Trigger[SignalBar + 1] ⇒ the previous bar showed downward momentum.
    • Close longs if EnableLongExits is true.
    • Open a short position when EnableShortEntries is true and TSI[SignalBar] ≥ Trigger[SignalBar].
  • Entry signals are keyed by the time of the analysed bar plus one full timeframe; each signal can trigger at most one trade thanks to _lastLongEntryTime / _lastShortEntryTime guards.
  • All actions are executed with market orders. Existing opposite positions are closed before reversals.

Parameters

Parameter Description Default
CandleType Data stream used for analysis. Supports any DataType (time, tick, volume candles). H4 time frame
Volume Fixed order size replacing the EA's money-management blocks. Must be > 0. 0.1
FirstMethod, FirstLength, FirstPhase First smoothing stage for momentum and absolute momentum. SMA, 12, 15
SecondMethod, SecondLength, SecondPhase Second smoothing stage. SMA, 12, 15
PriceMode Applied price option feeding the oscillator. Close
SignalBar Bar shift used for evaluating signals (1 = last closed bar). 1
TriggerShift Delay applied to the trigger line (1 reproduces original indicator). 1
EnableLongEntries / EnableShortEntries Allow opening long/short trades. true
EnableLongExits / EnableShortExits Allow closing positions on opposite context. true
StopLossPoints Stop-loss distance in price points (converted with instrument PriceStep). 1000
TakeProfitPoints Take-profit distance in price points. 2000

Risk management

  • The original EA relied on helper functions from TradeAlgorithms.mqh for SL/TP placement. The C# version calls StartProtection with the selected distances converted to UnitTypes.Point.
  • If either distance is set to 0, the corresponding protective order is omitted.
  • No trailing stops or position scaling are implemented; these match the MetaTrader behaviour for this expert.

Differences from the MetaTrader version

  • Margin-based lot sizing (MM and MMMode) is replaced by a fixed Volume parameter. This keeps the behaviour deterministic across brokers and avoids replicating account-specific leverage logic.
  • Slippage (Deviation_) is not emulated because StockSharp market orders do not expose a slippage parameter.
  • Indicator smoothing is fully reconstructed using StockSharp indicators (including Jurik phase handling through reflection), so signal values are consistent with the original buffers.
  • Python implementation is intentionally omitted as requested.

Usage notes

  • Ensure the selected security provides the candle type requested by CandleType. For standard timeframes use TimeSpan.FromHours(x).TimeFrame().
  • SignalBar must be ≥ TriggerShift to obtain valid trigger values; otherwise signals are skipped until enough history accumulates.
  • Because the strategy reacts on finished candles, enable real-time order registration only after IsFormedAndOnlineAndAllowTrading() becomes true.
  • The chart area visualises price candles and executed trades; indicators are reconstructed internally and are not auto-plotted.
  • To reproduce the MetaTrader defaults: keep all smoothing settings at SMA with length 12, keep both entry and exit toggles enabled, and use the default stop/take distances.
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;
	}
}