| Overall Statistics |
|
Total Orders 107 Average Win 2.71% Average Loss -1.16% Compounding Annual Return 3.509% Drawdown 36.100% Expectancy 0.227 Start Equity 100000 End Equity 115518.70 Net Profit 15.519% Sharpe Ratio -0.073 Sortino Ratio -0.08 Probabilistic Sharpe Ratio 3.010% Loss Rate 63% Win Rate 37% Profit-Loss Ratio 2.34 Alpha -0.027 Beta 0.205 Annual Standard Deviation 0.154 Annual Variance 0.024 Information Ratio -0.458 Tracking Error 0.193 Treynor Ratio -0.055 Total Fees $166.51 Estimated Strategy Capacity $9200000.00 Lowest Capacity Asset OEF RZ8CR0XXNOF9 Portfolio Turnover 2.28% Drawdown Recovery 175 |
#region imports
from AlgorithmImports import *
#endregion
"""
VXX MOMENTUM MODEL FOR OEF EXPOSURE
This strategy uses VXX momentum as a tactical signal for exposure to OEF.
OEF is the traded asset. It represents large-cap U.S. equity exposure through the
S&P 100 ETF.
VXX is used only as a signal asset. The model does not trade VXX directly.
Instead, it watches the short-term trend in VXX using two exponential moving
averages.
Signal logic:
1. If the fast EMA of VXX is above the slow EMA of VXX:
VXX is in an upward momentum regime.
This is interpreted as rising volatility pressure.
The strategy reduces or shorts OEF exposure.
2. If the fast EMA of VXX is below the slow EMA of VXX:
VXX is in a downward momentum regime.
This is interpreted as easing volatility pressure.
The strategy holds long OEF exposure.
Portfolio logic:
- Defensive regime:
target OEF weight = -base_exposure * leverage_up
- Risk-on regime:
target OEF weight = base_exposure * leverage_down
The model includes a maximum absolute exposure cap so that the portfolio does not
take excessive leverage.
The benchmark is buy-and-hold OEF. This is appropriate because the strategy is a
tactical overlay on OEF exposure.
"""
class VXXMOMENTUMPredictsStockIndexReturns(QCAlgorithm):
def Initialize(self):
# ------------------------------------------------------------
# 1. BACKTEST SETTINGS
# ------------------------------------------------------------
self.SetStartDate(2022, 3, 1)
self.SetEndDate(2026, 5, 5)
self.initial_cash = 100000
self.SetCash(self.initial_cash)
# ------------------------------------------------------------
# 2. SYMBOLS
# ------------------------------------------------------------
self.oef_ticker = "OEF"
self.vxx_ticker = "VXX"
self.oef = self.AddEquity(
self.oef_ticker,
Resolution.Daily
).Symbol
self.vxx = self.AddEquity(
self.vxx_ticker,
Resolution.Daily
).Symbol
self.SetBenchmark(self.oef)
# ------------------------------------------------------------
# 3. PARAMETERS
# ------------------------------------------------------------
self.ema_fast_period = self.GetIntParameter("ema-fast", 5)
self.ema_slow_period = self.GetIntParameter("ema-slow", 20)
if self.ema_fast_period < 1:
self.ema_fast_period = 1
if self.ema_slow_period <= self.ema_fast_period:
self.ema_slow_period = self.ema_fast_period + 1
# These preserve your original parameter names.
self.leverage_up = self.GetFloatParameter("leverage_up", 1.0)
self.leverage_down = self.GetFloatParameter("leverage_down", 1.0)
# Base exposure replaces the hard-coded 1.5 in the original model.
# You can set it to 1.5 if you want the original aggressiveness.
self.base_exposure = self.GetFloatParameter("base_exposure", 1.0)
# Maximum absolute target weight.
self.max_abs_weight = self.GetFloatParameter("max_abs_weight", 1.0)
# Avoid tiny repeated orders.
self.rebalance_threshold = self.GetFloatParameter("rebalance_threshold", 0.02)
# ------------------------------------------------------------
# 4. INDICATORS
# ------------------------------------------------------------
self.ema_fast = self.EMA(
self.vxx,
self.ema_fast_period,
Resolution.Daily
)
self.ema_slow = self.EMA(
self.vxx,
self.ema_slow_period,
Resolution.Daily
)
self.SetWarmUp(
self.ema_slow_period + 5,
Resolution.Daily
)
# ------------------------------------------------------------
# 5. BENCHMARK STATE
# ------------------------------------------------------------
self.initial_oef_price = None
self.current_target_weight = 0.0
self.Debug(
"Parameters: "
+ "ema_fast="
+ str(self.ema_fast_period)
+ ", ema_slow="
+ str(self.ema_slow_period)
+ ", leverage_up="
+ str(self.leverage_up)
+ ", leverage_down="
+ str(self.leverage_down)
+ ", base_exposure="
+ str(self.base_exposure)
+ ", max_abs_weight="
+ str(self.max_abs_weight)
)
def OnData(self, data):
# ------------------------------------------------------------
# 1. DATA CHECKS
# ------------------------------------------------------------
if self.IsWarmingUp:
return
if self.oef not in data or data[self.oef] is None:
return
if self.vxx not in data or data[self.vxx] is None:
return
if not self.ema_fast.IsReady or not self.ema_slow.IsReady:
return
oef_price = self.Securities[self.oef].Price
vxx_price = self.Securities[self.vxx].Price
if oef_price <= 0 or vxx_price <= 0:
return
if self.initial_oef_price is None:
self.initial_oef_price = oef_price
# ------------------------------------------------------------
# 2. SIGNAL
# ------------------------------------------------------------
volatility_momentum_up = (
self.ema_fast.Current.Value
> self.ema_slow.Current.Value
)
if volatility_momentum_up:
# Rising VXX momentum: defensive or short OEF.
target_weight = -self.base_exposure * self.leverage_up
else:
# Falling VXX momentum: long OEF.
target_weight = self.base_exposure * self.leverage_down
# ------------------------------------------------------------
# 3. EXPOSURE CAP
# ------------------------------------------------------------
if target_weight > self.max_abs_weight:
target_weight = self.max_abs_weight
if target_weight < -self.max_abs_weight:
target_weight = -self.max_abs_weight
# ------------------------------------------------------------
# 4. EXECUTION ONLY IF TARGET CHANGES MEANINGFULLY
# ------------------------------------------------------------
current_weight = self.GetCurrentWeight(self.oef)
if abs(target_weight - current_weight) >= self.rebalance_threshold:
self.SetHoldings(
self.oef,
target_weight
)
self.current_target_weight = target_weight
self.Debug(
str(self.Time.date())
+ " | VXX fast="
+ str(round(self.ema_fast.Current.Value, 4))
+ " slow="
+ str(round(self.ema_slow.Current.Value, 4))
+ " | target OEF="
+ str(round(target_weight, 2))
)
# ------------------------------------------------------------
# 5. PLOTS
# ------------------------------------------------------------
oef_buy_hold_value = (
self.initial_cash
* oef_price
/ self.initial_oef_price
)
self.Plot(
"Strategy Equity",
"Portfolio Value",
self.Portfolio.TotalPortfolioValue
)
self.Plot(
"Strategy Equity",
"Buy Hold OEF",
oef_buy_hold_value
)
self.Plot(
"VXX Signal",
"VXX Price",
vxx_price
)
self.Plot(
"VXX Signal",
"EMA Fast",
self.ema_fast.Current.Value
)
self.Plot(
"VXX Signal",
"EMA Slow",
self.ema_slow.Current.Value
)
self.Plot(
"Portfolio State",
"Target OEF Weight",
self.current_target_weight
)
def GetCurrentWeight(self, symbol):
if self.Portfolio.TotalPortfolioValue <= 0:
return 0.0
return (
self.Portfolio[symbol].HoldingsValue
/ self.Portfolio.TotalPortfolioValue
)
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)