tdxalert 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tdxalert-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mengyiheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxalert
3
+ Version: 0.1.0
4
+ Summary: 盘中预警推送引擎:可插拔数据源 + 基础规则集 + 微信推送,零依赖
5
+ Author: mengyiheng
6
+ License: MIT
7
+ Keywords: stock,alert,quant,wechat,monitor
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ <p align="center"><img src="assets/cover.png" alt="tdxalert" width="720"/></p>
20
+
21
+ # tdxalert — 盘中预警推送引擎
22
+
23
+ 把你的**全市场实时快照**接进来,按规则监控,触发即推送**微信**。
24
+
25
+ - 🔌 **数据源可插拔**:引擎只吃标准化的快照帧——你自己的采集器、pytdx、
26
+ [tdxreplay](https://gitee.com/mengyiheng/tdxreplay) 回放流,都能接
27
+ - 📏 **基础规则集**:价格上/下破、涨跌幅超阈、成交量异常放大、参考线上穿/下穿,
28
+ 每条规则独立冷却时间
29
+ - 💬 **微信推送**:Server酱 / PushPlus,另附钉钉/企微 webhook 与控制台渠道
30
+ - 🪶 **零第三方依赖**,纯 Python 标准库
31
+ - 🧪 规则引擎与推送解耦,方便先接回放源**离线验证预警逻辑**再上实盘
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ pip install tdxalert
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ### 1. 写配置 `alert.json`
42
+
43
+ ```json
44
+ {
45
+ "notifiers": { "serverchan": { "key": "SCTxxxxxxxx 你的SendKey" } },
46
+ "rules": [
47
+ { "type": "price_above", "code": "600519", "value": 1500, "cooldown": 600 },
48
+ { "type": "pct_below", "code": "*", "value": -3 },
49
+ { "type": "vol_spike", "code": "*", "window": 20, "ratio": 3 },
50
+ { "type": "change_dir", "code": "000001", "value": 10.5 }
51
+ ]
52
+ }
53
+ ```
54
+
55
+ ### 2. 把你的快照流接进来(每行一帧 JSON)
56
+
57
+ ```
58
+ {"600519": {"price": 1501.2, "pct": 1.2, "vol": 3200, "name": "贵州茅台"}, "000001": {...}}
59
+ ```
60
+
61
+ ```bash
62
+ your_snapshot_source | tdxalert run --config alert.json
63
+ ```
64
+
65
+ ### 3. 或者只用 Python API
66
+
67
+ ```python
68
+ from tdxalert import AlertEngine
69
+
70
+ engine = AlertEngine.from_config(cfg)
71
+ for frame in my_snapshot_loop(): # 你的 REA / 采集器
72
+ engine.feed(frame) # 触发即推微信
73
+ ```
74
+
75
+ ## 规则类型
76
+
77
+ | type | 含义 | 专属参数 |
78
+ |---|---|---|
79
+ | `price_above` / `price_below` | 价格上破 / 下破 | `value` 阈值 |
80
+ | `pct_above` / `pct_below` | 涨跌幅超阈(%) | `value` |
81
+ | `vol_spike` | 成交量较滚动均值放大 | `window` 窗口根数、`ratio` 倍数 |
82
+ | `change_dir` | 上穿 / 下穿参考线 | `value` 参考价 |
83
+
84
+ 通用参数:`code`(`"*"`=全市场)、`cooldown` 冷却秒数(默认 300)。
85
+
86
+ ## 快照帧字段约定
87
+
88
+ | 字段 | 必需 | 说明 |
89
+ |---|---|---|
90
+ | `price` | ✅ | 最新价 |
91
+ | `pct` | 建议 | 涨跌幅 %(正涨负跌) |
92
+ | `vol` | 建议 | 成交量(vol_spike 需要) |
93
+ | `name` | 可选 | 证券名称(出现在告警文案里) |
94
+
95
+ ## 提示
96
+
97
+ - **先离线验证再上实盘**:把 [tdxreplay](https://gitee.com/mengyiheng/tdxreplay)
98
+ 的回放流转成快照帧喂进来,历史数据上确认规则触发符合预期后再接实时源;
99
+ - Server酱 SendKey 获取:<https://sct.ftqq.com/>(微信扫码即用);
100
+ - 规则误报多就加大 `cooldown`;放量告警灵敏度高,`window` 建议不低于 10。
101
+
102
+ ## 免责声明
103
+
104
+ 本项目仅为个人研究工具,不构成任何投资建议;告警可能延迟、丢失或误报。
105
+ 使用前请[阅读完整免责声明](DISCLAIMER.md)。
106
+
107
+ ## License
108
+
109
+ MIT
110
+
111
+ ## 量化相关定制需求
112
+
113
+ 预警引擎恰恰是最值得按个人习惯定制的模块:
114
+
115
+ | 方向 | 示例 |
116
+ |---|---|
117
+ | 📏 规则扩展 | 接入你的策略条件、多规则组合、板块/概念联动预警 |
118
+ | 💬 渠道扩展 | Telegram/邮件/短信、按告警级别分流推送 |
119
+ | 🔌 源适配 | 把你现有的采集器/行情源接成标准快照帧 |
120
+
121
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxalert/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。规则类需求请附:监控对象、触发条件、期望推送方式。
@@ -0,0 +1,103 @@
1
+ <p align="center"><img src="assets/cover.png" alt="tdxalert" width="720"/></p>
2
+
3
+ # tdxalert — 盘中预警推送引擎
4
+
5
+ 把你的**全市场实时快照**接进来,按规则监控,触发即推送**微信**。
6
+
7
+ - 🔌 **数据源可插拔**:引擎只吃标准化的快照帧——你自己的采集器、pytdx、
8
+ [tdxreplay](https://gitee.com/mengyiheng/tdxreplay) 回放流,都能接
9
+ - 📏 **基础规则集**:价格上/下破、涨跌幅超阈、成交量异常放大、参考线上穿/下穿,
10
+ 每条规则独立冷却时间
11
+ - 💬 **微信推送**:Server酱 / PushPlus,另附钉钉/企微 webhook 与控制台渠道
12
+ - 🪶 **零第三方依赖**,纯 Python 标准库
13
+ - 🧪 规则引擎与推送解耦,方便先接回放源**离线验证预警逻辑**再上实盘
14
+
15
+ ## 安装
16
+
17
+ ```bash
18
+ pip install tdxalert
19
+ ```
20
+
21
+ ## 快速开始
22
+
23
+ ### 1. 写配置 `alert.json`
24
+
25
+ ```json
26
+ {
27
+ "notifiers": { "serverchan": { "key": "SCTxxxxxxxx 你的SendKey" } },
28
+ "rules": [
29
+ { "type": "price_above", "code": "600519", "value": 1500, "cooldown": 600 },
30
+ { "type": "pct_below", "code": "*", "value": -3 },
31
+ { "type": "vol_spike", "code": "*", "window": 20, "ratio": 3 },
32
+ { "type": "change_dir", "code": "000001", "value": 10.5 }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ ### 2. 把你的快照流接进来(每行一帧 JSON)
38
+
39
+ ```
40
+ {"600519": {"price": 1501.2, "pct": 1.2, "vol": 3200, "name": "贵州茅台"}, "000001": {...}}
41
+ ```
42
+
43
+ ```bash
44
+ your_snapshot_source | tdxalert run --config alert.json
45
+ ```
46
+
47
+ ### 3. 或者只用 Python API
48
+
49
+ ```python
50
+ from tdxalert import AlertEngine
51
+
52
+ engine = AlertEngine.from_config(cfg)
53
+ for frame in my_snapshot_loop(): # 你的 REA / 采集器
54
+ engine.feed(frame) # 触发即推微信
55
+ ```
56
+
57
+ ## 规则类型
58
+
59
+ | type | 含义 | 专属参数 |
60
+ |---|---|---|
61
+ | `price_above` / `price_below` | 价格上破 / 下破 | `value` 阈值 |
62
+ | `pct_above` / `pct_below` | 涨跌幅超阈(%) | `value` |
63
+ | `vol_spike` | 成交量较滚动均值放大 | `window` 窗口根数、`ratio` 倍数 |
64
+ | `change_dir` | 上穿 / 下穿参考线 | `value` 参考价 |
65
+
66
+ 通用参数:`code`(`"*"`=全市场)、`cooldown` 冷却秒数(默认 300)。
67
+
68
+ ## 快照帧字段约定
69
+
70
+ | 字段 | 必需 | 说明 |
71
+ |---|---|---|
72
+ | `price` | ✅ | 最新价 |
73
+ | `pct` | 建议 | 涨跌幅 %(正涨负跌) |
74
+ | `vol` | 建议 | 成交量(vol_spike 需要) |
75
+ | `name` | 可选 | 证券名称(出现在告警文案里) |
76
+
77
+ ## 提示
78
+
79
+ - **先离线验证再上实盘**:把 [tdxreplay](https://gitee.com/mengyiheng/tdxreplay)
80
+ 的回放流转成快照帧喂进来,历史数据上确认规则触发符合预期后再接实时源;
81
+ - Server酱 SendKey 获取:<https://sct.ftqq.com/>(微信扫码即用);
82
+ - 规则误报多就加大 `cooldown`;放量告警灵敏度高,`window` 建议不低于 10。
83
+
84
+ ## 免责声明
85
+
86
+ 本项目仅为个人研究工具,不构成任何投资建议;告警可能延迟、丢失或误报。
87
+ 使用前请[阅读完整免责声明](DISCLAIMER.md)。
88
+
89
+ ## License
90
+
91
+ MIT
92
+
93
+ ## 量化相关定制需求
94
+
95
+ 预警引擎恰恰是最值得按个人习惯定制的模块:
96
+
97
+ | 方向 | 示例 |
98
+ |---|---|
99
+ | 📏 规则扩展 | 接入你的策略条件、多规则组合、板块/概念联动预警 |
100
+ | 💬 渠道扩展 | Telegram/邮件/短信、按告警级别分流推送 |
101
+ | 🔌 源适配 | 把你现有的采集器/行情源接成标准快照帧 |
102
+
103
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxalert/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。规则类需求请附:监控对象、触发条件、期望推送方式。
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tdxalert"
7
+ version = "0.1.0"
8
+ description = "盘中预警推送引擎:可插拔数据源 + 基础规则集 + 微信推送,零依赖"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "mengyiheng" }]
13
+ keywords = ["stock", "alert", "quant", "wechat", "monitor"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ ]
22
+
23
+ [project.scripts]
24
+ tdxalert = "tdxalert.cli:main"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """tdxalert — 盘中预警推送引擎(数据源无关)。
2
+
3
+ 数据源可插拔:任何能产出 ``{code: {"price": .., "pct": .., "vol": ..}}``
4
+ 快照帧的代码都能接入,包括你自己的全市场采集器、pytdx、或 tdxreplay 回放流。
5
+
6
+ from tdxalert import AlertEngine
7
+
8
+ engine = AlertEngine.from_config({
9
+ "notifiers": {"serverchan": {"key": "SCTxxx"}},
10
+ "rules": [
11
+ {"type": "price_above", "code": "600519", "value": 1500},
12
+ {"type": "vol_spike", "code": "*", "window": 20, "ratio": 3},
13
+ ],
14
+ })
15
+ for frame in your_snapshot_loop(): # 你的 REA 全市场快照
16
+ engine.feed(frame) # 触发即推送微信
17
+ """
18
+
19
+ from .engine import AlertEngine
20
+ from .rules import Rule, evaluate, parse
21
+
22
+ __version__ = "0.1.0"
23
+ __all__ = ["AlertEngine", "Rule", "evaluate", "parse", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,53 @@
1
+ """tdxalert.cli — 命令行:读 JSONL 快照流,跑规则,推微信。
2
+
3
+ 用法:
4
+ tdxalert run --config alert.json --input quotes.jsonl # 快照文件/管道
5
+ your_snapshot_source | tdxalert run --config alert.jsonl # 接你自己的采集器
6
+
7
+ 快照帧格式(每行一个 JSON)::
8
+ {"600519": {"price": 1501.2, "pct": 1.2, "vol": 3200, "name": "贵州茅台"}, ...}
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ import time
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ p = argparse.ArgumentParser(prog="tdxalert", description="盘中预警推送引擎")
21
+ sub = p.add_subparsers(dest="cmd", required=True)
22
+ run = sub.add_parser("run", help="消费 JSONL 快照流并推送告警")
23
+ run.add_argument("--config", required=True, help="配置 JSON(规则 + 推送渠道)")
24
+ run.add_argument("--input", default="-", help="JSONL 快照流文件,默认 stdin")
25
+ run.add_argument("--interval", type=float, default=0.0,
26
+ help="每帧之间的模拟间隔秒数(回放测试用)")
27
+ a = p.parse_args(argv)
28
+
29
+ from .engine import AlertEngine
30
+
31
+ cfg = json.loads(open(a.config, encoding="utf-8-sig").read())
32
+ engine = AlertEngine.from_config(cfg)
33
+ print(f"[tdxalert] 规则 {len(engine.rules)} 条,渠道: {list(engine.notifiers)}")
34
+
35
+ stream = sys.stdin if a.input == "-" else open(a.input, encoding="utf-8")
36
+ n = 0
37
+ for line in stream:
38
+ line = line.strip().lstrip("\ufeff")
39
+ if not line:
40
+ continue
41
+ frame = json.loads(line)
42
+ alerts = engine.feed(frame)
43
+ for text in alerts:
44
+ print(f"[触发] {text}")
45
+ n += 1
46
+ if a.interval:
47
+ time.sleep(a.interval)
48
+ print(f"[tdxalert] 处理 {n} 帧,触发 {len(engine.sent)} 条告警")
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__":
53
+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1
+ """tdxalert.engine — 预警引擎:吃快照,吐告警。
2
+
3
+ 数据源无关:任何能产出 ``{code: {"price": float, "pct": float, "vol": float,
4
+ "name": str}}`` 快照的代码都能接入::
5
+
6
+ engine = AlertEngine.from_config(cfg)
7
+ for frame in your_snapshot_loop(): # 你的 REA 快照
8
+ for msg in engine.feed(frame):
9
+ ... # 已推送到配置的渠道
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, List
15
+
16
+ from .notify import build_notifiers
17
+ from .rules import Rule, evaluate, parse
18
+
19
+
20
+ class AlertEngine:
21
+ def __init__(self, rules: List[Rule], notifiers: Dict[str, Any] | None = None):
22
+ self.rules = rules
23
+ self.notifiers = build_notifiers(notifiers or {})
24
+ self.sent: List[str] = []
25
+
26
+ @classmethod
27
+ def from_config(cls, cfg: Dict[str, Any]) -> "AlertEngine":
28
+ rules = [parse(c) for c in cfg.get("rules", [])]
29
+ return cls(rules, cfg.get("notifiers", {}))
30
+
31
+ def feed(self, quotes: Dict[str, Dict[str, Any]]) -> List[str]:
32
+ """处理一帧全市场快照,返回触发的告警文本(同时推送渠道)。"""
33
+ alerts: List[str] = []
34
+ for rule in self.rules:
35
+ for code, q in quotes.items():
36
+ msg = evaluate(rule, code, q)
37
+ if msg:
38
+ name = q.get("name", "")
39
+ alerts.append(f"{name} {msg}" if name else msg)
40
+ if alerts:
41
+ for text in alerts:
42
+ self.sent.append(text)
43
+ for n in self.notifiers.values():
44
+ n.send(text)
45
+ return alerts
@@ -0,0 +1,78 @@
1
+ """tdxalert.notify — 推送渠道(可插拔,纯标准库实现)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.parse
7
+ import urllib.request
8
+ from typing import Any, Dict, Protocol
9
+
10
+
11
+ class Notifier(Protocol):
12
+ def send(self, text: str) -> None: ...
13
+
14
+
15
+ class ConsoleNotifier:
16
+ """本地控制台输出(默认兜底渠道)。"""
17
+
18
+ def send(self, text: str) -> None:
19
+ print(f"[tdxalert] {text}")
20
+
21
+
22
+ class ServerChanNotifier:
23
+ """Server酱·Turbo 版:https://sct.ftqq.com/ 微信服务号消息。"""
24
+
25
+ def __init__(self, key: str):
26
+ self.key = key
27
+
28
+ def send(self, text: str) -> None:
29
+ data = urllib.parse.urlencode({"title": text[:32], "desp": text}).encode()
30
+ urllib.request.urlopen(f"https://sctapi.ftqq.com/{self.key}.send",
31
+ data=data, timeout=10)
32
+
33
+
34
+ class PushPlusNotifier:
35
+ """PushPlus:https://www.pushplus.plus/ 微信公众号消息。"""
36
+
37
+ def __init__(self, token: str):
38
+ self.token = token
39
+
40
+ def send(self, text: str) -> None:
41
+ body = json.dumps({"token": self.token, "title": text[:64],
42
+ "content": text, "template": "txt"}).encode()
43
+ req = urllib.request.Request("https://www.pushplus.plus/send", data=body,
44
+ headers={"Content-Type": "application/json"})
45
+ urllib.request.urlopen(req, timeout=10)
46
+
47
+
48
+ class WebhookNotifier:
49
+ """通用 webhook(钉钉/企微机器人等 POST JSON 接口)。"""
50
+
51
+ def __init__(self, url: str):
52
+ self.url = url
53
+
54
+ def send(self, text: str) -> None:
55
+ body = json.dumps({"msgtype": "text", "text": {"content": text}}).encode()
56
+ req = urllib.request.Request(self.url, data=body,
57
+ headers={"Content-Type": "application/json"})
58
+ urllib.request.urlopen(req, timeout=10)
59
+
60
+
61
+ _BUILDERS = {
62
+ "console": lambda cfg: ConsoleNotifier(),
63
+ "serverchan": lambda cfg: ServerChanNotifier(cfg["key"]),
64
+ "pushplus": lambda cfg: PushPlusNotifier(cfg["token"]),
65
+ "webhook": lambda cfg: WebhookNotifier(cfg["url"]),
66
+ }
67
+
68
+
69
+ def build_notifiers(cfg: Dict[str, Any]) -> Dict[str, Notifier]:
70
+ out: Dict[str, Notifier] = {}
71
+ if not cfg:
72
+ out["console"] = ConsoleNotifier()
73
+ return out
74
+ for name, c in cfg.items():
75
+ if name not in _BUILDERS:
76
+ raise ValueError(f"未知推送渠道: {name}(支持 {list(_BUILDERS)})")
77
+ out[name] = _BUILDERS[name](c if isinstance(c, dict) else {"key": c})
78
+ return out
@@ -0,0 +1,104 @@
1
+ """tdxalert.rules — 基础预警规则集。
2
+
3
+ 规则在每帧快照上求值,返回告警消息(str)或 None。
4
+
5
+ 所有规则通用字段:
6
+ code 证券代码,"*" 表示全市场
7
+ cooldown 同一规则对同一代码的告警冷却秒数(默认 300)
8
+
9
+ 内置类型:
10
+ price_above / price_below 价格上破 / 下破阈值
11
+ pct_above / pct_below 涨跌幅超阈(%,正涨负跌)
12
+ vol_spike 成交量较滚动均值放大 ratio 倍
13
+ change_dir 方向反转(price 上穿/下穿 ref_price)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from typing import Any, Dict, List, Optional
21
+
22
+
23
+ @dataclass
24
+ class Rule:
25
+ type: str
26
+ code: str = "*"
27
+ value: float = 0.0
28
+ window: int = 20 # vol_spike 滚动窗口
29
+ ratio: float = 3.0 # vol_spike 放大倍数
30
+ cooldown: int = 300
31
+ name: str = ""
32
+
33
+ # 运行时状态
34
+ last_fired: Dict[str, str] = field(default_factory=dict) # code -> ISO 时间
35
+ vol_hist: Dict[str, List[float]] = field(default_factory=dict)
36
+
37
+ def matches(self, code: str) -> bool:
38
+ return self.code in ("*", code)
39
+
40
+ def _cooled(self, code: str) -> bool:
41
+ t = self.last_fired.get(code)
42
+ if not t:
43
+ return True
44
+ return (datetime.now() - datetime.fromisoformat(t)).total_seconds() >= self.cooldown
45
+
46
+ def _fire(self, code: str) -> None:
47
+ self.last_fired[code] = datetime.now().isoformat(timespec="seconds")
48
+
49
+
50
+ def parse(cfg: Dict[str, Any]) -> Rule:
51
+ known = {"type", "code", "value", "window", "ratio", "cooldown", "name"}
52
+ extra = set(cfg) - known
53
+ if extra:
54
+ raise ValueError(f"规则含未知字段: {extra}(规则: {cfg})")
55
+ r = Rule(type=cfg["type"], **{k: v for k, v in cfg.items() if k not in ("type", "name")})
56
+ r.name = cfg.get("name") or cfg["type"]
57
+ return r
58
+
59
+
60
+ def evaluate(rule: Rule, code: str, q: Dict[str, Any]) -> Optional[str]:
61
+ """对单只股票快照求值。q 至少含 price;pct/vol/amount 可选。"""
62
+ if not rule.matches(code) or not rule._cooled(code):
63
+ return None
64
+ price = q.get("price")
65
+ if price is None:
66
+ return None
67
+ msg: Optional[str] = None
68
+
69
+ if rule.type == "price_above" and price > rule.value:
70
+ msg = f"价格 {price} 上破 {rule.value}"
71
+ elif rule.type == "price_below" and price < rule.value:
72
+ msg = f"价格 {price} 下破 {rule.value}"
73
+ elif rule.type == "pct_above" and (q.get("pct") or 0) > rule.value:
74
+ msg = f"涨幅 {q['pct']}% > {rule.value}%"
75
+ elif rule.type == "pct_below" and (q.get("pct") or 0) < rule.value:
76
+ msg = f"跌幅 {q['pct']}% < {rule.value}%"
77
+ elif rule.type == "change_dir":
78
+ ref = rule.value
79
+ hist = rule.vol_hist.setdefault(code, [])
80
+ prev = hist[-1] if hist else None
81
+ hist.append(price)
82
+ if prev is not None:
83
+ if prev <= ref < price:
84
+ msg = f"价格上穿 {ref}({prev} -> {price})"
85
+ elif prev >= ref > price:
86
+ msg = f"价格下穿 {ref}({prev} -> {price})"
87
+ elif rule.type == "vol_spike":
88
+ vol = q.get("vol")
89
+ if vol:
90
+ h = rule.vol_hist.setdefault(code, [])
91
+ h.append(vol)
92
+ if len(h) > rule.window:
93
+ h.pop(0)
94
+ if len(h) >= max(3, rule.window // 2):
95
+ avg = sum(h[:-1]) / len(h[:-1])
96
+ if avg > 0 and vol >= avg * rule.ratio:
97
+ msg = f"成交量 {vol:.0f} 为前{len(h)-1}根均值 {avg:.0f} 的 {vol/avg:.1f} 倍"
98
+
99
+ if msg:
100
+ rule._fire(code)
101
+ if rule.type == "change_dir":
102
+ rule.vol_hist[code] = [price] # 重置基准,避免同一方向重复触发
103
+ return f"[{rule.name}] {code} {msg}(现价 {price})"
104
+ return None
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdxalert
3
+ Version: 0.1.0
4
+ Summary: 盘中预警推送引擎:可插拔数据源 + 基础规则集 + 微信推送,零依赖
5
+ Author: mengyiheng
6
+ License: MIT
7
+ Keywords: stock,alert,quant,wechat,monitor
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ <p align="center"><img src="assets/cover.png" alt="tdxalert" width="720"/></p>
20
+
21
+ # tdxalert — 盘中预警推送引擎
22
+
23
+ 把你的**全市场实时快照**接进来,按规则监控,触发即推送**微信**。
24
+
25
+ - 🔌 **数据源可插拔**:引擎只吃标准化的快照帧——你自己的采集器、pytdx、
26
+ [tdxreplay](https://gitee.com/mengyiheng/tdxreplay) 回放流,都能接
27
+ - 📏 **基础规则集**:价格上/下破、涨跌幅超阈、成交量异常放大、参考线上穿/下穿,
28
+ 每条规则独立冷却时间
29
+ - 💬 **微信推送**:Server酱 / PushPlus,另附钉钉/企微 webhook 与控制台渠道
30
+ - 🪶 **零第三方依赖**,纯 Python 标准库
31
+ - 🧪 规则引擎与推送解耦,方便先接回放源**离线验证预警逻辑**再上实盘
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ pip install tdxalert
37
+ ```
38
+
39
+ ## 快速开始
40
+
41
+ ### 1. 写配置 `alert.json`
42
+
43
+ ```json
44
+ {
45
+ "notifiers": { "serverchan": { "key": "SCTxxxxxxxx 你的SendKey" } },
46
+ "rules": [
47
+ { "type": "price_above", "code": "600519", "value": 1500, "cooldown": 600 },
48
+ { "type": "pct_below", "code": "*", "value": -3 },
49
+ { "type": "vol_spike", "code": "*", "window": 20, "ratio": 3 },
50
+ { "type": "change_dir", "code": "000001", "value": 10.5 }
51
+ ]
52
+ }
53
+ ```
54
+
55
+ ### 2. 把你的快照流接进来(每行一帧 JSON)
56
+
57
+ ```
58
+ {"600519": {"price": 1501.2, "pct": 1.2, "vol": 3200, "name": "贵州茅台"}, "000001": {...}}
59
+ ```
60
+
61
+ ```bash
62
+ your_snapshot_source | tdxalert run --config alert.json
63
+ ```
64
+
65
+ ### 3. 或者只用 Python API
66
+
67
+ ```python
68
+ from tdxalert import AlertEngine
69
+
70
+ engine = AlertEngine.from_config(cfg)
71
+ for frame in my_snapshot_loop(): # 你的 REA / 采集器
72
+ engine.feed(frame) # 触发即推微信
73
+ ```
74
+
75
+ ## 规则类型
76
+
77
+ | type | 含义 | 专属参数 |
78
+ |---|---|---|
79
+ | `price_above` / `price_below` | 价格上破 / 下破 | `value` 阈值 |
80
+ | `pct_above` / `pct_below` | 涨跌幅超阈(%) | `value` |
81
+ | `vol_spike` | 成交量较滚动均值放大 | `window` 窗口根数、`ratio` 倍数 |
82
+ | `change_dir` | 上穿 / 下穿参考线 | `value` 参考价 |
83
+
84
+ 通用参数:`code`(`"*"`=全市场)、`cooldown` 冷却秒数(默认 300)。
85
+
86
+ ## 快照帧字段约定
87
+
88
+ | 字段 | 必需 | 说明 |
89
+ |---|---|---|
90
+ | `price` | ✅ | 最新价 |
91
+ | `pct` | 建议 | 涨跌幅 %(正涨负跌) |
92
+ | `vol` | 建议 | 成交量(vol_spike 需要) |
93
+ | `name` | 可选 | 证券名称(出现在告警文案里) |
94
+
95
+ ## 提示
96
+
97
+ - **先离线验证再上实盘**:把 [tdxreplay](https://gitee.com/mengyiheng/tdxreplay)
98
+ 的回放流转成快照帧喂进来,历史数据上确认规则触发符合预期后再接实时源;
99
+ - Server酱 SendKey 获取:<https://sct.ftqq.com/>(微信扫码即用);
100
+ - 规则误报多就加大 `cooldown`;放量告警灵敏度高,`window` 建议不低于 10。
101
+
102
+ ## 免责声明
103
+
104
+ 本项目仅为个人研究工具,不构成任何投资建议;告警可能延迟、丢失或误报。
105
+ 使用前请[阅读完整免责声明](DISCLAIMER.md)。
106
+
107
+ ## License
108
+
109
+ MIT
110
+
111
+ ## 量化相关定制需求
112
+
113
+ 预警引擎恰恰是最值得按个人习惯定制的模块:
114
+
115
+ | 方向 | 示例 |
116
+ |---|---|
117
+ | 📏 规则扩展 | 接入你的策略条件、多规则组合、板块/概念联动预警 |
118
+ | 💬 渠道扩展 | Telegram/邮件/短信、按告警级别分流推送 |
119
+ | 🔌 源适配 | 把你现有的采集器/行情源接成标准快照帧 |
120
+
121
+ **联系合作**:[Gitee Issues](https://gitee.com/mengyiheng/tdxalert/issues) 或 [Gitee 私信](https://gitee.com/mengyiheng)。规则类需求请附:监控对象、触发条件、期望推送方式。
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/tdxalert/__init__.py
5
+ src/tdxalert/__main__.py
6
+ src/tdxalert/cli.py
7
+ src/tdxalert/engine.py
8
+ src/tdxalert/notify.py
9
+ src/tdxalert/rules.py
10
+ src/tdxalert.egg-info/PKG-INFO
11
+ src/tdxalert.egg-info/SOURCES.txt
12
+ src/tdxalert.egg-info/dependency_links.txt
13
+ src/tdxalert.egg-info/entry_points.txt
14
+ src/tdxalert.egg-info/top_level.txt
15
+ tests/test_alert.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tdxalert = tdxalert.cli:main
@@ -0,0 +1 @@
1
+ tdxalert
@@ -0,0 +1,63 @@
1
+ from datetime import datetime
2
+
3
+ import pytest
4
+
5
+ from tdxalert import AlertEngine, parse
6
+ from tdxalert.notify import ConsoleNotifier
7
+
8
+
9
+ def _engine(rules, sent):
10
+ e = AlertEngine(rules, {})
11
+ e.notifiers = {"console": ConsoleNotifier()}
12
+ e.sent = sent
13
+ return e
14
+
15
+
16
+ def test_price_threshold_and_cooldown():
17
+ cfg = [{"type": "price_above", "code": "600519", "value": 1500, "cooldown": 300}]
18
+ e = _engine([parse(c) for c in cfg], [])
19
+ q = {"600519": {"price": 1501, "name": "贵州茅台"}}
20
+ assert len(e.feed(q)) == 1
21
+ assert len(e.feed(q)) == 0 # 冷却期内不重复
22
+
23
+
24
+ def test_pct_and_vol_spike():
25
+ cfg = [
26
+ {"type": "pct_below", "code": "*", "value": -2},
27
+ {"type": "vol_spike", "code": "000001", "window": 4, "ratio": 3},
28
+ ]
29
+ e = _engine([parse(c) for c in cfg], [])
30
+ base = {"000001": {"price": 10, "pct": -3, "vol": 100}}
31
+ assert len(e.feed(base)) == 1 # 跌幅触发
32
+ for _ in range(3): # 平量若干根
33
+ e.feed({"000001": {"price": 10, "pct": 0, "vol": 100}})
34
+ alerts = e.feed({"000001": {"price": 10, "pct": 0, "vol": 500}})
35
+ assert any("成交量" in a for a in alerts) # 5 倍放量触发
36
+
37
+
38
+ def test_change_dir_resets_baseline():
39
+ e = _engine([parse({"type": "change_dir", "code": "600519", "value": 10, "cooldown": 0})], [])
40
+ e.feed({"600519": {"price": 9}})
41
+ assert len(e.feed({"600519": {"price": 11}})) == 1 # 上穿
42
+ assert len(e.feed({"600519": {"price": 11}})) == 0 # 不在同一方向重复
43
+ assert len(e.feed({"600519": {"price": 9}})) == 1 # 回落再次下穿
44
+
45
+
46
+ def test_wildcard_and_name_in_message():
47
+ e = _engine([parse({"type": "price_below", "code": "*", "value": 5})], [])
48
+ alerts = e.feed({"000001": {"price": 4.9, "name": "平安银行"}})
49
+ assert alerts and "平安银行" in alerts[0]
50
+
51
+
52
+ def test_config_load():
53
+ cfg = {
54
+ "notifiers": {"console": {}},
55
+ "rules": [{"type": "price_above", "code": "600519", "value": 1}],
56
+ }
57
+ e = AlertEngine.from_config(cfg)
58
+ assert len(e.rules) == 1 and "console" in e.notifiers
59
+
60
+
61
+ def test_unknown_rule_field_rejected():
62
+ with pytest.raises(ValueError):
63
+ parse({"type": "price_above", "code": "600519", "value": 1, "foo": 2})