| Overall Statistics |
|
Total Orders 1266 Average Win 1.38% Average Loss -1.26% Compounding Annual Return 89.518% Drawdown 40.700% Expectancy 0.421 Start Equity 100000 End Equity 2443408.43 Net Profit 2343.408% Sharpe Ratio 1.643 Sortino Ratio 1.78 Probabilistic Sharpe Ratio 81.079% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 1.10 Alpha 0.471 Beta 1.959 Annual Standard Deviation 0.392 Annual Variance 0.153 Information Ratio 1.863 Tracking Error 0.298 Treynor Ratio 0.329 Total Fees $30323.56 Estimated Strategy Capacity $160000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 19.19% Drawdown Recovery 298 |
# ==============================================================================
# QuantConnect Verification Script — Leveraged ETF Regime Rotation
# ==============================================================================
# Strategies:
# TQQQ: Asymmetric stay-in (OBV>SMA20 & QQQ_leading_SPY & OBV>SMA50) + RSI<30 override
# Defensive sleeve: 50% GLD + 50% SVXY
# SOXL: AND-stack (QQQ_leading_SPY & QQQ_RSI<75 & SMH_mom3m>0 & QQQ_SMA150_slope_up)
# Defensive sleeve: 100% GLD
#
# Expected Results (5Y backtest June 2020 - May 2025):
# TQQQ: CAGR~109%, DD~-31%, TIM~41%
# SOXL: CAGR~139%, DD~-39%, TIM~37%
#
# TIMING CONVENTION:
# Signal computed at close[d] → trade executed at close[d] → earns return close[d] to close[d+1]
# In QC: Schedule rebalance BeforeMarketClose to evaluate signals and trade.
# ==============================================================================
from AlgorithmImports import *
import numpy as np
from collections import deque
class LeveragedRegimeRotation(QCAlgorithm):
def Initialize(self):
# --- Backtest window ---
self.SetStartDate(2020, 6, 1)
self.SetEndDate(2025, 5, 31)
self.SetCash(100000)
# --- Add equities ---
self.tqqq = self.AddEquity("TQQQ", Resolution.Daily).Symbol
self.soxl = self.AddEquity("SOXL", Resolution.Daily).Symbol
self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.smh = self.AddEquity("SMH", Resolution.Daily).Symbol
self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
self.svxy = self.AddEquity("SVXY", Resolution.Daily).Symbol
# --- Parameters ---
self.RSI_REENTRY_THRESHOLD = 30
self.QQQ_RSI_OVERBOUGHT = 75
self.QQQ_SPY_RATIO_PERIOD = 50
self.OBV_SMA_SHORT = 20
self.OBV_SMA_LONG = 50
self.SMH_MOM_PERIOD = 63
self.QQQ_SMA150_PERIOD = 150
self.QQQ_SMA150_SLOPE_LOOKBACK = 10
# --- Allocation (split capital equally between the two strategies) ---
self.TQQQ_ALLOC = 0.5
self.SOXL_ALLOC = 0.5
# --- Manual indicator history buffers ---
# TQQQ OBV
self.tqqq_close_history = deque(maxlen=300)
self.tqqq_volume_history = deque(maxlen=300)
self.tqqq_obv_history = deque(maxlen=60)
# QQQ/SPY ratio
self.qqq_close_history = deque(maxlen=200)
self.spy_close_history = deque(maxlen=200)
self.qqq_spy_ratio_history = deque(maxlen=60)
# QQQ RSI
self.qqq_gain_history = deque(maxlen=20)
self.qqq_loss_history = deque(maxlen=20)
self.qqq_prev_close = None
self.qqq_avg_gain = None
self.qqq_avg_loss = None
self.qqq_rsi_warmup_count = 0
# SMH momentum
self.smh_close_history = deque(maxlen=70)
# QQQ SMA150
self.qqq_sma150_history = deque(maxlen=15)
# --- State ---
self.tqqq_in_market = True
self.soxl_in_market = True
self.warmup_days = 200
self.days_elapsed = 0
# --- Schedule rebalance at end of day ---
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5),
self.Rebalance
)
# --- Warmup ---
self.SetWarmUp(self.warmup_days, Resolution.Daily)
def OnData(self, data):
# Accumulate price/volume data for manual indicators
if data.Bars.ContainsKey(self.tqqq):
bar = data.Bars[self.tqqq]
self.tqqq_close_history.append(bar.Close)
self.tqqq_volume_history.append(bar.Volume)
self._update_tqqq_obv()
if data.Bars.ContainsKey(self.qqq):
bar = data.Bars[self.qqq]
self.qqq_close_history.append(bar.Close)
self._update_qqq_rsi(bar.Close)
self._update_qqq_sma150()
if data.Bars.ContainsKey(self.spy):
self.spy_close_history.append(data.Bars[self.spy].Close)
if data.Bars.ContainsKey(self.smh):
self.smh_close_history.append(data.Bars[self.smh].Close)
# Update QQQ/SPY ratio
if len(self.qqq_close_history) > 0 and len(self.spy_close_history) > 0:
ratio = float(self.qqq_close_history[-1]) / float(self.spy_close_history[-1])
self.qqq_spy_ratio_history.append(ratio)
def _update_tqqq_obv(self):
"""Compute cumulative OBV for TQQQ."""
if len(self.tqqq_close_history) < 2:
self.tqqq_obv_history.append(0)
return
price_change = float(self.tqqq_close_history[-1]) - float(self.tqqq_close_history[-2])
volume = float(self.tqqq_volume_history[-1])
if price_change > 0:
obv_delta = volume
elif price_change < 0:
obv_delta = -volume
else:
obv_delta = 0
prev_obv = self.tqqq_obv_history[-1] if len(self.tqqq_obv_history) > 0 else 0
self.tqqq_obv_history.append(prev_obv + obv_delta)
def _update_qqq_rsi(self, current_close):
"""Compute QQQ RSI(14) using standard Wilder smoothing."""
if self.qqq_prev_close is None:
self.qqq_prev_close = current_close
return
change = float(current_close) - float(self.qqq_prev_close)
gain = max(change, 0)
loss = max(-change, 0)
self.qqq_prev_close = current_close
self.qqq_rsi_warmup_count += 1
if self.qqq_rsi_warmup_count <= 14:
self.qqq_gain_history.append(gain)
self.qqq_loss_history.append(loss)
if self.qqq_rsi_warmup_count == 14:
self.qqq_avg_gain = sum(self.qqq_gain_history) / 14.0
self.qqq_avg_loss = sum(self.qqq_loss_history) / 14.0
else:
# Wilder smoothing (same as pandas rolling(14).mean() for first, then EMA-like)
# Note: pandas rolling(14).mean() uses simple rolling average, not Wilder.
# To match pandas: use simple rolling mean of last 14 values
self.qqq_gain_history.append(gain)
self.qqq_loss_history.append(loss)
def _get_qqq_rsi(self):
"""Get current QQQ RSI using simple 14-period rolling mean (matches pandas)."""
if len(self.qqq_gain_history) < 14:
return 50.0 # neutral during warmup
# Simple rolling mean of last 14 gains/losses (matches pandas rolling(14).mean())
gains = list(self.qqq_gain_history)[-14:]
losses = list(self.qqq_loss_history)[-14:]
avg_gain = sum(gains) / 14.0
avg_loss = sum(losses) / 14.0
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
return 100.0 - (100.0 / (1.0 + rs))
def _update_qqq_sma150(self):
"""Track QQQ SMA150 values for slope calculation."""
if len(self.qqq_close_history) >= self.QQQ_SMA150_PERIOD:
sma150 = sum(list(self.qqq_close_history)[-self.QQQ_SMA150_PERIOD:]) / self.QQQ_SMA150_PERIOD
self.qqq_sma150_history.append(sma150)
def _get_obv_above_sma(self, period):
"""Check if current OBV > N-period SMA of OBV."""
if len(self.tqqq_obv_history) < period:
return False
obv_values = list(self.tqqq_obv_history)[-period:]
sma = sum(obv_values) / period
return self.tqqq_obv_history[-1] > sma
def _get_qqq_leading_spy(self):
"""Check if QQQ/SPY ratio > its 50-day SMA."""
if len(self.qqq_spy_ratio_history) < self.QQQ_SPY_RATIO_PERIOD:
return False
ratio_values = list(self.qqq_spy_ratio_history)[-self.QQQ_SPY_RATIO_PERIOD:]
sma = sum(ratio_values) / self.QQQ_SPY_RATIO_PERIOD
return self.qqq_spy_ratio_history[-1] > sma
def _get_smh_mom3m_positive(self):
"""Check if SMH 63-day return > 0."""
if len(self.smh_close_history) < self.SMH_MOM_PERIOD + 1:
return False
current = float(self.smh_close_history[-1])
past = float(self.smh_close_history[-self.SMH_MOM_PERIOD - 1])
if past == 0:
return False
return (current / past - 1) > 0
def _get_qqq_sma150_slope_positive(self):
"""Check if QQQ SMA150 today > QQQ SMA150 10 days ago."""
if len(self.qqq_sma150_history) < self.QQQ_SMA150_SLOPE_LOOKBACK + 1:
return False
current_sma = self.qqq_sma150_history[-1]
past_sma = self.qqq_sma150_history[-self.QQQ_SMA150_SLOPE_LOOKBACK - 1]
return current_sma > past_sma
def Rebalance(self):
"""Daily end-of-day signal evaluation and position adjustment."""
if self.IsWarmingUp:
return
self.days_elapsed += 1
# ===== TQQQ SIGNAL =====
obv_above_sma20 = self._get_obv_above_sma(self.OBV_SMA_SHORT)
obv_above_sma50 = self._get_obv_above_sma(self.OBV_SMA_LONG)
qqq_leading_spy = self._get_qqq_leading_spy()
qqq_rsi = self._get_qqq_rsi()
# TQQQ base stay-in: OBV>SMA20 AND QQQ_leading_SPY AND OBV>SMA50
tqqq_stay_in = obv_above_sma20 and qqq_leading_spy and obv_above_sma50
# Override: RSI < 30 (oversold bounce)
tqqq_rsi_override = qqq_rsi < self.RSI_REENTRY_THRESHOLD
# Final: stay_in OR override
tqqq_in_market = tqqq_stay_in or tqqq_rsi_override
# ===== SOXL SIGNAL =====
qqq_rsi_below_75 = qqq_rsi < self.QQQ_RSI_OVERBOUGHT
smh_mom3m_pos = self._get_smh_mom3m_positive()
qqq_sma150_slope_pos = self._get_qqq_sma150_slope_positive()
# SOXL stay-in: all four conditions
soxl_in_market = (qqq_leading_spy and qqq_rsi_below_75 and
smh_mom3m_pos and qqq_sma150_slope_pos)
# ===== EXECUTE TRADES =====
self._execute_tqqq_allocation(tqqq_in_market)
self._execute_soxl_allocation(soxl_in_market)
# Log signals
if tqqq_in_market != self.tqqq_in_market or soxl_in_market != self.soxl_in_market:
self.Debug(f"{self.Time.date()} | TQQQ: {'IN' if tqqq_in_market else 'OUT'} "
f"(base={tqqq_stay_in}, rsi_override={tqqq_rsi_override}, RSI={qqq_rsi:.1f}) | "
f"SOXL: {'IN' if soxl_in_market else 'OUT'} "
f"(leading={qqq_leading_spy}, rsi<75={qqq_rsi_below_75}, "
f"smh_mom={smh_mom3m_pos}, slope={qqq_sma150_slope_pos})")
self.tqqq_in_market = tqqq_in_market
self.soxl_in_market = soxl_in_market
def _execute_tqqq_allocation(self, in_market):
"""Allocate TQQQ portion: either TQQQ or 50% GLD + 50% SVXY."""
alloc = self.TQQQ_ALLOC
if in_market:
self.SetHoldings(self.tqqq, alloc)
# Liquidate defensive sleeve
if self.Portfolio[self.gld].Invested and self._is_tqqq_sleeve_holder():
# Only liquidate GLD/SVXY if they belong to TQQQ sleeve
pass # Handled below via target percentages
self._set_tqqq_defensive(0, 0)
else:
# Defensive: 50% GLD + 50% SVXY (of the TQQQ allocation)
if self.Portfolio[self.tqqq].Invested:
self.Liquidate(self.tqqq)
self._set_tqqq_defensive(alloc * 0.5, alloc * 0.5)
def _execute_soxl_allocation(self, in_market):
"""Allocate SOXL portion: either SOXL or 100% GLD."""
alloc = self.SOXL_ALLOC
if in_market:
self.SetHoldings(self.soxl, alloc)
# Reduce GLD from SOXL portion (if any)
self._adjust_gld_for_soxl(0)
else:
if self.Portfolio[self.soxl].Invested:
self.Liquidate(self.soxl)
self._adjust_gld_for_soxl(alloc)
def _set_tqqq_defensive(self, gld_alloc, svxy_alloc):
"""Set the TQQQ defensive sleeve allocations."""
# We track desired allocations and combine with SOXL's GLD needs
self._tqqq_gld_target = gld_alloc
self._tqqq_svxy_target = svxy_alloc
self._apply_combined_targets()
def _adjust_gld_for_soxl(self, gld_alloc):
"""Set the SOXL defensive sleeve GLD allocation."""
self._soxl_gld_target = gld_alloc
self._apply_combined_targets()
def _apply_combined_targets(self):
"""Apply combined GLD + SVXY targets from both strategies."""
tqqq_gld = getattr(self, '_tqqq_gld_target', 0)
tqqq_svxy = getattr(self, '_tqqq_svxy_target', 0)
soxl_gld = getattr(self, '_soxl_gld_target', 0)
total_gld = tqqq_gld + soxl_gld
total_svxy = tqqq_svxy
if total_gld > 0:
self.SetHoldings(self.gld, total_gld)
elif self.Portfolio[self.gld].Invested:
self.Liquidate(self.gld)
if total_svxy > 0:
self.SetHoldings(self.svxy, total_svxy)
elif self.Portfolio[self.svxy].Invested:
self.Liquidate(self.svxy)
def _is_tqqq_sleeve_holder(self):
return not self.tqqq_in_market
def OnEndOfAlgorithm(self):
self.Debug(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
self.Debug(f"Days traded: {self.days_elapsed}")
# ==============================================================================
# ALTERNATIVE: Single-Strategy Version (run TQQQ or SOXL independently)
# Use this if you want to verify each strategy's CAGR/DD separately.
# ==============================================================================
class TQQQRegimeRotation(QCAlgorithm):
"""TQQQ-only strategy for isolated verification."""
def Initialize(self):
self.SetStartDate(2020, 6, 1)
self.SetEndDate(2025, 5, 31)
self.SetCash(100000)
self.tqqq = self.AddEquity("TQQQ", Resolution.Daily).Symbol
self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
self.svxy = self.AddEquity("SVXY", Resolution.Daily).Symbol
# Indicator buffers
self.tqqq_close_hist = deque(maxlen=300)
self.tqqq_volume_hist = deque(maxlen=300)
self.tqqq_obv_hist = deque(maxlen=60)
self.qqq_close_hist = deque(maxlen=200)
self.spy_close_hist = deque(maxlen=200)
self.qqq_spy_ratio_hist = deque(maxlen=60)
self.qqq_gain_hist = deque(maxlen=20)
self.qqq_loss_hist = deque(maxlen=20)
self.qqq_prev_close = None
self.qqq_rsi_count = 0
self.in_market = True
self.days_in = 0
self.days_out = 0
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5),
self.Rebalance
)
self.SetWarmUp(200, Resolution.Daily)
def OnData(self, data):
if data.Bars.ContainsKey(self.tqqq):
bar = data.Bars[self.tqqq]
self.tqqq_close_hist.append(float(bar.Close))
self.tqqq_volume_hist.append(float(bar.Volume))
self._update_obv()
if data.Bars.ContainsKey(self.qqq):
close = float(data.Bars[self.qqq].Close)
self.qqq_close_hist.append(close)
self._update_rsi(close)
if data.Bars.ContainsKey(self.spy):
self.spy_close_hist.append(float(data.Bars[self.spy].Close))
if len(self.qqq_close_hist) > 0 and len(self.spy_close_hist) > 0:
self.qqq_spy_ratio_hist.append(self.qqq_close_hist[-1] / self.spy_close_hist[-1])
def _update_obv(self):
if len(self.tqqq_close_hist) < 2:
self.tqqq_obv_hist.append(0)
return
diff = self.tqqq_close_hist[-1] - self.tqqq_close_hist[-2]
vol = self.tqqq_volume_hist[-1]
delta = vol if diff > 0 else (-vol if diff < 0 else 0)
prev = self.tqqq_obv_hist[-1] if self.tqqq_obv_hist else 0
self.tqqq_obv_hist.append(prev + delta)
def _update_rsi(self, close):
if self.qqq_prev_close is None:
self.qqq_prev_close = close
return
change = close - self.qqq_prev_close
self.qqq_prev_close = close
self.qqq_gain_hist.append(max(change, 0))
self.qqq_loss_hist.append(max(-change, 0))
self.qqq_rsi_count += 1
def _get_rsi(self):
if len(self.qqq_gain_hist) < 14:
return 50.0
gains = list(self.qqq_gain_hist)[-14:]
losses = list(self.qqq_loss_hist)[-14:]
avg_g = sum(gains) / 14.0
avg_l = sum(losses) / 14.0
if avg_l == 0:
return 100.0
return 100.0 - 100.0 / (1.0 + avg_g / avg_l)
def _obv_above_sma(self, period):
if len(self.tqqq_obv_hist) < period:
return False
vals = list(self.tqqq_obv_hist)[-period:]
return self.tqqq_obv_hist[-1] > (sum(vals) / period)
def _qqq_leading_spy(self):
if len(self.qqq_spy_ratio_hist) < 50:
return False
vals = list(self.qqq_spy_ratio_hist)[-50:]
return self.qqq_spy_ratio_hist[-1] > (sum(vals) / 50)
def Rebalance(self):
if self.IsWarmingUp:
return
obv20 = self._obv_above_sma(20)
obv50 = self._obv_above_sma(50)
leading = self._qqq_leading_spy()
rsi = self._get_rsi()
stay_in = obv20 and leading and obv50
override = rsi < 30
in_market = stay_in or override
if in_market:
self.SetHoldings(self.tqqq, 1.0)
if self.Portfolio[self.gld].Invested:
self.Liquidate(self.gld)
if self.Portfolio[self.svxy].Invested:
self.Liquidate(self.svxy)
self.days_in += 1
else:
if self.Portfolio[self.tqqq].Invested:
self.Liquidate(self.tqqq)
self.SetHoldings(self.gld, 0.5)
self.SetHoldings(self.svxy, 0.5)
self.days_out += 1
if in_market != self.in_market:
self.Debug(f"{self.Time.date()} TQQQ {'ENTER' if in_market else 'EXIT'} | "
f"OBV20={obv20} OBV50={obv50} Leading={leading} RSI={rsi:.1f} Override={override}")
self.in_market = in_market
def OnEndOfAlgorithm(self):
total = self.days_in + self.days_out
tim = self.days_in / total * 100 if total > 0 else 0
self.Debug(f"TQQQ Final: ${self.Portfolio.TotalPortfolioValue:,.2f} | "
f"TIM={tim:.1f}% ({self.days_in}/{total} days)")
class SOXLRegimeRotation(QCAlgorithm):
"""SOXL-only strategy for isolated verification."""
def Initialize(self):
self.SetStartDate(2020, 6, 1)
self.SetEndDate(2025, 5, 31)
self.SetCash(100000)
self.soxl = self.AddEquity("SOXL", Resolution.Daily).Symbol
self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.smh = self.AddEquity("SMH", Resolution.Daily).Symbol
self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
# Indicator buffers
self.qqq_close_hist = deque(maxlen=200)
self.spy_close_hist = deque(maxlen=200)
self.smh_close_hist = deque(maxlen=70)
self.qqq_spy_ratio_hist = deque(maxlen=60)
self.qqq_gain_hist = deque(maxlen=20)
self.qqq_loss_hist = deque(maxlen=20)
self.qqq_prev_close = None
self.qqq_rsi_count = 0
self.qqq_sma150_hist = deque(maxlen=15)
self.in_market = True
self.days_in = 0
self.days_out = 0
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 5),
self.Rebalance
)
self.SetWarmUp(200, Resolution.Daily)
def OnData(self, data):
if data.Bars.ContainsKey(self.qqq):
close = float(data.Bars[self.qqq].Close)
self.qqq_close_hist.append(close)
self._update_rsi(close)
self._update_sma150()
if data.Bars.ContainsKey(self.spy):
self.spy_close_hist.append(float(data.Bars[self.spy].Close))
if data.Bars.ContainsKey(self.smh):
self.smh_close_hist.append(float(data.Bars[self.smh].Close))
if len(self.qqq_close_hist) > 0 and len(self.spy_close_hist) > 0:
self.qqq_spy_ratio_hist.append(self.qqq_close_hist[-1] / self.spy_close_hist[-1])
def _update_rsi(self, close):
if self.qqq_prev_close is None:
self.qqq_prev_close = close
return
change = close - self.qqq_prev_close
self.qqq_prev_close = close
self.qqq_gain_hist.append(max(change, 0))
self.qqq_loss_hist.append(max(-change, 0))
self.qqq_rsi_count += 1
def _get_rsi(self):
if len(self.qqq_gain_hist) < 14:
return 50.0
gains = list(self.qqq_gain_hist)[-14:]
losses = list(self.qqq_loss_hist)[-14:]
avg_g = sum(gains) / 14.0
avg_l = sum(losses) / 14.0
if avg_l == 0:
return 100.0
return 100.0 - 100.0 / (1.0 + avg_g / avg_l)
def _update_sma150(self):
if len(self.qqq_close_hist) >= 150:
sma = sum(list(self.qqq_close_hist)[-150:]) / 150.0
self.qqq_sma150_hist.append(sma)
def _qqq_leading_spy(self):
if len(self.qqq_spy_ratio_hist) < 50:
return False
vals = list(self.qqq_spy_ratio_hist)[-50:]
return self.qqq_spy_ratio_hist[-1] > (sum(vals) / 50)
def _smh_mom3m_positive(self):
if len(self.smh_close_hist) < 64:
return False
return self.smh_close_hist[-1] > self.smh_close_hist[-64]
def _qqq_sma150_slope_positive(self):
if len(self.qqq_sma150_hist) < 11:
return False
return self.qqq_sma150_hist[-1] > self.qqq_sma150_hist[-11]
def Rebalance(self):
if self.IsWarmingUp:
return
leading = self._qqq_leading_spy()
rsi_ok = self._get_rsi() < 75
smh_mom = self._smh_mom3m_positive()
slope = self._qqq_sma150_slope_positive()
in_market = leading and rsi_ok and smh_mom and slope
if in_market:
self.SetHoldings(self.soxl, 1.0)
if self.Portfolio[self.gld].Invested:
self.Liquidate(self.gld)
self.days_in += 1
else:
if self.Portfolio[self.soxl].Invested:
self.Liquidate(self.soxl)
self.SetHoldings(self.gld, 1.0)
self.days_out += 1
if in_market != self.in_market:
self.Debug(f"{self.Time.date()} SOXL {'ENTER' if in_market else 'EXIT'} | "
f"Leading={leading} RSI<75={rsi_ok} SMH_mom={smh_mom} Slope={slope}")
self.in_market = in_market
def OnEndOfAlgorithm(self):
total = self.days_in + self.days_out
tim = self.days_in / total * 100 if total > 0 else 0
self.Debug(f"SOXL Final: ${self.Portfolio.TotalPortfolioValue:,.2f} | "
f"TIM={tim:.1f}% ({self.days_in}/{total} days)")