quantdb-sdk 0.4.8__tar.gz → 0.5.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.
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/CHANGELOG.md +56 -14
- {quantdb_sdk-0.4.8/quantdb_sdk.egg-info → quantdb-sdk-0.5.0}/PKG-INFO +3 -13
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/pyproject.toml +1 -1
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/__init__.py +1 -1
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/__main__.py +35 -4
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/_utils.py +1 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/async_client.py +59 -1
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/client.py +105 -24
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/hub.py +62 -5
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/layout.py +5 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0/quantdb_sdk.egg-info}/PKG-INFO +3 -13
- quantdb-sdk-0.5.0/quantdb_sdk.egg-info/SOURCES.txt +21 -0
- quantdb_sdk-0.4.8/quantdb_sdk/examples/__init__.py +0 -9
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_01_connect_basics.py +0 -55
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_02_kline_v2_daily.py +0 -48
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_03_sync_full_market.py +0 -50
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_04_duckdb_warehouse.py +0 -46
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_05_read_local_data.py +0 -68
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_06_tick_and_factors.py +0 -45
- quantdb_sdk-0.4.8/quantdb_sdk/examples/demo_07_data_hub.py +0 -66
- quantdb_sdk-0.4.8/quantdb_sdk.egg-info/SOURCES.txt +0 -39
- quantdb_sdk-0.4.8/tests/test_async_client.py +0 -218
- quantdb_sdk-0.4.8/tests/test_cli_encoding.py +0 -92
- quantdb_sdk-0.4.8/tests/test_client.py +0 -312
- quantdb_sdk-0.4.8/tests/test_data_hub.py +0 -351
- quantdb_sdk-0.4.8/tests/test_hub_models.py +0 -125
- quantdb_sdk-0.4.8/tests/test_layout.py +0 -101
- quantdb_sdk-0.4.8/tests/test_mcp_download.py +0 -134
- quantdb_sdk-0.4.8/tests/test_technical_indicators.py +0 -237
- quantdb_sdk-0.4.8/tests/test_update_pipeline.py +0 -44
- quantdb_sdk-0.4.8/tests/test_utils.py +0 -102
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/LICENSE +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/MANIFEST.in +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/README.md +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/errors.py +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/mcp_download.py +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk/py.typed +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/dependency_links.txt +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/entry_points.txt +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/requires.txt +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/quantdb_sdk.egg-info/top_level.txt +0 -0
- {quantdb_sdk-0.4.8 → quantdb-sdk-0.5.0}/setup.cfg +0 -0
|
@@ -3,6 +3,48 @@
|
|
|
3
3
|
所有 notable 变更都会记录在此文件。格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
4
4
|
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
5
5
|
|
|
6
|
+
## \[Unreleased]
|
|
7
|
+
|
|
8
|
+
### Removed
|
|
9
|
+
|
|
10
|
+
- **移除演示脚本包 `quantdb_sdk.examples`**(demo_01 ~ demo_07,共 7 个脚本):不再随 wheel 发布。CLI 帮助与文档中的演示脚本指引已同步删除,改用 `quantdb layout` / `quantdb datasets` 自述目录规范与数据集目录。
|
|
11
|
+
|
|
12
|
+
## \[0.5.0] - 2026-10-31
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **字段字典接入(列级字段说明)**:数据集可以自带一份字段字典 JSON(随数据集放在 COS),SDK 现在能直接读到列级的「中文名 / 公式 / 释义 / 分组 / 来源 / 方向」。首批支持 `factor_dictionary`(V5 全因子宽表,571 列 = 2 键 + 6 列后复权 OHLCV + 563 因子)。
|
|
17
|
+
- `client.get_fields(sub_category, group=..., query=..., field=..., limit=...)` → 列明细 DataFrame;
|
|
18
|
+
- `client.get_field_groups(sub_category)` → 分组摘要(组名 + 字段数);
|
|
19
|
+
- `client.get_field_dictionary(...)` → 原始响应(含 `schema_version` / `total_columns` / `groups`);
|
|
20
|
+
- `client.data.fields(...)` / `client.data.field_groups(...)` 为 Hub 层封装;
|
|
21
|
+
- 异步端对应 `a_get_fields` / `a_get_field_groups` / `a_get_field_dictionary` 与 `a_fields` / `a_field_groups`;
|
|
22
|
+
- `client.data.info("factor_dictionary")` 现在额外返回 `field_count` 与 `field_groups`(没有字典的数据集不受影响,404 被静默忽略)。
|
|
23
|
+
- 新增 CLI:`quantdb fields factor_dictionary [--group/--query/--field/--limit/--json]`,不带过滤条件时打印分组摘要。
|
|
24
|
+
- 字典接口**不消耗下载流量**;摘要响应在客户端进程内缓存,重复调用不重复请求(也不额外占用 30/min 限流额度)。
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `describe_dataset`(MCP)在有字典时额外返回 `field_groups` / `field_count`,并提示用 `describe_fields` 查看列明细。
|
|
29
|
+
|
|
30
|
+
## \[0.4.9] - 2026-10-31
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **注册新数据集 `factor_dictionary`(第 6 大类,订阅级)**:V5 全因子宽表,571 列(8 个键与后复权 OHLCV + 563 个因子),COS 布局为 `6_ml_datasets/factor_dictionary/dt=YYYYMMDD/data.parquet`。此前该数据集在 SDK 侧完全未注册,`client.data.*` / `sync_dataset` / `mount_local_dataset` / CLI / MCP 全部拒绝。本次补齐三张表:
|
|
35
|
+
- `_utils.SYNC_DATASET_CATEGORIES` 加 `"factor_dictionary" -> "6"`;
|
|
36
|
+
- `hub._ENTRIES` 加一条注册表条目(名称/描述/`subscription`/关键词/`v2_daily_partition`/选择器);
|
|
37
|
+
- `layout.V2_DAILY_PARTITION` 加 `factor_dictionary`。
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`layout.V2_DAILY_PARTITION` 与 COS 实际布局漂移**:补入 `min1_kline`、`min5_kline`、`etf_kline`。经 COS 复核,四者均为 `{category}/{sub}/dt=YYYYMMDD/data.parquet`,此前被 `dataset_layout()` 误判为 `v1_symbol`,导致公开 API `dataset_layout()` / `local_relative_path()` 与 `quantdb layout` 输出错误路径(etf_kline 的 V1 逐股票文件在 COS 上早已删除)。
|
|
42
|
+
- `hub` 模块 docstring 与 `DATASET_REGISTRY` 注释中的数据集数量(29)更正为 31;`__init__.__version__` 的 importlib 回退值(0.4.5)同步到 0.4.9。
|
|
43
|
+
|
|
44
|
+
### Known issues
|
|
45
|
+
|
|
46
|
+
- `tick_data` 在 COS 上同样是 `dt=YYYYMMDD/data.parquet`(单日全市场约 189 MB),但 SDK 仍按 `tick_shard` 处理本地读取(`{SYM}_{YYYYMMDD}.parquet` glob + 文件名取日期)。带 `symbol` 的 `read_local("tick_data", ...)` 因此找不到 dt 分区,属既有缺陷,切换布局是行为变更,留待单独修复。
|
|
47
|
+
|
|
6
48
|
## \[0.4.8] - 2026-10-27
|
|
7
49
|
|
|
8
50
|
### Fixed
|
|
@@ -31,20 +73,20 @@
|
|
|
31
73
|
|
|
32
74
|
> 兼容性:0.4.7 对接 2026-10-27 上线的服务端版本;旧服务端(两跳 cdn-bridge)仍可正常同步,无需回退。
|
|
33
75
|
|
|
34
|
-
## \[0.4.6] - 2026-10-27
|
|
35
|
-
|
|
36
|
-
### Fixed
|
|
37
|
-
|
|
38
|
-
- **增量同步的「检查更新」不再重复计费**:`sync_dataset` 此前对本地已存在的文件先发一次带 `If-None-Match` 的 GET 探测版本。ETag 命中时服务端在预扣前返回 304(免流量),但**ETag 不匹配时(对象已更新,正是需要下载的常见情况)该请求会继续走到流量预扣 + CDN 直连,把整个文件传完**,客户端却丢弃响应体再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被白白计费一次。
|
|
39
|
-
|
|
40
|
-
实测近 2 天生产数据:下载请求 309,973 次但只涉及 71,675 个不同文件(**放大到 4.3 倍**),其中 **93.6% 是同 ETag 的纯重复下载**,纯浪费带宽与用户流量。
|
|
41
|
-
|
|
42
|
-
修复:探测改用服务端新增的 `HEAD /api/v1/data/download`——只回 ETag 与长度,在流量预扣之前返回,**零流量、零令牌、零 PG 事务**。ETag 命中时同样返回 304。同步与异步客户端的 V2 release 与 V1 manifest 两条探测路径均已改用 HEAD。
|
|
43
|
-
|
|
44
|
-
客户端侧行为不变:探测结果与本地记录一致即免流量跳过,不一致才真正下载。
|
|
45
|
-
|
|
46
|
-
兼容性:服务端不支持 HEAD 时(老版本返回 405)自动退回原 GET 流程,不影响可用性。
|
|
47
|
-
|
|
76
|
+
## \[0.4.6] - 2026-10-27
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **增量同步的「检查更新」不再重复计费**:`sync_dataset` 此前对本地已存在的文件先发一次带 `If-None-Match` 的 GET 探测版本。ETag 命中时服务端在预扣前返回 304(免流量),但**ETag 不匹配时(对象已更新,正是需要下载的常见情况)该请求会继续走到流量预扣 + CDN 直连,把整个文件传完**,客户端却丢弃响应体再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被白白计费一次。
|
|
81
|
+
|
|
82
|
+
实测近 2 天生产数据:下载请求 309,973 次但只涉及 71,675 个不同文件(**放大到 4.3 倍**),其中 **93.6% 是同 ETag 的纯重复下载**,纯浪费带宽与用户流量。
|
|
83
|
+
|
|
84
|
+
修复:探测改用服务端新增的 `HEAD /api/v1/data/download`——只回 ETag 与长度,在流量预扣之前返回,**零流量、零令牌、零 PG 事务**。ETag 命中时同样返回 304。同步与异步客户端的 V2 release 与 V1 manifest 两条探测路径均已改用 HEAD。
|
|
85
|
+
|
|
86
|
+
客户端侧行为不变:探测结果与本地记录一致即免流量跳过,不一致才真正下载。
|
|
87
|
+
|
|
88
|
+
兼容性:服务端不支持 HEAD 时(老版本返回 405)自动退回原 GET 流程,不影响可用性。
|
|
89
|
+
|
|
48
90
|
## \[0.4.5] - 2026-10-18
|
|
49
91
|
|
|
50
92
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: quantdb-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: QuantDB 量化数据平台官方 Python SDK
|
|
5
5
|
Author: QuantDB Team
|
|
6
6
|
License: MIT
|
|
@@ -22,18 +22,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
22
22
|
Classifier: Topic :: Office/Business :: Financial
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: requests>=2.25.0
|
|
27
|
-
Requires-Dist: pandas>=1.3.0
|
|
28
|
-
Requires-Dist: httpx>=0.24.0
|
|
29
|
-
Requires-Dist: duckdb>=1.0.0
|
|
30
|
-
Requires-Dist: pyarrow>=10.0.0
|
|
31
25
|
Provides-Extra: dev
|
|
32
|
-
|
|
33
|
-
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
34
|
-
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
35
|
-
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
36
|
-
Dynamic: license-file
|
|
26
|
+
License-File: LICENSE
|
|
37
27
|
|
|
38
28
|
# QuantDB Python SDK
|
|
39
29
|
|
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
python -m quantdb datasets # 列出全部数据集目录(无需 API Key)
|
|
8
8
|
python -m quantdb datasets l1_factors # 查看某个数据集详情
|
|
9
9
|
QUANTDB_API_KEY=qdb_xxx quantdb check # 验证 API Key 是否有效
|
|
10
|
-
|
|
11
|
-
演示脚本见 quantdb_sdk.examples(demo_01 ~ demo_06),
|
|
12
|
-
如: python -m quantdb_sdk.examples.demo_03_sync_full_market
|
|
13
10
|
"""
|
|
14
11
|
|
|
15
12
|
import argparse
|
|
@@ -102,7 +99,7 @@ def main() -> int:
|
|
|
102
99
|
parser = argparse.ArgumentParser(
|
|
103
100
|
prog="quantdb",
|
|
104
101
|
description="QuantDB 量化数据平台官方 Python SDK",
|
|
105
|
-
epilog="
|
|
102
|
+
epilog="目录规范: quantdb layout;数据集目录: quantdb datasets",
|
|
106
103
|
)
|
|
107
104
|
parser.add_argument(
|
|
108
105
|
"--version",
|
|
@@ -150,6 +147,14 @@ def main() -> int:
|
|
|
150
147
|
download_parser.add_argument("--save-dir", default=None, help="本地数据根目录")
|
|
151
148
|
download_parser.add_argument("--json", action="store_true", help="输出 JSON 保存路径和文件大小")
|
|
152
149
|
|
|
150
|
+
fields_parser = subparsers.add_parser("fields", help="查询数据集的字段字典(列级说明,不消耗流量)")
|
|
151
|
+
fields_parser.add_argument("dataset", help="数据集,如 factor_dictionary")
|
|
152
|
+
fields_parser.add_argument("--group", default=None, help="分组子串,如 动量 / L2 2.1")
|
|
153
|
+
fields_parser.add_argument("--query", default=None, help="在字段名/中文名/释义/公式上子串匹配")
|
|
154
|
+
fields_parser.add_argument("--field", default=None, help="精确字段名,如 turn_1")
|
|
155
|
+
fields_parser.add_argument("--limit", type=int, default=None, help="返回列数上限(默认 200)")
|
|
156
|
+
fields_parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
157
|
+
|
|
153
158
|
args = parser.parse_args()
|
|
154
159
|
|
|
155
160
|
if not args.command:
|
|
@@ -205,6 +210,32 @@ def main() -> int:
|
|
|
205
210
|
print(f" Release: {result['release_id']}")
|
|
206
211
|
return 0
|
|
207
212
|
|
|
213
|
+
if args.command == "fields":
|
|
214
|
+
filtered = bool(args.group or args.query or args.field)
|
|
215
|
+
if args.json:
|
|
216
|
+
payload = client.get_field_dictionary(
|
|
217
|
+
args.dataset, group=args.group, query=args.query,
|
|
218
|
+
field=args.field, limit=args.limit,
|
|
219
|
+
)
|
|
220
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
221
|
+
return 0
|
|
222
|
+
if not filtered:
|
|
223
|
+
groups = client.get_field_groups(args.dataset)
|
|
224
|
+
print(f"{args.dataset} 字段分组(共 {int(groups['count'].sum())} 列):")
|
|
225
|
+
for _, row in groups.iterrows():
|
|
226
|
+
print(f" {int(row['count']):>4} {row['group']}")
|
|
227
|
+
print("\n加 --group / --query / --field 查看列明细。")
|
|
228
|
+
return 0
|
|
229
|
+
df = client.get_fields(args.dataset, group=args.group, query=args.query,
|
|
230
|
+
field=args.field, limit=args.limit)
|
|
231
|
+
if df.empty:
|
|
232
|
+
print("没有匹配的字段。")
|
|
233
|
+
return 0
|
|
234
|
+
print(f"匹配 {len(df)} 列:")
|
|
235
|
+
for _, row in df.iterrows():
|
|
236
|
+
print(f" {row['field']:<32} {row['name_cn']:<18} [{row['group']}]")
|
|
237
|
+
return 0
|
|
238
|
+
|
|
208
239
|
except Exception as e:
|
|
209
240
|
print(f"[ERROR] {e}", file=sys.stderr)
|
|
210
241
|
return 1
|
|
@@ -23,6 +23,7 @@ SYNC_DATASET_CATEGORIES: Dict[str, str] = {
|
|
|
23
23
|
"etf_pcf": "4", "convertible_bond": "4", "etf_kline": "4",
|
|
24
24
|
"valuation": "5", "technical_indicators": "5", "market_sentiment": "5",
|
|
25
25
|
"features_daily": "6", "l1_factors": "6", "l1_l2_factors": "6", "l2_factors": "6",
|
|
26
|
+
"factor_dictionary": "6",
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
|
|
@@ -28,7 +28,7 @@ from .errors import (
|
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
31
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.
|
|
31
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.5.0"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _etag_hex(value: Optional[str]) -> Optional[str]:
|
|
@@ -83,6 +83,9 @@ class AsyncQuantDBClient:
|
|
|
83
83
|
# 进程内 parquet ETag 缓存:同对象(ETag 未变)不重复下载,避免重复计费。
|
|
84
84
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
85
85
|
|
|
86
|
+
# 字段字典摘要缓存(dataset -> 原始响应),与同步客户端同语义。
|
|
87
|
+
self._field_cache: Dict[str, Dict[str, Any]] = {}
|
|
88
|
+
|
|
86
89
|
# 统一数据入口(quantdb_sdk.hub.AsyncDataHub)惰性实例,见 data property。
|
|
87
90
|
self._data_hub: Optional[Any] = None
|
|
88
91
|
|
|
@@ -609,6 +612,61 @@ class AsyncQuantDBClient:
|
|
|
609
612
|
df["file_size_gb"] = df["file_size"].fillna(0).map(bytes_to_gb).round(2)
|
|
610
613
|
return df
|
|
611
614
|
|
|
615
|
+
# ========== 字段字典(列级说明,不消耗下载流量) ==========
|
|
616
|
+
|
|
617
|
+
async def a_get_field_dictionary(
|
|
618
|
+
self,
|
|
619
|
+
sub_category: str,
|
|
620
|
+
group: Optional[str] = None,
|
|
621
|
+
query: Optional[str] = None,
|
|
622
|
+
field: Optional[str] = None,
|
|
623
|
+
limit: Optional[int] = None,
|
|
624
|
+
use_cache: bool = True,
|
|
625
|
+
) -> Dict[str, Any]:
|
|
626
|
+
"""返回字段字典原始响应(异步):schema 信息 + ``groups`` [+ ``columns``]。
|
|
627
|
+
|
|
628
|
+
不带过滤条件时服务端只返回分组摘要;同一进程内摘要会缓存复用。
|
|
629
|
+
"""
|
|
630
|
+
filtered = bool(group or query or field)
|
|
631
|
+
cache_key = f"summary:{sub_category}"
|
|
632
|
+
if use_cache and not filtered and cache_key in self._field_cache:
|
|
633
|
+
return self._field_cache[cache_key]
|
|
634
|
+
|
|
635
|
+
params: Dict[str, Any] = {"sub_category": sub_category}
|
|
636
|
+
if group:
|
|
637
|
+
params["group"] = group
|
|
638
|
+
if query:
|
|
639
|
+
params["query"] = query
|
|
640
|
+
if field:
|
|
641
|
+
params["field"] = field
|
|
642
|
+
if limit:
|
|
643
|
+
params["limit"] = int(limit)
|
|
644
|
+
payload = await self._get("/api/v1/data/fields", params)
|
|
645
|
+
if use_cache and not filtered:
|
|
646
|
+
self._field_cache[cache_key] = payload
|
|
647
|
+
return payload
|
|
648
|
+
|
|
649
|
+
async def a_get_fields(
|
|
650
|
+
self,
|
|
651
|
+
sub_category: str,
|
|
652
|
+
group: Optional[str] = None,
|
|
653
|
+
query: Optional[str] = None,
|
|
654
|
+
field: Optional[str] = None,
|
|
655
|
+
limit: Optional[int] = None,
|
|
656
|
+
) -> pd.DataFrame:
|
|
657
|
+
"""查询列级字段说明(异步);必须带 group / query / field 之一。"""
|
|
658
|
+
payload = await self.a_get_field_dictionary(
|
|
659
|
+
sub_category, group=group, query=query, field=field, limit=limit,
|
|
660
|
+
)
|
|
661
|
+
columns = payload.get("columns") or []
|
|
662
|
+
return pd.DataFrame(columns, columns=["field", "name_cn", "formula", "description",
|
|
663
|
+
"group", "source", "direction", "order"])
|
|
664
|
+
|
|
665
|
+
async def a_get_field_groups(self, sub_category: str) -> pd.DataFrame:
|
|
666
|
+
"""字段分组摘要(异步):``group`` + ``count``。"""
|
|
667
|
+
payload = await self.a_get_field_dictionary(sub_category)
|
|
668
|
+
return pd.DataFrame(payload.get("groups") or [], columns=["group", "count"])
|
|
669
|
+
|
|
612
670
|
# ========== 数据预览与下载 ==========
|
|
613
671
|
|
|
614
672
|
async def a_preview_as_df(
|
|
@@ -32,7 +32,13 @@ from .errors import (
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# 维护提示:每次发版必须与 pyproject.toml 版本号同步
|
|
35
|
-
_USER_AGENT = "QuantDB-Python-SDK/0.
|
|
35
|
+
_USER_AGENT = "QuantDB-Python-SDK/0.5.0"
|
|
36
|
+
|
|
37
|
+
# 字段字典的列顺序(与后端 /api/v1/data/fields 返回的 columns[] 一致)
|
|
38
|
+
_FIELD_COLUMNS = (
|
|
39
|
+
"field", "name_cn", "formula", "description",
|
|
40
|
+
"group", "source", "direction", "order",
|
|
41
|
+
)
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
def _etag_hex(value: Optional[str]) -> Optional[str]:
|
|
@@ -95,6 +101,10 @@ class QuantDBClient:
|
|
|
95
101
|
# 只需下载一次。进程退出即失效(无跨进程副作用)。
|
|
96
102
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
97
103
|
|
|
104
|
+
# 字段字典摘要缓存(dataset -> 原始响应):字典是静态文件,
|
|
105
|
+
# 同一进程内重复 info()/get_field_groups() 不必反复请求(也不吃 30/min 限流)。
|
|
106
|
+
self._field_cache: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
|
|
98
108
|
# 统一数据入口(quantdb_sdk.hub.DataHub)惰性实例,见 data property。
|
|
99
109
|
self._data_hub: Optional[Any] = None
|
|
100
110
|
|
|
@@ -145,29 +155,29 @@ class QuantDBClient:
|
|
|
145
155
|
resp = self._request("GET", path, params=params)
|
|
146
156
|
return self._check_response(resp)
|
|
147
157
|
|
|
148
|
-
def _probe_head(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
149
|
-
"""HEAD 探测对象元数据(增量同步的「检查更新」):零流量、零令牌、零计费。
|
|
150
|
-
|
|
151
|
-
服务端在流量预扣之前返回 ETag 与 Content-Length,客户端与本地记录
|
|
152
|
-
比对后决定是否需要真正下载。
|
|
153
|
-
|
|
154
|
-
为什么必须用 HEAD 而不是带 If-None-Match 的 GET:
|
|
155
|
-
GET 探测在 ETag 不匹配时(对象已更新,正是需要下载的常见情况)会
|
|
156
|
-
继续走到流量预扣 + CDN 直连,把整个文件传完;客户端却丢弃响应体
|
|
157
|
-
再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被
|
|
158
|
-
白白计费一次——实测近 2 天下载请求量放大到文件数的 4.3 倍,
|
|
159
|
-
其中 93.6% 是同 ETag 的纯重复下载。
|
|
160
|
-
|
|
161
|
-
服务端不支持 HEAD 时(老版本)返回 405,调用方应退回 GET 流程。
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
return self._request(
|
|
165
|
-
"HEAD", "/api/v1/data/download", params=params,
|
|
166
|
-
headers=headers, allow_redirects=False,
|
|
167
|
-
)
|
|
168
|
-
except requests.RequestException:
|
|
169
|
-
return requests.Response()
|
|
170
|
-
|
|
158
|
+
def _probe_head(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
159
|
+
"""HEAD 探测对象元数据(增量同步的「检查更新」):零流量、零令牌、零计费。
|
|
160
|
+
|
|
161
|
+
服务端在流量预扣之前返回 ETag 与 Content-Length,客户端与本地记录
|
|
162
|
+
比对后决定是否需要真正下载。
|
|
163
|
+
|
|
164
|
+
为什么必须用 HEAD 而不是带 If-None-Match 的 GET:
|
|
165
|
+
GET 探测在 ETag 不匹配时(对象已更新,正是需要下载的常见情况)会
|
|
166
|
+
继续走到流量预扣 + CDN 直连,把整个文件传完;客户端却丢弃响应体
|
|
167
|
+
再重下一次。CDN 直连模式签发即计费、不退款,于是「检查更新」被
|
|
168
|
+
白白计费一次——实测近 2 天下载请求量放大到文件数的 4.3 倍,
|
|
169
|
+
其中 93.6% 是同 ETag 的纯重复下载。
|
|
170
|
+
|
|
171
|
+
服务端不支持 HEAD 时(老版本)返回 405,调用方应退回 GET 流程。
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
return self._request(
|
|
175
|
+
"HEAD", "/api/v1/data/download", params=params,
|
|
176
|
+
headers=headers, allow_redirects=False,
|
|
177
|
+
)
|
|
178
|
+
except requests.RequestException:
|
|
179
|
+
return requests.Response()
|
|
180
|
+
|
|
171
181
|
def _download_stream(self, params: dict, headers: Optional[dict] = None) -> requests.Response:
|
|
172
182
|
"""下载专用流式请求:手动处理服务端 302 CDN 直连跳转。
|
|
173
183
|
|
|
@@ -684,6 +694,77 @@ class QuantDBClient:
|
|
|
684
694
|
df["file_size_gb"] = df["file_size"].fillna(0).map(bytes_to_gb).round(2)
|
|
685
695
|
return df
|
|
686
696
|
|
|
697
|
+
# ========== 字段字典(列级说明,不消耗下载流量) ==========
|
|
698
|
+
|
|
699
|
+
def get_field_dictionary(
|
|
700
|
+
self,
|
|
701
|
+
sub_category: str,
|
|
702
|
+
group: Optional[str] = None,
|
|
703
|
+
query: Optional[str] = None,
|
|
704
|
+
field: Optional[str] = None,
|
|
705
|
+
limit: Optional[int] = None,
|
|
706
|
+
use_cache: bool = True,
|
|
707
|
+
) -> Dict[str, Any]:
|
|
708
|
+
"""返回字段字典原始响应(schema 信息 + ``groups`` 分组摘要 [+ ``columns``])。
|
|
709
|
+
|
|
710
|
+
字典由数据侧随数据集发布(如 ``factor_dictionary`` 的
|
|
711
|
+
``v5_factor_dictionary.json``),后端读取 COS 后缓存,**不消耗下载流量**。
|
|
712
|
+
|
|
713
|
+
:param sub_category: 数据集标识,如 ``factor_dictionary``。
|
|
714
|
+
:param group: 可选,分组子串匹配(如 ``动量`` / ``L2 2.1``)。
|
|
715
|
+
:param query: 可选,在字段名/中文名/释义/公式上做子串匹配。
|
|
716
|
+
:param field: 可选,精确字段名(如 ``turn_1``)。
|
|
717
|
+
:param limit: 可选,返回列数上限(服务端默认 200,最大 1000)。
|
|
718
|
+
:param use_cache: 同一进程内复用已取过的「摘要」响应(仅当不带过滤条件时)。
|
|
719
|
+
:raises NotFoundError: 该数据集没有字段字典。
|
|
720
|
+
"""
|
|
721
|
+
filtered = bool(group or query or field)
|
|
722
|
+
cache_key = f"summary:{sub_category}"
|
|
723
|
+
if use_cache and not filtered and cache_key in self._field_cache:
|
|
724
|
+
return self._field_cache[cache_key]
|
|
725
|
+
|
|
726
|
+
params: Dict[str, Any] = {"sub_category": sub_category}
|
|
727
|
+
if group:
|
|
728
|
+
params["group"] = group
|
|
729
|
+
if query:
|
|
730
|
+
params["query"] = query
|
|
731
|
+
if field:
|
|
732
|
+
params["field"] = field
|
|
733
|
+
if limit:
|
|
734
|
+
params["limit"] = int(limit)
|
|
735
|
+
payload = self._get("/api/v1/data/fields", params)
|
|
736
|
+
if use_cache and not filtered:
|
|
737
|
+
self._field_cache[cache_key] = payload
|
|
738
|
+
return payload
|
|
739
|
+
|
|
740
|
+
def get_fields(
|
|
741
|
+
self,
|
|
742
|
+
sub_category: str,
|
|
743
|
+
group: Optional[str] = None,
|
|
744
|
+
query: Optional[str] = None,
|
|
745
|
+
field: Optional[str] = None,
|
|
746
|
+
limit: Optional[int] = None,
|
|
747
|
+
) -> pd.DataFrame:
|
|
748
|
+
"""查询某数据集的列级字段说明(中文名 / 公式 / 释义 / 分组 / 方向)。
|
|
749
|
+
|
|
750
|
+
**必须带 ``group`` / ``query`` / ``field`` 之一**:不带过滤条件时服务端只返回
|
|
751
|
+
分组摘要(571 列全量下发会撑爆调用方上下文),此时返回空 DataFrame,
|
|
752
|
+
请改用 :meth:`get_field_groups`。
|
|
753
|
+
|
|
754
|
+
:return: DataFrame,列为 ``field`` / ``name_cn`` / ``formula`` /
|
|
755
|
+
``description`` / ``group`` / ``source`` / ``direction`` / ``order``。
|
|
756
|
+
"""
|
|
757
|
+
payload = self.get_field_dictionary(
|
|
758
|
+
sub_category, group=group, query=query, field=field, limit=limit,
|
|
759
|
+
)
|
|
760
|
+
columns = payload.get("columns") or []
|
|
761
|
+
return pd.DataFrame(columns, columns=list(_FIELD_COLUMNS))
|
|
762
|
+
|
|
763
|
+
def get_field_groups(self, sub_category: str) -> pd.DataFrame:
|
|
764
|
+
"""字段分组摘要(``group`` 组名 + ``count`` 字段数),不消耗下载流量。"""
|
|
765
|
+
payload = self.get_field_dictionary(sub_category)
|
|
766
|
+
return pd.DataFrame(payload.get("groups") or [], columns=["group", "count"])
|
|
767
|
+
|
|
687
768
|
# ========== 数据预览与下载(消耗下载流量) ==========
|
|
688
769
|
|
|
689
770
|
def preview_as_df(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from quantdb_sdk import QuantDBClient
|
|
10
10
|
|
|
11
11
|
client = QuantDBClient(api_key="qdb_xxx")
|
|
12
|
-
client.data.datasets() #
|
|
12
|
+
client.data.datasets() # 31 个数据集目录(零流量)
|
|
13
13
|
client.data.info("l1_factors") # 覆盖区间 + 文件大小
|
|
14
14
|
client.data.read_local("daily_forward", start_date="2026-09-01") # 本地零流量
|
|
15
15
|
client.data.preview("balance", symbol="600519.SH") # 在线预览
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
``category_dir`` 复用 :data:`quantdb_sdk.layout.CATEGORY_DIRS`,避免重复维护分类事实源。
|
|
22
22
|
- ``layout`` 记录数据集的**实际 COS 形态**(``v2_daily_partition`` / ``v1_symbol`` /
|
|
23
23
|
``tick_shard``),用于 ``query()`` 路由;不用 :func:`quantdb_sdk.layout.dataset_layout`
|
|
24
|
-
(其 V2
|
|
24
|
+
(其 V2 集合截至 2026-10-31 已与 COS 对齐,但 tick_data 例外:COS 已改为 dt= 分区,
|
|
25
|
+
本地读取仍是分片语义)。
|
|
25
26
|
- 本地读取用**多形态 glob** 与目录布局集合解耦,支持显式 ``root``(数据根目录可配置,
|
|
26
27
|
非固定目录;缺省读 :func:`quantdb_sdk.layout.data_root`);读取本身交给 DuckDB 对
|
|
27
28
|
精确文件列表做一次 ``read_parquet``(列裁剪 / symbol / 日期下推),不逐文件拼接。
|
|
@@ -157,6 +158,8 @@ _ENTRIES: Tuple[Tuple[str, str, str, str, Tuple[str, ...], str, Tuple[str, ...]]
|
|
|
157
158
|
("l2", "因子", "factor"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
158
159
|
("l1_l2_factors", "L1+L2 合并因子", "L1 和 L2 的合并宽表。", "subscription",
|
|
159
160
|
("l1", "l2", "因子", "factor", "合并"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
161
|
+
("factor_dictionary", "V5 因子宽表", "V5 全因子宽表:571 列(2 键 + 6 列后复权 OHLCV + 563 因子),按交易日分区;字段字典见数据集目录内 v5_factor_dictionary.json。", "subscription",
|
|
162
|
+
("因子", "factor", "v5", "宽表", "f500", "字典"), "v2_daily_partition", ("symbol", "trade_date")),
|
|
160
163
|
)
|
|
161
164
|
|
|
162
165
|
|
|
@@ -178,7 +181,7 @@ def _build_registry() -> Dict[str, DatasetInfo]:
|
|
|
178
181
|
return registry
|
|
179
182
|
|
|
180
183
|
|
|
181
|
-
#: dataset 名 -> DatasetInfo(
|
|
184
|
+
#: dataset 名 -> DatasetInfo(31 项)
|
|
182
185
|
DATASET_REGISTRY: Dict[str, DatasetInfo] = _build_registry()
|
|
183
186
|
|
|
184
187
|
|
|
@@ -509,7 +512,9 @@ class DataHub:
|
|
|
509
512
|
"""返回数据集注册表信息,并可选叠加云端元数据(覆盖区间/文件数/大小)。
|
|
510
513
|
|
|
511
514
|
``with_meta=True``(默认)时叠加 ``client.query_meta(sub_category=dataset)``
|
|
512
|
-
的 ``min_date/max_date/file_count/file_size_gb/row_count/updated_at
|
|
515
|
+
的 ``min_date/max_date/file_count/file_size_gb/row_count/updated_at``,
|
|
516
|
+
并在该数据集带**字段字典**时额外叠加 ``field_count`` 与 ``field_groups``
|
|
517
|
+
(如 ``factor_dictionary`` 的 571 列分组)。没有字典的数据集不受影响。
|
|
513
518
|
"""
|
|
514
519
|
entry = _info(dataset).to_dict()
|
|
515
520
|
if not with_meta:
|
|
@@ -519,8 +524,36 @@ class DataHub:
|
|
|
519
524
|
except Exception:
|
|
520
525
|
meta = None
|
|
521
526
|
entry.update(_meta_row(meta))
|
|
527
|
+
try:
|
|
528
|
+
fields = self._client.get_field_dictionary(dataset)
|
|
529
|
+
except Exception:
|
|
530
|
+
fields = None
|
|
531
|
+
if isinstance(fields, dict) and fields.get("groups"):
|
|
532
|
+
entry["field_count"] = fields.get("total_columns")
|
|
533
|
+
entry["field_groups"] = fields.get("groups")
|
|
522
534
|
return entry
|
|
523
535
|
|
|
536
|
+
def fields(
|
|
537
|
+
self,
|
|
538
|
+
dataset: str,
|
|
539
|
+
group: Optional[str] = None,
|
|
540
|
+
query: Optional[str] = None,
|
|
541
|
+
field: Optional[str] = None,
|
|
542
|
+
limit: Optional[int] = None,
|
|
543
|
+
) -> pd.DataFrame:
|
|
544
|
+
"""查询列级字段说明(中文名 / 公式 / 释义 / 分组 / 方向),不消耗下载流量。
|
|
545
|
+
|
|
546
|
+
必须带 ``group`` / ``query`` / ``field`` 之一,否则服务端只回分组摘要、
|
|
547
|
+
返回空 DataFrame(用 :meth:`field_groups` 拿摘要)。
|
|
548
|
+
"""
|
|
549
|
+
return self._client.get_fields(
|
|
550
|
+
_info(dataset).dataset, group=group, query=query, field=field, limit=limit,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
def field_groups(self, dataset: str) -> pd.DataFrame:
|
|
554
|
+
"""字段分组摘要(``group`` + ``count``),不消耗下载流量。"""
|
|
555
|
+
return self._client.get_field_groups(_info(dataset).dataset)
|
|
556
|
+
|
|
524
557
|
def local_files(
|
|
525
558
|
self,
|
|
526
559
|
dataset: str,
|
|
@@ -827,7 +860,7 @@ class AsyncDataHub:
|
|
|
827
860
|
return pd.DataFrame(rows, columns=cols)
|
|
828
861
|
|
|
829
862
|
async def a_info(self, dataset: str, with_meta: bool = True) -> Dict[str, Any]:
|
|
830
|
-
"""异步版 :meth:`DataHub.info
|
|
863
|
+
"""异步版 :meth:`DataHub.info`(含字段字典分组摘要)。"""
|
|
831
864
|
entry = _info(dataset).to_dict()
|
|
832
865
|
if not with_meta:
|
|
833
866
|
return entry
|
|
@@ -836,8 +869,32 @@ class AsyncDataHub:
|
|
|
836
869
|
except Exception:
|
|
837
870
|
meta = None
|
|
838
871
|
entry.update(_meta_row(meta))
|
|
872
|
+
try:
|
|
873
|
+
fields = await self._client.a_get_field_dictionary(dataset)
|
|
874
|
+
except Exception:
|
|
875
|
+
fields = None
|
|
876
|
+
if isinstance(fields, dict) and fields.get("groups"):
|
|
877
|
+
entry["field_count"] = fields.get("total_columns")
|
|
878
|
+
entry["field_groups"] = fields.get("groups")
|
|
839
879
|
return entry
|
|
840
880
|
|
|
881
|
+
async def a_fields(
|
|
882
|
+
self,
|
|
883
|
+
dataset: str,
|
|
884
|
+
group: Optional[str] = None,
|
|
885
|
+
query: Optional[str] = None,
|
|
886
|
+
field: Optional[str] = None,
|
|
887
|
+
limit: Optional[int] = None,
|
|
888
|
+
) -> pd.DataFrame:
|
|
889
|
+
"""异步版 :meth:`DataHub.fields`。"""
|
|
890
|
+
return await self._client.a_get_fields(
|
|
891
|
+
_info(dataset).dataset, group=group, query=query, field=field, limit=limit,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
async def a_field_groups(self, dataset: str) -> pd.DataFrame:
|
|
895
|
+
"""异步版 :meth:`DataHub.field_groups`。"""
|
|
896
|
+
return await self._client.a_get_field_groups(_info(dataset).dataset)
|
|
897
|
+
|
|
841
898
|
async def a_local_files(
|
|
842
899
|
self,
|
|
843
900
|
dataset: str,
|
|
@@ -39,13 +39,18 @@ CATEGORY_DIRS = {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
# 纯 V2 按日分区数据集(COS 上无 V1 逐股票文件)
|
|
42
|
+
# 2026-10-31 按 COS 实际布局补齐 min1_kline / min5_kline / etf_kline / factor_dictionary:
|
|
43
|
+
# 四者在 COS 上均为 {category}/{sub}/dt=YYYYMMDD/data.parquet(单文件全市场当日)。
|
|
42
44
|
V2_DAILY_PARTITION = frozenset({
|
|
43
45
|
"daily_unadjusted", "daily_forward", "daily_backward", "index_daily",
|
|
44
46
|
"margin_trading", "valuation", "technical_indicators", "market_sentiment",
|
|
45
47
|
"features_daily", "l1_factors", "l1_l2_factors", "l2_factors",
|
|
48
|
+
"min1_kline", "min5_kline", "etf_kline", "factor_dictionary",
|
|
46
49
|
})
|
|
47
50
|
|
|
48
51
|
# tick 按交易日分片数据集
|
|
52
|
+
# 注意:tick_data 在 COS 上同样已是 dt=YYYYMMDD/data.parquet(V1 分片已删),
|
|
53
|
+
# 但其本地读取仍走分片 glob 与文件名取日期,切换布局属于行为变更,待单独修复。
|
|
49
54
|
TICK_SHARD = frozenset({"tick_data"})
|
|
50
55
|
|
|
51
56
|
Layout = Literal["v2_daily_partition", "v1_symbol", "tick_shard"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: quantdb-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: QuantDB 量化数据平台官方 Python SDK
|
|
5
5
|
Author: QuantDB Team
|
|
6
6
|
License: MIT
|
|
@@ -22,18 +22,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
22
22
|
Classifier: Topic :: Office/Business :: Financial
|
|
23
23
|
Requires-Python: >=3.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: requests>=2.25.0
|
|
27
|
-
Requires-Dist: pandas>=1.3.0
|
|
28
|
-
Requires-Dist: httpx>=0.24.0
|
|
29
|
-
Requires-Dist: duckdb>=1.0.0
|
|
30
|
-
Requires-Dist: pyarrow>=10.0.0
|
|
31
25
|
Provides-Extra: dev
|
|
32
|
-
|
|
33
|
-
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
34
|
-
Requires-Dist: responses>=0.23.0; extra == "dev"
|
|
35
|
-
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
36
|
-
Dynamic: license-file
|
|
26
|
+
License-File: LICENSE
|
|
37
27
|
|
|
38
28
|
# QuantDB Python SDK
|
|
39
29
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
quantdb_sdk/__init__.py
|
|
7
|
+
quantdb_sdk/__main__.py
|
|
8
|
+
quantdb_sdk/_utils.py
|
|
9
|
+
quantdb_sdk/async_client.py
|
|
10
|
+
quantdb_sdk/client.py
|
|
11
|
+
quantdb_sdk/errors.py
|
|
12
|
+
quantdb_sdk/hub.py
|
|
13
|
+
quantdb_sdk/layout.py
|
|
14
|
+
quantdb_sdk/mcp_download.py
|
|
15
|
+
quantdb_sdk/py.typed
|
|
16
|
+
quantdb_sdk.egg-info/PKG-INFO
|
|
17
|
+
quantdb_sdk.egg-info/SOURCES.txt
|
|
18
|
+
quantdb_sdk.egg-info/dependency_links.txt
|
|
19
|
+
quantdb_sdk.egg-info/entry_points.txt
|
|
20
|
+
quantdb_sdk.egg-info/requires.txt
|
|
21
|
+
quantdb_sdk.egg-info/top_level.txt
|