Asset Class Trend Following Strategy
This strategy follows trends across multiple asset classes. It applies a simple moving average filter to each security in the universe and rebalances the portfolio on the first trading day of each month. Positions are taken only when price is above the moving average.
Testing indicates an average annual return of about 15%. It performs best on diversified futures portfolios.
At the start of each month, securities trading above their SMA receive an equal allocation of capital. Positions are closed when price falls below the SMA or when capital is reassigned at the next rebalance.
Details
- Entry Criteria:
Close > SMA - Long/Short: Long only
- Exit Criteria:
Close <= SMAor removed at rebalance - Stops: None; capital is redistributed monthly
- Default Values:
SmaLength= 210MinTradeUsd= 50CandleType= daily
- Filters:
- Category: Trend following
- Direction: Long only
- Indicators: SMA
- Stops: No
- Complexity: Intermediate
- Timeframe: Long-term
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Configuration;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Relative asset class trend-following strategy that allocates to the primary instrument when its trend is stronger than the secondary benchmark.
/// </summary>
public class AssetClassTrendFollowingStrategy : Strategy
{
private readonly StrategyParam<string> _security2Id;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<decimal> _minTrendStrength;
private readonly StrategyParam<decimal> _relativeStrengthThreshold;
private readonly StrategyParam<int> _rebalanceIntervalBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private Security _security2 = null!;
private SimpleMovingAverage _primarySma = null!;
private SimpleMovingAverage _secondarySma = null!;
private decimal _latestPrimaryPrice;
private decimal _latestSecondaryPrice;
private decimal _latestPrimarySma;
private decimal _latestSecondarySma;
private bool _primaryUpdated;
private bool _secondaryUpdated;
private int _barsSinceRebalance;
/// <summary>
/// Secondary security identifier.
/// </summary>
public string Security2Id
{
get => _security2Id.Value;
set => _security2Id.Value = value;
}
/// <summary>
/// Trend moving average length.
/// </summary>
public int SmaLength
{
get => _smaLength.Value;
set => _smaLength.Value = value;
}
/// <summary>
/// Minimum absolute trend strength required to hold the primary instrument.
/// </summary>
public decimal MinTrendStrength
{
get => _minTrendStrength.Value;
set => _minTrendStrength.Value = value;
}
/// <summary>
/// Minimum relative outperformance of the primary instrument versus the benchmark.
/// </summary>
public decimal RelativeStrengthThreshold
{
get => _relativeStrengthThreshold.Value;
set => _relativeStrengthThreshold.Value = value;
}
/// <summary>
/// Number of paired candles between rebalancing decisions.
/// </summary>
public int RebalanceIntervalBars
{
get => _rebalanceIntervalBars.Value;
set => _rebalanceIntervalBars.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Candle type used for both instruments.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public AssetClassTrendFollowingStrategy()
{
_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
.SetDisplay("Second Security Id", "Identifier of the secondary benchmark security", "General");
_smaLength = Param(nameof(SmaLength), 36)
.SetRange(10, 200)
.SetDisplay("SMA Length", "Trend moving average length", "Indicators");
_minTrendStrength = Param(nameof(MinTrendStrength), 0.004m)
.SetRange(0.001m, 0.05m)
.SetDisplay("Min Trend Strength", "Minimum absolute trend strength required to hold the primary instrument", "Signals");
_relativeStrengthThreshold = Param(nameof(RelativeStrengthThreshold), 0.002m)
.SetRange(0m, 0.05m)
.SetDisplay("Relative Strength Threshold", "Minimum relative outperformance of the primary instrument", "Signals");
_rebalanceIntervalBars = Param(nameof(RebalanceIntervalBars), 18)
.SetRange(1, 200)
.SetDisplay("Rebalance Bars", "Number of paired candles between rebalancing decisions", "General");
_stopLoss = Param(nameof(StopLoss), 2m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle series for both instruments", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
if (!Security2Id.IsEmpty())
yield return (new Security { Id = Security2Id }, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_security2 = null!;
_primarySma = null!;
_secondarySma = null!;
_latestPrimaryPrice = 0m;
_latestSecondaryPrice = 0m;
_latestPrimarySma = 0m;
_latestSecondarySma = 0m;
_primaryUpdated = false;
_secondaryUpdated = false;
_barsSinceRebalance = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security is not specified.");
if (Security2Id.IsEmpty())
throw new InvalidOperationException("Secondary security identifier is not specified.");
_security2 = this.LookupById(Security2Id) ?? new Security { Id = Security2Id };
_primarySma = new SimpleMovingAverage { Length = SmaLength };
_secondarySma = new SimpleMovingAverage { Length = SmaLength };
_barsSinceRebalance = RebalanceIntervalBars;
var primarySubscription = SubscribeCandles(CandleType, security: Security);
var secondarySubscription = SubscribeCandles(CandleType, security: _security2);
primarySubscription
.Bind(ProcessPrimaryCandle)
.Start();
secondarySubscription
.Bind(ProcessSecondaryCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawCandles(area, secondarySubscription);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent));
}
private void ProcessPrimaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestPrimaryPrice = candle.ClosePrice;
_latestPrimarySma = _primarySma.Process(candle).ToDecimal();
_primaryUpdated = true;
TryRebalance();
}
private void ProcessSecondaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestSecondaryPrice = candle.ClosePrice;
_latestSecondarySma = _secondarySma.Process(candle).ToDecimal();
_secondaryUpdated = true;
TryRebalance();
}
private void TryRebalance()
{
if (!_primaryUpdated || !_secondaryUpdated)
return;
_primaryUpdated = false;
_secondaryUpdated = false;
if (!_primarySma.IsFormed || !_secondarySma.IsFormed)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_barsSinceRebalance < RebalanceIntervalBars)
{
_barsSinceRebalance++;
return;
}
_barsSinceRebalance = 0;
var primaryTrend = (_latestPrimaryPrice - _latestPrimarySma) / Math.Max(_latestPrimarySma, 1m);
var secondaryTrend = (_latestSecondaryPrice - _latestSecondarySma) / Math.Max(_latestSecondarySma, 1m);
var relativeStrength = primaryTrend - secondaryTrend;
var shouldHoldLong = primaryTrend >= MinTrendStrength && relativeStrength >= RelativeStrengthThreshold;
if (shouldHoldLong && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
}
else if (!shouldHoldLong && Position > 0)
{
SellMarket(Position);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
class asset_class_trend_following_strategy(Strategy):
"""Relative asset class trend-following strategy using dual securities."""
def __init__(self):
super(asset_class_trend_following_strategy, self).__init__()
self._security2_id = self.Param("Security2Id", "TONUSDT@BNBFT") \
.SetDisplay("Second Security Id", "Identifier of the secondary benchmark security", "General")
self._sma_length = self.Param("SmaLength", 36) \
.SetRange(10, 200) \
.SetDisplay("SMA Length", "Trend moving average length", "Indicators")
self._min_trend_strength = self.Param("MinTrendStrength", 0.004) \
.SetRange(0.001, 0.05) \
.SetDisplay("Min Trend Strength", "Minimum absolute trend strength required to hold the primary instrument", "Signals")
self._relative_strength_threshold = self.Param("RelativeStrengthThreshold", 0.002) \
.SetRange(0.0, 0.05) \
.SetDisplay("Relative Strength Threshold", "Minimum relative outperformance of the primary instrument", "Signals")
self._rebalance_interval_bars = self.Param("RebalanceIntervalBars", 18) \
.SetRange(1, 200) \
.SetDisplay("Rebalance Bars", "Number of paired candles between rebalancing decisions", "General")
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetRange(0.5, 10.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle series for both instruments", "General")
self._security2 = None
self._primary_sma = None
self._secondary_sma = None
self._latest_primary_price = 0.0
self._latest_secondary_price = 0.0
self._latest_primary_sma = 0.0
self._latest_secondary_sma = 0.0
self._primary_updated = False
self._secondary_updated = False
self._bars_since_rebalance = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
result = []
if self.Security is not None:
result.append((self.Security, self.candle_type))
sec2_id = str(self._security2_id.Value)
if sec2_id:
s = Security()
s.Id = sec2_id
result.append((s, self.candle_type))
return result
def OnReseted(self):
super(asset_class_trend_following_strategy, self).OnReseted()
self._security2 = None
self._primary_sma = None
self._secondary_sma = None
self._latest_primary_price = 0.0
self._latest_secondary_price = 0.0
self._latest_primary_sma = 0.0
self._latest_secondary_sma = 0.0
self._primary_updated = False
self._secondary_updated = False
self._bars_since_rebalance = 0
def OnStarted2(self, time):
super(asset_class_trend_following_strategy, self).OnStarted2(time)
sec2_id = str(self._security2_id.Value)
if not sec2_id:
raise Exception("Secondary security identifier is not specified.")
s = Security()
s.Id = sec2_id
self._security2 = s
sma_len = int(self._sma_length.Value)
self._primary_sma = SimpleMovingAverage()
self._primary_sma.Length = sma_len
self._secondary_sma = SimpleMovingAverage()
self._secondary_sma.Length = sma_len
self._bars_since_rebalance = int(self._rebalance_interval_bars.Value)
primary_subscription = self.SubscribeCandles(self.candle_type, True, self.Security)
secondary_subscription = self.SubscribeCandles(self.candle_type, True, self._security2)
primary_subscription.Bind(self.ProcessPrimaryCandle).Start()
secondary_subscription.Bind(self.ProcessSecondaryCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, primary_subscription)
self.DrawCandles(area, secondary_subscription)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessPrimaryCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_primary_price = float(candle.ClosePrice)
civ = CandleIndicatorValue(self._primary_sma, candle)
civ.IsFinal = True
result = self._primary_sma.Process(civ)
self._latest_primary_sma = float(result)
self._primary_updated = True
self.TryRebalance()
def ProcessSecondaryCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_secondary_price = float(candle.ClosePrice)
civ = CandleIndicatorValue(self._secondary_sma, candle)
civ.IsFinal = True
result = self._secondary_sma.Process(civ)
self._latest_secondary_sma = float(result)
self._secondary_updated = True
self.TryRebalance()
def TryRebalance(self):
if not self._primary_updated or not self._secondary_updated:
return
self._primary_updated = False
self._secondary_updated = False
if not self._primary_sma.IsFormed or not self._secondary_sma.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
rebalance_interval = int(self._rebalance_interval_bars.Value)
if self._bars_since_rebalance < rebalance_interval:
self._bars_since_rebalance += 1
return
self._bars_since_rebalance = 0
primary_sma_val = max(self._latest_primary_sma, 1.0)
secondary_sma_val = max(self._latest_secondary_sma, 1.0)
primary_trend = (self._latest_primary_price - self._latest_primary_sma) / primary_sma_val
secondary_trend = (self._latest_secondary_price - self._latest_secondary_sma) / secondary_sma_val
relative_strength = primary_trend - secondary_trend
min_strength = float(self._min_trend_strength.Value)
rel_thresh = float(self._relative_strength_threshold.Value)
should_hold_long = primary_trend >= min_strength and relative_strength >= rel_thresh
if should_hold_long and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
elif not should_hold_long and self.Position > 0:
self.SellMarket(self.Position)
def CreateClone(self):
return asset_class_trend_following_strategy()