Стратегия Asset Growth Effect
Стратегия покупает акции компаний с наименьшим ростом совокупных активов и продаёт в шорт бумаги с наибольшим ростом активов. Портфель ребалансируется ежегодно в июле по обновлённым фундаментальным данным.
Тесты показывают среднюю годовую доходность около 15%. Лучше всего работает на рынке акций.
Рост активов вычисляется по бухгалтерской отчётности. Бумаги ранжируются по квантилям, покупается нижний квантиль и продаётся верхний. Весовые коэффициенты подбираются для заданного плеча и пересчитываются ежегодно.
Детали
- Критерии входа:
- Лонг: акция в квантиле с наименьшим ростом активов;
- Шорт: акция в квантиле с наибольшим ростом активов.
- Направление: Лонг и шорт.
- Критерии выхода: Перебалансировка раз в год.
- Стопы: Нет.
- Значения по умолчанию:
Quantiles= 10Leverage= 1mMinTradeUsd= 50mCandleType= TimeSpan.FromMinutes(5).TimeFrame()
- Фильтры:
- Категория: Фундаментальная
- Направление: Оба
- Индикаторы: Фундаментальные данные
- Стопы: Нет
- Сложность: Средняя
- Таймфрейм: Долгосрочный
- Сезонность: Да
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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-growth strategy that fades excessive synthetic balance-sheet expansion in the primary instrument versus the secondary benchmark.
/// </summary>
public class AssetGrowthEffectStrategy : Strategy
{
private readonly StrategyParam<string> _security2Id;
private readonly StrategyParam<int> _assetLength;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _entryThreshold;
private readonly StrategyParam<decimal> _exitThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private Security _security2 = null!;
private ExponentialMovingAverage _primaryAssetBase = null!;
private ExponentialMovingAverage _secondaryAssetBase = null!;
private SimpleMovingAverage _growthSpreadAverage = null!;
private StandardDeviation _growthSpreadDeviation = null!;
private decimal _previousPrimaryAssetBase;
private decimal _previousSecondaryAssetBase;
private decimal _latestPrimaryGrowth;
private decimal _latestSecondaryGrowth;
private bool _primaryUpdated;
private bool _secondaryUpdated;
private decimal? _previousZScore;
private int _cooldownRemaining;
/// <summary>
/// Secondary security identifier.
/// </summary>
public string Security2Id
{
get => _security2Id.Value;
set => _security2Id.Value = value;
}
/// <summary>
/// Smoothing length for the synthetic asset base.
/// </summary>
public int AssetLength
{
get => _assetLength.Value;
set => _assetLength.Value = value;
}
/// <summary>
/// Lookback period used to normalize growth spread.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Z-score threshold required to open a position.
/// </summary>
public decimal EntryThreshold
{
get => _entryThreshold.Value;
set => _entryThreshold.Value = value;
}
/// <summary>
/// Z-score threshold required to close a position.
/// </summary>
public decimal ExitThreshold
{
get => _exitThreshold.Value;
set => _exitThreshold.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.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 AssetGrowthEffectStrategy()
{
_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
.SetDisplay("Second Security Id", "Identifier of the secondary benchmark security", "General");
_assetLength = Param(nameof(AssetLength), 8)
.SetRange(2, 40)
.SetDisplay("Asset Length", "Smoothing length for the synthetic asset base", "Indicators");
_lookbackPeriod = Param(nameof(LookbackPeriod), 24)
.SetRange(10, 150)
.SetDisplay("Lookback Period", "Lookback period used to normalize growth spread", "Indicators");
_entryThreshold = Param(nameof(EntryThreshold), 1.35m)
.SetRange(0.5m, 4m)
.SetDisplay("Entry Threshold", "Z-score threshold required to open a position", "Signals");
_exitThreshold = Param(nameof(ExitThreshold), 0.3m)
.SetRange(0m, 2m)
.SetDisplay("Exit Threshold", "Z-score threshold required to close a position", "Signals");
_cooldownBars = Param(nameof(CooldownBars), 12)
.SetRange(0, 100)
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk");
_stopLoss = Param(nameof(StopLoss), 2.5m)
.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!;
_primaryAssetBase = null!;
_secondaryAssetBase = null!;
_growthSpreadAverage = null!;
_growthSpreadDeviation = null!;
_previousPrimaryAssetBase = 0m;
_previousSecondaryAssetBase = 0m;
_latestPrimaryGrowth = 0m;
_latestSecondaryGrowth = 0m;
_primaryUpdated = false;
_secondaryUpdated = false;
_previousZScore = null;
_cooldownRemaining = 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 };
_primaryAssetBase = new ExponentialMovingAverage { Length = AssetLength };
_secondaryAssetBase = new ExponentialMovingAverage { Length = AssetLength };
_growthSpreadAverage = new SimpleMovingAverage { Length = LookbackPeriod };
_growthSpreadDeviation = new StandardDeviation { Length = LookbackPeriod };
_cooldownRemaining = 0;
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;
_latestPrimaryGrowth = UpdateGrowth(_primaryAssetBase, candle, ref _previousPrimaryAssetBase);
_primaryUpdated = true;
TryProcessGrowthSpread(candle.OpenTime);
}
private void ProcessSecondaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestSecondaryGrowth = UpdateGrowth(_secondaryAssetBase, candle, ref _previousSecondaryAssetBase);
_secondaryUpdated = true;
TryProcessGrowthSpread(candle.OpenTime);
}
private decimal UpdateGrowth(ExponentialMovingAverage average, ICandleMessage candle, ref decimal previousValue)
{
var syntheticAssets = CalculateSyntheticAssets(candle);
var assetBase = average.Process(syntheticAssets, candle.OpenTime, true).ToDecimal();
if (previousValue == 0m)
{
previousValue = assetBase;
return 0m;
}
var growth = (assetBase - previousValue) / Math.Max(Math.Abs(previousValue), 1m);
previousValue = assetBase;
return growth;
}
private decimal CalculateSyntheticAssets(ICandleMessage candle)
{
var priceBase = Math.Max(candle.OpenPrice, 1m);
var range = Math.Max(candle.HighPrice - candle.LowPrice, Security?.PriceStep ?? 1m);
var turnoverProxy = candle.ClosePrice * (1m + ((range / priceBase) * 5m));
var balanceSheetProxy = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
return turnoverProxy + balanceSheetProxy;
}
private void TryProcessGrowthSpread(DateTime time)
{
if (!_primaryUpdated || !_secondaryUpdated)
return;
_primaryUpdated = false;
_secondaryUpdated = false;
if (!_primaryAssetBase.IsFormed || !_secondaryAssetBase.IsFormed)
return;
var growthSpread = _latestPrimaryGrowth - _latestSecondaryGrowth;
var mean = _growthSpreadAverage.Process(growthSpread, time, true).ToDecimal();
var deviation = _growthSpreadDeviation.Process(growthSpread, time, true).ToDecimal();
if (!_growthSpreadAverage.IsFormed || !_growthSpreadDeviation.IsFormed || deviation <= 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var zScore = (growthSpread - mean) / deviation;
var bullishEntry = _previousZScore is decimal previousBullish && previousBullish > -EntryThreshold && zScore <= -EntryThreshold;
var bearishEntry = _previousZScore is decimal previousBearish && previousBearish < EntryThreshold && zScore >= EntryThreshold;
if (_cooldownRemaining == 0 && Position == 0)
{
if (bullishEntry)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (bearishEntry)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
}
else if (Position > 0 && zScore >= -ExitThreshold)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && zScore <= ExitThreshold)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_previousZScore = zScore;
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
from indicator_extensions import *
class asset_growth_effect_strategy(Strategy):
"""Relative asset-growth strategy using dual securities."""
def __init__(self):
super(asset_growth_effect_strategy, self).__init__()
self._security2_id = self.Param("Security2Id", "TONUSDT@BNBFT") \
.SetDisplay("Second Security Id", "Identifier of the secondary benchmark security", "General")
self._asset_length = self.Param("AssetLength", 8) \
.SetRange(2, 40) \
.SetDisplay("Asset Length", "Smoothing length for the synthetic asset base", "Indicators")
self._lookback_period = self.Param("LookbackPeriod", 24) \
.SetRange(10, 150) \
.SetDisplay("Lookback Period", "Lookback period used to normalize growth spread", "Indicators")
self._entry_threshold = self.Param("EntryThreshold", 1.35) \
.SetRange(0.5, 4.0) \
.SetDisplay("Entry Threshold", "Z-score threshold required to open a position", "Signals")
self._exit_threshold = self.Param("ExitThreshold", 0.3) \
.SetRange(0.0, 2.0) \
.SetDisplay("Exit Threshold", "Z-score threshold required to close a position", "Signals")
self._cooldown_bars = self.Param("CooldownBars", 12) \
.SetRange(0, 100) \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk")
self._stop_loss = self.Param("StopLoss", 2.5) \
.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_asset_base = None
self._secondary_asset_base = None
self._growth_spread_average = None
self._growth_spread_deviation = None
self._prev_primary_asset_base = 0.0
self._prev_secondary_asset_base = 0.0
self._latest_primary_growth = 0.0
self._latest_secondary_growth = 0.0
self._primary_updated = False
self._secondary_updated = False
self._previous_z_score = None
self._cooldown_remaining = 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_growth_effect_strategy, self).OnReseted()
self._security2 = None
self._primary_asset_base = None
self._secondary_asset_base = None
self._growth_spread_average = None
self._growth_spread_deviation = None
self._prev_primary_asset_base = 0.0
self._prev_secondary_asset_base = 0.0
self._latest_primary_growth = 0.0
self._latest_secondary_growth = 0.0
self._primary_updated = False
self._secondary_updated = False
self._previous_z_score = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(asset_growth_effect_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
asset_len = int(self._asset_length.Value)
lookback = int(self._lookback_period.Value)
self._primary_asset_base = ExponentialMovingAverage()
self._primary_asset_base.Length = asset_len
self._secondary_asset_base = ExponentialMovingAverage()
self._secondary_asset_base.Length = asset_len
self._growth_spread_average = SimpleMovingAverage()
self._growth_spread_average.Length = lookback
self._growth_spread_deviation = StandardDeviation()
self._growth_spread_deviation.Length = lookback
self._cooldown_remaining = 0
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_growth, self._prev_primary_asset_base = self.UpdateGrowth(
self._primary_asset_base, candle, self._prev_primary_asset_base)
self._primary_updated = True
self.TryProcessGrowthSpread(candle.OpenTime)
def ProcessSecondaryCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_secondary_growth, self._prev_secondary_asset_base = self.UpdateGrowth(
self._secondary_asset_base, candle, self._prev_secondary_asset_base)
self._secondary_updated = True
self.TryProcessGrowthSpread(candle.OpenTime)
def UpdateGrowth(self, average, candle, previous_value):
synthetic_assets = self.CalculateSyntheticAssets(candle)
result = process_float(average, synthetic_assets, candle.OpenTime, True)
asset_base = float(result)
if previous_value == 0.0:
return 0.0, asset_base
growth = (asset_base - previous_value) / max(abs(previous_value), 1.0)
return growth, asset_base
def CalculateSyntheticAssets(self, candle):
open_p = candle.OpenPrice
high_p = candle.HighPrice
low_p = candle.LowPrice
close_p = candle.ClosePrice
price_base = Math.Max(open_p, Decimal(1))
price_step = self.Security.PriceStep if self.Security is not None and self.Security.PriceStep is not None else Decimal(1)
range_val = Math.Max(high_p - low_p, price_step)
turnover_proxy = close_p * (Decimal(1) + ((range_val / price_base) * Decimal(5)))
balance_sheet_proxy = (high_p + low_p + close_p) / Decimal(3)
return float(turnover_proxy + balance_sheet_proxy)
def TryProcessGrowthSpread(self, time):
if not self._primary_updated or not self._secondary_updated:
return
self._primary_updated = False
self._secondary_updated = False
if not self._primary_asset_base.IsFormed or not self._secondary_asset_base.IsFormed:
return
growth_spread = self._latest_primary_growth - self._latest_secondary_growth
mean_result = process_float(self._growth_spread_average, growth_spread, time, True)
mean = float(mean_result)
dev_result = process_float(self._growth_spread_deviation, growth_spread, time, True)
deviation = float(dev_result)
if not self._growth_spread_average.IsFormed or not self._growth_spread_deviation.IsFormed or deviation <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
z_score = (growth_spread - mean) / deviation
entry_thresh = float(self._entry_threshold.Value)
exit_thresh = float(self._exit_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
bullish_entry = self._previous_z_score is not None and self._previous_z_score > -entry_thresh and z_score <= -entry_thresh
bearish_entry = self._previous_z_score is not None and self._previous_z_score < entry_thresh and z_score >= entry_thresh
if self._cooldown_remaining == 0 and self.Position == 0:
if bullish_entry:
self.BuyMarket()
self._cooldown_remaining = cooldown
elif bearish_entry:
self.SellMarket()
self._cooldown_remaining = cooldown
elif self.Position > 0 and z_score >= -exit_thresh:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and z_score <= exit_thresh:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._previous_z_score = z_score
def CreateClone(self):
return asset_growth_effect_strategy()