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). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWV2yLt3YXHHGVYHSQU2EE
This commit is contained in:
@@ -13,7 +13,7 @@ Design philosophy:
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -32,6 +32,13 @@ class IndicatorSignal:
|
||||
confidence: float # 0-100
|
||||
description: str
|
||||
weight: float = 1.0
|
||||
# RECORD-KEEPING ONLY (stamped by _compute_composite; nothing reads
|
||||
# them back). effective_weight = weight x regime multiplier actually
|
||||
# used, weighted_score = its contribution to the composite. Both stay
|
||||
# None for indicators the composite skipped (weight 0, `_`-prefixed
|
||||
# private series) and for analyses that never reached _compute_composite.
|
||||
effective_weight: Optional[float] = None
|
||||
weighted_score: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -50,6 +57,46 @@ class TechnicalAnalysis:
|
||||
pattern_signals: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# NOTE: ROC is directionally redundant with MACD/Keltner/EMA
|
||||
# (0.8-0.94 correlation), but zeroing its weight was backtested
|
||||
# 2026-07 and LOST 13R — the redundant vote effectively raises
|
||||
# the composite bar and the thresholds are calibrated with it.
|
||||
# Don't remove without retuning score thresholds.
|
||||
_TRENDING_WEIGHTS = {
|
||||
"ema_trend": 2.0, "rsi": 1.0, "macd": 2.0,
|
||||
"bollinger": 0.5, "keltner": 1.5, "roc": 1.5,
|
||||
"candlestick": 0.8, "divergence": 1.0, "adx": 0.8,
|
||||
}
|
||||
_DEFAULT_REGIME_WEIGHTS = {
|
||||
"trending_up": _TRENDING_WEIGHTS,
|
||||
"trending_down": _TRENDING_WEIGHTS,
|
||||
"trending": _TRENDING_WEIGHTS,
|
||||
"ranging": {
|
||||
"ema_trend": 0.3, "rsi": 1.5, "macd": 1.5,
|
||||
"bollinger": 1.5, "keltner": 2.0, "roc": 0.5,
|
||||
"candlestick": 1.0, "divergence": 2.5, "adx": 0.5,
|
||||
},
|
||||
"transitioning": {
|
||||
"ema_trend": 1.5, "rsi": 1.0, "macd": 2.5,
|
||||
"bollinger": 1.0, "keltner": 2.5, "roc": 2.0,
|
||||
"candlestick": 1.0, "divergence": 2.0, "adx": 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def regime_weight_table(config: TechnicalConfig) -> Dict[str, Dict[str, float]]:
|
||||
"""The regime -> indicator weight multipliers _compute_composite runs
|
||||
with (``regime_weights_override`` wins, else the gold-optimized
|
||||
defaults above).
|
||||
|
||||
Pure and side-effect free: ONE source of truth shared by the composite
|
||||
and by signal_aggregator.engine_config_fingerprint. Returns a copy, so
|
||||
a caller can never mutate the shipped table."""
|
||||
override = getattr(config, "regime_weights_override", None)
|
||||
table = override if override else _DEFAULT_REGIME_WEIGHTS
|
||||
return {regime: dict(weights) for regime, weights in table.items()}
|
||||
|
||||
|
||||
class TechnicalEngine:
|
||||
"""
|
||||
Core technical analysis engine.
|
||||
@@ -1081,35 +1128,9 @@ class TechnicalEngine:
|
||||
total_weight = 0.0
|
||||
total_confidence = 0.0
|
||||
|
||||
# Use override weights if provided, else gold-optimized defaults
|
||||
if self.config.regime_weights_override:
|
||||
regime_weights = self.config.regime_weights_override
|
||||
else:
|
||||
# NOTE: ROC is directionally redundant with MACD/Keltner/EMA
|
||||
# (0.8-0.94 correlation), but zeroing its weight was backtested
|
||||
# 2026-07 and LOST 13R — the redundant vote effectively raises
|
||||
# the composite bar and the thresholds are calibrated with it.
|
||||
# Don't remove without retuning score thresholds.
|
||||
_trending = {
|
||||
"ema_trend": 2.0, "rsi": 1.0, "macd": 2.0,
|
||||
"bollinger": 0.5, "keltner": 1.5, "roc": 1.5,
|
||||
"candlestick": 0.8, "divergence": 1.0, "adx": 0.8,
|
||||
}
|
||||
regime_weights = {
|
||||
"trending_up": _trending,
|
||||
"trending_down": _trending,
|
||||
"trending": _trending,
|
||||
"ranging": {
|
||||
"ema_trend": 0.3, "rsi": 1.5, "macd": 1.5,
|
||||
"bollinger": 1.5, "keltner": 2.0, "roc": 0.5,
|
||||
"candlestick": 1.0, "divergence": 2.5, "adx": 0.5,
|
||||
},
|
||||
"transitioning": {
|
||||
"ema_trend": 1.5, "rsi": 1.0, "macd": 2.5,
|
||||
"bollinger": 1.0, "keltner": 2.5, "roc": 2.0,
|
||||
"candlestick": 1.0, "divergence": 2.0, "adx": 1.0,
|
||||
},
|
||||
}
|
||||
# Override weights if provided, else gold-optimized defaults
|
||||
# (regime_weight_table is the single source of truth).
|
||||
regime_weights = regime_weight_table(self.config)
|
||||
|
||||
active_regime_weights = regime_weights.get(result.market_regime, {})
|
||||
|
||||
@@ -1123,6 +1144,9 @@ class TechnicalEngine:
|
||||
|
||||
signal_score = ind.signal.value
|
||||
weighted_score = signal_score * weight * (ind.confidence / 100.0)
|
||||
# Record-keeping stamp (no maths depends on these two lines).
|
||||
ind.effective_weight = float(weight)
|
||||
ind.weighted_score = float(weighted_score)
|
||||
|
||||
total_score += weighted_score
|
||||
total_weight += weight
|
||||
|
||||
Reference in New Issue
Block a user