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
+45 -3
View File
@@ -152,7 +152,12 @@ CREATE TABLE IF NOT EXISTS signals (
panel_payload_hash TEXT,
advisory_outcome_status TEXT,
advisory_r_multiple REAL,
advisory_outcome_json TEXT
advisory_outcome_json TEXT,
panel_prompt_version TEXT,
panel_probability REAL,
panel_probs_json TEXT,
panel_payloads_json TEXT,
panel_context_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_signals_sym_tf_ts
ON signals (symbol, timeframe, logged_at);
@@ -225,6 +230,16 @@ _ADDITIVE_SIGNAL_COLS = (
("watch_level", "TEXT"),
("watch_kinds", "TEXT"),
("watch_line", "TEXT"),
# 2026-09-17 agent-panel v2 (see agent_panel.py): prompt era, the mean
# persona probability, per-persona probabilities + reasons, the exact
# per-persona payloads and the shared context — written post-hoc by
# update_panel so scripts/panel_replay.py can re-score any prompt
# version on the same evidence.
("panel_prompt_version", "TEXT"),
("panel_probability", "REAL"),
("panel_probs_json", "TEXT"),
("panel_payloads_json", "TEXT"),
("panel_context_json", "TEXT"),
)
# Read-side compatibility view `signals_with_news`: legacy rows keep
@@ -1025,6 +1040,11 @@ class SignalLogger:
cache_status: Optional[str] = None,
payload_hash: Optional[str] = None,
verdict_source: Optional[str] = None,
prompt_version: Optional[str] = None,
probability: Optional[float] = None,
probs_json: Optional[str] = None,
payloads_json: Optional[str] = None,
context_json: Optional[str] = None,
) -> None:
"""Fill panel provenance; seeds an outcome only for rows that
log() did not already seed (rows written by an older process)."""
@@ -1051,11 +1071,18 @@ class SignalLogger:
" panel_cache_status=COALESCE(?, panel_cache_status),"
" panel_payload_hash=COALESCE(?, panel_payload_hash),"
" panel_verdict_source=COALESCE(?, panel_verdict_source),"
" panel_prompt_version=COALESCE(?, panel_prompt_version),"
" panel_probability=COALESCE(?, panel_probability),"
" panel_probs_json=COALESCE(?, panel_probs_json),"
" panel_payloads_json=COALESCE(?, panel_payloads_json),"
" panel_context_json=COALESCE(?, panel_context_json),"
" advisory_outcome_status=COALESCE(advisory_outcome_status, ?),"
" advisory_outcome_json=COALESCE(advisory_outcome_json, ?)"
" WHERE id=?",
(verdict, confidence, rationale, takes_json,
created_at, cache_status, payload_hash, verdict_source,
prompt_version, probability, probs_json, payloads_json,
context_json,
advisory_status, advisory_json, rowid))
self._conn.commit()
@@ -1592,15 +1619,30 @@ def log_panel_update(report: Any) -> None:
lg = _loggers.get(path)
if lg is None:
return
takes = getattr(report, "panel_takes", None) or []
probs = [{"persona": t.get("persona"), "p_win": _finite(t.get("p_win")),
"stance": t.get("stance"),
"confidence": _finite(t.get("confidence")),
"reason_for": t.get("reason_for") or "",
"reason_against": t.get("reason_against") or ""}
for t in takes if isinstance(t, dict)]
payloads = getattr(report, "panel_payloads", None) or {}
context = getattr(report, "panel_context", None) or {}
probability = _finite(getattr(report, "panel_probability", None))
lg.update_panel(
int(rowid), verdict,
_finite(getattr(report, "panel_confidence", None)),
getattr(report, "panel_rationale", "") or "",
_dumps(getattr(report, "panel_takes", None) or []),
_dumps(takes),
created_at=getattr(report, "panel_created_at", "") or None,
cache_status=getattr(report, "panel_cache_status", "") or None,
payload_hash=getattr(report, "panel_payload_hash", "") or None,
verdict_source=getattr(report, "panel_verdict_source", "") or None)
verdict_source=getattr(report, "panel_verdict_source", "") or None,
prompt_version=getattr(report, "panel_prompt_version", "") or None,
probability=probability if probability else None,
probs_json=_dumps(probs) if probs else None,
payloads_json=_dumps(payloads) if payloads else None,
context_json=_dumps(context) if context else None)
except Exception:
logger.warning("signal_log: failed to persist panel verdict",
exc_info=True)