移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。 Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""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(),
|
|
]
|