feat(ECR-009): Crypto Wyckoff Screener 独立页(D/W/M)

移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 15:46:35 +08:00
co-authored by Cursor
parent 6c627f009a
commit ec08de098e
34 changed files with 3212 additions and 9 deletions
+149
View File
@@ -0,0 +1,149 @@
"""Event Engine — active concurrent events via Rule Registry.
Note: `active_events` are rules that fire on the latest bar snapshot,
NOT a historical SC→AR→ST timeline. Do not present as chronological chain.
"""
from __future__ import annotations
from crypto_wyckoff.domain_models import EngineResult, WyckoffEvent
from crypto_wyckoff.rules.registry import rule_registry
# Display order only (not temporal history)
_DISPLAY_ORDER = [
WyckoffEvent.PS.value,
WyckoffEvent.SC.value,
WyckoffEvent.AR.value,
WyckoffEvent.ST.value,
WyckoffEvent.SPRING.value,
WyckoffEvent.TEST.value,
WyckoffEvent.SOS.value,
WyckoffEvent.LPS.value,
WyckoffEvent.JUMP.value,
WyckoffEvent.BACKUP.value,
WyckoffEvent.BC.value,
WyckoffEvent.UTAD.value,
WyckoffEvent.SOW.value,
WyckoffEvent.LPSY.value,
]
# Dominant event: highest confidence wins; ties broken by this priority
_DOMINANCE_PRIORITY = [
WyckoffEvent.SOS.value,
WyckoffEvent.LPS.value,
WyckoffEvent.UTAD.value,
WyckoffEvent.SPRING.value,
WyckoffEvent.JUMP.value,
WyckoffEvent.BACKUP.value,
WyckoffEvent.TEST.value,
WyckoffEvent.SC.value,
WyckoffEvent.SOW.value,
WyckoffEvent.AR.value,
WyckoffEvent.ST.value,
]
class EventEngine:
name = "Event"
version = "1.0.0"
def run(
self,
cycle: EngineResult,
phase: EngineResult,
feature: EngineResult,
timeframe: str,
) -> EngineResult:
if feature.payload.get("insufficient"):
return EngineResult(
name=self.name,
version=self.version,
confidence=20.0,
score=30.0,
reasons=["特征不足,跳过事件识别"],
warnings=["insufficient_features"],
payload={
"current_event": WyckoffEvent.NONE.value,
"active_events": [],
"recent_events": [], # alias for DB/API compat; same as active_events
"timeframe": timeframe,
"entry_score": 30.0,
},
)
context = {
"features": feature.payload,
"cycle": cycle.payload,
"phase": phase.payload,
"timeframe": timeframe,
}
hits = []
for rule in rule_registry.by_category("event", timeframe):
hit = rule.evaluate(context)
if hit and hit.event:
hits.append(hit)
if not hits:
return EngineResult(
name=self.name,
version=self.version,
confidence=35.0,
score=40.0,
reasons=["无显著事件"],
payload={
"current_event": WyckoffEvent.NONE.value,
"active_events": [],
"recent_events": [],
"timeframe": timeframe,
"entry_score": 40.0,
},
)
by_event: dict[str, float] = {}
reasons: list[str] = []
metrics: dict = {}
for h in hits:
prev = by_event.get(h.event, -1.0)
if h.confidence >= prev:
by_event[h.event] = h.confidence
reasons.extend(h.reasons)
metrics.update(h.metrics)
active = [e for e in _DISPLAY_ORDER if e in by_event]
for e in by_event:
if e not in active:
active.append(e)
# Dominant = max confidence; tie-break by dominance priority index
def _dom_key(ev: str) -> tuple:
conf = by_event[ev]
try:
prio = _DOMINANCE_PRIORITY.index(ev)
except ValueError:
prio = 99
return (conf, -prio)
current = max(by_event.keys(), key=_dom_key)
event_conf = by_event[current]
co_bonus = min(12.0, max(0, len(active) - 1) * 3)
entry_score = min(98.0, event_conf + co_bonus)
if current == WyckoffEvent.SPRING.value and WyckoffEvent.TEST.value in by_event:
entry_score = min(98.0, entry_score + 5)
return EngineResult(
name=self.name,
version=self.version,
confidence=event_conf,
score=entry_score,
reasons=list(dict.fromkeys(reasons))[:8],
warnings=["active_events_are_concurrent_not_timeline"],
metrics=metrics,
payload={
"current_event": current,
"active_events": active,
"recent_events": active, # persisted column name; semantic = active
"event_scores": by_event,
"timeframe": timeframe,
"entry_score": entry_score,
},
)