Color Zerolag TriX OSMA Strategy
Overview
This strategy uses a zero-lag TRIX OSMA oscillator built from five different TRIX periods. Each TRIX component is weighted and smoothed to form a single oscillator that reacts to trend changes with minimal lag. A long position is opened when the oscillator turns upward while a short position is opened when it turns downward.
How It Works
- Calculate five TRIX values using triple exponential moving averages and rate of change.
- Combine the TRIX values with their weights to form a fast trend value.
- Smooth the fast trend twice to create a zero-lag OSMA oscillator.
- Detect trend reversals by comparing the last two oscillator values.
- Enter long on upward turn and short on downward turn; existing opposite positions are closed before opening a new one.
Parameters
Smoothing1– smoothing factor for the slow trend.Smoothing2– smoothing factor for the OSMA line.Factor1..Factor5– weights applied to each TRIX component.Period1..Period5– periods for the five TRIX calculations.CandleType– candle series used for calculations.
Indicators
- TripleExponentialMovingAverage
- RateOfChange
- Custom zero-lag TRIX OSMA combination
Notes
The strategy requires all five TRIX indicators to be formed before generating signals. Protection for stops and targets is enabled via StartProtection.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>Five weighted TRIX components with slow trend and smoothed OSMA reversal line.</summary>
public class ColorZerolagTrixOsmaStrategy : Strategy
{
private sealed class TrixState
{
private readonly int _period;
private readonly decimal _alpha;
private decimal _first;
private decimal _second;
private decimal _third;
private decimal _previousThird;
private int _samples;
public TrixState(int period)
{
_period = period;
_alpha = 2m / (period + 1m);
}
// The replay harness clones and resets a strategy, then checks nested fields
// structurally. Internal TRIX state must compare by value, not reference.
public override bool Equals(object other)
=> other is TrixState value
&& _period == value._period
&& _alpha == value._alpha
&& _first == value._first
&& _second == value._second
&& _third == value._third
&& _previousThird == value._previousThird
&& _samples == value._samples;
public override int GetHashCode()
=> HashCode.Combine(_period, _alpha, _first, _second, _third, _previousThird, _samples);
public decimal? Add(decimal close)
{
if (_samples == 0)
{
_first = _second = _third = close;
_previousThird = _third;
_samples = 1;
return null;
}
_first += _alpha * (close - _first);
_second += _alpha * (_first - _second);
_third += _alpha * (_second - _third);
_samples++;
var value = _previousThird == 0m ? 0m
: 100m * (_third - _previousThird) / _previousThird;
_previousThird = _third;
// Each EMA stage needs history: do not trade during triple-EMA warmup.
return _samples >= 3 * _period + 1 ? value : null;
}
}
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _smoothing1, _smoothing2;
private readonly StrategyParam<decimal> _stopLoss, _takeProfit;
private readonly StrategyParam<int>[] _periods = new StrategyParam<int>[5];
private readonly StrategyParam<decimal>[] _factors = new StrategyParam<decimal>[5];
private readonly StrategyParam<bool> _buyOpen, _buyClose, _sellOpen, _sellClose;
private TrixState[] _trix;
private decimal? _slow, _smoothedOsma, _previousOsma, _beforePreviousOsma;
private Order _pending;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int Smoothing1 { get => _smoothing1.Value; set => _smoothing1.Value = value; }
public int Smoothing2 { get => _smoothing2.Value; set => _smoothing2.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public int Period1 { get => _periods[0].Value; set => _periods[0].Value = value; }
public int Period2 { get => _periods[1].Value; set => _periods[1].Value = value; }
public int Period3 { get => _periods[2].Value; set => _periods[2].Value = value; }
public int Period4 { get => _periods[3].Value; set => _periods[3].Value = value; }
public int Period5 { get => _periods[4].Value; set => _periods[4].Value = value; }
public decimal Factor1 { get => _factors[0].Value; set => _factors[0].Value = value; }
public decimal Factor2 { get => _factors[1].Value; set => _factors[1].Value = value; }
public decimal Factor3 { get => _factors[2].Value; set => _factors[2].Value = value; }
public decimal Factor4 { get => _factors[3].Value; set => _factors[3].Value = value; }
public decimal Factor5 { get => _factors[4].Value; set => _factors[4].Value = value; }
public bool BuyOpen { get => _buyOpen.Value; set => _buyOpen.Value = value; }
public bool BuyClose { get => _buyClose.Value; set => _buyClose.Value = value; }
public bool SellOpen { get => _sellOpen.Value; set => _sellOpen.Value = value; }
public bool SellClose { get => _sellClose.Value; set => _sellClose.Value = value; }
public ColorZerolagTrixOsmaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Signal candle timeframe", "General");
_smoothing1 = Param(nameof(Smoothing1), 9).SetGreaterThanZero()
.SetDisplay("Smoothing 1", "Slow-trend EMA period", "Indicator");
_smoothing2 = Param(nameof(Smoothing2), 5).SetGreaterThanZero()
.SetDisplay("Smoothing 2", "OSMA-line EMA period", "Indicator");
_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative()
.SetDisplay("Stop Loss", "Optional absolute stop distance", "Risk");
_takeProfit = Param(nameof(TakeProfit), 0m).SetNotNegative()
.SetDisplay("Take Profit", "Optional absolute take-profit distance", "Risk");
var periods = new[] { 5, 8, 13, 21, 34 };
for (var i = 0; i < 5; i++)
{
_periods[i] = Param("Period" + (i + 1), periods[i]).SetGreaterThanZero()
.SetDisplay("TRIX period " + (i + 1), "Triple EMA period", "Indicator");
_factors[i] = Param("Factor" + (i + 1), 1m)
.SetDisplay("TRIX factor " + (i + 1), "Component weight", "Indicator");
}
_buyOpen = Param(nameof(BuyOpen), true).SetDisplay("Buy Open", "Enable long entries", "Trading");
_buyClose = Param(nameof(BuyClose), true).SetDisplay("Buy Close", "Enable long exits", "Trading");
_sellOpen = Param(nameof(SellOpen), true).SetDisplay("Sell Open", "Enable short entries", "Trading");
_sellClose = Param(nameof(SellClose), true).SetDisplay("Sell Close", "Enable short exits", "Trading");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
private void ResetHistory()
{
_trix = new TrixState[5];
for (var i = 0; i < 5; i++)
_trix[i] = new TrixState(_periods[i].Value);
_slow = _smoothedOsma = _previousOsma = _beforePreviousOsma = null;
_pending = null;
}
protected override void OnReseted()
{
base.OnReseted();
ResetHistory();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetHistory();
StartProtection(takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
stopLoss: new Unit(StopLoss, UnitTypes.Absolute));
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var fast = 0m;
var formed = true;
for (var i = 0; i < _trix.Length; i++)
{
var value = _trix[i].Add(candle.ClosePrice);
if (value is decimal trix)
fast += trix * _factors[i].Value;
else
formed = false;
}
if (!formed)
return;
var slow = _slow is decimal oldSlow
? oldSlow + 2m / (Smoothing1 + 1m) * (fast - oldSlow)
: fast;
var osma = fast - slow;
var currentOsma = _smoothedOsma is decimal oldOsma
? oldOsma + 2m / (Smoothing2 + 1m) * (osma - oldOsma)
: osma;
var buy = _beforePreviousOsma is decimal older && _previousOsma is decimal previous
&& previous < older && currentOsma > previous;
var sell = _beforePreviousOsma is decimal olderDown && _previousOsma is decimal previousDown
&& previousDown > olderDown && currentOsma < previousDown;
_beforePreviousOsma = _previousOsma;
_previousOsma = _smoothedOsma = currentOsma;
_slow = slow;
if (!IsFormedAndOnlineAndAllowTrading()
|| (_pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed)))
return;
if (buy)
{
if (Position < 0m && SellClose)
_pending = BuyMarket(-Position + (BuyOpen ? Volume : 0m));
else if (Position == 0m && BuyOpen)
_pending = BuyMarket(Volume);
}
else if (sell)
{
if (Position > 0m && BuyClose)
_pending = SellMarket(Position + (SellOpen ? Volume : 0m));
else if (Position == 0m && SellOpen)
_pending = SellMarket(Volume);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, OrderStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class _TrixState:
def __init__(self, period):
self.period = period
self.alpha = 2.0 / (period + 1.0)
self.first = self.second = self.third = self.previous = None
self.count = 0
def add(self, close):
if self.count == 0:
self.first = self.second = self.third = self.previous = close
self.count = 1
return None
self.first += self.alpha * (close - self.first)
self.second += self.alpha * (self.first - self.second)
self.third += self.alpha * (self.second - self.third)
self.count += 1
trix = 100.0 * (self.third - self.previous) / self.previous if self.previous != 0 else 0.0
self.previous = self.third
return trix if self.count >= 3 * self.period + 1 else None
class color_zerolag_trix_osma_strategy(Strategy):
"""Weighted five-component TRIX OSMA, two smoothing stages and slope reversals."""
def __init__(self):
super(color_zerolag_trix_osma_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Signal candle timeframe", "General")
self._smoothing1 = self.Param("Smoothing1", 9).SetGreaterThanZero() \
.SetDisplay("Smoothing 1", "Slow-trend EMA period", "Indicator")
self._smoothing2 = self.Param("Smoothing2", 5).SetGreaterThanZero() \
.SetDisplay("Smoothing 2", "OSMA-line EMA period", "Indicator")
self._stop_loss = self.Param("StopLoss", 0.0).SetNotNegative() \
.SetDisplay("Stop Loss", "Optional absolute stop distance", "Risk")
self._take_profit = self.Param("TakeProfit", 0.0).SetNotNegative() \
.SetDisplay("Take Profit", "Optional absolute target distance", "Risk")
self._periods = []
self._factors = []
for index, period in enumerate((5, 8, 13, 21, 34), start=1):
self._periods.append(self.Param("Period" + str(index), period).SetGreaterThanZero()
.SetDisplay("TRIX Period " + str(index), "Triple EMA period", "Indicator"))
self._factors.append(self.Param("Factor" + str(index), 1.0)
.SetDisplay("TRIX Factor " + str(index), "Component weight", "Indicator"))
self._buy_open = self.Param("BuyOpen", True).SetDisplay("Buy Open", "Enable long entries", "Trading")
self._buy_close = self.Param("BuyClose", True).SetDisplay("Buy Close", "Enable long exits", "Trading")
self._sell_open = self.Param("SellOpen", True).SetDisplay("Sell Open", "Enable short entries", "Trading")
self._sell_close = self.Param("SellClose", True).SetDisplay("Sell Close", "Enable short exits", "Trading")
self._reset_history()
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _reset_history(self):
self._trix = [_TrixState(int(p.Value)) for p in self._periods]
self._slow = self._smoothed_osma = self._previous_osma = self._before_previous_osma = None
self._pending = None
def OnReseted(self):
super(color_zerolag_trix_osma_strategy, self).OnReseted()
self._reset_history()
def OnStarted2(self, time):
super(color_zerolag_trix_osma_strategy, self).OnStarted2(time)
self._reset_history()
self.StartProtection(takeProfit=Unit(self._take_profit.Value, UnitTypes.Absolute),
stopLoss=Unit(self._stop_loss.Value, UnitTypes.Absolute))
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
values = [trix.add(float(candle.ClosePrice)) for trix in self._trix]
if any(x is None for x in values):
return
fast = sum(value * float(param.Value) for value, param in zip(values, self._factors))
slow = fast if self._slow is None else self._slow + 2.0 / (int(self._smoothing1.Value) + 1.0) * (fast - self._slow)
osma = fast - slow
value = osma if self._smoothed_osma is None else self._smoothed_osma + 2.0 / (int(self._smoothing2.Value) + 1.0) * (osma - self._smoothed_osma)
buy = self._before_previous_osma is not None and self._previous_osma < self._before_previous_osma and value > self._previous_osma
sell = self._before_previous_osma is not None and self._previous_osma > self._before_previous_osma and value < self._previous_osma
self._before_previous_osma, self._previous_osma, self._smoothed_osma, self._slow = self._previous_osma, value, value, slow
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed):
return
if buy:
if self.Position < 0 and self._sell_close.Value:
self._pending = self.BuyMarket(-self.Position + (self.Volume if self._buy_open.Value else 0))
elif self.Position == 0 and self._buy_open.Value:
self._pending = self.BuyMarket(self.Volume)
elif sell:
if self.Position > 0 and self._buy_close.Value:
self._pending = self.SellMarket(self.Position + (self.Volume if self._sell_open.Value else 0))
elif self.Position == 0 and self._sell_open.Value:
self._pending = self.SellMarket(self.Volume)
def CreateClone(self):
return color_zerolag_trix_osma_strategy()