Metadata-Version: 2.4
Name: quotekit
Version: 0.10.0
Summary: A股/北交所/场内ETF统一行情工具箱: 新浪默认源(五档盘口,逗号分割字段) + 腾讯/mootdx 回退, 统一DataFrame返回, 实时行情/K线(8频度)/股票列表/搜索
Author-email: haifeng <chenhaifeng@touguyun.cn>
License: MIT
Project-URL: Homepage, https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit
Project-URL: Repository, https://github.com/fenglex/qdata/tree/master/qdata-labs/quotekit
Keywords: quant,finance,a-share,stock,etf,index,quotes,kline,mootdx,easyquotation,tongdaxin
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas
Requires-Dist: mootdx
Requires-Dist: easyquotation
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# quotekit — A股/北交所/场内ETF 实时行情

一个出口拉取 A 股个股（含北交所）、**场内 ETF** 与指数的实时行情，统一返回 pandas DataFrame。

- **标的覆盖**：沪深 A 股 + 北交所（43/83/87/92 开头）+ 场内 ETF（沪 50/51/52/56/58、深 15/16 开头）+ 股指指数
- **默认源新浪**（easyquotation，约 800 只/次批量，**五档盘口齐全**），回退链：新浪 → 腾讯 → mootdx → 同花顺
- **盘口精简**：五档价/量压缩为 4 个逗号分割字符串字段（`bids`/`asks`/`bid_vols`/`ask_vols`），14 列轻量 schema
- **休市可用**：休市时返回最后交易日收盘快照（自动回退 eq 源，属设计行为）
- **轮询监视**：`StockWatcher` 后台线程间隔拉全量快照，仅"变化行"（日期+代码+价格+成交额 与上一批不同）进回调

## 安装

```bash
pip install quotekit                      # PyPI
pip install -e .                          # 开发安装(源码)
```

依赖：`pandas`、`mootdx`、`easyquotation`（见 pyproject.toml）。

## 快速上手

```python
from quotekit import get_stock_quotes, get_etf_quotes, get_index_quotes, configure

# 个股: 三种代码写法混用均可
df = get_stock_quotes(['600519', '000001.SZ', 'sh600000', '920099'])
print(df[['symbol', 'price', 'open', 'high', 'low', 'volume', 'amount', 'time']])

# 指数: 000001上证 399001深成 399006创业板指 000300沪深300
idx = get_index_quotes(['000001', '399001', '000300'])

# 场内ETF: 与个股同接口同列(volume 单位为份)
etf = get_etf_quotes(['510300', '510050', '588000', '159915', '511990'])

# 强制指定源 / 临时改线程数
df = get_stock_quotes(['600519'], source='sina')      # 新浪(默认, 五档齐全, 800只/批)
df = get_stock_quotes(['600519'], source='tencent')   # 腾讯(北交所老代码数据最全)
# 大批量: get_stock_quotes(codes, workers=16)   # 临时提高 mootdx 并发线程数

# 全局配置(线程数/超时/批量, 批量上限60)
configure(workers=32, timeout=10, batch=60)
```

## 轮询监视（StockWatcher）

间隔 N 秒拉一批全量快照，**只把与上一批不同的行交给回调**。
去重键 = 日期(`time` 前 10 位) + `code` + `price` + `amount`，仅与上一批对比（非累积）：

- 首批无"上一批"→ 全量进回调；跨交易日 → 全量重发
- 价格/成交额都没变的行不再进回调（成交额为累计值，同一价位持续成交也会触发）
- 休市时源返回上一交易日收盘快照 → 第二批起自然全部去重
- 单轮失败（断网/回调抛错）打印到 stderr 后继续下轮，不终止轮询

```python
from quotekit import StockWatcher

def on_change(df):                      # df: 14 列, 仅与上一批不同的行
    print(df[['symbol', 'price', 'amount', 'time']])

w = StockWatcher(all_codes, on_change, interval=3)   # all_codes=全部A股代码清单(调用方提供)
w.start()            # 后台 daemon 线程开始轮询
w.stop()             # 停止(等待本轮结束)
w.run_once()         # 单步: 拉一批 -> diff -> 回调, 返回变化行 DataFrame(可测)
```

## API 参数速查

| 接口 | 返回 | codes 参数 | 备注 |
|---|---|---|---|
| `get_stock_quotes(codes, source='auto', workers=None)` | 个股 14 列 | `'600519'`/`'sh600519'`/`'600519.SH'` 混写; 北交所 43/83/87/92 开头 | 返回行数可少于请求只数(三源均无, 如退市股) |
| `get_etf_quotes(codes, source='auto', workers=None)` | ETF 14 列(字段同个股) | 沪 50/51/52/56/58、深 15/16 开头, `510300`/`159915.SZ` 混写 | `volume` 单位为**份** |
| `get_index_quotes(codes, source='auto', workers=None)` | 指数 10 列(无盘口) | `000001`/`399001`/`000300`, 可带后缀; 无后缀时 399→SZ、899→BJ、其余→SH | — |
| `get_ths_bars(codes, freq='day', adjust='forward', start=None, end=None, workers=None)` | K线 9 列(同花顺, **独立接口**) | 个股+指数(指数须显式前缀 `sh000001`); **ETF/分钟线不支持** | freq 统一词表 `m1/m5/m10/m15/m30/m60/d/w/m`(本源仅 `d`/`w`/`m`, 分钟档报错); adjust: `forward`/`backward`/`none`; start/end `YYYY-MM-DD`(默认近 1 年, 上限 10 年) |
| `StockWatcher(codes, callback, interval=3, source='auto', workers=None)` | 变化行增量(回调) | 同 `get_stock_quotes` codes(全部 A 股清单由调用方提供) | 回调仅收"日期+代码+价格+成交额 与上一批不同"的行; 首批/跨日为全量 |

`source`: `'auto'`(默认, 新浪→腾讯→mootdx→同花顺 回退) / `'sina'` / `'tencent'` / `'mootdx'` / `'ths'`(同花顺);
`workers`: 临时覆盖 mootdx/ths 源并发线程数; 三个接口签名一致, `codes` 均支持单只 str 或 list.
`'ths'` 为同花顺 REST 快照(需 API key, 无盘口, **仅个股**——指数/ETF 走其他源).

## 返回样例（真实数据）

所有数据接口统一返回 **pandas DataFrame**：`get_stock_quotes` / `get_etf_quotes` / `get_index_quotes`（`configure` 为全局配置函数，无返回值）。
以下样例为真实拉取数据（2026-10-13 交易日盘中，新浪默认源采集）。

### get_stock_quotes / get_etf_quotes — 个股 / ETF 实时快照（同为 14 列，主列示例如下）

```text
   symbol    price     open     high      low    volume       amount                time
600519.SH 1270.070 1287.000 1288.480 1269.010   1903943 2.435202e+09 2026-10-13 14:03:20
000001.SZ   11.600   11.730   11.770   11.570  73562382 8.571155e+08 2026-10-13 14:03:21
510300.SH    4.362    4.313    4.384    4.286 954983314 4.129196e+09 2026-10-13 14:03:21
920099.BJ   15.220   15.370   15.380   14.790    673317 1.015826e+07 2026-10-13 14:03:14
```

五档盘口为 4 个逗号分割字符串字段（`bids`/`bid_vols`/`asks`/`ask_vols`，量单位股）：

```text
   symbol                                 bids                        bid_vols                                  asks                           ask_vols
600519.SH 1270.08,1270.07,1270.06,1270.01,1270                 300,100,300,100,1900 1270.18,1270.29,1270.3,1270.32,1270.5                 100,300,100,100,100
000001.SZ         11.6,11.59,11.58,11.57,11.56 122400,267600,754700,519700,574400         11.61,11.62,11.63,11.64,11.65       2700,93700,41000,60400,104000
510300.SH         4.362,4.361,4.36,4.359,4.358   7950,661400,1873100,772800,1062400         4.363,4.364,4.365,4.366,4.367 23400,384600,1098700,160700,1216800
920099.BJ        15.22,15.21,15.19,15.18,15.16             33,296,1900,3734,5496          15.25,15.27,15.3,15.31,15.33            1241,1388,1035,1000,4000
```

拆解（`r = df.iloc[0]` 后 `r['bids'].split(',')` 得到买一~买五价列表）：

```text
bids    = '1270.08,1270.07,1270.06,1270.01,1270'      # 买一 -> 买五价
bid_vols= '300,100,300,100,1900'                       # 对应买档量(股)
asks    = '1270.18,1270.29,1270.3,1270.32,1270.5'      # 卖一 -> 卖五价
ask_vols= '100,300,100,100,100'                        # 对应卖档量(股)
```

### get_index_quotes — 指数实时快照（10 列）

```text
  code    symbol      price       open       high        low      volume       amount                time
000001 000001.SH  3808.4379  3760.7988  3822.1050  3741.5262 40014660600 6.489840e+11 2026-10-13 14:03:20
399001 399001.SZ 12430.2060 12163.2310 12486.2060 12060.7980 46857378819 7.529831e+11 2026-10-13 14:03:18
000300 000300.SH  4290.8714  4236.9492  4310.3330  4216.8602 14477712300 3.641103e+11 2026-10-13 14:03:16
```

## 返回 schema

个股/ETF 14 列 / 指数 10 列，`symbol` 格式 `600519.SH` / `000001.SZ` / `920099.BJ` / `510300.SH`。

| 列 | 类型 | 说明 |
|---|---|---|
| code | str | 6 位纯代码 |
| symbol | str | 带市场后缀 |
| pre_close | float | 昨收盘价 |
| price | float | 现价 |
| open / high / low | float | 今开 / 最高 / 最低 |
| volume | int64 | 成交量（股票=股、ETF=份；mootdx 源手×100） |
| amount | float | 成交额（元） |
| time | str | `YYYY-MM-DD HH:MM:SS` |
| bids | str | 买一~买五价，逗号分割（如 `1270.08,1270.07,...`）；无盘口为空串 |
| bid_vols | str | 买一~买五量（股），逗号分割 |
| asks | str | 卖一~卖五价，逗号分割 |
| ask_vols | str | 卖一~卖五量（股），逗号分割 |

指数（INDEX_COLS 10 列）只含前 10 列，无盘口字段。五档拆解：`r['bids'].split(',')` 即得买一~买五价列表。

场内 ETF 实测说明（2026-09-30 快照验证）：与个股完全同接口；成交量口径为**份**，
经"金额÷量≈现价"自检通过（510300/510050/588000/159915/511990 五只全部吻合）。

## 数据源与实现

| 数据 | 主路径 | 回退路径 | 说明 |
|---|---|---|---|
| 个股/ETF 实时快照 | 新浪 → easyquotation（约 800 只/次批量，五档齐全） | 腾讯（~60 只/次）→ mootdx | 新浪字段名错位已修正（`turnover`=量、`volume`=额） |
| 指数实时快照 | 新浪（同接口） | 腾讯 → mootdx 底层 tdxpy | 新浪沪市指数量单位为手已 ×100 归一 |
| 个股实时快照(同花顺) | ths REST（500 只/请求，`workers` 并发分块；全市场 16 线程 ~0.7s） | — | 无盘口/无逐票时间(共用快照 timestamp)；指数与 ETF 不支持（自动落回退链）；北交所老代码无残缺问题 |

注：东财 push2 曾作主源（0.7.x），因其实时接口无五档且五档字段已下线（2026-10 实测），0.8.0 起弃用。
mootdx 于 2026-10-13 起行情接口全网失效中（快照空返回），恢复后 auto 链自动回归。

## 性能实测（2026-10，休市环境）

| 操作 | 耗时 |
|---|---|
| 全市场实时快照（mootdx 32 线程，交易日） | ~0.3s（休市计时为协议往返上界） |

## 口径与已知行为（实测）

- 单位统一：量=股（ETF 为份）、额=元；已修正各源字段错位（sina `turnover` 实为量/`volume` 实为额、tencent `成交额(万)` 实为元、sina 沪市指数量单位为手需 ×100、mootdx 手×100）。
- **新浪源（默认）**：五档价/量齐全（量=股），约 800 只/次批量，A股/ETF/北交 920/87 开头正常；**老北交码（83/43）数据残缺**（有价无开高低），此类代码 auto 链自动落到腾讯补齐；集合竞价时段（9:15-9:25）`now=0` 会被过滤，同样回退腾讯。
- **盘口字段格式**：`bids`/`asks`/`bid_vols`/`ask_vols` 为逗号分割字符串（五档，量=股），指数/无盘口为空串；拆解用 `r['bids'].split(',')`。
- mootdx 单连接独占、批量上限 60 只/次；并发靠"每线程一条连接"。2026-10-13 起其行情接口全网失效中（快照空返回），恢复后 auto 链自动回归。
- mootdx 的 `servertime` 不含日期，按调用当日拼时间；交易日调用即正确。
- 同花顺源（`source='ths'` 或回退链末位）：API key 经 `configure(ths_key=...)` 或环境变量 `QUOTEKIT_THS_KEY` 注入，无 key 时该源整体跳过；无五档（盘口字段为空串）；沪深+北交所全覆盖，但指数/ETF 不在覆盖面内（静默丢弃，由回退链接管）。
- 全部源失败抛 `QuoteSourceError`（含各源失败原因）。
- 休市期间：新浪/腾讯返回最后交易日收盘快照。

## 运行测试

```bash
python -m unittest discover -s tests -v     # 离线单元测试
python tests/test_live_smoke.py             # 在线冒烟(需网络, 交易日效果最佳)
```

## 目录结构

```
quotekit/
├── pyproject.toml
├── src/quotekit/
│   ├── __init__.py     # 出口: get_stock_quotes / get_etf_quotes / get_index_quotes / configure
│   ├── api.py          # 实时行情编排与回退链
│   ├── sources.py      # easyquotation(新浪默认/腾讯) / mootdx 实时行情适配器
│   ├── codes.py        # 代码归一(sh600519 / 600519.SH / 600519)
│   ├── schema.py       # 统一列定义
│   ├── config.py       # 全局配置
│   └── watch.py        # StockWatcher 轮询监视(变化行回调)
├── tests/              # 离线单测 + 在线冒烟
└── examples/basic_usage.py
```
