"""Cycle Engine — monthly/weekly macro cycle via Rule Registry.""" from __future__ import annotations from crypto_wyckoff.domain_models import EngineResult, WyckoffCycle from crypto_wyckoff.rules.base import RuleHit from crypto_wyckoff.rules.registry import rule_registry def _resolve_range_conflict(hits: list[RuleHit], features: dict) -> list[RuleHit]: """Accumulation vs Distribution overlap → mutually exclusive by MA120 position.""" accum = [h for h in hits if h.cycle == WyckoffCycle.ACCUMULATION.value] dist = [h for h in hits if h.cycle == WyckoffCycle.DISTRIBUTION.value] if not (accum and dist): return hits close = float(features.get("close") or 0) ma120 = float(features.get("ma120") or close) or close others = [ h for h in hits if h.cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.DISTRIBUTION.value) ] # Below MA120 → accumulation; above → distribution; equal band uses relative position if close < ma120 * 0.995: return others + accum if close > ma120 * 1.005: return others + dist # Tight band: keep higher confidence only best_a = max(accum, key=lambda h: h.confidence) best_d = max(dist, key=lambda h: h.confidence) return others + ([best_a] if best_a.confidence >= best_d.confidence else [best_d]) class CycleEngine: name = "Cycle" version = "1.0.0" def run(self, feature: EngineResult, timeframe: str) -> EngineResult: features = feature.payload if features.get("insufficient"): return EngineResult( name=self.name, version=self.version, confidence=15.0, score=40.0, reasons=[f"{timeframe} 数据不足,Cycle=Unknown"], warnings=["insufficient_features"], payload={ "cycle": WyckoffCycle.UNKNOWN.value, "timeframe": timeframe, "trend_score": 40.0, }, ) context = {"features": features, "timeframe": timeframe} hits: list[RuleHit] = [] for rule in rule_registry.by_category("cycle", timeframe): hit = rule.evaluate(context) if hit and hit.cycle: hits.append(hit) hits = _resolve_range_conflict(hits, features) if not hits: return EngineResult( name=self.name, version=self.version, confidence=30.0, score=40.0, reasons=["无匹配周期规则,标记 Unknown"], payload={ "cycle": WyckoffCycle.UNKNOWN.value, "timeframe": timeframe, "trend_score": 40.0, }, ) best = max(hits, key=lambda h: h.confidence) trend_score = best.score if best.cycle == WyckoffCycle.MARKUP.value: trend_score = max(trend_score, 75.0) elif best.cycle == WyckoffCycle.ACCUMULATION.value: trend_score = max(60.0, trend_score * 0.9) elif best.cycle == WyckoffCycle.DISTRIBUTION.value: trend_score = min(45.0, 100 - trend_score * 0.5) elif best.cycle == WyckoffCycle.MARKDOWN.value: trend_score = min(30.0, 100 - trend_score) return EngineResult( name=self.name, version=self.version, confidence=best.confidence, score=trend_score, reasons=best.reasons, metrics=best.metrics, payload={ "cycle": best.cycle, "timeframe": timeframe, "rule_id": best.rule_id, "trend_score": trend_score, }, )