hamuna-quant-cli 0.1.10__tar.gz → 0.1.11__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 (39) hide show
  1. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/PKG-INFO +1 -1
  2. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/__init__.py +1 -1
  3. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/__main__.py +51 -3
  4. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/server_client.py +23 -4
  5. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
  6. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/pyproject.toml +1 -1
  7. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/MANIFEST.in +0 -0
  8. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/README.md +0 -0
  9. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/README.md +0 -0
  10. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_market_fallback.py +0 -0
  11. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_metrics_15.py +0 -0
  12. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
  13. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
  14. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_runner.py +0 -0
  15. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
  16. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/base_strategy.py +0 -0
  17. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
  18. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/__init__.py +0 -0
  19. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/loader.py +0 -0
  20. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/qmt_broker.py +0 -0
  21. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/qmt_market.py +0 -0
  22. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/runner.py +0 -0
  23. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
  24. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
  25. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/qmt_translator.py +0 -0
  26. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/__init__.py +0 -0
  27. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/backtest.py +0 -0
  28. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/cache.py +0 -0
  29. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/discipline.py +0 -0
  30. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/http_client.py +0 -0
  31. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/s3client.py +0 -0
  32. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/scripts/server.json +0 -0
  33. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
  34. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
  35. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
  36. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/requires.txt +0 -0
  37. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
  38. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/setup.cfg +0 -0
  39. {hamuna_quant_cli-0.1.10 → hamuna_quant_cli-0.1.11}/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.10
3
+ Version: 0.1.11
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.10"
15
+ __version__ = "0.1.11"
@@ -335,10 +335,18 @@ def cmd_commit(args) -> int:
335
335
  Round 18.62 P0-1: 新增 step 1.5 replay diff gate — `--replay-result` 必填,
336
336
  与 `--result` 算 sharpe diff, 超阈值 (HAMUNA_REPLAY_GATE_PCT 缺省 0.20) → exit 5.
337
337
  server `UpdateStrategyResult` handler 二次校验防绕过.
338
+
339
+ Round 18.62 P1-1: 新增 step 1.4 version bump — `GET /strategies/{id}` 拿
340
+ `latest_version` (无 → "0.0.0"); 默认 patch 自动 bump (`0.0.0 → 0.0.1`),
341
+ `--major` → `X+1.0.0`, `--minor` → `X.Y+1.0`. `--major + --minor` 互斥.
342
+ server handler 强校验 semver 格式 + 严格递增; PUT body 必含 `version`.
338
343
  """
339
344
  from . import runtime
345
+ from .runtime.server_client import ServerError
346
+ import io
340
347
  import json
341
348
  import os
349
+ import re
342
350
  import tarfile
343
351
 
344
352
  sid = args.strategy_id
@@ -370,10 +378,36 @@ def cmd_commit(args) -> int:
370
378
  print(f"replay_result.json 不存在: {args.replay_result}", file=sys.stderr)
371
379
  return 2
372
380
 
381
+ # ----- Round 18.62 P1-1 step 1.4: version bump -----
382
+ # cli 先 GET /strategies/{id} 拿 latest_version (无 → "0.0.0"); 默认 patch 自动 bump,
383
+ # --major → X+1.0.0, --minor → X.Y+1.0. 互斥校验防呆. server 二次校验 (semver 格式 +
384
+ # 严格递增) 防绕过. PUT body 必含 version (P1-1 server 强校验).
385
+ if args.major and args.minor:
386
+ print("ERROR: --major 与 --minor 互斥, 选一个或不传 (默认 patch)", file=sys.stderr)
387
+ return 2
388
+ try:
389
+ strat = runtime.server_client.get_strategy(sid)
390
+ latest = (strat or {}).get("latest_version") or "0.0.0"
391
+ except ServerError:
392
+ latest = "0.0.0" # 404 / 401 等都 fallback, 新策略首次 commit
393
+ if not re.fullmatch(r"\d+\.\d+\.\d+", latest):
394
+ print(f"ERROR: server latest_version '{latest}' 不是合法 semver (X.Y.Z), "
395
+ f"请手动清理 strategies.latest_version 后重试", file=sys.stderr)
396
+ return 2
397
+ major, minor, patch = (int(x) for x in latest.split("."))
398
+ if args.major:
399
+ new_version = f"{major + 1}.0.0"
400
+ elif args.minor:
401
+ new_version = f"{major}.{minor + 1}.0"
402
+ else:
403
+ new_version = f"{major}.{minor}.{patch + 1}"
404
+ print(f"=== Step 1.4/3: P1-1 version bump ({latest} → {new_version}) ===",
405
+ file=sys.stderr)
406
+
373
407
  bundle_out = Path(args.bundle_out) if args.bundle_out else Path(f"runs/{sid}/bundle.tar.gz")
374
408
  bundle_out.parent.mkdir(parents=True, exist_ok=True)
375
409
 
376
- print("=== Step 1/3: 打包 bundle (strategy + result + config + params) ===",
410
+ print("=== Step 1/3: 打包 bundle (strategy + result + config + params + replay + version) ===",
377
411
  file=sys.stderr)
378
412
  with tarfile.open(bundle_out, "w:gz") as tar:
379
413
  tar.add(strategy_path, arcname=strategy_path.name)
@@ -385,6 +419,11 @@ def cmd_commit(args) -> int:
385
419
  # Round 18.62 P0-1: replay_result.json 也打进去, 方便回溯 / 审计.
386
420
  if replay_path:
387
421
  tar.add(replay_path, arcname=replay_path.name)
422
+ # Round 18.62 P1-1: version.json 内存写, audit 用 (server 不读, 仅本地回溯).
423
+ ver_bytes = json.dumps({"version": new_version}, ensure_ascii=False).encode("utf-8")
424
+ ver_info = tarfile.TarInfo(name="version.json")
425
+ ver_info.size = len(ver_bytes)
426
+ tar.addfile(ver_info, io.BytesIO(ver_bytes))
388
427
  print(f" ✓ bundle = {bundle_out} ({bundle_out.stat().st_size} bytes)", file=sys.stderr)
389
428
 
390
429
  # ----- Round 18.62 P0-1 step 1.5: replay diff gate -----
@@ -420,14 +459,17 @@ def cmd_commit(args) -> int:
420
459
  print(f" ✓ replay gate pass (diff={diff_pct:.2%} <= {threshold:.0%}, "
421
460
  f"replay.sharpe={rp_sharpe:.4f})", file=sys.stderr)
422
461
 
423
- print("=== Step 2/3: PUT /strategies/:id/result (metrics + replay_metrics) ===", file=sys.stderr)
462
+ print("=== Step 2/3: PUT /strategies/:id/result (metrics + replay_metrics + version) ===",
463
+ file=sys.stderr)
424
464
  try:
425
465
  result = json.loads(result_path.read_text(encoding="utf-8"))
426
466
  except json.JSONDecodeError as e:
427
467
  print(f" ✗ result.json 解析失败: {e}", file=sys.stderr)
428
468
  return 2
429
469
  try:
430
- resp = runtime.server_client.upload_backtest_result(sid, result, replay_metrics=replay_metrics)
470
+ resp = runtime.server_client.upload_backtest_result(
471
+ sid, result, replay_metrics=replay_metrics, version=new_version,
472
+ )
431
473
  except runtime.server_client.ServerError as e:
432
474
  print(f" ✗ 上传 metrics 失败: {e}", file=sys.stderr)
433
475
  return 4
@@ -1000,6 +1042,12 @@ def build_parser() -> argparse.ArgumentParser:
1000
1042
  "或 --skip-replay-gate 跳过)")
1001
1043
  cm.add_argument("--skip-replay-gate", action="store_true",
1002
1044
  help="[qa 临时] 跳过 replay diff gate (非 dev 默认)")
1045
+ # Round 18.62 P1-1: version bump. 默认 patch 自动 (从 server latest_version +0.0.1),
1046
+ # 显式 --major / --minor opt-in. 互斥.
1047
+ cm.add_argument("--major", action="store_true",
1048
+ help="[P1-1] major bump (X+1.0.0); 跟 --minor 互斥")
1049
+ cm.add_argument("--minor", action="store_true",
1050
+ help="[P1-1] minor bump (X.Y+1.0); 跟 --major 互斥")
1003
1051
  cm.add_argument("--config", help="config.json 路径")
1004
1052
  cm.add_argument("--params", default=None,
1005
1053
  help="params.json 路径 (固定 schema; 整个内容作 multipart params 字段, desktop 实盘配置源)")
@@ -72,19 +72,38 @@ def _api_call(method: str, path: str, body: dict | None = None) -> dict:
72
72
  return envelope
73
73
 
74
74
 
75
- def upload_backtest_result(strategy_id: str, result: dict, replay_metrics: dict | None = None) -> dict:
76
- """PUT /api/v1/strategies/{id}/result → 上传 BacktestResult-shape dict + 可选 replay_metrics.
75
+ def upload_backtest_result(strategy_id: str, result: dict, replay_metrics: dict | None = None, version: str | None = None) -> dict:
76
+ """PUT /api/v1/strategies/{id}/result → 上传 BacktestResult-shape dict + 可选 replay_metrics + version.
77
77
 
78
78
  兼容 akquant_runner.run_akquant_backtest() 输出 (13 顶层 key + metrics 子 dict 15 key + ...),
79
79
  先做轻清洗: NaN/Inf → None (json 标准不允许), 后传给 server.
80
80
 
81
81
  Round 18.62 P0-1: replay_metrics (Sharpe/TotalReturn/WinRate/Trades/DiffPct/RunAt) —
82
82
  跟 result 平级, server handler 二次校验 diff 防绕过. None 时不传该 key (老调用方不破).
83
+ Round 18.62 P1-1: version (X.Y.Z semver) — cli 默认 patch 自动 bump (cmd_commit step 1.4
84
+ 拿 server latest_version 算新 version), server handler 二次校验 semver 格式 + 严格递增.
85
+ 老调用方 (无 version kwarg) → 不传该 key → server 422 (P1-1 强校验).
83
86
  """
84
87
  payload = _sanitize_for_json(result)
88
+ # P1-1 P0-1.1 ponytail: 无条件 wrap `{"result": ...}` 让 server 解析形状一致
89
+ # (即使 replay_metrics=None / version=None 也要 wrap, 防止 server handler 422 invalid shape).
90
+ # 此改动在 P1-1 同步实施, P0-1 Case F 已知问题修复.
91
+ body: dict = {"result": payload}
85
92
  if replay_metrics is not None:
86
- payload = {"result": payload, "replay_metrics": _sanitize_for_json(replay_metrics)}
87
- return _api_call('PUT', f'/api/v1/strategies/{strategy_id}/result', payload)
93
+ body["replay_metrics"] = _sanitize_for_json(replay_metrics)
94
+ if version is not None:
95
+ body["version"] = version
96
+ return _api_call('PUT', f'/api/v1/strategies/{strategy_id}/result', body)
97
+
98
+
99
+ def get_strategy(strategy_id: str) -> dict:
100
+ """GET /api/v1/strategies/{id} → server DataValue (含 latest_version / result / versions[]).
101
+
102
+ Round 18.62 P1-1: cli 端 version auto-bump 需要 latest_version. 404 raise ServerError,
103
+ caller (cmd_commit) fallback "0.0.0" + patch bump. 返 dict (含 latest_version 字段);
104
+ 若 server 返回 DataValue 是 dict 直接返, 是 None 也直接返让 caller 用 .get() 容错.
105
+ """
106
+ return _api_call('GET', f'/api/v1/strategies/{strategy_id}')
88
107
 
89
108
 
90
109
  def create_strategy(name: str, code: str = '', params: dict | None = None) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hamuna-quant-cli
3
- Version: 0.1.10
3
+ Version: 0.1.11
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hamuna-quant-cli"
7
- version = "0.1.10"
7
+ version = "0.1.11"
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"