Overall Statistics
Total Orders
12869
Average Win
0.06%
Average Loss
-0.07%
Compounding Annual Return
6.642%
Drawdown
12.900%
Expectancy
0.076
Start Equity
200000
End Equity
275892.06
Net Profit
37.946%
Sharpe Ratio
0.093
Sortino Ratio
0.102
Probabilistic Sharpe Ratio
10.100%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
0.95
Alpha
-0.011
Beta
0.324
Annual Standard Deviation
0.091
Annual Variance
0.008
Information Ratio
-0.41
Tracking Error
0.124
Treynor Ratio
0.026
Total Fees
$14762.54
Estimated Strategy Capacity
$88000000.00
Lowest Capacity Asset
FRT R735QTJ8XC9X
Portfolio Turnover
24.03%
Drawdown Recovery
769
from datetime import timedelta
from AlgorithmImports import *
import pandas as pd
import numpy as np


class EquityVolatilityTargetingUniverse(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(days=5 * 365))
        self.set_cash(200000)
        self.settings.seed_initial_prices = True

        self.universe_settings.resolution = Resolution.DAILY

        # Universe: SPY constituents
        self._universe = self.add_universe(self.universe.etf("SPY"), self._select_spy)

        # Rebalance daily at 08:00 ET so universe selection is current
        self.schedule.on(
            self.date_rules.every_day("SPY"),
            self.time_rules.at(8, 0),
            self._rebalance,
        )

    def _select_spy(self, constituents):
        return [c.symbol for c in constituents]

    def _rebalance(self):
        symbols = list(self._universe.selected)
        if not symbols:
            return

        # Request 31 days of daily bars to compute 30 daily returns
        history = self.history(TradeBar, symbols, 31, Resolution.DAILY)
        if history.empty:
            return

        # Pivot to time x symbol close-price matrix
        close_prices = history["close"].unstack(level=0)
        if close_prices.empty:
            return

        # Daily returns
        returns = close_prices.pct_change().dropna()
        if returns.empty:
            return

        # Annualized realized volatility (std of daily returns * sqrt(252))
        vol = returns.std() * np.sqrt(252)

        # Keep only names below 15 % annualized vol
        low_vol = vol[vol < 0.15]
        if low_vol.empty:
            return

        # Lowest-vol 30, equal-weighted
        targets = low_vol.nsmallest(30).index.tolist()
        if not targets:
            return

        # Use a 2 % cash buffer to avoid margin exhaustion during rebalance
        weight = 0.98 / len(targets)

        # Build explicit targets for every symbol (current + new) so LEAN
        # computes order deltas instead of relying on liquidate_existing_holdings.
        all_symbols = set(targets)
        for kvp in self.portfolio:
            all_symbols.add(kvp.key)

        target_list = []
        for symbol in all_symbols:
            if symbol in targets:
                target_list.append(PortfolioTarget(symbol, weight))
            else:
                target_list.append(PortfolioTarget(symbol, 0.0))

        self.set_holdings(target_list, liquidate_existing_holdings=False)