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)