Files
ForexBot2/scripts/backfill_signal_outcomes.py
Harwinder Sekhon 985ec83ef3 chore: remove dead functions and write-only state (no behaviour change)
Zero callers anywhere (tests included), verified by grep/AST:
- dashboard_publisher.DashboardPublisher.publish_all (run_cycle inlines both publishes)
- signal_log.SignalLogger.advance_with_bars (the backfill script uses
  backfill_seeds / resolve_from_store / reseed_unresolvable) and the never-read
  SignalLogger.path property plus its write-only self._path
- llm_client.reset_llm_client (tests reset the singleton by assignment)
- market_outlook._last_run_ts, residue of the old "now - last < 3600" dedup;
  _last_run_hour (the UTC hour bucket) is the live key
- news_engine FundamentalContext.event_impact_on_pairs: written once, read by
  nothing; its only feeder NewsEngine._get_affected_pairs and that helper's
  unit test go with it
- position_sizing.LIVE_DEPTH_BARS (referenced only by its own docstring, which
  now states the depths inline), the unused Iterable import and the unused
  atr_ok parameter of _fill_geometry
- analyze_signal_log: unused `field` import and the unused `rows` parameter of
  persona_scorecard / news_outcome_buckets (both read the DB directly)

The two backfill/label scripts' --db help text now names the config.json
setting it actually falls back to.

Suite: 463 passed, 0 failed (one test removed with its subject).

Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
2026-09-06 16:48:24 -04:00

92 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""
backfill_signal_outcomes.py — seed and advance advisory outcomes for every
logged trade setup (not only panel rows), one per advisory event.
Pass A (offline, no network): assign setup_key / advisory_event_id to all
legacy setup rows, mark same-setup re-logs within 60 min as DUPLICATE, and
seed a PENDING outcome on each canonical row that has none. Existing
(panel-seeded) outcomes are never overwritten.
Pass B: advance PENDING rows from the closed-candle streams already stored
in the DB (union across every snapshot of the timeframe — no provider).
Pass C (--reseed-unresolvable): bounded re-seed of continuity-broken rows,
then Pass B again.
Safe beside the live writers: short per-row transactions with a busy
timeout. Read-only preview with --dry-run.
python scripts/backfill_signal_outcomes.py --dry-run
python scripts/backfill_signal_outcomes.py
python scripts/backfill_signal_outcomes.py --reseed-unresolvable
"""
import argparse
import os
import sqlite3
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import app_config # noqa: E402
from signal_log import SignalLogger # noqa: E402
def _status_counts(con: sqlite3.Connection) -> dict:
return dict(con.execute(
"SELECT COALESCE(advisory_outcome_status, 'NULL'), COUNT(*)"
" FROM signals WHERE setup_json IS NOT NULL AND setup_json != 'null'"
" GROUP BY 1").fetchall())
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
ap.add_argument("--db", default=None, help="defaults to the signal_log.path setting in config.json")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--reseed-unresolvable", action="store_true")
ap.add_argument("--symbol", default="XAU/USD")
args = ap.parse_args(argv)
path = args.db or app_config.signal_log_path()
if not os.path.exists(path):
print(f"no database at {path}")
return 1
if args.dry_run:
con = sqlite3.connect(f"file:{os.path.abspath(path)}?mode=ro", uri=True)
print("setup rows by outcome status:", _status_counts(con))
have = {r[1] for r in con.execute("PRAGMA table_info(signals)")}
if "setup_key" in have:
n_keyless = con.execute(
"SELECT COUNT(*) FROM signals WHERE setup_json IS NOT NULL"
" AND setup_json != 'null' AND setup_key IS NULL").fetchone()[0]
print(f"setup rows without setup_key: {n_keyless}")
else:
print("setup_key column not present yet (schema migrates on"
" the first non-dry run)")
con.close()
return 0
lg = SignalLogger(path)
try:
print("before:", _status_counts(lg._conn))
print("pass A (seed/dedup):", lg.backfill_seeds())
total = 0
for _ in range(50):
n = lg.resolve_from_store(args.symbol)
total += n
if n == 0:
break
print(f"pass B (advance from stored bars): {total} rows changed")
if args.reseed_unresolvable:
reseeded = lg.reseed_unresolvable()
print(f"pass C (re-seed): {len(reseeded)} rows re-seeded")
for _ in range(50):
if lg.resolve_from_store(args.symbol) == 0:
break
print("after:", _status_counts(lg._conn))
finally:
lg.close()
return 0
if __name__ == "__main__":
sys.exit(main())