fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新

自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-25 22:57:43 +08:00
co-authored by Cursor
parent 1e60ab3bfa
commit 8ee11317d3
104 changed files with 21452 additions and 4988 deletions
+195
View File
@@ -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},
},
},
)