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.
Files changed (23) hide show
  1. qdata_quote-0.2.2/PKG-INFO +145 -0
  2. qdata_quote-0.2.2/README.md +131 -0
  3. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/pyproject.toml +1 -1
  4. qdata_quote-0.2.2/src/qdata_quote/sources/sina.py +111 -0
  5. qdata_quote-0.2.2/src/qdata_quote/sources/tencent.py +114 -0
  6. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_tencent.py +1 -1
  7. qdata_quote-0.2.0/PKG-INFO +0 -17
  8. qdata_quote-0.2.0/README.md +0 -3
  9. qdata_quote-0.2.0/src/qdata_quote/sources/sina.py +0 -103
  10. qdata_quote-0.2.0/src/qdata_quote/sources/tencent.py +0 -122
  11. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/.gitignore +0 -0
  12. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/docs/superpowers/plans/2026-06-12-quote-service.md +0 -0
  13. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/docs/superpowers/specs/2026-06-12-quote-service-design.md +0 -0
  14. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/__init__.py +0 -0
  15. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/bench.py +0 -0
  16. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/service.py +0 -0
  17. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/sources/__init__.py +0 -0
  18. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/sources/base.py +0 -0
  19. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/src/qdata_quote/types.py +0 -0
  20. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_service.py +0 -0
  21. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_sina.py +0 -0
  22. {qdata_quote-0.2.0 → qdata_quote-0.2.2}/tests/test_types.py +0 -0
  23. {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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qdata-quote"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "实时行情采集服务,支持新浪和腾讯数据源"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 == 200
163
+ assert tencent.batch_size == 60
164
164
 
165
165
  def test_build_url(self, tencent):
166
166
  url = tencent._build_url(["sh600000", "sz000001"])
@@ -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
- 实时行情采集服务,支持新浪和腾讯数据源。
@@ -1,3 +0,0 @@
1
- # qdata-quote
2
-
3
- 实时行情采集服务,支持新浪和腾讯数据源。
@@ -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