mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
feat(monitor): 新增板块异动监控
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user