"""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, }, )