"""Decision Engine — multi-timeframe fusion and tradability (Architecture v1.0).""" from __future__ import annotations from crypto_wyckoff.domain_models import ( DecisionSignal, EngineResult, RiskLevel, WyckoffCycle, WyckoffEvent, WyckoffPhase, ) BULL_CYCLES = { WyckoffCycle.ACCUMULATION.value, WyckoffCycle.RE_ACCUMULATION.value, WyckoffCycle.MARKUP.value, } BEAR_CYCLES = { WyckoffCycle.DISTRIBUTION.value, WyckoffCycle.RE_DISTRIBUTION.value, WyckoffCycle.MARKDOWN.value, } class DecisionEngine: name = "Decision" version = "1.0.0" def run( self, monthly_cycle: EngineResult, weekly_cycle: EngineResult, weekly_phase: EngineResult, weekly_event: EngineResult, daily_event: EngineResult, daily_signal: EngineResult, ) -> EngineResult: m_cycle = monthly_cycle.payload.get("cycle", WyckoffCycle.UNKNOWN.value) w_cycle = weekly_cycle.payload.get("cycle", WyckoffCycle.UNKNOWN.value) w_phase = weekly_phase.payload.get("phase", WyckoffPhase.NONE.value) w_event = weekly_event.payload.get("current_event", WyckoffEvent.NONE.value) d_event = daily_event.payload.get("current_event", WyckoffEvent.NONE.value) trend_score = float(monthly_cycle.payload.get("trend_score", monthly_cycle.score)) structure_score = float(weekly_phase.payload.get("structure_score", weekly_phase.score)) entry_score = float(daily_event.payload.get("entry_score", daily_event.score)) overall_score = 0.30 * trend_score + 0.30 * structure_score + 0.40 * entry_score reasons: list[str] = [] warnings: list[str] = [] alignment = 50.0 m_bull = m_cycle in BULL_CYCLES m_bear = m_cycle in BEAR_CYCLES w_bull = w_cycle in BULL_CYCLES d_bullish_event = d_event in { WyckoffEvent.SPRING.value, WyckoffEvent.TEST.value, WyckoffEvent.SOS.value, WyckoffEvent.LPS.value, WyckoffEvent.JUMP.value, WyckoffEvent.BACKUP.value, } d_bearish_event = d_event in { WyckoffEvent.UTAD.value, WyckoffEvent.SOW.value, WyckoffEvent.LPSY.value, } # Alignment scoring if m_bull and w_bull and d_bullish_event: alignment = 92.0 reasons.append("✓ 月/周多头结构与日线多头事件一致") elif m_bull and d_bullish_event: alignment = 78.0 reasons.append("✓ 月线支持,日线有入场事件") if not w_bull: warnings.append("周线结构未完全确认") alignment -= 8 elif m_bear and d_bullish_event: alignment = 35.0 reasons.append("✗ 月线派发/下跌,日线弹簧可能只是反弹") elif m_bear and d_bearish_event: alignment = 85.0 reasons.append("✓ 空头多周期一致") else: alignment = 55.0 reasons.append("○ 多周期部分一致,需观察") if w_phase in (WyckoffPhase.D.value, WyckoffPhase.E.value) and m_bull: alignment = min(98.0, alignment + 6) reasons.append(f"✓ 周线阶段 {w_phase} 结构成熟({w_event})") active = daily_event.payload.get("active_events") or daily_event.payload.get("recent_events") or [] if d_event == WyckoffEvent.SPRING.value and len(active) >= 3: alignment = min(98.0, alignment + 4) reasons.append("✓ 日线多重事件同时确认") # Decision signal — hard gate on monthly bear + daily spring decision = DecisionSignal.WATCH.value risk = RiskLevel.MEDIUM.value if m_bear and d_event == WyckoffEvent.SPRING.value: decision = DecisionSignal.WATCH.value risk = RiskLevel.HIGH.value overall_score = min(overall_score, 55.0) reasons.append("→ 决策:观察(月线不支持,禁止追日线弹簧)") elif m_bear and d_bullish_event: decision = DecisionSignal.AVOID.value risk = RiskLevel.HIGH.value overall_score = min(overall_score, 48.0) reasons.append("→ 决策:回避(逆大周期多头事件)") elif ( m_bull and w_phase in (WyckoffPhase.D.value, WyckoffPhase.E.value, WyckoffPhase.C.value) and d_event in (WyckoffEvent.SPRING.value, WyckoffEvent.LPS.value, WyckoffEvent.SOS.value) and alignment >= 85 and overall_score >= 80 ): decision = DecisionSignal.STRONG_BUY.value risk = RiskLevel.LOW.value reasons.append("→ 决策:强烈买入(三级共振)") elif m_bull and d_bullish_event and overall_score >= 68 and alignment >= 70: decision = DecisionSignal.BUY.value risk = RiskLevel.LOW.value if alignment >= 80 else RiskLevel.MEDIUM.value reasons.append("→ 决策:买入") elif m_bear and d_bearish_event and overall_score >= 65: decision = DecisionSignal.SELL.value risk = RiskLevel.MEDIUM.value reasons.append("→ 决策:卖出") else: decision = DecisionSignal.WATCH.value reasons.append("→ 决策:观察") # Stars from score + alignment combo = 0.6 * overall_score + 0.4 * alignment if combo >= 90: stars = 5 elif combo >= 80: stars = 4 elif combo >= 65: stars = 3 elif combo >= 50: stars = 2 else: stars = 1 overall_confidence = ( 0.25 * monthly_cycle.confidence + 0.25 * weekly_phase.confidence + 0.25 * daily_event.confidence + 0.25 * daily_signal.confidence ) # Weak event pulls overall down if daily_event.confidence < 60: overall_confidence = min(overall_confidence, daily_event.confidence + 15) return EngineResult( name=self.name, version=self.version, confidence=overall_confidence, score=overall_score, reasons=reasons, warnings=warnings, metrics={ "trend_score": trend_score, "structure_score": structure_score, "entry_score": entry_score, "alignment": alignment, "stars": stars, }, payload={ "decision_signal": decision, "alignment": alignment, "stars": stars, "risk": risk, "overall_score": overall_score, "overall_confidence": overall_confidence, "trend_score": trend_score, "structure_score": structure_score, "entry_score": entry_score, "m_cycle": m_cycle, "w_cycle": w_cycle, "w_phase": w_phase, "w_event": w_event, "d_event": d_event, # Facts preserved — never overwritten "facts": { "monthly": {"cycle": m_cycle}, "weekly": {"cycle": w_cycle, "phase": w_phase, "event": w_event}, "daily": {"event": d_event}, }, }, )