Quant review 2026-09-26: alert cooldown, plain-language alerts, M15 grade-A SELL block, 1R printed targets, panel/breach fixes

Live signals.db review (2026-09-04 → 09-25, 100 unique setups replayed on the
scans' stored candles): the stop-hunt claim is refuted (71% of stop-outs ran
≥ 1R past the stop, the same as the books' 70–72%); −25.1R of the −37.8R raw
live book were consecutive-bar re-entries the backtester's 5-bar cooldown never
takes; the cooldown view sits at the 3rd–5th percentile of the books' three-week
windows.

Delivery (book-neutral):
- telegram_bot: 5-bar alert cooldown per timeframe (_ALERT_COOLDOWN_BARS =
  backtest.cooldown_bars); setup block prints the trail first as the tested
  exit, TP1/TP2 as guides with their R, "Planned TP1 RR", and one validated-book
  line per setup class (agent_panel_base_rates.json); the informational 100-pip
  TP line is removed (owner request).
- AI block rewritten for a new trader: "AI check: the odds look better than /
  worse than / about usual", the chance "in 100" against the usual odds, one
  reason for and one against, no verdict word or confidence figure; rendered
  whenever the verdict carries a probability. agent_panel: persona prompts ban
  trading jargon (PANEL_PROMPT_VERSION v2.1-2026-09-26); the macro-source
  verdict now carries its persona's p_win (every live v2 row had
  panel_probability NULL). Dashboard and guides mirror the wording.
- telegram_bot: level-breach rescans skip the settlement-pause window (the
  20:45 M15 bar was re-logged at 22:01/22:06/22:11 every evening).

Signal (pre-registered rule-1 campaign, CUDA twin paired with gpu_backtest.py):
- M15 grade-A SELLs blocked (sell_min_quality ["A+","B"], M15 override 6):
  W2 813/+154.15R PF 1.53 vs 931/+146.14R PF 1.43, OOS +68.5 vs +60.67,
  W3 max DD 20.9 vs 19.3R. PASSED a–f.
- Printed target 1 floored at 1R in every generator (signals.tp_print_floor_rr),
  applied AFTER the min-RR and spread gates so no trade changes; mirrored in
  cuda_signal_kernels.py K2. Label-only re-pin of all four timeframes (only
  tp1/rr_planned columns move). A floor applied before the gate cost −24.6R and
  was rejected.
- FAILED: M15 min_rr_ratio 1.0 (trade-identical, −2.0R). Owner's call: M5
  tp_floor_rr 1.0 (+48.9R W2, max DD 10.1 → 15.9R), not shipped.

New pins: M5 W2 e599c8a8…, M15 W2 5b65ecf6…, M15 OOS 09b8a8e7…, H1 W3
a44fd19b…, H4 W3 f3587bae…; twin and reference MD5s equal on every window.
Base-rate table rebuilt from the new pins. Suite 558 passed.

Evidence: backtest/research/rr_and_sell_campaign_2026-09-26.md (plus the
2026-09-18 loss-anatomy and wrong-direction reports it builds on).
This commit is contained in:
Harwinder Sekhon
2026-09-26 21:06:05 -04:00
parent 266f204d35
commit 4e0a083955
16 changed files with 1695 additions and 201 deletions
+20 -5
View File
@@ -126,7 +126,7 @@ VERDICT_SOURCES = ("mean", "macro", "chair")
#: Logged with every verdict and folded into the payload hash. Bump on
#: ANY prompt or context-schema change so the scorecard can split eras
#: (analyze_signal_log splits by panel_prompt_version).
PANEL_PROMPT_VERSION = "v2-2026-09-17"
PANEL_PROMPT_VERSION = "v2.1-2026-09-26" # v2.1: beginner wording rules in _TAKE_CONTRACT
#: Symmetric probability bands (percent). A persona's stance and the
#: mean verdict are DERIVED from p_win with the same thresholds, so
@@ -216,7 +216,14 @@ Write everything in plain everyday English for someone who has never
traded: short simple sentences, no trading jargon, no acronyms, no
abbreviations. Say "profit versus loss", not "RR"; "the 4-hour chart",
not "H4"; describe what an indicator MEANS ("the price has climbed
unusually fast"), never its name or number.
unusually fast"), never its name or number. Never use these words:
"priced in", "bullish", "bearish", "hawkish", "dovish", "macro",
"sentiment", "momentum", "volatility", "resistance", "support level",
"breakout", "pullback", "basis points", "yields". Say instead: "the
price already moved on that news", "news that is good for gold",
"news that is bad for gold", "higher interest rates", "the price
fell fast", "the price bounced". Each sentence must make sense to a
reader who does not know what gold or interest rates usually do.
Reply with ONLY JSON:
{"p_win": <0-100>, "reason_for": "<max 12 words>",
"reason_against": "<max 12 words>", "note": "<max 15 words>"}
@@ -250,8 +257,8 @@ gold this way"; the payload's gold_influence already did that scoring.
Example: brief direction SELL, price already fell 5 ATR in 4 hours, every
bearish headline published before move_started_at, base_rate.win_rate 58
-> {"p_win": 55, "reason_for": "Recent news still leans against gold.",
"reason_against": "That news is older than the drop, so it is priced in.",
"note": "The bad news is already in the price; expect the usual odds."}
"reason_against": "That news came before the drop; the price already moved on it.",
"note": "The price already moved on the bad news; expect the usual odds."}
Example: brief direction BUY, a high-impact release came out 20 minutes
ago with a surprise the code marked bullish for gold, base_rate.win_rate 60
-> {"p_win": 68, "reason_for": "A fresh surprise in the data favours gold.",
@@ -1244,11 +1251,19 @@ def run_panel(report: "SignalReport",
if verdict_source == "macro":
macro = next((t for t in takes if t.persona == "macro"), None)
if macro is not None:
# The macro persona's own p_win is the verdict's probability
# (2026-09-26 signals.db review: every v2 row logged under
# verdict_source="macro" had panel_probability NULL — the
# verdict carried none, _write_fields wrote 0.0 and the logger
# dropped the falsy value — so the 100/300-outcome evaluation
# would have had no live probabilities to score).
return _finish(PanelVerdict(
verdict=_STANCE_TO_VERDICT[macro.stance],
confidence=macro.confidence, rationale=macro.note[:240],
takes=takes, created_ts=time.time(),
payload_hash=payload_hash, source="macro"))
payload_hash=payload_hash, source="macro",
probability=(round(float(macro.p_win), 1)
if macro.p_win is not None else None)))
# Macro take missing (timed out / invalid) — fall back to the chair.
# Quorum guards the CHAIR only: it weighs several voices, so one lone
# take is not worth a synthesizer request. The macro path above needs