Overall Statistics
Total Orders
34
Average Win
17.93%
Average Loss
-1.05%
Compounding Annual Return
26.329%
Drawdown
26.000%
Expectancy
8.550
Start Equity
100000
End Equity
321475.70
Net Profit
221.476%
Sharpe Ratio
0.757
Sortino Ratio
0.854
Probabilistic Sharpe Ratio
36.095%
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
17.04
Alpha
0.129
Beta
0.609
Annual Standard Deviation
0.215
Annual Variance
0.046
Information Ratio
0.522
Tracking Error
0.204
Treynor Ratio
0.267
Total Fees
$391.49
Estimated Strategy Capacity
$140000000.00
Lowest Capacity Asset
XLE RGRPZX100F39
Portfolio Turnover
1.48%
Drawdown Recovery
429
# region imports
from AlgorithmImports import *
# endregion


class MomentumETFRotationLean(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(self.end_date - timedelta(5 * 365))
        self.set_cash(100_000)
        self._universe = ["SPY", "QQQ", "IWM", "XLK", "XLV", "XLE", "XLF", "XLI", "XLB", "XLU", "GLD", "VNQ"]
        self._safe_haven = "AGG"
        self._mom_long = 252
        for ticker in self._universe + [self._safe_haven]:
            self.add_equity(ticker, Resolution.DAILY)
        self.set_warm_up(280, Resolution.DAILY)
        self.schedule.on(self.date_rules.month_start(self._universe[0]), self.time_rules.at(8, 0), self._rebalance)

    def on_warmup_finished(self) -> None:
        self._rebalance()

    def _momentum_score(self, symbol: Symbol) -> float:
        history = self.history(symbol, self._mom_long + 1, Resolution.DAILY)
        if history.empty or len(history) < self._mom_long:
            return float("-inf")
        closes = history["close"]
        price_long = closes.iloc[0]
        if price_long <= 0:
            return float("-inf")
        # Skip the most recent month so the score captures 12-1 month momentum.
        return (closes.iloc[-22] / price_long) - 1.0

    def _rebalance(self) -> None:
        if self.is_warming_up:
            return
        scores = {ticker: self._momentum_score(self.symbol(ticker)) for ticker in self._universe}
        top_ticker, top_score = max(scores.items(), key=lambda item: item[1])
        target = top_ticker if top_score > 0 else self._safe_haven
        for holding in self.portfolio.values():
            if holding.invested and holding.symbol.value != target:
                self.liquidate(holding.symbol)
        self.set_holdings(target, 1.0)