Overall Statistics
Total Orders
385
Average Win
1.19%
Average Loss
-1.26%
Compounding Annual Return
15.536%
Drawdown
28.100%
Expectancy
0.289
Start Equity
1000000
End Equity
2059263.06
Net Profit
105.926%
Sharpe Ratio
0.436
Sortino Ratio
0.443
Probabilistic Sharpe Ratio
5.387%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
0.94
Alpha
0.022
Beta
1.033
Annual Standard Deviation
0.187
Annual Variance
0.035
Information Ratio
0.205
Tracking Error
0.115
Treynor Ratio
0.079
Total Fees
$3537.70
Estimated Strategy Capacity
$2300000.00
Lowest Capacity Asset
VIXY UT076X30D0MD
Portfolio Turnover
4.20%
Drawdown Recovery
510
from AlgorithmImports import *


class VIXDualStrategy(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(1_000_000)

        self._lookback = self.get_parameter('lookback', 10)
        self._churn_threshold = self.get_parameter('churn_threshold', 0.02)

        # Add SPY as an asset to trade.
        self._spy = self.add_equity("SPY")
        # Add some indicators to calculate trailing returns.
        self._spy.daily_returns = self.roc(self._spy, 1, Resolution.DAILY)
        self._spy.daily_returns.window[self._lookback-2]
        self._spy.previous_close = self.identity(self._spy, Resolution.DAILY)
        # Add the VIX and VIX3M Indices.
        self._vix = self.add_index("VIX")
        self._vix3m = self.add_index("VIX3M")
        # Add the VIXY, which tracks the S&P 500 VIX Short-Term Futures Index.
        self._vixy = self.add_equity("VIXY")
        # Add a warm-up period to prime SPY's trailing daily returns.
        self.set_warm_up(self._lookback, Resolution.DAILY)    
        # Add a Scheduled Event to rebalance the portfolio each day.
        # 16 minutes before the close is the last chance to place MOC orders.
        self.schedule.on(self.date_rules.every_day(self._spy), self.time_rules.before_market_close(self._spy, 16), self._rebalance)

    def _rebalance(self) -> None:
        # During warm-up, do nothing.
        if self.is_warming_up:
            return
        # Get the trailing returns over the last 10 days.
        # Use the return from the previous close to now as the latest "daily return".
        returns = [x.value for x in self._spy.daily_returns.window]
        returns.append(self._spy.price / self._spy.previous_close.current.value - 1)
        # Calculate the expected realized vol (annualised, VIX points).
        e_rv30 = np.std(returns, ddof=1) * np.sqrt(252) * 100
        # Calculate the expected VRP.
        e_vrp = self._vix.price - e_rv30
        # Determine the target weight for VIXY.
        if e_vrp > 0 and self._vix.price < self._vix3m.price:
            # Case 1: Full short-vol conviction
            target_weight = -self._vix.price / 100
        elif e_vrp < 0 and self._vix.price < self._vix3m.price:
            # Case 2: Medium short-vol conviction (half size)
            target_weight = -0.5 * self._vix.price / 100
        elif e_vrp < 0 and self._vix.price > self._vix3m.price:
            # Case 3: Full long-vol conviction
            target_weight = self._vix.price / 100
        else:
            # Case 4: Cash (conflicting signals or eVRP == 0)
            target_weight = 0
        # To reduce churn, only rebalance when the target weight changes sign or is at least 
        # 2% away from the current weight.
        current_weight = self._vixy.holdings.holdings_value / self.portfolio.total_portfolio_value
        if np.sign(self._vixy.holdings.holdings_value) == np.sign(target_weight) and abs(current_weight - target_weight) <= self._churn_threshold:
            return
        # Rebalance VIXY to the target weight.
        vixy_qty = self.calculate_order_quantity(self._vixy, target_weight)
        if vixy_qty:
            self.market_on_close_order(self._vixy, vixy_qty)
        # If we are short volaility, buy SPY to keep gross exposure at 100%
        # instead of just holding onto cash.
        if target_weight < 0:
            spy_qty = self.calculate_order_quantity(self._spy, 1+target_weight)
            if spy_qty:
                self.market_on_close_order(self._spy, spy_qty)
        # Otherwise, exit SPY.
        elif self._spy.invested:
            self.market_on_close_order(self._spy, -self._spy.holdings.quantity)