feat(web): 增量自动刷新、结构区修复与默认指标/周期
自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+102
-40
@@ -1,8 +1,7 @@
|
||||
"""SQLite persistence for crypto wyckoff scan rows."""
|
||||
"""SQLite persistence for crypto wyckoff scan rows (per combo)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
@@ -11,7 +10,7 @@ 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",
|
||||
"trade_date", "combo_id", "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",
|
||||
@@ -22,40 +21,80 @@ _COLS = [
|
||||
"feature_snapshot_json", "markers_json", "scanned_at",
|
||||
]
|
||||
|
||||
_CREATE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS wyckoff_scan (
|
||||
trade_date TEXT NOT NULL,
|
||||
combo_id TEXT NOT NULL DEFAULT 'd_w_m',
|
||||
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, combo_id, ts_code)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def _migrate(c: sqlite3.Connection) -> None:
|
||||
cur = c.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='wyckoff_scan'"
|
||||
)
|
||||
if not cur.fetchone():
|
||||
c.execute(_CREATE_SQL)
|
||||
c.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_cw_score "
|
||||
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
|
||||
)
|
||||
return
|
||||
|
||||
cols = {r[1] for r in c.execute("PRAGMA table_info(wyckoff_scan)")}
|
||||
if "combo_id" in cols:
|
||||
c.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_cw_score "
|
||||
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
|
||||
)
|
||||
return
|
||||
|
||||
# Legacy PK (trade_date, ts_code) → add combo_id via table rebuild
|
||||
c.execute("ALTER TABLE wyckoff_scan RENAME TO wyckoff_scan_old")
|
||||
c.execute(_CREATE_SQL)
|
||||
old_cols = [r[1] for r in c.execute("PRAGMA table_info(wyckoff_scan_old)")]
|
||||
shared = [col for col in _COLS if col != "combo_id" and col in old_cols]
|
||||
col_sql = ",".join(shared)
|
||||
c.execute(
|
||||
f"""
|
||||
INSERT INTO wyckoff_scan (combo_id, {col_sql})
|
||||
SELECT 'd_w_m', {col_sql} FROM wyckoff_scan_old
|
||||
"""
|
||||
)
|
||||
c.execute("DROP TABLE wyckoff_scan_old")
|
||||
c.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_cw_score "
|
||||
"ON wyckoff_scan(trade_date, combo_id, overall_score DESC)"
|
||||
)
|
||||
|
||||
|
||||
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)"
|
||||
)
|
||||
_migrate(c)
|
||||
c.commit()
|
||||
return c
|
||||
|
||||
|
||||
def upsert_row(row: WyckoffScanRow) -> None:
|
||||
combo_id = getattr(row, "combo_id", None) or "d_w_m"
|
||||
vals = (
|
||||
row.trade_date.isoformat() if hasattr(row.trade_date, "isoformat") else str(row.trade_date),
|
||||
combo_id,
|
||||
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,
|
||||
@@ -71,11 +110,15 @@ def upsert_row(row: WyckoffScanRow) -> None:
|
||||
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"))
|
||||
updates = ",".join(
|
||||
f"{col}=excluded.{col}"
|
||||
for col in _COLS
|
||||
if col not in ("trade_date", "combo_id", "ts_code")
|
||||
)
|
||||
c.execute(
|
||||
f"""
|
||||
INSERT INTO wyckoff_scan ({col_sql}) VALUES ({placeholders})
|
||||
ON CONFLICT(trade_date, ts_code) DO UPDATE SET {updates}
|
||||
ON CONFLICT(trade_date, combo_id, ts_code) DO UPDATE SET {updates}
|
||||
""",
|
||||
vals,
|
||||
)
|
||||
@@ -84,23 +127,35 @@ def upsert_row(row: WyckoffScanRow) -> None:
|
||||
c.close()
|
||||
|
||||
|
||||
def latest_trade_date() -> str | None:
|
||||
def latest_trade_date(combo_id: str | None = None) -> str | None:
|
||||
c = _conn()
|
||||
try:
|
||||
cur = c.execute("SELECT MAX(trade_date) FROM wyckoff_scan")
|
||||
if combo_id:
|
||||
cur = c.execute(
|
||||
"SELECT MAX(trade_date) FROM wyckoff_scan WHERE combo_id=?",
|
||||
(combo_id,),
|
||||
)
|
||||
else:
|
||||
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()
|
||||
def count_for_date(trade_date: str | None = None, combo_id: str | None = None) -> int:
|
||||
td = trade_date or latest_trade_date(combo_id)
|
||||
if not td:
|
||||
return 0
|
||||
c = _conn()
|
||||
try:
|
||||
cur = c.execute("SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=?", (td,))
|
||||
if combo_id:
|
||||
cur = c.execute(
|
||||
"SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=? AND combo_id=?",
|
||||
(td, combo_id),
|
||||
)
|
||||
else:
|
||||
cur = c.execute("SELECT COUNT(*) FROM wyckoff_scan WHERE trade_date=?", (td,))
|
||||
return int(cur.fetchone()[0])
|
||||
finally:
|
||||
c.close()
|
||||
@@ -109,6 +164,7 @@ def count_for_date(trade_date: str | None = None) -> int:
|
||||
def query_scan(
|
||||
*,
|
||||
trade_date: str | None = None,
|
||||
combo_id: str | None = None,
|
||||
m_cycle: str | None = None,
|
||||
w_phase: str | None = None,
|
||||
d_event: str | None = None,
|
||||
@@ -119,14 +175,15 @@ def query_scan(
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[dict[str, Any]]:
|
||||
td = trade_date or latest_trade_date()
|
||||
cid = combo_id or "d_w_m"
|
||||
td = trade_date or latest_trade_date(cid)
|
||||
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]
|
||||
clauses = ["trade_date=?", "combo_id=?"]
|
||||
args: list[Any] = [td, cid]
|
||||
if m_cycle:
|
||||
clauses.append("m_cycle=?")
|
||||
args.append(m_cycle)
|
||||
@@ -158,15 +215,20 @@ def query_scan(
|
||||
c.close()
|
||||
|
||||
|
||||
def get_symbol(ts_code: str, trade_date: str | None = None) -> dict[str, Any] | None:
|
||||
td = trade_date or latest_trade_date()
|
||||
def get_symbol(
|
||||
ts_code: str,
|
||||
trade_date: str | None = None,
|
||||
combo_id: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
cid = combo_id or "d_w_m"
|
||||
td = trade_date or latest_trade_date(cid)
|
||||
if not td:
|
||||
return None
|
||||
c = _conn()
|
||||
try:
|
||||
cur = c.execute(
|
||||
"SELECT * FROM wyckoff_scan WHERE trade_date=? AND ts_code=?",
|
||||
(td, ts_code),
|
||||
"SELECT * FROM wyckoff_scan WHERE trade_date=? AND combo_id=? AND ts_code=?",
|
||||
(td, cid, ts_code),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
Reference in New Issue
Block a user