audit 2026-09-13: eleven delivery/measurement fixes; books byte-identical
Signal path
- A shipped setup always names a direction: grade C needs confidence
>= 45 (gate 6c grants B at >= 45) while BUY/SELL needs >= 50, so C/B
reports reached the alert gate as NEUTRAL with a full setup. The
backtester traded them (the replay never reads final_signal) while
Telegram rejected NEUTRAL first — 5/91 trades, +3.7R of +22.2R, in
the H1 12-month book. _attach_core_setup stamps the setup's direction
and repairs the pre-gate action text; no-setup reports stay NEUTRAL.
- analyze_pair fails the scan when ANY required frame is missing
("DATA UNAVAILABLE:", scan_kind='failed'); a lost confirmation frame
used to run with an unearned -15 MTF penalty.
Outcomes and labels
- advisory_outcome.resolve_locked_floor: a gap the database cannot fill
resolves at the locked trailing stop (GAP_LOCKED, result.floor) when
the stop had crossed the entry; the rebuild fires on both gap reasons
(rows 915/920: +1.14R / +0.84R floors; resolved sum -2.46R -> -0.48R).
- label_scan_outcomes: SCAN_LABEL_MAX_AGE_H = 72 h, stale rows never
anchor the shared bar stream, labeled_at bumped on re-examination,
_stream_since warns at its row cap (the Labor-Day pile would have
blinded scan_outcomes around 2026-09-14).
Providers and scheduling
- The daily settlement pause (Mon-Thu 17:00 New York) is a scheduled
closure for _has_latest_closed_bar (whole-span label walk-back) and
_is_data_fresh (closed seconds are not age); bot and dashboard skip
scans whose just-closed label sits in the pause (61 false stale rows
in three nights).
- fetch_range retries a failed page in place (three failures on one
window raise) and logs an ERROR for interior gaps of >3 unscheduled
bars.
- pd.Timestamp.utcnow() replaced in providers/base.py and engine.py;
requirements pin pandas<4, numpy<3.
- telegram_bot: boundary loops share one scan per bar with the breach
handler (_remember_scan keys on the analysed bar; breach defers 20 s
after an M15 boundary); the rotating file handler attaches in main()
only (the test suite used to write into logs/telegram.log).
- dashboard_publisher: catch_up_timeframes re-scans boundaries missed
while a cycle overran or the process was down.
Backtester and engine guards
- _replay_span returns the per-span GPU cache-miss count; run_backtest
sums, logs it (must be 0) and writes cache_fallbacks to the manifest.
- regime_weight_table raises on a partial regime_weights_override.
Verification: 519 tests; M5/M15 2-yr, M15 OOS, H1/H4 3-yr books
byte-identical before and after; live signals.db untouched (checked on
a copy). CLAUDE.md carries the dated entry and the blocked-hours union
semantics that every book was validated under.
Claude-Session: https://claude.ai/code/session_0139xz1J6YccndA2be6yFnEo
This commit is contained in:
@@ -12,6 +12,7 @@ Design philosophy:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
@@ -84,15 +85,65 @@ _DEFAULT_REGIME_WEIGHTS = {
|
||||
}
|
||||
|
||||
|
||||
def _validate_regime_override(override: Dict[str, Dict[str, float]]) -> None:
|
||||
"""Reject a PARTIAL ``regime_weights_override`` (raises ValueError).
|
||||
|
||||
An override REPLACES the whole table, it does not patch it: whatever
|
||||
it omits is multiplied by 1.0 in _compute_composite, not by the
|
||||
shipped default. A half-filled research table (``--set
|
||||
technical.regime_weights_override=<JSON>``) would therefore silently
|
||||
run 1.0 where the default said 0.3 or 2.5, with no warning — so every
|
||||
regime and all nine scored indicators must be present, with a finite
|
||||
multiplier >= 0. The shipped M15 override is derived from the default
|
||||
table (pair_signals/xauusd._m15_regime_weights) and passes."""
|
||||
problems: List[str] = []
|
||||
want_regimes = set(_DEFAULT_REGIME_WEIGHTS)
|
||||
want_indicators = set(_TRENDING_WEIGHTS)
|
||||
got_regimes = set(override)
|
||||
if got_regimes != want_regimes:
|
||||
problems.append(
|
||||
f"missing regimes {sorted(want_regimes - got_regimes)}, "
|
||||
f"unexpected {sorted(got_regimes - want_regimes)}")
|
||||
for regime in sorted(got_regimes & want_regimes):
|
||||
weights = override[regime]
|
||||
if not isinstance(weights, dict):
|
||||
problems.append(f"regime '{regime}' is not an indicator -> "
|
||||
f"multiplier mapping: {weights!r}")
|
||||
continue
|
||||
got = set(weights)
|
||||
if got != want_indicators:
|
||||
problems.append(
|
||||
f"regime '{regime}' missing {sorted(want_indicators - got)}, "
|
||||
f"unexpected {sorted(got - want_indicators)}")
|
||||
for name in sorted(got & want_indicators):
|
||||
mult = weights[name]
|
||||
if (isinstance(mult, bool) or not isinstance(mult, (int, float))
|
||||
or not math.isfinite(mult) or mult < 0):
|
||||
problems.append(
|
||||
f"regime '{regime}' indicator '{name}' multiplier "
|
||||
f"{mult!r} is not a finite number >= 0")
|
||||
if problems:
|
||||
raise ValueError(
|
||||
"regime_weights_override is partial: " + "; ".join(problems)
|
||||
+ "; an override replaces the WHOLE table "
|
||||
"(see engine._DEFAULT_REGIME_WEIGHTS)")
|
||||
|
||||
|
||||
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).
|
||||
|
||||
A non-empty override must cover the WHOLE table (every regime, all
|
||||
nine scored indicators) — a partial one raises ValueError instead of
|
||||
silently multiplying the cells it omits by 1.0.
|
||||
|
||||
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)
|
||||
if override:
|
||||
_validate_regime_override(override)
|
||||
table = override if override else _DEFAULT_REGIME_WEIGHTS
|
||||
return {regime: dict(weights) for regime, weights in table.items()}
|
||||
|
||||
@@ -119,7 +170,7 @@ class TechnicalEngine:
|
||||
return TechnicalAnalysis(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
timestamp=str(pd.Timestamp.utcnow()),
|
||||
timestamp=str(pd.Timestamp.now(tz="UTC")),
|
||||
confidence=0.0,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user