quantdb-sdk 0.4.0__tar.gz → 0.4.1__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 (38) hide show
  1. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/CHANGELOG.md +15 -0
  2. {quantdb_sdk-0.4.0/quantdb_sdk.egg-info → quantdb_sdk-0.4.1}/PKG-INFO +2 -2
  3. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/pyproject.toml +69 -67
  4. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/__init__.py +1 -1
  5. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/async_client.py +1 -1
  6. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/client.py +1 -1
  7. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/hub.py +120 -36
  8. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1/quantdb_sdk.egg-info}/PKG-INFO +2 -2
  9. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk.egg-info/requires.txt +1 -1
  10. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_data_hub.py +26 -0
  11. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/LICENSE +0 -0
  12. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/MANIFEST.in +0 -0
  13. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/README.md +0 -0
  14. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/__main__.py +0 -0
  15. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/_utils.py +0 -0
  16. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/errors.py +0 -0
  17. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/__init__.py +0 -0
  18. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_01_connect_basics.py +0 -0
  19. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_02_kline_v2_daily.py +0 -0
  20. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_03_sync_full_market.py +0 -0
  21. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_04_duckdb_warehouse.py +0 -0
  22. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_05_read_local_data.py +0 -0
  23. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_06_tick_and_factors.py +0 -0
  24. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/examples/demo_07_data_hub.py +0 -0
  25. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/layout.py +0 -0
  26. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk/py.typed +0 -0
  27. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk.egg-info/SOURCES.txt +0 -0
  28. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk.egg-info/dependency_links.txt +0 -0
  29. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk.egg-info/entry_points.txt +0 -0
  30. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/quantdb_sdk.egg-info/top_level.txt +0 -0
  31. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/setup.cfg +0 -0
  32. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_async_client.py +0 -0
  33. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_client.py +0 -0
  34. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_hub_models.py +0 -0
  35. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_layout.py +0 -0
  36. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_technical_indicators.py +0 -0
  37. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_update_pipeline.py +0 -0
  38. {quantdb_sdk-0.4.0 → quantdb_sdk-0.4.1}/tests/test_utils.py +0 -0
@@ -3,6 +3,21 @@
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
+ ## \[0.4.1] - 2026-10-05
7
+
8
+ ### Changed
9
+
10
+ - **本地零流量读取提速**(`client.data.read_local` / `read`):改用 DuckDB 对**精确分区文件列表**做一次 `read_parquet`,把 `symbol` / 日期区间 / 列裁剪全部下推到 SQL,替代原先逐文件 `pd.read_parquet` 再 `concat`。实测(120 分区 × 1000 标的)单标的全历史 `0.62s → 0.22s`,带 `columns` 列裁剪 `→ 0.07s`。
11
+
12
+ - V2 按日分区与 tick 分片的 `trade_date` 由分区目录名 / 文件名推导,**不依赖** hive 注入的 `dt` 列(该列仅在较新 DuckDB 存在)。
13
+ - V1 逐股票表日期过滤用 `substr(replace(CAST(col AS VARCHAR),'-',''),1,8)` 归一化后下推,同时兼容整型报告期 `20251231` 与字符串 `2025-12-31` / 带时分秒。
14
+
15
+ - **依赖下界调整**:`duckdb` 由 `>=0.8.0` 改为 `>=1.0.0`。`read_local` 用到 `read_parquet(filename=true)` + `* EXCLUDE (filename)`;0.8.x 在部分环境(Windows / Python 3.10)导入即崩溃,1.0.0 ~ 1.5.5 已验证通过。
16
+
17
+ ### Fixed
18
+
19
+ - `read_local(..., columns=[...])` 在 V2 / tick 上未把 `symbol` 过滤下推:当 `columns` 不含 `symbol` 时,旧实现会在读取后被静默跳过过滤、返回全市场行。现 `symbol` 始终在 SQL 中下推。
20
+
6
21
  ## \[0.4.0] - 2026-10-05
7
22
 
8
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantdb-sdk
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: QuantDB 量化数据平台官方 Python SDK
5
5
  Author: QuantDB Team
6
6
  License: MIT
@@ -26,7 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: requests>=2.25.0
27
27
  Requires-Dist: pandas>=1.3.0
28
28
  Requires-Dist: httpx>=0.24.0
29
- Requires-Dist: duckdb>=0.8.0
29
+ Requires-Dist: duckdb>=1.0.0
30
30
  Requires-Dist: pyarrow>=10.0.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -1,67 +1,69 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "quantdb-sdk"
7
- version = "0.4.0"
8
- description = "QuantDB 量化数据平台官方 Python SDK"
9
- readme = "README.md"
10
- license = {text = "MIT"}
11
- requires-python = ">=3.8"
12
- authors = [
13
- {name = "QuantDB Team"},
14
- ]
15
- keywords = ["quant", "finance", "data", "a-share", "parquet"]
16
- classifiers = [
17
- "Development Status :: 3 - Alpha",
18
- "Intended Audience :: Developers",
19
- "Intended Audience :: Financial and Insurance Industry",
20
- "License :: OSI Approved :: MIT License",
21
- "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.8",
23
- "Programming Language :: Python :: 3.9",
24
- "Programming Language :: Python :: 3.10",
25
- "Programming Language :: Python :: 3.11",
26
- "Programming Language :: Python :: 3.12",
27
- "Topic :: Office/Business :: Financial",
28
- ]
29
- dependencies = [
30
- "requests>=2.25.0",
31
- "pandas>=1.3.0",
32
- "httpx>=0.24.0",
33
- "duckdb>=0.8.0",
34
- # 0.1.1 修复:pyarrow 是 pd.read_parquet 的硬依赖(client.py:482, async_client.py:413),
35
- # 但 0.1.0 漏声明,导致 pip install 后首次 K线/Tick 查询会抛 ImportError。
36
- # 锁定到 >=10.0.0 以获得 Python 3.10+ 完整支持(pyarrow 14+ 更好)。
37
- "pyarrow>=10.0.0",
38
- ]
39
-
40
- # 0.1.2 新增:装包后提供 `quantdb` shell 命令,等价于 `python -m quantdb_sdk`。
41
- # 0.1.0/0.1.1 README 写的是 `python -m quantdb`(用了 PyPI 名带连字符),
42
- # 但 Python 模块名是下划线,安装后 `python -m quantdb` 会报 No module named quantdb。
43
- # 加这个 entry point 后两个名字都能用,README 不需要再改。
44
- [project.scripts]
45
- quantdb = "quantdb_sdk.__main__:main"
46
-
47
- [project.optional-dependencies]
48
- dev = [
49
- "pytest>=7.0.0",
50
- "pytest-asyncio>=0.21.0",
51
- "responses>=0.23.0",
52
- "respx>=0.20.0",
53
- ]
54
-
55
- [project.urls]
56
- Homepage = "https://quantdb.quantmind.cloud"
57
- Documentation = "https://quantdb.quantmind.cloud/docs/sdk.html"
58
- Repository = "https://github.com/quantdb/quantdb"
59
- Changelog = "https://github.com/quantdb/quantdb/blob/main/CHANGELOG.md"
60
-
61
- [tool.setuptools.packages.find]
62
- where = ["."]
63
- include = ["quantdb_sdk*"]
64
-
65
- # 0.1.2 新增:让 wheel 包含 LICENSE 和 CHANGELOG.md
66
- [tool.setuptools.package-data]
67
- quantdb_sdk = ["py.typed"]
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "quantdb-sdk"
7
+ version = "0.4.1"
8
+ description = "QuantDB 量化数据平台官方 Python SDK"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "QuantDB Team"},
14
+ ]
15
+ keywords = ["quant", "finance", "data", "a-share", "parquet"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Financial and Insurance Industry",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Office/Business :: Financial",
28
+ ]
29
+ dependencies = [
30
+ "requests>=2.25.0",
31
+ "pandas>=1.3.0",
32
+ "httpx>=0.24.0",
33
+ # 0.4.1 起 client.data.read_local 用 read_parquet(filename=true) + `* EXCLUDE (filename)`
34
+ # 做精确分区读取;0.8.x 在部分环境(Windows/Python 3.10)导入即崩溃,1.0.0~1.5.5 已验证。
35
+ "duckdb>=1.0.0",
36
+ # 0.1.1 修复:pyarrow 是 pd.read_parquet 的硬依赖(client.py:482, async_client.py:413),
37
+ # 但 0.1.0 漏声明,导致 pip install 后首次 K线/Tick 查询会抛 ImportError。
38
+ # 锁定到 >=10.0.0 以获得 Python 3.10+ 完整支持(pyarrow 14+ 更好)。
39
+ "pyarrow>=10.0.0",
40
+ ]
41
+
42
+ # 0.1.2 新增:装包后提供 `quantdb` shell 命令,等价于 `python -m quantdb_sdk`。
43
+ # 0.1.0/0.1.1 README 写的是 `python -m quantdb`(用了 PyPI 名带连字符),
44
+ # 但 Python 模块名是下划线,安装后 `python -m quantdb` 会报 No module named quantdb。
45
+ # 加这个 entry point 后两个名字都能用,README 不需要再改。
46
+ [project.scripts]
47
+ quantdb = "quantdb_sdk.__main__:main"
48
+
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "pytest>=7.0.0",
52
+ "pytest-asyncio>=0.21.0",
53
+ "responses>=0.23.0",
54
+ "respx>=0.20.0",
55
+ ]
56
+
57
+ [project.urls]
58
+ Homepage = "https://quantdb.quantmind.cloud"
59
+ Documentation = "https://quantdb.quantmind.cloud/docs/sdk.html"
60
+ Repository = "https://github.com/quantdb/quantdb"
61
+ Changelog = "https://github.com/quantdb/quantdb/blob/main/CHANGELOG.md"
62
+
63
+ [tool.setuptools.packages.find]
64
+ where = ["."]
65
+ include = ["quantdb_sdk*"]
66
+
67
+ # 0.1.2 新增:让 wheel 包含 LICENSE 和 CHANGELOG.md
68
+ [tool.setuptools.package-data]
69
+ quantdb_sdk = ["py.typed"]
@@ -5,7 +5,7 @@ try:
5
5
  # 注意:此处用 PyPI 包名(连字符)查找版本号,不是模块名(下划线)
6
6
  __version__ = version("quantdb-sdk")
7
7
  except ImportError:
8
- __version__ = "0.4.0"
8
+ __version__ = "0.4.1"
9
9
 
10
10
  from . import layout
11
11
  from ._utils import scan_local_directory
@@ -28,7 +28,7 @@ from .errors import (
28
28
  )
29
29
 
30
30
  # 维护提示:每次发版必须与 pyproject.toml 版本号同步
31
- _USER_AGENT = "QuantDB-Python-SDK/0.4.0"
31
+ _USER_AGENT = "QuantDB-Python-SDK/0.4.1"
32
32
 
33
33
 
34
34
  def _etag_hex(value: Optional[str]) -> Optional[str]:
@@ -32,7 +32,7 @@ from .errors import (
32
32
  )
33
33
 
34
34
  # 维护提示:每次发版必须与 pyproject.toml 版本号同步
35
- _USER_AGENT = "QuantDB-Python-SDK/0.4.0"
35
+ _USER_AGENT = "QuantDB-Python-SDK/0.4.1"
36
36
 
37
37
 
38
38
  def _etag_hex(value: Optional[str]) -> Optional[str]:
@@ -23,12 +23,14 @@
23
23
  ``tick_shard``),用于 ``query()`` 路由;不用 :func:`quantdb_sdk.layout.dataset_layout`
24
24
  (其 V2 集合已过期,缺 min1/min5/etf/tick)。
25
25
  - 本地读取用**多形态 glob** 与目录布局集合解耦,支持显式 ``root``(数据根目录可配置,
26
- 非固定目录;缺省读 :func:`quantdb_sdk.layout.data_root`)。
26
+ 非固定目录;缺省读 :func:`quantdb_sdk.layout.data_root`);读取本身交给 DuckDB 对
27
+ 精确文件列表做一次 ``read_parquet``(列裁剪 / symbol / 日期下推),不逐文件拼接。
27
28
  """
28
29
 
29
30
  import glob
30
31
  import os
31
32
  import re
33
+ import threading
32
34
  from dataclasses import dataclass, field
33
35
  from typing import Any, Dict, List, Optional, Tuple
34
36
 
@@ -52,6 +54,16 @@ _KLINE_ADJ_TYPES = {
52
54
  # V1 逐股票表的日期列候选(财务表多为 m_timetag;holder_num 为 endDate;l1_l2_factors 为 date)
53
55
  _V1_DATE_COLUMNS = ("time", "trade_date", "date", "_filename_date", "m_timetag", "endDate")
54
56
 
57
+ # 从路径推导 trade_date 的 DuckDB 表达式(V2 用 dt= 目录名;tick 用文件名 _YYYYMMDD)。
58
+ # 统一从 filename 提取,不依赖 hive 注入的 dt 列——dt 列仅在较新 DuckDB 自动出现,
59
+ # 低版本(如 1.0.x)不存在,会使 `EXCLUDE (dt)` / `CAST(dt ...)` 直接报错。
60
+ _V2_DATE_EXPR = (
61
+ "strftime(strptime(regexp_extract(filename, 'dt=(\\d{8})', 1), '%Y%m%d'), '%Y-%m-%d')"
62
+ )
63
+ _TICK_DATE_EXPR = (
64
+ "strftime(strptime(regexp_extract(filename, '_(\\d{8})\\.parquet$', 1), '%Y%m%d'), '%Y-%m-%d')"
65
+ )
66
+
55
67
 
56
68
  @dataclass(frozen=True)
57
69
  class DatasetInfo:
@@ -315,6 +327,41 @@ def _local_path(
315
327
  return files[-1]
316
328
 
317
329
 
330
+ # DuckDB 连接(线程内复用):精确文件列表查询用裸连接即可,无需挂载 glob 视图。
331
+ # 参考实现 quantmindoss QuantDBDataHub._exact_conn 的结论:复用视图连接会为每个
332
+ # 新线程重建视图,首查开销极大;精确路径查询不需要视图。
333
+ _DUCK_LOCAL = threading.local()
334
+
335
+
336
+ def _duck_conn():
337
+ """返回当前线程复用的 DuckDB 连接(懒建)。"""
338
+ conn = getattr(_DUCK_LOCAL, "conn", None)
339
+ if conn is None:
340
+ try:
341
+ import duckdb # type: ignore
342
+ except ImportError as exc:
343
+ raise RuntimeError("本地零流量读取需要 duckdb:pip install duckdb") from exc
344
+ conn = duckdb.connect()
345
+ _DUCK_LOCAL.conn = conn
346
+ return conn
347
+
348
+
349
+ def _sql_file_list(files: List[str]) -> str:
350
+ """把文件列表转成 ``read_parquet([...])`` 的 SQL 字面量(/ 分隔、转义单引号)。"""
351
+ return ", ".join("'" + p.replace("\\", "/").replace("'", "''") + "'" for p in files)
352
+
353
+
354
+ def _exec_df(sql: str, params: Optional[List[Any]] = None) -> pd.DataFrame:
355
+ return _duck_conn().execute(sql, params or []).fetchdf()
356
+
357
+
358
+ def _detect_local_date_column(files: List[str]) -> Optional[str]:
359
+ """用 DuckDB 元数据(``LIMIT 0``,不读行数据)探测 V1 文件的日期列。"""
360
+ sql = f"SELECT * FROM read_parquet([{_sql_file_list(files)}], union_by_name=true) LIMIT 0"
361
+ names = [col[0] for col in _duck_conn().execute(sql).description]
362
+ return next((c for c in _V1_DATE_COLUMNS if c in names), None)
363
+
364
+
318
365
  def _read_local(
319
366
  info: DatasetInfo,
320
367
  symbol: Optional[str] = None,
@@ -325,52 +372,86 @@ def _read_local(
325
372
  root: Optional[str] = None,
326
373
  latest: bool = False,
327
374
  ) -> pd.DataFrame:
328
- """读取本地 Parquet(零流量)。V2/tick 无日期列时从路径注入 ``trade_date``。"""
375
+ """读取本地 Parquet(零流量)。
376
+
377
+ 先用多形态 glob 定位**所需分区**(只枚举目录名 / 文件名,不读文件内容),再交给
378
+ DuckDB 对精确文件列表做**一次** ``read_parquet``,并把 symbol / 日期 / 列裁剪
379
+ 下推到 SQL——不再逐文件 ``pd.read_parquet`` 再 ``concat``。
380
+
381
+ V2 按日分区与 tick 分片文件内无日期列,``trade_date`` 由分区目录名 / 文件名推导。
382
+ """
329
383
  files = _local_files(info, symbol, trade_date, root, start_date, end_date)
330
384
  if not files:
331
385
  raise NotFoundError(f"本地未找到数据集 {info.dataset} 的文件(root={_local_root(root)})")
332
386
  if latest:
333
387
  files = [files[-1]]
334
388
 
335
- need_date_filter = info.layout == "v1_symbol" and bool(start_date or end_date)
336
- frames: List[pd.DataFrame] = []
337
- for path in files:
338
- # V1 需要日期列做区间过滤,先整表读;其余按 columns 下推
339
- df = pd.read_parquet(path) if need_date_filter else pd.read_parquet(path, columns=columns)
340
- if info.layout == "v2_daily_partition":
341
- td = _v2_trade_date(path)
342
- if td and "trade_date" not in df.columns:
343
- df = df.copy()
344
- df["trade_date"] = td
345
- elif info.layout == "tick_shard":
346
- td = _tick_trade_date(path)
347
- if td and "trade_date" not in df.columns:
348
- df = df.copy()
349
- df["trade_date"] = td
350
- frames.append(df)
389
+ if info.layout == "v1_symbol":
390
+ return _read_local_v1(files, start_date, end_date, columns)
391
+ return _read_local_partitioned(info, files, symbol, columns)
351
392
 
352
- df = frames[0] if len(frames) == 1 else pd.concat(frames, ignore_index=True, sort=False)
353
393
 
354
- # tick / V2 全市场分区需按 symbol 后置过滤
355
- if symbol and "symbol" in df.columns:
356
- df = df[df["symbol"].astype(str).str.upper() == str(symbol).strip().upper()].copy()
394
+ def _read_local_partitioned(
395
+ info: DatasetInfo,
396
+ files: List[str],
397
+ symbol: Optional[str],
398
+ columns: Optional[List[str]],
399
+ ) -> pd.DataFrame:
400
+ """V2 按日分区 / tick 分片:``trade_date`` 由路径推导,读取与过滤下推到 DuckDB。"""
401
+ if info.layout == "v2_daily_partition":
402
+ date_expr, order = _V2_DATE_EXPR, " ORDER BY trade_date"
403
+ else:
404
+ date_expr, order = _TICK_DATE_EXPR, ""
405
+
406
+ if columns:
407
+ wanted = [c for c in columns if c != "trade_date"]
408
+ projected = ", ".join(f'"{c}"' for c in wanted)
409
+ select = f"{projected}, {date_expr} AS trade_date" if projected else f"{date_expr} AS trade_date"
410
+ else:
411
+ select = f"* EXCLUDE (filename), {date_expr} AS trade_date"
412
+
413
+ where: List[str] = []
414
+ params: List[Any] = []
415
+ if symbol:
416
+ where.append("UPPER(symbol) = ?")
417
+ params.append(str(symbol).strip().upper())
418
+
419
+ sql = f"SELECT {select} FROM read_parquet([{_sql_file_list(files)}], union_by_name=true, filename=true)"
420
+ if where:
421
+ sql += " WHERE " + " AND ".join(where)
422
+ sql += order
423
+ return _exec_df(sql, params).reset_index(drop=True)
424
+
425
+
426
+ def _read_local_v1(
427
+ files: List[str],
428
+ start_date: Optional[str],
429
+ end_date: Optional[str],
430
+ columns: Optional[List[str]],
431
+ ) -> pd.DataFrame:
432
+ """V1 逐股票 / 单文件:日期列在文件内,用归一化 YYYYMMDD 字符串比较下推过滤。
357
433
 
358
- if need_date_filter:
359
- date_col = _detect_date_column(df)
434
+ ``substr(replace(CAST(col AS VARCHAR),'-',''),1,8)`` 同时兼容 ``20260331``(整型
435
+ 报告期)与 ``2025-12-31`` / ``2025-12-31 09:30:00``(字符串),无需先探测列类型。
436
+ """
437
+ where: List[str] = []
438
+ params: List[Any] = []
439
+ if start_date or end_date:
440
+ date_col = _detect_local_date_column(files)
360
441
  if date_col:
361
- dt = pd.to_datetime(df[date_col], errors="coerce")
362
- mask = pd.Series(True, index=df.index)
442
+ norm = f"substr(replace(CAST(\"{date_col}\" AS VARCHAR), '-', ''), 1, 8)"
363
443
  if start_date:
364
- mask &= dt >= pd.to_datetime(_norm_date(start_date))
444
+ where.append(f"{norm} >= ?")
445
+ params.append(_yyyymmdd(start_date))
365
446
  if end_date:
366
- mask &= dt <= pd.to_datetime(_norm_date(end_date))
367
- df = df[mask].copy()
368
- if columns:
369
- df = df.loc[:, [c for c in columns if c in df.columns]]
447
+ where.append(f"{norm} <= ?")
448
+ params.append(_yyyymmdd(end_date))
370
449
 
371
- if info.layout == "v2_daily_partition" and "trade_date" in df.columns:
372
- df = df.sort_values("trade_date", kind="stable")
373
- return df.reset_index(drop=True)
450
+ select = ", ".join(f'"{c}"' for c in columns) if columns else "*"
451
+ sql = f"SELECT {select} FROM read_parquet([{_sql_file_list(files)}], union_by_name=true)"
452
+ if where:
453
+ sql += " WHERE " + " AND ".join(where)
454
+ return _exec_df(sql, params).reset_index(drop=True)
374
455
 
375
456
 
376
457
  # ---------------------------------------------------------------------------
@@ -478,8 +559,11 @@ class DataHub:
478
559
  ) -> pd.DataFrame:
479
560
  """读取本地已落地的 Parquet(零流量)。
480
561
 
481
- - ``start_date/end_date`` 先按 ``dt=`` 目录名 / tick 文件名裁剪文件,再读取;
482
- - V2 与 tick 文件无日期列时,``trade_date`` 从路径注入;
562
+ - ``start_date/end_date`` 先按 ``dt=`` 目录名 / tick 文件名裁剪文件(只枚举目录,
563
+ 不读内容),再交给 DuckDB 对精确文件列表做一次 ``read_parquet``;
564
+ - ``symbol`` / ``columns`` / 日期过滤全部下推到 SQL(宽表只读需要的列),
565
+ 避免逐文件 ``pd.read_parquet`` 再 ``concat``;
566
+ - V2 与 tick 文件无日期列时,``trade_date`` 由分区目录名 / 文件名推导;
483
567
  - ``latest=True`` 只读最新一个文件;
484
568
  - 未命中本地文件时抛 ``NotFoundError``(不自动走在线,避免静默消耗流量)。
485
569
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantdb-sdk
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: QuantDB 量化数据平台官方 Python SDK
5
5
  Author: QuantDB Team
6
6
  License: MIT
@@ -26,7 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: requests>=2.25.0
27
27
  Requires-Dist: pandas>=1.3.0
28
28
  Requires-Dist: httpx>=0.24.0
29
- Requires-Dist: duckdb>=0.8.0
29
+ Requires-Dist: duckdb>=1.0.0
30
30
  Requires-Dist: pyarrow>=10.0.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -1,7 +1,7 @@
1
1
  requests>=2.25.0
2
2
  pandas>=1.3.0
3
3
  httpx>=0.24.0
4
- duckdb>=0.8.0
4
+ duckdb>=1.0.0
5
5
  pyarrow>=10.0.0
6
6
 
7
7
  [dev]
@@ -250,6 +250,32 @@ def test_read_local_v1_date_filter(tmp_path, client):
250
250
  assert len(df) == 2
251
251
 
252
252
 
253
+ def test_read_local_v1_int_report_period_filter(tmp_path, client):
254
+ """V1 整型报告期(如 m_timetag=20251231)也要能被日期区间正确过滤。"""
255
+ root = str(tmp_path)
256
+ path = tmp_path / "3_financial_data" / "income" / "600519.SH.parquet"
257
+ path.parent.mkdir(parents=True)
258
+ pd.DataFrame({
259
+ "Symbol": ["600519.SH"] * 3,
260
+ "m_timetag": [20251231, 20250930, 20250630],
261
+ "x": [1, 2, 3],
262
+ }).to_parquet(path)
263
+ df = client.data.read_local("income", symbol="600519.SH", root=root,
264
+ start_date="2025-09-01", end_date="2025-12-31")
265
+ assert len(df) == 2
266
+
267
+
268
+ def test_read_local_v2_columns_projection_and_symbol_pushdown(tmp_path, client):
269
+ """V2:列裁剪下推 + symbol 下推(symbol 不在 columns 里也照样过滤)。"""
270
+ root = str(tmp_path)
271
+ _write_v2(root, "daily_forward", ["20260901"])
272
+ df = client.data.read_local("daily_forward", symbol="600519.SH", columns=["close"], root=root)
273
+ assert list(df.columns) == ["close", "trade_date"]
274
+ assert len(df) == 1
275
+ assert df.iloc[0]["close"] == 1.0
276
+ assert df.iloc[0]["trade_date"] == "2026-09-01"
277
+
278
+
253
279
  def test_local_path_precise_hit(tmp_path, client):
254
280
  root = str(tmp_path)
255
281
  _write_v2(root, "daily_forward", ["20260901"])
File without changes
File without changes
File without changes
File without changes