quantzone 0.4.0__cp313-cp313-win_amd64.whl

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.
quantzone/__init__.py ADDED
@@ -0,0 +1,119 @@
1
+ """宽舟科技量化数据平台 Python SDK。
2
+
3
+ 两种使用方式:
4
+
5
+ 1. 实例化客户端(推荐)::
6
+
7
+ from quantzone import QuantZone
8
+
9
+ client = QuantZone(access_key="AK...", sign_secret="...", base_url="https://...")
10
+ df = client.get_factors(order_book_ids="000001.SZ", factor="alpha1",
11
+ start_date="2024-01-01", end_date="2024-12-31")
12
+
13
+ 2. 模块级单例(可用 ``import qz`` 简写)::
14
+
15
+ import qz
16
+
17
+ qz.init(access_key="AK...", sign_secret="...", base_url="https://...")
18
+ df = qz.get_factors(order_book_ids="000001.SZ", factor="alpha1",
19
+ start_date="2024-01-01", end_date="2024-12-31")
20
+ """
21
+
22
+ from datetime import date
23
+
24
+ import pandas as pd
25
+
26
+ from ._dates import DateLike
27
+ from ._singleton import get_instance, init
28
+ from .client import QuantZone
29
+ from .exceptions import (
30
+ ApiKeyRateLimitError,
31
+ ApiKeyRevokedError,
32
+ AuthError,
33
+ IpNotAllowedError,
34
+ NetworkError,
35
+ QuantError,
36
+ QuotaExceededError,
37
+ SDKVersionError,
38
+ SignatureError,
39
+ )
40
+ from .quota import Quota
41
+
42
+ __version__ = "0.4.0"
43
+
44
+
45
+ def get_factors(
46
+ order_book_ids: str | list[str] | None = None,
47
+ factor: str | list[str] | None = None,
48
+ start_date: DateLike | None = None,
49
+ end_date: DateLike | None = None,
50
+ filename: str | None = None,
51
+ ) -> pd.DataFrame:
52
+ """模块级 ``get_factors``(需先调用 ``quantzone.init``)。"""
53
+ return get_instance().get_factors(
54
+ order_book_ids=order_book_ids,
55
+ factor=factor,
56
+ start_date=start_date,
57
+ end_date=end_date,
58
+ filename=filename,
59
+ )
60
+
61
+
62
+ def list_factors() -> list[dict]:
63
+ """模块级 ``list_factors``。"""
64
+ return get_instance().list_factors()
65
+
66
+
67
+ def list_stocks() -> pd.DataFrame:
68
+ """模块级 ``list_stocks``。"""
69
+ return get_instance().list_stocks()
70
+
71
+
72
+ def get_trading_dates(start_date: DateLike, end_date: DateLike) -> list[date]:
73
+ """模块级 ``get_trading_dates``。"""
74
+ return get_instance().get_trading_dates(start_date, end_date)
75
+
76
+
77
+ def get_previous_trading_date(current_date: DateLike, n: int = 1) -> date:
78
+ """模块级 ``get_previous_trading_date``。"""
79
+ return get_instance().get_previous_trading_date(current_date, n)
80
+
81
+
82
+ def get_next_trading_date(current_date: DateLike, n: int = 1) -> date:
83
+ """模块级 ``get_next_trading_date``。"""
84
+ return get_instance().get_next_trading_date(current_date, n)
85
+
86
+
87
+ def is_trading_date(current_date: DateLike) -> bool:
88
+ """模块级 ``is_trading_date``。"""
89
+ return get_instance().is_trading_date(current_date)
90
+
91
+
92
+ def get_quota() -> Quota:
93
+ """模块级 ``get_quota``。"""
94
+ return get_instance().get_quota()
95
+
96
+
97
+ __all__ = [
98
+ "ApiKeyRateLimitError",
99
+ "ApiKeyRevokedError",
100
+ "QuantZone",
101
+ "Quota",
102
+ "QuantError",
103
+ "AuthError",
104
+ "IpNotAllowedError",
105
+ "QuotaExceededError",
106
+ "NetworkError",
107
+ "SignatureError",
108
+ "SDKVersionError",
109
+ "init",
110
+ "get_factors",
111
+ "list_factors",
112
+ "list_stocks",
113
+ "get_trading_dates",
114
+ "get_previous_trading_date",
115
+ "get_next_trading_date",
116
+ "is_trading_date",
117
+ "get_quota",
118
+ "__version__",
119
+ ]
Binary file
quantzone/_auth.pyi ADDED
@@ -0,0 +1,9 @@
1
+ from .exceptions import AuthError as AuthError
2
+
3
+ SERVICE_NAME: str
4
+ USERNAME_ACCESS_KEY: str
5
+ USERNAME_SIGN_SECRET: str
6
+
7
+ def get_credentials(access_key: str | None = None, sign_secret: str | None = None) -> tuple[str, str]: ...
8
+ def save_credentials(access_key: str, sign_secret: str) -> None: ...
9
+ def delete_credentials() -> None: ...
quantzone/_dates.py ADDED
@@ -0,0 +1,49 @@
1
+ """日期参数归一化工具。
2
+
3
+ 接受 int(YYYYMMDD) / str / date / datetime / Timestamp 多种格式,
4
+ 本模块统一归一为 ISO ``YYYY-MM-DD`` 字符串发往服务端。
5
+ """
6
+
7
+ from datetime import date, datetime
8
+ from typing import Union
9
+
10
+ import pandas as pd
11
+
12
+ DateLike = Union[int, str, date, datetime, pd.Timestamp]
13
+
14
+ _ISO_LEN = 10
15
+ _INT_DATE_LEN = 8
16
+ _INT_DATE_MIN = 19000101
17
+ _INT_DATE_MAX = 99991231
18
+
19
+
20
+ def normalize_date(value: DateLike | None) -> str | None:
21
+ """把多种日期表示归一为 ISO ``YYYY-MM-DD`` 字符串。
22
+
23
+ Args:
24
+ value: 日期,支持 ``int(YYYYMMDD)`` / ``str`` / ``date`` / ``datetime`` /
25
+ ``pd.Timestamp``;为 None 时直接返回 None。
26
+
27
+ Returns:
28
+ ISO 格式日期字符串,或 None。
29
+
30
+ Raises:
31
+ ValueError: 类型不支持或字符串无法解析为日期。
32
+ """
33
+ if value is None:
34
+ return None
35
+ if isinstance(value, bool):
36
+ raise ValueError(f"日期不接受 bool 类型: {value!r}")
37
+ if isinstance(value, int):
38
+ if not (_INT_DATE_MIN <= value <= _INT_DATE_MAX) or len(str(value)) != _INT_DATE_LEN:
39
+ raise ValueError(f"int 日期必须为 8 位 YYYYMMDD: {value!r}")
40
+ return f"{value // 10000:04d}-{(value // 100) % 100:02d}-{value % 100:02d}"
41
+ if isinstance(value, datetime):
42
+ return value.date().isoformat()
43
+ if isinstance(value, date):
44
+ return value.isoformat()
45
+ if isinstance(value, pd.Timestamp):
46
+ return value.date().isoformat()
47
+ if isinstance(value, str):
48
+ return pd.Timestamp(value).date().isoformat()
49
+ raise ValueError(f"不支持的日期类型: {type(value).__name__}={value!r}")
Binary file
quantzone/_http.pyi ADDED
@@ -0,0 +1,24 @@
1
+ from ._signer import sign_request as sign_request
2
+ from .exceptions import ApiKeyRateLimitError as ApiKeyRateLimitError, ApiKeyRevokedError as ApiKeyRevokedError, AuthError as AuthError, IpNotAllowedError as IpNotAllowedError, NetworkError as NetworkError, QuantError as QuantError, QuotaExceededError as QuotaExceededError, SDKVersionError as SDKVersionError, SignatureError as SignatureError
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ DEFAULT_TIMEOUT: float
9
+ SDK_VERSION_STATUS: int
10
+ STREAM_CHUNK_SIZE: int
11
+ HEADER_API_KEY: str
12
+ HEADER_SDK_VERSION: str
13
+ HEADER_ESTIMATED_BYTES: str
14
+ HEADER_ROW_COUNT: str
15
+
16
+ class HttpClient:
17
+ def __init__(self, access_key: str, sign_secret: str, base_url: str, timeout: float = ...) -> None: ...
18
+ def close(self) -> None: ...
19
+ def request(self, method: str, path: str, *, params: dict[str, Any] | None = None, json_body: Any | None = None) -> Any: ...
20
+ def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any: ...
21
+ def post(self, path: str, *, json_body: Any | None = None, params: dict[str, Any] | None = None) -> Any: ...
22
+ def stream_post(self, path: str, json_body: dict[str, Any], out_file: Path) -> dict[str, int]: ...
23
+ @contextmanager
24
+ def stream_post_iter(self, path: str, json_body: dict[str, Any]) -> Iterator[Iterator[bytes]]: ...
quantzone/_logger.py ADDED
@@ -0,0 +1,17 @@
1
+ """SDK 共享 logger,默认输出到 stderr。
2
+
3
+ 用户可通过 ``logging.getLogger("quantzone").setLevel(...)`` 调整级别;
4
+ 若用户已配置 root logger,可设置 ``logger.propagate = True`` 让消息传递。
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+
10
+ logger = logging.getLogger("quantzone")
11
+
12
+ if not logger.handlers:
13
+ handler = logging.StreamHandler(sys.stderr)
14
+ handler.setFormatter(logging.Formatter("[quantzone] %(message)s"))
15
+ logger.addHandler(handler)
16
+ logger.setLevel(logging.INFO)
17
+ logger.propagate = False
Binary file
quantzone/_signer.pyi ADDED
@@ -0,0 +1 @@
1
+ def sign_request(sign_secret: str, method: str, path: str, body: bytes | None = None, *, params: dict[str, str] | None = None) -> dict[str, str]: ...
@@ -0,0 +1,72 @@
1
+ """模块级 ``quantzone.init(...)`` 维护的单例 ``QuantZone``。
2
+
3
+ 顶层 init 风格:用户在程序启动时调用一次 ``init``,
4
+ 之后通过模块顶层函数(如 ``quantzone.get_factors``)直接使用。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from .client import QuantZone
14
+
15
+ _INSTANCE: "QuantZone | None" = None
16
+
17
+
18
+ def init(
19
+ access_key: str | None = None,
20
+ sign_secret: str | None = None,
21
+ *,
22
+ base_url: str,
23
+ timeout: float = 60.0,
24
+ download_dir: Path | str | None = None,
25
+ ) -> "QuantZone":
26
+ """初始化模块级单例 ``QuantZone``。
27
+
28
+ Args:
29
+ access_key: 公开标识符;None 时从 keyring 读取。
30
+ sign_secret: 签名密钥;None 时从 keyring 读取。
31
+ base_url: 服务端地址,必须显式传入。
32
+ timeout: 请求超时(秒)。
33
+ download_dir: 下载目录;None 时使用 ``~/.quantzone/downloads``。
34
+
35
+ Returns:
36
+ 已初始化的 ``QuantZone``。重复调用会先关闭旧实例再创建新的。
37
+ """
38
+ from .client import QuantZone
39
+
40
+ global _INSTANCE
41
+ if _INSTANCE is not None:
42
+ _INSTANCE.close()
43
+ _INSTANCE = QuantZone(
44
+ access_key=access_key,
45
+ sign_secret=sign_secret,
46
+ base_url=base_url,
47
+ timeout=timeout,
48
+ download_dir=download_dir,
49
+ )
50
+ return _INSTANCE
51
+
52
+
53
+ def get_instance() -> "QuantZone":
54
+ """获取已初始化的单例。
55
+
56
+ Returns:
57
+ 当前单例。
58
+
59
+ Raises:
60
+ RuntimeError: 调用 ``init()`` 之前调用本函数。
61
+ """
62
+ if _INSTANCE is None:
63
+ raise RuntimeError("尚未调用 quantzone.init(),请先初始化")
64
+ return _INSTANCE
65
+
66
+
67
+ def reset() -> None:
68
+ """关闭并清除单例(仅用于测试隔离)。"""
69
+ global _INSTANCE
70
+ if _INSTANCE is not None:
71
+ _INSTANCE.close()
72
+ _INSTANCE = None
quantzone/calendar.py ADDED
@@ -0,0 +1,96 @@
1
+ """交易日历查询服务(仅 A 股)。
2
+
3
+ 接口约定(服务端实现见 quant-cloud):
4
+
5
+ - ``GET /api/calendar/trading-dates?start_date=&end_date=`` → ``["2024-01-02", ...]``
6
+ - ``GET /api/calendar/previous?date=&n=1`` → ``"2024-01-12"``
7
+ - ``GET /api/calendar/next?date=&n=1`` → ``"2024-01-16"``
8
+ - ``GET /api/calendar/is-trading-date?date=`` → ``{"is_trading": true}``
9
+ """
10
+
11
+ from datetime import date
12
+
13
+ from ._dates import DateLike, normalize_date
14
+ from ._http import HttpClient
15
+
16
+
17
+ def _to_date(s: str) -> date:
18
+ """ISO 字符串转 ``date``。"""
19
+ return date.fromisoformat(s)
20
+
21
+
22
+ class CalendarService:
23
+ """交易日历查询服务。"""
24
+
25
+ def __init__(self, http: HttpClient) -> None:
26
+ """初始化。
27
+
28
+ Args:
29
+ http: HTTP 客户端。
30
+ """
31
+ self._http = http
32
+
33
+ def get_trading_dates(self, start_date: DateLike, end_date: DateLike) -> list[date]:
34
+ """获取区间内所有交易日(含起止两端)。
35
+
36
+ Args:
37
+ start_date: 起始日期。
38
+ end_date: 截止日期。
39
+
40
+ Returns:
41
+ 升序排列的交易日列表。
42
+ """
43
+ params = {
44
+ "start_date": normalize_date(start_date),
45
+ "end_date": normalize_date(end_date),
46
+ }
47
+ data = self._http.get("/api/calendar/trading-dates", params=params)
48
+ items = data if isinstance(data, list) else data.get("items", [])
49
+ return [_to_date(s) for s in items]
50
+
51
+ def get_previous_trading_date(self, current_date: DateLike, n: int = 1) -> date:
52
+ """返回 ``current_date`` 之前第 n 个交易日。
53
+
54
+ Args:
55
+ current_date: 参考日期(不计入返回结果)。
56
+ n: 向前偏移的交易日数,默认 1。
57
+
58
+ Returns:
59
+ 交易日。
60
+ """
61
+ params = {"date": normalize_date(current_date), "n": str(n)}
62
+ data = self._http.get("/api/calendar/previous", params=params)
63
+ if isinstance(data, dict):
64
+ return _to_date(data["date"])
65
+ return _to_date(data)
66
+
67
+ def get_next_trading_date(self, current_date: DateLike, n: int = 1) -> date:
68
+ """返回 ``current_date`` 之后第 n 个交易日。
69
+
70
+ Args:
71
+ current_date: 参考日期(不计入返回结果)。
72
+ n: 向后偏移的交易日数,默认 1。
73
+
74
+ Returns:
75
+ 交易日。
76
+ """
77
+ params = {"date": normalize_date(current_date), "n": str(n)}
78
+ data = self._http.get("/api/calendar/next", params=params)
79
+ if isinstance(data, dict):
80
+ return _to_date(data["date"])
81
+ return _to_date(data)
82
+
83
+ def is_trading_date(self, current_date: DateLike) -> bool:
84
+ """判断给定日期是否为交易日。
85
+
86
+ Args:
87
+ current_date: 待判断日期。
88
+
89
+ Returns:
90
+ 是否为交易日。
91
+ """
92
+ params = {"date": normalize_date(current_date)}
93
+ data = self._http.get("/api/calendar/is-trading-date", params=params)
94
+ if isinstance(data, dict):
95
+ return bool(data.get("is_trading", False))
96
+ return bool(data)
quantzone/client.py ADDED
@@ -0,0 +1,145 @@
1
+ """QuantZone 主类 — 组装所有模块,提供统一 API。"""
2
+
3
+ from datetime import date
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+
8
+ from ._auth import get_credentials
9
+ from ._dates import DateLike
10
+ from ._http import HttpClient
11
+ from .calendar import CalendarService
12
+ from .factor import FactorService
13
+ from .quota import Quota, QuotaService
14
+ from .stock import StockService
15
+
16
+
17
+ class QuantZone:
18
+ """宽舟科技量化数据平台客户端(AK/SK 双密钥)。
19
+
20
+ 使用示例::
21
+
22
+ from quantzone import QuantZone
23
+
24
+ client = QuantZone(
25
+ access_key="AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
26
+ sign_secret="...",
27
+ base_url="https://your-api.example.com",
28
+ )
29
+
30
+ df = client.get_factors(
31
+ order_book_ids=["000001.SZ", "000002.SZ"],
32
+ factor=["alpha1", "vwap"],
33
+ start_date="2024-01-01",
34
+ end_date="2024-12-31",
35
+ )
36
+
37
+ client.close()
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ access_key: str | None = None,
43
+ sign_secret: str | None = None,
44
+ *,
45
+ base_url: str,
46
+ timeout: float = 60.0,
47
+ download_dir: Path | str | None = None,
48
+ ) -> None:
49
+ """初始化客户端。
50
+
51
+ Args:
52
+ access_key: 公开标识符;None 时从 keyring 读取。
53
+ sign_secret: 签名密钥;None 时从 keyring 读取。
54
+ base_url: 服务端地址,必须显式提供(SDK 不内置默认地址)。
55
+ timeout: 请求超时(秒),默认 60。大数据量查询调到 600+。
56
+ download_dir: 默认下载目录;None 表示 ``~/.quantzone/downloads``。
57
+ """
58
+ resolved_ak, resolved_sk = get_credentials(access_key, sign_secret)
59
+
60
+ self._http = HttpClient(
61
+ access_key=resolved_ak,
62
+ sign_secret=resolved_sk,
63
+ base_url=base_url,
64
+ timeout=timeout,
65
+ )
66
+ resolved_dir = Path(download_dir).expanduser() if download_dir else None
67
+ self._factor_svc = FactorService(self._http, download_dir=resolved_dir)
68
+ self._stock_svc = StockService(self._http)
69
+ self._quota_svc = QuotaService(self._http)
70
+ self._calendar_svc = CalendarService(self._http)
71
+
72
+ def close(self) -> None:
73
+ """关闭底层 HTTP 连接。"""
74
+ self._http.close()
75
+
76
+ def __enter__(self) -> "QuantZone":
77
+ return self
78
+
79
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
80
+ self.close()
81
+ return False
82
+
83
+ # ── 因子 ──────────────────────────────────────────────
84
+
85
+ def get_factors(
86
+ self,
87
+ order_book_ids: str | list[str] | None = None,
88
+ factor: str | list[str] | None = None,
89
+ start_date: DateLike | None = None,
90
+ end_date: DateLike | None = None,
91
+ filename: str | None = None,
92
+ ) -> pd.DataFrame:
93
+ """一次性返回完整因子数据(窄表)。
94
+
95
+ Args:
96
+ order_book_ids: 股票代码(``str`` / ``list[str]`` / None=全市场)。
97
+ factor: 因子名(``str`` / ``list[str]`` / None=全部)。
98
+ start_date: 起始日期。
99
+ end_date: 截止日期。
100
+ filename: 自定义保存文件名。
101
+
102
+ Returns:
103
+ 窄表 ``DataFrame``,列:``DataDate / ukey / x / factorname``。
104
+ """
105
+ return self._factor_svc.get_factors(
106
+ order_book_ids=order_book_ids,
107
+ factor=factor,
108
+ start_date=start_date,
109
+ end_date=end_date,
110
+ filename=filename,
111
+ )
112
+
113
+ def list_factors(self) -> list[dict]:
114
+ """获取因子元信息列表(含描述、分类)。"""
115
+ return self._factor_svc.list_factors()
116
+
117
+ # ── 合约元信息 ────────────────────────────────────
118
+
119
+ def list_stocks(self) -> pd.DataFrame:
120
+ """获取全量合约(``order_book_id / industry_name / symbol``)。"""
121
+ return self._stock_svc.list_stocks()
122
+
123
+ # ── 交易日历 ──────────────────────────────────────
124
+
125
+ def get_trading_dates(self, start_date: DateLike, end_date: DateLike) -> list[date]:
126
+ """获取区间内交易日列表。"""
127
+ return self._calendar_svc.get_trading_dates(start_date, end_date)
128
+
129
+ def get_previous_trading_date(self, current_date: DateLike, n: int = 1) -> date:
130
+ """前 n 个交易日。"""
131
+ return self._calendar_svc.get_previous_trading_date(current_date, n)
132
+
133
+ def get_next_trading_date(self, current_date: DateLike, n: int = 1) -> date:
134
+ """后 n 个交易日。"""
135
+ return self._calendar_svc.get_next_trading_date(current_date, n)
136
+
137
+ def is_trading_date(self, current_date: DateLike) -> bool:
138
+ """判断是否为交易日。"""
139
+ return self._calendar_svc.is_trading_date(current_date)
140
+
141
+ # ── 配额 ──────────────────────────────────────────
142
+
143
+ def get_quota(self) -> Quota:
144
+ """获取当前用户配额。"""
145
+ return self._quota_svc.get_quota()
@@ -0,0 +1,108 @@
1
+ """宽舟科技 SDK 异常定义."""
2
+
3
+
4
+ class QuantError(Exception):
5
+ """SDK 基类异常.
6
+
7
+ Attributes:
8
+ message: 错误信息。
9
+ status_code: HTTP 状态码;非 HTTP 错误为 None。
10
+ code: 服务端业务错误码(对齐 dwyeapi ApiResponse.code);
11
+ 未知/未携带时为 None。
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ message: str = "",
17
+ *,
18
+ status_code: int | None = None,
19
+ code: str | None = None,
20
+ ):
21
+ self.message = message
22
+ self.status_code = status_code
23
+ self.code = code
24
+ super().__init__(message)
25
+
26
+
27
+ class AuthError(QuantError):
28
+ """API Key 无效或已过期."""
29
+
30
+ def __init__(self, message: str = "API Key 无效或已过期", *, code: str | None = None):
31
+ super().__init__(message, status_code=401, code=code)
32
+
33
+
34
+ class ApiKeyRevokedError(AuthError):
35
+ """API Key 已被吊销 → 服务端返回 401 + code=API_KEY_REVOKED.
36
+
37
+ 用户应在 admin 后台重置 / 重新申请 API Key 后再调用 SDK。
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ message: str = "API Key 已被吊销,请联系管理员或重新申请",
43
+ ):
44
+ super().__init__(message, code="API_KEY_REVOKED")
45
+
46
+
47
+ class IpNotAllowedError(QuantError):
48
+ """请求来源 IP 不在白名单 → 服务端返回 403 + code=IP_NOT_ALLOWED.
49
+
50
+ 用户应检查 admin 后台为该 API Key 设置的 IP 白名单是否覆盖当前出口 IP。
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ message: str = "请求来源 IP 不在 API Key 白名单内",
56
+ ):
57
+ super().__init__(message, status_code=403, code="IP_NOT_ALLOWED")
58
+
59
+
60
+ class ApiKeyRateLimitError(QuantError):
61
+ """API Key 调用频率超限 → 服务端返回 422 + code=API_KEY_RATE_LIMITED.
62
+
63
+ 与 QuotaExceededError 区分:前者是按"调用次数"限流(防滥用),
64
+ 后者是按"流量字节"扣费(配额耗尽)。
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ message: str = "API Key 调用频率超限,请稍后再试",
70
+ ):
71
+ super().__init__(message, status_code=422, code="API_KEY_RATE_LIMITED")
72
+
73
+
74
+ class QuotaExceededError(QuantError):
75
+ """配额不足."""
76
+
77
+ def __init__(self, message: str = "配额不足", *, remaining: int = 0, required: int = 0):
78
+ self.remaining = remaining
79
+ self.required = required
80
+ super().__init__(message, status_code=429)
81
+
82
+
83
+ class NetworkError(QuantError):
84
+ """网络连接问题."""
85
+
86
+ def __init__(self, message: str = "网络连接失败", *, cause: Exception | None = None):
87
+ self.cause = cause
88
+ super().__init__(message)
89
+
90
+
91
+ class SignatureError(QuantError):
92
+ """签名校验失败(服务端返回 401)."""
93
+
94
+ def __init__(self, message: str = "请求签名校验失败"):
95
+ super().__init__(message, status_code=401)
96
+
97
+
98
+ class SDKVersionError(QuantError):
99
+ """SDK 版本过低,服务端已停止支持(HTTP 426)。
100
+
101
+ 错误消息包含升级命令,用户按提示执行 `pip install -U quantzone` 即可。
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ message: str = "当前 SDK 版本已停止支持,请执行 `pip install -U quantzone` 升级后重试",
107
+ ):
108
+ super().__init__(message, status_code=426)
quantzone/factor.py ADDED
@@ -0,0 +1,231 @@
1
+ """因子查询。
2
+
3
+ 对外仅提供 :meth:`FactorService.get_factors` — 一次性返回完整 ``DataFrame``。
4
+
5
+ 底层走 ``POST /api/factors/stream`` 拉 Arrow IPC stream:
6
+ - 边收边解码:每个 RecordBatch 立即转 ``DataFrame``。
7
+ - 边收边落盘:HTTP 字节同时写到 ``download_dir/temp/<id>.arrow``。
8
+ - 流自然结束 → 原子重命名到 ``download_dir/factors_<ts>.arrow``。
9
+ - 失败 → temp 文件保留,不删除。
10
+
11
+ 字段名与 DolphinDB 表 ``dfs://factor_day/normal_factors_DB`` 一致:
12
+ ``DataDate / ukey / x / factorname``。SDK 不做列名转换或宽表 pivot。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import os
19
+ import uuid
20
+ from collections.abc import Iterator
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+ from typing import IO, Any
24
+
25
+ import pandas as pd
26
+ import pyarrow.ipc as ipc
27
+
28
+ from ._dates import DateLike, normalize_date
29
+ from ._http import HttpClient
30
+ from ._logger import logger
31
+
32
+ DEFAULT_DOWNLOAD_DIR = Path.home() / ".quantzone" / "downloads"
33
+ PERSIST_FILENAME_PREFIX = "factors_"
34
+ TEMP_SUBDIR = "temp"
35
+ ARROW_EXT = ".arrow"
36
+
37
+
38
+ class _TeeReader(io.RawIOBase):
39
+ """把上游 chunk 流复制一份到磁盘文件,同时对外暴露 file-like ``read`` 接口。
40
+
41
+ 用于 ``pa.ipc.open_stream`` 这种需要 file-like 的解码器:它从这里 ``read``,
42
+ 我们一边给它字节、一边把字节写到 temp 文件,实现 tee。
43
+
44
+ 继承 ``io.RawIOBase`` 是为了一次性补齐 ``closed`` / ``seekable`` / ``writable``
45
+ 等 pyarrow 校验需要的属性。
46
+ """
47
+
48
+ def __init__(self, chunks: Iterator[bytes], sink: IO[bytes]) -> None:
49
+ """初始化。
50
+
51
+ Args:
52
+ chunks: 上游 HTTP chunk 字节迭代器(``httpx.Response.iter_bytes``)。
53
+ sink: 已打开的 temp 文件句柄(二进制写)。
54
+ """
55
+ super().__init__()
56
+ self._chunks = chunks
57
+ self._sink = sink
58
+ self._buffer = bytearray()
59
+
60
+ def _pull_one(self) -> bool:
61
+ """从上游再拉一个 chunk,同时写入 sink。返回是否拉到。"""
62
+ try:
63
+ chunk = next(self._chunks)
64
+ except StopIteration:
65
+ return False
66
+ if not chunk:
67
+ return True
68
+ self._buffer.extend(chunk)
69
+ self._sink.write(chunk)
70
+ return True
71
+
72
+ def read(self, n: int = -1) -> bytes:
73
+ """读取至多 n 字节;n<0 表示读到 EOF。"""
74
+ if n is None or n < 0:
75
+ while self._pull_one():
76
+ pass
77
+ data = bytes(self._buffer)
78
+ self._buffer.clear()
79
+ return data
80
+ while len(self._buffer) < n:
81
+ if not self._pull_one():
82
+ break
83
+ take = bytes(self._buffer[:n])
84
+ del self._buffer[:n]
85
+ return take
86
+
87
+ def readall(self) -> bytes:
88
+ return self.read(-1)
89
+
90
+ def readinto(self, b) -> int:
91
+ data = self.read(len(b))
92
+ n = len(data)
93
+ b[:n] = data
94
+ return n
95
+
96
+ def readable(self) -> bool:
97
+ return True
98
+
99
+ def writable(self) -> bool:
100
+ return False
101
+
102
+ def seekable(self) -> bool:
103
+ return False
104
+
105
+
106
+ def _to_str_list(value: str | list[str] | None) -> list[str] | None:
107
+ """把 ``str`` / ``list[str]`` / None 归一为 ``list[str]`` 或 None。"""
108
+ if value is None:
109
+ return None
110
+ if isinstance(value, str):
111
+ return [value]
112
+ return list(value)
113
+
114
+
115
+ class FactorService:
116
+ """因子数据查询服务。"""
117
+
118
+ def __init__(self, http: HttpClient, download_dir: Path | None = None) -> None:
119
+ """初始化。
120
+
121
+ Args:
122
+ http: HTTP 客户端。
123
+ download_dir: 默认下载目录;None 表示 ``~/.quantzone/downloads``。
124
+ """
125
+ self._http = http
126
+ self._download_dir = download_dir or DEFAULT_DOWNLOAD_DIR
127
+
128
+ def get_factors(
129
+ self,
130
+ order_book_ids: str | list[str] | None = None,
131
+ factor: str | list[str] | None = None,
132
+ start_date: DateLike | None = None,
133
+ end_date: DateLike | None = None,
134
+ filename: str | None = None,
135
+ ) -> pd.DataFrame:
136
+ """一次性返回完整因子数据(窄表)。
137
+
138
+ 服务端按月分批查 DolphinDB,每个 RecordBatch 立即推过来;SDK 边收边解码、
139
+ 边落盘到 ``download_dir/temp/<id>.arrow``。流自然结束后原子重命名到
140
+ ``download_dir/<filename or factors_<ts>.arrow>``;失败 → temp 文件保留,
141
+ 不删除。
142
+
143
+ Args:
144
+ order_book_ids: 股票代码(``str`` 或 ``list[str]``),None 表示全市场。
145
+ 示例:``"000001.SZ"`` 或 ``["000001.SZ", "000002.SZ"]``。
146
+ factor: 因子名(``str`` 或 ``list[str]``),None 表示全部因子。
147
+ 示例:``"alpha1"`` 或 ``["alpha1", "vwap"]``。
148
+ start_date: 起始日期,支持 ``int(YYYYMMDD)`` / ``str`` / ``date`` /
149
+ ``datetime`` / ``Timestamp``。None 表示不限。
150
+ end_date: 截止日期,格式同上。
151
+ filename: 自定义最终文件名(落到 ``download_dir`` 下);None 时按时间戳生成。
152
+
153
+ Returns:
154
+ 窄表 ``DataFrame``,列:``DataDate / ukey / x / factorname``。
155
+ 空结果时返回带正确列名的空 DataFrame。
156
+ """
157
+ ids_list = _to_str_list(order_book_ids)
158
+ factor_list = _to_str_list(factor)
159
+
160
+ body: dict[str, Any] = {}
161
+ if factor_list:
162
+ body["factor"] = factor_list
163
+ if ids_list:
164
+ body["order_book_ids"] = ids_list
165
+ if start_date is not None:
166
+ body["start_date"] = normalize_date(start_date)
167
+ if end_date is not None:
168
+ body["end_date"] = normalize_date(end_date)
169
+
170
+ download_dir = self._download_dir
171
+ temp_dir = download_dir / TEMP_SUBDIR
172
+ temp_dir.mkdir(parents=True, exist_ok=True)
173
+ download_dir.mkdir(parents=True, exist_ok=True)
174
+
175
+ temp_path = temp_dir / f"{uuid.uuid4().hex}{ARROW_EXT}"
176
+ final_name = filename or (
177
+ f"{PERSIST_FILENAME_PREFIX}{datetime.now():%Y%m%d_%H%M%S}_{uuid.uuid4().hex[:8]}{ARROW_EXT}"
178
+ )
179
+ final_path = download_dir / final_name
180
+
181
+ logger.info("下载文件(temp): %s", temp_path)
182
+
183
+ chunks_dfs = list(self._iter_batches(body, temp_path, final_path))
184
+ if not chunks_dfs:
185
+ return pd.DataFrame(columns=["DataDate", "ukey", "x", "factorname"])
186
+ return pd.concat(chunks_dfs, ignore_index=True)
187
+
188
+ def _iter_batches(
189
+ self,
190
+ body: dict[str, Any],
191
+ temp_path: Path,
192
+ final_path: Path,
193
+ ) -> Iterator[pd.DataFrame]:
194
+ """流式解码生成器:每个 RecordBatch 立即 yield ``DataFrame``,边收边写 temp。"""
195
+ success = False
196
+ sink = temp_path.open("wb")
197
+ try:
198
+ with self._http.stream_post_iter("/api/factors/stream", body) as chunks:
199
+ tee = _TeeReader(chunks, sink)
200
+ reader = ipc.open_stream(tee)
201
+ while True:
202
+ try:
203
+ batch = reader.read_next_batch()
204
+ except StopIteration:
205
+ break
206
+ if batch is None:
207
+ break
208
+ yield batch.to_pandas()
209
+ success = True
210
+ finally:
211
+ sink.flush()
212
+ sink.close()
213
+ if success:
214
+ os.replace(temp_path, final_path)
215
+ size = final_path.stat().st_size
216
+ logger.info("已保存到: %s (%d bytes)", final_path, size)
217
+ else:
218
+ logger.info("流未正常结束,temp 文件保留: %s", temp_path)
219
+
220
+ def list_factors(self) -> list[dict]:
221
+ """获取可用因子元信息列表。
222
+
223
+ 字段与 DolphinDB 表 ``dfs://factor_meta/factor_meta_info`` 原始列对齐。
224
+
225
+ Returns:
226
+ 因子列表,每个元素含 ``factorname`` / ``startDate`` / ``endDate`` 三个字段。
227
+ """
228
+ data = self._http.get("/api/factors/")
229
+ if isinstance(data, list):
230
+ return data
231
+ return data.get("items", data.get("factors", []))
quantzone/py.typed ADDED
File without changes
quantzone/quota.py ADDED
@@ -0,0 +1,66 @@
1
+ """配额查询服务。
2
+
3
+ 对外暴露简洁字段:``bytes_limit / bytes_used / remaining_days / license_type``。
4
+
5
+ 后端 ``/api/quota/`` 返回的是三层账本字段(``daily_quota_bytes`` / ``daily_used_bytes``
6
+ / ``extra_quota_bytes`` 等),SDK 在此处映射为对外简洁字段。
7
+ """
8
+
9
+ from typing import TypedDict
10
+
11
+ from ._http import HttpClient
12
+
13
+
14
+ class Quota(TypedDict, total=False):
15
+ """配额信息字典。
16
+
17
+ Attributes:
18
+ bytes_limit: 总流量上限(字节)。
19
+ bytes_used: 已用流量(字节)。
20
+ remaining_days: 许可剩余天数。
21
+ license_type: 许可证类型(如 ``"trial"`` / ``"commercial"``)。
22
+ """
23
+
24
+ bytes_limit: int
25
+ bytes_used: int
26
+ remaining_days: int
27
+ license_type: str
28
+
29
+
30
+ class QuotaService:
31
+ """用户配额查询服务。"""
32
+
33
+ def __init__(self, http: HttpClient) -> None:
34
+ """初始化。
35
+
36
+ Args:
37
+ http: HTTP 客户端。
38
+ """
39
+ self._http = http
40
+
41
+ def get_quota(self) -> Quota:
42
+ """获取当前用户配额。
43
+
44
+ 把后端三层字段映射为对外简洁字段:
45
+ - ``bytes_limit = daily_quota_bytes + extra_quota_bytes``
46
+ - ``bytes_used = daily_used_bytes``
47
+ - ``remaining_days`` / ``license_type`` 透传
48
+
49
+ Returns:
50
+ 配额字典,含 ``bytes_limit`` / ``bytes_used`` / ``remaining_days`` /
51
+ ``license_type``。服务端缺失的源字段在结果中也会缺失。
52
+ """
53
+ data = self._http.get("/api/quota/")
54
+ if not isinstance(data, dict):
55
+ return {}
56
+
57
+ result: Quota = {}
58
+ if "daily_quota_bytes" in data and "extra_quota_bytes" in data:
59
+ result["bytes_limit"] = int(data["daily_quota_bytes"]) + int(data["extra_quota_bytes"])
60
+ if "daily_used_bytes" in data:
61
+ result["bytes_used"] = int(data["daily_used_bytes"])
62
+ if "remaining_days" in data:
63
+ result["remaining_days"] = int(data["remaining_days"])
64
+ if "license_type" in data:
65
+ result["license_type"] = str(data["license_type"])
66
+ return result
quantzone/stock.py ADDED
@@ -0,0 +1,53 @@
1
+ """股票/合约元信息查询服务。
2
+
3
+ 接口约定:
4
+
5
+ - ``GET /api/stocks/`` → 全量合约列表(服务端缓存,12h TTL)
6
+
7
+ 返回 ``DataFrame``,列固定 3 列:``order_book_id / industry_name / symbol``。
8
+ """
9
+
10
+ import pandas as pd
11
+
12
+ from ._http import HttpClient
13
+
14
+ INSTRUMENT_COLUMNS = ["order_book_id", "industry_name", "symbol"]
15
+
16
+
17
+ def _to_dataframe(items: list[dict]) -> pd.DataFrame:
18
+ """把 ``list[dict]`` 转为合约 ``DataFrame``,缺失列补 ``NA``,列顺序固定。"""
19
+ if not items:
20
+ return pd.DataFrame(columns=INSTRUMENT_COLUMNS)
21
+ df = pd.DataFrame(items)
22
+ for col in INSTRUMENT_COLUMNS:
23
+ if col not in df.columns:
24
+ df[col] = pd.NA
25
+ return df[INSTRUMENT_COLUMNS]
26
+
27
+
28
+ class StockService:
29
+ """股票/合约信息查询服务。"""
30
+
31
+ def __init__(self, http: HttpClient) -> None:
32
+ """初始化。
33
+
34
+ Args:
35
+ http: HTTP 客户端。
36
+ """
37
+ self._http = http
38
+
39
+ def list_stocks(self) -> pd.DataFrame:
40
+ """获取全量合约列表。
41
+
42
+ 服务端不再支持按交易所过滤,客户端拿全量后自行筛选,例如::
43
+
44
+ df = client.list_stocks()
45
+ sz = df[df["order_book_id"].str.endswith(".SZ")]
46
+
47
+ Returns:
48
+ 合约 ``DataFrame``,列固定 3 列:
49
+ ``order_book_id / industry_name / symbol``。
50
+ """
51
+ data = self._http.get("/api/stocks/")
52
+ items = data if isinstance(data, list) else data.get("items", data.get("stocks", []))
53
+ return _to_dataframe(items)
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantzone
3
+ Version: 0.4.0
4
+ Summary: 宽舟科技量化数据平台 Python SDK
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx<2.0,>=0.27
9
+ Requires-Dist: pandas>=2.0
10
+ Requires-Dist: pyarrow>=14.0
11
+ Requires-Dist: keyring>=25.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0; extra == "dev"
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
15
+ Requires-Dist: respx>=0.21; extra == "dev"
16
+ Requires-Dist: ruff>=0.4; extra == "dev"
17
+ Requires-Dist: build>=1.2; extra == "dev"
18
+ Requires-Dist: mypy>=1.10; extra == "dev"
19
+
20
+ # quantzone
21
+
22
+ 宽舟科技量化数据平台 Python SDK。
23
+
24
+ ## 安装
25
+
26
+ ```bash
27
+ pip install quantzone
28
+ ```
29
+
30
+ ## Supported Platforms
31
+
32
+ | OS | Architecture | Python |
33
+ |----|--------------|--------|
34
+ | Linux (glibc, e.g. Ubuntu/Debian/CentOS) | x86_64, aarch64 | 3.11, 3.12, 3.13, 3.14 |
35
+ | Linux (musl, e.g. Alpine) | x86_64, aarch64 | 3.11, 3.12, 3.13, 3.14 |
36
+ | macOS | Intel (10.9+), Apple Silicon (11.0+) | 3.11, 3.12, 3.13, 3.14 |
37
+ | Windows | AMD64 (Windows 10/11) | 3.11, 3.12, 3.13, 3.14 |
38
+
39
+ Other platforms (32-bit Linux/Windows, FreeBSD, ppc64le, s390x, RISC-V, Python <= 3.10) are not supported.
40
+ If `pip install quantzone` reports `No matching distribution found`, your platform/Python version is not in the supported matrix.
41
+
42
+ ## 快速开始
43
+
44
+ ```python
45
+ # 方式 1:实例化客户端
46
+ from quantzone import QuantZone
47
+
48
+ # base_url 必须显式传入(SDK 不内置默认服务端地址)
49
+ client = QuantZone(
50
+ access_key="AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
51
+ sign_secret="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
52
+ base_url="https://your-api.example.com",
53
+ )
54
+
55
+ # 方式 2:模块级单例(短别名 qz)
56
+ import qz
57
+
58
+ qz.init(
59
+ access_key="AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
60
+ sign_secret="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
61
+ base_url="https://your-api.example.com",
62
+ )
63
+ df = qz.get_factors(factor="alpha1", start_date="2024-01-01", end_date="2024-12-31")
64
+ ```
65
+
66
+ ## API
67
+
68
+ ### 因子查询
69
+
70
+ ```python
71
+ # 查询因子数据 → 窄表 DataFrame,列:DataDate / ukey / x / factorname
72
+ # 4 个参数都可省略,服务端按缺省维度展开:
73
+ # factor 空 → 全部因子
74
+ # order_book_ids 空 → 全市场
75
+ df = client.get_factors(
76
+ factor=["alpha1", "alpha2", "vwap"],
77
+ order_book_ids=["000001.SZ", "000002.SZ"],
78
+ start_date="2024-01-01",
79
+ end_date="2024-12-31",
80
+ )
81
+
82
+ # 因子元信息(含 name / description / category)
83
+ factors = client.list_factors()
84
+ ```
85
+
86
+ ### 合约元信息
87
+
88
+ ```python
89
+ df = client.list_stocks(exchange="SZ") # 全量合约 → DataFrame
90
+ df = client.instruments(["000001.SZ", "600000.SH"]) # 指定合约 → DataFrame
91
+ ```
92
+
93
+ ### 交易日历
94
+
95
+ ```python
96
+ client.get_trading_dates("2024-01-01", "2024-01-31") # list[date]
97
+ client.get_previous_trading_date("2024-01-15", n=1) # date
98
+ client.get_next_trading_date("2024-01-15", n=1) # date
99
+ client.is_trading_date("2024-01-01") # bool
100
+ ```
101
+
102
+ ### 配额查询
103
+
104
+ ```python
105
+ quota = client.get_quota()
106
+ # {bytes_limit, bytes_used, remaining_days, license_type}
107
+ ```
108
+
109
+ ### Context Manager
110
+
111
+ ```python
112
+ with QuantZone(access_key="AK...", sign_secret="...", base_url="https://your-api.example.com") as client:
113
+ df = client.get_factors(factor=["alpha1"])
114
+ ```
115
+
116
+ ### 大查询超时
117
+
118
+ ```python
119
+ # 全市场全因子单日 ~12M 数据点,默认 60s 超时会被打断 → 显式调到 600s+
120
+ client = QuantZone(
121
+ access_key="AK...", sign_secret="...",
122
+ base_url="https://your-api.example.com",
123
+ timeout=600,
124
+ )
125
+ ```
126
+
127
+ ### 数据本地存储
128
+
129
+ **所有下载的 Arrow 文件都会持久化保留**,SDK 不主动删除。
130
+
131
+ ```python
132
+ # 默认:落到 ~/.quantzone/downloads/factors_<timestamp>_<short>.arrow
133
+ client = QuantZone(
134
+ access_key="AK...", sign_secret="...",
135
+ base_url="https://your-api.example.com",
136
+ )
137
+ df = client.get_factors(factor=["alpha1"])
138
+
139
+ # 全局指定下载目录
140
+ client = QuantZone(
141
+ access_key="AK...", sign_secret="...",
142
+ base_url="https://your-api.example.com",
143
+ download_dir="~/data/quantzone",
144
+ )
145
+
146
+ # 单次指定文件名(落到 download_dir/<filename>)
147
+ df = client.get_factors(factor=["alpha1"], filename="alpha1_2024.arrow")
148
+ ```
149
+
150
+ 每次下载都会通过 logger 打印落盘位置,默认输出到 stderr:
151
+ ```
152
+ [quantzone] 下载文件(temp): /Users/me/.quantzone/downloads/temp/abc123.arrow
153
+ [quantzone] 已保存到: /Users/me/.quantzone/downloads/factors_20240115_120000_abc123.arrow (1024 bytes)
154
+ ```
155
+
156
+ 历史文件由用户自行管理(SDK 不做任何 GC)。
157
+
158
+ ## 特性
159
+
160
+ - **请求签名**:每个请求自动带 HMAC-SHA256 签名
161
+ - **流式下载**:Arrow IPC 边收边写临时文件,完毕后原子重命名为最终文件并加载为 DataFrame
162
+ - **凭证安全存储**:通过 keyring 自动读取系统密钥环(`QuantZone()` 不传 AK/SK 时生效)
163
+ - **不自动重试**:量化接口非幂等(消耗配额、跑昂贵 DDB 查询),SDK 不内置重试。需要重试请用 `tenacity` / `backoff` 装饰 `client.get_factors`,自定义退避策略
164
+
165
+ ## 异常处理
166
+
167
+ ```python
168
+ from quantzone import QuantZone, AuthError, QuotaExceededError, NetworkError
169
+
170
+ try:
171
+ df = client.get_factors(factor=["alpha1"])
172
+ except AuthError:
173
+ print("API Key 无效")
174
+ except QuotaExceededError as e:
175
+ print(f"配额不足:剩余 {e.remaining},需要 {e.required}")
176
+ except NetworkError:
177
+ print("网络连接失败")
178
+ ```
179
+
180
+ ## 开发
181
+
182
+ ```bash
183
+ cd quant-sdk
184
+ uv sync --dev
185
+ uv run pytest tests/ -v
186
+ uv run ruff check src/ --fix && uv run ruff format src/
187
+ ```
@@ -0,0 +1,23 @@
1
+ quantzone/__init__.py,sha256=MKQ3ppq4KX8t8ZKyqgFc8-AYkvZhmeWgjgIzhELk1sM,3236
2
+ quantzone/_auth.cp313-win_amd64.pyd,sha256=WzreMWWBQpAy51luLLbb2qCr0WXyUzrj6T--zDxq5OA,46592
3
+ quantzone/_auth.pyi,sha256=oysbXAov0OROpOFWTLrK3t_cjlsE5lz6AgSdeGMjruE,341
4
+ quantzone/_dates.py,sha256=cK8gQOxexVcRHSnOPXlN40gFvU68Rfb-9Gz0RoCHksg,1722
5
+ quantzone/_http.cp313-win_amd64.pyd,sha256=s6TO8x154xa44D8nz0eUqjbTVFz9xHxAzQeInj6Crz8,130048
6
+ quantzone/_http.pyi,sha256=PKWgtY3JgdWunoxmWnjeDuvqHQuGMpxe7-lvKmrsdvI,1415
7
+ quantzone/_logger.py,sha256=KIyFcJpfZ4x4-HbIDQguNVnR_pqEyVPDN7gKfwWsUAY,542
8
+ quantzone/_signer.cp313-win_amd64.pyd,sha256=lyv5EDRpZkZL6Ks8IehliaZfUosrnuVaAbJyRZZXSSM,48640
9
+ quantzone/_signer.pyi,sha256=CZyftqqAx8B4EBNMQ4kFdtBZWqrOLH5cmMY0ljwFb9w,151
10
+ quantzone/_singleton.py,sha256=tMgjS9LoMzU_8qqRwzFCPQeCp9LMP6msB7VURPsCQe4,1970
11
+ quantzone/calendar.py,sha256=HPdqWF2-VL2NL9W8qJCr8YezHzdGC-1AA970fyAousk,3238
12
+ quantzone/client.py,sha256=5CS45tyd2oR2BKZU2QcFy20kw9dATO38AdQonHenazc,5399
13
+ quantzone/exceptions.py,sha256=KVlk0sQDdqnPb01aKXtB-Iw_GKgBwD1FfOgvumTP_SM,3329
14
+ quantzone/factor.py,sha256=KbTg0MhhBX-O5I5q96-Ns0LOjNigalX974l8BxAw3kw,8360
15
+ quantzone/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ quantzone/quota.py,sha256=zfJ_P2ARypHckGrg_tiUO3vyLAAevJ1fb537Otnmcyw,2185
17
+ quantzone/stock.py,sha256=OA7wZx1KJnW1UGZuTCrG9gdcWlqlomtdh5YDmym-Amk,1593
18
+ qz/__init__.py,sha256=rECZe7WlcRO7cpmutEBArlfMXbNSVl60Te97keD4JLA,334
19
+ qz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ quantzone-0.4.0.dist-info/METADATA,sha256=xlmmTXe2ze4qgo7mka3SaaWifc43jl_BDa2plUjpFcw,5718
21
+ quantzone-0.4.0.dist-info/WHEEL,sha256=x5Wpw_tLx5PQKiWdxpqvs0e7Sg-SO0mTWdEADYDGPGA,101
22
+ quantzone-0.4.0.dist-info/top_level.txt,sha256=Nc-VKur3jcc8XKj1BL-PJbYSx_x50zrYhVEGe0iMX5g,13
23
+ quantzone-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ quantzone
2
+ qz
qz/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """``qz`` — ``quantzone`` 的短别名。
2
+
3
+ 允许用户 ``import qz`` 然后直接 ``qz.init(...)`` / ``qz.get_factors(...)`` /
4
+ ``qz.QuantZone(...)``。与 ``import quantzone as qz`` 等价,无需用户手动 alias。
5
+ """
6
+
7
+ from quantzone import * # noqa: F401, F403
8
+ from quantzone import __all__, __version__ # noqa: F401
qz/py.typed ADDED
File without changes