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:
@@ -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,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user