"""SignalDedup — SQLite-backed 24h duplicate signal blocker.""" from __future__ import annotations import sqlite3 from contextlib import contextmanager from datetime import datetime, timedelta from pathlib import Path from zoneinfo import ZoneInfo KST = ZoneInfo("Asia/Seoul") def _now_iso() -> str: """Test seam — overridable via monkeypatch.""" return datetime.now(KST).isoformat() _SCHEMA = """ CREATE TABLE IF NOT EXISTS signal_dedup ( ticker TEXT NOT NULL, action TEXT NOT NULL, last_sent TEXT NOT NULL, confidence REAL NOT NULL, PRIMARY KEY (ticker, action) ); CREATE INDEX IF NOT EXISTS idx_signal_dedup_last_sent ON signal_dedup(last_sent); """ class SignalDedup: """24h dedup interface. WAL + busy_timeout=120000.""" def __init__(self, db_path: Path): self._db_path = Path(db_path) self._db_path.parent.mkdir(parents=True, exist_ok=True) self._init_schema() @contextmanager def _conn(self): conn = sqlite3.connect(self._db_path, timeout=120.0) try: conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=120000") yield conn finally: conn.close() def _init_schema(self) -> None: with self._conn() as conn: conn.executescript(_SCHEMA) conn.commit() def is_recent(self, ticker: str, action: str, within_hours: int = 24) -> bool: threshold_dt = datetime.fromisoformat(_now_iso()) - timedelta(hours=within_hours) threshold_iso = threshold_dt.isoformat() with self._conn() as conn: row = conn.execute( "SELECT last_sent FROM signal_dedup WHERE ticker = ? AND action = ?", (ticker, action), ).fetchone() return row is not None and row[0] >= threshold_iso def record(self, ticker: str, action: str, confidence: float) -> None: with self._conn() as conn: conn.execute( """INSERT INTO signal_dedup (ticker, action, last_sent, confidence) VALUES (?, ?, ?, ?) ON CONFLICT (ticker, action) DO UPDATE SET last_sent = excluded.last_sent, confidence = excluded.confidence""", (ticker, action, _now_iso(), confidence), ) conn.commit()