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.
Files changed (50) hide show
  1. aquote_router-0.1.0/CHANGELOG.md +10 -0
  2. aquote_router-0.1.0/CODE_OF_CONDUCT.md +7 -0
  3. aquote_router-0.1.0/CONTRIBUTING.md +30 -0
  4. aquote_router-0.1.0/LICENSE +21 -0
  5. aquote_router-0.1.0/MANIFEST.in +12 -0
  6. aquote_router-0.1.0/PKG-INFO +190 -0
  7. aquote_router-0.1.0/README.md +158 -0
  8. aquote_router-0.1.0/SECURITY.md +7 -0
  9. aquote_router-0.1.0/aquote_router/__init__.py +8 -0
  10. aquote_router-0.1.0/aquote_router/adapters/__init__.py +11 -0
  11. aquote_router-0.1.0/aquote_router/adapters/base.py +88 -0
  12. aquote_router-0.1.0/aquote_router/adapters/easyquotation_sina_adapter.py +82 -0
  13. aquote_router-0.1.0/aquote_router/adapters/easyquotation_tencent_adapter.py +10 -0
  14. aquote_router-0.1.0/aquote_router/adapters/pytdx_adapter.py +153 -0
  15. aquote_router-0.1.0/aquote_router/audit.py +157 -0
  16. aquote_router-0.1.0/aquote_router/cli.py +158 -0
  17. aquote_router-0.1.0/aquote_router/exceptions.py +26 -0
  18. aquote_router-0.1.0/aquote_router/models.py +91 -0
  19. aquote_router-0.1.0/aquote_router/policy.py +182 -0
  20. aquote_router-0.1.0/aquote_router/router.py +302 -0
  21. aquote_router-0.1.0/aquote_router.egg-info/PKG-INFO +190 -0
  22. aquote_router-0.1.0/aquote_router.egg-info/SOURCES.txt +48 -0
  23. aquote_router-0.1.0/aquote_router.egg-info/dependency_links.txt +1 -0
  24. aquote_router-0.1.0/aquote_router.egg-info/entry_points.txt +2 -0
  25. aquote_router-0.1.0/aquote_router.egg-info/requires.txt +14 -0
  26. aquote_router-0.1.0/aquote_router.egg-info/top_level.txt +1 -0
  27. aquote_router-0.1.0/config/pytdx_servers.example.json +23 -0
  28. aquote_router-0.1.0/config/source_policy.example.yaml +26 -0
  29. aquote_router-0.1.0/docs/AUDIT_TRAIL.md +28 -0
  30. aquote_router-0.1.0/docs/COMPETITOR_NOTES.md +10 -0
  31. aquote_router-0.1.0/docs/FAQ.md +21 -0
  32. aquote_router-0.1.0/docs/RELEASE_CHECKLIST.md +34 -0
  33. aquote_router-0.1.0/docs/SOURCE_POLICY.md +30 -0
  34. aquote_router-0.1.0/examples/diagnose_demo.py +17 -0
  35. aquote_router-0.1.0/examples/full_realtime_quotes_demo.py +22 -0
  36. aquote_router-0.1.0/examples/index_realtime_demo.py +22 -0
  37. aquote_router-0.1.0/examples/minute_kline_pytdx_only_demo.py +22 -0
  38. aquote_router-0.1.0/examples/realtime_quotes_demo.py +22 -0
  39. aquote_router-0.1.0/pyproject.toml +63 -0
  40. aquote_router-0.1.0/scripts/check_release.py +110 -0
  41. aquote_router-0.1.0/scripts/smoke_test.py +48 -0
  42. aquote_router-0.1.0/scripts/windows_acceptance.bat +17 -0
  43. aquote_router-0.1.0/setup.cfg +4 -0
  44. aquote_router-0.1.0/tests/conftest.py +8 -0
  45. aquote_router-0.1.0/tests/test_audit_jsonl.py +60 -0
  46. aquote_router-0.1.0/tests/test_audit_sqlite.py +63 -0
  47. aquote_router-0.1.0/tests/test_models.py +20 -0
  48. aquote_router-0.1.0/tests/test_no_private_leak.py +48 -0
  49. aquote_router-0.1.0/tests/test_policy.py +38 -0
  50. 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,8 @@
1
+ """Public API for aquote-router."""
2
+
3
+ from .models import QuoteRecord
4
+ from .router import QuoteRouter
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["QuoteRecord", "QuoteRouter", "__version__"]
@@ -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"