Overall Statistics
Total Orders
267
Average Win
1.50%
Average Loss
-1.34%
Compounding Annual Return
7.697%
Drawdown
15.600%
Expectancy
0.232
Start Equity
200000
End Equity
289804.41
Net Profit
44.902%
Sharpe Ratio
0.167
Sortino Ratio
0.192
Probabilistic Sharpe Ratio
8.840%
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
1.12
Alpha
-0.012
Beta
0.526
Annual Standard Deviation
0.117
Annual Variance
0.014
Information Ratio
-0.355
Tracking Error
0.112
Treynor Ratio
0.037
Total Fees
$1509.09
Estimated Strategy Capacity
$100000000.00
Lowest Capacity Asset
XLB RGRPZX100F39
Portfolio Turnover
3.63%
Drawdown Recovery
605
from AlgorithmImports import *


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

        self._tickers = [
            "XLK", "XLF", "XLE", "XLV", "XLI", "XLB",
            "XLY", "XLP", "XLU", "XLRE", "XLC"
        ]

        self._roc_period = 63
        self._top_count = 3
        self._rocs = {}
        self._volatilities = {}
        self._entry_prices = {}

        for ticker in self._tickers:
            equity = self.add_equity(ticker, Resolution.DAILY)
            self._rocs[equity.symbol] = self.roc(equity.symbol, self._roc_period, Resolution.DAILY)
            self._volatilities[equity.symbol] = self.std(equity.symbol, 20, Resolution.DAILY)

        self._spy = self.add_equity("SPY", Resolution.DAILY)
        self._spy_roc = self.roc(self._spy.symbol, self._roc_period, Resolution.DAILY)

        self._shy = self.add_equity("SHY", Resolution.DAILY)

        self.schedule.on(
            self.date_rules.month_start('SPY'),
            self.time_rules.at(8, 0),
            self._rebalance
        )

        self.schedule.on(
            self.date_rules.every_day('SPY'),
            self.time_rules.before_market_close('SPY', 5),
            self._check_stops
        )

    def _rebalance(self):
        if not all(r.is_ready for r in self._rocs.values()):
            return
        if not self._spy_roc.is_ready:
            return

        spy_roc = self._spy_roc.current.value

        scores = []
        for symbol, roc in self._rocs.items():
            rel_strength = roc.current.value - spy_roc
            scores.append((symbol, rel_strength))

        scores.sort(key=lambda x: x[1], reverse=True)
        top = scores[:self._top_count]
        top_symbols = [s[0] for s in top]
        top_scores = [s[1] for s in top]

        avg_rs = sum(top_scores) / len(top_scores)
        if avg_rs <= 0:
            self.liquidate()
            self.set_holdings(self._shy.symbol, 1.0)
            return

        vols = {}
        for symbol in top_symbols:
            vols[symbol] = max(self._volatilities[symbol].current.value, 1e-6)

        inv_vols = {s: 1.0 / vols[s] for s in top_symbols}
        total_inv = sum(inv_vols.values())
        weights = {s: inv_vols[s] / total_inv for s in top_symbols}

        targets = [PortfolioTarget(s, weights[s]) for s in top_symbols]
        self.set_holdings(targets, liquidate_existing_holdings=True)

        for symbol in top_symbols:
            self._entry_prices[symbol] = self.securities[symbol].price

    def _check_stops(self):
        for symbol, entry_price in list(self._entry_prices.items()):
            if self.portfolio[symbol].invested:
                current_price = self.securities[symbol].price
                if current_price < entry_price * 0.95:
                    self.liquidate(symbol)
                    del self._entry_prices[symbol]