Color Zerolag Momentum OSMA Strategy
This strategy builds a custom zero-lag momentum OSMA oscillator using five momentum calculations. When the oscillator's value two bars ago is below the value three bars ago the trend is considered upward. In this case short positions are closed and a new long can be opened if the most recent value is above the value two bars ago. When the value two bars ago is above the value three bars ago the trend is downward, long positions are closed and a short may be opened if the last value is below the value two bars ago.
Parameters
Smoothing1– first smoothing factor for the slow trend.Smoothing2– second smoothing factor for the OSMA line.Factor1-5– weights applied to each momentum component.MomentumPeriod1-5– periods for the momentum indicators.CandleType– candle timeframe for calculations.BuyOpen– allow opening long positions.SellOpen– allow opening short positions.BuyClose– allow closing long positions.SellClose– allow closing short positions.
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>Five weighted Momentum indicators, two OSMA smoothing stages and the documented t-2/t-3 signal.</summary>
public class ColorZerolagMomentumOsmaStrategy : Strategy
{
private readonly StrategyParam<int>[] _periods = new StrategyParam<int>[5];
private readonly StrategyParam<decimal>[] _factors = new StrategyParam<decimal>[5];
private readonly StrategyParam<int> _smoothing1, _smoothing2;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _buyOpen, _sellOpen, _buyClose, _sellClose;
private readonly Queue<decimal> _closes = new();
private decimal? _slow, _smoothed, _last, _prev2, _prev3;
private Order _pending;
public int MomentumPeriod1 { get => _periods[0].Value; set => _periods[0].Value = value; }
public int MomentumPeriod2 { get => _periods[1].Value; set => _periods[1].Value = value; }
public int MomentumPeriod3 { get => _periods[2].Value; set => _periods[2].Value = value; }
public int MomentumPeriod4 { get => _periods[3].Value; set => _periods[3].Value = value; }
public int MomentumPeriod5 { 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 int Smoothing1 { get => _smoothing1.Value; set => _smoothing1.Value = value; }
public int Smoothing2 { get => _smoothing2.Value; set => _smoothing2.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public bool BuyOpen { get => _buyOpen.Value; set => _buyOpen.Value = value; }
public bool SellOpen { get => _sellOpen.Value; set => _sellOpen.Value = value; }
public bool BuyClose { get => _buyClose.Value; set => _buyClose.Value = value; }
public bool SellClose { get => _sellClose.Value; set => _sellClose.Value = value; }
public ColorZerolagMomentumOsmaStrategy()
{
var periods = new[] { 5, 8, 13, 21, 34 };
for (var i = 0; i < 5; i++)
{
_periods[i] = Param("MomentumPeriod" + (i + 1), periods[i]).SetGreaterThanZero()
.SetDisplay("Momentum period " + (i + 1), "Price difference lookback", "Indicator");
_factors[i] = Param("Factor" + (i + 1), 1m)
.SetDisplay("Factor " + (i + 1), "Component weight", "Indicator");
}
_smoothing1 = Param(nameof(Smoothing1), 9).SetGreaterThanZero()
.SetDisplay("Smoothing 1", "Slow trend EMA", "Indicator");
_smoothing2 = Param(nameof(Smoothing2), 5).SetGreaterThanZero()
.SetDisplay("Smoothing 2", "OSMA EMA", "Indicator");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Signal candles", "General");
_buyOpen = Param(nameof(BuyOpen), true).SetDisplay("Buy Open", "Enable long entries", "Trading");
_sellOpen = Param(nameof(SellOpen), true).SetDisplay("Sell Open", "Enable short entries", "Trading");
_buyClose = Param(nameof(BuyClose), true).SetDisplay("Buy Close", "Enable long exits", "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()
{
_closes.Clear();
_slow = _smoothed = _last = _prev2 = _prev3 = null;
_pending = null;
}
protected override void OnReseted()
{
base.OnReseted();
ResetHistory();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetHistory();
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var max = _periods.Max(p => p.Value);
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > max + 1)
_closes.Dequeue();
if (_closes.Count < max + 1)
return;
var closes = _closes.ToArray();
var fast = 0m;
for (var i = 0; i < 5; i++)
{
// Matches StockSharp Momentum: close[t] - close[t - period].
var period = _periods[i].Value;
fast += (candle.ClosePrice - closes[closes.Length - period - 1]) * _factors[i].Value;
}
var slow = _slow is decimal oldSlow
? oldSlow + 2m / (Smoothing1 + 1m) * (fast - oldSlow)
: fast;
var rawOsma = fast - slow;
var osma = _smoothed is decimal oldOsma
? oldOsma + 2m / (Smoothing2 + 1m) * (rawOsma - oldOsma)
: rawOsma;
// The README refers to t-2 against t-3, then t against t-2.
var buy = _prev3 is decimal third && _prev2 is decimal second
&& second < third && osma > second;
var sell = _prev3 is decimal thirdDown && _prev2 is decimal secondDown
&& secondDown > thirdDown && osma < secondDown;
_prev3 = _prev2;
_prev2 = _last;
_last = _smoothed = osma;
_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
from StockSharp.Algo.Strategies import Strategy
class color_zerolag_momentum_osma_strategy(Strategy):
"""Weighted five momentum calculations with the documented t-2/t-3 OSMA turning signal."""
def __init__(self):
super(color_zerolag_momentum_osma_strategy, self).__init__()
self._periods = []
self._factors = []
for index, period in enumerate((5, 8, 13, 21, 34), start=1):
self._periods.append(self.Param("MomentumPeriod" + str(index), period).SetGreaterThanZero()
.SetDisplay("Momentum Period " + str(index), "Price difference lookback", "Indicator"))
self._factors.append(self.Param("Factor" + str(index), 1.0)
.SetDisplay("Factor " + str(index), "Component weight", "Indicator"))
self._smoothing1 = self.Param("Smoothing1", 9).SetGreaterThanZero() \
.SetDisplay("Smoothing 1", "Slow trend EMA", "Indicator")
self._smoothing2 = self.Param("Smoothing2", 5).SetGreaterThanZero() \
.SetDisplay("Smoothing 2", "OSMA EMA", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Signal candles", "General")
self._buy_open = self.Param("BuyOpen", True).SetDisplay("Buy Open", "Enable long entries", "Trading")
self._sell_open = self.Param("SellOpen", True).SetDisplay("Sell Open", "Enable short entries", "Trading")
self._buy_close = self.Param("BuyClose", True).SetDisplay("Buy Close", "Enable long exits", "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._closes = []
self._slow = self._smoothed = self._last = self._prev2 = self._prev3 = None
self._pending = None
def OnReseted(self):
super(color_zerolag_momentum_osma_strategy, self).OnReseted()
self._reset_history()
def OnStarted2(self, time):
super(color_zerolag_momentum_osma_strategy, self).OnStarted2(time)
self._reset_history()
self.SubscribeCandles(self.CandleType).Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
max_period = max(int(p.Value) for p in self._periods)
close = float(candle.ClosePrice)
self._closes.append(close)
if len(self._closes) > max_period + 1:
self._closes.pop(0)
if len(self._closes) < max_period + 1:
return
fast = 0.0
for param, factor in zip(self._periods, self._factors):
period = int(param.Value)
fast += (close - self._closes[-period-1]) * float(factor.Value)
slow = fast if self._slow is None else self._slow + 2.0 / (int(self._smoothing1.Value) + 1.0) * (fast - self._slow)
raw_osma = fast - slow
osma = raw_osma if self._smoothed is None else self._smoothed + 2.0 / (int(self._smoothing2.Value) + 1.0) * (raw_osma - self._smoothed)
buy = self._prev3 is not None and self._prev2 < self._prev3 and osma > self._prev2
sell = self._prev3 is not None and self._prev2 > self._prev3 and osma < self._prev2
self._prev3, self._prev2, self._last = self._prev2, self._last, osma
self._smoothed, self._slow = osma, 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_momentum_osma_strategy()