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:
+42
-9
@@ -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)),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user