| Overall Statistics |
|
Total Orders 2252 Average Win 0.16% Average Loss -0.12% Compounding Annual Return 13.165% Drawdown 11.000% Expectancy 0.554 Start Equity 100000 End Equity 210143.08 Net Profit 110.143% Sharpe Ratio 0.812 Sortino Ratio 0.969 Probabilistic Sharpe Ratio 71.318% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 1.32 Alpha 0.04 Beta 0.245 Annual Standard Deviation 0.075 Annual Variance 0.006 Information Ratio -0.177 Tracking Error 0.144 Treynor Ratio 0.248 Total Fees $2591.69 Estimated Strategy Capacity $1400000.00 Lowest Capacity Asset BIL TT1EBZ21QWKL Portfolio Turnover 2.61% Drawdown Recovery 305 |
# region imports
from AlgorithmImports import *
# endregion
TICKERS = [
"BIL", "UVXY", "TQQQ", "TECL", "UPRO", "SQQQ", "SOXL",
"SPY", "SPXL", "XLK", "DBC", "TLT", "NVDA",
]
BLOCKS = [
(0.9, 0.1), (0.8, 0.2), (0.7, 0.3), (0.6, 0.4), (0.5, 0.5),
(0.4, 0.6), (0.3, 0.7), (0.2, 0.8), (0.1, 0.9),
]
BIL_BIAS = 0.4 # Increased cash allocation
MAX_LEVERAGE = 0.55 # Reduced leverage for lower drawdown
VOL_LOOKBACK = 20 # Days to measure volatility
DD_STOP_THRESHOLD = 0.10 # Lower drawdown threshold for earlier exit
class TQQQFTLTQC(QCAlgorithm):
"""TQQQ FTLT Yolo - top 14 RSI Sort. Updates RSI/TI at 15:40 only."""
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2025, 12, 31)
self.set_cash(100000)
self.symbols = {}
self._rsi = {}
self._ma20 = {}
self._ma200 = {}
self._nvda_roc = RateOfChangePercent(1)
self._nvda_stdev5 = StandardDeviation(5)
self._spy_returns = RollingWindow[float](VOL_LOOKBACK)
self._peak_equity = self.portfolio.total_portfolio_value
for t in TICKERS:
sym = self.add_equity(t, Resolution.MINUTE).symbol
self.symbols[t] = sym
self._rsi[t] = {
2: RelativeStrengthIndex(2, MovingAverageType.WILDERS),
5: RelativeStrengthIndex(5, MovingAverageType.WILDERS),
10: RelativeStrengthIndex(10, MovingAverageType.WILDERS),
14: RelativeStrengthIndex(14, MovingAverageType.WILDERS),
}
self._ma20[t] = SimpleMovingAverage(20)
self._ma200[t] = SimpleMovingAverage(200)
self.set_warm_up(200, Resolution.MINUTE)
def _get_volatility_scalar(self) -> float:
"""Calculate position size scalar based on volatility and drawdown."""
if self._spy_returns.count < VOL_LOOKBACK:
return 0.4
returns = [self._spy_returns[i] for i in range(self._spy_returns.count)]
vol = np.std(returns) * np.sqrt(252)
# Balanced vol scaling
if vol > 0.30:
vol_scalar = 0.3
elif vol > 0.25:
vol_scalar = 0.5
elif vol > 0.20:
vol_scalar = 0.7
else:
vol_scalar = 1.0
# Balanced drawdown protection
current_equity = self.portfolio.total_portfolio_value
if current_equity > self._peak_equity:
self._peak_equity = current_equity
drawdown = (self._peak_equity - current_equity) / self._peak_equity
# Hard stop: exit all risky positions
if drawdown > DD_STOP_THRESHOLD:
return 0.0
elif drawdown > 0.10:
dd_scalar = 0.4
elif drawdown > 0.07:
dd_scalar = 0.6
else:
dd_scalar = 1.0
return min(vol_scalar, dd_scalar)
def _update_indicators(self, slice):
"""Update RSI and TI at 15:40 using bar closes."""
spy_sym = self.symbols["SPY"]
if spy_sym in slice.bars and self._spy_returns.count > 0:
prev_price = self.securities[spy_sym].price
curr_price = slice.bars[spy_sym].close
if prev_price > 0:
daily_return = (curr_price - prev_price) / prev_price
self._spy_returns.add(daily_return)
for t in TICKERS:
sym = self.symbols[t]
if sym not in slice.bars:
continue
bar = slice.bars[sym]
self._rsi[t][2].update(bar.end_time, bar.close)
self._rsi[t][5].update(bar.end_time, bar.close)
self._rsi[t][10].update(bar.end_time, bar.close)
self._rsi[t][14].update(bar.end_time, bar.close)
self._ma20[t].update(bar.end_time, bar.close)
self._ma200[t].update(bar.end_time, bar.close)
if t == "NVDA":
self._nvda_roc.update(bar.end_time, bar.close)
if self._nvda_roc.is_ready:
self._nvda_stdev5.update(bar.end_time, self._nvda_roc.current.value)
def _get_ti(self, ticker: str) -> dict:
"""Get current TI values. Returns dict with rsi_2, rsi_5, rsi_10, rsi_14, ma_20, ma_200, close, stdev5."""
out = {}
sym = self.symbols[ticker]
close = self.securities[sym].price
out["close"] = close
out["rsi_2"] = self._rsi[ticker][2].current.value
out["rsi_5"] = self._rsi[ticker][5].current.value
out["rsi_10"] = self._rsi[ticker][10].current.value
out["rsi_14"] = self._rsi[ticker][14].current.value
out["ma_20"] = self._ma20[ticker].current.value
out["ma_200"] = self._ma200[ticker].current.value
out["stdev5"] = self._nvda_stdev5.current.value * 100 if ticker == "NVDA" else 0.0
return out
def _bil_replace(self) -> str:
ti = self._get_ti("NVDA")
if not self._nvda_stdev5.is_ready:
return "BIL"
return "SOXL" if ti["stdev5"] > ti["rsi_2"] else "BIL"
def _tqqq_ftlt_reddit(self) -> str:
spy = self._get_ti("SPY")
if not self._ma200["SPY"].is_ready:
return "BIL"
spy_above_ma200 = spy["close"] > spy["ma_200"]
if spy_above_ma200:
tqqq = self._get_ti("TQQQ")
if tqqq["rsi_10"] > 79:
return "UVXY"
spxl = self._get_ti("SPXL")
if spxl["rsi_10"] > 80:
return "UVXY"
xlk = self._get_ti("XLK")
dbc = self._get_ti("DBC")
if xlk["rsi_10"] > dbc["rsi_10"]:
return "TQQQ"
return "BIL"
tqqq = self._get_ti("TQQQ")
if tqqq["rsi_10"] < 31:
return "TECL"
if spy["rsi_10"] < 30:
return "UPRO"
if self._ma20["TQQQ"].is_ready and tqqq["close"] < tqqq["ma_20"]:
sqqq = self._get_ti("SQQQ")
tlt = self._get_ti("TLT")
xlk = self._get_ti("XLK")
dbc = self._get_ti("DBC")
if sqqq["rsi_10"] > tlt["rsi_10"]:
if xlk["rsi_10"] > dbc["rsi_10"]:
return "BIL"
return "SQQQ"
return "BIL"
sqqq = self._get_ti("SQQQ")
if sqqq["rsi_10"] < 31:
return "SQQQ"
xlk = self._get_ti("XLK")
dbc = self._get_ti("DBC")
if xlk["rsi_10"] > dbc["rsi_10"]:
if dbc["close"] < dbc["ma_20"]:
return "TQQQ"
return "BIL"
return "BIL"
def _decide_weights(self) -> dict:
merged = {}
for w_bil, w_ftlt in BLOCKS:
t_bil = self._bil_replace()
t_ftlt = self._tqqq_ftlt_reddit()
bw = 1.0 / len(BLOCKS)
merged[t_bil] = merged.get(t_bil, 0.0) + bw * w_bil
merged[t_ftlt] = merged.get(t_ftlt, 0.0) + bw * w_ftlt
candidates = [(t, merged[t]) for t in merged if merged[t] > 0]
if not candidates:
return {"BIL": 1.0}
top2 = sorted(candidates, key=lambda x: -self._get_ti(x[0])["rsi_14"])[:2]
top2_weights = {t: 1.0 / len(top2) for t, _ in top2}
out = {t: 0.0 for t in TICKERS}
total = sum(top2_weights.values())
if total > 0:
for t, w in top2_weights.items():
out[t] = w / total
return out
def on_data(self, slice):
if self.time.hour != 15 or self.time.minute != 40:
return
self._update_indicators(slice)
if not self._rsi["SPY"][14].is_ready or not self._ma200["SPY"].is_ready:
return
if self.is_warming_up:
return
weights = self._decide_weights()
if "BIL" in weights:
weights["BIL"] += BIL_BIAS
# Apply volatility/drawdown scaling
vol_scalar = self._get_volatility_scalar()
# If in severe drawdown mode, go 100% cash
if vol_scalar == 0.0:
for t in TICKERS:
sym = self.symbols[t]
if t == "BIL":
self.set_holdings(sym, 1.0)
else:
self.liquidate(sym)
return
# Scale down risky assets, increase cash
risky_assets = ["TQQQ", "TECL", "UPRO", "SQQQ", "SOXL", "UVXY"]
for t in risky_assets:
if t in weights:
weights[t] *= vol_scalar
# Add remaining to BIL
reduction = 1.0 - vol_scalar
weights["BIL"] = weights.get("BIL", 0) + reduction
# Apply max leverage constraint
total = sum(weights.values())
if total > MAX_LEVERAGE:
scale = MAX_LEVERAGE / total
for t in weights:
weights[t] *= scale
weights["BIL"] = weights.get("BIL", 0) + (1.0 - MAX_LEVERAGE)
total = sum(weights.values())
if total <= 0:
return
for t in TICKERS:
sym = self.symbols[t]
if sym not in slice.bars:
continue
w = weights.get(t, 0) / total if total > 0 else 0.0
self.set_holdings(sym, float(w))