| 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)