Files
ForexBot2/backtest/research/panel_v2_2026-09-17.md
T
Harwinder Sekhon ad14ea4fed news corpus for a local scorer + agent panel v2 + dashboard read-only (2026-09-17); books byte-identical
Agent panel v2: personas return p_win anchored to the book base rates with a
code-computed decision context; verdict = equal-weight mean (verdict_source
"mean"); only the macro persona runs live (agent_panel.personas ["macro"]);
prompt version and exact payloads logged; offline replay harness
scripts/panel_replay.py; base-rate table agent_panel_base_rates.json
(scripts/build_panel_base_rates.py). Backtester `strategy` column now carries
the generator's own stamp (label-only; five reference books re-pinned, all
62 other columns identical).

News corpus (after evaluating the owner's ML-NEWS scikit model against
DeepSeek: golden set 39/78 vs 78/78, out-of-sample agreement 64% against a
90% self-agreement ceiling; verdict: fallback only, not a replacement):
- NewsSentiment.xau_score + archive column xau_score keep DeepSeek's gold
  score for every headline (930 of the first 1,546 DeepSeek rows had none);
  an existing newsdata.db gains the column in place on first open
- headlines naming no tracked currency are scored and archived
  (currencies_json []) but never returned to the platform
- _apply_llm_sentiment sends every new headline in its cycle, 25 per call;
  a failed call ends the cycle's sending

Dashboard publisher never writes a database: main() calls
app_config.set_db_writes_for_process(False), honoured by signal_log_enabled()
and news_archive_enabled() (owner decision: the bot is the single writer).

Test suite: the hermetic app config now points both writers at a
suite-private temp dir — the RSS fixture tests were writing fixture
headlines into the real logs/newsdata.db — with a Module 32 test pinning it.
529 tests.
2026-09-17 19:49:29 -04:00

182 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent panel v2 — probabilities anchored to base rates — 2026-09-17
**Why.** The live review of 2026-09-04 → 09-17 (`signal_log_review_2026-09-17.md`, §5 and §7.1)
found the panel's verdict was tilted the wrong way: the macro persona's stance was the verdict,
its only input was headlines (a one-day-lagging echo of price, `news_sentiment_value_2026-09-04.md`),
opposed setups reached their first target more often than agreed ones (54% vs 42%), three of four
personas voted the same way on nearly every setup (technical 79% support, risk 79% oppose,
contrarian 65% oppose), and the stated confidence was uncalibrated. The chair had already been
shown anti-informative on 2026-09-03. Nothing versioned the prompts, so no change could be measured.
**What shipped (code, all advisory-only; the panel still never runs inside
`analyze_from_dataframes` and no gate reads it).**
| Piece | Where |
|---|---|
| Every persona returns `p_win` (0-100, the chance the trade closes profitably under its trailing exit) plus one `reason_for` and one `reason_against`; stance is derived symmetrically (≥ 60 support, ≤ 40 oppose) | `agent_panel.py` `_TAKE_CONTRACT`, `derive_stance`, `_validate_take` (v1 stance+confidence replies still accepted and mapped to a pseudo-probability) |
| Verdict = equal-weight mean of the personas' `p_win`, quorum 3 of 4, no LLM aggregator (`verdict_source: "mean"`, the new default; `macro` and `chair` kept for comparison) | `mean_verdict`, `run_panel`, `app_config.py` |
| A code-computed `context` block in every payload: stop / target in ATR and R, cost-to-risk, the class **base rate** from the validated books, prior move 4 h / 24 h in ATR, distance from the 24 h extreme and its age, `move_started_at`, largest bar of the last 3 in ATR, signal-bar body, event timing against the class's expected hold, penalty tags with the book's verdict on them | `_price_context`, `_base_rate`, `agent_panel_base_rates.json` (built by `scripts/build_panel_base_rates.py` from the MD5-verified books) |
| Macro persona: no news doctrine; judges whether news is NEW relative to `move_started_at`; receives the ATR-normalised candles (as does the contrarian) | `_MACRO_SYSTEM`, `_panel_jobs` |
| Technical persona: indicator agreement declared uninformative; judges higher-timeframe conflict and key levels. Risk: only setup-specific risks versus the class. Contrarian: the engine's own warnings are not evidence | `_TECH_SYSTEM`, `_RISK_SYSTEM`, `_CONTRARIAN_SYSTEM` |
| `PANEL_PROMPT_VERSION` logged with every verdict and folded into the payload hash; per-persona probabilities and reasons, the exact per-persona payloads and the context logged (`panel_prompt_version`, `panel_probability`, `panel_probs_json`, `panel_payloads_json`, `panel_context_json`) | `signal_log.py` additive migration, `update_panel`, `log_panel_update` |
| Live frames handed to the add-ons through `report._frames` in `analyze_pair` (never in the core) | `signal_aggregator.py` |
| Plain-language "AI estimate" line on Telegram, CLI and dashboard; legacy sources render as before | `telegram_bot._panel_estimate_lines`, `main.py`, `static/dashboard.js` |
| Scorecard: Brier, log-loss, calibration bands, discrimination, constant-voter rate, ρ(p, R), counterfactual books, split by prompt version | `analyze_signal_log.probability_scorecard` (printed by `--stored-candles`) |
| Offline replay harness: rebuilds each persona's payload for every resolved logged setup (exact payloads when logged, else from the stored blobs, interned news and the kept Oanda cache, events evaluated as of the signal bar), runs the current prompts, scores them against the outcome and against the logged v1 takes and the base-rate predictor | `scripts/panel_replay.py` (`--dry-run`, `--max-calls`) |
| Backtester export `strategy` tag fixed to the generator's own stamp (label-only; every gold ATR setup was exported as `GENERIC`) | `backtest/gpu_backtest.py`; books re-pinned, see below |
Tests: `test_all.py` Module 35 (15 tests) plus the extended Module 23 parity test; suite
`RESULTS: 521 passed, 0 failed, 521 total`.
## Reference books re-pinned (label-only)
The five books were regenerated with the committed code (stashed) and with the fix, on the same
cache. Dropping the `strategy` column, every book is byte-identical to its previous pin; trades, R
and PF are unchanged. New labels: M15 W2 174 `XAU_ATR` / 757 `GENERIC`; M5 W2 32 / 411 / 338
`XAU_CP`; M15 OOS 1 / 354; H1 W3 57 / 196; H4 W3 53 / 61.
| Book | old MD5 | new MD5 |
|---|---|---|
| M5 W2 | `29120a1ee32979431d062f210d41a9d1` | `49f8e602fc6ae769713c94ef7b257810` |
| M15 W2 | `d185f099482a07f29502cb5a371a9bbf` | `b4aa07e33e17e23774c486a6900d673b` |
| M15 OOS | `6f8f24b49597ced28f50a19e08c3472f` | `9fd624fb387c36052122bd1830cf9c32` |
| H1 W3 | `3b45d12bade823178b267c13bc698458` | `7b848c917dcb5c371ecbb9f8f7292278` |
| H4 W3 | `e75adae13c839b15736f69c0114e6aa5` | `186e677fbbd53cca735c94094c23ce05` |
`Backend: NumPy CPU fallback`, `GPU cache fallbacks: 0` on all ten runs.
## Base-rate table
`agent_panel_base_rates.json`: 45 classes (34 exact) from the relabelled M5 W2, M15 W2, H1 W3 and
H4 W3 books; key `timeframe|strategy|grade|direction`, fallbacks `tf|*|*|direction` and `tf|*|*|*`;
the exact class is used only with n ≥ 30. Examples: M15 all 931 trades win 60.5%, median hold 30
bars; M15 SELL 296 / 54.4%; M15 `XAU_ATR` A SELL 78 / 50.0%, median hold 6 bars; M5 `XAU_CP` B
BUY 321 / 51.7%; H1 all 253 / 64.4%.
## Pre-registered acceptance (from the approved plan)
On the resolved logged setups (37 today), a new prompt version ships only if: (1) no persona's
modal stance is above 65%; (2) Brier ≤ the logged v1 takes and ≤ the base-rate-only predictor;
(3) the skip-OPPOSE counterfactual does not remove a net-positive bucket. Outcome = advisory R > 0
at $0.65. Predictors compared on the same events: base-rate only (the class win rate for every
trade), the logged v1 takes (stance × confidence → pseudo-probability; 27 events carry them), and
the new prompts run live through DeepSeek on the rebuilt payloads (4 calls per event).
## Replay round 1 (draft prompts)
| predictor | n | Brier | log-loss | mean p | modal stance | constant % | R skip ≤ 40 |
|---|---|---|---|---|---|---|---|
| base-rate only | 37 | 0.263 | 0.719 | 54.8 | neutral | 81 | −7.15 (nothing skipped) |
| v1 logged, mean | 27 | 0.270 | 0.738 | 48.2 | neutral | 48 | −4.00 |
| v1 logged, macro | 27 | 0.299 | 0.855 | 56.5 | support | 44 | −2.82 |
| v1 logged, technical | 27 | 0.371 | 1.011 | 71.7 | support | 78 | −6.47 |
| v1 logged, risk | 27 | 0.343 | 0.947 | 25.7 | oppose | 78 | +0.03 |
| v2 draft, mean | 37 | 0.257 | 0.707 | 51.3 | neutral | 95 | −7.15 (nothing skipped) |
Like-for-like on the 27 events with v1 takes: Brier v2 0.257, v1 mean 0.270, v1 macro 0.299,
base rate 0.259, coin flip 0.250. Rank correlation of p with R: v2 −0.05, v1 mean +0.14, v1 macro
+0.36, base rate +0.16 (n = 27; the ±0.35 confidence band swallows all of them).
Reading: the draft is calibrated (it sits 3.5 points under the base rate with a spread of 1.7
points) and no longer directional, but it carries no rank information: winners averaged p = 50.9,
losers 51.7. The old macro verdict had some ordering (+0.36) and terrible calibration (82%
confidence on the three losing FOMC shorts); the new one has calibration and no ordering. Criterion
(2) passes, (3) is vacuous (no OPPOSE issued), (1) fails **in the opposite sense to the one it was
written for**: the personas cluster on neutral around the base rate instead of voting a constant
direction. That is hedging, not bias.
**A flaw in the draft prompts, found from the per-event reasons.** The five lowest probabilities
went to the Sep 10 and Sep 14 shorts that won (+1.31, +1.05, +0.70R), because the technical and
contrarian prompts told the personas to lower p when "the move has already run far" or "a big
candle just printed". That instruction encoded the live fortnight's pattern, which the two-year
books contradict (stretched entries are the system's best trades, 2026-08-04; the big-candle gate
was re-rejected in the review: those buckets are +32R and +60R). The final prompts remove that
instruction, state the book's finding in the context legend and in both personas, and limit
downward moves to higher-timeframe conflict, key levels, event timing inside the expected hold,
news newer than the move, and a stop far wider than the class norm.
## Replay round 2 (final prompts, the text that ships)
Same 37 events, same rebuilt payloads, the final prompt text (the technical and contrarian
personas told that a long prior move or a big candle is not a reason by itself; downward moves
limited to higher-timeframe conflict, key levels, event timing, news newer than the move, and a
stop far wider than the class norm).
| predictor | n | Brier | log-loss | mean p | modal stance | directional % | R skip ≤ 40 | R ≥ 60 only |
|---|---|---|---|---|---|---|---|---|
| base-rate only | 37 | 0.263 | 0.719 | 54.8 | neutral | 19 | −7.15 | −3.07 |
| v1 logged, mean | 27 | 0.270 | 0.738 | 48.2 | neutral | 52 | −4.00 | −1.66 |
| v1 logged, macro | 27 | 0.299 | 0.855 | 56.5 | support | 56 | −2.82 | −2.16 |
| **v2 final, mean** | 37 | **0.262** | 0.717 | 53.8 | neutral | 8 | −7.15 | −1.98 |
| v2 final, macro | 37 | 0.259 | 0.712 | 53.4 | neutral | 11 | | |
| v2 final, technical | 37 | 0.270 | 0.734 | 54.2 | neutral | 16 | | |
| v2 final, risk | 37 | 0.258 | 0.709 | 53.3 | neutral | 11 | | |
| v2 final, contrarian | 37 | 0.262 | 0.718 | 54.1 | neutral | 11 | | |
Like-for-like on the 27 events with v1 takes: Brier v2 final 0.260, v1 mean 0.270, v1 macro
0.299, base rate 0.259. Rank correlation of p with R: v2 final +0.13 (round 1 −0.05), v1 mean
+0.14, v1 macro +0.36. Calibration of the v2 mean: 34 events in the 40–60 band averaged p = 53
and won 47%; the 3 events at 60–61 won 1. The final personas sit 1.0 point under the base rate
(sd 1.8, no event moved 5 points or more); winners averaged p = 53.1, losers 54.3.
Acceptance: (2) passes — Brier below the logged v1 takes and equal to the base-rate predictor
(0.262 vs 0.263; 0.260 vs 0.259 on the common 27); (3) passes vacuously (no OPPOSE issued); (1)
fails as written (92% neutral) and passes as re-specified below (directional share 8%). The
correction removed the chase penalty: the winning Sep 10 / Sep 14 shorts moved from 45–47 to
49–51, and the reasons against now name what the books do not already price (a release due in 20
minutes, price sitting on a support level) instead of "the move already ran far".
Cost: 2 × 148 DeepSeek calls (both rounds), inside the shared daily cap of 500.
## Decision and what it means for the alert
The shipped verdict is a probability anchored to the validated base rate that moves only for
facts the books do not already price. On this sample it is calibrated and uninformative: the line
will usually read "about a 50% chance … (usual for this kind of setup: 55%)". That is the honest
state of the evidence — the 65-feature sizing model was a clean null too — and it is strictly
better than the previous line, which asserted 82% confidence in the wrong direction. The
information the line adds is the base rate itself, the biggest concrete worry (event inside the
hold, news newer than the move), and each persona's reason. It never gates; the user guide says
so.
Re-specified criterion (1) for future prompt versions: the share of *directional* stances
(support + oppose) on one side must not exceed 65% — neutral clustering around a calibrated base
rate is not the pathology the criterion targets.
## Persona set (owner decision, same day)
The owner's rule: a persona that does not give correct guidance is removed, to save confusion,
tokens and log space. Evidence per persona:
| persona | v1 (27 outcomes) | v2 final (37 outcomes) | verdict |
|---|---|---|---|
| macro | Brier 0.299, rank correlation with R **+0.36**, 88% / 25% win split on 2026-09-03 | Brier 0.259, ρ −0.06 | the only voice that ever showed ordering, and the only one holding facts the engine lacks (news) → **kept** |
| technical | support on 79%, Brier 0.371 (worst) | Brier 0.270 (worst), ρ −0.20 | re-reads the indicators the signal was built from → removed |
| risk | oppose on 79%, Brier 0.343 | Brier 0.258, ρ −0.02 | its inputs (stop in ATR, cost, event timing) are now computed in the context block → removed |
| contrarian | oppose on 65%, Brier 0.322 | Brier 0.262, ρ −0.05 | oppose by construction, no information → removed |
Shipped as `agent_panel.personas` (default `["macro"]`; `app_config` validates the names, `chair`
needs two or more, `macro` source needs the macro persona). One DeepSeek call per panel instead of
four (three quarters of the panel's token spend), `panel_payloads_json` about 4 KB instead of about
14 KB, `panel_takes_json` one entry, no persona votes line in the alert. The mean verdict with one
persona is that persona's probability (quorum = min(3, personas)). The full panel remains one
config line away for research replays (`scripts/panel_replay.py --personas macro,technical,risk,contrarian`).
## Next evaluation
- Every live v2 verdict logs its exact payloads, so `scripts/panel_replay.py` can re-score any
future prompt on identical evidence. At 100 resolved v2 outcomes (about five weeks): the
`--stored-candles` probability scorecard by prompt version. At 300: fit a probability-band →
observed-win-rate table on the first half by time, verify on the second, ship it as a numpy
lookup; only then revisit persona weights (owner rule).
- Do not iterate prompts on the current 37 events again: any further gain there is in-sample.
- Deployment: the live box needs the new code (both the bot and the dashboard daemon, which has
run `2ac7301` since Sep 10); no config change is required (`verdict_source` defaults to
`mean`); the additive migration runs on first open.
Commands: `venv/bin/python scripts/build_panel_base_rates.py --book 5min=<m5_w2.csv> --book
15min=<m15_w2.csv> --book 1h=<h1_w3.csv> --book 4h=<h4_w3.csv> --out agent_panel_base_rates.json`;
`venv/bin/python scripts/panel_replay.py --db logs/signals.db --dry-run`; `venv/bin/python
scripts/panel_replay.py --db logs/signals.db --max-calls 160 --out replay.csv`;
`venv/bin/python analyze_signal_log.py --db logs/signals.db --stored-candles`.