feat(monitor): 新增板块异动监控

This commit is contained in:
shy3130
2026-08-06 19:28:39 +08:00
parent 99bdec875d
commit ecfddb451e
14 changed files with 1214 additions and 30 deletions
+37 -3
View File
@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
# ── 常量 ────────────────────────────────────────────────
ID_RE = re.compile(r"^[a-z0-9_]{1,40}$")
RULE_TYPES = {"strategy", "signal", "price", "market", "ladder"}
RULE_TYPES = {"strategy", "signal", "price", "market", "ladder", "sector"}
SCOPES = {"symbols", "all", "sector"}
LOGICS = {"and", "or"}
DIRECTIONS = {"entry", "exit", "both"}
@@ -38,6 +38,9 @@ OPS = {">", ">=", "<", "<=", "==", "!="}
LADDER_METRICS = {"sealed_vol", "sealed_amount"}
# ladder 规则: 方向 (up=涨停炸板预警, down=跌停翘板预警)
LADDER_DIRECTIONS = {"up", "down"}
SECTOR_KINDS = {"index", "concept", "industry"}
SECTOR_TRIGGERS = {"change_pct", "momentum"}
SECTOR_WINDOWS = {1, 3, 5, 10, 15}
# 布尔信号列前缀 (op=truth 时 field 取这些)
_SIGNAL_PREFIXES = ("signal_", "csg_")
@@ -138,6 +141,29 @@ def validate(rule: dict) -> None:
thr = rule.get("threshold")
if not isinstance(thr, (int, float)) or thr < 0:
raise ValueError("threshold 必须是非负数字 (封单 ≤ 此值时报警)")
elif rule.get("type") == "sector":
kind = rule.get("sector_kind")
if kind not in SECTOR_KINDS:
raise ValueError(f"sector_kind 必须是 {SECTOR_KINDS} 之一")
targets = rule.get("sector_targets")
if not isinstance(targets, list) or not targets:
raise ValueError("板块监控至少选择一个监控对象")
if len(targets) > 20:
raise ValueError("板块监控对象最多 20 个")
for target in targets:
if not isinstance(target, dict) or not target.get("key") or not target.get("name"):
raise ValueError("板块监控对象格式错误")
if target.get("kind") != kind:
raise ValueError("板块监控对象类型必须一致")
if rule.get("sector_trigger") not in SECTOR_TRIGGERS:
raise ValueError(f"sector_trigger 必须是 {SECTOR_TRIGGERS} 之一")
if rule.get("direction") not in LADDER_DIRECTIONS:
raise ValueError("板块监控 direction 必须是 up 或 down")
threshold_pct = rule.get("threshold_pct")
if not isinstance(threshold_pct, (int, float)) or not 0 < threshold_pct <= 20:
raise ValueError("板块监控阈值必须大于 0 且不超过 20%")
if rule.get("sector_trigger") == "momentum" and rule.get("window_minutes") not in SECTOR_WINDOWS:
raise ValueError(f"板块异动窗口必须是 {sorted(SECTOR_WINDOWS)} 分钟之一")
else:
# 信号/价格/市场类型: 需要 conditions
conds = rule.get("conditions")
@@ -196,9 +222,14 @@ def normalize(rule: dict) -> dict:
r.setdefault("scope", "symbols")
r.setdefault("symbols", [])
r.setdefault("sector", None)
r.setdefault("sector_kind", None)
r.setdefault("sector_targets", [])
r.setdefault("sector_trigger", "change_pct")
r.setdefault("threshold_pct", 1.0)
r.setdefault("window_minutes", 5)
r.setdefault("strategy_id", None)
# direction 默认值: ladder 用 "up", 其余用 "entry"
r.setdefault("direction", "up" if r.get("type") == "ladder" else "entry")
# direction 默认值: ladder/sector 用 "up", 其余用 "entry"
r.setdefault("direction", "up" if r.get("type") in {"ladder", "sector"} else "entry")
if r.get("type") == "strategy":
if r.get("notify_events") is None:
# 兼容统一监控上线后的旧规则: 当时实际行为是同时通知进入和移出。
@@ -211,6 +242,9 @@ def normalize(rule: dict) -> dict:
# ladder 专属默认字段
r.setdefault("metric", "sealed_vol")
r.setdefault("threshold", 0)
if r.get("type") == "sector":
r["scope"] = "all"
r["symbols"] = []
r.setdefault("logic", "and")
r.setdefault("cooldown_seconds", 3600)
r.setdefault("severity", "info")