Asset Class Momentum Rotational
This rotational model allocates capital to the asset classes exhibiting the strongest recent momentum. Each period the system ranks asset ETFs and holds the leaders while avoiding laggards.
Rebalancing occurs monthly with cash as a defensive asset when no momentum is positive.
Details
- Data: Monthly total returns of asset class ETFs.
- Entry: Hold top N assets with positive momentum.
- Exit: Replace assets when they fall out of the top ranking.
- Instruments: Broad asset class ETFs.
- Risk: Uses cash proxy and position caps.
// AssetClassMomentumRotationalStrategy.cs
// -----------------------------------------------------------------------------
// Momentum rotation strategy using rate of change indicator.
// Enters long when ROC is positive and above threshold.
// Exits when ROC turns negative. Uses SMA filter for trend confirmation.
// Cooldown prevents excessive trading.
// -----------------------------------------------------------------------------
// Date: 2 Aug 2025
// -----------------------------------------------------------------------------
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>
/// Momentum rotation strategy using ROC and SMA trend filter.
/// </summary>
public class AssetClassMomentumRotationalStrategy : Strategy
{
private readonly StrategyParam<int> _rocLength;
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
/// <summary>
/// Rate of change lookback length.
/// </summary>
public int RocLength
{
get => _rocLength.Value;
set => _rocLength.Value = value;
}
/// <summary>
/// SMA period for trend filter.
/// </summary>
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
/// <summary>
/// Cooldown bars between trades.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type used to compute momentum.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private RateOfChange _roc;
private SimpleMovingAverage _sma;
private int _cooldownRemaining;
public AssetClassMomentumRotationalStrategy()
{
_rocLength = Param(nameof(RocLength), 14)
.SetDisplay("ROC Length", "Rate of change lookback", "Parameters");
_smaPeriod = Param(nameof(SmaPeriod), 30)
.SetDisplay("SMA Period", "SMA period for trend filter", "Parameters");
_cooldownBars = Param(nameof(CooldownBars), 20)
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for momentum", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_roc = null;
_sma = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_roc = new RateOfChange { Length = RocLength };
_sma = new SimpleMovingAverage { Length = SmaPeriod };
SubscribeCandles(CandleType)
.Bind(_roc, _sma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal rocValue, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_roc.IsFormed || !_sma.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
return;
}
var close = candle.ClosePrice;
// Strong positive momentum + price above SMA -> long
if (rocValue > 0 && close > smaValue && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
_cooldownRemaining = CooldownBars;
}
// Negative momentum or price below SMA -> exit/short
else if (rocValue < 0 && close < smaValue && Position >= 0)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
SellMarket(Volume);
_cooldownRemaining = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import RateOfChange, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class asset_class_momentum_rotational_strategy(Strategy):
"""Momentum rotation strategy using ROC and SMA trend filter."""
def __init__(self):
super(asset_class_momentum_rotational_strategy, self).__init__()
self._roc_length = self.Param("RocLength", 14) \
.SetDisplay("ROC Length", "Rate of change lookback", "Parameters")
self._sma_period = self.Param("SmaPeriod", 30) \
.SetDisplay("SMA Period", "SMA period for trend filter", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 20) \
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Candle type used for momentum", "General")
self._roc = None
self._sma = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(asset_class_momentum_rotational_strategy, self).OnReseted()
self._roc = None
self._sma = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(asset_class_momentum_rotational_strategy, self).OnStarted2(time)
self._roc = RateOfChange()
self._roc.Length = int(self._roc_length.Value)
self._sma = SimpleMovingAverage()
self._sma.Length = int(self._sma_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._roc, self._sma, self._process_candle).Start()
def _process_candle(self, candle, roc_val, sma_val):
if candle.State != CandleStates.Finished:
return
if not self._roc.IsFormed or not self._sma.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
return
close = float(candle.ClosePrice)
rv = float(roc_val)
sv = float(sma_val)
cooldown = int(self._cooldown_bars.Value)
if rv > 0 and close > sv and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
self._cooldown_remaining = cooldown
elif rv < 0 and close < sv and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
self._cooldown_remaining = cooldown
def CreateClone(self):
return asset_class_momentum_rotational_strategy()