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