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.
This commit is contained in:
Harwinder Sekhon
2026-09-17 19:49:29 -04:00
parent 183df7832b
commit ad14ea4fed
21 changed files with 3803 additions and 259 deletions
+33 -1
View File
@@ -324,6 +324,36 @@ _DISCLAIMER = (
)
def _panel_estimate_lines(report: SignalReport) -> list[str]:
"""v2 panel (verdict_source="mean"): a plain-language probability, the
class base rate it is anchored to, the biggest worry, and each
persona's own estimate. Advisory only — no colour, no verdict word,
never a recommendation (2026-09-17 review: the old AGREE/OPPOSE line
opposed target-reaching trades as often as it agreed)."""
base = (report.panel_context or {}).get("base_rate") or {}
usual = base.get("win_rate")
line = (f"🤖 AI estimate: about a *{report.panel_probability:.0f}%* "
f"chance this trade pays off")
if usual is not None:
line += f" (usual for this kind of setup: {usual:.0f}%)"
line += "."
if report.panel_rationale:
line += f" {_escape_md(report.panel_rationale)}"
worst = min((t for t in report.panel_takes
if t.get("p_win") is not None and t.get("reason_against")),
key=lambda t: t["p_win"], default=None)
if worst is not None:
line += f" Biggest worry: {_escape_md(worst['reason_against'])}"
line += " Still being tested."
out = [line]
if len(report.panel_takes) > 1: # one persona: the headline says it all
votes = " · ".join(
f"{t['persona']} {t['p_win']:.0f}%" if t.get("p_win") is not None
else f"{t['persona']} -" for t in report.panel_takes)
out.append(f" _{_escape_md(votes)}_")
return out
def format_signal_message(report: SignalReport) -> str:
sig = report.final_signal
emoji = _SIGNAL_EMOJI.get(sig, "⬜")
@@ -423,7 +453,9 @@ def format_signal_message(report: SignalReport) -> str:
extra.append(_escape_md(report.watch_line))
if report.news_summary:
extra.append(f"📰 {_escape_md(report.news_summary)}")
if report.panel_verdict:
if report.panel_verdict and report.panel_verdict_source == "mean":
extra.extend(_panel_estimate_lines(report))
elif report.panel_verdict:
mark = {"AGREE": "✅", "OPPOSE": "⛔", "NEUTRAL": "➖"}.get(
report.panel_verdict, "")
panel_label = ("AI macro view"