Architecture: - Restructure into 5 subsystems: data/, features/, market/, signals/, execution/, apps/ - Unified ts_code conversion in core/codes.py (idempotent, kills 4 duplicate copies) - analytics_conn() + kline_glob() — zero hardcoded DB/Parquet paths - Fixed double-suffix bug (.SZ.SZ) in backfill pipeline root cause Signal Intelligence (the moat): - 14 signal types: EMA52, Vegas, Chan, ORB, Gap, NR7, Inside Bar - 640K+ historical signal instances across 8 backfilled types - Multi-signal Expectancy Engine with breadth-similarity matching - Signal backfill CLI: ashare-dp backfill signals Market Intelligence: - 8 engines: State, Leadership, Opportunity, Flow, Sentiment, Memory, Knowledge Graph, Recommendations - Real limit-up/down sentiment via akshare (108 ZT, 19 DT, 52 broken board) - Knowledge Graph: 8 themes × 30+ concepts with keyword matching - Money-flow stock recommendations with entry/stop/target trade plans Dashboard Command Center: - Decision-first layout: COMMAND → WHERE → WHY → RISK → EXPECTANCY - Multi-signal Expectancy comparison table (8 types ranked by WR) - Theme Map visualization with rotation detection - Intraday Replay infrastructure (30min state snapshots) - RECOMMENDATIONS card with actionable trade plans Trading Memory: - trade_log table + POST/GET/PUT API for trade recording - Performance stats aggregation Code Quality: - 0 hardcoded DB paths, 0 REPLACE hacks, 0 dead ts_code copies - EMA52 screening deduplicated (CLI + scheduler share one function) - read_parquet_sql() helper for 28 duplicate patterns - 6 bugs fixed from code review (NR7 window, theme matching, column indices, etc.) Co-Authored-By: Claude <noreply@anthropic.com>
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""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
|