Overall Statistics
Total Orders
107
Average Win
4.56%
Average Loss
-2.26%
Compounding Annual Return
39.588%
Drawdown
25.500%
Expectancy
1.525
Start Equity
100000
End Equity
529311.33
Net Profit
429.311%
Sharpe Ratio
1.175
Sortino Ratio
1.307
Probabilistic Sharpe Ratio
70.748%
Loss Rate
16%
Win Rate
84%
Profit-Loss Ratio
2.02
Alpha
0.211
Beta
0.647
Annual Standard Deviation
0.21
Annual Variance
0.044
Information Ratio
0.978
Tracking Error
0.196
Treynor Ratio
0.382
Total Fees
$1274.85
Estimated Strategy Capacity
$140000000.00
Lowest Capacity Asset
XLE RGRPZX100F39
Portfolio Turnover
3.81%
Drawdown Recovery
291
# 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.week_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
        self.set_holdings([PortfolioTarget(self.symbol(target), 0.98)], True)