e08133e2b5
New additive columns on every logged scan: bar_ts (primary closed-bar label, indexed with timeframe), engine_fingerprint (hash of the scoring config incl. the regime weight table in force - SignalAggregator.engine_config_fingerprint, twin of the gate fingerprint), code_version (short git SHA), penalty_tags_json / penalty_total / momentum_fading / exhaustion_count, and post-hoc watch_level / watch_kinds / watch_line written by violent_watch.attach_watch via signal_log.update_watch. Each indicators_json entry now also carries eff_weight (base weight x regime multiplier, the weight the composite actually used) and wscore. engine: IndicatorSignal.effective_weight / weighted_score are stamped inside _compute_composite (record-keeping only); regime_weight_table(config) is the single source of truth for the multiplier table (override honoured). Composite output byte-identical (120-report hash proof). New table scan_outcomes: model-free forward moves per scan (fwd_1h/4h/24h in $ and ATRs, mfe/mae over 24h, burst_1h, bars_used, complete) measured on the stored candle stream with the advisory model's continuity rule; labelled opportunistically at the end of log() (time-boxed, throttled, idempotent) - no daemon; scripts/label_scan_outcomes.py drives the backlog (--dry-run is read-only). analyze_signal_log --scan-outcomes: forward move + hit rate by TF x signal/grade with a NEUTRAL baseline and burst rate by watch level. Readers dedup by the first row per (timeframe, bar_ts). Module 31 (10 tests). Suite: 463 passed. Backtester untouched (test-locked). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
116 lines
4.5 KiB
Python
Executable File
116 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
label_scan_outcomes.py — batch driver for the scan_outcomes table.
|
|
|
|
Labels model-free forward returns (1/4/24 h, $ and ATR-normalized, plus
|
|
MFE/MAE, a 3xATR burst flag and a completeness flag) for every logged scan
|
|
whose 24 h window has closed, from the closed-candle streams already
|
|
stored in the DB. No provider, no network.
|
|
|
|
The live loggers do the same work opportunistically at the end of every
|
|
scan (time-boxed to ~2 s); this script is the catch-up pass for a backlog
|
|
— a freshly imported database, or rows whose bars only arrived later.
|
|
|
|
Idempotent: a row is rewritten only when its numbers change, so a settled
|
|
database costs one read pass. Safe beside the live writers (short per-row
|
|
transactions with a busy timeout). Read-only preview with --dry-run.
|
|
|
|
python scripts/label_scan_outcomes.py --dry-run
|
|
python scripts/label_scan_outcomes.py
|
|
python scripts/label_scan_outcomes.py --db logs/signals.db --budget 5000
|
|
"""
|
|
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 SCAN_LABEL_MIN_AGE_H, SignalLogger # noqa: E402
|
|
|
|
CHUNK = 2000
|
|
|
|
|
|
def _counts(con: sqlite3.Connection, symbol: str,
|
|
labelled: bool = True) -> dict:
|
|
"""Candidate / labelled / complete / pending counts (read-only).
|
|
|
|
The age filter uses logged_at (always an ISO string) as a proxy for
|
|
the bar close — the labeller itself parses the real bar label."""
|
|
scans = ("SELECT COUNT(*) FROM signals s WHERE s.symbol = ?"
|
|
" AND s.indicators_json IS NOT NULL AND s.price > 0")
|
|
due = (scans + " AND s.logged_at <"
|
|
f" datetime('now', '-{int(SCAN_LABEL_MIN_AGE_H)} hours')")
|
|
out = {
|
|
"scan rows with indicators": con.execute(scans, (symbol,)).fetchone()[0],
|
|
"of those, window elapsed": con.execute(due, (symbol,)).fetchone()[0],
|
|
}
|
|
if labelled:
|
|
out["labelled"] = con.execute(
|
|
"SELECT COUNT(*) FROM scan_outcomes").fetchone()[0]
|
|
out["complete"] = con.execute(
|
|
"SELECT COUNT(*) FROM scan_outcomes WHERE complete = 1"
|
|
).fetchone()[0]
|
|
out["pending (labelled, incomplete)"] = con.execute(
|
|
"SELECT COUNT(*) FROM scan_outcomes WHERE complete = 0"
|
|
).fetchone()[0]
|
|
return out
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
|
ap.add_argument("--db", default=None, help="defaults to signal_log.path")
|
|
ap.add_argument("--dry-run", action="store_true",
|
|
help="read-only candidate counts, writes nothing")
|
|
ap.add_argument("--budget", type=int, default=100000,
|
|
help="maximum rows to write in this run")
|
|
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)
|
|
try:
|
|
have = {r[0] for r in con.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'")}
|
|
if "scan_outcomes" not in have:
|
|
print("scan_outcomes table not present yet (created on the"
|
|
" first non-dry run / next logged scan)")
|
|
for key, value in _counts(con, args.symbol,
|
|
labelled="scan_outcomes" in have).items():
|
|
print(f" {key}: {value}")
|
|
finally:
|
|
con.close()
|
|
return 0
|
|
|
|
lg = SignalLogger(path)
|
|
try:
|
|
print("before:", _counts(lg._conn, args.symbol))
|
|
written = 0
|
|
previous_complete = -1
|
|
while written < args.budget:
|
|
n = lg.label_scan_outcomes(
|
|
budget=min(CHUNK, args.budget - written), time_budget=120.0)
|
|
written += n
|
|
complete = lg._conn.execute(
|
|
"SELECT COUNT(*) FROM scan_outcomes WHERE complete = 1"
|
|
).fetchone()[0]
|
|
# Stop on a pass that wrote nothing, or one that could not
|
|
# complete a single further row (only re-labelling stragglers).
|
|
if n == 0 or complete == previous_complete:
|
|
break
|
|
previous_complete = complete
|
|
print(f"labelled (rows written): {written}")
|
|
print("after:", _counts(lg._conn, args.symbol))
|
|
finally:
|
|
lg.close()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|