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
+42 -9
View File
@@ -43,10 +43,19 @@ Contract
headline seen 50 times is one row with ``last_seen`` bumped.
* **stdlib only** (``sqlite3``), like ``signal_log``.
Known corpus limitation: a headline mentioning no tracked currency is
never constructed by the news engine at all (``news_engine`` drops it
before scoring), so DeepSeek never labels it either and it cannot appear
here.
Two columns exist for training only (2026-09-17):
* ``xau_score`` — DeepSeek's gold-specific score for the headline,
whatever currencies it names. ``score_by_currency_json["XAU"]``
carries it only for gold-keyword headlines; 930 of the first 1,546
DeepSeek rows had no gold label without this column. NULL for a
keyword-scored row and for rows written before the column existed
(an existing database is migrated in place, values are not
recoverable).
* ``currencies_json`` ``[]`` marks a headline naming no tracked currency.
The news engine used to drop those before scoring; now it scores and
archives them but never returns them to the platform. About 23% of a
day's feed; DeepSeek gave 10 of 24 a gold direction.
"""
from __future__ import annotations
@@ -85,7 +94,8 @@ CREATE TABLE IF NOT EXISTS headlines (
score_source TEXT NOT NULL DEFAULT 'keyword',
teacher_model TEXT,
teacher_prompt_sha1 TEXT,
origin TEXT NOT NULL DEFAULT 'live'
origin TEXT NOT NULL DEFAULT 'live',
xau_score REAL
);
CREATE INDEX IF NOT EXISTS idx_headlines_source ON headlines (score_source);
CREATE INDEX IF NOT EXISTS idx_headlines_first_seen ON headlines (first_seen);
@@ -155,8 +165,22 @@ class NewsArchive:
self._with_lock_retry(
lambda: self._conn.execute("PRAGMA journal_mode=WAL"))
self._with_lock_retry(lambda: self._conn.executescript(_SCHEMA))
self._with_lock_retry(self._migrate)
self._conn.commit()
#: Columns added after the first databases were created, in order.
#: CREATE TABLE IF NOT EXISTS leaves an existing table untouched, so
#: each one is added in place on open; existing rows read NULL.
_ADDED_COLUMNS = (("xau_score", "REAL"),)
def _migrate(self) -> None:
present = {row[1] for row in
self._conn.execute("PRAGMA table_info(headlines)")}
for name, decl in self._ADDED_COLUMNS:
if name not in present:
self._conn.execute(
f"ALTER TABLE headlines ADD COLUMN {name} {decl}")
@staticmethod
def _with_lock_retry(fn, attempts: int = 8, delay: float = 0.05):
"""Run ``fn`` again on 'database is locked' (concurrent openers
@@ -202,8 +226,8 @@ class NewsArchive:
" published_at, first_seen, last_seen, seen_count,"
" score, score_by_currency_json, currencies_json,"
" keywords_json, score_source, teacher_model,"
" teacher_prompt_sha1, origin"
") VALUES (?,?,?,?,?,?,?,?,?,1,?,?,?,?,?,?,?,?)"
" teacher_prompt_sha1, origin, xau_score"
") VALUES (?,?,?,?,?,?,?,?,?,1,?,?,?,?,?,?,?,?,?)"
" ON CONFLICT(hash) DO UPDATE SET"
# The backfill stamps historical timestamps, so clamp rather
# than assign: first_seen only ever moves earlier, last_seen
@@ -236,6 +260,13 @@ class NewsArchive:
" ELSE headlines.teacher_prompt_sha1 END,"
f" score_source = CASE WHEN {wins} THEN excluded.score_source"
" ELSE headlines.score_source END,"
# Same upgrade-only rule as score, and COALESCE on top: a
# keyword re-sighting carries no gold score and loses on
# rank, and even a same-rank write without one (a future
# backfill shape) must never blank a stored value.
f" xau_score = CASE WHEN {wins}"
" THEN COALESCE(excluded.xau_score, headlines.xau_score)"
" ELSE headlines.xau_score END,"
# Enrichment fills gaps but never blanks what is already there
# (the legacy backfill shape carries no description or link).
" description = COALESCE(excluded.description, headlines.description),"
@@ -246,9 +277,9 @@ class NewsArchive:
# The rank is bound once per CASE arm. Positional binding means a
# miscount silently shifts every parameter, so assert it instead of
# trusting the arithmetic: 17 INSERT values + one rank per arm.
# trusting the arithmetic: 18 INSERT values + one rank per arm.
n_rank = sql.count("? >= ")
n_values = 17
n_values = 18
assert sql.count("?") == n_values + n_rank, (
f"placeholder count drifted: {sql.count('?')} "
f"!= {n_values} + {n_rank}")
@@ -264,6 +295,7 @@ class NewsArchive:
row["currencies_json"], row["keywords_json"],
row["score_source"], row["teacher_model"],
row["teacher_prompt_sha1"], row.get("origin", "live"),
row.get("xau_score"),
) + (rank,) * n_rank)
def _write() -> None:
@@ -321,6 +353,7 @@ def build_row(headline: Any, *, teacher_model: Optional[str] = None,
"teacher_prompt_sha1": (
teacher_prompt_sha1 if source == "llm" else None),
"origin": origin,
"xau_score": _finite_score(getattr(headline, "xau_score", None)),
}