hamuna-quant-cli 0.1.0.dev93__py3-none-any.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.
- hamuna_quant_cli/README.md +117 -0
- hamuna_quant_cli/__init__.py +17 -0
- hamuna_quant_cli/__main__.py +978 -0
- hamuna_quant_cli/_market_fallback.py +82 -0
- hamuna_quant_cli/_metrics_15.py +342 -0
- hamuna_quant_cli/_test_akquant_parity.py +530 -0
- hamuna_quant_cli/akquant_data_adapter.py +295 -0
- hamuna_quant_cli/akquant_runner.py +620 -0
- hamuna_quant_cli/akquant_schema_adapter.py +443 -0
- hamuna_quant_cli/base_strategy.py +80 -0
- hamuna_quant_cli/cross_sectional_helpers.py +118 -0
- hamuna_quant_cli/live/__init__.py +25 -0
- hamuna_quant_cli/live/loader.py +121 -0
- hamuna_quant_cli/live/qmt_broker.py +683 -0
- hamuna_quant_cli/live/qmt_market.py +448 -0
- hamuna_quant_cli/live/runner.py +449 -0
- hamuna_quant_cli/prebuilt_downloader.py +263 -0
- hamuna_quant_cli/prebuilt_resolver.py +470 -0
- hamuna_quant_cli/qmt_translator.py +609 -0
- hamuna_quant_cli/runtime/__init__.py +2 -0
- hamuna_quant_cli/runtime/backtest.py +38 -0
- hamuna_quant_cli/runtime/cache.py +255 -0
- hamuna_quant_cli/runtime/discipline.py +359 -0
- hamuna_quant_cli/runtime/http_client.py +209 -0
- hamuna_quant_cli/runtime/s3client.py +109 -0
- hamuna_quant_cli/runtime/server_client.py +285 -0
- hamuna_quant_cli/scripts/server.json +4 -0
- hamuna_quant_cli-0.1.0.dev93.dist-info/METADATA +154 -0
- hamuna_quant_cli-0.1.0.dev93.dist-info/RECORD +32 -0
- hamuna_quant_cli-0.1.0.dev93.dist-info/WHEEL +5 -0
- hamuna_quant_cli-0.1.0.dev93.dist-info/entry_points.txt +2 -0
- hamuna_quant_cli-0.1.0.dev93.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""ADR-0040 Phase C — prebuilt 数据集下载器 (v2 自有版本, B6).
|
|
2
|
+
|
|
3
|
+
v2 自足版 (2026-08-19 修复): 原 wholesale copy 引用了包根不存在的
|
|
4
|
+
`cache` / `server_client` / `market` 模块 (`from . import cache` 挂 =
|
|
5
|
+
dataset list/fetch 全废)。本版改为直用 v2 runtime:
|
|
6
|
+
- runtime.cache — bundle/single 落盘路径 (~/.hamuna/data_cache/datasets/)
|
|
7
|
+
- runtime.server_client — list_datasets / s3_credentials (加密 S3 凭据)
|
|
8
|
+
- runtime.s3client — HKDF 解密 + boto3 全头签名下载 (COS 反代 presign 403)
|
|
9
|
+
|
|
10
|
+
三层下载:
|
|
11
|
+
list_available() — 调 server `list_datasets` 枚举内置预构建
|
|
12
|
+
download_bundle(uni) — 加密凭据 + boto3 直连 → 落 __bundle__<uni>_<dt>.parquet
|
|
13
|
+
download_single(sym) — 从所属 bundle 切片 → 落 <bare>_<dt>_fq1.parquet
|
|
14
|
+
(bundle 缺失则先整包下载; server 无 per-symbol artifact)
|
|
15
|
+
|
|
16
|
+
CLI:
|
|
17
|
+
hamuna_quant_cli dataset list
|
|
18
|
+
hamuna_quant_cli dataset fetch --symbols 600000,600036 [--start] [--end]
|
|
19
|
+
(bundle/all 入口保留: python -m hamuna_quant_cli.prebuilt_downloader fetch bundle all_a)
|
|
20
|
+
|
|
21
|
+
隔离:
|
|
22
|
+
- 不重新实现 S3 / 容维 client; 全部走 v2 包内 runtime
|
|
23
|
+
- 失败统一抛 RuntimeError, CLI 出口码 = 2 (与 hamuna_quant_cli 一致: 数据缺失)
|
|
24
|
+
- 新依赖: boto3 + cryptography (pyproject 已声明, S3 签名下载必需)
|
|
25
|
+
|
|
26
|
+
Ponytail ceiling: 只支持日线 (period='1d' 唯一, 与 KNOWN_PREBUILT 对齐).
|
|
27
|
+
升级: 加 5m/15m 需先扩 _PREBUILT_TYPE_CODE 注册 + server 端预构建 + 容维 minute 权限.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from .runtime import cache as _cache
|
|
36
|
+
from .runtime.http_client import _load_token
|
|
37
|
+
from .runtime.server_client import list_datasets, s3_credentials
|
|
38
|
+
from .runtime.s3client import decrypt_s3_params, download_to
|
|
39
|
+
|
|
40
|
+
# dt → server data_type_code (server 目前仅日线预构建; 5m 有数据集但 resolver 锁 1d)
|
|
41
|
+
_PREBUILT_TYPE_CODE: dict[str, str] = {'D': 'kline_d'}
|
|
42
|
+
|
|
43
|
+
# universe → 覆盖前缀 (与 prebuilt_resolver.KNOWN_PREBUILT 的 bundle_prefix_predicate 同步)
|
|
44
|
+
_BUNDLE_PREFIXES: dict[str, tuple[str, ...]] = {
|
|
45
|
+
'all_a': ('60', '68', '00', '30', '40', '43', '83', '87', '88'),
|
|
46
|
+
'etf': ('51', '56', '15', '16'),
|
|
47
|
+
'convertible_bond': ('11', '12'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# single parquet 写入列 (bundle 切片; resolver._read_single_with_pushdown 认 time/OHLCV)
|
|
51
|
+
_SINGLE_COLUMNS = ('time', 'stockCode', 'open', 'high', 'low', 'close', 'volume', 'amount')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_symbol(symbol: str) -> str:
|
|
55
|
+
"""裸码 → 带后缀 (与 akquant_schema_adapter.normalize_symbol 同规则, 避免循环 import)."""
|
|
56
|
+
s = symbol.strip().upper()
|
|
57
|
+
if s.endswith(('.SH', '.SZ', '.BJ')):
|
|
58
|
+
return symbol
|
|
59
|
+
if len(s) > 2 and s[:2] in ('SH', 'SZ', 'BJ'):
|
|
60
|
+
s = s[2:]
|
|
61
|
+
if len(s) != 6 or not s.isdigit():
|
|
62
|
+
return symbol
|
|
63
|
+
if s.startswith(('60', '68', '11', '13')):
|
|
64
|
+
return f'{s}.SH'
|
|
65
|
+
return f'{s}.SZ'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _bundle_for(symbol: str) -> str | None:
|
|
69
|
+
"""带后缀 symbol → 所属 bundle universe 名 (前缀匹配; 无 → None)."""
|
|
70
|
+
bare = symbol.split('.')[0]
|
|
71
|
+
for uni, prefixes in _BUNDLE_PREFIXES.items():
|
|
72
|
+
if any(bare.startswith(p) for p in prefixes):
|
|
73
|
+
return uni
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---- API ----
|
|
78
|
+
|
|
79
|
+
def list_available() -> list[dict]:
|
|
80
|
+
"""GET /api/v1/datasets → server 内置预构建列表.
|
|
81
|
+
|
|
82
|
+
每条 {universe_code, data_type_code, builtin, enabled, ...}.
|
|
83
|
+
返 [] 表示请求失败 / 无内置 — CLI 提示"无预构建".
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
return list_datasets()
|
|
87
|
+
except Exception as e: # noqa: BLE001 — 与 v1 同语义: 请求失败 → [] 引导
|
|
88
|
+
print(f'[warn] list_datasets 失败: {type(e).__name__}: {str(e)[:160]}', file=sys.stderr)
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def download_bundle(universe: str, dt: str = 'D') -> Path:
|
|
93
|
+
"""下载 server 预构建整包 → 落 __bundle__<uni>_<dt>.parquet.
|
|
94
|
+
|
|
95
|
+
走 s3_credentials (加密凭据) + s3client (boto3 全头签名直连) — COS 反代
|
|
96
|
+
presign 403, 这是唯一可靠路径。本地已存在 → 直接复用 (无 TTL, 历史不变).
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RuntimeError: 整包下载失败 / 非预构建类型 / server 无此内置
|
|
100
|
+
"""
|
|
101
|
+
type_code = _PREBUILT_TYPE_CODE.get(dt)
|
|
102
|
+
if type_code is None:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f'dt={dt!r} 无预构建 (仅日线 D). 已知: {list(_PREBUILT_TYPE_CODE.keys())}'
|
|
105
|
+
)
|
|
106
|
+
path = _cache.bundle_path(universe, dt)
|
|
107
|
+
if path.exists() and path.stat().st_size > 0:
|
|
108
|
+
return path # 已落盘复用 (无 TTL, 历史不变)
|
|
109
|
+
|
|
110
|
+
available = [d for d in list_available()
|
|
111
|
+
if d.get('universe_code') == universe
|
|
112
|
+
and d.get('data_type_code') == type_code
|
|
113
|
+
and d.get('builtin') and d.get('enabled')]
|
|
114
|
+
if not available:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
f'server 无内置预构建 {universe}/{dt} (或 builtin/enabled 未开)'
|
|
117
|
+
)
|
|
118
|
+
did = available[0].get('id') or available[0].get('_id')
|
|
119
|
+
if not did:
|
|
120
|
+
raise RuntimeError(f'dataset {universe}/{dt} 缺 id: {available[0]}')
|
|
121
|
+
|
|
122
|
+
# S3 key 布局: <datasetID>/<universe>/<type_code>/_merged/all.parquet
|
|
123
|
+
key = f'{did}/{universe}/{type_code}/_merged/all.parquet'
|
|
124
|
+
try:
|
|
125
|
+
env = s3_credentials(did, key)
|
|
126
|
+
params = decrypt_s3_params(_load_token(), did, env)
|
|
127
|
+
download_to(params, path)
|
|
128
|
+
except Exception as e: # noqa: BLE001 — 失败原因必须可见, 不静默回退
|
|
129
|
+
raise RuntimeError(f'整包下载失败 {universe}/{type_code}: {type(e).__name__}: {e}') from e
|
|
130
|
+
return path
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def download_single(symbol: str, period: str = '1d',
|
|
134
|
+
start: str | None = None,
|
|
135
|
+
end: str | None = None) -> Path:
|
|
136
|
+
"""下载单股数据集 → 落 <bare>_<dt>_fq1.parquet.
|
|
137
|
+
|
|
138
|
+
从所属 bundle 切片 (server 无 per-symbol artifact; bundle 覆盖 2019~今,
|
|
139
|
+
全量历史, start/end 不截断 — 语义 = "确保该股 dataset 可用").
|
|
140
|
+
bundle 缺失 → 先整包下载 (一次 422MB, 之后全部 single 复用).
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
RuntimeError: bundle 不含该股 / 切片为空 (次新股/停牌/数据不可达)
|
|
144
|
+
"""
|
|
145
|
+
if period != '1d':
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f'period={period!r} 仅支持 1d (与 KNOWN_PREBUILT 对齐); '
|
|
148
|
+
f'非日线 dataset build 走 strategy_cli dataset --config'
|
|
149
|
+
)
|
|
150
|
+
full = _normalize_symbol(symbol)
|
|
151
|
+
bare = full.split('.')[0]
|
|
152
|
+
universe = _bundle_for(full)
|
|
153
|
+
if universe is None:
|
|
154
|
+
raise RuntimeError(f'{full} 不属于任何预构建 universe (all_a/etf/convertible_bond)')
|
|
155
|
+
|
|
156
|
+
bundle = download_bundle(universe, 'D')
|
|
157
|
+
import pyarrow.parquet as pq
|
|
158
|
+
try:
|
|
159
|
+
table = pq.read_table(
|
|
160
|
+
bundle, columns=list(_SINGLE_COLUMNS),
|
|
161
|
+
filters=[('stockCode', '=', full)],
|
|
162
|
+
)
|
|
163
|
+
except Exception as e: # noqa: BLE001
|
|
164
|
+
raise RuntimeError(f'bundle 读取失败 ({bundle}): {e}') from e
|
|
165
|
+
if table.num_rows == 0:
|
|
166
|
+
raise RuntimeError(
|
|
167
|
+
f'bundle {universe} 不含 {full} (可能退市/停更/不在成分池)'
|
|
168
|
+
)
|
|
169
|
+
out = _cache._dataset_path(full, 'D', 1)
|
|
170
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
table.to_pandas().to_parquet(out, index=False)
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def fetch_universe(universe: str, dt: str = 'D') -> tuple[Path, int]:
|
|
176
|
+
"""bundle 落盘 + bundle 内全部 single 切片.
|
|
177
|
+
|
|
178
|
+
返 (bundle_path, n_single_written). 单次读入 bundle 后 groupby 切片,
|
|
179
|
+
避免逐股 pyarrow 全文件扫描 (5000 股 × 422MB = 不可接受).
|
|
180
|
+
"""
|
|
181
|
+
bundle_path = download_bundle(universe, dt)
|
|
182
|
+
import pyarrow.parquet as pq
|
|
183
|
+
df = pq.read_table(bundle_path, columns=list(_SINGLE_COLUMNS)).to_pandas()
|
|
184
|
+
n_ok = 0
|
|
185
|
+
for code, sub in df.groupby('stockCode'):
|
|
186
|
+
out = _cache._dataset_path(str(code), 'D', 1)
|
|
187
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
sub.to_parquet(out, index=False)
|
|
189
|
+
n_ok += 1
|
|
190
|
+
return bundle_path, n_ok
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---- CLI ----
|
|
194
|
+
|
|
195
|
+
def _cmd_list(_args) -> int:
|
|
196
|
+
try:
|
|
197
|
+
items = list_available()
|
|
198
|
+
except RuntimeError as e:
|
|
199
|
+
print(f'[ERR] {e}', file=sys.stderr)
|
|
200
|
+
return 2
|
|
201
|
+
if not items:
|
|
202
|
+
print('[OK] server 内置预构建 = 空 (或 builtin/enabled 全关)', file=sys.stderr)
|
|
203
|
+
return 0
|
|
204
|
+
print(f'[OK] server 内置预构建 ({len(items)} 条):', file=sys.stderr)
|
|
205
|
+
for d in items:
|
|
206
|
+
print(f' {d.get("universe_code"):20s} {d.get("data_type_code"):12s} '
|
|
207
|
+
f'builtin={d.get("builtin")} enabled={d.get("enabled")}',
|
|
208
|
+
file=sys.stderr)
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _cmd_fetch(args) -> int:
|
|
213
|
+
target = args.target
|
|
214
|
+
try:
|
|
215
|
+
if target == 'bundle':
|
|
216
|
+
p = download_bundle(args.universe, dt=args.dt)
|
|
217
|
+
size_mb = round(p.stat().st_size / 1e6, 1) if p.exists() else 0
|
|
218
|
+
print(f'[OK] bundle {args.universe}/{args.dt} → {p} ({size_mb} MB)',
|
|
219
|
+
file=sys.stderr)
|
|
220
|
+
elif target == 'single':
|
|
221
|
+
p = download_single(args.symbol, period=args.period,
|
|
222
|
+
start=args.start, end=args.end)
|
|
223
|
+
print(f'[OK] single {args.symbol} ({args.period}) → {p}', file=sys.stderr)
|
|
224
|
+
elif target == 'all':
|
|
225
|
+
bundle_path, n_ok = fetch_universe(args.universe, dt=args.dt)
|
|
226
|
+
print(f'[OK] all {args.universe}/{args.dt} → bundle={bundle_path}, '
|
|
227
|
+
f'{n_ok} single 落盘', file=sys.stderr)
|
|
228
|
+
else:
|
|
229
|
+
print(f'[ERR] 未知 target={target!r}', file=sys.stderr)
|
|
230
|
+
return 2
|
|
231
|
+
except RuntimeError as e:
|
|
232
|
+
print(f'[ERR] {e}', file=sys.stderr)
|
|
233
|
+
return 2
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def main(argv: list[str] | None = None) -> int:
|
|
238
|
+
p = argparse.ArgumentParser(
|
|
239
|
+
prog='prebuilt_downloader',
|
|
240
|
+
description='下载 hamuna prebuilt 数据集 (v2 自有: server 加密凭据 + boto3 直连)',
|
|
241
|
+
)
|
|
242
|
+
sub = p.add_subparsers(dest='cmd', required=True)
|
|
243
|
+
|
|
244
|
+
sub.add_parser('list', help='列出 server 内置预构建数据集').set_defaults(func=_cmd_list)
|
|
245
|
+
|
|
246
|
+
f = sub.add_parser('fetch', help='下载 prebuilt (bundle / single / all)')
|
|
247
|
+
f.add_argument('target', choices=['bundle', 'single', 'all'])
|
|
248
|
+
# bundle + all 用 universe
|
|
249
|
+
f.add_argument('--universe', help='bundle/all 用: universe 名 (e.g. all_a / etf / convertible_bond)')
|
|
250
|
+
f.add_argument('--dt', default='D', help='bundle/all 用: 日期类型 (默认 D)')
|
|
251
|
+
# single 用 symbol
|
|
252
|
+
f.add_argument('--symbol', help='single 用: 标的代码 (e.g. 600000.SH)')
|
|
253
|
+
f.add_argument('--period', default='1d', help='single 用: 周期 (默认 1d)')
|
|
254
|
+
f.add_argument('--start', help='single 用: 起始 YYYYMMDD (bundle 切片全量, 不截断)')
|
|
255
|
+
f.add_argument('--end', help='single 用: 截止 YYYYMMDD (bundle 切片全量, 不截断)')
|
|
256
|
+
f.set_defaults(func=_cmd_fetch)
|
|
257
|
+
|
|
258
|
+
args = p.parse_args(argv)
|
|
259
|
+
return args.func(args)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == '__main__':
|
|
263
|
+
sys.exit(main())
|