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