Fix: paper trader missing TQQQ overlay evaluation in parking gate

_parking_evaluate_gate() was returning only QQQM or SGOV, never TQQQ,
because the low-vol overlay logic (_evaluate_low_vol_overlay_target in
backtester) was not implemented in the live engine. Add overlay check
at the end of the gate: vol threshold, temperature, entropy, and Hurst
R/S analysis — matching the backtester's conditions exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent cbeb463685
commit 6489b22c89

@ -1343,6 +1343,81 @@ class PaperTradingEngine:
if entropy is not None and entropy > ent_thr:
return "sgov"
# Low-vol overlay: upgrade base symbol (e.g. QQQM → TQQQ) when market is calm
overlay_sym = (risk.cash_parking_low_vol_overlay_symbol or "").upper()
if overlay_sym and overlay_sym != sym and self._parking_brake_cooldown_remaining <= 0:
def _compute_hurst(lb: int) -> float | None:
if n < lb + 1:
return None
rets = []
for i in range(n - lb, n):
if closes[i - 1] > 0:
rets.append((closes[i] - closes[i - 1]) / closes[i - 1])
if len(rets) < 30:
return None
def _rs(series: list[float]) -> float:
nn = len(series)
mean = sum(series) / nn
devs = [x - mean for x in series]
cumdev, s = [], 0.0
for d in devs:
s += d
cumdev.append(s)
r = max(cumdev) - min(cumdev)
std = (sum(d ** 2 for d in devs) / nn) ** 0.5
return r / std if std > 0 else 0.0
window_sizes = [w for w in [8, 12, 16, 24, 32] if w <= len(rets) // 2]
if len(window_sizes) < 2:
return None
log_n, log_rs = [], []
for w in window_sizes:
rs_vals = [_rs(rets[s:s + w]) for s in range(0, len(rets) - w + 1, w) if len(rets[s:s + w]) == w]
if rs_vals:
avg_rs = sum(rs_vals) / len(rs_vals)
if avg_rs > 0:
log_n.append(math.log(w))
log_rs.append(math.log(avg_rs))
if len(log_n) < 2:
return None
nn2 = len(log_n)
xm, ym = sum(log_n) / nn2, sum(log_rs) / nn2
num = sum((log_n[i] - xm) * (log_rs[i] - ym) for i in range(nn2))
den = sum((log_n[i] - xm) ** 2 for i in range(nn2))
return num / den if den > 0 else 0.5
overlay_ok = True
overlay_vol_thr = risk.cash_parking_low_vol_overlay_vol_threshold
if overlay_vol_thr > 0 and (vol is None or vol >= overlay_vol_thr):
overlay_ok = False
if overlay_ok:
temp_max = risk.cash_parking_low_vol_overlay_temperature_max
if temp_max > 0:
if vol_15 is None or vol_50 is None or vol_50 <= 0 or (vol_15 / vol_50) > temp_max:
overlay_ok = False
if overlay_ok:
entropy_max = risk.cash_parking_low_vol_overlay_entropy_max
if entropy_max > 0:
ov_entropy = _compute_entropy(risk.cash_parking_entropy_lookback)
if ov_entropy is None or ov_entropy > entropy_max:
overlay_ok = False
if overlay_ok:
hurst_min = risk.cash_parking_low_vol_overlay_hurst_min
if hurst_min > 0:
hurst = _compute_hurst(60)
if hurst is None or hurst < hurst_min:
overlay_ok = False
if overlay_ok:
logger.info("parking_overlay_selected", symbol=overlay_sym)
return overlay_sym.lower()
return actual_sym
def _parking_check_shock_brake(self, today: dt.date) -> bool:

Loading…
Cancel
Save