quotekit 0.6.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.
quotekit-0.6.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 haifeng
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,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: quotekit
3
+ Version: 0.6.0
4
+ Summary: A股/北交所/场内ETF统一行情工具箱: mootdx(默认,多线程) + easyquotation(新浪/腾讯回退), 统一DataFrame返回, 实时行情/K线(8频度)/股票列表
5
+ Author-email: haifeng <chenhaifeng@touguyun.cn>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit
8
+ Project-URL: Repository, https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit
9
+ Keywords: quant,finance,a-share,stock,etf,index,quotes,kline,mootdx,easyquotation,tongdaxin
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pandas
26
+ Requires-Dist: mootdx
27
+ Requires-Dist: easyquotation
28
+ Requires-Dist: pypinyin
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # quotekit — A股/北交所/场内ETF 统一行情工具箱
34
+
35
+ 一个出口拉取 A 股个股(含北交所)、**场内 ETF** 与指数的实时行情和 K 线,统一返回 pandas DataFrame。
36
+
37
+ - **标的覆盖**:沪深 A 股 + 北交所(43/83/87/92 开头)+ 场内 ETF(沪 50/51/52/56/58、深 15/16 开头)+ 股指指数
38
+ - **默认源 mootdx**(通达信协议,32 线程并发,快、带五档盘口)
39
+ - **自动回退**:mootdx 失败/缺失的代码(含北交所、休市场景)自动用 easyquotation 新浪源补,仍缺失再用腾讯源
40
+ - **休市可用**:休市时返回最后交易日收盘快照(自动落到 eq 源,属设计行为)
41
+
42
+ ## 安装
43
+
44
+ ```bash
45
+ pip install -e . # 开发安装
46
+ pip install build && python -m build # 打包: dist/quotekit-0.6.0-py3-none-any.whl
47
+ pip install dist/quotekit-0.6.0-py3-none-any.whl
48
+ ```
49
+
50
+ 依赖:`pandas`、`mootdx`、`easyquotation`、`pypinyin`(见 pyproject.toml)。
51
+
52
+ ## 快速上手
53
+
54
+ ```python
55
+ from quotekit import get_quotes, get_index_quotes, configure
56
+
57
+ # 个股: 三种代码写法混用均可
58
+ df = get_quotes(['600519', '000001.SZ', 'sh600000', '920099'])
59
+ print(df[['symbol', 'price', 'open', 'high', 'low', 'volume', 'amount', 'time']])
60
+
61
+ # 指数: 000001上证 399001深成 399006创业板指 000300沪深300
62
+ idx = get_index_quotes(['000001', '399001', '000300'])
63
+
64
+ # 场内ETF: 与个股同一接口, 支持实时快照(含五档)和K线
65
+ etf = get_quotes(['510300', '510050', '588000', '159915', '511990'])
66
+ etf_bars = get_bars('510300', freq='day', sd='2026-01-01') # ETF日K
67
+
68
+ # 交易所 A 股列表(symbol 格式 000001.SZ): 'all' | 'SH' | 'SZ' | 'BJ'
69
+ lst = get_stock_list() # 全市场 5400+ 只, 含名称
70
+ lst = get_stock_list('BJ') # 仅北交所
71
+ lst = get_stock_list(refresh=True) # 强制绕过缓存重拉
72
+
73
+ # K线: 单只或多只, 频度 1m/5m/15m/30m/60m/day/week/month, 支持复权与日期区间
74
+ bars = get_bars('600519', freq='day', limit=120) # 最近120根
75
+ bars = get_bars(['600519', '000001.SZ'], freq='30m', limit=100) # 多只30分钟K
76
+ bars = get_bars('600519', freq='day', limit=800, adjust='qfq') # 前复权
77
+ bars = get_bars('600519', freq='day', sd='2024-01-01') # 日期区间(优先于limit)
78
+ bars = get_bars('600519', freq='day', sd='2026-01-01', ed='2026-03-31')
79
+ bars = get_bars('000001.SZ', freq='day', ed='2025-12-31') # ed之前全部历史
80
+ bars = get_bars('920099', freq='week', limit=52) # 北交所周K
81
+
82
+ # 搜索: 股票/ETF/指数, 支持代码/中文名/拼音首字母/全拼
83
+ search('茅台') # 中文名 -> 600519
84
+ search('mt') # 拼音首字母
85
+ search('maotai') # 全拼
86
+ search('000001') # 同时命中 上证指数 + 平安银行
87
+ search('hs300', kind='etf') # kind: 'all'|'stock'|'etf'|'index'
88
+
89
+ # 强制指定源 / 临时改线程数
90
+ df = get_quotes(['600519'], source='tencent')
91
+ df = get_quotes(all_codes, workers=16)
92
+
93
+ # 全局配置(线程数/超时/批量, 批量上限60)
94
+ configure(workers=32, timeout=10, batch=60)
95
+ ```
96
+
97
+ ## 返回 schema
98
+
99
+ 个股/ETF 29 列 / 指数 9 列,`symbol` 格式 `600519.SH` / `000001.SZ` / `920099.BJ` / `510300.SH`。
100
+
101
+ | 列 | 类型 | 说明 |
102
+ |---|---|---|
103
+ | code | str | 6 位纯代码 |
104
+ | symbol | str | 带市场后缀 |
105
+ | price | float | 现价 |
106
+ | open / high / low | float | 今开 / 最高 / 最低 |
107
+ | volume | int64 | 成交量(股票=股、ETF=份;mootdx 源手×100) |
108
+ | amount | float | 成交额(元) |
109
+ | time | str | `YYYY-MM-DD HH:MM:SS` |
110
+ | bid1..5 / ask1..5 | float | 五档价(个股与 ETF) |
111
+ | bid_vol1..5 / ask_vol1..5 | int64 | 五档量(个股与 ETF) |
112
+
113
+ 场内 ETF 实测说明(2026-09-30 快照验证):实时与 K 线与个股完全同接口;成交量口径为**份**,
114
+ 经"金额÷量≈现价"自检通过(510300/510050/588000/159915/511990 五只全部吻合);日K收盘与量
115
+ 和实时快照精确一致;mootdx 主路径交易日生效(TDX 协议 ETF 与股票同接口)。
116
+
117
+ `get_stock_list(market)` 返回 4 列:`code`(6 位)、`symbol`(`000001.SZ` 格式)、`name`、`market`(SH/SZ/BJ)。
118
+ SH/SZ 来自 mootdx 实时列表(含名称,休市可用),A 股口径前缀:SH 60/68、SZ 00/30;BJ 来自 easyquotation
119
+ 内置代码表(静态文件,名称经 eq 补齐,新上市股票可能滞后)。进程内缓存,`refresh=True` 重拉。
120
+ **注意**:列表仅含 A 股,不含 ETF/指数(`get_quotes`/`get_bars` 支持场内 ETF,代码需自行传入)。
121
+
122
+ `search(keyword, kind, limit)` 返回 5 列:`code / symbol / name / market / type`(type 为
123
+ `stock`/`etf`/`index`,etf 含 LOF 等场内基金)。证券池覆盖全部 A 股 + 北交所 + 场内基金 + 沪深指数,
124
+ 来自 mootdx 全列表(与 `get_stock_list` 共享缓存)+ 北交所清单。
125
+
126
+ 匹配排序两级:先按**匹配层级**(代码精确 > 代码前缀 > 名称前缀 > 名称包含 > 拼音首字母前缀 >
127
+ 首字母包含 > 全拼包含 > 模糊兜底),同层内按**相似度降序**(difflib SequenceMatcher 的字母重合度,
128
+ 如 `mt` 会把"煤炭"排在"煤炭指数"前);拼写有误差时(如 `maotia`)由模糊层兜底召回(阈值 0.55,
129
+ 结合最长公共子串比率)。
130
+
131
+ 全量列表获取:**8 线程**分页并行拉取(每页 1000 条,绕过 mootdx 自带 stocks() 的串行翻页,
132
+ 直接调底层 tdxpy 协议接口),**三级缓存**——进程内 → 磁盘(TTL 8 小时,`~/.quotekit/market_list.csv`,
133
+ 环境变量 `QUOTEKIT_CACHE_DIR` 可改路径,跨进程复用)→ 网络。`refresh=True` 强制重拉并刷新磁盘缓存。
134
+
135
+ `get_bars(codes, freq, limit, adjust, sd, ed)` 返回 9 列:`code / symbol / datetime / open / high / low / close /
136
+ volume(股) / amount(元)`。datetime:分钟线 `YYYY-MM-DD HH:MM:SS`,日线及以上 `YYYY-MM-DD`。
137
+
138
+ 取数方式两种(给了 `sd`/`ed` 时优先,忽略 `limit`):
139
+ - **limit 模式**:最近 N 根。mootdx 逐只自动翻页凑足;tencent/sina 按源上限截断(日K 800、分钟 320/1970)。
140
+ - **日期区间模式**:`sd`/`ed`(`'YYYY-MM-DD'`,分钟线可带时分秒;均可单独给,ed 缺省=至今)。
141
+ tencent 日线原生支持日期段并按年分段回拉(可取全部历史,实测 1991 年起);mootdx 回翻过滤;
142
+ 分钟线的日期区间受源保留窗口限制(mootdx 近期、tencent 最近 320 根、sina 最近 1970 根)。
143
+ 日期模式下区间无数据返回空 DataFrame(不报错)。
144
+
145
+ 频度与源能力:
146
+
147
+ | 频度 | mootdx(主源) | tencent(回退) | sina(回退,仅分钟) |
148
+ |---|---|---|---|
149
+ | 1m/5m/15m/30m/60m | 近期滚动窗口,复权支持 | 320 根,无复权 | 1970 根,无复权 |
150
+ | day/week/month | 回溯到上市,复权支持 | 800 根/段(自动分段翻全部历史),复权支持 | ✗ |
151
+
152
+ 分钟频率的复权仅 mootdx 支持;腾讯源无成交额字段(amount 为 NaN);休市时 mootdx 返回空自动回退。
153
+
154
+ ## 数据源与实现
155
+
156
+ | 数据 | 主路径 | 回退路径 | 说明 |
157
+ |---|---|---|---|
158
+ | 个股/ETF 实时快照 | mootdx → tdxpy(通达信 TCP 7709,32 线程 × 60 只/批) | 新浪 → 腾讯(easyquotation) | 休市时 mootdx 空,自动回退拿最后快照 |
159
+ | 指数实时快照 | mootdx 底层 tdxpy(显式 market+code,绕过 mootdx 的市场误判) | 新浪 → 腾讯 | sina 沪市指数量单位为手已 ×100 归一 |
160
+ | K线 分钟 | mootdx(近期窗口,支持复权) | 腾讯 mkline(320 根)→ 新浪(1970 根) | 回退源分钟无复权 |
161
+ | K线 日/周/月 | mootdx(回溯到上市,支持复权) | 腾讯 fqkline(800 根/段,自动按年分段回拉全部历史,支持复权) | — |
162
+ | 股票列表 | mootdx → tdxpy `get_security_list`(8 线程分页) | — | 北交所来自 easyquotation 静态代码表,名称经腾讯补齐 |
163
+ | 搜索池 | 同上(与股票列表共享缓存)+ pypinyin 拼音 | — | 沪深指数取自 mootdx 全列表 |
164
+
165
+ 注:腾讯 K 线接口为 `ifzq.gtimg.cn` 的公开端点(fqkline/mkline),直接 HTTP 调用,不经 easyquotation。
166
+
167
+ ## 性能实测(2026-10,休市环境)
168
+
169
+ | 操作 | 耗时 |
170
+ |---|---|
171
+ | 全市场列表冷拉取(51861 条,8 线程) | **1.8~5.7s**(受 mootdx 服务器波动,慢轮曾 20s) |
172
+ | 线程数对比(2/4/8/16 各 3 轮均值) | 4.8s / 2.4s / **1.9s** / 5.9s(负优化)——8 为甜点 |
173
+ | 新进程命中磁盘缓存建池 | ~0.8s |
174
+ | 同进程搜索(缓存后) | ~50ms |
175
+ | 全市场实时快照(mootdx 32 线程,交易日) | ~0.3s(休市计时为协议往返上界) |
176
+
177
+ ## 口径与已知行为(实测)
178
+
179
+ - 单位统一:量=股(ETF 为份)、额=元;已修正各源字段错位(sina `volume` 实为成交额、tencent `成交额(万)` 实为元、sina 沪市指数量单位为手需 ×100)。
180
+ - mootdx 单连接独占、批量上限 60 只/次;并发靠"每线程一条连接"。
181
+ - 北交所(43/83/87/92 开头)mootdx 不支持,auto 模式自动由 eq 补齐;老北交码(83/43)sina 数据残缺(有价无开高低),需要完整数据请强制 `source='tencent'`。
182
+ - mootdx 的 `servertime` 不含日期,按调用当日拼时间;交易日调用即正确。
183
+ - 全部源失败抛 `QuoteSourceError`(含各源失败原因)。
184
+ - 休市期间:mootdx 快照为空 → 自动回退 eq 拿最后快照。
185
+
186
+ ## 运行测试
187
+
188
+ ```bash
189
+ python -m unittest discover -s tests -v # 离线单元测试
190
+ python tests/test_live_smoke.py # 在线冒烟(需网络, 交易日效果最佳)
191
+ ```
192
+
193
+ ## 目录结构
194
+
195
+ ```
196
+ quotekit/
197
+ ├── pyproject.toml
198
+ ├── src/quotekit/
199
+ │ ├── __init__.py # 出口: get_quotes / get_index_quotes / get_bars / get_stock_list / configure
200
+ │ ├── api.py # 实时行情编排与回退链
201
+ │ ├── bars.py # K线接口(8频度, 复权, 多源回退)
202
+ │ ├── lists.py # 交易所 A 股列表(8线程分页+三级缓存)
203
+ │ ├── search.py # 证券搜索(拼音/相似度/模糊兜底)
204
+ │ ├── sources.py # mootdx / easyquotation 实时行情适配器
205
+ │ ├── codes.py # 代码归一(sh600519 / 600519.SH / 600519)
206
+ │ ├── schema.py # 统一列定义
207
+ │ └── config.py # 全局配置
208
+ ├── tests/ # 离线单测 + 在线冒烟
209
+ └── examples/basic_usage.py
210
+ ```
@@ -0,0 +1,178 @@
1
+ # quotekit — A股/北交所/场内ETF 统一行情工具箱
2
+
3
+ 一个出口拉取 A 股个股(含北交所)、**场内 ETF** 与指数的实时行情和 K 线,统一返回 pandas DataFrame。
4
+
5
+ - **标的覆盖**:沪深 A 股 + 北交所(43/83/87/92 开头)+ 场内 ETF(沪 50/51/52/56/58、深 15/16 开头)+ 股指指数
6
+ - **默认源 mootdx**(通达信协议,32 线程并发,快、带五档盘口)
7
+ - **自动回退**:mootdx 失败/缺失的代码(含北交所、休市场景)自动用 easyquotation 新浪源补,仍缺失再用腾讯源
8
+ - **休市可用**:休市时返回最后交易日收盘快照(自动落到 eq 源,属设计行为)
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ pip install -e . # 开发安装
14
+ pip install build && python -m build # 打包: dist/quotekit-0.6.0-py3-none-any.whl
15
+ pip install dist/quotekit-0.6.0-py3-none-any.whl
16
+ ```
17
+
18
+ 依赖:`pandas`、`mootdx`、`easyquotation`、`pypinyin`(见 pyproject.toml)。
19
+
20
+ ## 快速上手
21
+
22
+ ```python
23
+ from quotekit import get_quotes, get_index_quotes, configure
24
+
25
+ # 个股: 三种代码写法混用均可
26
+ df = get_quotes(['600519', '000001.SZ', 'sh600000', '920099'])
27
+ print(df[['symbol', 'price', 'open', 'high', 'low', 'volume', 'amount', 'time']])
28
+
29
+ # 指数: 000001上证 399001深成 399006创业板指 000300沪深300
30
+ idx = get_index_quotes(['000001', '399001', '000300'])
31
+
32
+ # 场内ETF: 与个股同一接口, 支持实时快照(含五档)和K线
33
+ etf = get_quotes(['510300', '510050', '588000', '159915', '511990'])
34
+ etf_bars = get_bars('510300', freq='day', sd='2026-01-01') # ETF日K
35
+
36
+ # 交易所 A 股列表(symbol 格式 000001.SZ): 'all' | 'SH' | 'SZ' | 'BJ'
37
+ lst = get_stock_list() # 全市场 5400+ 只, 含名称
38
+ lst = get_stock_list('BJ') # 仅北交所
39
+ lst = get_stock_list(refresh=True) # 强制绕过缓存重拉
40
+
41
+ # K线: 单只或多只, 频度 1m/5m/15m/30m/60m/day/week/month, 支持复权与日期区间
42
+ bars = get_bars('600519', freq='day', limit=120) # 最近120根
43
+ bars = get_bars(['600519', '000001.SZ'], freq='30m', limit=100) # 多只30分钟K
44
+ bars = get_bars('600519', freq='day', limit=800, adjust='qfq') # 前复权
45
+ bars = get_bars('600519', freq='day', sd='2024-01-01') # 日期区间(优先于limit)
46
+ bars = get_bars('600519', freq='day', sd='2026-01-01', ed='2026-03-31')
47
+ bars = get_bars('000001.SZ', freq='day', ed='2025-12-31') # ed之前全部历史
48
+ bars = get_bars('920099', freq='week', limit=52) # 北交所周K
49
+
50
+ # 搜索: 股票/ETF/指数, 支持代码/中文名/拼音首字母/全拼
51
+ search('茅台') # 中文名 -> 600519
52
+ search('mt') # 拼音首字母
53
+ search('maotai') # 全拼
54
+ search('000001') # 同时命中 上证指数 + 平安银行
55
+ search('hs300', kind='etf') # kind: 'all'|'stock'|'etf'|'index'
56
+
57
+ # 强制指定源 / 临时改线程数
58
+ df = get_quotes(['600519'], source='tencent')
59
+ df = get_quotes(all_codes, workers=16)
60
+
61
+ # 全局配置(线程数/超时/批量, 批量上限60)
62
+ configure(workers=32, timeout=10, batch=60)
63
+ ```
64
+
65
+ ## 返回 schema
66
+
67
+ 个股/ETF 29 列 / 指数 9 列,`symbol` 格式 `600519.SH` / `000001.SZ` / `920099.BJ` / `510300.SH`。
68
+
69
+ | 列 | 类型 | 说明 |
70
+ |---|---|---|
71
+ | code | str | 6 位纯代码 |
72
+ | symbol | str | 带市场后缀 |
73
+ | price | float | 现价 |
74
+ | open / high / low | float | 今开 / 最高 / 最低 |
75
+ | volume | int64 | 成交量(股票=股、ETF=份;mootdx 源手×100) |
76
+ | amount | float | 成交额(元) |
77
+ | time | str | `YYYY-MM-DD HH:MM:SS` |
78
+ | bid1..5 / ask1..5 | float | 五档价(个股与 ETF) |
79
+ | bid_vol1..5 / ask_vol1..5 | int64 | 五档量(个股与 ETF) |
80
+
81
+ 场内 ETF 实测说明(2026-09-30 快照验证):实时与 K 线与个股完全同接口;成交量口径为**份**,
82
+ 经"金额÷量≈现价"自检通过(510300/510050/588000/159915/511990 五只全部吻合);日K收盘与量
83
+ 和实时快照精确一致;mootdx 主路径交易日生效(TDX 协议 ETF 与股票同接口)。
84
+
85
+ `get_stock_list(market)` 返回 4 列:`code`(6 位)、`symbol`(`000001.SZ` 格式)、`name`、`market`(SH/SZ/BJ)。
86
+ SH/SZ 来自 mootdx 实时列表(含名称,休市可用),A 股口径前缀:SH 60/68、SZ 00/30;BJ 来自 easyquotation
87
+ 内置代码表(静态文件,名称经 eq 补齐,新上市股票可能滞后)。进程内缓存,`refresh=True` 重拉。
88
+ **注意**:列表仅含 A 股,不含 ETF/指数(`get_quotes`/`get_bars` 支持场内 ETF,代码需自行传入)。
89
+
90
+ `search(keyword, kind, limit)` 返回 5 列:`code / symbol / name / market / type`(type 为
91
+ `stock`/`etf`/`index`,etf 含 LOF 等场内基金)。证券池覆盖全部 A 股 + 北交所 + 场内基金 + 沪深指数,
92
+ 来自 mootdx 全列表(与 `get_stock_list` 共享缓存)+ 北交所清单。
93
+
94
+ 匹配排序两级:先按**匹配层级**(代码精确 > 代码前缀 > 名称前缀 > 名称包含 > 拼音首字母前缀 >
95
+ 首字母包含 > 全拼包含 > 模糊兜底),同层内按**相似度降序**(difflib SequenceMatcher 的字母重合度,
96
+ 如 `mt` 会把"煤炭"排在"煤炭指数"前);拼写有误差时(如 `maotia`)由模糊层兜底召回(阈值 0.55,
97
+ 结合最长公共子串比率)。
98
+
99
+ 全量列表获取:**8 线程**分页并行拉取(每页 1000 条,绕过 mootdx 自带 stocks() 的串行翻页,
100
+ 直接调底层 tdxpy 协议接口),**三级缓存**——进程内 → 磁盘(TTL 8 小时,`~/.quotekit/market_list.csv`,
101
+ 环境变量 `QUOTEKIT_CACHE_DIR` 可改路径,跨进程复用)→ 网络。`refresh=True` 强制重拉并刷新磁盘缓存。
102
+
103
+ `get_bars(codes, freq, limit, adjust, sd, ed)` 返回 9 列:`code / symbol / datetime / open / high / low / close /
104
+ volume(股) / amount(元)`。datetime:分钟线 `YYYY-MM-DD HH:MM:SS`,日线及以上 `YYYY-MM-DD`。
105
+
106
+ 取数方式两种(给了 `sd`/`ed` 时优先,忽略 `limit`):
107
+ - **limit 模式**:最近 N 根。mootdx 逐只自动翻页凑足;tencent/sina 按源上限截断(日K 800、分钟 320/1970)。
108
+ - **日期区间模式**:`sd`/`ed`(`'YYYY-MM-DD'`,分钟线可带时分秒;均可单独给,ed 缺省=至今)。
109
+ tencent 日线原生支持日期段并按年分段回拉(可取全部历史,实测 1991 年起);mootdx 回翻过滤;
110
+ 分钟线的日期区间受源保留窗口限制(mootdx 近期、tencent 最近 320 根、sina 最近 1970 根)。
111
+ 日期模式下区间无数据返回空 DataFrame(不报错)。
112
+
113
+ 频度与源能力:
114
+
115
+ | 频度 | mootdx(主源) | tencent(回退) | sina(回退,仅分钟) |
116
+ |---|---|---|---|
117
+ | 1m/5m/15m/30m/60m | 近期滚动窗口,复权支持 | 320 根,无复权 | 1970 根,无复权 |
118
+ | day/week/month | 回溯到上市,复权支持 | 800 根/段(自动分段翻全部历史),复权支持 | ✗ |
119
+
120
+ 分钟频率的复权仅 mootdx 支持;腾讯源无成交额字段(amount 为 NaN);休市时 mootdx 返回空自动回退。
121
+
122
+ ## 数据源与实现
123
+
124
+ | 数据 | 主路径 | 回退路径 | 说明 |
125
+ |---|---|---|---|
126
+ | 个股/ETF 实时快照 | mootdx → tdxpy(通达信 TCP 7709,32 线程 × 60 只/批) | 新浪 → 腾讯(easyquotation) | 休市时 mootdx 空,自动回退拿最后快照 |
127
+ | 指数实时快照 | mootdx 底层 tdxpy(显式 market+code,绕过 mootdx 的市场误判) | 新浪 → 腾讯 | sina 沪市指数量单位为手已 ×100 归一 |
128
+ | K线 分钟 | mootdx(近期窗口,支持复权) | 腾讯 mkline(320 根)→ 新浪(1970 根) | 回退源分钟无复权 |
129
+ | K线 日/周/月 | mootdx(回溯到上市,支持复权) | 腾讯 fqkline(800 根/段,自动按年分段回拉全部历史,支持复权) | — |
130
+ | 股票列表 | mootdx → tdxpy `get_security_list`(8 线程分页) | — | 北交所来自 easyquotation 静态代码表,名称经腾讯补齐 |
131
+ | 搜索池 | 同上(与股票列表共享缓存)+ pypinyin 拼音 | — | 沪深指数取自 mootdx 全列表 |
132
+
133
+ 注:腾讯 K 线接口为 `ifzq.gtimg.cn` 的公开端点(fqkline/mkline),直接 HTTP 调用,不经 easyquotation。
134
+
135
+ ## 性能实测(2026-10,休市环境)
136
+
137
+ | 操作 | 耗时 |
138
+ |---|---|
139
+ | 全市场列表冷拉取(51861 条,8 线程) | **1.8~5.7s**(受 mootdx 服务器波动,慢轮曾 20s) |
140
+ | 线程数对比(2/4/8/16 各 3 轮均值) | 4.8s / 2.4s / **1.9s** / 5.9s(负优化)——8 为甜点 |
141
+ | 新进程命中磁盘缓存建池 | ~0.8s |
142
+ | 同进程搜索(缓存后) | ~50ms |
143
+ | 全市场实时快照(mootdx 32 线程,交易日) | ~0.3s(休市计时为协议往返上界) |
144
+
145
+ ## 口径与已知行为(实测)
146
+
147
+ - 单位统一:量=股(ETF 为份)、额=元;已修正各源字段错位(sina `volume` 实为成交额、tencent `成交额(万)` 实为元、sina 沪市指数量单位为手需 ×100)。
148
+ - mootdx 单连接独占、批量上限 60 只/次;并发靠"每线程一条连接"。
149
+ - 北交所(43/83/87/92 开头)mootdx 不支持,auto 模式自动由 eq 补齐;老北交码(83/43)sina 数据残缺(有价无开高低),需要完整数据请强制 `source='tencent'`。
150
+ - mootdx 的 `servertime` 不含日期,按调用当日拼时间;交易日调用即正确。
151
+ - 全部源失败抛 `QuoteSourceError`(含各源失败原因)。
152
+ - 休市期间:mootdx 快照为空 → 自动回退 eq 拿最后快照。
153
+
154
+ ## 运行测试
155
+
156
+ ```bash
157
+ python -m unittest discover -s tests -v # 离线单元测试
158
+ python tests/test_live_smoke.py # 在线冒烟(需网络, 交易日效果最佳)
159
+ ```
160
+
161
+ ## 目录结构
162
+
163
+ ```
164
+ quotekit/
165
+ ├── pyproject.toml
166
+ ├── src/quotekit/
167
+ │ ├── __init__.py # 出口: get_quotes / get_index_quotes / get_bars / get_stock_list / configure
168
+ │ ├── api.py # 实时行情编排与回退链
169
+ │ ├── bars.py # K线接口(8频度, 复权, 多源回退)
170
+ │ ├── lists.py # 交易所 A 股列表(8线程分页+三级缓存)
171
+ │ ├── search.py # 证券搜索(拼音/相似度/模糊兜底)
172
+ │ ├── sources.py # mootdx / easyquotation 实时行情适配器
173
+ │ ├── codes.py # 代码归一(sh600519 / 600519.SH / 600519)
174
+ │ ├── schema.py # 统一列定义
175
+ │ └── config.py # 全局配置
176
+ ├── tests/ # 离线单测 + 在线冒烟
177
+ └── examples/basic_usage.py
178
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quotekit"
7
+ version = "0.6.0"
8
+ description = "A股/北交所/场内ETF统一行情工具箱: mootdx(默认,多线程) + easyquotation(新浪/腾讯回退), 统一DataFrame返回, 实时行情/K线(8频度)/股票列表"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "haifeng", email = "chenhaifeng@touguyun.cn" }]
13
+ keywords = ["quant", "finance", "a-share", "stock", "etf", "index", "quotes", "kline", "mootdx", "easyquotation", "tongdaxin"]
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
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Office/Business :: Financial :: Investment",
27
+ ]
28
+ dependencies = [
29
+ "pandas",
30
+ "mootdx",
31
+ "easyquotation",
32
+ "pypinyin",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit"
37
+ Repository = "https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit"
38
+
39
+ [project.optional-dependencies]
40
+ dev = ["pytest"]
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ # -*- coding: utf-8 -*-
2
+ """quotekit: A股/北交所/场内ETF 统一行情工具箱 (mootdx 默认源 + easyquotation 新浪/腾讯回退)."""
3
+ from .api import QuoteSourceError, get_index_quotes, get_quotes
4
+ from .bars import get_bars
5
+ from .config import configure
6
+ from .lists import get_stock_list
7
+ from .search import search
8
+ from .schema import INDEX_COLS, STOCK_COLS
9
+
10
+ __version__ = '0.6.0'
11
+ __all__ = ['get_quotes', 'get_index_quotes', 'get_bars', 'get_stock_list', 'search', 'configure',
12
+ 'QuoteSourceError', 'STOCK_COLS', 'INDEX_COLS']
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ """统一出口: get_quotes(个股) / get_index_quotes(指数), auto 回退 mootdx -> sina -> tencent."""
3
+ import pandas as pd
4
+
5
+ from . import sources
6
+ from .codes import split_code, symbol
7
+ from .config import configure # noqa: F401 (re-export)
8
+ from .schema import STOCK_COLS, INDEX_COLS
9
+
10
+ _SOURCES = {'mootdx': sources.mootdx_stocks, 'sina': sources.eq_stocks, 'tencent': sources.eq_stocks}
11
+ _IDX_SOURCES = {'mootdx': sources.mootdx_index, 'sina': sources.eq_index, 'tencent': sources.eq_index}
12
+
13
+
14
+ class QuoteSourceError(RuntimeError):
15
+ """所有数据源均失败时抛出, message 汇总各源失败原因."""
16
+
17
+
18
+ def _pick(codes_map, providers):
19
+ """按顺序尝试各 provider(dict), 同一代码先到先得, 返回 (rows, errors)."""
20
+ rows, errors, covered = [], [], set()
21
+ for name, fn in providers:
22
+ if len(covered) == len(codes_map):
23
+ break
24
+ missing = [c for c in codes_map if c not in covered]
25
+ try:
26
+ got = fn(missing)
27
+ except Exception as e:
28
+ got, errors = {}, errors + [f'{name}: {e}']
29
+ for c in missing:
30
+ if c in got:
31
+ rows.append(got[c])
32
+ covered.add(c)
33
+ if not got and missing:
34
+ errors = errors + [f'{name}: {len(missing)}只未覆盖']
35
+ return rows, errors
36
+
37
+
38
+ def _providers(kind, source, codes_map):
39
+ table = _IDX_SOURCES if kind == 'index' else _SOURCES
40
+ def wrap(src):
41
+ if src in ('sina', 'tencent'):
42
+ return lambda ms: table[src](src, [(c, codes_map[c]) for c in ms])
43
+ return lambda ms: table[src]([(c, codes_map[c]) for c in ms])
44
+ order = ['mootdx', 'sina', 'tencent'] if source == 'auto' else [source]
45
+ return [(s, wrap(s)) for s in order]
46
+
47
+
48
+ def get_quotes(codes, source='auto', workers=None):
49
+ """个股实时行情, 统一 29 列 DataFrame.
50
+
51
+ :param codes: '600519' / 'sh600519' / '600519.SH' 混写均可
52
+ :param source: 'auto'(默认, mootdx -> sina -> tencent 回退) | 'mootdx' | 'sina' | 'tencent'
53
+ :param workers: 临时覆盖并发线程数(仅 mootdx 源生效)
54
+ """
55
+ if workers is not None:
56
+ configure(workers=workers)
57
+ items = [split_code(c) for c in ([codes] if isinstance(codes, str) else codes)]
58
+ codes_map = dict(items)
59
+ rows, errors = _pick(codes_map, _providers('stock', source, codes_map))
60
+ if not rows:
61
+ raise QuoteSourceError('全部数据源失败: ' + '; '.join(errors))
62
+ df = pd.DataFrame(rows, columns=STOCK_COLS)
63
+ return (df.set_index('code')
64
+ .reindex([c for c, _ in items if c in set(df['code'])])
65
+ .reset_index())
66
+
67
+
68
+ def get_index_quotes(codes, source='auto', workers=None):
69
+ """指数实时行情, 统一 9 列 DataFrame.
70
+
71
+ :param codes: '000001'(上证指数) '399001'(深证成指) '000300'(沪深300) 等, 可带后缀;
72
+ 无后缀时按规则识别: 399 开头=SZ, 899 开头=BJ, 其余=SH
73
+ """
74
+ if workers is not None:
75
+ configure(workers=workers)
76
+ items = []
77
+ for c in ([codes] if isinstance(codes, str) else codes):
78
+ code, mkt = split_code(c)
79
+ if not (('.' in str(c)) or str(c).lower().startswith(('sh', 'sz', 'bj'))):
80
+ mkt = 'SZ' if code.startswith('399') else ('BJ' if code.startswith('899') else 'SH')
81
+ items.append((code, mkt))
82
+ codes_map = dict(items)
83
+ rows, errors = _pick(codes_map, _providers('index', source, codes_map))
84
+ if not rows:
85
+ raise QuoteSourceError('全部数据源失败: ' + '; '.join(errors))
86
+ for code, mkt in items:
87
+ for r in rows:
88
+ if r['code'] == code:
89
+ r['symbol'] = symbol(code, mkt)
90
+ df = pd.DataFrame(rows, columns=INDEX_COLS)
91
+ return (df.set_index('code')
92
+ .reindex([c for c, _ in items if c in set(df['code'])])
93
+ .reset_index())