自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
"""Scan pipeline: load local frames → engines → store (per TF combo)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from datetime import date, datetime, timezone
|
|
|
|
from crypto_wyckoff.combos import ROLE_HIGH, ROLE_LOW, ROLE_MID, get_combo, lookback_for
|
|
from crypto_wyckoff.cycle import CycleEngine
|
|
from crypto_wyckoff.decision import DecisionEngine
|
|
from crypto_wyckoff.domain_models import WyckoffScanRow
|
|
from crypto_wyckoff.event import EventEngine
|
|
from crypto_wyckoff.features import FeatureEngine
|
|
from crypto_wyckoff.io import load_frame
|
|
from crypto_wyckoff.phase import PhaseEngine
|
|
from crypto_wyckoff.plan import PlanEngine
|
|
from crypto_wyckoff.signal import SignalEngine
|
|
from crypto_wyckoff.store import upsert_row
|
|
from crypto_wyckoff.symbols_cn import display_name_cn
|
|
from crypto_wyckoff.version import WYCKOFF_ENGINE_VERSION
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def analyze_symbol(
|
|
low_frame,
|
|
mid_frame,
|
|
high_frame,
|
|
*,
|
|
feature_eng: FeatureEngine,
|
|
cycle_eng: CycleEngine,
|
|
phase_eng: PhaseEngine,
|
|
event_eng: EventEngine,
|
|
signal_eng: SignalEngine,
|
|
decision_eng: DecisionEngine,
|
|
plan_eng: PlanEngine,
|
|
) -> dict:
|
|
"""Run engines with D/W/M *role* aliases so existing rules match.
|
|
|
|
Frames may be any TF combo (e.g. 1h/4h/8h); rules still see 1d/1w/1M roles.
|
|
"""
|
|
f_d = feature_eng.run(low_frame, ROLE_LOW)
|
|
f_w = feature_eng.run(mid_frame, ROLE_MID)
|
|
f_m = feature_eng.run(high_frame, ROLE_HIGH)
|
|
|
|
c_m = cycle_eng.run(f_m, ROLE_HIGH)
|
|
c_w = cycle_eng.run(f_w, ROLE_MID)
|
|
|
|
p_w = phase_eng.run(c_w, f_w, ROLE_MID)
|
|
p_d = phase_eng.run(c_w, f_d, ROLE_LOW)
|
|
|
|
e_w = event_eng.run(c_w, p_w, f_w, ROLE_MID)
|
|
e_d = event_eng.run(c_w, p_d, f_d, ROLE_LOW)
|
|
|
|
s_d = signal_eng.run(e_d, p_d)
|
|
decision = decision_eng.run(c_m, c_w, p_w, e_w, e_d, s_d)
|
|
plan = plan_eng.run(f_d, decision)
|
|
|
|
return {
|
|
"f_d": f_d, "f_w": f_w, "f_m": f_m,
|
|
"c_m": c_m, "c_w": c_w, "p_w": p_w,
|
|
"e_w": e_w, "e_d": e_d, "s_d": s_d,
|
|
"decision": decision, "plan": plan,
|
|
}
|
|
|
|
|
|
def _to_row(
|
|
trade_date: date,
|
|
symbol: str,
|
|
result: dict,
|
|
*,
|
|
combo_id: str,
|
|
combo_label: str,
|
|
) -> WyckoffScanRow:
|
|
d = result["decision"]
|
|
p = result["plan"]
|
|
c_m, c_w, p_w = result["c_m"], result["c_w"], result["p_w"]
|
|
e_w, e_d, s_d = result["e_w"], result["e_d"], result["s_d"]
|
|
f_d, f_w, f_m = result["f_d"], result["f_w"], result["f_m"]
|
|
|
|
snapshot = {
|
|
"combo_id": combo_id,
|
|
"combo_label": combo_label,
|
|
"daily": {k: f_d.payload.get(k) for k in (
|
|
"ma20", "ma60", "ma120", "atr", "adx", "volume_ratio",
|
|
"range_high", "range_low", "swing_high", "swing_low", "close",
|
|
)},
|
|
"weekly": {k: f_w.payload.get(k) for k in ("ma20", "ma60", "adx", "close")},
|
|
"monthly": {k: f_m.payload.get(k) for k in ("ma20", "ma60", "adx", "close")},
|
|
}
|
|
markers = []
|
|
for key, typ in (("entry", "entry"), ("stop", "stop"), ("target1", "target1"), ("target2", "target2")):
|
|
if p.payload.get(key) is not None:
|
|
markers.append({"type": typ, "price": p.payload[key]})
|
|
|
|
return WyckoffScanRow(
|
|
trade_date=trade_date,
|
|
ts_code=symbol,
|
|
name=display_name_cn(symbol),
|
|
industry="crypto",
|
|
engine_version=WYCKOFF_ENGINE_VERSION,
|
|
m_cycle=c_m.payload.get("cycle", "Unknown"),
|
|
cycle_confidence=c_m.confidence,
|
|
trend_score=float(d.payload.get("trend_score", c_m.score)),
|
|
w_cycle=c_w.payload.get("cycle", "Unknown"),
|
|
w_phase=p_w.payload.get("phase", "None"),
|
|
w_current_event=e_w.payload.get("current_event", "None"),
|
|
w_recent_events_json=json.dumps(
|
|
e_w.payload.get("active_events") or e_w.payload.get("recent_events") or [],
|
|
ensure_ascii=False,
|
|
),
|
|
phase_confidence=p_w.confidence,
|
|
structure_score=float(d.payload.get("structure_score", p_w.score)),
|
|
d_current_event=e_d.payload.get("current_event", "None"),
|
|
d_recent_events_json=json.dumps(
|
|
e_d.payload.get("active_events") or e_d.payload.get("recent_events") or [],
|
|
ensure_ascii=False,
|
|
),
|
|
event_confidence=e_d.confidence,
|
|
entry_score=float(d.payload.get("entry_score", e_d.score)),
|
|
entry=p.payload.get("entry"),
|
|
stop=p.payload.get("stop"),
|
|
target1=p.payload.get("target1"),
|
|
target2=p.payload.get("target2"),
|
|
rr=p.payload.get("rr"),
|
|
alignment=float(d.payload.get("alignment", 0)),
|
|
stars=int(d.payload.get("stars", 1)),
|
|
decision_signal=d.payload.get("decision_signal", "Watch"),
|
|
signal_confidence=s_d.confidence,
|
|
overall_confidence=float(d.payload.get("overall_confidence", d.confidence)),
|
|
overall_score=float(d.payload.get("overall_score", d.score)),
|
|
risk=d.payload.get("risk", "Medium"),
|
|
reasons_json=json.dumps(d.reasons + d.warnings, ensure_ascii=False),
|
|
feature_snapshot_json=json.dumps(snapshot, ensure_ascii=False),
|
|
markers_json=json.dumps(markers, ensure_ascii=False),
|
|
scanned_at=datetime.now(timezone.utc),
|
|
combo_id=combo_id,
|
|
)
|
|
|
|
|
|
_ENGINES = None
|
|
|
|
|
|
def _engines():
|
|
global _ENGINES
|
|
if _ENGINES is None:
|
|
_ENGINES = {
|
|
"feature_eng": FeatureEngine(),
|
|
"cycle_eng": CycleEngine(),
|
|
"phase_eng": PhaseEngine(),
|
|
"event_eng": EventEngine(),
|
|
"signal_eng": SignalEngine(),
|
|
"decision_eng": DecisionEngine(),
|
|
"plan_eng": PlanEngine(),
|
|
}
|
|
return _ENGINES
|
|
|
|
|
|
def analyze_and_store(
|
|
symbol: str,
|
|
trade_date: date | None = None,
|
|
*,
|
|
combo_id: str | None = None,
|
|
) -> WyckoffScanRow | None:
|
|
eng = _engines()
|
|
combo = get_combo(combo_id)
|
|
low_tf, mid_tf, high_tf = combo["low"], combo["mid"], combo["high"]
|
|
|
|
low = load_frame(symbol, low_tf, lookback_for(low_tf))
|
|
mid = load_frame(symbol, mid_tf, lookback_for(mid_tf))
|
|
high = load_frame(symbol, high_tf, lookback_for(high_tf))
|
|
if low is None or len(low) < 40:
|
|
return None
|
|
result = analyze_symbol(low, mid, high, **eng)
|
|
td = trade_date or (
|
|
low.trade_dates[-1] if low.trade_dates else datetime.now(timezone.utc).date()
|
|
)
|
|
row = _to_row(td, symbol, result, combo_id=combo["id"], combo_label=combo["label"])
|
|
upsert_row(row)
|
|
return row
|