qdc 0.2.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.
qdc-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: qdc
3
+ Version: 0.2.0
4
+ Summary: QDC online data client (tushare-compatible) for the QDC cloud data bridge.
5
+ Author: QDC Data
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://data.qdc-data.com
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.25
11
+ Requires-Dist: pandas>=1.1
12
+
13
+ # qdc
14
+
15
+ QDC「云端数据桥」的 Python 连接器(`https://data.qdc-data.com`)。
16
+ 返回 pandas DataFrame,并带一层 **tushare 兼容壳**,重叠接口零迁移。
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0-py3-none-any.whl
22
+ ```
23
+
24
+ 源码包(需要自行构建时用):
25
+
26
+ ```bash
27
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0.tar.gz
28
+ ```
29
+
30
+ > ⚠️ **不要写 `pip install qdc`**。这个包**没有发布到 PyPI**,PyPI 上那个 `qdc` 不是我们的东西。
31
+ > 本文件在 v0.1.0 里曾经写着 `pip install qdc` —— 那条命令从来没能用过。
32
+
33
+ 需要 Python ≥ 3.8、`requests`、`pandas`(会自动装)。
34
+
35
+ ## 三行上手
36
+
37
+ ```python
38
+ import qdc
39
+ qdc.connect("你的API密钥", cache_dir="~/.qdc")
40
+ df = qdc.daily("600519.SH", "20240101", "20240105") # -> pandas DataFrame
41
+ ```
42
+
43
+ `df` 的列:`ts_code, trade_date, open, high, low, close, vol, amount`(字段已对齐 tushare)。
44
+
45
+ ## tushare 兼容用法
46
+
47
+ ```python
48
+ import qdc
49
+ pro = qdc.pro_api("你的API密钥")
50
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20241231")
51
+ df = pro.moneyflow(ts_code="600519.SH", start_date="20240101", end_date="20240131")
52
+ df = pro.top_list(trade_date="20240105") # 龙虎榜
53
+ ```
54
+
55
+ ## 原生接口
56
+
57
+ ```python
58
+ c = qdc.connect(key, cache_dir="~/.qdc") # 或 qdc.Client(key)
59
+
60
+ qdc.query(sku, code=, start=, end=, table=, limit=, fields=, date_field=, key=)
61
+ qdc.daily(code, start, end, adjust=None) # adjust: None / "hfq" 后复权 / "qfq" 前复权
62
+ qdc.moneyflow(code, start, end)
63
+ qdc.lhb(code=None, date=None) # 龙虎榜
64
+ qdc.financials(code, statement="income") # income 利润表 / balance 资产负债表 / cashflow 现金流量表
65
+ qdc.index_daily(code, start, end) # 指数日线
66
+ qdc.adj_factor(code, start, end) # 复权因子(稀疏表,见下)
67
+ qdc.adj_factor_table() # 全市场复权因子整表(自动翻页 + 缓存)
68
+
69
+ qdc.universe(st=False, list_days_gt=None, amount_gt=None, index=None, industry=None)
70
+ qdc.lookup("茅台") # 名称/代码模糊查
71
+ qdc.describe("cn_daily_full") # 字段/口径/覆盖范围
72
+ ```
73
+
74
+ ## 三件必须知道的事
75
+
76
+ ### 1. 你拿到的是不是全部 —— 看 `df.attrs["qdc"]`
77
+
78
+ 服务端单次最多返回 10,000 行(你这一档的 `max_rows`)。**本连接器会自动翻页取全**,
79
+ 并把过程记在 `df.attrs["qdc"]` 里:
80
+
81
+ ```python
82
+ df = qdc.query("stocks_1m", code="002027.SZ", start="20240101", end="20241231")
83
+ print(len(df)) # 58322 —— 全年真实行数
84
+ print(df.attrs["qdc"]["pages"]) # 6 —— 翻了 6 页
85
+ print(df.attrs["qdc"]["complete"]) # True —— 取完了
86
+ ```
87
+
88
+ `complete` 为 `False` 只会在撞到 `max_pages` 上限时出现,那时同时会抛一条警告。
89
+
90
+ > ★ v0.1.0 **不翻页**:上面这个查询它只交给你第一页 10,000 行,HTTP 200、没有任何提示 ——
91
+ > 少 82.9% 的数据,而你的回测不会告诉你。这是升级到 0.2.0 最重要的理由。
92
+
93
+ ### 2. 复权是在本地算的
94
+
95
+ 原「A股日线(后复权)」商品已于 2026-08-09 退役(它是纯派生数据)。现在:
96
+
97
+ - `adjust="hfq"` = 取不复权日线 + 复权因子,本地按 `不复权价 × 复权因子` 还原;
98
+ - `adjust="qfq"` = 后复权再除以该票最新因子(以最新价为基准);
99
+ - 成交量与成交额**不参与复权**(退役登记实测:后复权的 vol/amount 与不复权完全相同)。
100
+
101
+ 因子表是**稀疏**的(只在除权日有记录),按代码前向填充;整表未收录的股票表示从未除权,按因子 = 1。
102
+ 如果某票在你这一档深度内取不到「该交易日之前最近一条」因子,对应行的价格会置为 `NaN` 并告警 ——
103
+ **不会**按因子 = 1 蒙混过去给你一个错价。
104
+
105
+ 因子表整表约 5.4 万行,`connect(cache_dir=...)` 之后会缓存到本地,不必每次重取。
106
+
107
+ ### 3. 限额:同一只票每台设备每天只能取一次
108
+
109
+ 去重键是 `(密钥, 设备, 代码, 日期)`,**不含数据集也不含表名**。所以:
110
+
111
+ - 先 `daily("600519.SH")` 再 `adj_factor("600519.SH")` → 第二次必然 429。
112
+ (正因为如此,`adjust="hfq"` 才用**不带代码的整表**去取因子 —— 那条路不计次。)
113
+ - `financials(code, "income")` 之后当天再取同一只票的 `balance` 也会 429。
114
+ 三张表要分三天取,或者走整包下载。
115
+ - **翻页不重复计次**:第 2 页起算同一次取数。
116
+
117
+ `device` 必须是**固定不变**的串。每次现生成新串会把设备槽位一个个占满;
118
+ 不传时本连接器会按主机名 + 网卡地址生成一个稳定值。
119
+
120
+ ## 约定
121
+
122
+ - 代码格式:`600519.SH` / `000001.SZ`(内部 `.XSHG/.XSHE/.XBSE` 会自动归一,大小写不敏感)。
123
+ - 日期:`YYYYMMDD` 字符串。
124
+
125
+ ## 错误
126
+
127
+ HTTP `401`(没有密钥)、`403`(未授权的数据集 / 设备数超限)、`429`(限额)等一律抛 `qdc.QDCError`,
128
+ 带服务端给的 `message` 和 `.status_code`。
129
+
130
+ ```python
131
+ try:
132
+ qdc.daily("600519.SH", "20240101", "20240105")
133
+ except qdc.QDCError as e:
134
+ print(e.status_code, e.message)
135
+ ```
136
+
137
+ ## 版本
138
+
139
+ ### 0.2.0(2026-09-06)
140
+
141
+ 三处都属于「不报错、只给错答案」,升级前请留意你既有代码的结论可能变:
142
+
143
+ 1. **自动翻页**。之前只取第一页,一只票一年分钟线交给你 10,000 行(真实 58,322 行),零提示。
144
+ 2. **复权**。之前 `adjust="hfq"` 打到已退役的 `cn_daily_hfq` 直接 422 报错;
145
+ `adjust="qfq"` 更糟 —— 它静默返回**不复权**价。现在两者都在本地正确还原。
146
+ 3. **财报表名**。之前 `statement="balance"` 打到不存在的表名 `balancesheet`,
147
+ 服务端静默回退到默认表,于是你要资产负债表、拿到的是**利润表**,HTTP 200 无提示。
148
+ 现在表名在本地校验,拼错当场报错。
149
+
150
+ 另外:`index_daily` 之前指向 A 股日线表,查指数稳定返回 0 行,现已改指「指数日线」;
151
+ 查询已退役的数据集会在本地给出继任者和迁移办法,而不是一句 `unknown sku`。
152
+
153
+ ### 0.1.0
154
+
155
+ 首个版本(从未对外分发)。
qdc-0.2.0/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # qdc
2
+
3
+ QDC「云端数据桥」的 Python 连接器(`https://data.qdc-data.com`)。
4
+ 返回 pandas DataFrame,并带一层 **tushare 兼容壳**,重叠接口零迁移。
5
+
6
+ ## 安装
7
+
8
+ ```bash
9
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0-py3-none-any.whl
10
+ ```
11
+
12
+ 源码包(需要自行构建时用):
13
+
14
+ ```bash
15
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0.tar.gz
16
+ ```
17
+
18
+ > ⚠️ **不要写 `pip install qdc`**。这个包**没有发布到 PyPI**,PyPI 上那个 `qdc` 不是我们的东西。
19
+ > 本文件在 v0.1.0 里曾经写着 `pip install qdc` —— 那条命令从来没能用过。
20
+
21
+ 需要 Python ≥ 3.8、`requests`、`pandas`(会自动装)。
22
+
23
+ ## 三行上手
24
+
25
+ ```python
26
+ import qdc
27
+ qdc.connect("你的API密钥", cache_dir="~/.qdc")
28
+ df = qdc.daily("600519.SH", "20240101", "20240105") # -> pandas DataFrame
29
+ ```
30
+
31
+ `df` 的列:`ts_code, trade_date, open, high, low, close, vol, amount`(字段已对齐 tushare)。
32
+
33
+ ## tushare 兼容用法
34
+
35
+ ```python
36
+ import qdc
37
+ pro = qdc.pro_api("你的API密钥")
38
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20241231")
39
+ df = pro.moneyflow(ts_code="600519.SH", start_date="20240101", end_date="20240131")
40
+ df = pro.top_list(trade_date="20240105") # 龙虎榜
41
+ ```
42
+
43
+ ## 原生接口
44
+
45
+ ```python
46
+ c = qdc.connect(key, cache_dir="~/.qdc") # 或 qdc.Client(key)
47
+
48
+ qdc.query(sku, code=, start=, end=, table=, limit=, fields=, date_field=, key=)
49
+ qdc.daily(code, start, end, adjust=None) # adjust: None / "hfq" 后复权 / "qfq" 前复权
50
+ qdc.moneyflow(code, start, end)
51
+ qdc.lhb(code=None, date=None) # 龙虎榜
52
+ qdc.financials(code, statement="income") # income 利润表 / balance 资产负债表 / cashflow 现金流量表
53
+ qdc.index_daily(code, start, end) # 指数日线
54
+ qdc.adj_factor(code, start, end) # 复权因子(稀疏表,见下)
55
+ qdc.adj_factor_table() # 全市场复权因子整表(自动翻页 + 缓存)
56
+
57
+ qdc.universe(st=False, list_days_gt=None, amount_gt=None, index=None, industry=None)
58
+ qdc.lookup("茅台") # 名称/代码模糊查
59
+ qdc.describe("cn_daily_full") # 字段/口径/覆盖范围
60
+ ```
61
+
62
+ ## 三件必须知道的事
63
+
64
+ ### 1. 你拿到的是不是全部 —— 看 `df.attrs["qdc"]`
65
+
66
+ 服务端单次最多返回 10,000 行(你这一档的 `max_rows`)。**本连接器会自动翻页取全**,
67
+ 并把过程记在 `df.attrs["qdc"]` 里:
68
+
69
+ ```python
70
+ df = qdc.query("stocks_1m", code="002027.SZ", start="20240101", end="20241231")
71
+ print(len(df)) # 58322 —— 全年真实行数
72
+ print(df.attrs["qdc"]["pages"]) # 6 —— 翻了 6 页
73
+ print(df.attrs["qdc"]["complete"]) # True —— 取完了
74
+ ```
75
+
76
+ `complete` 为 `False` 只会在撞到 `max_pages` 上限时出现,那时同时会抛一条警告。
77
+
78
+ > ★ v0.1.0 **不翻页**:上面这个查询它只交给你第一页 10,000 行,HTTP 200、没有任何提示 ——
79
+ > 少 82.9% 的数据,而你的回测不会告诉你。这是升级到 0.2.0 最重要的理由。
80
+
81
+ ### 2. 复权是在本地算的
82
+
83
+ 原「A股日线(后复权)」商品已于 2026-08-09 退役(它是纯派生数据)。现在:
84
+
85
+ - `adjust="hfq"` = 取不复权日线 + 复权因子,本地按 `不复权价 × 复权因子` 还原;
86
+ - `adjust="qfq"` = 后复权再除以该票最新因子(以最新价为基准);
87
+ - 成交量与成交额**不参与复权**(退役登记实测:后复权的 vol/amount 与不复权完全相同)。
88
+
89
+ 因子表是**稀疏**的(只在除权日有记录),按代码前向填充;整表未收录的股票表示从未除权,按因子 = 1。
90
+ 如果某票在你这一档深度内取不到「该交易日之前最近一条」因子,对应行的价格会置为 `NaN` 并告警 ——
91
+ **不会**按因子 = 1 蒙混过去给你一个错价。
92
+
93
+ 因子表整表约 5.4 万行,`connect(cache_dir=...)` 之后会缓存到本地,不必每次重取。
94
+
95
+ ### 3. 限额:同一只票每台设备每天只能取一次
96
+
97
+ 去重键是 `(密钥, 设备, 代码, 日期)`,**不含数据集也不含表名**。所以:
98
+
99
+ - 先 `daily("600519.SH")` 再 `adj_factor("600519.SH")` → 第二次必然 429。
100
+ (正因为如此,`adjust="hfq"` 才用**不带代码的整表**去取因子 —— 那条路不计次。)
101
+ - `financials(code, "income")` 之后当天再取同一只票的 `balance` 也会 429。
102
+ 三张表要分三天取,或者走整包下载。
103
+ - **翻页不重复计次**:第 2 页起算同一次取数。
104
+
105
+ `device` 必须是**固定不变**的串。每次现生成新串会把设备槽位一个个占满;
106
+ 不传时本连接器会按主机名 + 网卡地址生成一个稳定值。
107
+
108
+ ## 约定
109
+
110
+ - 代码格式:`600519.SH` / `000001.SZ`(内部 `.XSHG/.XSHE/.XBSE` 会自动归一,大小写不敏感)。
111
+ - 日期:`YYYYMMDD` 字符串。
112
+
113
+ ## 错误
114
+
115
+ HTTP `401`(没有密钥)、`403`(未授权的数据集 / 设备数超限)、`429`(限额)等一律抛 `qdc.QDCError`,
116
+ 带服务端给的 `message` 和 `.status_code`。
117
+
118
+ ```python
119
+ try:
120
+ qdc.daily("600519.SH", "20240101", "20240105")
121
+ except qdc.QDCError as e:
122
+ print(e.status_code, e.message)
123
+ ```
124
+
125
+ ## 版本
126
+
127
+ ### 0.2.0(2026-09-06)
128
+
129
+ 三处都属于「不报错、只给错答案」,升级前请留意你既有代码的结论可能变:
130
+
131
+ 1. **自动翻页**。之前只取第一页,一只票一年分钟线交给你 10,000 行(真实 58,322 行),零提示。
132
+ 2. **复权**。之前 `adjust="hfq"` 打到已退役的 `cn_daily_hfq` 直接 422 报错;
133
+ `adjust="qfq"` 更糟 —— 它静默返回**不复权**价。现在两者都在本地正确还原。
134
+ 3. **财报表名**。之前 `statement="balance"` 打到不存在的表名 `balancesheet`,
135
+ 服务端静默回退到默认表,于是你要资产负债表、拿到的是**利润表**,HTTP 200 无提示。
136
+ 现在表名在本地校验,拼错当场报错。
137
+
138
+ 另外:`index_daily` 之前指向 A 股日线表,查指数稳定返回 0 行,现已改指「指数日线」;
139
+ 查询已退役的数据集会在本地给出继任者和迁移办法,而不是一句 `unknown sku`。
140
+
141
+ ### 0.1.0
142
+
143
+ 首个版本(从未对外分发)。
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qdc"
7
+ # 版本号只有一处真源:qdc/__init__.py 的 __version__。
8
+ # 写死在这里会和包里的 __version__ 各走各的 —— 两份副本必然漂移。
9
+ dynamic = ["version"]
10
+ description = "QDC online data client (tushare-compatible) for the QDC cloud data bridge."
11
+ readme = "README.md"
12
+ requires-python = ">=3.8"
13
+ license = { text = "Proprietary" }
14
+ authors = [{ name = "QDC Data" }]
15
+ dependencies = [
16
+ "requests>=2.25",
17
+ "pandas>=1.1",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://data.qdc-data.com"
22
+
23
+ [tool.setuptools]
24
+ packages = ["qdc"]
25
+
26
+ [tool.setuptools.dynamic]
27
+ version = { attr = "qdc.__version__" }
@@ -0,0 +1,54 @@
1
+ """qdc - online data client for the QDC cloud data bridge (data.qdc-data.com).
2
+
3
+ Quick start:
4
+
5
+ import qdc
6
+ qdc.connect("YOUR_API_KEY")
7
+ df = qdc.daily("600519.SH", "20240101", "20240105")
8
+
9
+ tushare-compatible:
10
+
11
+ pro = qdc.pro_api("YOUR_API_KEY")
12
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240105")
13
+ """
14
+
15
+ from .client import (
16
+ Client,
17
+ QDCError,
18
+ connect,
19
+ query,
20
+ daily,
21
+ moneyflow,
22
+ lhb,
23
+ financials,
24
+ index_daily,
25
+ adj_factor,
26
+ adj_factor_table,
27
+ universe,
28
+ lookup,
29
+ describe,
30
+ pro_api,
31
+ ProApi,
32
+ )
33
+
34
+ __version__ = "0.2.0"
35
+
36
+ __all__ = [
37
+ "Client",
38
+ "QDCError",
39
+ "connect",
40
+ "query",
41
+ "daily",
42
+ "moneyflow",
43
+ "lhb",
44
+ "financials",
45
+ "index_daily",
46
+ "adj_factor",
47
+ "adj_factor_table",
48
+ "universe",
49
+ "lookup",
50
+ "describe",
51
+ "pro_api",
52
+ "ProApi",
53
+ "__version__",
54
+ ]
@@ -0,0 +1,576 @@
1
+ """QDC data-bridge client.
2
+
3
+ Talks to the live QDC cloud data bridge (default https://data.qdc-data.com).
4
+ Returns pandas DataFrames from the native API, and provides a tushare-compatible
5
+ shim (`pro_api`) for drop-in use.
6
+
7
+ [2026-09-06 客户端 · v0.2.0] 三处「不报错、只给错答案」的修正,见 CHANGELOG:
8
+ 1. 自动翻页 —— 之前只取第一页,一只票一年分钟线交付 10,000 行(真实 58,322 行)且零提示。
9
+ 2. 复权 —— 之前 adjust="hfq" 打到已退役的 cn_daily_hfq(422);qfq 静默返回不复权价。
10
+ 3. 财报表名 —— 之前 statement="balance" 打到不存在的表名 balancesheet,
11
+ 服务端静默回退到利润表,客户拿到的是利润表而不是资产负债表。
12
+ """
13
+
14
+ import hashlib
15
+ import os
16
+ import socket
17
+ import time
18
+ import uuid
19
+ import warnings
20
+
21
+ import pandas as pd
22
+ import requests
23
+
24
+ __all__ = [
25
+ "Client",
26
+ "QDCError",
27
+ "connect",
28
+ "query",
29
+ "daily",
30
+ "moneyflow",
31
+ "lhb",
32
+ "financials",
33
+ "index_daily",
34
+ "adj_factor",
35
+ "universe",
36
+ "lookup",
37
+ "describe",
38
+ "pro_api",
39
+ "ProApi",
40
+ ]
41
+
42
+ DEFAULT_BASE = "https://data.qdc-data.com"
43
+ _VERSION = "0.2.0"
44
+ _USER_AGENT = "qdc-python/" + _VERSION
45
+
46
+ # 翻页安全上限。到顶时【显式告警并标记 meta["complete"]=False】,绝不静默截断。
47
+ _MAX_PAGES = 200
48
+
49
+ # 价格列:复权只乘这几列。成交量/成交额不乘(退役登记实测:后复权的 vol/amount 与不复权完全相同)。
50
+ _PRICE_COLS = ("open", "high", "low", "close", "pre_close", "settle", "pre_settle")
51
+
52
+ # adjust -> 处理方式。cn_daily_hfq 已于 2026-08-09 退役,继任 = 不复权日线 + 复权因子本地还原。
53
+ _ADJUST_ALIASES = {
54
+ None: None, "": None, "none": None, "raw": None, "bfq": None,
55
+ "hfq": "hfq", "post": "hfq", "back": "hfq",
56
+ "qfq": "qfq", "pre": "qfq", "front": "qfq",
57
+ }
58
+
59
+ # statement -> cn_fin_statements 的真实表名。
60
+ # ★真源:B1 registry 的 overrides —— cn_fin_statements.duckdb::{income,balance,cashflow}。
61
+ # 注意【没有】balancesheet 这张表;传它服务端会静默回退到 income。
62
+ _FIN_TABLES = {
63
+ "income": "income",
64
+ "balance": "balance",
65
+ "balancesheet": "balance",
66
+ "balance_sheet": "balance",
67
+ "cashflow": "cashflow",
68
+ "cash_flow": "cashflow",
69
+ }
70
+
71
+ # 指数日线的 sku。旧版错指 cn_daily_full(A股日线),指数代码在那张表里不存在,
72
+ # 于是 HTTP 200 + 0 行 + 无提示 —— 和 W-275 同族的失效方式。
73
+ _INDEX_DAILY_SKU = "cn_index_1d"
74
+
75
+ # 已退役 sku -> (继任, 人话说明)。直接 query 这些名字时本地就给出可操作的错误。
76
+ _RETIRED = {
77
+ "cn_daily_hfq": (
78
+ "cn_daily_full",
79
+ "「A股日线(后复权)」已于 2026-08-09 退役,继任为「A股日线(不复权)+ 复权因子」。"
80
+ "请改用 qdc.daily(code, start, end, adjust='hfq') —— 本客户端会自动取不复权日线与复权因子并在本地还原。",
81
+ ),
82
+ "cn_tech_hfq": (
83
+ "cn_tech_calc",
84
+ "「技术指标(后复权)」已于 2026-08-09 退役,继任为「技术指标(不复权)」。"
85
+ "★指标不能像日线那样简单乘因子还原(MA/MACD/BOLL 会随复权变化),需要后复权指标请用不复权日线+复权因子复权后自行重算。",
86
+ ),
87
+ "cn_analyst_consensus": ("cn_estimates", "「分析师一致预期」已退役,继任为「IMAI一致性预期」,历史覆盖从 2017 起扩到 2005 起。"),
88
+ "intl_sanctions": ("intl_trade_measures", "「国际制裁清单(CSL)」已退役,继任为「全球贸易干预与管制清单」,原中国关联子集原样保留在新商品里。"),
89
+ }
90
+
91
+
92
+ class QDCError(Exception):
93
+ """Raised on API/HTTP errors (401 no key, 403 unauthorized sku, 429 rate limit, etc.).
94
+
95
+ Attributes:
96
+ status_code: HTTP status code (may be None for transport errors).
97
+ message: server-provided message when available.
98
+ """
99
+
100
+ def __init__(self, message, status_code=None):
101
+ super().__init__(message)
102
+ self.status_code = status_code
103
+ self.message = message
104
+
105
+
106
+ def _device_fingerprint():
107
+ """Stable short device id derived from hostname + MAC.
108
+
109
+ ★必须稳定:每次现生成新串会把 device_limit 的槽位一个个占满(槽位按最近使用保留,
110
+ 30 天内没再出现过的才会在槽位已满时被回收)。
111
+ """
112
+ try:
113
+ host = socket.gethostname()
114
+ except Exception:
115
+ host = "unknown-host"
116
+ mac = uuid.getnode() # 48-bit MAC as int (stable per machine)
117
+ raw = "{}|{}".format(host, mac).encode("utf-8")
118
+ return "dev-" + hashlib.sha1(raw).hexdigest()[:12]
119
+
120
+
121
+ class Client:
122
+ """QDC online query client. Returns pandas DataFrames.
123
+
124
+ 每个返回的 DataFrame 都带 `df.attrs["qdc"]`,里面有 pages / row_count /
125
+ complete / row_limit / limits。**complete=False 表示还有数据没取完** ——
126
+ 这是「我拿到的是不是全部」唯一可信的判据。
127
+ """
128
+
129
+ def __init__(self, key, base=DEFAULT_BASE, device=None, timeout=30,
130
+ cache_dir=None, max_pages=_MAX_PAGES):
131
+ if not key:
132
+ raise QDCError("API key is required")
133
+ self.key = key
134
+ self.base = base.rstrip("/")
135
+ self.device = device or _device_fingerprint()
136
+ self.timeout = timeout
137
+ self.max_pages = int(max_pages)
138
+ self.cache_dir = os.path.expanduser(cache_dir) if cache_dir else None
139
+ self._adj_cache = None # (fetched_at, DataFrame)
140
+ self._session = requests.Session()
141
+ self._session.headers.update(
142
+ {
143
+ "X-API-Key": self.key,
144
+ "X-Device": self.device,
145
+ # A User-Agent is REQUIRED: the CDN/WAF (Cloudflare) in front of
146
+ # data.qdc-data.com returns 403 (error 1010) for requests with an
147
+ # empty/blocked UA. requests sets one, but we make it explicit/stable.
148
+ "User-Agent": _USER_AGENT,
149
+ "Accept": "application/json",
150
+ }
151
+ )
152
+
153
+ # ---- low level -------------------------------------------------------
154
+ def _get(self, path, params):
155
+ params = {k: v for k, v in params.items() if v is not None}
156
+ url = self.base + path
157
+ try:
158
+ resp = self._session.get(url, params=params, timeout=self.timeout)
159
+ except requests.RequestException as ex:
160
+ raise QDCError("network error: {}".format(ex))
161
+ if resp.status_code >= 400:
162
+ msg = None
163
+ try:
164
+ body = resp.json()
165
+ msg = body.get("message") or body.get("error") or body.get("detail")
166
+ except Exception:
167
+ msg = (resp.text or "").strip()[:300]
168
+ raise QDCError(
169
+ "HTTP {}: {}".format(resp.status_code, msg or "request failed"),
170
+ status_code=resp.status_code,
171
+ )
172
+ try:
173
+ return resp.json()
174
+ except ValueError:
175
+ raise QDCError("invalid JSON response from server", status_code=resp.status_code)
176
+
177
+ def _get_all(self, path, params, limit=None):
178
+ """自动翻页取全。返回 (rows, info)。
179
+
180
+ 服务端单次最多返回 row_limit 行(本档 max_rows,现为 10000),并给出
181
+ `truncated` / `next_cursor`。旧版本只读 rows 就返回,于是客户静默拿到第一页。
182
+
183
+ ★翻页不重复计「每股每设备每天 1 次」:第 2 页起属于同一次取数。
184
+ ★除 cursor 外其余参数必须与第一页完全相同,否则服务端 422(而不是错位返回)。
185
+ """
186
+ base_params = {k: v for k, v in params.items() if v is not None}
187
+ rows = []
188
+ info = {"pages": 0, "complete": True, "row_limit": None, "limits": None,
189
+ "truncated_by": None, "as_of": None, "date_field": None}
190
+ cursor = None
191
+ while True:
192
+ p = dict(base_params)
193
+ if cursor:
194
+ p["cursor"] = cursor
195
+ body = self._get(path, p)
196
+ if info["pages"] == 0:
197
+ meta = body.get("meta") or {}
198
+ info["row_limit"] = body.get("row_limit")
199
+ info["limits"] = meta.get("limits")
200
+ info["as_of"] = meta.get("as_of")
201
+ info["date_field"] = meta.get("date_field")
202
+ if body.get("truncated_by"):
203
+ info["truncated_by"] = body.get("truncated_by")
204
+ rows.extend(body.get("rows") or [])
205
+ info["pages"] += 1
206
+ cursor = body.get("next_cursor")
207
+ if limit is not None and len(rows) >= limit:
208
+ rows = rows[:limit]
209
+ cursor = None
210
+ if not cursor:
211
+ break
212
+ if info["pages"] >= self.max_pages:
213
+ info["complete"] = False
214
+ warnings.warn(
215
+ "qdc: 已取 {} 页({} 行)仍未取完,达到 max_pages={} 上限而停止。"
216
+ "结果【不完整】(df.attrs['qdc']['complete'] is False)。"
217
+ "请缩小 start/end 区间,或调大 Client(max_pages=...)。".format(
218
+ info["pages"], len(rows), self.max_pages),
219
+ stacklevel=3,
220
+ )
221
+ break
222
+ info["row_count"] = len(rows)
223
+ return rows, info
224
+
225
+ @staticmethod
226
+ def _frame(rows, info, columns=None):
227
+ df = pd.DataFrame(rows, columns=columns) if (columns and not rows) else pd.DataFrame(rows)
228
+ try:
229
+ df.attrs["qdc"] = info
230
+ except Exception: # pandas < 1.0 没有 attrs,不因此失败
231
+ pass
232
+ return df
233
+
234
+ # ---- native API ------------------------------------------------------
235
+ def query(self, sku, code=None, start=None, end=None, table=None, limit=None,
236
+ fields=None, date_field=None, key=None):
237
+ """Run a query. Returns a pandas DataFrame of **all** matching rows.
238
+
239
+ 自动翻页取全;`limit` 是「我只要这么多行」的上限,不是服务端单页上限。
240
+ `df.attrs["qdc"]["complete"]` 为 False 时表示没取完(只会在撞 max_pages 时发生)。
241
+ """
242
+ if sku in _RETIRED:
243
+ successor, note = _RETIRED[sku]
244
+ raise QDCError("sku '{}' 已退役(继任 '{}')。{}".format(sku, successor, note))
245
+ params = {
246
+ "sku": sku, "code": code, "start": start, "end": end,
247
+ "table": table, "limit": limit, "fields": fields, "date_field": date_field,
248
+ }
249
+ if key:
250
+ params["key"] = key
251
+ rows, info = self._get_all("/v1/query", params, limit=limit)
252
+ return self._frame(rows, info)
253
+
254
+ # ---- 复权 -------------------------------------------------------------
255
+ def adj_factor_table(self, refresh=False, ttl_hours=12):
256
+ """全市场复权因子表(累计因子),自动翻页取全并缓存。
257
+
258
+ ★为什么不按 code 取:「每股每设备每天 1 次」的去重键是
259
+ (key, device, code, day),**不含 sku**。先取了该票日线,再按同一个 code
260
+ 取它的因子必然 429 —— 后复权就永远还原不出来。不带 code 的整表查询
261
+ 不进这个去重(服务端 `if code and not paging` 才记账),所以整表取是唯一走得通的路。
262
+ 实测整表 53,501 行 / 6 页 / 3.4 秒,取一次可反复用。
263
+ """
264
+ now = time.time()
265
+ if self._adj_cache and not refresh and (now - self._adj_cache[0]) < ttl_hours * 3600:
266
+ return self._adj_cache[1]
267
+
268
+ disk = None
269
+ if self.cache_dir:
270
+ disk = os.path.join(self.cache_dir, "cn_adj_factor.csv.gz")
271
+ if not refresh and os.path.exists(disk) and (now - os.path.getmtime(disk)) < ttl_hours * 3600:
272
+ try:
273
+ df = pd.read_csv(disk, parse_dates=["trade_date"])
274
+ self._adj_cache = (now, df)
275
+ return df
276
+ except Exception:
277
+ pass
278
+
279
+ rows, _info = self._get_all("/v1/query", {"sku": "cn_adj_factor"})
280
+ df = pd.DataFrame(rows)
281
+ if df.empty:
282
+ raise QDCError("复权因子表取回为空,无法做复权还原(sku=cn_adj_factor)")
283
+ df["trade_date"] = pd.to_datetime(df["trade_date"])
284
+ df = df.sort_values(["ts_code", "trade_date"]).reset_index(drop=True)
285
+ if disk:
286
+ try:
287
+ os.makedirs(self.cache_dir, exist_ok=True)
288
+ df.to_csv(disk, index=False, compression="gzip")
289
+ except Exception:
290
+ pass
291
+ self._adj_cache = (now, df)
292
+ return df
293
+
294
+ def _apply_adjust(self, bars, mode):
295
+ """把不复权日线还原成前/后复权。
296
+
297
+ 口径来自「A股日线(后复权)」的退役登记:
298
+ 后复权价 = 不复权价 × 复权因子;成交量与成交额不变。
299
+ ★因子表是稀疏的(只在除权日有记录),按 symbol 前向填充;
300
+ 因子表【未收录】的股票表示从未除权,按因子=1 处理。
301
+ 前复权 = 后复权 / 该票最新因子(以最新价为基准,业界通行口径)。
302
+
303
+ ★安全边界:如果某票在因子表里【有】记录,但最早那条晚于要还原的交易日,
304
+ 说明它在此之前的除权记录不在本 key 的深度范围内 —— 这时【不】假定因子=1
305
+ (那会静默给出错价),而是把这些行的价格置为 NaN 并显式告警。
306
+ """
307
+ if bars.empty or mode not in ("hfq", "qfq"):
308
+ return bars
309
+ fac_all = self.adj_factor_table()
310
+ out = bars.copy()
311
+ out["trade_date"] = pd.to_datetime(out["trade_date"])
312
+ codes = sorted(set(out["ts_code"].dropna().unique()))
313
+ fac = fac_all[fac_all["ts_code"].isin(codes)]
314
+
315
+ price_cols = [c for c in _PRICE_COLS if c in out.columns]
316
+ if not price_cols:
317
+ return bars
318
+
319
+ frames = []
320
+ no_record, gapped = [], []
321
+ for code, grp in out.groupby("ts_code", sort=False):
322
+ grp = grp.sort_values("trade_date").copy()
323
+ f = fac[fac["ts_code"] == code]
324
+ if f.empty:
325
+ grp["adj_factor"] = 1.0 # 从未除权
326
+ no_record.append(code)
327
+ else:
328
+ merged = pd.merge_asof(
329
+ grp[["trade_date"]].sort_values("trade_date"),
330
+ f[["trade_date", "adj_factor"]].sort_values("trade_date"),
331
+ on="trade_date", direction="backward",
332
+ )
333
+ grp["adj_factor"] = merged["adj_factor"].values
334
+ if grp["adj_factor"].isna().any():
335
+ gapped.append(code)
336
+ if mode == "qfq":
337
+ f_latest = float(f["adj_factor"].iloc[-1]) if not f.empty else 1.0
338
+ grp["adj_factor"] = grp["adj_factor"] / f_latest
339
+ for c in price_cols:
340
+ grp[c] = grp[c] * grp["adj_factor"]
341
+ frames.append(grp)
342
+
343
+ res = pd.concat(frames).sort_index()
344
+ if gapped:
345
+ warnings.warn(
346
+ "qdc: {} 这些代码在你这档深度内取不到「该交易日之前最近一条」复权因子,"
347
+ "对应行的价格已置为 NaN 而不是按因子=1 蒙混过去。"
348
+ "如需更早的因子请升级深度档位。代码:{}".format(len(gapped), gapped[:10]),
349
+ stacklevel=3,
350
+ )
351
+ try:
352
+ info = dict(bars.attrs.get("qdc") or {})
353
+ info["adjust"] = mode
354
+ info["adjust_note"] = "本地还原:不复权价 × 复权因子(前复权再除以最新因子);vol/amount 不变"
355
+ info["codes_never_ex_rights"] = no_record
356
+ info["codes_factor_out_of_depth"] = gapped
357
+ res.attrs["qdc"] = info
358
+ except Exception:
359
+ pass
360
+ return res
361
+
362
+ def daily(self, code, start=None, end=None, adjust=None):
363
+ """A股日线。adjust: None(不复权) / 'hfq'(后复权) / 'qfq'(前复权)。
364
+
365
+ ★后复权与前复权都是【本地还原】的:取不复权日线 + 复权因子后在本地计算。
366
+ 原「A股日线(后复权)」商品已于 2026-08-09 退役(它是纯派生数据)。
367
+ """
368
+ akey = adjust.lower() if isinstance(adjust, str) else adjust
369
+ if akey not in _ADJUST_ALIASES:
370
+ raise QDCError(
371
+ "adjust={!r} 不认识;可选:None(不复权)/ 'hfq'(后复权)/ 'qfq'(前复权)".format(adjust))
372
+ mode = _ADJUST_ALIASES[akey]
373
+ bars = self.query("cn_daily_full", code=code, start=start, end=end)
374
+ return self._apply_adjust(bars, mode) if mode else bars
375
+
376
+ def moneyflow(self, code, start=None, end=None):
377
+ """Main-force money flow (sku cn_mainflow_1d)."""
378
+ return self.query("cn_mainflow_1d", code=code, start=start, end=end)
379
+
380
+ def lhb(self, code=None, date=None):
381
+ """Dragon-Tiger list (sku cn_lhb). Filter by code and/or date (YYYYMMDD)."""
382
+ return self.query("cn_lhb", code=code, start=date, end=date)
383
+
384
+ def financials(self, code, statement="income"):
385
+ """财报三表(sku cn_fin_statements)。
386
+
387
+ statement: 'income'(利润表)/ 'balance'(资产负债表)/ 'cashflow'(现金流量表)。
388
+
389
+ ★不认识的表名在【本地】就报错。不能指望服务端:传一个不存在的表名,
390
+ 服务端会静默回退到默认表(income)并返回 HTTP 200 —— 客户要资产负债表、
391
+ 拿到利润表、没有任何提示。旧版本正是这么坏的(它传的是 'balancesheet')。
392
+ ★同一只票今天只能取一次(去重键不含表名),所以三张表要分三天取,
393
+ 或者走整包下载。
394
+ """
395
+ skey = str(statement).lower()
396
+ if skey not in _FIN_TABLES:
397
+ raise QDCError(
398
+ "statement={!r} 不认识;可选:'income'(利润表)/'balance'(资产负债表)/'cashflow'(现金流量表)".format(statement))
399
+ return self.query("cn_fin_statements", code=code, table=_FIN_TABLES[skey])
400
+
401
+ def index_daily(self, code, start=None, end=None):
402
+ """指数日线(sku cn_index_1d)。
403
+
404
+ ★旧版本错指 cn_daily_full(A股日线)。指数代码在那张表里不存在,
405
+ 于是稳定地返回 HTTP 200 + 0 行 + 无任何提示。
406
+ """
407
+ return self.query(_INDEX_DAILY_SKU, code=code, start=start, end=end)
408
+
409
+ def adj_factor(self, code, start=None, end=None):
410
+ """复权因子(sku cn_adj_factor)。
411
+
412
+ ★这张表是【稀疏】的:只在除权日有记录。查一个没有除权的区间返回 0 行是正常的,
413
+ 不是故障;查不到任何记录的股票表示它从未除权(按因子=1 处理)。
414
+ """
415
+ return self.query("cn_adj_factor", code=code, start=start, end=end)
416
+
417
+ def universe(self, st=False, list_days_gt=None, amount_gt=None, index=None, industry=None):
418
+ """Return a list of ts_codes matching the filters."""
419
+ body = self._get(
420
+ "/v1/universe",
421
+ {
422
+ "st": ("true" if st else None),
423
+ "list_days_gt": list_days_gt,
424
+ "amount_gt": amount_gt,
425
+ "index": index,
426
+ "industry": industry,
427
+ },
428
+ )
429
+ return list(body.get("codes") or [])
430
+
431
+ def lookup(self, q):
432
+ """Fuzzy name/code lookup. Returns DataFrame with columns code/name/market."""
433
+ body = self._get("/v1/lookup", {"q": q})
434
+ matches = body.get("matches") or []
435
+ return pd.DataFrame(matches) if matches else pd.DataFrame(columns=["code", "name", "market"])
436
+
437
+ def describe(self, sku, table=None):
438
+ """Describe a sku/table: returns a dict {sku, table, fields, date_col, coverage}."""
439
+ return self._get("/v1/describe", {"sku": sku, "table": table})
440
+
441
+ # ---- tushare shim ----------------------------------------------------
442
+ def pro_api(self):
443
+ """Return a tushare-compatible facade bound to this client."""
444
+ return ProApi(self)
445
+
446
+
447
+ class ProApi:
448
+ """tushare `pro_api`-style facade. Methods mirror the tushare pro interface
449
+ and map to the native QDC client. Server-side fields are already aligned to
450
+ tushare, so rows are passed through unchanged."""
451
+
452
+ def __init__(self, client):
453
+ self._c = client
454
+
455
+ def daily(self, ts_code=None, start_date=None, end_date=None, **kwargs):
456
+ return self._c.daily(ts_code, start=start_date, end=end_date)
457
+
458
+ def daily_hfq(self, ts_code=None, start_date=None, end_date=None, **kwargs):
459
+ return self._c.daily(ts_code, start=start_date, end=end_date, adjust="hfq")
460
+
461
+ def daily_qfq(self, ts_code=None, start_date=None, end_date=None, **kwargs):
462
+ return self._c.daily(ts_code, start=start_date, end=end_date, adjust="qfq")
463
+
464
+ def moneyflow(self, ts_code=None, start_date=None, end_date=None, **kwargs):
465
+ return self._c.moneyflow(ts_code, start=start_date, end=end_date)
466
+
467
+ def top_list(self, trade_date=None, ts_code=None, **kwargs):
468
+ """tushare top_list (Dragon-Tiger). Maps to lhb."""
469
+ return self._c.lhb(code=ts_code, date=trade_date)
470
+
471
+ def adj_factor(self, ts_code=None, start_date=None, end_date=None, **kwargs):
472
+ return self._c.adj_factor(ts_code, start=start_date, end=end_date)
473
+
474
+ def index_daily(self, ts_code=None, start_date=None, end_date=None, **kwargs):
475
+ return self._c.index_daily(ts_code, start=start_date, end=end_date)
476
+
477
+ def income(self, ts_code=None, **kwargs):
478
+ return self._c.financials(ts_code, statement="income")
479
+
480
+ def balancesheet(self, ts_code=None, **kwargs):
481
+ return self._c.financials(ts_code, statement="balance")
482
+
483
+ def cashflow(self, ts_code=None, **kwargs):
484
+ return self._c.financials(ts_code, statement="cashflow")
485
+
486
+ def query(self, api_name, **kwargs):
487
+ """Generic tushare-style dispatch: pro.query('daily', ts_code=...)."""
488
+ fn = getattr(self, api_name, None)
489
+ if fn is None or api_name.startswith("_"):
490
+ raise QDCError("unsupported tushare api: {}".format(api_name))
491
+ return fn(**kwargs)
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # Module-level convenience API (a default connected client)
496
+ # ---------------------------------------------------------------------------
497
+ _default = None
498
+
499
+
500
+ def _require():
501
+ if _default is None:
502
+ raise QDCError("not connected; call qdc.connect(key) first")
503
+ return _default
504
+
505
+
506
+ def connect(key, base=DEFAULT_BASE, device=None, timeout=30, cache_dir=None, max_pages=_MAX_PAGES):
507
+ """Create and register a default Client. Returns the Client.
508
+
509
+ cache_dir: 给复权因子表这类整表数据做本地缓存的目录(如 "~/.qdc")。不传则只在进程内缓存。
510
+ """
511
+ global _default
512
+ _default = Client(key, base=base, device=device, timeout=timeout,
513
+ cache_dir=cache_dir, max_pages=max_pages)
514
+ return _default
515
+
516
+
517
+ def query(sku, code=None, start=None, end=None, table=None, limit=None,
518
+ fields=None, date_field=None, key=None):
519
+ return _require().query(sku, code=code, start=start, end=end, table=table,
520
+ limit=limit, fields=fields, date_field=date_field, key=key)
521
+
522
+
523
+ def daily(code, start=None, end=None, adjust=None):
524
+ return _require().daily(code, start=start, end=end, adjust=adjust)
525
+
526
+
527
+ def moneyflow(code, start=None, end=None):
528
+ return _require().moneyflow(code, start=start, end=end)
529
+
530
+
531
+ def lhb(code=None, date=None):
532
+ return _require().lhb(code=code, date=date)
533
+
534
+
535
+ def financials(code, statement="income"):
536
+ return _require().financials(code, statement=statement)
537
+
538
+
539
+ def index_daily(code, start=None, end=None):
540
+ return _require().index_daily(code, start=start, end=end)
541
+
542
+
543
+ def adj_factor(code, start=None, end=None):
544
+ return _require().adj_factor(code, start=start, end=end)
545
+
546
+
547
+ def adj_factor_table(refresh=False):
548
+ """全市场复权因子表(整表,自动翻页 + 缓存)。"""
549
+ return _require().adj_factor_table(refresh=refresh)
550
+
551
+
552
+ def universe(st=False, list_days_gt=None, amount_gt=None, index=None, industry=None):
553
+ return _require().universe(
554
+ st=st, list_days_gt=list_days_gt, amount_gt=amount_gt, index=index, industry=industry
555
+ )
556
+
557
+
558
+ def lookup(q):
559
+ return _require().lookup(q)
560
+
561
+
562
+ def describe(sku, table=None):
563
+ return _require().describe(sku, table=table)
564
+
565
+
566
+ def pro_api(key=None, base=DEFAULT_BASE, device=None, timeout=30, cache_dir=None):
567
+ """Return a tushare-compatible facade.
568
+
569
+ If `key` is given, a fresh Client is created (and registered as default);
570
+ otherwise the already-connected default client is reused.
571
+ """
572
+ if key is not None:
573
+ c = connect(key, base=base, device=device, timeout=timeout, cache_dir=cache_dir)
574
+ else:
575
+ c = _require()
576
+ return ProApi(c)
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: qdc
3
+ Version: 0.2.0
4
+ Summary: QDC online data client (tushare-compatible) for the QDC cloud data bridge.
5
+ Author: QDC Data
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://data.qdc-data.com
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: requests>=2.25
11
+ Requires-Dist: pandas>=1.1
12
+
13
+ # qdc
14
+
15
+ QDC「云端数据桥」的 Python 连接器(`https://data.qdc-data.com`)。
16
+ 返回 pandas DataFrame,并带一层 **tushare 兼容壳**,重叠接口零迁移。
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0-py3-none-any.whl
22
+ ```
23
+
24
+ 源码包(需要自行构建时用):
25
+
26
+ ```bash
27
+ pip install https://api.qdc-data.com:8443/release/qdc-0.2.0.tar.gz
28
+ ```
29
+
30
+ > ⚠️ **不要写 `pip install qdc`**。这个包**没有发布到 PyPI**,PyPI 上那个 `qdc` 不是我们的东西。
31
+ > 本文件在 v0.1.0 里曾经写着 `pip install qdc` —— 那条命令从来没能用过。
32
+
33
+ 需要 Python ≥ 3.8、`requests`、`pandas`(会自动装)。
34
+
35
+ ## 三行上手
36
+
37
+ ```python
38
+ import qdc
39
+ qdc.connect("你的API密钥", cache_dir="~/.qdc")
40
+ df = qdc.daily("600519.SH", "20240101", "20240105") # -> pandas DataFrame
41
+ ```
42
+
43
+ `df` 的列:`ts_code, trade_date, open, high, low, close, vol, amount`(字段已对齐 tushare)。
44
+
45
+ ## tushare 兼容用法
46
+
47
+ ```python
48
+ import qdc
49
+ pro = qdc.pro_api("你的API密钥")
50
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20241231")
51
+ df = pro.moneyflow(ts_code="600519.SH", start_date="20240101", end_date="20240131")
52
+ df = pro.top_list(trade_date="20240105") # 龙虎榜
53
+ ```
54
+
55
+ ## 原生接口
56
+
57
+ ```python
58
+ c = qdc.connect(key, cache_dir="~/.qdc") # 或 qdc.Client(key)
59
+
60
+ qdc.query(sku, code=, start=, end=, table=, limit=, fields=, date_field=, key=)
61
+ qdc.daily(code, start, end, adjust=None) # adjust: None / "hfq" 后复权 / "qfq" 前复权
62
+ qdc.moneyflow(code, start, end)
63
+ qdc.lhb(code=None, date=None) # 龙虎榜
64
+ qdc.financials(code, statement="income") # income 利润表 / balance 资产负债表 / cashflow 现金流量表
65
+ qdc.index_daily(code, start, end) # 指数日线
66
+ qdc.adj_factor(code, start, end) # 复权因子(稀疏表,见下)
67
+ qdc.adj_factor_table() # 全市场复权因子整表(自动翻页 + 缓存)
68
+
69
+ qdc.universe(st=False, list_days_gt=None, amount_gt=None, index=None, industry=None)
70
+ qdc.lookup("茅台") # 名称/代码模糊查
71
+ qdc.describe("cn_daily_full") # 字段/口径/覆盖范围
72
+ ```
73
+
74
+ ## 三件必须知道的事
75
+
76
+ ### 1. 你拿到的是不是全部 —— 看 `df.attrs["qdc"]`
77
+
78
+ 服务端单次最多返回 10,000 行(你这一档的 `max_rows`)。**本连接器会自动翻页取全**,
79
+ 并把过程记在 `df.attrs["qdc"]` 里:
80
+
81
+ ```python
82
+ df = qdc.query("stocks_1m", code="002027.SZ", start="20240101", end="20241231")
83
+ print(len(df)) # 58322 —— 全年真实行数
84
+ print(df.attrs["qdc"]["pages"]) # 6 —— 翻了 6 页
85
+ print(df.attrs["qdc"]["complete"]) # True —— 取完了
86
+ ```
87
+
88
+ `complete` 为 `False` 只会在撞到 `max_pages` 上限时出现,那时同时会抛一条警告。
89
+
90
+ > ★ v0.1.0 **不翻页**:上面这个查询它只交给你第一页 10,000 行,HTTP 200、没有任何提示 ——
91
+ > 少 82.9% 的数据,而你的回测不会告诉你。这是升级到 0.2.0 最重要的理由。
92
+
93
+ ### 2. 复权是在本地算的
94
+
95
+ 原「A股日线(后复权)」商品已于 2026-08-09 退役(它是纯派生数据)。现在:
96
+
97
+ - `adjust="hfq"` = 取不复权日线 + 复权因子,本地按 `不复权价 × 复权因子` 还原;
98
+ - `adjust="qfq"` = 后复权再除以该票最新因子(以最新价为基准);
99
+ - 成交量与成交额**不参与复权**(退役登记实测:后复权的 vol/amount 与不复权完全相同)。
100
+
101
+ 因子表是**稀疏**的(只在除权日有记录),按代码前向填充;整表未收录的股票表示从未除权,按因子 = 1。
102
+ 如果某票在你这一档深度内取不到「该交易日之前最近一条」因子,对应行的价格会置为 `NaN` 并告警 ——
103
+ **不会**按因子 = 1 蒙混过去给你一个错价。
104
+
105
+ 因子表整表约 5.4 万行,`connect(cache_dir=...)` 之后会缓存到本地,不必每次重取。
106
+
107
+ ### 3. 限额:同一只票每台设备每天只能取一次
108
+
109
+ 去重键是 `(密钥, 设备, 代码, 日期)`,**不含数据集也不含表名**。所以:
110
+
111
+ - 先 `daily("600519.SH")` 再 `adj_factor("600519.SH")` → 第二次必然 429。
112
+ (正因为如此,`adjust="hfq"` 才用**不带代码的整表**去取因子 —— 那条路不计次。)
113
+ - `financials(code, "income")` 之后当天再取同一只票的 `balance` 也会 429。
114
+ 三张表要分三天取,或者走整包下载。
115
+ - **翻页不重复计次**:第 2 页起算同一次取数。
116
+
117
+ `device` 必须是**固定不变**的串。每次现生成新串会把设备槽位一个个占满;
118
+ 不传时本连接器会按主机名 + 网卡地址生成一个稳定值。
119
+
120
+ ## 约定
121
+
122
+ - 代码格式:`600519.SH` / `000001.SZ`(内部 `.XSHG/.XSHE/.XBSE` 会自动归一,大小写不敏感)。
123
+ - 日期:`YYYYMMDD` 字符串。
124
+
125
+ ## 错误
126
+
127
+ HTTP `401`(没有密钥)、`403`(未授权的数据集 / 设备数超限)、`429`(限额)等一律抛 `qdc.QDCError`,
128
+ 带服务端给的 `message` 和 `.status_code`。
129
+
130
+ ```python
131
+ try:
132
+ qdc.daily("600519.SH", "20240101", "20240105")
133
+ except qdc.QDCError as e:
134
+ print(e.status_code, e.message)
135
+ ```
136
+
137
+ ## 版本
138
+
139
+ ### 0.2.0(2026-09-06)
140
+
141
+ 三处都属于「不报错、只给错答案」,升级前请留意你既有代码的结论可能变:
142
+
143
+ 1. **自动翻页**。之前只取第一页,一只票一年分钟线交给你 10,000 行(真实 58,322 行),零提示。
144
+ 2. **复权**。之前 `adjust="hfq"` 打到已退役的 `cn_daily_hfq` 直接 422 报错;
145
+ `adjust="qfq"` 更糟 —— 它静默返回**不复权**价。现在两者都在本地正确还原。
146
+ 3. **财报表名**。之前 `statement="balance"` 打到不存在的表名 `balancesheet`,
147
+ 服务端静默回退到默认表,于是你要资产负债表、拿到的是**利润表**,HTTP 200 无提示。
148
+ 现在表名在本地校验,拼错当场报错。
149
+
150
+ 另外:`index_daily` 之前指向 A 股日线表,查指数稳定返回 0 行,现已改指「指数日线」;
151
+ 查询已退役的数据集会在本地给出继任者和迁移办法,而不是一句 `unknown sku`。
152
+
153
+ ### 0.1.0
154
+
155
+ 首个版本(从未对外分发)。
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ qdc/__init__.py
4
+ qdc/client.py
5
+ qdc.egg-info/PKG-INFO
6
+ qdc.egg-info/SOURCES.txt
7
+ qdc.egg-info/dependency_links.txt
8
+ qdc.egg-info/requires.txt
9
+ qdc.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests>=2.25
2
+ pandas>=1.1
@@ -0,0 +1 @@
1
+ qdc
qdc-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+