Metadata-Version: 2.4
Name: qdata-quote
Version: 1.2.0
Summary: 实时行情采集服务，支持新浪和腾讯数据源
License-Expression: MIT
Requires-Python: >=3.10
Requires-Dist: aiohttp>=3.9
Requires-Dist: pandas>=2.0
Requires-Dist: requests>=2.28
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# qdata-quote

实时 A 股行情采集服务，支持新浪和腾讯两个数据源。性能超越 easyquotation 约 8-10%。

## 安装

```bash
pip install qdata-quote
```

## 快速开始

```python
from qdata_quote import QuoteService

# 默认加载全市场股票列表（初始化时自动从网络刷新）
service = QuoteService()

# 同步获取指定股票行情
df = service.get_real_sync(["000001", "600000"])
print(df)

# 同步获取全市场行情快照
df_all = service.get_all_sync()

# 异步获取（高性能路径）
import asyncio
df = asyncio.run(service.get_real(["000001", "600000"]))
df_all = asyncio.run(service.get_all())
```

## 自定义股票列表

```python
from qdata_quote import QuoteService

# 传入自定义股票列表（不会自动刷新）
service = QuoteService(codes=["000001", "600000", "300750"])

# 随时替换股票列表（切换为自定义模式，不再接受 refresh）
service.set_stock_codes(["000001", "600000"])

# 刷新全市场股票列表（仅在使用默认列表时生效）
service.refresh_stock_codes()
```

## 获取股票代码

```python
from qdata_quote import get_stock_codes

# 读取缓存（如果没有缓存则自动获取）
codes = get_stock_codes()

# 强制从网络刷新
codes = get_stock_codes(refresh=True)
```

缓存文件位置：`~/.qdata_quote/stock_codes.json`

## 数据源

支持三个数据源，通过 `source` 参数指定：

```python
# 新浪源（默认）
df = service.get_real_sync(["000001"], source="sina")

# 腾讯源（字段更丰富）
df = service.get_real_sync(["000001"], source="tencent")

# 腾讯完整源（跨市场 + 最多字段）
df = service.get_real_sync(["000001"], source="tencent_full")
```

| 数据源 | 每批数量 | 字段数 | 市场支持 | 特点 |
|--------|---------|--------|---------|------|
| sina | 800 只/批 | 42 | 仅 A 股 | 速度快，基础字段齐全 |
| tencent | 60 只/批 | 42 | 仅 A 股 | 额外提供涨跌、市盈率、市值、量比等 |
| **tencent_full** | **800 只/批** | **66** | **A 股 + 港股 + 美股 + ETF + 北交所** | **最多字段（+24 扩展），跨市场支持** |

### tencent_full 扩展字段

相比 sina/tencent，tencent_full 额外提供 24 列：

| 类别 | 字段 |
|------|------|
| 累计涨跌幅 | `change_ytd` `change_5d` `change_10d` `change_20d` `change_60d` `change_6m` `change_1y` |
| 52 周高低 | `high_52w` `low_52w` |
| 涨速 | `speed_5min` `speed_5min_real` |
| 外内盘 | `outer_volume` `inner_volume` |
| 股本 | `float_shares` `total_shares` |
| 中文化 | `stock_type_cn` `market_tags_cn` |
| 状态 | `currency` `is_suspend` `market_mark` |
| 其它 | `turnover_rate` `turnover_full` `capital_ratio` `secu_code` |

### 跨市场支持

tencent_full 支持 A 股 / 港股 / 美股 / ETF / 北交所：

```python
# 跨市场查询（港美股字段较少，错位字段自动置 NaN）
df = service.get_real_sync(
    ["600519.SH", "000001.SZ", "00700.HK", "AAPL.US"],
    source="tencent_full"
)
```

## 返回格式

返回统一的 `pandas.DataFrame`，index 为带后缀的股票代码（如 `600000.SH`、`000001.SZ`、`830789.BJ`）。

### 股票代码格式

- **输出统一后缀格式**：`<代码>.<交易所>`，交易所后缀大写 —— `.SH`（上交所）、`.SZ`（深交所）、`.BJ`（北交所）、`.HK`（港股）、`.US`（美股）。
- **输入兼容多种写法**（tencent_full 独有跨市场支持）：
  - 纯代码：`000001`、`600519`、`00700`、`AAPL`
  - 带后缀：`000001.SZ`、`00700.HK`、`AAPL.US`（大小写不敏感）
  - 带前缀：`sh600519`、`hk00700`、`usAAPL`
- `get_stock_codes()` 返回的默认列表同样为后缀格式（仅 A 股）。
- **跨市场查询仅 tencent_full 支持**（sina/tencent 仅 A 股）。

### 字段列表

字段按数据源覆盖范围递增排列：**三源共有** → **tencent/tencent_full 共有** → **tencent_full 独有**。

| 字段 | 类型 | 新浪 | 腾讯 | 腾讯完整 | 说明 |
|------|------|:----:|:----:|:--------:|------|
| **A. 三源共有** | | | | | |
| code | str | ✅ | ✅ | ✅ | 股票代码（index） |
| name | str | ✅ | ✅ | ✅ | 股票名称 |
| high | float | ✅ | ✅ | ✅ | 最高价 |
| open | float | ✅ | ✅ | ✅ | 开盘价 |
| low | float | ✅ | ✅ | ✅ | 最低价 |
| pre_close | float | ✅ | ✅ | ✅ | 昨收价 |
| now | float | ✅ | ✅ | ✅ | 当前价 |
| volume | float | ✅ | ✅ | ✅ | 成交量（股） |
| turnover | float | ✅ | ✅ | ✅ | 成交额（元） |
| bid1 ~ bid5 | float | ✅ | ✅ | ✅ | 买一到买五价 |
| ask1 ~ ask5 | float | ✅ | ✅ | ✅ | 卖一到卖五价 |
| bid1_volume ~ bid5_volume | float | ✅ | ✅ | ✅ | 买一到买五量（股） |
| ask1_volume ~ ask5_volume | float | ✅ | ✅ | ✅ | 卖一到卖五量（股） |
| datetime | str | ✅ | ✅ | ✅ | 行情时间 |
| **B. 腾讯/腾讯完整共有**（新浪为 NaN） | | | | | |
| change | float | - | ✅ | ✅ | 涨跌额 |
| change_pct | float | - | ✅ | ✅ | 涨跌幅(%) |
| amplitude | float | - | ✅ | ✅ | 振幅 |
| pe_dynamic | float | - | ✅ | ✅ | 动态市盈率 |
| pe_static | float | - | ✅ | ✅ | 静态市盈率 |
| pb | float | - | ✅ | ✅ | 市净率 |
| total_market_cap | float | - | ✅ | ✅ | 总市值 |
| circulating_market_cap | float | - | ✅ | ✅ | 流通市值 |
| volume_ratio | float | - | ✅ | ✅ | 量比 |
| bid_ask_ratio | float | - | ✅ | ✅ | 委比 |
| avg_price | float | - | ✅ | ✅ | 均价 |
| limit_up | float | - | ✅ | ✅ | 涨停价 |
| limit_down | float | - | ✅ | ✅ | 跌停价 |
| **C. 腾讯完整独有**（扩展字段） | | | | | |
| secu_code | str | - | - | ✅ | 原始证券代码 |
| market_mark | int | - | - | ✅ | 市场标记 |
| stock_type_cn | str | - | - | ✅ | 股票类型（中文） |
| market_tags_cn | str | - | - | ✅ | 市场标签（中文） |
| currency | str | - | - | ✅ | 币种（CNY/HKD/USD） |
| is_suspend | int | - | - | ✅ | 是否停牌 |
| turnover_rate | float | - | - | ✅ | 换手率(%) |
| turnover_full | float | - | - | ✅ | 成交额（完整精度） |
| outer_volume | int | - | - | ✅ | 外盘（主动买盘，股） |
| inner_volume | int | - | - | ✅ | 内盘（主动卖盘，股） |
| float_shares | int | - | - | ✅ | 流通股本 |
| total_shares | int | - | - | ✅ | 总股本 |
| capital_ratio | float | - | - | ✅ | 流通占比 |
| speed_5min | float | - | - | ✅ | 5 分钟涨速 |
| speed_5min_real | float | - | - | ✅ | 5 分钟涨速（实时） |
| change_ytd | float | - | - | ✅ | 年初至今涨跌幅(%) |
| change_5d/10d/20d/60d | float | - | - | ✅ | 5/10/20/60 日涨跌幅(%) |
| change_6m/1y | float | - | - | ✅ | 近 6 月/1 年涨跌幅(%) |
| high_52w/low_52w | float | - | - | ✅ | 52 周最高/最低价 |

✅ 表示有数据，- 表示 NaN。腾讯完整源（tencent_full）提供最丰富的字段（66 列）和跨市场支持。

## 会话管理

建议使用上下文管理器复用连接，在频繁轮询场景下性能更佳：

```python
# 同步
with QuoteService() as service:
    df = service.get_all_sync()

# 异步
async with QuoteService() as service:
    df = await service.get_all()
```

## 行情监控（去重 + 回调）

`QuoteMonitor` 在内存中维护每只股票的最新行情，循环获取时自动过滤重复数据，仅将**有变化的行情**通过回调通知调用方。适合高频轮询后推送消息队列的场景。

### 基本用法

```python
import asyncio
from qdata_quote import QuoteService, QuoteMonitor

async def main():
    async with QuoteService() as service:
        # interval=0.5 表示每 0.5 秒获取一次
        monitor = QuoteMonitor(service, interval=0.5)

        async def on_change(df):
            """回调：将变化的行情发送到消息队列"""
            for code, row in df.iterrows():
                await mq.send(code, row.to_dict())

        await monitor.run(on_change)

asyncio.run(main())
```

### 去重规则

每批次行情与内存缓存逐行对比，判断逻辑：

| 条件 | 结果 |
|------|------|
| code 不在缓存中 | 保留（新股票） |
| `datetime` 晚于缓存 | 保留（时间更新） |
| `datetime` 早于缓存 | 丢弃（旧数据） |
| `datetime` 相同且比较字段全一致 | 丢弃（完全重复） |
| `datetime` 相同但字段有变化 | 保留（同一秒内更新） |

保留的行会同步更新缓存，确保下一批次继续与最新数据对比。

### 自定义比较字段

默认比较 `DATA_COLUMNS` 中除 `datetime` 外的所有字段。如只需比较价格和成交量：

```python
monitor = QuoteMonitor(
    service,
    interval=0.1,
    compare_fields=["now", "high", "low", "open", "volume", "turnover"],
)
```

### 同步回调

回调也支持同步函数：

```python
monitor = QuoteMonitor(service, interval=1.0)

def on_change(df):
    for code, row in df.iterrows():
        print(f"{code} {row['now']} {row['datetime']}")

await monitor.run(on_change)
```

### 停止循环

```python
monitor = QuoteMonitor(service, interval=0.5)

async def on_change(df):
    # 满足条件时停止
    if some_condition:
        monitor.stop()

await monitor.run(on_change)
```

### 异常处理

- **网络请求异常**：不中断循环，等待 `interval` 秒后自动重试
- **回调异常**：向上传播并终止循环（如消息队列断连，需调用方决定是否重启）

## 性能对比

与 easyquotation 对比（5500+ 只股票）：

| 数据源 | easyquotation | qdata_quote sync | qdata_quote async |
|--------|--------------|-------------------|-------------------|
| 新浪 | ~710ms | **~640ms** | **~640ms** |
| 腾讯 | ~1830ms | **~1720ms** | **~1680ms** |

三个 source 实测对比（全 A 股 5527 只）：

| source | 耗时 | 速率 | 字段数 | 市场支持 |
|--------|------|------|--------|---------|
| sina | 0.12s | 44304 只/秒 | 42 | 仅 A 股 |
| tencent | 0.28s | 19680 只/秒 | 42 | 仅 A 股 |
| **tencent_full** | **0.48s** | 11479 只/秒 | **66** | **A 股 + 港股 + 美股 + ETF + 北交所** |

- 同步引擎：`requests` + `ThreadPoolExecutor` 并发请求
- 异步引擎：`aiohttp` + `asyncio.gather` 并发请求
- 解析优化：文本合并后一次性正则匹配，元组直接构建 DataFrame
- tencent_full：qt 端点纯文本解析（GBK `~` 分隔），无 JSON 开销

## 运行基准测试

```bash
python -m qdata_quote.bench
```

## 依赖

- Python >= 3.10
- requests >= 2.28
- aiohttp >= 3.9
- pandas >= 2.0
