signal_log: weight-research data points, scan_outcomes forward labels, watch provenance

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).

Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
This commit is contained in:
Harwinder Sekhon
2026-09-05 22:22:59 -04:00
parent e79b533a60
commit fc55da17db
8 changed files with 1117 additions and 39 deletions
+30 -7
View File
@@ -9,7 +9,7 @@ import hashlib
import json
import logging
import os
from dataclasses import dataclass, field
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional
from zoneinfo import ZoneInfo
@@ -21,7 +21,7 @@ _NY_TZ = ZoneInfo("America/New_York")
from chart_patterns import detect_chart_pattern
from config import PlatformConfig, SignalStrength, Timeframe
from providers import BaseDataProvider, make_data_provider
from engine import TechnicalEngine, TechnicalAnalysis
from engine import TechnicalEngine, TechnicalAnalysis, regime_weight_table
from news_engine import NewsEngine, FundamentalContext
import signal_log
@@ -322,11 +322,13 @@ class SignalAggregator:
logger.debug("signal log: news context unavailable",
exc_info=True)
extra = {"scan_kind": "blocked" if blocked else "scan"}
try:
extra["config_fingerprint"] = self.gate_config_fingerprint()
except Exception:
logger.debug("signal log: config fingerprint unavailable",
exc_info=True)
for key, fingerprint in (
("config_fingerprint", self.gate_config_fingerprint),
("engine_fingerprint", self.engine_config_fingerprint)):
try:
extra[key] = fingerprint()
except Exception:
logger.debug("signal log: %s unavailable", key, exc_info=True)
signal_log.log_signal(
report,
context=context,
@@ -365,6 +367,27 @@ class SignalAggregator:
json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
).hexdigest()[:12]
def engine_config_fingerprint(self) -> str:
"""Short hash of the SCORING configuration this process runs with.
gate_config_fingerprint covers the post-grade gates; this covers
what produced the composite in the first place — indicator
parameters, signal thresholds/geometry, the regime weight
multipliers actually in force (override included) and the
timeframe set. Logged on every row so indicator-weight research
can tell which rows share a scoring configuration."""
payload = {
"technical": asdict(self.config.technical),
"signals": asdict(self.config.signals),
"regime_weights": regime_weight_table(self.config.technical),
"primary": self.config.primary_timeframe.value,
"confirm": [tf.value for tf in self.config.confirmation_timeframes],
"display_name": self.config.display_name,
}
return hashlib.sha1(
json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
).hexdigest()[:12]
def _apply_regime_filter_live(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> None: