qdata-quote 0.2.0__tar.gz → 0.2.2__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.
- qdata_quote-0.2.2/PKG-INFO +145 -0
- qdata_quote-0.2.2/README.md +131 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/pyproject.toml +1 -1
- qdata_quote-0.2.2/src/qdata_quote/sources/sina.py +111 -0
- qdata_quote-0.2.2/src/qdata_quote/sources/tencent.py +114 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_tencent.py +1 -1
- qdata_quote-0.2.0/PKG-INFO +0 -17
- qdata_quote-0.2.0/README.md +0 -3
- qdata_quote-0.2.0/src/qdata_quote/sources/sina.py +0 -103
- qdata_quote-0.2.0/src/qdata_quote/sources/tencent.py +0 -122
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/.gitignore +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/docs/superpowers/plans/2026-06-12-quote-service.md +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/docs/superpowers/specs/2026-06-12-quote-service-design.md +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/__init__.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/bench.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/service.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/sources/__init__.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/sources/base.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/types.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_service.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_sina.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_types.py +0 -0
- {qdata_quote-0.2.0 → qdata_quote-0.2.2}/uv.lock +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qdata-quote
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: 实时行情采集服务,支持新浪和腾讯数据源
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: aiohttp>=3.9
|
|
8
|
+
Requires-Dist: pandas>=2.0
|
|
9
|
+
Requires-Dist: requests>=2.28
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# qdata-quote
|
|
16
|
+
|
|
17
|
+
实时 A 股行情采集服务,支持新浪和腾讯两个数据源。性能超越 easyquotation 约 8-10%。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install qdata-quote
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from qdata_quote import QuoteService
|
|
29
|
+
|
|
30
|
+
service = QuoteService()
|
|
31
|
+
|
|
32
|
+
# 设置全市场股票代码(必需)
|
|
33
|
+
service.set_stock_codes(["000001", "600000", "600036", "300750"])
|
|
34
|
+
|
|
35
|
+
# 同步获取指定股票行情
|
|
36
|
+
df = service.get_real_sync(["000001", "600000"])
|
|
37
|
+
print(df)
|
|
38
|
+
|
|
39
|
+
# 同步获取全市场行情快照
|
|
40
|
+
df_all = service.get_all_sync()
|
|
41
|
+
|
|
42
|
+
# 异步获取(高性能路径)
|
|
43
|
+
import asyncio
|
|
44
|
+
df = asyncio.run(service.get_real(["000001", "600000"]))
|
|
45
|
+
df_all = asyncio.run(service.get_all())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 数据源
|
|
49
|
+
|
|
50
|
+
支持两个数据源,通过 `source` 参数指定:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# 新浪源(默认)
|
|
54
|
+
df = service.get_real_sync(["000001"], source="sina")
|
|
55
|
+
|
|
56
|
+
# 腾讯源(字段更丰富)
|
|
57
|
+
df = service.get_real_sync(["000001"], source="tencent")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| 数据源 | 每批数量 | 特点 |
|
|
61
|
+
|--------|---------|------|
|
|
62
|
+
| sina | 800 只/批 | 速度快,基础字段齐全 |
|
|
63
|
+
| tencent | 60 只/批 | 额外提供涨跌、市盈率、市值、量比等 |
|
|
64
|
+
|
|
65
|
+
## 返回格式
|
|
66
|
+
|
|
67
|
+
返回统一的 `pandas.DataFrame`,index 为带市场前缀的股票代码(如 `sh600000`、`sz000001`)。
|
|
68
|
+
|
|
69
|
+
### 字段列表
|
|
70
|
+
|
|
71
|
+
| 字段 | 类型 | 新浪 | 腾讯 | 说明 |
|
|
72
|
+
|------|------|:----:|:----:|------|
|
|
73
|
+
| code | str | ✅ | ✅ | 股票代码(index) |
|
|
74
|
+
| name | str | ✅ | ✅ | 股票名称 |
|
|
75
|
+
| open | float | ✅ | ✅ | 开盘价 |
|
|
76
|
+
| close | float | ✅ | ✅ | 昨收价 |
|
|
77
|
+
| now | float | ✅ | ✅ | 当前价 |
|
|
78
|
+
| high | float | ✅ | ✅ | 最高价 |
|
|
79
|
+
| low | float | ✅ | ✅ | 最低价 |
|
|
80
|
+
| buy | float | ✅ | - | 买一价 |
|
|
81
|
+
| sell | float | ✅ | - | 卖一价 |
|
|
82
|
+
| volume | float | ✅ | ✅ | 成交量(股) |
|
|
83
|
+
| turnover | float | ✅ | ✅ | 成交额(元) |
|
|
84
|
+
| bid1 ~ bid5 | float | ✅ | ✅ | 买一到买五价 |
|
|
85
|
+
| bid1_volume ~ bid5_volume | float | ✅ | ✅ | 买一到买五量(股) |
|
|
86
|
+
| ask1 ~ ask5 | float | ✅ | ✅ | 卖一到卖五价 |
|
|
87
|
+
| ask1_volume ~ ask5_volume | float | ✅ | ✅ | 卖一到卖五量(股) |
|
|
88
|
+
| datetime | str | ✅ | ✅ | 行情时间 |
|
|
89
|
+
| change | float | - | ✅ | 涨跌额 |
|
|
90
|
+
| change_pct | float | - | ✅ | 涨跌幅(%) |
|
|
91
|
+
| amplitude | float | - | ✅ | 振幅 |
|
|
92
|
+
| pe_dynamic | float | - | ✅ | 动态市盈率 |
|
|
93
|
+
| pe_static | float | - | ✅ | 静态市盈率 |
|
|
94
|
+
| pb | float | - | ✅ | 市净率 |
|
|
95
|
+
| total_market_cap | float | - | ✅ | 总市值 |
|
|
96
|
+
| circulating_market_cap | float | - | ✅ | 流通市值 |
|
|
97
|
+
| volume_ratio | float | - | ✅ | 量比 |
|
|
98
|
+
| bid_ask_ratio | float | - | ✅ | 委比 |
|
|
99
|
+
| avg_price | float | - | ✅ | 均价 |
|
|
100
|
+
| limit_up | float | - | ✅ | 涨停价 |
|
|
101
|
+
| limit_down | float | - | ✅ | 跌停价 |
|
|
102
|
+
|
|
103
|
+
✅ 表示有数据,- 表示 NaN。腾讯源提供更丰富的衍生指标。
|
|
104
|
+
|
|
105
|
+
## 会话管理
|
|
106
|
+
|
|
107
|
+
建议使用上下文管理器复用连接,在频繁轮询场景下性能更佳:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# 同步
|
|
111
|
+
with QuoteService() as service:
|
|
112
|
+
service.set_stock_codes(codes)
|
|
113
|
+
df = service.get_all_sync()
|
|
114
|
+
|
|
115
|
+
# 异步
|
|
116
|
+
async with QuoteService() as service:
|
|
117
|
+
service.set_stock_codes(codes)
|
|
118
|
+
df = await service.get_all()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 性能对比
|
|
122
|
+
|
|
123
|
+
与 easyquotation 对比(5610 只股票):
|
|
124
|
+
|
|
125
|
+
| 数据源 | easyquotation | qdata_quote sync | qdata_quote async |
|
|
126
|
+
|--------|--------------|-------------------|-------------------|
|
|
127
|
+
| 新浪 | ~710ms | **~640ms** | **~640ms** |
|
|
128
|
+
| 腾讯 | ~1830ms | **~1720ms** | **~1680ms** |
|
|
129
|
+
|
|
130
|
+
- 同步引擎:`requests` + `ThreadPoolExecutor` 并发请求
|
|
131
|
+
- 异步引擎:`aiohttp` + `asyncio.gather` 并发请求
|
|
132
|
+
- 解析优化:文本合并后一次性正则匹配,元组直接构建 DataFrame
|
|
133
|
+
|
|
134
|
+
## 运行基准测试
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m qdata_quote.bench
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 依赖
|
|
141
|
+
|
|
142
|
+
- Python >= 3.10
|
|
143
|
+
- requests >= 2.28
|
|
144
|
+
- aiohttp >= 3.9
|
|
145
|
+
- pandas >= 2.0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# qdata-quote
|
|
2
|
+
|
|
3
|
+
实时 A 股行情采集服务,支持新浪和腾讯两个数据源。性能超越 easyquotation 约 8-10%。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install qdata-quote
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from qdata_quote import QuoteService
|
|
15
|
+
|
|
16
|
+
service = QuoteService()
|
|
17
|
+
|
|
18
|
+
# 设置全市场股票代码(必需)
|
|
19
|
+
service.set_stock_codes(["000001", "600000", "600036", "300750"])
|
|
20
|
+
|
|
21
|
+
# 同步获取指定股票行情
|
|
22
|
+
df = service.get_real_sync(["000001", "600000"])
|
|
23
|
+
print(df)
|
|
24
|
+
|
|
25
|
+
# 同步获取全市场行情快照
|
|
26
|
+
df_all = service.get_all_sync()
|
|
27
|
+
|
|
28
|
+
# 异步获取(高性能路径)
|
|
29
|
+
import asyncio
|
|
30
|
+
df = asyncio.run(service.get_real(["000001", "600000"]))
|
|
31
|
+
df_all = asyncio.run(service.get_all())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 数据源
|
|
35
|
+
|
|
36
|
+
支持两个数据源,通过 `source` 参数指定:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# 新浪源(默认)
|
|
40
|
+
df = service.get_real_sync(["000001"], source="sina")
|
|
41
|
+
|
|
42
|
+
# 腾讯源(字段更丰富)
|
|
43
|
+
df = service.get_real_sync(["000001"], source="tencent")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| 数据源 | 每批数量 | 特点 |
|
|
47
|
+
|--------|---------|------|
|
|
48
|
+
| sina | 800 只/批 | 速度快,基础字段齐全 |
|
|
49
|
+
| tencent | 60 只/批 | 额外提供涨跌、市盈率、市值、量比等 |
|
|
50
|
+
|
|
51
|
+
## 返回格式
|
|
52
|
+
|
|
53
|
+
返回统一的 `pandas.DataFrame`,index 为带市场前缀的股票代码(如 `sh600000`、`sz000001`)。
|
|
54
|
+
|
|
55
|
+
### 字段列表
|
|
56
|
+
|
|
57
|
+
| 字段 | 类型 | 新浪 | 腾讯 | 说明 |
|
|
58
|
+
|------|------|:----:|:----:|------|
|
|
59
|
+
| code | str | ✅ | ✅ | 股票代码(index) |
|
|
60
|
+
| name | str | ✅ | ✅ | 股票名称 |
|
|
61
|
+
| open | float | ✅ | ✅ | 开盘价 |
|
|
62
|
+
| close | float | ✅ | ✅ | 昨收价 |
|
|
63
|
+
| now | float | ✅ | ✅ | 当前价 |
|
|
64
|
+
| high | float | ✅ | ✅ | 最高价 |
|
|
65
|
+
| low | float | ✅ | ✅ | 最低价 |
|
|
66
|
+
| buy | float | ✅ | - | 买一价 |
|
|
67
|
+
| sell | float | ✅ | - | 卖一价 |
|
|
68
|
+
| volume | float | ✅ | ✅ | 成交量(股) |
|
|
69
|
+
| turnover | float | ✅ | ✅ | 成交额(元) |
|
|
70
|
+
| bid1 ~ bid5 | float | ✅ | ✅ | 买一到买五价 |
|
|
71
|
+
| bid1_volume ~ bid5_volume | float | ✅ | ✅ | 买一到买五量(股) |
|
|
72
|
+
| ask1 ~ ask5 | float | ✅ | ✅ | 卖一到卖五价 |
|
|
73
|
+
| ask1_volume ~ ask5_volume | float | ✅ | ✅ | 卖一到卖五量(股) |
|
|
74
|
+
| datetime | str | ✅ | ✅ | 行情时间 |
|
|
75
|
+
| change | float | - | ✅ | 涨跌额 |
|
|
76
|
+
| change_pct | float | - | ✅ | 涨跌幅(%) |
|
|
77
|
+
| amplitude | float | - | ✅ | 振幅 |
|
|
78
|
+
| pe_dynamic | float | - | ✅ | 动态市盈率 |
|
|
79
|
+
| pe_static | float | - | ✅ | 静态市盈率 |
|
|
80
|
+
| pb | float | - | ✅ | 市净率 |
|
|
81
|
+
| total_market_cap | float | - | ✅ | 总市值 |
|
|
82
|
+
| circulating_market_cap | float | - | ✅ | 流通市值 |
|
|
83
|
+
| volume_ratio | float | - | ✅ | 量比 |
|
|
84
|
+
| bid_ask_ratio | float | - | ✅ | 委比 |
|
|
85
|
+
| avg_price | float | - | ✅ | 均价 |
|
|
86
|
+
| limit_up | float | - | ✅ | 涨停价 |
|
|
87
|
+
| limit_down | float | - | ✅ | 跌停价 |
|
|
88
|
+
|
|
89
|
+
✅ 表示有数据,- 表示 NaN。腾讯源提供更丰富的衍生指标。
|
|
90
|
+
|
|
91
|
+
## 会话管理
|
|
92
|
+
|
|
93
|
+
建议使用上下文管理器复用连接,在频繁轮询场景下性能更佳:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# 同步
|
|
97
|
+
with QuoteService() as service:
|
|
98
|
+
service.set_stock_codes(codes)
|
|
99
|
+
df = service.get_all_sync()
|
|
100
|
+
|
|
101
|
+
# 异步
|
|
102
|
+
async with QuoteService() as service:
|
|
103
|
+
service.set_stock_codes(codes)
|
|
104
|
+
df = await service.get_all()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 性能对比
|
|
108
|
+
|
|
109
|
+
与 easyquotation 对比(5610 只股票):
|
|
110
|
+
|
|
111
|
+
| 数据源 | easyquotation | qdata_quote sync | qdata_quote async |
|
|
112
|
+
|--------|--------------|-------------------|-------------------|
|
|
113
|
+
| 新浪 | ~710ms | **~640ms** | **~640ms** |
|
|
114
|
+
| 腾讯 | ~1830ms | **~1720ms** | **~1680ms** |
|
|
115
|
+
|
|
116
|
+
- 同步引擎:`requests` + `ThreadPoolExecutor` 并发请求
|
|
117
|
+
- 异步引擎:`aiohttp` + `asyncio.gather` 并发请求
|
|
118
|
+
- 解析优化:文本合并后一次性正则匹配,元组直接构建 DataFrame
|
|
119
|
+
|
|
120
|
+
## 运行基准测试
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python -m qdata_quote.bench
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 依赖
|
|
127
|
+
|
|
128
|
+
- Python >= 3.10
|
|
129
|
+
- requests >= 2.28
|
|
130
|
+
- aiohttp >= 3.9
|
|
131
|
+
- pandas >= 2.0
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""新浪行情数据源"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from qdata_quote.sources.base import BaseSource
|
|
12
|
+
from qdata_quote.types import DATA_COLUMNS, empty_dataframe
|
|
13
|
+
|
|
14
|
+
# 新浪源产出的列(不含 code,code 作为 index)
|
|
15
|
+
_SINA_COLS = [
|
|
16
|
+
"name", "open", "close", "now", "high", "low", "buy", "sell",
|
|
17
|
+
"volume", "turnover",
|
|
18
|
+
"bid1_volume", "bid1", "bid2_volume", "bid2", "bid3_volume", "bid3",
|
|
19
|
+
"bid4_volume", "bid4", "bid5_volume", "bid5",
|
|
20
|
+
"ask1_volume", "ask1", "ask2_volume", "ask2", "ask3_volume", "ask3",
|
|
21
|
+
"ask4_volume", "ask4", "ask5_volume", "ask5",
|
|
22
|
+
"datetime",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# 新浪缺失的列(需要填充 NaN)
|
|
26
|
+
_SINA_NAN_COLS = [c for c in DATA_COLUMNS if c not in _SINA_COLS]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SinaSource(BaseSource):
|
|
30
|
+
"""新浪免费行情源"""
|
|
31
|
+
|
|
32
|
+
# 匹配带前缀的股票代码和 29 个数值字段 + 2 个日期时间字段
|
|
33
|
+
# 主正则已自然跳过空数据行(空行不满足 31 个逗号分隔字段的要求)
|
|
34
|
+
_pattern = re.compile(
|
|
35
|
+
r"(\w{2}\d+)=[^\s]([^\s,]+?)"
|
|
36
|
+
+ r",([\.\d]+)" * 29
|
|
37
|
+
+ r",([-\.\d:]+)" * 2
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def _build_url(self, prefixed_codes: list[str]) -> str:
|
|
41
|
+
"""构建请求 URL"""
|
|
42
|
+
codes_str = ",".join(prefixed_codes)
|
|
43
|
+
return f"http://hq.sinajs.cn/rn={int(time.time() * 1000)}&list={codes_str}"
|
|
44
|
+
|
|
45
|
+
def _get_headers(self) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"Referer": "http://finance.sina.com.cn/",
|
|
48
|
+
"User-Agent": (
|
|
49
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
50
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
51
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def _parse(self, text: str) -> pd.DataFrame:
|
|
56
|
+
"""解析新浪行情响应,返回统一格式的 DataFrame"""
|
|
57
|
+
text = text.replace(" ", "")
|
|
58
|
+
|
|
59
|
+
rows = []
|
|
60
|
+
for match in self._pattern.finditer(text):
|
|
61
|
+
g = match.groups()
|
|
62
|
+
try:
|
|
63
|
+
# 主正则保证所有捕获组都是合法数值,直接转换
|
|
64
|
+
rows.append((
|
|
65
|
+
g[0], # code
|
|
66
|
+
g[1], # name
|
|
67
|
+
float(g[2]), # open
|
|
68
|
+
float(g[3]), # close
|
|
69
|
+
float(g[4]), # now
|
|
70
|
+
float(g[5]), # high
|
|
71
|
+
float(g[6]), # low
|
|
72
|
+
float(g[7]), # buy
|
|
73
|
+
float(g[8]), # sell
|
|
74
|
+
int(g[9]), # volume
|
|
75
|
+
float(g[10]), # turnover
|
|
76
|
+
int(g[11]), # bid1_volume
|
|
77
|
+
float(g[12]), # bid1
|
|
78
|
+
int(g[13]), # bid2_volume
|
|
79
|
+
float(g[14]), # bid2
|
|
80
|
+
int(g[15]), # bid3_volume
|
|
81
|
+
float(g[16]), # bid3
|
|
82
|
+
int(g[17]), # bid4_volume
|
|
83
|
+
float(g[18]), # bid4
|
|
84
|
+
int(g[19]), # bid5_volume
|
|
85
|
+
float(g[20]), # bid5
|
|
86
|
+
int(g[21]), # ask1_volume
|
|
87
|
+
float(g[22]), # ask1
|
|
88
|
+
int(g[23]), # ask2_volume
|
|
89
|
+
float(g[24]), # ask2
|
|
90
|
+
int(g[25]), # ask3_volume
|
|
91
|
+
float(g[26]), # ask3
|
|
92
|
+
int(g[27]), # ask4_volume
|
|
93
|
+
float(g[28]), # ask4
|
|
94
|
+
int(g[29]), # ask5_volume
|
|
95
|
+
float(g[30]), # ask5
|
|
96
|
+
g[31] + " " + g[32], # datetime
|
|
97
|
+
))
|
|
98
|
+
except (IndexError, ValueError):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
if not rows:
|
|
102
|
+
return empty_dataframe()
|
|
103
|
+
|
|
104
|
+
# 从元组列表直接构建 DataFrame
|
|
105
|
+
cols = ["code"] + _SINA_COLS
|
|
106
|
+
df = pd.DataFrame(rows, columns=cols).set_index("code")
|
|
107
|
+
# 填充新浪缺失的列为 NaN
|
|
108
|
+
for col in _SINA_NAN_COLS:
|
|
109
|
+
df[col] = np.nan
|
|
110
|
+
df = df[DATA_COLUMNS]
|
|
111
|
+
return df
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""腾讯行情数据源"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from qdata_quote.sources.base import BaseSource
|
|
11
|
+
from qdata_quote.types import DATA_COLUMNS, empty_dataframe
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _safe_float(fields: list[str], index: int) -> float:
|
|
15
|
+
"""安全获取指定索引的浮点值"""
|
|
16
|
+
try:
|
|
17
|
+
return float(fields[index])
|
|
18
|
+
except (IndexError, ValueError):
|
|
19
|
+
return float("nan")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TencentSource(BaseSource):
|
|
23
|
+
"""腾讯免费行情源"""
|
|
24
|
+
|
|
25
|
+
batch_size: int = 60 # 与 easyquotation 一致,更多批次 + 更高并发
|
|
26
|
+
|
|
27
|
+
_code_pattern = re.compile(r"(?<=_)\w+")
|
|
28
|
+
|
|
29
|
+
def _build_url(self, prefixed_codes: list[str]) -> str:
|
|
30
|
+
"""构建请求 URL"""
|
|
31
|
+
codes_str = ",".join(prefixed_codes)
|
|
32
|
+
return f"http://qt.gtimg.cn/q={codes_str}"
|
|
33
|
+
|
|
34
|
+
def _get_headers(self) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"User-Agent": (
|
|
37
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
38
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
39
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def _parse(self, text: str) -> pd.DataFrame:
|
|
44
|
+
"""解析腾讯行情响应,返回统一格式的 DataFrame"""
|
|
45
|
+
rows = []
|
|
46
|
+
for segment in text.split(";"):
|
|
47
|
+
segment = segment.strip().rstrip('"')
|
|
48
|
+
if not segment:
|
|
49
|
+
continue
|
|
50
|
+
fields = segment.split("~")
|
|
51
|
+
if len(fields) <= 49:
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
code_match = self._code_pattern.search(fields[0])
|
|
55
|
+
if not code_match:
|
|
56
|
+
continue
|
|
57
|
+
code = code_match.group()
|
|
58
|
+
|
|
59
|
+
# 元组顺序严格对应 DATA_COLUMNS
|
|
60
|
+
rows.append((
|
|
61
|
+
code,
|
|
62
|
+
fields[1], # name
|
|
63
|
+
float(fields[5]), # open
|
|
64
|
+
float(fields[4]), # close
|
|
65
|
+
float(fields[3]), # now
|
|
66
|
+
float(fields[33]), # high
|
|
67
|
+
float(fields[34]), # low
|
|
68
|
+
np.nan, # buy
|
|
69
|
+
np.nan, # sell
|
|
70
|
+
int(fields[6]) * 100, # volume 手→股
|
|
71
|
+
float(fields[37]) * 10000, # turnover 万→元
|
|
72
|
+
float(fields[9]), # bid1
|
|
73
|
+
float(fields[11]), # bid2
|
|
74
|
+
float(fields[13]), # bid3
|
|
75
|
+
float(fields[15]), # bid4
|
|
76
|
+
float(fields[17]), # bid5
|
|
77
|
+
int(fields[10]) * 100, # bid1_volume
|
|
78
|
+
int(fields[12]) * 100, # bid2_volume
|
|
79
|
+
int(fields[14]) * 100, # bid3_volume
|
|
80
|
+
int(fields[16]) * 100, # bid4_volume
|
|
81
|
+
int(fields[18]) * 100, # bid5_volume
|
|
82
|
+
float(fields[19]), # ask1
|
|
83
|
+
float(fields[21]), # ask2
|
|
84
|
+
float(fields[23]), # ask3
|
|
85
|
+
float(fields[25]), # ask4
|
|
86
|
+
float(fields[27]), # ask5
|
|
87
|
+
int(fields[20]) * 100, # ask1_volume
|
|
88
|
+
int(fields[22]) * 100, # ask2_volume
|
|
89
|
+
int(fields[24]) * 100, # ask3_volume
|
|
90
|
+
int(fields[26]) * 100, # ask4_volume
|
|
91
|
+
int(fields[28]) * 100, # ask5_volume
|
|
92
|
+
fields[30], # datetime
|
|
93
|
+
float(fields[31]), # change
|
|
94
|
+
float(fields[32]), # change_pct
|
|
95
|
+
float(fields[43]), # amplitude
|
|
96
|
+
_safe_float(fields, 52), # pe_dynamic
|
|
97
|
+
_safe_float(fields, 53), # pe_static
|
|
98
|
+
float(fields[46]), # pb
|
|
99
|
+
float(fields[45]), # total_market_cap
|
|
100
|
+
float(fields[44]), # circulating_market_cap
|
|
101
|
+
_safe_float(fields, 49), # volume_ratio
|
|
102
|
+
_safe_float(fields, 50), # bid_ask_ratio
|
|
103
|
+
_safe_float(fields, 51), # avg_price
|
|
104
|
+
float(fields[47]), # limit_up
|
|
105
|
+
float(fields[48]), # limit_down
|
|
106
|
+
))
|
|
107
|
+
except (IndexError, ValueError):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if not rows:
|
|
111
|
+
return empty_dataframe()
|
|
112
|
+
|
|
113
|
+
df = pd.DataFrame(rows, columns=["code"] + DATA_COLUMNS).set_index("code")
|
|
114
|
+
return df
|
|
@@ -160,7 +160,7 @@ class TestTencentParse:
|
|
|
160
160
|
assert len(df) == 0
|
|
161
161
|
|
|
162
162
|
def test_batch_size(self, tencent):
|
|
163
|
-
assert tencent.batch_size ==
|
|
163
|
+
assert tencent.batch_size == 60
|
|
164
164
|
|
|
165
165
|
def test_build_url(self, tencent):
|
|
166
166
|
url = tencent._build_url(["sh600000", "sz000001"])
|
qdata_quote-0.2.0/PKG-INFO
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: qdata-quote
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: 实时行情采集服务,支持新浪和腾讯数据源
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Requires-Dist: aiohttp>=3.9
|
|
8
|
-
Requires-Dist: pandas>=2.0
|
|
9
|
-
Requires-Dist: requests>=2.28
|
|
10
|
-
Provides-Extra: dev
|
|
11
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
12
|
-
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
|
-
# qdata-quote
|
|
16
|
-
|
|
17
|
-
实时行情采集服务,支持新浪和腾讯数据源。
|
qdata_quote-0.2.0/README.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""新浪行情数据源"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
import pandas as pd
|
|
9
|
-
|
|
10
|
-
from qdata_quote.sources.base import BaseSource
|
|
11
|
-
from qdata_quote.types import (
|
|
12
|
-
DATA_COLUMNS,
|
|
13
|
-
empty_dataframe,
|
|
14
|
-
safe_float,
|
|
15
|
-
safe_int,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SinaSource(BaseSource):
|
|
20
|
-
"""新浪免费行情源"""
|
|
21
|
-
|
|
22
|
-
# 匹配带前缀的股票代码和 29 个数值字段 + 2 个日期时间字段
|
|
23
|
-
_pattern = re.compile(
|
|
24
|
-
r"(\w{2}\d+)=[^\s]([^\s,]+?)"
|
|
25
|
-
+ r",([\.\d]+)" * 29
|
|
26
|
-
+ r",([-\.\d:]+)" * 2
|
|
27
|
-
)
|
|
28
|
-
_empty_pattern = re.compile(r'(\w{2}\d+)="";')
|
|
29
|
-
|
|
30
|
-
def _build_url(self, prefixed_codes: list[str]) -> str:
|
|
31
|
-
"""构建请求 URL"""
|
|
32
|
-
codes_str = ",".join(prefixed_codes)
|
|
33
|
-
return f"http://hq.sinajs.cn/rn={int(time.time() * 1000)}&list={codes_str}"
|
|
34
|
-
|
|
35
|
-
def _get_headers(self) -> dict:
|
|
36
|
-
return {
|
|
37
|
-
"Referer": "http://finance.sina.com.cn/",
|
|
38
|
-
"User-Agent": (
|
|
39
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
40
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
41
|
-
"Chrome/120.0.0.0 Safari/537.36"
|
|
42
|
-
),
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
def _parse(self, text: str) -> pd.DataFrame:
|
|
46
|
-
"""解析新浪行情响应,返回统一格式的 DataFrame"""
|
|
47
|
-
# 移除空数据行
|
|
48
|
-
text = self._empty_pattern.sub("", text)
|
|
49
|
-
text = text.replace(" ", "")
|
|
50
|
-
|
|
51
|
-
rows = []
|
|
52
|
-
for match in self._pattern.finditer(text):
|
|
53
|
-
g = match.groups()
|
|
54
|
-
try:
|
|
55
|
-
row = {
|
|
56
|
-
"name": g[1],
|
|
57
|
-
"open": safe_float(g[2]),
|
|
58
|
-
"close": safe_float(g[3]),
|
|
59
|
-
"now": safe_float(g[4]),
|
|
60
|
-
"high": safe_float(g[5]),
|
|
61
|
-
"low": safe_float(g[6]),
|
|
62
|
-
"buy": safe_float(g[7]),
|
|
63
|
-
"sell": safe_float(g[8]),
|
|
64
|
-
"volume": safe_int(g[9]),
|
|
65
|
-
"turnover": safe_float(g[10]),
|
|
66
|
-
"bid1_volume": safe_int(g[11]),
|
|
67
|
-
"bid1": safe_float(g[12]),
|
|
68
|
-
"bid2_volume": safe_int(g[13]),
|
|
69
|
-
"bid2": safe_float(g[14]),
|
|
70
|
-
"bid3_volume": safe_int(g[15]),
|
|
71
|
-
"bid3": safe_float(g[16]),
|
|
72
|
-
"bid4_volume": safe_int(g[17]),
|
|
73
|
-
"bid4": safe_float(g[18]),
|
|
74
|
-
"bid5_volume": safe_int(g[19]),
|
|
75
|
-
"bid5": safe_float(g[20]),
|
|
76
|
-
"ask1_volume": safe_int(g[21]),
|
|
77
|
-
"ask1": safe_float(g[22]),
|
|
78
|
-
"ask2_volume": safe_int(g[23]),
|
|
79
|
-
"ask2": safe_float(g[24]),
|
|
80
|
-
"ask3_volume": safe_int(g[25]),
|
|
81
|
-
"ask3": safe_float(g[26]),
|
|
82
|
-
"ask4_volume": safe_int(g[27]),
|
|
83
|
-
"ask4": safe_float(g[28]),
|
|
84
|
-
"ask5_volume": safe_int(g[29]),
|
|
85
|
-
"ask5": safe_float(g[30]),
|
|
86
|
-
"datetime": f"{g[31]} {g[32]}",
|
|
87
|
-
}
|
|
88
|
-
rows.append((g[0], row))
|
|
89
|
-
except (IndexError, ValueError):
|
|
90
|
-
continue
|
|
91
|
-
|
|
92
|
-
if not rows:
|
|
93
|
-
return empty_dataframe()
|
|
94
|
-
|
|
95
|
-
codes, data = zip(*rows)
|
|
96
|
-
df = pd.DataFrame(list(data), index=list(codes))
|
|
97
|
-
df.index.name = "code"
|
|
98
|
-
# 确保所有统一列都存在(新浪缺失的字段自动为 NaN)
|
|
99
|
-
for col in DATA_COLUMNS:
|
|
100
|
-
if col not in df.columns:
|
|
101
|
-
df[col] = float("nan")
|
|
102
|
-
df = df[DATA_COLUMNS]
|
|
103
|
-
return df
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""腾讯行情数据源"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
from qdata_quote.sources.base import BaseSource
|
|
10
|
-
from qdata_quote.types import (
|
|
11
|
-
DATA_COLUMNS,
|
|
12
|
-
empty_dataframe,
|
|
13
|
-
safe_float,
|
|
14
|
-
safe_int,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TencentSource(BaseSource):
|
|
19
|
-
"""腾讯免费行情源"""
|
|
20
|
-
|
|
21
|
-
batch_size: int = 200
|
|
22
|
-
|
|
23
|
-
_code_pattern = re.compile(r"(?<=_)\w+")
|
|
24
|
-
|
|
25
|
-
def _build_url(self, prefixed_codes: list[str]) -> str:
|
|
26
|
-
"""构建请求 URL"""
|
|
27
|
-
codes_str = ",".join(prefixed_codes)
|
|
28
|
-
return f"http://qt.gtimg.cn/q={codes_str}"
|
|
29
|
-
|
|
30
|
-
def _get_headers(self) -> dict:
|
|
31
|
-
return {
|
|
32
|
-
"User-Agent": (
|
|
33
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
34
|
-
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
35
|
-
"Chrome/120.0.0.0 Safari/537.36"
|
|
36
|
-
),
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
def _parse(self, text: str) -> pd.DataFrame:
|
|
40
|
-
"""解析腾讯行情响应,返回统一格式的 DataFrame"""
|
|
41
|
-
rows = []
|
|
42
|
-
for segment in text.split(";"):
|
|
43
|
-
segment = segment.strip().rstrip('"')
|
|
44
|
-
if not segment:
|
|
45
|
-
continue
|
|
46
|
-
fields = segment.split("~")
|
|
47
|
-
if len(fields) <= 49:
|
|
48
|
-
continue
|
|
49
|
-
try:
|
|
50
|
-
# 从 v_sh600000="1 中提取带前缀的代码
|
|
51
|
-
code_match = self._code_pattern.search(fields[0])
|
|
52
|
-
if not code_match:
|
|
53
|
-
continue
|
|
54
|
-
code = code_match.group()
|
|
55
|
-
|
|
56
|
-
row = {
|
|
57
|
-
"name": fields[1],
|
|
58
|
-
"open": safe_float(fields[5]),
|
|
59
|
-
"close": safe_float(fields[4]),
|
|
60
|
-
"now": safe_float(fields[3]),
|
|
61
|
-
"high": safe_float(fields[33]),
|
|
62
|
-
"low": safe_float(fields[34]),
|
|
63
|
-
"volume": safe_int(fields[6]) * 100, # 手 → 股
|
|
64
|
-
"turnover": safe_float(fields[37]) * 10000, # 万 → 元
|
|
65
|
-
"bid1": safe_float(fields[9]),
|
|
66
|
-
"bid1_volume": safe_int(fields[10]) * 100,
|
|
67
|
-
"bid2": safe_float(fields[11]),
|
|
68
|
-
"bid2_volume": safe_int(fields[12]) * 100,
|
|
69
|
-
"bid3": safe_float(fields[13]),
|
|
70
|
-
"bid3_volume": safe_int(fields[14]) * 100,
|
|
71
|
-
"bid4": safe_float(fields[15]),
|
|
72
|
-
"bid4_volume": safe_int(fields[16]) * 100,
|
|
73
|
-
"bid5": safe_float(fields[17]),
|
|
74
|
-
"bid5_volume": safe_int(fields[18]) * 100,
|
|
75
|
-
"ask1": safe_float(fields[19]),
|
|
76
|
-
"ask1_volume": safe_int(fields[20]) * 100,
|
|
77
|
-
"ask2": safe_float(fields[21]),
|
|
78
|
-
"ask2_volume": safe_int(fields[22]) * 100,
|
|
79
|
-
"ask3": safe_float(fields[23]),
|
|
80
|
-
"ask3_volume": safe_int(fields[24]) * 100,
|
|
81
|
-
"ask4": safe_float(fields[25]),
|
|
82
|
-
"ask4_volume": safe_int(fields[26]) * 100,
|
|
83
|
-
"ask5": safe_float(fields[27]),
|
|
84
|
-
"ask5_volume": safe_int(fields[28]) * 100,
|
|
85
|
-
"datetime": fields[30],
|
|
86
|
-
"change": safe_float(fields[31]),
|
|
87
|
-
"change_pct": safe_float(fields[32]),
|
|
88
|
-
"amplitude": safe_float(fields[43]),
|
|
89
|
-
"pe_dynamic": _safe_float_at(fields, 52),
|
|
90
|
-
"pe_static": _safe_float_at(fields, 53),
|
|
91
|
-
"pb": safe_float(fields[46]),
|
|
92
|
-
"total_market_cap": safe_float(fields[45]),
|
|
93
|
-
"circulating_market_cap": safe_float(fields[44]),
|
|
94
|
-
"volume_ratio": _safe_float_at(fields, 49),
|
|
95
|
-
"bid_ask_ratio": _safe_float_at(fields, 50),
|
|
96
|
-
"avg_price": _safe_float_at(fields, 51),
|
|
97
|
-
"limit_up": safe_float(fields[47]),
|
|
98
|
-
"limit_down": safe_float(fields[48]),
|
|
99
|
-
}
|
|
100
|
-
rows.append((code, row))
|
|
101
|
-
except (IndexError, ValueError):
|
|
102
|
-
continue
|
|
103
|
-
|
|
104
|
-
if not rows:
|
|
105
|
-
return empty_dataframe()
|
|
106
|
-
|
|
107
|
-
codes, data = zip(*rows)
|
|
108
|
-
df = pd.DataFrame(list(data), index=list(codes))
|
|
109
|
-
df.index.name = "code"
|
|
110
|
-
for col in DATA_COLUMNS:
|
|
111
|
-
if col not in df.columns:
|
|
112
|
-
df[col] = float("nan")
|
|
113
|
-
df = df[DATA_COLUMNS]
|
|
114
|
-
return df
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _safe_float_at(fields: list[str], index: int) -> float:
|
|
118
|
-
"""安全获取指定索引的浮点值,越界返回 NaN"""
|
|
119
|
-
try:
|
|
120
|
-
return safe_float(fields[index])
|
|
121
|
-
except IndexError:
|
|
122
|
-
return float("nan")
|
|
File without changes
|
|
File without changes
|
{qdata_quote-0.2.0 → qdata_quote-0.2.2}/docs/superpowers/specs/2026-06-12-quote-service-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|