aquote-router 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.
- aquote_router-0.1.0/CHANGELOG.md +10 -0
- aquote_router-0.1.0/CODE_OF_CONDUCT.md +7 -0
- aquote_router-0.1.0/CONTRIBUTING.md +30 -0
- aquote_router-0.1.0/LICENSE +21 -0
- aquote_router-0.1.0/MANIFEST.in +12 -0
- aquote_router-0.1.0/PKG-INFO +190 -0
- aquote_router-0.1.0/README.md +158 -0
- aquote_router-0.1.0/SECURITY.md +7 -0
- aquote_router-0.1.0/aquote_router/__init__.py +8 -0
- aquote_router-0.1.0/aquote_router/adapters/__init__.py +11 -0
- aquote_router-0.1.0/aquote_router/adapters/base.py +88 -0
- aquote_router-0.1.0/aquote_router/adapters/easyquotation_sina_adapter.py +82 -0
- aquote_router-0.1.0/aquote_router/adapters/easyquotation_tencent_adapter.py +10 -0
- aquote_router-0.1.0/aquote_router/adapters/pytdx_adapter.py +153 -0
- aquote_router-0.1.0/aquote_router/audit.py +157 -0
- aquote_router-0.1.0/aquote_router/cli.py +158 -0
- aquote_router-0.1.0/aquote_router/exceptions.py +26 -0
- aquote_router-0.1.0/aquote_router/models.py +91 -0
- aquote_router-0.1.0/aquote_router/policy.py +182 -0
- aquote_router-0.1.0/aquote_router/router.py +302 -0
- aquote_router-0.1.0/aquote_router.egg-info/PKG-INFO +190 -0
- aquote_router-0.1.0/aquote_router.egg-info/SOURCES.txt +48 -0
- aquote_router-0.1.0/aquote_router.egg-info/dependency_links.txt +1 -0
- aquote_router-0.1.0/aquote_router.egg-info/entry_points.txt +2 -0
- aquote_router-0.1.0/aquote_router.egg-info/requires.txt +14 -0
- aquote_router-0.1.0/aquote_router.egg-info/top_level.txt +1 -0
- aquote_router-0.1.0/config/pytdx_servers.example.json +23 -0
- aquote_router-0.1.0/config/source_policy.example.yaml +26 -0
- aquote_router-0.1.0/docs/AUDIT_TRAIL.md +28 -0
- aquote_router-0.1.0/docs/COMPETITOR_NOTES.md +10 -0
- aquote_router-0.1.0/docs/FAQ.md +21 -0
- aquote_router-0.1.0/docs/RELEASE_CHECKLIST.md +34 -0
- aquote_router-0.1.0/docs/SOURCE_POLICY.md +30 -0
- aquote_router-0.1.0/examples/diagnose_demo.py +17 -0
- aquote_router-0.1.0/examples/full_realtime_quotes_demo.py +22 -0
- aquote_router-0.1.0/examples/index_realtime_demo.py +22 -0
- aquote_router-0.1.0/examples/minute_kline_pytdx_only_demo.py +22 -0
- aquote_router-0.1.0/examples/realtime_quotes_demo.py +22 -0
- aquote_router-0.1.0/pyproject.toml +63 -0
- aquote_router-0.1.0/scripts/check_release.py +110 -0
- aquote_router-0.1.0/scripts/smoke_test.py +48 -0
- aquote_router-0.1.0/scripts/windows_acceptance.bat +17 -0
- aquote_router-0.1.0/setup.cfg +4 -0
- aquote_router-0.1.0/tests/conftest.py +8 -0
- aquote_router-0.1.0/tests/test_audit_jsonl.py +60 -0
- aquote_router-0.1.0/tests/test_audit_sqlite.py +63 -0
- aquote_router-0.1.0/tests/test_models.py +20 -0
- aquote_router-0.1.0/tests/test_no_private_leak.py +48 -0
- aquote_router-0.1.0/tests/test_policy.py +38 -0
- aquote_router-0.1.0/tests/test_router_fallback.py +192 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-06-14
|
|
4
|
+
|
|
5
|
+
- Initial open-source release.
|
|
6
|
+
- Added pytdx server pool routing with primary, hot backup and backup roles.
|
|
7
|
+
- Added easyquotation Sina and Tencent fallback for realtime APIs.
|
|
8
|
+
- Added pytdx-only minute kline routing.
|
|
9
|
+
- Added source policy, normalized quote model, JSONL audit and SQLite audit.
|
|
10
|
+
- Added CLI, examples, tests, GitHub Actions and release checklist.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
We follow the Contributor Covenant spirit: be respectful, constructive and focused on improving the project.
|
|
4
|
+
|
|
5
|
+
Harassment, discrimination, spam and abusive behavior are not acceptable in project spaces.
|
|
6
|
+
|
|
7
|
+
Maintainers may edit, hide or remove content that is hostile, off-topic or unsafe for public collaboration.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping improve aquote-router.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -X utf8 -m pip install -e ".[dev,test]"
|
|
9
|
+
python -X utf8 -m pytest -q
|
|
10
|
+
python -X utf8 scripts/check_release.py
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- Keep Python source files UTF-8.
|
|
16
|
+
- Use explicit `encoding="utf-8"` for text reads and writes.
|
|
17
|
+
- Default tests must not connect to live quote sources.
|
|
18
|
+
- Live smoke tests must require `ENABLE_LIVE_SMOKE_TEST=1`.
|
|
19
|
+
- Add adapter tests before adding a real data source.
|
|
20
|
+
- Preserve normalized fields and audit schema compatibility.
|
|
21
|
+
- Do not commit private configuration, credentials or local absolute paths.
|
|
22
|
+
|
|
23
|
+
## Pull Requests
|
|
24
|
+
|
|
25
|
+
Every pull request should include:
|
|
26
|
+
|
|
27
|
+
- What changed.
|
|
28
|
+
- Which tests ran.
|
|
29
|
+
- Whether source policy or audit schema changed.
|
|
30
|
+
- Any user-visible compatibility notes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aquote-router contributors
|
|
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,12 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CONTRIBUTING.md
|
|
5
|
+
include SECURITY.md
|
|
6
|
+
include CODE_OF_CONDUCT.md
|
|
7
|
+
include pyproject.toml
|
|
8
|
+
recursive-include config *.json *.yaml
|
|
9
|
+
recursive-include docs *.md
|
|
10
|
+
recursive-include examples *.py
|
|
11
|
+
recursive-include scripts *.py *.bat
|
|
12
|
+
recursive-include tests *.py
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aquote-router
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A-share quote source router with pytdx failover, easyquotation fallback and audit trail
|
|
5
|
+
Author: aquote-router contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aquote-router/aquote-router
|
|
8
|
+
Project-URL: Issues, https://github.com/aquote-router/aquote-router/issues
|
|
9
|
+
Keywords: a-share,quote,router,pytdx,easyquotation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: click>=8.1
|
|
21
|
+
Requires-Dist: easyquotation>=0.7
|
|
22
|
+
Requires-Dist: pytdx>=1.72
|
|
23
|
+
Requires-Dist: PyYAML>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# aquote-router
|
|
34
|
+
|
|
35
|
+
本项目不提供投资建议,不生成候选股池,不生成买卖点,不接入真实交易。
|
|
36
|
+
|
|
37
|
+
`aquote-router` 是一个面向 A 股量化研究的轻量行情源路由器,提供 pytdx 服务器池、主备切换、easyquotation 兜底、统一返回模型、source policy 和 JSONL / SQLite 审计追踪。
|
|
38
|
+
|
|
39
|
+
## 功能
|
|
40
|
+
|
|
41
|
+
- pytdx 服务器池读取,按 `primary -> hot_backup -> backup` 路由。
|
|
42
|
+
- 同级 pytdx 服务器按 `latency_ms` 升序选择。
|
|
43
|
+
- `realtime_quotes`、`full_realtime_quotes`、`index_realtime` 支持 pytdx 失败后切换到 `easyquotation_sina -> easyquotation_tencent`。
|
|
44
|
+
- `minute_kline` 保持 pytdx-only,所有 pytdx 源失败时抛出明确异常。
|
|
45
|
+
- 统一 `QuoteRecord` 返回模型,包含 `source`、`source_level`、`is_fallback`、`fallback_from`、`trace_id`。
|
|
46
|
+
- JSONL 和 SQLite 双审计,记录每次调用的来源尝试、耗时、结果和错误。
|
|
47
|
+
- 提供 Python API、CLI、示例、单元测试、CI 和发布工作流。
|
|
48
|
+
|
|
49
|
+
## 不做什么
|
|
50
|
+
|
|
51
|
+
- 不提供投资建议。
|
|
52
|
+
- 不生成候选股池。
|
|
53
|
+
- 不生成买卖点。
|
|
54
|
+
- 不接入真实交易。
|
|
55
|
+
- 不保存账号、登录态、密钥或 webhook。
|
|
56
|
+
- 不做行情 API 服务端或数据再分发服务。
|
|
57
|
+
|
|
58
|
+
## 安装
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -X utf8 -m pip install aquote-router
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
本地开发:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -X utf8 -m pip install -e ".[dev,test]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 快速开始
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from aquote_router import QuoteRouter
|
|
74
|
+
|
|
75
|
+
router = QuoteRouter.from_config(
|
|
76
|
+
pytdx_servers_path="config/pytdx_servers.example.json",
|
|
77
|
+
source_policy_path="config/source_policy.example.yaml",
|
|
78
|
+
audit_jsonl_path="logs/aquote_router_audit.jsonl",
|
|
79
|
+
audit_sqlite_path="logs/aquote_router_audit.sqlite3",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
records = router.realtime_quotes(["000001", "600000"])
|
|
83
|
+
for record in records:
|
|
84
|
+
print(record.to_dict())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
公开示例 pytdx 服务器可用性会变化,用户应维护自己的服务器池配置。不要把生产配置、账号信息或本地绝对路径提交到仓库。
|
|
88
|
+
|
|
89
|
+
## 公共 API
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
router.realtime_quotes(["000001", "600000"])
|
|
93
|
+
router.full_realtime_quotes(["000001", "600000"])
|
|
94
|
+
router.index_realtime(["000001", "399001"])
|
|
95
|
+
router.minute_kline("000001", period="1m")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## CLI
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
aquote-router diagnose
|
|
102
|
+
aquote-router realtime 000001 600000
|
|
103
|
+
aquote-router full-realtime 000001 600000
|
|
104
|
+
aquote-router index 000001 399001
|
|
105
|
+
aquote-router minute 000001 --period 1m
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
常用选项:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
aquote-router --json diagnose
|
|
112
|
+
aquote-router --config config/source_policy.example.yaml realtime 000001
|
|
113
|
+
aquote-router --audit-jsonl logs/audit.jsonl --audit-sqlite logs/audit.sqlite3 realtime 000001
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
失败时 CLI 返回非 0 exit code,并输出简洁错误信息。
|
|
117
|
+
|
|
118
|
+
## Source Policy
|
|
119
|
+
|
|
120
|
+
`config/source_policy.example.yaml` 定义每个 API 的来源顺序:
|
|
121
|
+
|
|
122
|
+
- `realtime_quotes`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
123
|
+
- `full_realtime_quotes`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
124
|
+
- `index_realtime`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
125
|
+
- `minute_kline`:`pytdx`
|
|
126
|
+
|
|
127
|
+
pytdx 内部会按 `primary -> hot_backup -> backup` 选择;同级按 `latency_ms` 升序。
|
|
128
|
+
|
|
129
|
+
## 审计
|
|
130
|
+
|
|
131
|
+
每次调用都会生成同一个 `trace_id`,并记录:
|
|
132
|
+
|
|
133
|
+
- `api_name`
|
|
134
|
+
- `symbols`
|
|
135
|
+
- `started_at` / `finished_at` / `duration_ms`
|
|
136
|
+
- `selected_source` / `selected_source_level`
|
|
137
|
+
- `attempts`
|
|
138
|
+
- `fallback_chain`
|
|
139
|
+
- `success`
|
|
140
|
+
- `error_type` / `error_message`
|
|
141
|
+
- `record_count`
|
|
142
|
+
|
|
143
|
+
详见 [docs/AUDIT_TRAIL.md](docs/AUDIT_TRAIL.md)。
|
|
144
|
+
|
|
145
|
+
## Fallback 示例
|
|
146
|
+
|
|
147
|
+
如果 `realtime_quotes` 的 pytdx primary 和 hot_backup 都失败,backup 成功,则记录:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
selected_source=pytdx
|
|
151
|
+
selected_source_level=backup
|
|
152
|
+
fallback_chain=["pytdx:primary", "pytdx:hot_backup"]
|
|
153
|
+
is_fallback=True
|
|
154
|
+
fallback_from=pytdx:hot_backup
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
如果所有 pytdx 源失败,路由继续尝试 `easyquotation_sina`,再尝试 `easyquotation_tencent`。
|
|
158
|
+
|
|
159
|
+
## 为什么 minute_kline 是 pytdx-only
|
|
160
|
+
|
|
161
|
+
分钟 K 线对字段语义、时间粒度和数据完整性更敏感。第一版只允许 pytdx 提供 `minute_kline`,避免跨来源混用导致不可解释的差异。所有 pytdx 源失败时,库会抛出结构化异常并写入审计记录,不会伪造数据。
|
|
162
|
+
|
|
163
|
+
## 与常见库的关系
|
|
164
|
+
|
|
165
|
+
- AKShare:覆盖面广,适合直接获取多类金融数据;本项目只做行情来源路由和审计。
|
|
166
|
+
- efinance:可作为用户侧数据工具;本项目第一版不内置为默认来源。
|
|
167
|
+
- easyquotation:本项目通过它作为 realtime 兜底来源。
|
|
168
|
+
- pytdx:本项目的主行情来源和分钟 K 线来源。
|
|
169
|
+
|
|
170
|
+
## 贡献
|
|
171
|
+
|
|
172
|
+
请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。新增真实数据源前,必须先提交 adapter 单元测试和字段标准化测试。默认测试不得联网,真实来源 smoke test 必须显式启用。
|
|
173
|
+
|
|
174
|
+
## Issue 反馈
|
|
175
|
+
|
|
176
|
+
请使用 GitHub issue 模板提交:
|
|
177
|
+
|
|
178
|
+
- Bug report
|
|
179
|
+
- Adapter request
|
|
180
|
+
- Data source failure
|
|
181
|
+
|
|
182
|
+
报告来源失败时,请提供 API 名称、symbol、时间、配置片段和审计 trace_id。请不要粘贴账号信息或私有配置。
|
|
183
|
+
|
|
184
|
+
## 风险免责声明
|
|
185
|
+
|
|
186
|
+
本项目仅用于研究基础设施。行情来源可能延迟、中断或返回异常字段。用户应自行校验数据质量,并遵守数据来源的服务条款和适用法规。
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# aquote-router
|
|
2
|
+
|
|
3
|
+
本项目不提供投资建议,不生成候选股池,不生成买卖点,不接入真实交易。
|
|
4
|
+
|
|
5
|
+
`aquote-router` 是一个面向 A 股量化研究的轻量行情源路由器,提供 pytdx 服务器池、主备切换、easyquotation 兜底、统一返回模型、source policy 和 JSONL / SQLite 审计追踪。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- pytdx 服务器池读取,按 `primary -> hot_backup -> backup` 路由。
|
|
10
|
+
- 同级 pytdx 服务器按 `latency_ms` 升序选择。
|
|
11
|
+
- `realtime_quotes`、`full_realtime_quotes`、`index_realtime` 支持 pytdx 失败后切换到 `easyquotation_sina -> easyquotation_tencent`。
|
|
12
|
+
- `minute_kline` 保持 pytdx-only,所有 pytdx 源失败时抛出明确异常。
|
|
13
|
+
- 统一 `QuoteRecord` 返回模型,包含 `source`、`source_level`、`is_fallback`、`fallback_from`、`trace_id`。
|
|
14
|
+
- JSONL 和 SQLite 双审计,记录每次调用的来源尝试、耗时、结果和错误。
|
|
15
|
+
- 提供 Python API、CLI、示例、单元测试、CI 和发布工作流。
|
|
16
|
+
|
|
17
|
+
## 不做什么
|
|
18
|
+
|
|
19
|
+
- 不提供投资建议。
|
|
20
|
+
- 不生成候选股池。
|
|
21
|
+
- 不生成买卖点。
|
|
22
|
+
- 不接入真实交易。
|
|
23
|
+
- 不保存账号、登录态、密钥或 webhook。
|
|
24
|
+
- 不做行情 API 服务端或数据再分发服务。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -X utf8 -m pip install aquote-router
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
本地开发:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -X utf8 -m pip install -e ".[dev,test]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from aquote_router import QuoteRouter
|
|
42
|
+
|
|
43
|
+
router = QuoteRouter.from_config(
|
|
44
|
+
pytdx_servers_path="config/pytdx_servers.example.json",
|
|
45
|
+
source_policy_path="config/source_policy.example.yaml",
|
|
46
|
+
audit_jsonl_path="logs/aquote_router_audit.jsonl",
|
|
47
|
+
audit_sqlite_path="logs/aquote_router_audit.sqlite3",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
records = router.realtime_quotes(["000001", "600000"])
|
|
51
|
+
for record in records:
|
|
52
|
+
print(record.to_dict())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
公开示例 pytdx 服务器可用性会变化,用户应维护自己的服务器池配置。不要把生产配置、账号信息或本地绝对路径提交到仓库。
|
|
56
|
+
|
|
57
|
+
## 公共 API
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
router.realtime_quotes(["000001", "600000"])
|
|
61
|
+
router.full_realtime_quotes(["000001", "600000"])
|
|
62
|
+
router.index_realtime(["000001", "399001"])
|
|
63
|
+
router.minute_kline("000001", period="1m")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
aquote-router diagnose
|
|
70
|
+
aquote-router realtime 000001 600000
|
|
71
|
+
aquote-router full-realtime 000001 600000
|
|
72
|
+
aquote-router index 000001 399001
|
|
73
|
+
aquote-router minute 000001 --period 1m
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
常用选项:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
aquote-router --json diagnose
|
|
80
|
+
aquote-router --config config/source_policy.example.yaml realtime 000001
|
|
81
|
+
aquote-router --audit-jsonl logs/audit.jsonl --audit-sqlite logs/audit.sqlite3 realtime 000001
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
失败时 CLI 返回非 0 exit code,并输出简洁错误信息。
|
|
85
|
+
|
|
86
|
+
## Source Policy
|
|
87
|
+
|
|
88
|
+
`config/source_policy.example.yaml` 定义每个 API 的来源顺序:
|
|
89
|
+
|
|
90
|
+
- `realtime_quotes`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
91
|
+
- `full_realtime_quotes`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
92
|
+
- `index_realtime`:`pytdx -> easyquotation_sina -> easyquotation_tencent`
|
|
93
|
+
- `minute_kline`:`pytdx`
|
|
94
|
+
|
|
95
|
+
pytdx 内部会按 `primary -> hot_backup -> backup` 选择;同级按 `latency_ms` 升序。
|
|
96
|
+
|
|
97
|
+
## 审计
|
|
98
|
+
|
|
99
|
+
每次调用都会生成同一个 `trace_id`,并记录:
|
|
100
|
+
|
|
101
|
+
- `api_name`
|
|
102
|
+
- `symbols`
|
|
103
|
+
- `started_at` / `finished_at` / `duration_ms`
|
|
104
|
+
- `selected_source` / `selected_source_level`
|
|
105
|
+
- `attempts`
|
|
106
|
+
- `fallback_chain`
|
|
107
|
+
- `success`
|
|
108
|
+
- `error_type` / `error_message`
|
|
109
|
+
- `record_count`
|
|
110
|
+
|
|
111
|
+
详见 [docs/AUDIT_TRAIL.md](docs/AUDIT_TRAIL.md)。
|
|
112
|
+
|
|
113
|
+
## Fallback 示例
|
|
114
|
+
|
|
115
|
+
如果 `realtime_quotes` 的 pytdx primary 和 hot_backup 都失败,backup 成功,则记录:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
selected_source=pytdx
|
|
119
|
+
selected_source_level=backup
|
|
120
|
+
fallback_chain=["pytdx:primary", "pytdx:hot_backup"]
|
|
121
|
+
is_fallback=True
|
|
122
|
+
fallback_from=pytdx:hot_backup
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
如果所有 pytdx 源失败,路由继续尝试 `easyquotation_sina`,再尝试 `easyquotation_tencent`。
|
|
126
|
+
|
|
127
|
+
## 为什么 minute_kline 是 pytdx-only
|
|
128
|
+
|
|
129
|
+
分钟 K 线对字段语义、时间粒度和数据完整性更敏感。第一版只允许 pytdx 提供 `minute_kline`,避免跨来源混用导致不可解释的差异。所有 pytdx 源失败时,库会抛出结构化异常并写入审计记录,不会伪造数据。
|
|
130
|
+
|
|
131
|
+
## 与常见库的关系
|
|
132
|
+
|
|
133
|
+
- AKShare:覆盖面广,适合直接获取多类金融数据;本项目只做行情来源路由和审计。
|
|
134
|
+
- efinance:可作为用户侧数据工具;本项目第一版不内置为默认来源。
|
|
135
|
+
- easyquotation:本项目通过它作为 realtime 兜底来源。
|
|
136
|
+
- pytdx:本项目的主行情来源和分钟 K 线来源。
|
|
137
|
+
|
|
138
|
+
## 贡献
|
|
139
|
+
|
|
140
|
+
请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。新增真实数据源前,必须先提交 adapter 单元测试和字段标准化测试。默认测试不得联网,真实来源 smoke test 必须显式启用。
|
|
141
|
+
|
|
142
|
+
## Issue 反馈
|
|
143
|
+
|
|
144
|
+
请使用 GitHub issue 模板提交:
|
|
145
|
+
|
|
146
|
+
- Bug report
|
|
147
|
+
- Adapter request
|
|
148
|
+
- Data source failure
|
|
149
|
+
|
|
150
|
+
报告来源失败时,请提供 API 名称、symbol、时间、配置片段和审计 trace_id。请不要粘贴账号信息或私有配置。
|
|
151
|
+
|
|
152
|
+
## 风险免责声明
|
|
153
|
+
|
|
154
|
+
本项目仅用于研究基础设施。行情来源可能延迟、中断或返回异常字段。用户应自行校验数据质量,并遵守数据来源的服务条款和适用法规。
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Please report security concerns through GitHub private vulnerability reporting when available, or by contacting the maintainers through the repository owner profile.
|
|
4
|
+
|
|
5
|
+
Do not paste credentials, private configuration, personal account details, or full local paths into public issues.
|
|
6
|
+
|
|
7
|
+
This project is a research infrastructure library. It does not connect to personal accounts and does not execute orders.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Quote source adapters."""
|
|
2
|
+
|
|
3
|
+
from .easyquotation_sina_adapter import EasyQuotationSinaAdapter
|
|
4
|
+
from .easyquotation_tencent_adapter import EasyQuotationTencentAdapter
|
|
5
|
+
from .pytdx_adapter import PytdxAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"EasyQuotationSinaAdapter",
|
|
9
|
+
"EasyQuotationTencentAdapter",
|
|
10
|
+
"PytdxAdapter",
|
|
11
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Base adapter contracts and shared helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from aquote_router.exceptions import AdapterError
|
|
9
|
+
from aquote_router.models import QuoteRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class PytdxServer:
|
|
14
|
+
"""One pytdx server configuration entry."""
|
|
15
|
+
|
|
16
|
+
host: str
|
|
17
|
+
port: int
|
|
18
|
+
role: str
|
|
19
|
+
latency_ms: int
|
|
20
|
+
enabled: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseQuoteAdapter:
|
|
24
|
+
"""Synchronous quote adapter interface."""
|
|
25
|
+
|
|
26
|
+
source: str = "unknown"
|
|
27
|
+
source_level: str | None = None
|
|
28
|
+
|
|
29
|
+
def realtime_quotes(
|
|
30
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
31
|
+
) -> list[QuoteRecord]:
|
|
32
|
+
raise AdapterError(f"{self.source} does not support realtime_quotes")
|
|
33
|
+
|
|
34
|
+
def full_realtime_quotes(
|
|
35
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
36
|
+
) -> list[QuoteRecord]:
|
|
37
|
+
return self.realtime_quotes(symbols, include_raw=include_raw)
|
|
38
|
+
|
|
39
|
+
def index_realtime(
|
|
40
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
41
|
+
) -> list[QuoteRecord]:
|
|
42
|
+
return self.realtime_quotes(symbols, include_raw=include_raw)
|
|
43
|
+
|
|
44
|
+
def minute_kline(
|
|
45
|
+
self,
|
|
46
|
+
symbol: str,
|
|
47
|
+
*,
|
|
48
|
+
period: str = "1m",
|
|
49
|
+
count: int = 240,
|
|
50
|
+
include_raw: bool = False,
|
|
51
|
+
) -> list[QuoteRecord]:
|
|
52
|
+
raise AdapterError(f"{self.source} does not support minute_kline")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def as_float(value: Any) -> float | None:
|
|
56
|
+
"""Best-effort conversion to float with empty values mapped to None."""
|
|
57
|
+
|
|
58
|
+
if value is None or value == "":
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
return float(value)
|
|
62
|
+
except (TypeError, ValueError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def first_value(row: dict[str, Any], keys: tuple[str, ...]) -> Any:
|
|
67
|
+
"""Return the first present value for a tuple of candidate keys."""
|
|
68
|
+
|
|
69
|
+
for key in keys:
|
|
70
|
+
if key in row and row[key] not in (None, ""):
|
|
71
|
+
return row[key]
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def market_for_symbol(symbol: str) -> int:
|
|
76
|
+
"""Return pytdx market id for a common A-share symbol."""
|
|
77
|
+
|
|
78
|
+
if symbol.startswith(("5", "6", "9")):
|
|
79
|
+
return 1
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def source_id(source: str, source_level: str | None) -> str:
|
|
84
|
+
"""Return a compact source identifier for fallback chains."""
|
|
85
|
+
|
|
86
|
+
if source_level:
|
|
87
|
+
return f"{source}:{source_level}"
|
|
88
|
+
return source
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""easyquotation Sina adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aquote_router.adapters.base import BaseQuoteAdapter, as_float, first_value
|
|
8
|
+
from aquote_router.exceptions import AdapterError, SourceUnavailableError
|
|
9
|
+
from aquote_router.models import QuoteRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EasyQuotationSinaAdapter(BaseQuoteAdapter):
|
|
13
|
+
"""Adapter for easyquotation's Sina provider."""
|
|
14
|
+
|
|
15
|
+
source = "easyquotation_sina"
|
|
16
|
+
provider = "sina"
|
|
17
|
+
|
|
18
|
+
def realtime_quotes(
|
|
19
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
20
|
+
) -> list[QuoteRecord]:
|
|
21
|
+
return self._stocks(symbols, include_raw=include_raw)
|
|
22
|
+
|
|
23
|
+
def full_realtime_quotes(
|
|
24
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
25
|
+
) -> list[QuoteRecord]:
|
|
26
|
+
return self._stocks(symbols, include_raw=include_raw)
|
|
27
|
+
|
|
28
|
+
def index_realtime(
|
|
29
|
+
self, symbols: list[str], *, include_raw: bool = False
|
|
30
|
+
) -> list[QuoteRecord]:
|
|
31
|
+
return self._stocks(symbols, include_raw=include_raw)
|
|
32
|
+
|
|
33
|
+
def _stocks(self, symbols: list[str], *, include_raw: bool) -> list[QuoteRecord]:
|
|
34
|
+
if not symbols:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
quotation = self._quotation()
|
|
38
|
+
data = quotation.stocks(symbols)
|
|
39
|
+
if not data:
|
|
40
|
+
raise SourceUnavailableError(f"{self.source} returned no quote records")
|
|
41
|
+
|
|
42
|
+
records: list[QuoteRecord] = []
|
|
43
|
+
for symbol in symbols:
|
|
44
|
+
row = data.get(symbol) or data.get(symbol.lower()) or data.get(symbol.upper())
|
|
45
|
+
if row:
|
|
46
|
+
records.append(self._normalize(symbol, row, include_raw=include_raw))
|
|
47
|
+
if not records:
|
|
48
|
+
raise SourceUnavailableError(f"{self.source} returned no requested symbols")
|
|
49
|
+
return records
|
|
50
|
+
|
|
51
|
+
def _quotation(self) -> Any:
|
|
52
|
+
try:
|
|
53
|
+
import easyquotation
|
|
54
|
+
except Exception as exc: # pragma: no cover - depends on user env
|
|
55
|
+
raise AdapterError("easyquotation package is not available") from exc
|
|
56
|
+
return easyquotation.use(self.provider)
|
|
57
|
+
|
|
58
|
+
def _normalize(
|
|
59
|
+
self, symbol: str, row: dict[str, Any], *, include_raw: bool
|
|
60
|
+
) -> QuoteRecord:
|
|
61
|
+
date_value = first_value(row, ("date",))
|
|
62
|
+
time_value = first_value(row, ("time",))
|
|
63
|
+
if date_value and time_value:
|
|
64
|
+
dt_value = f"{date_value} {time_value}"
|
|
65
|
+
else:
|
|
66
|
+
dt_value = str(first_value(row, ("datetime", "time")) or "") or None
|
|
67
|
+
|
|
68
|
+
return QuoteRecord(
|
|
69
|
+
symbol=symbol,
|
|
70
|
+
name=first_value(row, ("name",)),
|
|
71
|
+
price=as_float(first_value(row, ("now", "price", "close"))),
|
|
72
|
+
open=as_float(first_value(row, ("open",))),
|
|
73
|
+
high=as_float(first_value(row, ("high",))),
|
|
74
|
+
low=as_float(first_value(row, ("low",))),
|
|
75
|
+
pre_close=as_float(first_value(row, ("close", "pre_close", "last_close"))),
|
|
76
|
+
volume=as_float(first_value(row, ("volume", "vol"))),
|
|
77
|
+
amount=as_float(first_value(row, ("turnover", "amount"))),
|
|
78
|
+
datetime=dt_value,
|
|
79
|
+
source=self.source,
|
|
80
|
+
source_level=self.source_level,
|
|
81
|
+
raw=dict(row) if include_raw else None,
|
|
82
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""easyquotation Tencent adapter."""
|
|
2
|
+
|
|
3
|
+
from aquote_router.adapters.easyquotation_sina_adapter import EasyQuotationSinaAdapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EasyQuotationTencentAdapter(EasyQuotationSinaAdapter):
|
|
7
|
+
"""Adapter for easyquotation's Tencent provider."""
|
|
8
|
+
|
|
9
|
+
source = "easyquotation_tencent"
|
|
10
|
+
provider = "tencent"
|