Стратегия Следования Тренду по Классам Активов
Эта стратегия отслеживает тренды в разных классах активов. Для каждого инструмента из вселенной применяется простой скользящий средний фильтр. Портфель ребалансируется в первый торговый день месяца, а позиции открываются только если цена выше скользящей средней.
Тестирование показывает среднегодовую доходность около 15%. Наилучшие результаты достигаются на диверсифицированных портфелях фьючерсов.
В начале каждого месяца инструментам, чья цена выше SMA, присваивается равная доля капитала. Позиции закрываются при падении цены ниже SMA или при перераспределении капитала.
Детали
- Условия входа:
Close > SMA - Длинные/короткие: Только длинные
- Условия выхода:
Close <= SMAили исключение при ребалансировке - Стопы: Нет; капитал перераспределяется ежемесячно
- Параметры по умолчанию:
SmaLength= 210MinTradeUsd= 50CandleType= дневные свечи
- Фильтры:
- Категория: Следование тренду
- Направление: Только long
- Индикаторы: SMA
- Стопы: Нет
- Сложность: Средняя
- Таймфрейм: Долгосрочный
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()