"""Signal Instance Store — CRUD for signal_instance table. Separate from detection logic. Detection finds signals, store persists them. Both used by backfill pipeline. """ from __future__ import annotations import pandas as pd from loguru import logger from ashare_dp.data.store.database import get_db def store_signals(df: pd.DataFrame) -> int: """Store detected signals in signal_instance table. Uses ON CONFLICT DO NOTHING to safely handle re-runs. Returns number of rows stored. """ if df.empty: return 0 with get_db(read_only=False) as db: stored = 0 for _, row in df.iterrows(): try: db.execute( """ INSERT INTO signal_instance (signal_type, ts_code, trade_date, signal_price, state_breadth, state_volatility, return_5d, return_10d, return_20d, max_return_5d, max_drawdown_5d, outcome_known) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE) ON CONFLICT (signal_type, ts_code, trade_date) DO NOTHING """, ( row["signal_type"], row["ts_code"], row["trade_date"], float(row["signal_price"]), float(row["daily_advance_ratio"]) if pd.notna(row.get("daily_advance_ratio")) else 0.5, float(row["daily_range"]) if pd.notna(row.get("daily_range")) else 0.02, float(row["ret_5d"]) if pd.notna(row.get("ret_5d")) else None, float(row["ret_10d"]) if pd.notna(row.get("ret_10d")) else None, float(row["ret_20d"]) if pd.notna(row.get("ret_20d")) else None, float(row["max_return_5d"]) if pd.notna(row.get("max_return_5d")) else None, float(row["max_dd_5d"]) if pd.notna(row.get("max_dd_5d")) else None, ), ) stored += 1 except Exception as e: logger.debug(f"Signal store failed for {row.get('ts_code')}: {e}") logger.info(f"Stored {stored} signal instances") return stored def count_signals(signal_type: str | None = None) -> int: """Count stored signal instances, optionally filtered by type.""" with get_db(read_only=True) as db: if signal_type: rows = db.query( "SELECT COUNT(*) FROM signal_instance WHERE signal_type = ?", (signal_type,), ) else: rows = db.query("SELECT COUNT(*) FROM signal_instance") return int(rows[0][0]) if rows else 0