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:
@@ -0,0 +1,126 @@
|
||||
"""Cycle classification rules (monthly / weekly)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from crypto_wyckoff.domain_models import WyckoffCycle
|
||||
from crypto_wyckoff.rules.base import RuleHit, WyckoffRule
|
||||
|
||||
|
||||
def _f(ctx: dict[str, Any], key: str, default: float = 0.0) -> float:
|
||||
v = ctx.get("features", {}).get(key, default)
|
||||
try:
|
||||
return float(v) if v is not None else default
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
class MarkupCycleRule(WyckoffRule):
|
||||
rule_id = "cycle_markup"
|
||||
category = "cycle"
|
||||
timeframes = ("1M", "1w")
|
||||
|
||||
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
|
||||
close = _f(context, "close")
|
||||
ma20 = _f(context, "ma20")
|
||||
ma60 = _f(context, "ma60")
|
||||
ma120 = _f(context, "ma120")
|
||||
adx = _f(context, "adx")
|
||||
slope = _f(context, "ma60_slope")
|
||||
if close > ma20 > ma60 and (ma60 >= ma120 or slope > 0) and adx >= 18:
|
||||
conf = min(95.0, 55 + adx + (10 if close > ma120 else 0))
|
||||
return RuleHit(
|
||||
rule_id=self.rule_id,
|
||||
cycle=WyckoffCycle.MARKUP.value,
|
||||
confidence=conf,
|
||||
score=conf,
|
||||
reasons=["价格位于均线多头排列", f"ADX={adx:.1f}"],
|
||||
metrics={"adx": adx, "slope": slope},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class MarkdownCycleRule(WyckoffRule):
|
||||
rule_id = "cycle_markdown"
|
||||
category = "cycle"
|
||||
timeframes = ("1M", "1w")
|
||||
|
||||
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
|
||||
close = _f(context, "close")
|
||||
ma20 = _f(context, "ma20")
|
||||
ma60 = _f(context, "ma60")
|
||||
ma120 = _f(context, "ma120")
|
||||
adx = _f(context, "adx")
|
||||
slope = _f(context, "ma60_slope")
|
||||
if close < ma20 < ma60 and (ma60 <= ma120 or slope < 0) and adx >= 18:
|
||||
conf = min(95.0, 55 + adx + (10 if close < ma120 else 0))
|
||||
return RuleHit(
|
||||
rule_id=self.rule_id,
|
||||
cycle=WyckoffCycle.MARKDOWN.value,
|
||||
confidence=conf,
|
||||
score=conf,
|
||||
reasons=["价格位于均线空头排列", f"ADX={adx:.1f}"],
|
||||
metrics={"adx": adx},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class AccumulationCycleRule(WyckoffRule):
|
||||
rule_id = "cycle_accumulation"
|
||||
category = "cycle"
|
||||
timeframes = ("1M", "1w")
|
||||
|
||||
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
|
||||
adx = _f(context, "adx")
|
||||
range_pct = _f(context, "range_pct_60")
|
||||
close = _f(context, "close")
|
||||
ma120 = _f(context, "ma120")
|
||||
vol_trend = _f(context, "volume_trend")
|
||||
# Range-bound after decline: strictly at/below MA120 (mutually exclusive vs Distribution)
|
||||
if adx < 22 and range_pct < 0.28 and close <= ma120:
|
||||
conf = 60 + (10 if vol_trend > 0 else 0) + (10 if close < ma120 else 0)
|
||||
return RuleHit(
|
||||
rule_id=self.rule_id,
|
||||
cycle=WyckoffCycle.ACCUMULATION.value,
|
||||
confidence=min(90.0, conf),
|
||||
score=min(90.0, conf),
|
||||
reasons=["低趋势强度区间震荡", "疑似吸筹区间"],
|
||||
metrics={"adx": adx, "range_pct_60": range_pct},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class DistributionCycleRule(WyckoffRule):
|
||||
rule_id = "cycle_distribution"
|
||||
category = "cycle"
|
||||
timeframes = ("1M", "1w")
|
||||
|
||||
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
|
||||
adx = _f(context, "adx")
|
||||
range_pct = _f(context, "range_pct_60")
|
||||
close = _f(context, "close")
|
||||
ma120 = _f(context, "ma120")
|
||||
vol_trend = _f(context, "volume_trend")
|
||||
# Range-bound near highs: strictly above MA120 (mutually exclusive vs Accumulation)
|
||||
if adx < 22 and range_pct < 0.28 and close > ma120:
|
||||
conf = 60 + (10 if vol_trend < 0 else 0) + (10 if close > ma120 else 0)
|
||||
return RuleHit(
|
||||
rule_id=self.rule_id,
|
||||
cycle=WyckoffCycle.DISTRIBUTION.value,
|
||||
confidence=min(90.0, conf),
|
||||
score=min(90.0, conf),
|
||||
reasons=["高位低趋势震荡", "疑似派发区间"],
|
||||
metrics={"adx": adx, "range_pct_60": range_pct},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def build_rules() -> list[WyckoffRule]:
|
||||
# Order: trend cycles first (more decisive), then range cycles
|
||||
return [
|
||||
MarkupCycleRule(),
|
||||
MarkdownCycleRule(),
|
||||
AccumulationCycleRule(),
|
||||
DistributionCycleRule(),
|
||||
]
|
||||
Reference in New Issue
Block a user