| Overall Statistics |
|
Total Orders 285 Average Win 1.03% Average Loss -0.97% Compounding Annual Return 7.130% Drawdown 29.900% Expectancy -0.047 Start Equity 100000 End Equity 134879.14 Net Profit 34.879% Sharpe Ratio 0.102 Sortino Ratio 0.115 Probabilistic Sharpe Ratio 5.909% Loss Rate 54% Win Rate 46% Profit-Loss Ratio 1.07 Alpha -0.024 Beta 0.895 Annual Standard Deviation 0.161 Annual Variance 0.026 Information Ratio -0.304 Tracking Error 0.094 Treynor Ratio 0.018 Total Fees $1076.19 Estimated Strategy Capacity $390000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 4.30% Drawdown Recovery 787 |
#region imports
from AlgorithmImports import *
#endregion
"""
CORE-TILT MODEL WITH PARAMETERIZED TACTICAL ETF OVERLAY
This strategy uses a core-satellite structure.
The core position is SPY. This gives the portfolio stable broad-market exposure.
The tactical tilt is smaller and can rotate between a levered long ETF and an
inverse ETF depending on the trend signal.
The default setup is:
Core:
SPY
Bullish tilt:
TQQQ
Bearish tilt:
SH
The model uses SPY as the signal asset. If SPY is in an uptrend, the strategy
adds the bullish tilt ETF. If SPY is in a downtrend and RSI confirms weakness, the
strategy adds the inverse ETF. If signals are mixed, the tilt sleeve stays in cash.
The model is parameterized so it can be optimized from the QuantConnect interface.
It uses the same parameter style as the prior examples.
Parameters:
core-weight:
Permanent SPY allocation, expressed as a percentage.
tilt-weight:
Maximum tactical sleeve allocation, expressed as a percentage.
ema-slow:
Slow EMA period.
ema-spread:
Difference between slow EMA and fast EMA.
fast EMA = ema-slow - ema-spread.
period_rsi:
RSI period.
low_rsi:
Lower RSI threshold used to confirm bearish conditions.
high_rsi:
Upper RSI threshold used to confirm bullish strength.
tilt-long:
Levered or aggressive ETF used for bullish tilt.
tilt-short:
Inverse ETF used for bearish tilt.
Important risk note:
Levered and inverse ETFs are tactical instruments. Their long-term behavior can
differ materially from the simple multiple of the underlying index because of
daily compounding and path dependency. The tilt sleeve should therefore remain
smaller than the core position.
"""
class CoreTiltParameterizedAlgorithm(QCAlgorithm):
def Initialize(self):
# ------------------------------------------------------------
# 1. BACKTEST SETTINGS
# ------------------------------------------------------------
self.SetStartDate(2022, 1, 1)
self.SetEndDate(2026, 5, 5)
self.initial_cash = 100000
self.SetCash(self.initial_cash)
# ------------------------------------------------------------
# 2. PARAMETERS
# ------------------------------------------------------------
self.core_weight = self.GetFloatParameter("core-weight", 70) / 100.0
self.tilt_weight = self.GetFloatParameter("tilt-weight", 30) / 100.0
self.slow = self.GetIntParameter("ema-slow", 50)
self.spread_ema = self.GetIntParameter("ema-spread", 30)
self.fast = self.slow - self.spread_ema
if self.fast < 1:
self.fast = 1
if self.slow <= self.fast:
self.slow = self.fast + 1
self.rsi_period = self.GetIntParameter("period_rsi", 14)
self.low_rsi = self.GetIntParameter("low_rsi", 45)
self.high_rsi = self.GetIntParameter("high_rsi", 55)
if self.low_rsi < 0:
self.low_rsi = 0
if self.high_rsi > 100:
self.high_rsi = 100
if self.high_rsi <= self.low_rsi:
self.high_rsi = self.low_rsi + 1
self.long_tilt_ticker = self.GetStringParameter("tilt-long", "TQQQ")
self.short_tilt_ticker = self.GetStringParameter("tilt-short", "SH")
# Make sure portfolio weights do not exceed 100%.
total_weight = self.core_weight + self.tilt_weight
if total_weight > 1.0:
scale = 1.0 / total_weight
self.core_weight = self.core_weight * scale
self.tilt_weight = self.tilt_weight * scale
# ------------------------------------------------------------
# 3. SECURITIES
# ------------------------------------------------------------
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.long_tilt = self.AddEquity(
self.long_tilt_ticker,
Resolution.Daily
).Symbol
self.short_tilt = self.AddEquity(
self.short_tilt_ticker,
Resolution.Daily
).Symbol
self.SetBenchmark(self.spy)
# ------------------------------------------------------------
# 4. INDICATORS ON SPY
# ------------------------------------------------------------
self.ema_fast = self.EMA(
self.spy,
self.fast,
Resolution.Daily
)
self.ema_slow = self.EMA(
self.spy,
self.slow,
Resolution.Daily
)
self.rsi = self.RSI(
self.spy,
self.rsi_period,
MovingAverageType.Wilders,
Resolution.Daily
)
warmup_period = max(
self.slow,
self.rsi_period
)
self.SetWarmUp(
warmup_period,
Resolution.Daily
)
# ------------------------------------------------------------
# 5. BENCHMARK VARIABLES
# ------------------------------------------------------------
self.initial_spy_price = None
# ------------------------------------------------------------
# 6. TRADE CONTROL
# ------------------------------------------------------------
self.current_core_weight = 0
self.current_long_tilt_weight = 0
self.current_short_tilt_weight = 0
self.rebalance_threshold = 0.02
self.Debug(
"Parameters: "
+ "core="
+ str(round(self.core_weight, 2))
+ ", tilt="
+ str(round(self.tilt_weight, 2))
+ ", fast EMA="
+ str(self.fast)
+ ", slow EMA="
+ str(self.slow)
+ ", RSI period="
+ str(self.rsi_period)
+ ", low RSI="
+ str(self.low_rsi)
+ ", high RSI="
+ str(self.high_rsi)
+ ", long tilt="
+ self.long_tilt_ticker
+ ", short tilt="
+ self.short_tilt_ticker
)
def OnData(self, data):
# ------------------------------------------------------------
# 1. CHECK DATA
# ------------------------------------------------------------
if self.spy not in data or data[self.spy] is None:
return
if self.long_tilt not in data or data[self.long_tilt] is None:
return
if self.short_tilt not in data or data[self.short_tilt] is None:
return
spy_price = self.Securities[self.spy].Price
if spy_price <= 0:
return
if self.initial_spy_price is None:
self.initial_spy_price = spy_price
# ------------------------------------------------------------
# 2. WAIT FOR INDICATORS
# ------------------------------------------------------------
if self.IsWarmingUp:
return
if not self.ema_fast.IsReady:
return
if not self.ema_slow.IsReady:
return
if not self.rsi.IsReady:
return
# ------------------------------------------------------------
# 3. SIGNALS
# ------------------------------------------------------------
bullish_trend = self.ema_fast.Current.Value > self.ema_slow.Current.Value
bearish_trend = self.ema_fast.Current.Value < self.ema_slow.Current.Value
rsi_value = self.rsi.Current.Value
bullish_confirmation = rsi_value > self.high_rsi
bearish_confirmation = rsi_value < self.low_rsi
# ------------------------------------------------------------
# 4. TARGET WEIGHTS
# ------------------------------------------------------------
target_spy_weight = self.core_weight
target_long_tilt_weight = 0.0
target_short_tilt_weight = 0.0
if bullish_trend and bullish_confirmation:
target_long_tilt_weight = self.tilt_weight
target_short_tilt_weight = 0.0
elif bearish_trend and bearish_confirmation:
target_long_tilt_weight = 0.0
target_short_tilt_weight = self.tilt_weight
else:
target_long_tilt_weight = 0.0
target_short_tilt_weight = 0.0
# ------------------------------------------------------------
# 5. EXECUTION
# ------------------------------------------------------------
self.ApplyTargetWeight(
self.spy,
target_spy_weight,
"SPY Core"
)
self.ApplyTargetWeight(
self.long_tilt,
target_long_tilt_weight,
"Long Tilt"
)
self.ApplyTargetWeight(
self.short_tilt,
target_short_tilt_weight,
"Short Tilt"
)
# ------------------------------------------------------------
# 6. PLOTS
# ------------------------------------------------------------
spy_benchmark_value = (
self.initial_cash
* spy_price
/ self.initial_spy_price
)
self.Plot(
"Strategy Equity",
"Portfolio Value",
self.Portfolio.TotalPortfolioValue
)
self.Plot(
"Strategy Equity",
"Buy Hold SPY",
spy_benchmark_value
)
core_cash_benchmark = (
self.initial_cash
* (
(1.0 - self.core_weight)
+
self.core_weight
* spy_price
/ self.initial_spy_price
)
)
self.Plot(
"Strategy Equity",
"Core SPY Cash Benchmark",
core_cash_benchmark
)
self.Plot(
"Signal",
"EMA Fast",
self.ema_fast.Current.Value
)
self.Plot(
"Signal",
"EMA Slow",
self.ema_slow.Current.Value
)
self.Plot(
"RSI",
"RSI",
rsi_value
)
self.Plot(
"RSI",
"Low RSI",
self.low_rsi
)
self.Plot(
"RSI",
"High RSI",
self.high_rsi
)
self.Plot(
"Target Weights",
"SPY Core",
target_spy_weight
)
self.Plot(
"Target Weights",
"Long Tilt",
target_long_tilt_weight
)
self.Plot(
"Target Weights",
"Inverse Tilt",
target_short_tilt_weight
)
# ------------------------------------------------------------
# EXECUTION HELPER
# ------------------------------------------------------------
def ApplyTargetWeight(self, symbol, target_weight, label):
current_weight = 0.0
if self.Portfolio.TotalPortfolioValue > 0:
current_weight = (
self.Portfolio[symbol].HoldingsValue
/ self.Portfolio.TotalPortfolioValue
)
if abs(target_weight - current_weight) > self.rebalance_threshold:
self.SetHoldings(
symbol,
target_weight
)
self.Debug(
label
+ " target "
+ str(round(target_weight, 2))
+ " at "
+ str(self.Time.date())
)
# ------------------------------------------------------------
# PARAMETER HELPERS
# ------------------------------------------------------------
def GetIntParameter(self, name, default_value):
value = self.GetParameter(name)
if value is None or value == "":
return default_value
return int(value)
def GetFloatParameter(self, name, default_value):
value = self.GetParameter(name)
if value is None or value == "":
return default_value
return float(value)
def GetStringParameter(self, name, default_value):
value = self.GetParameter(name)
if value is None or value == "":
return default_value
return value