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.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Harwinder Sekhon
2026-09-17 19:49:29 -04:00
parent 552199857a
commit 1f4741b557
21 changed files with 3803 additions and 259 deletions
+17 -1
View File
@@ -471,7 +471,23 @@ function detailHTML(tf) {
${esc(r.news_summary || "")}</div>`);
}
if (r.panel_verdict) {
if (r.panel_verdict && r.panel_verdict_source === "mean") {
// v2 panel: a probability anchored to the setup class's base rate,
// one reason each way per persona. Advisory only; no verdict colour.
const base = (r.panel_context && r.panel_context.base_rate) || {};
const usual = base.win_rate != null ? ` (usual for this kind of setup: ${Math.round(base.win_rate)}%)` : "";
const takes = (r.panel_takes || []).map(t =>
`<tr><td class="ind-k">${esc(t.persona)}</td>
<td class="num text-end">${t.p_win != null ? Math.round(t.p_win) + "%" : "—"}</td>
<td class="small text-secondary">${esc(t.reason_for || "")}</td>
<td class="small text-secondary">${esc(t.reason_against || "")}</td></tr>`).join("");
out += sec("AI estimate (advisory, still being tested)", `<div class="text-secondary small mb-2">
About a <strong>${Math.round(r.panel_probability || 0)}%</strong> chance this trade pays off${esc(usual)}.<br>
${esc(r.panel_rationale || "")}</div>
${takes ? `<div class="detail-table"><table class="table table-sm d-table">
<thead><tr><th></th><th class="text-end">chance</th><th>for</th><th>against</th></tr></thead>
<tbody>${takes}</tbody></table></div>` : ""}`);
} else if (r.panel_verdict) {
const vcls = r.panel_verdict === "AGREE" ? "text-success"
: r.panel_verdict === "OPPOSE" ? "text-danger" : "text-secondary";
const takes = (r.panel_takes || []).map(t =>