diff --git a/analyze_signal_log.py b/analyze_signal_log.py index 5f18679..0887291 100644 --- a/analyze_signal_log.py +++ b/analyze_signal_log.py @@ -67,7 +67,7 @@ import math import os import sqlite3 import sys -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Sequence, Tuple @@ -754,8 +754,7 @@ def _fmt_r(r: Optional[float]) -> str: return " n/a " if r is None else f"{r:+.2f}R" -def persona_scorecard(rows: Sequence[LogRow], events: Sequence[SignalEvent], - db_path: str) -> List[dict]: +def persona_scorecard(events: Sequence[SignalEvent], db_path: str) -> List[dict]: """Per persona × stance: n, mean R and win rate of the underlying event (needs panel_takes_json — read straight from the DB).""" by_row = _row_index(events) @@ -921,8 +920,7 @@ def news_extremes(rows: Sequence[LogRow], streams: Dict[str, List[dict]], } -def news_outcome_buckets(rows: Sequence[LogRow], - events: Sequence[SignalEvent], +def news_outcome_buckets(events: Sequence[SignalEvent], db_path: str) -> List[dict]: """Resolved events bucketed by (direction, news bucket, macro stance). @@ -1306,13 +1304,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int: print_report(rows, events, panel, hit) print_stored_books(events) print("\n── Persona scorecard (stance × event outcome) " + "─" * 22) - for p in persona_scorecard(rows, events, args.db): + for p in persona_scorecard(events, args.db): print(f" {p['persona']:11} {p['stance']:8} n={p['n']:3d} " f"mean {p['mean_r']:+.3f}R win {100 * p['win_rate']:.0f}%") if args.news: print_news(news_lead_lag(rows, streams), news_extremes(rows, streams), - news_outcome_buckets(rows, events, args.db)) + news_outcome_buckets(events, args.db)) print_parity(parity_report(rows)) if args.csv: export_csv(events, args.csv) diff --git a/dashboard_publisher.py b/dashboard_publisher.py index 77ff85c..365f39c 100644 --- a/dashboard_publisher.py +++ b/dashboard_publisher.py @@ -432,10 +432,6 @@ class DashboardPublisher: if self.news_payload is not None: publish(self.news_payload, os.path.join(self.out_dir, NEWS_FILE)) - def publish_all(self) -> None: - self.publish_snapshot() - self.publish_news() - def run_cycle(self, tfs: List[Timeframe]) -> None: for tf in tfs: # sequential — see module docstring (cache dedup) self.scan_tf(tf) diff --git a/llm_client.py b/llm_client.py index 47c025a..d128caa 100644 --- a/llm_client.py +++ b/llm_client.py @@ -198,10 +198,3 @@ def get_llm_client() -> DeepSeekClient: enabled=settings.get("enabled", True) is not False, ) return _client - - -def reset_llm_client() -> None: - """Drop the singleton (tests / config reloads).""" - global _client - with _client_lock: - _client = None diff --git a/market_outlook.py b/market_outlook.py index 38f70c1..973735d 100644 --- a/market_outlook.py +++ b/market_outlook.py @@ -329,7 +329,6 @@ def _collect_payload(settings: Dict[str, Any]) -> Dict[str, Any]: # ── Orchestration, hourly dedup, and request budget ────────────────── _state_lock = threading.Lock() -_last_run_ts: Optional[float] = None _last_run_hour: Optional[int] = None # UTC hour bucket of the last attempt _outlook_day: Optional[_dt.date] = None _outlook_count = 0 @@ -344,10 +343,9 @@ def _skip(reason: str) -> None: def reset_outlook_state() -> None: """Drop hourly and daily state (tests / deployment config reloads).""" - global _last_run_ts, _last_run_hour, _outlook_day, _outlook_count + global _last_run_hour, _outlook_day, _outlook_count global _warned_budget with _state_lock: - _last_run_ts = None _last_run_hour = None _outlook_day = None _outlook_count = 0 @@ -360,7 +358,7 @@ def _begin_attempt(cap: int, force: bool) -> bool: Hourly dedup is by UTC hour BUCKET, not ``now - last < 3600``: the H1 loop calls this at boundary+10 s+scan time, so consecutive hours were routinely 3,590 s apart and every other hour was dropped.""" - global _last_run_ts, _last_run_hour, _outlook_day, _outlook_count + global _last_run_hour, _outlook_day, _outlook_count global _warned_budget now = time.time() hour_bucket = int(now // 3600) @@ -383,7 +381,6 @@ def _begin_attempt(cap: int, force: bool) -> bool: _skip("daily budget exhausted") return False _outlook_count += 1 - _last_run_ts = now _last_run_hour = hour_bucket return True diff --git a/news_engine.py b/news_engine.py index 079e539..888114f 100755 --- a/news_engine.py +++ b/news_engine.py @@ -223,7 +223,6 @@ class FundamentalContext: warning_message: str = "" news_sentiment: Dict[str, float] = field(default_factory=dict) # currency -> sentiment news_headlines: List[NewsSentiment] = field(default_factory=list) - event_impact_on_pairs: Dict[str, str] = field(default_factory=dict) calendar_available: bool = False # True if live calendar data was fetched llm_summary: str = "" # DeepSeek market-context blurb (display-only) @@ -415,15 +414,6 @@ class NewsEngine: ) break - # Map upcoming high-impact events to affected pairs - for event in context.events_upcoming_1h: - if event.is_high_impact: - affected = self._get_affected_pairs(event.currency) - for pair in affected: - context.event_impact_on_pairs[pair] = ( - f"{event.currency} {event.event} in {event.minutes_until:.0f}min" - ) - except Exception as e: logger.warning(f"Economic calendar fetch failed: {e}") @@ -1149,11 +1139,3 @@ class NewsEngine: except ValueError: continue return None - - @staticmethod - def _get_affected_pairs(currency: str) -> List[str]: - _MAP = { - "USD": ["XAU/USD"], - "XAU": ["XAU/USD"], - } - return _MAP.get(currency, []) diff --git a/position_sizing.py b/position_sizing.py index ce13786..3c0698e 100644 --- a/position_sizing.py +++ b/position_sizing.py @@ -10,11 +10,11 @@ inputs (rule 4: live == backtest parity). Design contract --------------- * **Bounded lookbacks.** No feature may read further back than the live - fetch depth of its timeframe (``LIVE_DEPTH_BARS``). The primary frame - is hard-capped at ``MAX_LOOKBACK_BARS`` rows, confirmation frames at - 60 rows, and the burst scan at 200 rows, so a feature computed on a - full backtest frame equals the same feature computed on the shorter - live tail. + fetch depth of its timeframe (5000 bars on M5/M15, 1460 on H1, 380 on + H4). The primary frame is hard-capped at ``MAX_LOOKBACK_BARS`` rows, + confirmation frames at 60 rows, and the burst scan at 200 rows, so a + feature computed on a full backtest frame equals the same feature + computed on the shorter live tail. * **Closed bars only.** The frames handed in are exactly the frames ``analyze_from_dataframes`` received: UTC tz-aware, oldest-first, capitalized OHLCV columns, forming candle already dropped. The last @@ -46,7 +46,7 @@ import json import logging import math from pathlib import Path -from typing import Dict, Iterable, Optional, Sequence, Tuple +from typing import Dict, Optional, Sequence, Tuple import numpy as np import pandas as pd @@ -60,17 +60,6 @@ logger = logging.getLogger(__name__) #: A trained artifact carrying a different value is ignored. FEATURE_VERSION = 1 -#: Bars each timeframe actually holds in LIVE mode. Any feature whose -#: lookback exceeds its timeframe's depth here would be unreproducible -#: live, so the windows below stay far inside these numbers. -LIVE_DEPTH_BARS: Dict[Timeframe, int] = { - Timeframe.M5: 5000, - Timeframe.M15: 5000, - Timeframe.H1: 1460, - Timeframe.H4: 380, - Timeframe.D1: 300, -} - #: Hard cap on any primary-timeframe window used in this module. MAX_LOOKBACK_BARS = 500 @@ -338,7 +327,7 @@ def _fill_time(out: Dict[str, float], dt: pd.Timestamp) -> None: def _fill_geometry(out: Dict[str, float], P: pd.DataFrame, setup, - atr: float, atr_ok: bool, norm, d: float) -> None: + atr: float, norm, d: float) -> None: out["f_atr"] = max(0.0, _finite(atr)) ranges = (P["High"] - P["Low"]).astype(float) last_range = _finite(ranges.iloc[-1]) @@ -566,7 +555,7 @@ def build_features( for step, args in ( (_fill_time, (out, decision_time(P, primary_tf))), - (_fill_geometry, (out, P, setup, atr, atr_ok, norm, d)), + (_fill_geometry, (out, P, setup, atr, norm, d)), (_fill_exhaustion, (out, report, KL, px, d, norm)), (_fill_quality, (out, report, setup, IND, d)), (_fill_mtf, (out, report, primary_tf, confirmation_tfs, d)), diff --git a/scripts/backfill_signal_outcomes.py b/scripts/backfill_signal_outcomes.py index 24ca887..b4d0ed3 100644 --- a/scripts/backfill_signal_outcomes.py +++ b/scripts/backfill_signal_outcomes.py @@ -41,7 +41,7 @@ def _status_counts(con: sqlite3.Connection) -> dict: 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("--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") diff --git a/scripts/label_scan_outcomes.py b/scripts/label_scan_outcomes.py index 7ba76ad..5d4e32c 100755 --- a/scripts/label_scan_outcomes.py +++ b/scripts/label_scan_outcomes.py @@ -60,7 +60,7 @@ def _counts(con: sqlite3.Connection, symbol: str, 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("--db", default=None, help="defaults to the signal_log.path setting in config.json") ap.add_argument("--dry-run", action="store_true", help="read-only candidate counts, writes nothing") ap.add_argument("--budget", type=int, default=100000, diff --git a/signal_log.py b/signal_log.py index b717345..c8f4ade 100644 --- a/signal_log.py +++ b/signal_log.py @@ -806,7 +806,6 @@ class SignalLogger: the daemon's thread pool.""" def __init__(self, path: str): - self._path = path parent = os.path.dirname(path) if parent: os.makedirs(parent, exist_ok=True) @@ -860,10 +859,6 @@ class SignalLogger: raise time.sleep(delay * (attempt + 1)) - @property - def path(self) -> str: - return self._path - def _migrate_signal_columns(self) -> None: """Add newer signal columns to pre-existing tables (idempotent).""" existing = {r[1] for r in self._conn.execute("PRAGMA table_info(signals)")} @@ -1291,27 +1286,6 @@ class SignalLogger: rowid, exc_info=True) return written - def advance_with_bars(self, rowid: int, bars: Iterable[Dict[str, Any]], - evidence: str = "external_bars") -> str: - """Advance ONE pending outcome from caller-supplied bars (backfill - from provider history). Returns the resulting status.""" - with self._lock: - row = self._conn.execute( - "SELECT advisory_outcome_status, advisory_outcome_json" - " FROM signals WHERE id=?", (rowid,)).fetchone() - if row is None or row[0] != "PENDING" or not row[1]: - return str(row[0]) if row else "MISSING" - state = advance_outcome(json.loads(row[1]), list(bars)) - state["evidence"] = evidence - status = state.get("status", "UNRESOLVABLE") - self._conn.execute( - "UPDATE signals SET advisory_outcome_status=?," - " advisory_r_multiple=?, advisory_outcome_json=?" - " WHERE id=? AND advisory_outcome_status='PENDING'", - (status, canonical_r(state), _dumps(state), rowid)) - self._conn.commit() - return status - def backfill_seeds(self, limit: Optional[int] = None) -> Dict[str, int]: """Assign setup_key / advisory_event_id to legacy setup rows and seed the canonical ones that carry no outcome yet. Existing diff --git a/test_all.py b/test_all.py index d94f8f6..cded7e5 100644 --- a/test_all.py +++ b/test_all.py @@ -881,18 +881,6 @@ def _(): assert NewsEngine._parse_rss_date("not-a-date") is None -@test("NewsEngine._get_affected_pairs") -def _(): - pairs = NewsEngine._get_affected_pairs("USD") - assert "XAU/USD" in pairs - - pairs_xau = NewsEngine._get_affected_pairs("XAU") - assert "XAU/USD" in pairs_xau - - pairs_unknown = NewsEngine._get_affected_pairs("ARS") - assert pairs_unknown == [] - - @test("EconomicEvent properties") def _(): future_time = datetime.now(timezone.utc) + timedelta(minutes=30) @@ -8174,7 +8162,7 @@ def _(): "stance": "support"}]))) con.commit() con.close() - buckets = asl.news_outcome_buckets(rows, events, path) + buckets = asl.news_outcome_buckets(events, path) finally: os.remove(path) assert sum(b["n"] for b in buckets) == 3 @@ -9117,7 +9105,7 @@ def _(): ev = _types.SimpleNamespace( rows=[_types.SimpleNamespace(id=1), _types.SimpleNamespace(id=2)], r_headline=2.0) - out = asl.persona_scorecard([], [ev], path) + out = asl.persona_scorecard([ev], path) assert len(out) == 1, out assert out[0]["n"] == 1, f"two rows of ONE event must count once: {out}" assert out[0]["mean_r"] == 2.0