Files
Chan/crypto_wyckoff/rules/event_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

255 lines
8.1 KiB
Python

"""Event rules: Spring/SOS/LPS/UTAD/SC/AR/ST/..."""
from __future__ import annotations
from typing import Any
from crypto_wyckoff.domain_models import WyckoffCycle, WyckoffEvent, WyckoffPhase
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
def _cycle(ctx: dict[str, Any]) -> str:
return (ctx.get("cycle") or {}).get("cycle") or ""
def _phase(ctx: dict[str, Any]) -> str:
return (ctx.get("phase") or {}).get("phase") or ""
class SpringRule(WyckoffRule):
rule_id = "event_spring"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
if cycle not in (WyckoffCycle.ACCUMULATION.value, WyckoffCycle.RE_ACCUMULATION.value,
WyckoffCycle.MARKUP.value):
# Allow spring only in accumulative contexts; Decision will filter MTF
if cycle == WyckoffCycle.DISTRIBUTION.value:
pass # still detect for facts but lower confidence
pierce = _f(context, "pierce_below_range")
reclaim = _f(context, "reclaim_speed")
vol_ratio = _f(context, "volume_ratio")
close_in_range = _f(context, "close_back_in_range")
if pierce >= 0.002 and close_in_range >= 0.5 and reclaim >= 0.3:
strength = min(98.0, 50 + pierce * 2000 + reclaim * 20 + (15 if vol_ratio < 1.2 else 5))
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SPRING.value,
confidence=strength,
score=strength,
reasons=[
f"跌破区间后收回 (pierce={pierce:.3%})",
f"回收速度={reclaim:.2f}",
f"量比={vol_ratio:.2f}",
],
metrics={"pierce": pierce, "reclaim": reclaim, "volume_ratio": vol_ratio},
)
return None
class TestRule(WyckoffRule):
rule_id = "event_test"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pos = _f(context, "range_position")
vol_ratio = _f(context, "volume_ratio")
near_low = pos < 0.2
if near_low and vol_ratio < 0.85:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.TEST.value,
confidence=68.0,
score=65.0,
reasons=["低位缩量回测"],
)
return None
class SOSRule(WyckoffRule):
rule_id = "event_sos"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
breakout = _f(context, "breakout_above_range")
vol_ratio = _f(context, "volume_ratio")
close = _f(context, "close")
ma20 = _f(context, "ma20")
if breakout >= 0.0 and vol_ratio >= 1.2 and close > ma20:
conf = min(95.0, 70 + vol_ratio * 8)
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SOS.value,
confidence=conf,
score=conf,
reasons=["放量突破区间上沿 (SOS)"],
metrics={"vol_ratio": vol_ratio},
)
return None
class LPSRule(WyckoffRule):
rule_id = "event_lps"
category = "event"
timeframes = ("1d", "1w")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
# Pullback hold above broken range / MA20 after prior strength
pullback = _f(context, "pullback_hold")
vol_ratio = _f(context, "volume_ratio")
above_ma = _f(context, "close") > _f(context, "ma20")
if pullback >= 0.5 and above_ma and vol_ratio <= 1.1:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.LPS.value,
confidence=74.0,
score=76.0,
reasons=["突破后缩量回踩支撑 (LPS)"],
)
return None
class SCRule(WyckoffRule):
rule_id = "event_sc"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
vol_ratio = _f(context, "volume_ratio")
bar_range = _f(context, "bar_range_atr")
pos = _f(context, "range_position")
if vol_ratio >= 1.8 and bar_range >= 1.5 and pos < 0.35:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.SC.value,
confidence=72.0,
score=70.0,
reasons=["低位放量宽幅,疑似 Selling Climax"],
)
return None
class ARRule(WyckoffRule):
rule_id = "event_ar"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
# Automatic rally: bounce from lows
bounce = _f(context, "bounce_from_low")
if bounce >= 0.04:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.AR.value,
confidence=65.0,
score=62.0,
reasons=["低点后自动反弹 (AR)"],
)
return None
class STRule(WyckoffRule):
rule_id = "event_st"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pos = _f(context, "range_position")
vol_ratio = _f(context, "volume_ratio")
if 0.15 < pos < 0.45 and vol_ratio < 1.0:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.ST.value,
confidence=60.0,
score=58.0,
reasons=["次级测试 (ST)"],
)
return None
class UTADRule(WyckoffRule):
rule_id = "event_utad"
category = "event"
timeframes = ("1w", "1d")
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
cycle = _cycle(context)
pierce_up = _f(context, "pierce_above_range")
fail = _f(context, "fail_back_into_range")
if cycle in (WyckoffCycle.DISTRIBUTION.value, WyckoffCycle.RE_DISTRIBUTION.value,
WyckoffCycle.MARKUP.value):
if pierce_up >= 0.002 and fail >= 0.5:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.UTAD.value,
confidence=76.0,
score=74.0,
reasons=["冲高失败回到区间 (UTAD)"],
)
return None
class JumpRule(WyckoffRule):
rule_id = "event_jump"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
gap = _f(context, "gap_up_pct")
vol_ratio = _f(context, "volume_ratio")
if gap >= 0.03 and vol_ratio >= 1.3:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.JUMP.value,
confidence=70.0,
score=72.0,
reasons=["放量向上跳跃 (Jump)"],
)
return None
class BackupRule(WyckoffRule):
rule_id = "event_backup"
category = "event"
timeframes = ("1d",)
def evaluate(self, context: dict[str, Any]) -> RuleHit | None:
pullback = _f(context, "pullback_hold")
after_jump = _f(context, "after_strength")
if after_jump >= 0.5 and pullback >= 0.5:
return RuleHit(
rule_id=self.rule_id,
event=WyckoffEvent.BACKUP.value,
confidence=68.0,
score=70.0,
reasons=["跳跃后回踩 (Backup)"],
)
return None
def build_rules() -> list[WyckoffRule]:
return [
SpringRule(),
UTADRule(),
SOSRule(),
LPSRule(),
SCRule(),
JumpRule(),
BackupRule(),
TestRule(),
ARRule(),
STRule(),
]