quantzone 0.5.0__cp39-cp39-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,127 @@
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. 模块级单例(推荐用 ``as qz`` 简写)::
14
+
15
+ import quantzone as 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 __future__ import annotations
23
+
24
+ from datetime import date
25
+ from importlib.metadata import PackageNotFoundError
26
+ from importlib.metadata import version as _pkg_version
27
+
28
+ import pandas as pd
29
+
30
+ from ._dates import DateLike
31
+ from ._singleton import get_instance, init
32
+ from .client import QuantZone
33
+ from .exceptions import (
34
+ ApiKeyRateLimitError,
35
+ ApiKeyRevokedError,
36
+ AuthError,
37
+ IpNotAllowedError,
38
+ NetworkError,
39
+ QuantError,
40
+ QuotaExceededError,
41
+ SDKVersionError,
42
+ SignatureError,
43
+ )
44
+ from .quota import Quota
45
+
46
+ try:
47
+ __version__ = _pkg_version("quantzone")
48
+ except PackageNotFoundError:
49
+ # 开发模式未安装时回退
50
+ __version__ = "0.0.0+dev"
51
+
52
+
53
+ def get_factors(
54
+ order_book_ids: str | list[str] | None = None,
55
+ factor: str | list[str] | None = None,
56
+ start_date: DateLike | None = None,
57
+ end_date: DateLike | None = None,
58
+ filename: str | None = None,
59
+ ) -> pd.DataFrame:
60
+ """模块级 ``get_factors``(需先调用 ``quantzone.init``)。"""
61
+ return get_instance().get_factors(
62
+ order_book_ids=order_book_ids,
63
+ factor=factor,
64
+ start_date=start_date,
65
+ end_date=end_date,
66
+ filename=filename,
67
+ )
68
+
69
+
70
+ def list_factors() -> list[dict]:
71
+ """模块级 ``list_factors``。"""
72
+ return get_instance().list_factors()
73
+
74
+
75
+ def list_stocks() -> pd.DataFrame:
76
+ """模块级 ``list_stocks``。"""
77
+ return get_instance().list_stocks()
78
+
79
+
80
+ def get_trading_dates(start_date: DateLike, end_date: DateLike) -> list[date]:
81
+ """模块级 ``get_trading_dates``。"""
82
+ return get_instance().get_trading_dates(start_date, end_date)
83
+
84
+
85
+ def get_previous_trading_date(current_date: DateLike, n: int = 1) -> date:
86
+ """模块级 ``get_previous_trading_date``。"""
87
+ return get_instance().get_previous_trading_date(current_date, n)
88
+
89
+
90
+ def get_next_trading_date(current_date: DateLike, n: int = 1) -> date:
91
+ """模块级 ``get_next_trading_date``。"""
92
+ return get_instance().get_next_trading_date(current_date, n)
93
+
94
+
95
+ def is_trading_date(current_date: DateLike) -> bool:
96
+ """模块级 ``is_trading_date``。"""
97
+ return get_instance().is_trading_date(current_date)
98
+
99
+
100
+ def get_quota() -> Quota:
101
+ """模块级 ``get_quota``。"""
102
+ return get_instance().get_quota()
103
+
104
+
105
+ __all__ = [
106
+ "ApiKeyRateLimitError",
107
+ "ApiKeyRevokedError",
108
+ "AuthError",
109
+ "IpNotAllowedError",
110
+ "NetworkError",
111
+ "QuantError",
112
+ "QuantZone",
113
+ "Quota",
114
+ "QuotaExceededError",
115
+ "SDKVersionError",
116
+ "SignatureError",
117
+ "__version__",
118
+ "get_factors",
119
+ "get_next_trading_date",
120
+ "get_previous_trading_date",
121
+ "get_quota",
122
+ "get_trading_dates",
123
+ "init",
124
+ "is_trading_date",
125
+ "list_factors",
126
+ "list_stocks",
127
+ ]
Binary file
quantzone/_auth.pyi ADDED
@@ -0,0 +1,3 @@
1
+ from .exceptions import AuthError as AuthError
2
+
3
+ def get_credentials(access_key: str | None, sign_secret: str | None) -> tuple[str, str]: ...
quantzone/_dates.py ADDED
@@ -0,0 +1,66 @@
1
+ """日期参数归一化工具。
2
+
3
+ 接受 int(YYYYMMDD) / str / date / datetime / Timestamp 多种格式,
4
+ 本模块统一归一为 ISO ``YYYY-MM-DD`` 字符串发往服务端。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from datetime import date, datetime
11
+ from typing import Union
12
+
13
+ import pandas as pd
14
+
15
+ DateLike = Union[int, str, date, datetime, pd.Timestamp]
16
+
17
+ _ISO_LEN = 10
18
+ _INT_DATE_LEN = 8
19
+ _INT_DATE_MIN = 19000101
20
+ _INT_DATE_MAX = 99991231
21
+
22
+ # 严格白名单:只接受三种常见日期字符串格式,拒绝 "now" / "today" / "NaT" 等魔法字符串
23
+ # 以及带时区后缀、多余空格的字符串
24
+ _DATE_STR_PATTERNS: tuple[re.Pattern[str], ...] = (
25
+ re.compile(r"^\d{4}-\d{2}-\d{2}$"), # 2024-01-31
26
+ re.compile(r"^\d{8}$"), # 20240131
27
+ re.compile(r"^\d{4}/\d{2}/\d{2}$"), # 2024/01/31
28
+ )
29
+
30
+
31
+ def normalize_date(value: DateLike | None) -> str | None:
32
+ """把多种日期表示归一为 ISO ``YYYY-MM-DD`` 字符串。
33
+
34
+ Args:
35
+ value: 日期,支持 ``int(YYYYMMDD)`` / ``str`` / ``date`` / ``datetime`` /
36
+ ``pd.Timestamp``;为 None 时直接返回 None。字符串只接受
37
+ ``YYYY-MM-DD`` / ``YYYYMMDD`` / ``YYYY/MM/DD`` 三种格式,其他(含
38
+ ``"now"`` / ``"today"`` / ``"NaT"`` / 带时区后缀等)一律拒绝。
39
+
40
+ Returns:
41
+ ISO 格式日期字符串,或 None。
42
+
43
+ Raises:
44
+ ValueError: 类型不支持或字符串格式不在白名单内。
45
+ """
46
+ if value is None:
47
+ return None
48
+ if isinstance(value, bool):
49
+ raise ValueError(f"日期不接受 bool 类型: {value!r}")
50
+ if isinstance(value, int):
51
+ if not (_INT_DATE_MIN <= value <= _INT_DATE_MAX) or len(str(value)) != _INT_DATE_LEN:
52
+ raise ValueError(f"int 日期必须为 8 位 YYYYMMDD: {value!r}")
53
+ return f"{value // 10000:04d}-{(value // 100) % 100:02d}-{value % 100:02d}"
54
+ if isinstance(value, datetime):
55
+ return value.date().isoformat()
56
+ if isinstance(value, date):
57
+ return value.isoformat()
58
+ if isinstance(value, pd.Timestamp):
59
+ return value.date().isoformat()
60
+ if isinstance(value, str):
61
+ if not any(p.fullmatch(value) for p in _DATE_STR_PATTERNS):
62
+ raise ValueError(
63
+ f"日期字符串格式不合法,必须是 YYYY-MM-DD / YYYYMMDD / YYYY/MM/DD: {value!r}"
64
+ )
65
+ return pd.Timestamp(value).date().isoformat()
66
+ raise ValueError(f"不支持的日期类型: {type(value).__name__}={value!r}")
Binary file
quantzone/_http.pyi ADDED
@@ -0,0 +1,27 @@
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
+ MAX_TIMEOUT: float
10
+ SDK_VERSION_STATUS: int
11
+ STREAM_CHUNK_SIZE: int
12
+ HEADER_API_KEY: str
13
+ HEADER_SDK_VERSION: str
14
+ HEADER_ESTIMATED_BYTES: str
15
+ HEADER_ROW_COUNT: str
16
+ API_VERSION: str
17
+ API_PREFIX: str
18
+
19
+ class HttpClient:
20
+ def __init__(self, access_key: str, sign_secret: str, base_url: str, timeout: float = ...) -> None: ...
21
+ def close(self) -> None: ...
22
+ def request(self, method: str, path: str, *, params: dict[str, Any] | None = None, json_body: Any | None = None) -> Any: ...
23
+ def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any: ...
24
+ def post(self, path: str, *, json_body: Any | None = None, params: dict[str, Any] | None = None) -> Any: ...
25
+ def stream_post(self, path: str, json_body: dict[str, Any], out_file: Path) -> dict[str, int]: ...
26
+ @contextmanager
27
+ 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,90 @@
1
+ """模块级 ``quantzone.init(...)`` 维护的单例 ``QuantZone``。
2
+
3
+ 顶层 init 风格:用户在程序启动时调用一次 ``init``,
4
+ 之后通过模块顶层函数(如 ``quantzone.get_factors``)直接使用。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from .client import QuantZone
15
+
16
+ _INSTANCE: QuantZone | None = None
17
+ _LOCK = threading.Lock()
18
+
19
+
20
+ def init(
21
+ access_key: str | None = None,
22
+ sign_secret: str | None = None,
23
+ *,
24
+ base_url: str,
25
+ timeout: float = 60.0,
26
+ download_dir: Path | str | None = None,
27
+ ) -> QuantZone:
28
+ """初始化模块级单例 ``QuantZone``。
29
+
30
+ Args:
31
+ access_key (str | None): 公开标识符,必须显式传入,不再支持 keyring。
32
+ 示例:``"AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"``。
33
+ sign_secret (str | None): 签名密钥,必须显式传入,不再支持 keyring。
34
+ 示例:``"sk_xxxx..."``。
35
+ base_url (str): 服务端地址,必须显式传入。
36
+ 示例:``"https://your-api.example.com"``。
37
+ timeout (float): 请求超时(秒),默认 ``60.0``。示例:``30.0``。
38
+ download_dir (Path | str | None): 下载目录;``None`` 时使用 ``~/.quantzone/downloads``。
39
+ 示例:``"/tmp/quantzone-downloads"``。
40
+
41
+ Returns:
42
+ QuantZone: 已初始化的实例。重复调用会先关闭旧实例再创建新的;
43
+ 多线程并发调用时,只有最后一个进入临界区的线程构造的实例会被保留,
44
+ 中间被覆盖的实例都会被 close,避免连接句柄泄漏。
45
+
46
+ Raises:
47
+ AuthError: ``access_key`` 或 ``sign_secret`` 缺失或为空字符串。
48
+ """
49
+ from .client import QuantZone
50
+
51
+ global _INSTANCE
52
+ new_instance = QuantZone(
53
+ access_key=access_key,
54
+ sign_secret=sign_secret,
55
+ base_url=base_url,
56
+ timeout=timeout,
57
+ download_dir=download_dir,
58
+ )
59
+ with _LOCK:
60
+ old_instance = _INSTANCE
61
+ _INSTANCE = new_instance
62
+ if old_instance is not None:
63
+ old_instance.close()
64
+ return new_instance
65
+
66
+
67
+ def get_instance() -> QuantZone:
68
+ """获取已初始化的单例。
69
+
70
+ Returns:
71
+ 当前单例。
72
+
73
+ Raises:
74
+ RuntimeError: 调用 ``init()`` 之前调用本函数。
75
+ """
76
+ with _LOCK:
77
+ instance = _INSTANCE
78
+ if instance is None:
79
+ raise RuntimeError("尚未调用 quantzone.init(),请先初始化")
80
+ return instance
81
+
82
+
83
+ def reset() -> None:
84
+ """关闭并清除单例(仅用于测试隔离)。"""
85
+ global _INSTANCE
86
+ with _LOCK:
87
+ old_instance = _INSTANCE
88
+ _INSTANCE = None
89
+ if old_instance is not None:
90
+ old_instance.close()
quantzone/calendar.py ADDED
@@ -0,0 +1,88 @@
1
+ """交易日历查询服务(仅 A 股)。"""
2
+
3
+ from datetime import date
4
+
5
+ from ._dates import DateLike, normalize_date
6
+ from ._http import API_PREFIX, HttpClient
7
+
8
+
9
+ def _to_date(s: str) -> date:
10
+ """ISO 字符串转 ``date``。"""
11
+ return date.fromisoformat(s)
12
+
13
+
14
+ class CalendarService:
15
+ """交易日历查询服务。"""
16
+
17
+ def __init__(self, http: HttpClient) -> None:
18
+ """初始化。
19
+
20
+ Args:
21
+ http: HTTP 客户端。
22
+ """
23
+ self._http = http
24
+
25
+ def get_trading_dates(self, start_date: DateLike, end_date: DateLike) -> list[date]:
26
+ """获取区间内所有交易日(含起止两端)。
27
+
28
+ Args:
29
+ start_date: 起始日期。
30
+ end_date: 截止日期。
31
+
32
+ Returns:
33
+ 升序排列的交易日列表。
34
+ """
35
+ params = {
36
+ "start_date": normalize_date(start_date),
37
+ "end_date": normalize_date(end_date),
38
+ }
39
+ data = self._http.get(f"{API_PREFIX}/calendar/trading-dates", params=params)
40
+ items = data if isinstance(data, list) else data.get("items", [])
41
+ return [_to_date(s) for s in items]
42
+
43
+ def get_previous_trading_date(self, current_date: DateLike, n: int = 1) -> date:
44
+ """返回 ``current_date`` 之前第 n 个交易日。
45
+
46
+ Args:
47
+ current_date: 参考日期(不计入返回结果)。
48
+ n: 向前偏移的交易日数,默认 1。
49
+
50
+ Returns:
51
+ 交易日。
52
+ """
53
+ params = {"date": normalize_date(current_date), "n": str(n)}
54
+ data = self._http.get(f"{API_PREFIX}/calendar/previous", params=params)
55
+ if isinstance(data, dict):
56
+ return _to_date(data["date"])
57
+ return _to_date(data)
58
+
59
+ def get_next_trading_date(self, current_date: DateLike, n: int = 1) -> date:
60
+ """返回 ``current_date`` 之后第 n 个交易日。
61
+
62
+ Args:
63
+ current_date: 参考日期(不计入返回结果)。
64
+ n: 向后偏移的交易日数,默认 1。
65
+
66
+ Returns:
67
+ 交易日。
68
+ """
69
+ params = {"date": normalize_date(current_date), "n": str(n)}
70
+ data = self._http.get(f"{API_PREFIX}/calendar/next", params=params)
71
+ if isinstance(data, dict):
72
+ return _to_date(data["date"])
73
+ return _to_date(data)
74
+
75
+ def is_trading_date(self, current_date: DateLike) -> bool:
76
+ """判断给定日期是否为交易日。
77
+
78
+ Args:
79
+ current_date: 待判断日期。
80
+
81
+ Returns:
82
+ 是否为交易日。
83
+ """
84
+ params = {"date": normalize_date(current_date)}
85
+ data = self._http.get(f"{API_PREFIX}/calendar/is-trading-date", params=params)
86
+ if isinstance(data, dict):
87
+ return bool(data.get("is_trading", False))
88
+ return bool(data)
quantzone/client.py ADDED
@@ -0,0 +1,158 @@
1
+ """QuantZone 主类 — 组装所有模块,提供统一 API。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+
10
+ from ._auth import get_credentials
11
+ from ._dates import DateLike
12
+ from ._http import HttpClient
13
+ from .calendar import CalendarService
14
+ from .factor import FactorService
15
+ from .quota import Quota, QuotaService
16
+ from .stock import StockService
17
+
18
+
19
+ class QuantZone:
20
+ """宽舟科技量化数据平台客户端(AK/SK 双密钥)。
21
+
22
+ 使用示例::
23
+
24
+ from quantzone import QuantZone
25
+
26
+ client = QuantZone(
27
+ access_key="AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
28
+ sign_secret="...",
29
+ base_url="https://your-api.example.com",
30
+ )
31
+
32
+ df = client.get_factors(
33
+ order_book_ids=["000001.SZ", "000002.SZ"],
34
+ factor=["alpha1", "vwap"],
35
+ start_date="2024-01-01",
36
+ end_date="2024-12-31",
37
+ )
38
+
39
+ client.close()
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ access_key: str | None = None,
45
+ sign_secret: str | None = None,
46
+ *,
47
+ base_url: str,
48
+ timeout: float = 60.0,
49
+ download_dir: Path | str | None = None,
50
+ ) -> None:
51
+ """初始化客户端。
52
+
53
+ Args:
54
+ access_key (str | None): 公开标识符,必须显式传入,不再支持 keyring。
55
+ 长度由服务端约束,示例:``"AKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"``。
56
+ sign_secret (str | None): 签名密钥,必须显式传入,不再支持 keyring。
57
+ 示例:``"sk_xxxx..."``。
58
+ base_url (str): 服务端地址,必须显式提供(SDK 不内置默认地址)。
59
+ 示例:``"https://your-api.example.com"``。
60
+ timeout (float): 请求超时(秒),默认 ``60.0``,上限 ``60.0``;超过会被截断。
61
+ 取值范围 ``(0, 60]``。示例:``30.0``。
62
+ download_dir (Path | str | None): 默认下载目录;``None`` 表示
63
+ ``~/.quantzone/downloads``。示例:``"/tmp/quantzone-downloads"``。
64
+
65
+ Raises:
66
+ AuthError: ``access_key`` 或 ``sign_secret`` 缺失或为空字符串。
67
+ """
68
+ resolved_ak, resolved_sk = get_credentials(access_key, sign_secret)
69
+
70
+ self._http = HttpClient(
71
+ access_key=resolved_ak,
72
+ sign_secret=resolved_sk,
73
+ base_url=base_url,
74
+ timeout=timeout,
75
+ )
76
+ resolved_dir = Path(download_dir).expanduser() if download_dir else None
77
+ self._factor_svc = FactorService(self._http, download_dir=resolved_dir)
78
+ self._stock_svc = StockService(self._http)
79
+ self._quota_svc = QuotaService(self._http)
80
+ self._calendar_svc = CalendarService(self._http)
81
+
82
+ def close(self) -> None:
83
+ """关闭底层 HTTP 连接。"""
84
+ self._http.close()
85
+
86
+ def __enter__(self) -> QuantZone:
87
+ """进入上下文,返回客户端自身。"""
88
+ return self
89
+
90
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
91
+ """退出上下文,关闭底层 HTTP 连接。"""
92
+ self.close()
93
+ return False
94
+
95
+ # ── 因子 ──────────────────────────────────────────────
96
+
97
+ def get_factors(
98
+ self,
99
+ order_book_ids: str | list[str] | None = None,
100
+ factor: str | list[str] | None = None,
101
+ start_date: DateLike | None = None,
102
+ end_date: DateLike | None = None,
103
+ filename: str | None = None,
104
+ ) -> pd.DataFrame:
105
+ """一次性返回完整因子数据(窄表)。
106
+
107
+ Args:
108
+ order_book_ids: 股票代码(``str`` / ``list[str]`` / None=全市场),
109
+ 支持纯 6 位或带交易所后缀,详见 :meth:`FactorService.get_factors`。
110
+ factor: 因子名(``str`` / ``list[str]``),必填,1-20 个。
111
+ start_date: 起始日期,必填。
112
+ end_date: 截止日期,省略 = 服务端当日。
113
+ filename: 自定义保存文件名。
114
+
115
+ Returns:
116
+ 窄表 ``DataFrame``,列:``date / order_book_id / value / factor``。
117
+ """
118
+ return self._factor_svc.get_factors(
119
+ order_book_ids=order_book_ids,
120
+ factor=factor,
121
+ start_date=start_date,
122
+ end_date=end_date,
123
+ filename=filename,
124
+ )
125
+
126
+ def list_factors(self) -> list[dict]:
127
+ """获取因子元信息列表(含描述、分类)。"""
128
+ return self._factor_svc.list_factors()
129
+
130
+ # ── 合约元信息 ────────────────────────────────────
131
+
132
+ def list_stocks(self) -> pd.DataFrame:
133
+ """获取全量合约(``order_book_id / industry_name / symbol``)。"""
134
+ return self._stock_svc.list_stocks()
135
+
136
+ # ── 交易日历 ──────────────────────────────────────
137
+
138
+ def get_trading_dates(self, start_date: DateLike, end_date: DateLike) -> list[date]:
139
+ """获取区间内交易日列表。"""
140
+ return self._calendar_svc.get_trading_dates(start_date, end_date)
141
+
142
+ def get_previous_trading_date(self, current_date: DateLike, n: int = 1) -> date:
143
+ """前 n 个交易日。"""
144
+ return self._calendar_svc.get_previous_trading_date(current_date, n)
145
+
146
+ def get_next_trading_date(self, current_date: DateLike, n: int = 1) -> date:
147
+ """后 n 个交易日。"""
148
+ return self._calendar_svc.get_next_trading_date(current_date, n)
149
+
150
+ def is_trading_date(self, current_date: DateLike) -> bool:
151
+ """判断是否为交易日。"""
152
+ return self._calendar_svc.is_trading_date(current_date)
153
+
154
+ # ── 配额 ──────────────────────────────────────────
155
+
156
+ def get_quota(self) -> Quota:
157
+ """获取当前用户配额。"""
158
+ return self._quota_svc.get_quota()
@@ -0,0 +1,110 @@
1
+ """宽舟科技 SDK 异常定义."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class QuantError(Exception):
7
+ """SDK 基类异常.
8
+
9
+ Attributes:
10
+ message: 错误信息。
11
+ status_code: HTTP 状态码;非 HTTP 错误为 None。
12
+ code: 服务端业务错误码(对齐 dwyeapi ApiResponse.code);
13
+ 未知/未携带时为 None。
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ message: str = "",
19
+ *,
20
+ status_code: int | None = None,
21
+ code: str | None = None,
22
+ ):
23
+ self.message = message
24
+ self.status_code = status_code
25
+ self.code = code
26
+ super().__init__(message)
27
+
28
+
29
+ class AuthError(QuantError):
30
+ """API Key 无效或已过期."""
31
+
32
+ def __init__(self, message: str = "API Key 无效或已过期", *, code: str | None = None):
33
+ super().__init__(message, status_code=401, code=code)
34
+
35
+
36
+ class ApiKeyRevokedError(AuthError):
37
+ """API Key 已被吊销 → 服务端返回 401 + code=API_KEY_REVOKED.
38
+
39
+ 用户应在 admin 后台重置 / 重新申请 API Key 后再调用 SDK。
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ message: str = "API Key 已被吊销,请联系管理员或重新申请",
45
+ ):
46
+ super().__init__(message, code="API_KEY_REVOKED")
47
+
48
+
49
+ class IpNotAllowedError(QuantError):
50
+ """请求来源 IP 不在白名单 → 服务端返回 403 + code=IP_NOT_ALLOWED.
51
+
52
+ 用户应检查 admin 后台为该 API Key 设置的 IP 白名单是否覆盖当前出口 IP。
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ message: str = "请求来源 IP 不在 API Key 白名单内",
58
+ ):
59
+ super().__init__(message, status_code=403, code="IP_NOT_ALLOWED")
60
+
61
+
62
+ class ApiKeyRateLimitError(QuantError):
63
+ """API Key 调用频率超限 → 服务端返回 422 + code=API_KEY_RATE_LIMITED.
64
+
65
+ 与 QuotaExceededError 区分:前者是按"调用次数"限流(防滥用),
66
+ 后者是按"流量字节"扣费(配额耗尽)。
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ message: str = "API Key 调用频率超限,请稍后再试",
72
+ ):
73
+ super().__init__(message, status_code=422, code="API_KEY_RATE_LIMITED")
74
+
75
+
76
+ class QuotaExceededError(QuantError):
77
+ """配额不足."""
78
+
79
+ def __init__(self, message: str = "配额不足", *, remaining: int = 0, required: int = 0):
80
+ self.remaining = remaining
81
+ self.required = required
82
+ super().__init__(message, status_code=429)
83
+
84
+
85
+ class NetworkError(QuantError):
86
+ """网络连接问题."""
87
+
88
+ def __init__(self, message: str = "网络连接失败", *, cause: Exception | None = None):
89
+ self.cause = cause
90
+ super().__init__(message)
91
+
92
+
93
+ class SignatureError(QuantError):
94
+ """签名校验失败(服务端返回 401)."""
95
+
96
+ def __init__(self, message: str = "请求签名校验失败"):
97
+ super().__init__(message, status_code=401)
98
+
99
+
100
+ class SDKVersionError(QuantError):
101
+ """SDK 版本过低,服务端已停止支持(HTTP 426)。
102
+
103
+ 错误消息包含升级命令,用户按提示执行 `pip install -U quantzone` 即可。
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ message: str = "当前 SDK 版本已停止支持,请执行 `pip install -U quantzone` 升级后重试",
109
+ ):
110
+ super().__init__(message, status_code=426)
quantzone/factor.py ADDED
@@ -0,0 +1,252 @@
1
+ """因子查询。
2
+
3
+ 对外仅提供 :meth:`FactorService.get_factors` — 一次性返回完整 ``DataFrame``。
4
+
5
+ - 边收边解码 Arrow IPC stream:每个 RecordBatch 立即转 ``DataFrame``。
6
+ - 边收边落盘:HTTP 字节同时写到 ``download_dir/temp/<id>.arrow``。
7
+ - 流自然结束 → 原子重命名到 ``download_dir/factors_<ts>.arrow``。
8
+ - 失败 → temp 文件保留,不删除。
9
+
10
+ 返回窄表,列固定 4 列:``date / order_book_id / value / factor``。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import io
16
+ import os
17
+ import uuid
18
+ from collections.abc import Iterator
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import IO, Any
22
+
23
+ import pandas as pd
24
+ import pyarrow.ipc as ipc
25
+
26
+ from ._dates import DateLike, normalize_date
27
+ from ._http import API_PREFIX, HttpClient
28
+ from ._logger import logger
29
+
30
+ DEFAULT_DOWNLOAD_DIR = Path.home() / ".quantzone" / "downloads"
31
+ PERSIST_FILENAME_PREFIX = "factors_"
32
+ TEMP_SUBDIR = "temp"
33
+ ARROW_EXT = ".arrow"
34
+
35
+ _PUBLIC_FACTOR_META_FIELDS = ("factorname", "startDate", "endDate")
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
+ 响应以 Arrow IPC 流式返回,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]``,最多 100 个,None 表示全市场。
145
+ 支持两种等价格式:
146
+
147
+ - 纯 6 位数字:``"000001"`` / ``["000001", "600036"]``
148
+ - 6 位 + 交易所后缀:``"000001.SZ"`` / ``["600036.SH", "430047.BJ"]``
149
+ factor: 因子名(``str`` 或 ``list[str]``),必填,1-20 个。
150
+ 示例:``"alpha1"`` 或 ``["alpha1", "vwap"]``。省略会被服务端拒绝。
151
+ start_date: 起始日期,必填,支持 ``int(YYYYMMDD)`` / ``str`` / ``date`` /
152
+ ``datetime`` / ``Timestamp``。
153
+ end_date: 截止日期,格式同上;省略 = 服务端当日。
154
+ filename: 自定义最终文件名(落到 ``download_dir`` 下);None 时按时间戳生成。
155
+
156
+ Returns:
157
+ 窄表 ``DataFrame``,列:``date / order_book_id / value / factor``。
158
+ 空结果时返回带正确列名的空 DataFrame。
159
+ """
160
+ ids_list = _to_str_list(order_book_ids)
161
+ factor_list = _to_str_list(factor)
162
+
163
+ body: dict[str, Any] = {}
164
+ if factor_list:
165
+ body["factor"] = factor_list
166
+ if ids_list:
167
+ body["order_book_ids"] = ids_list
168
+ if start_date is not None:
169
+ body["start_date"] = normalize_date(start_date)
170
+ if end_date is not None:
171
+ body["end_date"] = normalize_date(end_date)
172
+
173
+ download_dir = self._download_dir
174
+ temp_dir = download_dir / TEMP_SUBDIR
175
+ temp_dir.mkdir(parents=True, exist_ok=True)
176
+ download_dir.mkdir(parents=True, exist_ok=True)
177
+
178
+ temp_path = temp_dir / f"{uuid.uuid4().hex}{ARROW_EXT}"
179
+ if filename is not None:
180
+ # 路径遍历防护:filename 只能是纯文件名,不允许任何路径分隔符或父目录引用
181
+ # 注意 POSIX 下 Path("a\\b").name 返回 "a\\b",必须显式拒绝反斜杠以保证跨平台安全
182
+ if (
183
+ filename in ("", ".", "..")
184
+ or "/" in filename
185
+ or "\\" in filename
186
+ or filename != Path(filename).name
187
+ ):
188
+ raise ValueError(
189
+ f"filename 必须是纯文件名(不含路径分隔符 / 或 \\,不含 . .. 前缀): {filename!r}"
190
+ )
191
+ final_name = filename
192
+ else:
193
+ final_name = (
194
+ f"{PERSIST_FILENAME_PREFIX}{datetime.now():%Y%m%d_%H%M%S}_{uuid.uuid4().hex[:8]}{ARROW_EXT}"
195
+ )
196
+ final_path = download_dir / final_name
197
+ # 最终验证:解析后的路径必须仍在 download_dir 下,防止符号链接/相对路径绕过
198
+ if final_path.resolve().parent != download_dir.resolve():
199
+ raise ValueError(f"filename 解析后超出下载目录: {filename!r}")
200
+
201
+ logger.info("下载文件(temp): %s", temp_path)
202
+
203
+ chunks_dfs = list(self._iter_batches(body, temp_path, final_path))
204
+ if not chunks_dfs:
205
+ return pd.DataFrame(columns=["date", "order_book_id", "value", "factor"])
206
+ return pd.concat(chunks_dfs, ignore_index=True)
207
+
208
+ def _iter_batches(
209
+ self,
210
+ body: dict[str, Any],
211
+ temp_path: Path,
212
+ final_path: Path,
213
+ ) -> Iterator[pd.DataFrame]:
214
+ """流式解码生成器:每个 RecordBatch 立即 yield ``DataFrame``,边收边写 temp。"""
215
+ success = False
216
+ sink = temp_path.open("wb")
217
+ try:
218
+ with self._http.stream_post_iter(f"{API_PREFIX}/factors/stream", body) as chunks:
219
+ tee = _TeeReader(chunks, sink)
220
+ reader = ipc.open_stream(tee)
221
+ while True:
222
+ try:
223
+ batch = reader.read_next_batch()
224
+ except StopIteration:
225
+ break
226
+ if batch is None:
227
+ break
228
+ yield batch.to_pandas()
229
+ success = True
230
+ finally:
231
+ sink.flush()
232
+ sink.close()
233
+ if success:
234
+ os.replace(temp_path, final_path)
235
+ size = final_path.stat().st_size
236
+ logger.info("已保存到: %s (%d bytes)", final_path, size)
237
+ else:
238
+ logger.info("流未正常结束,temp 文件保留: %s", temp_path)
239
+
240
+ def list_factors(self) -> list[dict]:
241
+ """获取可用因子元信息列表。
242
+
243
+ Returns:
244
+ 因子列表,每个元素含 ``factorname`` / ``startDate`` / ``endDate`` 三个字段。
245
+ 服务端返回的其他字段一律丢弃,不暴露未知字段。
246
+ """
247
+ data = self._http.get(f"{API_PREFIX}/factors")
248
+ raw = data if isinstance(data, list) else data.get("items", data.get("factors", []))
249
+ return [
250
+ {k: item[k] for k in _PUBLIC_FACTOR_META_FIELDS if k in item}
251
+ for item in raw
252
+ ]
quantzone/py.typed ADDED
File without changes
quantzone/quota.py ADDED
@@ -0,0 +1,77 @@
1
+ """配额查询服务。
2
+
3
+ 对外暴露三层配额视图:今日基础额度(``daily_quota_bytes`` / ``daily_used_bytes``)、
4
+ 扩容包余额(``extra_quota_bytes``)、合并后的精确可用余量(``available_bytes``),
5
+ 以及套餐元信息(``remaining_days`` / ``license_type``)。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TypedDict
11
+
12
+ from ._http import API_PREFIX, HttpClient
13
+
14
+ # 注:remaining_days 单独处理(可为 None 表示永久套餐),不放此列表
15
+ _PUBLIC_INT_FIELDS = (
16
+ "daily_quota_bytes",
17
+ "daily_used_bytes",
18
+ "extra_quota_bytes",
19
+ "available_bytes",
20
+ )
21
+
22
+
23
+ class Quota(TypedDict, total=False):
24
+ """配额信息字典。
25
+
26
+ Attributes:
27
+ daily_quota_bytes: 今日基础流量额度上限(字节)。次日重置。
28
+ daily_used_bytes: 今日已用流量(字节)。次日重置。
29
+ extra_quota_bytes: 扩容包剩余流量(字节)。永久有效,用完为止,默认 0。
30
+ available_bytes: 当前实际可用余量(字节),三层合并扣除已用与冻结后的精确值。
31
+ remaining_days: 套餐剩余天数;``None`` 表示永久套餐(如免费版)。
32
+ license_type: 套餐类型(如 ``"trial"`` / ``"commercial"`` / ``"免费版"``)。
33
+ """
34
+
35
+ daily_quota_bytes: int
36
+ daily_used_bytes: int
37
+ extra_quota_bytes: int
38
+ available_bytes: int
39
+ remaining_days: int | None
40
+ license_type: str
41
+
42
+
43
+ class QuotaService:
44
+ """用户配额查询服务。"""
45
+
46
+ def __init__(self, http: HttpClient) -> None:
47
+ """初始化。
48
+
49
+ Args:
50
+ http: HTTP 客户端。
51
+ """
52
+ self._http = http
53
+
54
+ def get_quota(self) -> Quota:
55
+ """获取当前用户配额。
56
+
57
+ Returns:
58
+ 配额字典,含 ``daily_quota_bytes`` / ``daily_used_bytes`` /
59
+ ``extra_quota_bytes`` / ``available_bytes`` / ``remaining_days`` /
60
+ ``license_type`` 六个字段。响应缺失的字段在结果中也会缺失;
61
+ ``remaining_days`` 可能为 ``None`` 表示永久套餐(如免费版)。
62
+ """
63
+ data = self._http.get(f"{API_PREFIX}/quota")
64
+ if not isinstance(data, dict):
65
+ return {}
66
+
67
+ result: Quota = {}
68
+ for key in _PUBLIC_INT_FIELDS:
69
+ if key in data:
70
+ result[key] = int(data[key])
71
+ # remaining_days 单独处理:服务端可能返 None(永久套餐),保持 None 不转 int
72
+ if "remaining_days" in data:
73
+ value = data["remaining_days"]
74
+ result["remaining_days"] = None if value is None else int(value)
75
+ if "license_type" in data:
76
+ result["license_type"] = str(data["license_type"])
77
+ return result
quantzone/stock.py ADDED
@@ -0,0 +1,49 @@
1
+ """股票/合约元信息查询服务。
2
+
3
+ 返回 ``DataFrame``,列固定 3 列:``order_book_id / industry_name / symbol``。
4
+ """
5
+
6
+ import pandas as pd
7
+
8
+ from ._http import API_PREFIX, HttpClient
9
+
10
+ INSTRUMENT_COLUMNS = ["order_book_id", "industry_name", "symbol"]
11
+
12
+
13
+ def _to_dataframe(items: list[dict]) -> pd.DataFrame:
14
+ """把 ``list[dict]`` 转为合约 ``DataFrame``,缺失列补 ``NA``,列顺序固定。"""
15
+ if not items:
16
+ return pd.DataFrame(columns=INSTRUMENT_COLUMNS)
17
+ df = pd.DataFrame(items)
18
+ for col in INSTRUMENT_COLUMNS:
19
+ if col not in df.columns:
20
+ df[col] = pd.NA
21
+ return df[INSTRUMENT_COLUMNS]
22
+
23
+
24
+ class StockService:
25
+ """股票/合约信息查询服务。"""
26
+
27
+ def __init__(self, http: HttpClient) -> None:
28
+ """初始化。
29
+
30
+ Args:
31
+ http: HTTP 客户端。
32
+ """
33
+ self._http = http
34
+
35
+ def list_stocks(self) -> pd.DataFrame:
36
+ """获取全量合约列表。
37
+
38
+ 服务端不再支持按交易所过滤,客户端拿全量后自行筛选,例如::
39
+
40
+ df = client.list_stocks()
41
+ sz = df[df["order_book_id"].str.endswith(".SZ")]
42
+
43
+ Returns:
44
+ 合约 ``DataFrame``,列固定 3 列:
45
+ ``order_book_id / industry_name / symbol``。
46
+ """
47
+ data = self._http.get(f"{API_PREFIX}/stocks")
48
+ items = data if isinstance(data, list) else data.get("items", data.get("stocks", []))
49
+ return _to_dataframe(items)
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantzone
3
+ Version: 0.5.0
4
+ Summary: 宽舟科技量化数据平台 Python SDK
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.9
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
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
+ Requires-Dist: respx>=0.21; extra == "dev"
15
+ Requires-Dist: ruff>=0.4; extra == "dev"
16
+ Requires-Dist: build>=1.2; extra == "dev"
17
+ Requires-Dist: mypy>=1.10; extra == "dev"
18
+ Requires-Dist: tomli; python_version < "3.11" and 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
+ 完整使用文档、API 说明、示例与配额规则请见官网:
45
+
46
+ **http://www.quantzone.tech**
@@ -0,0 +1,21 @@
1
+ quantzone/__init__.py,sha256=Rkqbbtf0jXhZOEhZRKe7TNkcEfAKo9QsC82PjlBg5YY,3522
2
+ quantzone/_auth.cp39-win_amd64.pyd,sha256=0rEDbPjuD1kb-au8urWBoLGJVZxpTgMv51tEN678WKI,37376
3
+ quantzone/_auth.pyi,sha256=mJJgyic2dHSa-Cbo9iWRN8PbsNcqm5q8FOKDWoQUkAE,144
4
+ quantzone/_dates.py,sha256=gEKMSFPVKb20NrbnVbTyUjwpGMTwLhpTR-i-nlsYLnw,2586
5
+ quantzone/_http.cp39-win_amd64.pyd,sha256=nEKK51d1r6acUnb1Lu2vkZqE5-_uQMXC6oIiSFYIkko,141824
6
+ quantzone/_http.pyi,sha256=Zgz3_yp5LV88mCbFThqyELx8QEb9krNBDKuYnGMOvKs,1470
7
+ quantzone/_logger.py,sha256=KIyFcJpfZ4x4-HbIDQguNVnR_pqEyVPDN7gKfwWsUAY,542
8
+ quantzone/_signer.cp39-win_amd64.pyd,sha256=hsrAh1WKZzXbtqU6VQMEhemKU-QG5fU94slm1i4VwdY,49152
9
+ quantzone/_signer.pyi,sha256=CZyftqqAx8B4EBNMQ4kFdtBZWqrOLH5cmMY0ljwFb9w,151
10
+ quantzone/_singleton.py,sha256=jTqyVnRDKmWkTGaWLJxHEPk6rLTA0457v31rpiEUG4w,2840
11
+ quantzone/calendar.py,sha256=Vts8UXYAjyipaEylTtuL707gNeqt1tk5zSg-gOgjDAg,2936
12
+ quantzone/client.py,sha256=qURt3CJws6ygg8S5a3cSQDPKeVBGxNWplgT20yCZZsE,6221
13
+ quantzone/exceptions.py,sha256=SBSw6V-qs9qL2LoltAtnXZAS4hnO9U0DxnW63gIvH98,3367
14
+ quantzone/factor.py,sha256=xmq_fNZlnS8_kEp9RcMowtnCNMLzYg1fwpOtR33aXqE,9537
15
+ quantzone/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ quantzone/quota.py,sha256=CqFaWhWR48y0wiI22RIG-t2CUK7eECZZNq2iNiExgXg,2762
17
+ quantzone/stock.py,sha256=tu69BQ2MXmiVfb4qGLiA5Jn63mFP-o8VE5m8r-oRt00,1522
18
+ quantzone-0.5.0.dist-info/METADATA,sha256=GOAldrkbdhZuoyp7DUYJSOEz3al60Wz5zlDiWX8xE6Q,1511
19
+ quantzone-0.5.0.dist-info/WHEEL,sha256=zuNlb9Um05hyCt8XRmFaVi795Ee7Bc9lOiOIpwTM7Gs,99
20
+ quantzone-0.5.0.dist-info/top_level.txt,sha256=81VUVakCg5LCWs-GJ5_jyQ5aTSQPllguMTGwu1h4YEU,10
21
+ quantzone-0.5.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: cp39-cp39-win_amd64
5
+
@@ -0,0 +1 @@
1
+ quantzone