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
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Build agent_panel_base_rates.json from the reference backtest books.
The agent panel (agent_panel.py, prompt version v2) anchors every
persona's probability to how trades of the same class fared in the
validated books: (timeframe, strategy, grade, direction) with fallbacks
to (timeframe, direction) and (timeframe). This script turns the exported
63-column book CSVs into that table. Research-side (pandas only); the
live module reads the JSON and never imports this.
Usage (the section-9 harness books, regenerated from the kept cache):
venv/bin/python scripts/build_panel_base_rates.py \\
--book 5min=/path/m5_w2.csv --book 15min=/path/m15_w2.csv \\
--book 1h=/path/h1_w3.csv --book 4h=/path/h4_w3.csv \\
--out agent_panel_base_rates.json
Statistics per class: n, win_rate (percent of trades with r_multiple >
0), mean_r, tp1_reach_rate (percent whose best excursion reached the
planned first target: mfe_r >= rr_planned), median_bars_held. Every
class with n >= 1 is written; agent_panel._base_rate applies the
minimum-n rule (30) and the fallback order at read time.
Re-run whenever a reference book is re-pinned (CLAUDE.md section 9); the
JSON records each book's file name, MD5 and trade count so the table's
provenance is auditable.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from datetime import datetime, timezone
from typing import Dict, List
import pandas as pd
STATS_COLS = ("r_multiple", "mfe_r", "rr_planned", "bars_held",
"strategy", "quality", "direction")
def _md5(path: str) -> str:
h = hashlib.md5()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def _stats(df: pd.DataFrame, book: str) -> Dict[str, object]:
r = df["r_multiple"].astype(float)
reached = (df["mfe_r"].astype(float) >= df["rr_planned"].astype(float))
return {
"n": int(len(df)),
"win_rate": round(float((r > 0).mean() * 100.0), 2),
"mean_r": round(float(r.mean()), 4),
"tp1_reach_rate": round(float(reached.mean() * 100.0), 2),
"median_bars_held": int(round(float(df["bars_held"].median()))),
"book": book,
}
def build(books: Dict[str, str]) -> Dict[str, object]:
classes: Dict[str, Dict[str, object]] = {}
meta: Dict[str, Dict[str, object]] = {}
for tf, path in books.items():
df = pd.read_csv(path)
missing = [c for c in STATS_COLS if c not in df.columns]
if missing:
raise SystemExit(f"{path}: missing columns {missing}")
book = path.rsplit("/", 1)[-1]
meta[tf] = {"file": book, "md5": _md5(path), "trades": int(len(df)),
"first_entry": str(df["entry_time"].min()),
"last_entry": str(df["entry_time"].max())}
if df.empty:
continue
classes[f"{tf}|*|*|*"] = _stats(df, book)
for direction, g in df.groupby("direction"):
classes[f"{tf}|*|*|{direction}"] = _stats(g, book)
for (strategy, grade, direction), g in df.groupby(
["strategy", "quality", "direction"]):
classes[f"{tf}|{strategy}|{grade}|{direction}"] = _stats(g, book)
return {
"schema": "agent-panel-base-rates-v1",
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"key": "timeframe|strategy|grade|direction ('*' = any)",
"min_n_for_exact_class": 30,
"books": meta,
"classes": dict(sorted(classes.items())),
}
def main(argv: List[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--book", action="append", default=[], metavar="TF=CSV",
help="timeframe (5min/15min/1h/4h) = exported book CSV")
ap.add_argument("--out", default="agent_panel_base_rates.json")
args = ap.parse_args(argv)
books: Dict[str, str] = {}
for item in args.book:
if "=" not in item:
ap.error(f"--book expects TF=CSV, got {item!r}")
tf, path = item.split("=", 1)
books[tf.strip()] = path.strip()
if not books:
ap.error("at least one --book is required")
table = build(books)
with open(args.out, "w", encoding="utf-8") as fh:
json.dump(table, fh, indent=1, sort_keys=False)
fh.write("\n")
n_exact = sum(1 for k in table["classes"] if "|*|" not in k)
print(f"wrote {args.out}: {len(table['classes'])} classes "
f"({n_exact} exact) from {len(books)} book(s)")
for tf, m in table["books"].items():
print(f" {tf:6s} {m['file']} md5={m['md5']} trades={m['trades']}")
return 0
if __name__ == "__main__":
sys.exit(main())