Files
Chan/crypto_wyckoff/rules/cycle_rules.py
T
jackyu66gitandCursor ec08de098e feat(ECR-009): Crypto Wyckoff Screener 独立页(D/W/M)
移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:46:35 +08:00

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(),
]