hamuna-quant-cli 0.1.2__tar.gz → 0.1.6__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 (40) hide show
  1. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/PKG-INFO +1 -1
  2. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/__init__.py +1 -1
  3. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/__main__.py +29 -54
  4. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/akquant_runner.py +5 -3
  5. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/qmt_translator.py +3 -2
  6. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/server_client.py +26 -72
  7. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
  8. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/SOURCES.txt +1 -2
  9. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/pyproject.toml +1 -1
  10. hamuna_quant_cli-0.1.2/tests/test_skeleton.py +0 -154
  11. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/MANIFEST.in +0 -0
  12. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/README.md +0 -0
  13. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/README.md +0 -0
  14. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/_market_fallback.py +0 -0
  15. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/_metrics_15.py +0 -0
  16. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
  17. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
  18. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
  19. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/base_strategy.py +0 -0
  20. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
  21. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/live/__init__.py +0 -0
  22. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/live/loader.py +0 -0
  23. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/live/qmt_broker.py +0 -0
  24. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/live/qmt_market.py +0 -0
  25. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/live/runner.py +0 -0
  26. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
  27. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
  28. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/__init__.py +0 -0
  29. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/backtest.py +0 -0
  30. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/cache.py +0 -0
  31. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/discipline.py +0 -0
  32. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/http_client.py +0 -0
  33. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/runtime/s3client.py +0 -0
  34. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli/scripts/server.json +0 -0
  35. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
  36. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
  37. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/requires.txt +0 -0
  38. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
  39. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/setup.cfg +0 -0
  40. {hamuna_quant_cli-0.1.2 → hamuna_quant_cli-0.1.6}/tests/test_live_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hamuna-quant-cli
3
- Version: 0.1.2
3
+ Version: 0.1.6
4
4
  Summary: Hamuna A 股回测 + 实盘统一 CLI (akquant 0.3.x). 替代 v1 自建 driver + QMT-style 策略规范.
5
5
  Author-email: Hamuna Team <team@hamuna.example>
6
6
  License: MIT
@@ -12,4 +12,4 @@ server (13-key metrics schema). 全部走 akquant 0.3.x 引擎.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
- __version__ = "0.1.2"
15
+ __version__ = "0.1.6"
@@ -7,7 +7,7 @@ Usage:
7
7
  hamuna_quant_cli create --name ... --config ...
8
8
  hamuna_quant_cli parity [--strategies ...]
9
9
  hamuna_quant_cli qmt-translate <strategy.py> <cfg.json>
10
- hamuna_quant_cli commit <strategy_id> --strategy ... --result ...
10
+ hamuna_quant_cli commit <strategy_id> --strategy ... --result ... --config ... --params ...
11
11
  hamuna_quant_cli dataset {list|fetch|manifest}
12
12
  hamuna_quant_cli live run <strategy.py> --mode paper --broker qmt ...
13
13
 
@@ -288,14 +288,20 @@ def cmd_qmt_translate(args) -> int:
288
288
  print(f"QMT body saved → {out}", file=sys.stderr)
289
289
  import json
290
290
  print(json.dumps({"qmt_body_path": str(out), "note":
291
- "本地 stub; cloud export 端点会重新渲染 + 签 cert + 嵌入 embedded API key"},
291
+ "本地 stub — Round 18.42 起 cloud 不再渲染 .qmt.py, 实盘由 desktop 落盘原始 .py → bridge_server 执行"},
292
292
  ensure_ascii=False))
293
293
  return 0
294
294
 
295
295
 
296
296
  def cmd_commit(args) -> int:
297
- """commiter 5 步打包上传 (qmt-translate + bundle + PUT result + POST code + POST export)."""
298
- from .qmt_translator import write_qmt_body
297
+ """commiter 3 步打包上传 (bundle + PUT result + POST code).
298
+
299
+ Round 18.42: POST /strategies/:id/export 端点已废, 不再生成 .qmt.py shell —
300
+ server 仅存策略源码. QMT 实盘由 desktop deploy_strategy 落盘原始 .py →
301
+ bridge_server 扫描执行; hamuna-quant-cli 下单走 bridge_server HTTP.
302
+ bundle = 4 文件 (strategy + result + config + params). multipart params 字段
303
+ = 整个 params.json 内容 (ADR-0025, desktop 实盘配置源).
304
+ """
299
305
  from . import runtime
300
306
  import json
301
307
  import tarfile
@@ -314,35 +320,26 @@ def cmd_commit(args) -> int:
314
320
  if cfg_path and not cfg_path.exists():
315
321
  print(f"config.json 不存在: {cfg_path}", file=sys.stderr)
316
322
  return 2
317
- spec_path = Path(args.spec) if args.spec else None
318
- if spec_path and not spec_path.exists():
319
- print(f"spec_strategy.json 不存在: {spec_path} (--spec 可选)", file=sys.stderr)
320
- spec_path = None
323
+ params_path = Path(args.params) if args.params else None
324
+ if params_path and not params_path.exists():
325
+ print(f"params.json 不存在: {params_path} (--params 可选)", file=sys.stderr)
326
+ return 2
321
327
 
322
328
  bundle_out = Path(args.bundle_out) if args.bundle_out else Path(f"runs/{sid}/bundle.tar.gz")
323
329
  bundle_out.parent.mkdir(parents=True, exist_ok=True)
324
330
 
325
- print("=== Step 1/5: akquant → QMT body 翻译 (本地 stub) ===", file=sys.stderr)
326
- try:
327
- qmt_body_path = write_qmt_body(strategy_path, cfg_path, spec_path)
328
- except NotImplementedError as e:
329
- print(f" ✗ 不支持: {e}", file=sys.stderr)
330
- return 3
331
- print(f" ✓ qmt_body = {qmt_body_path}", file=sys.stderr)
332
-
333
- print("=== Step 2/5: 打包 bundle (strategy + qmt_body + metrics + spec) ===",
331
+ print("=== Step 1/3: 打包 bundle (strategy + result + config + params) ===",
334
332
  file=sys.stderr)
335
333
  with tarfile.open(bundle_out, "w:gz") as tar:
336
334
  tar.add(strategy_path, arcname=strategy_path.name)
337
- tar.add(qmt_body_path, arcname=qmt_body_path.name)
338
335
  tar.add(result_path, arcname=result_path.name)
339
- if spec_path:
340
- tar.add(spec_path, arcname=spec_path.name)
341
336
  if cfg_path:
342
337
  tar.add(cfg_path, arcname=cfg_path.name)
338
+ if params_path:
339
+ tar.add(params_path, arcname=params_path.name)
343
340
  print(f" ✓ bundle = {bundle_out} ({bundle_out.stat().st_size} bytes)", file=sys.stderr)
344
341
 
345
- print("=== Step 3/5: PUT /strategies/:id/result (metrics) ===", file=sys.stderr)
342
+ print("=== Step 2/3: PUT /strategies/:id/result (metrics) ===", file=sys.stderr)
346
343
  try:
347
344
  result = json.loads(result_path.read_text(encoding="utf-8"))
348
345
  except json.JSONDecodeError as e:
@@ -355,47 +352,27 @@ def cmd_commit(args) -> int:
355
352
  return 4
356
353
  print(f" ✓ metrics uploaded (resp keys: {sorted(resp.keys())[:5]})", file=sys.stderr)
357
354
 
358
- print("=== Step 4/5: POST /strategies/:id/code (multipart body + params) ===",
355
+ print("=== Step 3/3: POST /strategies/:id/code (multipart body + params) ===",
359
356
  file=sys.stderr)
360
- params: dict = {}
361
- if spec_path:
362
- params["spec_strategy"] = json.loads(spec_path.read_text(encoding="utf-8"))
363
- if cfg_path:
364
- params["config"] = json.loads(cfg_path.read_text(encoding="utf-8"))
357
+ params: dict | None = None
358
+ if params_path:
359
+ params = json.loads(params_path.read_text(encoding="utf-8"))
365
360
  try:
366
- resp_code = runtime.server_client.upload_strategy_code(sid, str(qmt_body_path),
367
- params=params or None)
361
+ resp_code = runtime.server_client.upload_strategy_code(sid, str(strategy_path),
362
+ params=params)
368
363
  except runtime.server_client.ServerError as e:
369
364
  print(f" ✗ 上传源码失败: {e}", file=sys.stderr)
370
365
  return 4
371
366
  print(f" ✓ body uploaded (body_hash={resp_code.get('body_hash', '?')[:16]})",
372
367
  file=sys.stderr)
373
368
 
374
- export_path = None
375
- if not args.skip_export:
376
- print("=== Step 5/5: POST /strategies/:id/export (拉 .qmt.py shell) ===",
377
- file=sys.stderr)
378
- export_out = Path(args.export_out) if args.export_out else Path(
379
- f"runs/{sid}/{sid}.qmt.py"
380
- )
381
- export_out.parent.mkdir(parents=True, exist_ok=True)
382
- try:
383
- resp_exp = runtime.server_client.export_strategy(sid, str(export_out))
384
- except runtime.server_client.ServerError as e:
385
- print(f" ✗ QMT export 失败: {e}", file=sys.stderr)
386
- return 4
387
- export_path = str(export_out)
388
- print(f" ✓ shell saved → {export_path} "
389
- f"({resp_exp.get('shell_bytes', '?')} bytes)", file=sys.stderr)
390
- else:
391
- print("=== Step 5/5: SKIPPED (--skip-export) ===", file=sys.stderr)
369
+ # Round 18.42: step 4 (POST /strategies/:id/export) 已废. QMT shell 不再
370
+ # server 渲染, 无 export_path 产出. 实盘落盘由 desktop 负责.
392
371
 
393
372
  print('', file=sys.stderr)
394
373
  print(json.dumps({
395
374
  "strategy_id": sid,
396
375
  "bundle_path": str(bundle_out),
397
- "qmt_body_path": str(qmt_body_path),
398
- "export_path": export_path,
399
376
  "verdict": "OK",
400
377
  }, ensure_ascii=False, indent=2))
401
378
  return 0
@@ -928,16 +905,14 @@ def build_parser() -> argparse.ArgumentParser:
928
905
  qt.add_argument("--output", help="QMT body 落盘路径")
929
906
  qt.set_defaults(func=cmd_qmt_translate)
930
907
 
931
- cm = sub.add_parser("commit", help="commiter 5 步打包上传")
908
+ cm = sub.add_parser("commit", help="commiter 3 步打包上传 (bundle + PUT result + POST code)")
932
909
  cm.add_argument("strategy_id", help="server 端 strategy_id")
933
910
  cm.add_argument("--strategy", help="akquant strategy .py 路径 (默认 <id>.py)")
934
911
  cm.add_argument("--result", required=True, help="metrics result.json 路径")
935
912
  cm.add_argument("--config", help="config.json 路径")
936
- cm.add_argument("--spec", help="spec_strategy.json 路径")
913
+ cm.add_argument("--params", default=None,
914
+ help="params.json 路径 (固定 schema; 整个内容作 multipart params 字段, desktop 实盘配置源)")
937
915
  cm.add_argument("--bundle-out", help="bundle tar.gz 落盘路径")
938
- cm.add_argument("--skip-export", action="store_true",
939
- help="跳过 step 5 (POST /strategies/:id/export)")
940
- cm.add_argument("--export-out", help="step 5 输出 .qmt.py 路径")
941
916
  cm.set_defaults(func=cmd_commit)
942
917
 
943
918
  d = sub.add_parser("dataset", help="构建/列出 prebuilt 数据集")
@@ -323,9 +323,11 @@ def run_akquant_backtest(strategy_path: str, cfg: dict) -> dict:
323
323
  for k, v in validated.items():
324
324
  if k not in accepted_fields:
325
325
  continue
326
- # 不覆盖 runner 已注入的 universe/exclude_symbols (它们来自 cfg, 与
327
- # params.json 的 backtest 段同源; validate 返回的默认 [] 会误覆盖).
328
- if k in kwargs:
326
+ # 不覆盖 runner 已注入的 universe/exclude_symbols **非空值** (它们来自 cfg,
327
+ # 与 params.json 的 backtest 段同源; validate 返回的默认 [] 会误覆盖).
328
+ # 空 list 时允许 params.json 覆盖 — 否则 --params strategy_params.exclude_symbols
329
+ # 永远被 cfg 空注入跳过, 隔离参数静默失效 (Round 18.35 真回测踩坑).
330
+ if kwargs.get(k):
329
331
  continue
330
332
  kwargs[k] = v
331
333
 
@@ -3,8 +3,9 @@
3
3
  设计意图 (per `skills/hamuna-strategy-v2/references/qmt-export.md` §转换):
4
4
  - body 用 QMT 原生签名 (`def init(C)` + `def handlebar(C)` + `passorder`/`C.get_market_data_ex`)
5
5
  - CLI 端 driver / Context 适配 (v2 时代由 akquant 适配) — 双跑靠 "body 零翻译 + shim 状态代理"
6
- - 本地 translator **仅产出 audit/对照版 QMT body**, 真值由 server 端
7
- `POST /api/v1/strategies/:id/export` 重新渲染 shell + 嵌入 embedded API key + 签 cert
6
+ - 本地 translator **仅产出 audit/对照版 QMT body**. Round 18.42 起 server 端
7
+ `/strategies/:id/export` 端点已删除 — 实盘由 desktop `deploy_strategy` 落盘
8
+ 原始 .py → bridge_server 扫描执行, 下单由 hamuna-quant-cli 调 bridge_server.
8
9
 
9
10
  **支持的 API 映射** (8 类, 够覆盖 BuyHold / DualMA / LowVolTopK / CrossSection 主流形态):
10
11
  - `self.subscribe(sym)` → `C.subscribe(sym)`
@@ -1,13 +1,15 @@
1
1
  """strategy_cli.runtime.server_client (hamuna-strategy-v2): CLI → server 上传 (backtest result).
2
2
 
3
3
  v2 精简底座: 仅保留 _api_call + upload_backtest_result + create_strategy;
4
- 扩展部分 (commiter 5 步打包上传需要):
4
+ 扩展部分 (commiter 3 步打包上传需要):
5
5
  - upload_strategy_code POST /strategies/{id}/code (multipart body + params)
6
- - export_strategy POST /strategies/{id}/export (拉 server 端 .qmt.py shell)
7
6
 
8
- 不进 server_client 的 (不在 skill 端做): pyarmor / embedded API key / cert / QMT
9
- shell 渲染 — 这些都跑在 server 端 (ADR-0023 + ADR-0025), cloud 是 single source
10
- of truth. 本地仅做 multipart 拼 + 提交, 不碰加密 / 不读 cert.
7
+ 不进 server_client 的 (不在 skill 端做): pyarmor / QMT shell 渲染 — 这些都跑在
8
+ server 端 (ADR-0025), cloud 是 single source of truth. 本地仅做 multipart 拼 +
9
+ 提交, 不碰加密 / 不读 shell. Round 18.40: 无 embedded API key / 无 cert 签 —
10
+ 策略体明文上传 + 服务器 inline 渲染 (base64 in template). Round 18.42 起: server
11
+ 不再生成 QMT shell (.qmt.py), desktop 落盘原始 .py → bridge_server 执行 →
12
+ hamuna-quant-cli 调 bridge_server 下单; commiter 4 步 → 3 步 (去掉 export).
11
13
 
12
14
  底层鉴权 / envelope 解构 / NaN 清洗与 v1 完全一致 (wholesale copy → 极简裁剪, 见 v1
13
15
  server_client.py git blame); 服务端契约 backend/internal/model/model.go::BacktestResult
@@ -83,9 +85,10 @@ def upload_backtest_result(strategy_id: str, result: dict) -> dict:
83
85
  def create_strategy(name: str, code: str = '', params: dict | None = None) -> dict:
84
86
  """POST /api/v1/strategies → 新建 strategy (供 _upload_after_run --name auto-create 用).
85
87
 
86
- name 必填 (id 一致性锚点, 与 v1 server_client 同); code 可选 (后端落库 +
87
- /source envelope 加密用, v2 --upload --code 路径才填); params 默认 {} (作
88
- BacktestResult.params 落库). 返 server DataValue (含 _id / id / name).
88
+ name 必填 (id 一致性锚点, 与 v1 server_client 同); code 可选 (Round 18.41+
89
+ 后端落 body 字段 UTF-8 verbatim, 无 envelope, v2 --upload --code 路径才填);
90
+ params 默认 {} (作 BacktestResult.params 落库). 返 server DataValue
91
+ (含 _id / id / name).
89
92
  """
90
93
  body: dict = {'name': name, 'code': code}
91
94
  if params is not None:
@@ -100,19 +103,21 @@ def upload_strategy_code(
100
103
  ) -> dict:
101
104
  """POST /api/v1/strategies/{id}/code (multipart) → 上传策略源码 + params.
102
105
 
103
- ADR-0023: 后端存 code_obfuscated = body bytes (GBK 编码); cert + subscription gate
104
- 是真保护, 不在 skill 端做加密/pyminifier.
105
- ADR-0025: multipart 含 `params` 字段 (JSON 字符串), server 落库覆盖 strategies.params
106
- (CONFIG 块渲染源).
106
+ Round 18.43+: 明文上传 UTF-8 verbatim, server 存到 `body` 字段. Round 18.42
107
+ 的 GBK 编码约定已废 (用户 grill: 「明文上传」字面理解应是 UTF-8, 不应在
108
+ client/server 间夹 GBK 转码; 「明文」指不加密, 编码另说). 老 doc (Round
109
+ 18.42 之前上传) 走 CodeObfuscated GBK fallback, server 读时 GBK→UTF-8 解码.
110
+ ADR-0025: multipart 含 `params` 字段 (JSON 字符串), server 落库覆盖
111
+ strategies.params (CONFIG 块渲染源).
107
112
 
108
- body bytes = UTF-8 源 → GBK (per `backend/internal/handler/qmt_export.go::storeBody`
109
- 注释 "Strategy files are GBK-encoded, server reads GBK first fall back UTF-8");
110
- 无法 GBK 编码的字符 → errors='replace' (不阻断上传).
113
+ body bytes = UTF-8 源 verbatim (per `backend/internal/handler/qmt_export.go::storeBody`
114
+ Round 18.43+: "客户端直传 UTF-8 (code.encode('utf-8')), server passthrough
115
+ 存到 `body` 字段; 不再做 GBK 转码").
111
116
 
112
117
  返 {body_hash, ok} (per handler::UploadStrategyCode 响应 shape).
113
118
  """
114
119
  code = Path(strategy_path).read_text(encoding='utf-8')
115
- gbk = code.encode('gbk', errors='replace')
120
+ utf8 = code.encode('utf-8')
116
121
  boundary = f'----hamuna-{uuid.uuid4().hex}'
117
122
  filename = Path(strategy_path).name
118
123
  ctype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
@@ -121,7 +126,7 @@ def upload_strategy_code(
121
126
  (f'--{boundary}\r\n'
122
127
  f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
123
128
  f'Content-Type: {ctype}\r\n\r\n').encode('utf-8'),
124
- gbk,
129
+ utf8,
125
130
  ]
126
131
  if params is not None:
127
132
  # multipart 表单字段: params = JSON 字符串 (per ADR-0025 + handler::storeBody)
@@ -196,61 +201,10 @@ def s3_credentials(dataset_id: str, key: str) -> dict:
196
201
  )
197
202
 
198
203
 
199
- def export_strategy(strategy_id: str, output_path: str | None = None) -> dict:
200
- """POST /api/v1/strategies/{id}/export → 拉 cloud 渲染的 .qmt.py shell.
201
-
202
- ADR-0023: cloud 是 single source of truth — shell 渲染 (CONFIG 块 + embedded
203
- API key + cert 签) 全在 server 端. 本函数仅下载 + 落盘, 不读 shell 内容.
204
-
205
- 返 dict: {shell_path, shell_bytes} (shell_path = output_path 或 None).
206
- """
207
- token = _load_token()
208
- url = f'{_resolve_server()}/api/v1/strategies/{strategy_id}/export'
209
- req = Request(
210
- url, data=b'', method='POST',
211
- headers={
212
- 'Authorization': f'Bearer {token}',
213
- 'Accept': 'application/octet-stream, application/json',
214
- },
215
- )
216
- try:
217
- with urlopen(req, timeout=60) as resp:
218
- # server 既可返 raw .qmt.py 也可返 JSON envelope — 读 Content-Type 判
219
- ctype = resp.headers.get('Content-Type', '')
220
- raw = resp.read()
221
- if 'json' in ctype.lower():
222
- envelope = json.loads(raw.decode('utf-8'))
223
- if isinstance(envelope, dict) and 'DataValue' in envelope:
224
- if not envelope.get('Result', False):
225
- raise ServerError(
226
- f'QMT export 业务失败: Error={envelope.get("Error")} '
227
- f'Message={envelope.get("Message")}'
228
- )
229
- # DataValue 可能是 base64 编码的 shell 字节
230
- import base64
231
- shell_bytes = base64.b64decode(envelope['DataValue'])
232
- else:
233
- shell_bytes = raw
234
- else:
235
- shell_bytes = raw
236
- except HTTPError as e:
237
- resp_body = e.read().decode('utf-8', errors='replace')[:500]
238
- if e.code == 401:
239
- raise ServerError(
240
- f'QMT export 失败: HTTP 401 — API key 无效或已失效'
241
- ) from e
242
- if e.code == 402:
243
- raise ServerError(
244
- f'QMT export 失败: HTTP 402 — 订阅未激活 (subscription gate)'
245
- ) from e
246
- raise ServerError(
247
- f'QMT export 失败: HTTP {e.code} {e.reason}; body={resp_body}'
248
- ) from e
249
-
250
- if output_path:
251
- Path(output_path).write_bytes(shell_bytes)
252
- return {'shell_path': str(output_path), 'shell_bytes': len(shell_bytes)}
253
- return {'shell_path': None, 'shell_bytes': len(shell_bytes)}
204
+ # Round 18.42: export_strategy 删 — /strategies/:id/export 端点已废.
205
+ # QMT shell 渲染整体从 server 撤出: desktop deploy_strategy 落盘原始 .py →
206
+ # bridge_server 扫描执行; 实盘下单走 hamuna-quant-cli → bridge_server HTTP.
207
+ # 如需 audit 渲染, 走 skill 镜像步骤(本仓库不维护 client).
254
208
 
255
209
 
256
210
  def _sanitize_for_json(obj):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hamuna-quant-cli
3
- Version: 0.1.2
3
+ Version: 0.1.6
4
4
  Summary: Hamuna A 股回测 + 实盘统一 CLI (akquant 0.3.x). 替代 v1 自建 driver + QMT-style 策略规范.
5
5
  Author-email: Hamuna Team <team@hamuna.example>
6
6
  License: MIT
@@ -34,5 +34,4 @@ hamuna_quant_cli/runtime/http_client.py
34
34
  hamuna_quant_cli/runtime/s3client.py
35
35
  hamuna_quant_cli/runtime/server_client.py
36
36
  hamuna_quant_cli/scripts/server.json
37
- tests/test_live_safety.py
38
- tests/test_skeleton.py
37
+ tests/test_live_safety.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hamuna-quant-cli"
7
- version = "0.1.2"
7
+ version = "0.1.6"
8
8
  description = "Hamuna A 股回测 + 实盘统一 CLI (akquant 0.3.x). 替代 v1 自建 driver + QMT-style 策略规范."
9
9
  readme = "hamuna_quant_cli/README.md"
10
10
  requires-python = ">=3.10"
@@ -1,154 +0,0 @@
1
- """ADR-0029 §10 验证清单 — Phase 2.3 自检。"""
2
- from __future__ import annotations
3
-
4
- import sys
5
- import numpy as np
6
-
7
- sys.path.insert(0, 'D:/Coding/hamuna-strategy-platform')
8
-
9
- from strategy_cli.runtime.context import Context
10
- from strategy_cli.runtime.driver import run_backtest
11
- from strategy_cli.runtime.score import winsorize, zscore, composite_score
12
- from strategy_cli.fundamental.data import pool, derived
13
-
14
-
15
- # ---- §10 验证 1: init() 幂等 ----
16
- _BASE_CFG = {
17
- 'stock': None, 'period': '1d', 'account_type': 'STOCK',
18
- 'init_capital': 1_000_000, 'backtest_start': '20240101', 'backtest_end': '20240131',
19
- 'pool': {'a_share': {'enabled': True, 'sub_universe': 'hs300'},
20
- 'convertible_bond': {'enabled': False}, 'etf': {'enabled': False}},
21
- 'rebalance_freq': 'weekly', 'rebalance_dow': 4, 'rebalance_dom': -1,
22
- 'filters': [], 'factors': [], 'scoring': {'top_n': 5},
23
- 'cost': {'commission_bps': 30, 'stamp_tax_bps': 100, 'min_commission': 5,
24
- 'slippage_bps': 5, 'volume_cap_pct': 0.10},
25
- 'risk': {'single_holding_cap_pct': 0.20, 'sector_cap_pct': 0.30,
26
- 'stop_loss_pct': -0.08, 'max_drawdown_pct': -0.20},
27
- }
28
-
29
- _BODY_PATH = 'D:/Coding/hamuna-strategy-platform/examples/strategies/pe_pb_mom_top10.py'
30
-
31
-
32
- def _load_body(config: dict):
33
- """通过 driver 加载 body, 注入 CONFIG。"""
34
- import importlib.util
35
- import sys as _sys
36
- spec = importlib.util.spec_from_file_location('_hamuna_strat', _BODY_PATH)
37
- if spec is None or spec.loader is None:
38
- raise ImportError(f'cannot load {_BODY_PATH}')
39
- mod = importlib.util.module_from_spec(spec)
40
- _sys.modules['_hamuna_strat'] = mod
41
- spec.loader.exec_module(mod)
42
- mod.__dict__['CONFIG'] = config
43
- return mod
44
-
45
-
46
- def test_init_idempotent():
47
- cfg = dict(_BASE_CFG)
48
- C1 = Context(period='1d', start='20240101', end='20240131')
49
- C1._trading_days = ['20240102', '20240103']
50
- body = _load_body(cfg)
51
- body.init(C1)
52
- snap1 = dict(C1.snapshot)
53
-
54
- C2 = Context(period='1d', start='20240101', end='20240131')
55
- C2._trading_days = ['20240102', '20240103']
56
- body.init(C2)
57
- snap2 = dict(C2.snapshot)
58
-
59
- assert snap1 == snap2, 'init() 应幂等'
60
- assert len(C1.pool_a) == 20, f'池大小异常: {len(C1.pool_a)}'
61
- print(' [OK] init() 幂等 + 池大小正确')
62
-
63
-
64
- # ---- §10 验证 2: 非 rebalance bar 不下单 ----
65
- def test_handlebar_no_order_on_non_rebalance():
66
- cfg = dict(_BASE_CFG)
67
- cfg['rebalance_freq'] = 'monthly'
68
- C = Context(period='1d', start='20240101', end='20240131')
69
- C._trading_days = ['20240102', '20240103', '20240131']
70
- body = _load_body(cfg)
71
- body.init(C)
72
- C.cur_bar_idx = 0
73
- C.cur_date = '20240102'
74
- pending_before = len(getattr(C, '_pending_orders', []))
75
- body.handlebar(C)
76
- pending_after = len(getattr(C, '_pending_orders', []))
77
- assert pending_after == pending_before, '非 rebalance bar 不应产生 pending_orders'
78
- print(' [OK] 非 rebalance bar 不下单')
79
-
80
-
81
- # ---- §10 验证 3: winsorize + zscore 不抛 ----
82
- def test_score_no_crash_on_all_nan():
83
- arr = np.array([np.nan, np.nan, np.nan])
84
- out = winsorize(arr)
85
- assert np.isnan(out).all()
86
- z = zscore(arr)
87
- assert (z == 0).all(), f'全 NaN zscore 应为 0, got {z}'
88
- factors = [{'name': 'x', 'direction': 'low', 'weight': 1.0}]
89
- score = composite_score({'x': arr}, factors)
90
- assert (score == 0).all()
91
- print(' [OK] 全 NaN winsorize/zscore 不抛')
92
-
93
-
94
- # ---- §10 验证 4: _normalize_weights 上限生效 ----
95
- def test_normalize_cap():
96
- """权重归一化 + cap 截断 + 二次归一化; 测试每阶段值。"""
97
- from examples.strategies.pe_pb_mom_top10 import _normalize_weights
98
- top = [('A', 0.5), ('B', 0.3), ('C', 0.2)]
99
- # cap=0.4: A 截断到 0.4; sum(capped)=0.9; 二次归一化 0.4/0.9=0.444, 0.3/0.9=0.333
100
- w = _normalize_weights(top, cap_pct=0.4)
101
- assert abs(w['A'] - 0.444) < 1e-3, w
102
- assert abs(sum(w.values()) - 1.0) < 1e-9, sum(w.values())
103
- # cap=0.2: 全部被截断, A=B=C=1/3
104
- w = _normalize_weights(top, cap_pct=0.2)
105
- assert abs(w['A'] - 1/3) < 1e-9, w
106
- assert abs(sum(w.values()) - 1.0) < 1e-9, sum(w.values())
107
- print(f' [OK] cap 生效 (cap=0.4): A={w["A"]:.3f} B={w["B"]:.3f} C={w["C"]:.3f}')
108
-
109
-
110
- # ---- §10 验证 5: suspended 守门 ----
111
- def test_suspended_skip():
112
- cfg = dict(_BASE_CFG)
113
- C = Context(period='1d', start='20240101', end='20240131')
114
- C._trading_days = ['20240102', '20240131']
115
- body = _load_body(cfg)
116
- body.init(C)
117
- C.suspended = True
118
- C.cur_bar_idx = 1
119
- C.cur_date = '20240131'
120
- pending_before = len(getattr(C, '_pending_orders', []))
121
- body.handlebar(C)
122
- pending_after = len(getattr(C, '_pending_orders', []))
123
- assert pending_after == pending_before, 'suspended=True 时 handlebar 必须 return'
124
- print(' [OK] suspended=True handlebar 守门')
125
-
126
-
127
- # ---- 端到端: 跑一次完整回测 ----
128
- def test_e2e_run():
129
- cfg = dict(_BASE_CFG)
130
- cfg['filters'] = [
131
- {'field': 'derived.pe_dynamic', 'op': 'gt', 'value': 0},
132
- {'field': 'derived.pe_dynamic', 'op': 'lt', 'value': 50},
133
- ]
134
- cfg['factors'] = [
135
- {'name': 'pe', 'field': 'derived.pe_dynamic', 'direction': 'low', 'weight': 0.4},
136
- {'name': 'pb', 'field': 'derived.pb_ratio', 'direction': 'low', 'weight': 0.3},
137
- {'name': 'mom', 'field': 'market.close_20d_change', 'direction': 'high', 'weight': 0.3},
138
- ]
139
- result = run_backtest(_BODY_PATH, cfg)
140
- print(f' [OK] e2e 跑通: trading_days={result["trading_days"]}, final_nav={result["final_nav"]:.0f}')
141
- assert result['trading_days'] > 0
142
-
143
-
144
- if __name__ == '__main__':
145
- print('ADR-0029 §10 验证清单')
146
- print('-' * 40)
147
- test_init_idempotent()
148
- test_handlebar_no_order_on_non_rebalance()
149
- test_score_no_crash_on_all_nan()
150
- test_normalize_cap()
151
- test_suspended_skip()
152
- test_e2e_run()
153
- print('-' * 40)
154
- print('全部通过 [OK]')