移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。 Co-authored-by: Cursor <cursoragent@cursor.com>
175 lines
5.9 KiB
Python
175 lines
5.9 KiB
Python
"""SQLite persistence for crypto wyckoff scan rows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from crypto_wyckoff.domain_models import WyckoffScanRow
|
|
from crypto_wyckoff.io import SCAN_DB, ensure_dirs
|
|
|
|
_COLS = [
|
|
"trade_date", "ts_code", "name", "industry", "engine_version",
|
|
"m_cycle", "cycle_confidence", "trend_score",
|
|
"w_cycle", "w_phase", "w_current_event", "w_recent_events_json",
|
|
"phase_confidence", "structure_score",
|
|
"d_current_event", "d_recent_events_json", "event_confidence", "entry_score",
|
|
"entry", "stop", "target1", "target2", "rr",
|
|
"alignment", "stars", "decision_signal", "signal_confidence",
|
|
"overall_confidence", "overall_score", "risk", "reasons_json",
|
|
"feature_snapshot_json", "markers_json", "scanned_at",
|
|
]
|
|
|
|
|
|
def _conn() -> sqlite3.Connection:
|
|
ensure_dirs()
|
|
c = sqlite3.connect(str(SCAN_DB), timeout=60)
|
|
c.row_factory = sqlite3.Row
|
|
c.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS wyckoff_scan (
|
|
trade_date TEXT NOT NULL,
|
|
ts_code TEXT NOT NULL,
|
|
name TEXT DEFAULT '',
|
|
industry TEXT DEFAULT '',
|
|
engine_version TEXT,
|
|
m_cycle TEXT, cycle_confidence REAL, trend_score REAL,
|
|
w_cycle TEXT, w_phase TEXT, w_current_event TEXT, w_recent_events_json TEXT,
|
|
phase_confidence REAL, structure_score REAL,
|
|
d_current_event TEXT, d_recent_events_json TEXT, event_confidence REAL, entry_score REAL,
|
|
entry REAL, stop REAL, target1 REAL, target2 REAL, rr REAL,
|
|
alignment REAL, stars INTEGER, decision_signal TEXT, signal_confidence REAL,
|
|
overall_confidence REAL, overall_score REAL, risk TEXT, reasons_json TEXT,
|
|
feature_snapshot_json TEXT, markers_json TEXT, scanned_at TEXT,
|
|
PRIMARY KEY (trade_date, ts_code)
|
|
)
|
|
"""
|
|
)
|
|
c.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_cw_score ON wyckoff_scan(trade_date, overall_score DESC)"
|
|
)
|
|
return c
|
|
|
|
|
|
def upsert_row(row: WyckoffScanRow) -> None:
|
|
vals = (
|
|
row.trade_date.isoformat() if hasattr(row.trade_date, "isoformat") else str(row.trade_date),
|
|
row.ts_code, row.name, row.industry, row.engine_version,
|
|
row.m_cycle, row.cycle_confidence, row.trend_score,
|
|
row.w_cycle, row.w_phase, row.w_current_event, row.w_recent_events_json,
|
|
row.phase_confidence, row.structure_score,
|
|
row.d_current_event, row.d_recent_events_json, row.event_confidence, row.entry_score,
|
|
row.entry, row.stop, row.target1, row.target2, row.rr,
|
|
row.alignment, row.stars, row.decision_signal, row.signal_confidence,
|
|
row.overall_confidence, row.overall_score, row.risk, row.reasons_json,
|
|
row.feature_snapshot_json, row.markers_json,
|
|
row.scanned_at.isoformat() if isinstance(row.scanned_at, datetime) else str(row.scanned_at),
|
|
)
|
|
c = _conn()
|
|
try:
|
|
placeholders = ",".join("?" * len(_COLS))
|
|
col_sql = ",".join(_COLS)
|
|
updates = ",".join(f"{c}=excluded.{c}" for c in _COLS if c not in ("trade_date", "ts_code"))
|
|
c.execute(
|
|
f"""
|
|
INSERT INTO wyckoff_scan ({col_sql}) VALUES ({placeholders})
|
|
ON CONFLICT(trade_date, ts_code) DO UPDATE SET {updates}
|
|
""",
|
|
vals,
|
|
)
|
|
c.commit()
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def latest_trade_date() -> str | None:
|
|
c = _conn()
|
|
try:
|
|
cur = c.execute("SELECT MAX(trade_date) FROM wyckoff_scan")
|
|
row = cur.fetchone()
|
|
return row[0] if row and row[0] else None
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def count_for_date(trade_date: str | None = None) -> int:
|
|
td = trade_date or latest_trade_date()
|
|
if not td:
|
|
return 0
|
|
c = _conn()
|
|
try:
|
|
cur = c.execute("SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=?", (td,))
|
|
return int(cur.fetchone()[0])
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def query_scan(
|
|
*,
|
|
trade_date: str | None = None,
|
|
m_cycle: str | None = None,
|
|
w_phase: str | None = None,
|
|
d_event: str | None = None,
|
|
decision_signal: str | None = None,
|
|
min_overall_score: float | None = None,
|
|
min_alignment: float | None = None,
|
|
sort: str = "overall_score",
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
) -> list[dict[str, Any]]:
|
|
td = trade_date or latest_trade_date()
|
|
if not td:
|
|
return []
|
|
sort_col = sort if sort in {
|
|
"overall_score", "alignment", "entry_score", "trend_score", "structure_score", "stars"
|
|
} else "overall_score"
|
|
clauses = ["trade_date=?"]
|
|
args: list[Any] = [td]
|
|
if m_cycle:
|
|
clauses.append("m_cycle=?")
|
|
args.append(m_cycle)
|
|
if w_phase:
|
|
clauses.append("w_phase=?")
|
|
args.append(w_phase)
|
|
if d_event:
|
|
clauses.append("d_current_event=?")
|
|
args.append(d_event)
|
|
if decision_signal:
|
|
clauses.append("decision_signal=?")
|
|
args.append(decision_signal)
|
|
if min_overall_score is not None:
|
|
clauses.append("overall_score>=?")
|
|
args.append(min_overall_score)
|
|
if min_alignment is not None:
|
|
clauses.append("alignment>=?")
|
|
args.append(min_alignment)
|
|
where = " AND ".join(clauses)
|
|
args.extend([limit, offset])
|
|
c = _conn()
|
|
try:
|
|
cur = c.execute(
|
|
f"SELECT * FROM wyckoff_scan WHERE {where} ORDER BY {sort_col} DESC LIMIT ? OFFSET ?",
|
|
args,
|
|
)
|
|
return [dict(r) for r in cur.fetchall()]
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def get_symbol(ts_code: str, trade_date: str | None = None) -> dict[str, Any] | None:
|
|
td = trade_date or latest_trade_date()
|
|
if not td:
|
|
return None
|
|
c = _conn()
|
|
try:
|
|
cur = c.execute(
|
|
"SELECT * FROM wyckoff_scan WHERE trade_date=? AND ts_code=?",
|
|
(td, ts_code),
|
|
)
|
|
row = cur.fetchone()
|
|
return dict(row) if row else None
|
|
finally:
|
|
c.close()
|