hamuna-quant-cli 0.1.9__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.
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/__init__.py +1 -1
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/__main__.py +108 -3
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/runner.py +64 -2
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/server_client.py +27 -3
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/pyproject.toml +1 -1
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/MANIFEST.in +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/README.md +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/README.md +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_market_fallback.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_metrics_15.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_runner.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/base_strategy.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/__init__.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/loader.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/qmt_broker.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/live/qmt_market.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/qmt_translator.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/__init__.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/backtest.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/cache.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/discipline.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/http_client.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/s3client.py +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/scripts/server.json +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/requires.txt +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/setup.cfg +0 -0
- {hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/tests/test_live_safety.py +0 -0
|
@@ -331,9 +331,22 @@ def cmd_commit(args) -> int:
|
|
|
331
331
|
bridge_server 扫描执行; hamuna-quant-cli 下单走 bridge_server HTTP.
|
|
332
332
|
bundle = 4 文件 (strategy + result + config + params). multipart params 字段
|
|
333
333
|
= 整个 params.json 内容 (ADR-0025, desktop 实盘配置源).
|
|
334
|
+
|
|
335
|
+
Round 18.62 P0-1: 新增 step 1.5 replay diff gate — `--replay-result` 必填,
|
|
336
|
+
与 `--result` 算 sharpe diff, 超阈值 (HAMUNA_REPLAY_GATE_PCT 缺省 0.20) → exit 5.
|
|
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`.
|
|
334
343
|
"""
|
|
335
344
|
from . import runtime
|
|
345
|
+
from .runtime.server_client import ServerError
|
|
346
|
+
import io
|
|
336
347
|
import json
|
|
348
|
+
import os
|
|
349
|
+
import re
|
|
337
350
|
import tarfile
|
|
338
351
|
|
|
339
352
|
sid = args.strategy_id
|
|
@@ -354,11 +367,47 @@ def cmd_commit(args) -> int:
|
|
|
354
367
|
if params_path and not params_path.exists():
|
|
355
368
|
print(f"params.json 不存在: {params_path} (--params 可选)", file=sys.stderr)
|
|
356
369
|
return 2
|
|
370
|
+
# Round 18.62 P0-1: replay_result.json 必填 (除非 --skip-replay-gate qa 通道).
|
|
371
|
+
replay_path = Path(args.replay_result) if args.replay_result else None
|
|
372
|
+
if not args.skip_replay_gate:
|
|
373
|
+
if replay_path is None:
|
|
374
|
+
print("ERROR: P0-1: 必须传 --replay-result, 先跑 live run --broker replay 拿 metrics "
|
|
375
|
+
"(或 --skip-replay-gate qa 通道)", file=sys.stderr)
|
|
376
|
+
return 2
|
|
377
|
+
if not replay_path.exists():
|
|
378
|
+
print(f"replay_result.json 不存在: {args.replay_result}", file=sys.stderr)
|
|
379
|
+
return 2
|
|
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)
|
|
357
406
|
|
|
358
407
|
bundle_out = Path(args.bundle_out) if args.bundle_out else Path(f"runs/{sid}/bundle.tar.gz")
|
|
359
408
|
bundle_out.parent.mkdir(parents=True, exist_ok=True)
|
|
360
409
|
|
|
361
|
-
print("=== Step 1/3: 打包 bundle (strategy + result + config + params) ===",
|
|
410
|
+
print("=== Step 1/3: 打包 bundle (strategy + result + config + params + replay + version) ===",
|
|
362
411
|
file=sys.stderr)
|
|
363
412
|
with tarfile.open(bundle_out, "w:gz") as tar:
|
|
364
413
|
tar.add(strategy_path, arcname=strategy_path.name)
|
|
@@ -367,16 +416,60 @@ def cmd_commit(args) -> int:
|
|
|
367
416
|
tar.add(cfg_path, arcname=cfg_path.name)
|
|
368
417
|
if params_path:
|
|
369
418
|
tar.add(params_path, arcname=params_path.name)
|
|
419
|
+
# Round 18.62 P0-1: replay_result.json 也打进去, 方便回溯 / 审计.
|
|
420
|
+
if replay_path:
|
|
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))
|
|
370
427
|
print(f" ✓ bundle = {bundle_out} ({bundle_out.stat().st_size} bytes)", file=sys.stderr)
|
|
371
428
|
|
|
372
|
-
|
|
429
|
+
# ----- Round 18.62 P0-1 step 1.5: replay diff gate -----
|
|
430
|
+
# 必在 step 2 (PUT) 之前 — gate fail 后 step 2-3 都不跑, 不留半上传状态.
|
|
431
|
+
replay_metrics: dict | None = None
|
|
432
|
+
if not args.skip_replay_gate:
|
|
433
|
+
try:
|
|
434
|
+
bt = json.loads(result_path.read_text(encoding="utf-8"))
|
|
435
|
+
rp = json.loads(replay_path.read_text(encoding="utf-8")) # type: ignore[union-attr]
|
|
436
|
+
except json.JSONDecodeError as e:
|
|
437
|
+
print(f" ✗ result/replay JSON 解析失败: {e}", file=sys.stderr)
|
|
438
|
+
return 2
|
|
439
|
+
bt_sharpe = float(bt.get("metrics", {}).get("sharpe", 0.0))
|
|
440
|
+
rp_sharpe = float(rp.get("metrics", {}).get("sharpe", 0.0))
|
|
441
|
+
diff_pct = abs(rp_sharpe - bt_sharpe) / max(abs(bt_sharpe), 0.01)
|
|
442
|
+
try:
|
|
443
|
+
threshold = float(os.environ.get("HAMUNA_REPLAY_GATE_PCT", "0.20"))
|
|
444
|
+
except ValueError:
|
|
445
|
+
threshold = 0.20
|
|
446
|
+
if diff_pct > threshold:
|
|
447
|
+
print(f"ERROR: replay gate diff={diff_pct:.2%} > {threshold:.0%} "
|
|
448
|
+
f"(backtest.sharpe={bt_sharpe:.4f} vs replay.sharpe={rp_sharpe:.4f})",
|
|
449
|
+
file=sys.stderr)
|
|
450
|
+
return 5
|
|
451
|
+
replay_metrics = {
|
|
452
|
+
"sharpe": rp_sharpe,
|
|
453
|
+
"total_return": float(rp.get("metrics", {}).get("total_return", 0.0)),
|
|
454
|
+
"win_rate": float(rp.get("metrics", {}).get("win_rate", 0.0)),
|
|
455
|
+
"trades": len(rp.get("trades", [])),
|
|
456
|
+
"diff_pct": diff_pct,
|
|
457
|
+
"run_at": (rp.get("period") or {}).get("end"),
|
|
458
|
+
}
|
|
459
|
+
print(f" ✓ replay gate pass (diff={diff_pct:.2%} <= {threshold:.0%}, "
|
|
460
|
+
f"replay.sharpe={rp_sharpe:.4f})", file=sys.stderr)
|
|
461
|
+
|
|
462
|
+
print("=== Step 2/3: PUT /strategies/:id/result (metrics + replay_metrics + version) ===",
|
|
463
|
+
file=sys.stderr)
|
|
373
464
|
try:
|
|
374
465
|
result = json.loads(result_path.read_text(encoding="utf-8"))
|
|
375
466
|
except json.JSONDecodeError as e:
|
|
376
467
|
print(f" ✗ result.json 解析失败: {e}", file=sys.stderr)
|
|
377
468
|
return 2
|
|
378
469
|
try:
|
|
379
|
-
resp = runtime.server_client.upload_backtest_result(
|
|
470
|
+
resp = runtime.server_client.upload_backtest_result(
|
|
471
|
+
sid, result, replay_metrics=replay_metrics, version=new_version,
|
|
472
|
+
)
|
|
380
473
|
except runtime.server_client.ServerError as e:
|
|
381
474
|
print(f" ✗ 上传 metrics 失败: {e}", file=sys.stderr)
|
|
382
475
|
return 4
|
|
@@ -943,6 +1036,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
943
1036
|
cm.add_argument("strategy_id", help="server 端 strategy_id")
|
|
944
1037
|
cm.add_argument("--strategy", help="akquant strategy .py 路径 (默认 <id>.py)")
|
|
945
1038
|
cm.add_argument("--result", required=True, help="metrics result.json 路径")
|
|
1039
|
+
# Round 18.62 P0-1: replay gate. --replay-result 必填 (除非 --skip-replay-gate).
|
|
1040
|
+
cm.add_argument("--replay-result", default=None,
|
|
1041
|
+
help="replay_result.json 路径 (P0-1 必填; 先跑 live run --broker replay 拿 metrics, "
|
|
1042
|
+
"或 --skip-replay-gate 跳过)")
|
|
1043
|
+
cm.add_argument("--skip-replay-gate", action="store_true",
|
|
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 互斥")
|
|
946
1051
|
cm.add_argument("--config", help="config.json 路径")
|
|
947
1052
|
cm.add_argument("--params", default=None,
|
|
948
1053
|
help="params.json 路径 (固定 schema; 整个内容作 multipart params 字段, desktop 实盘配置源)")
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import sys
|
|
12
|
+
import json # Round 18.62 P0-1: replay metrics 落盘
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
@@ -216,19 +217,39 @@ def _build_instruments(symbols: list[str] | None, normalize: bool = False) -> li
|
|
|
216
217
|
|
|
217
218
|
normalize=True: 裸码 → 带后缀 (replay 数据来自 prebuilt, stockCode 是 '600000.SH'
|
|
218
219
|
形态, instrument symbol 必须一致才匹配 ReplayMarketGateway 的订阅过滤).
|
|
220
|
+
|
|
221
|
+
Round 18.59: 按 6 位代码前缀推断 AssetType — akquant 0.3.x AssetType 枚举没 ETF 也没 Bond,
|
|
222
|
+
510/511/512/515/159 (沪/深 ETF) → Fund; 113/127/128 (可转债) → Fund; 其他 6 位 → Stock.
|
|
223
|
+
之前硬写 AssetType.Stock 让 351 个 ETF 被 broker 内部按 asset_type 分流时丢弃 → universe=[].
|
|
224
|
+
ponytail: akquant 0.4.x 若新增 AssetType.ETF / AssetType.Bond 则此映射需重审.
|
|
219
225
|
"""
|
|
220
226
|
if not symbols:
|
|
221
227
|
return None
|
|
222
228
|
# 延迟 import akquant — 让 loader 在 akquant 未装环境也能跑通
|
|
223
229
|
from akquant import AssetType, Instrument
|
|
224
230
|
|
|
231
|
+
_ETF_PREFIXES = ("510", "511", "512", "515", "159")
|
|
232
|
+
_BOND_PREFIXES = ("113", "127", "128") # 可转债 (沪 113 / 深 127 / 沪 128)
|
|
233
|
+
|
|
225
234
|
def _sym(s: str) -> str:
|
|
226
235
|
if normalize:
|
|
227
236
|
from ..akquant_schema_adapter import normalize_symbol
|
|
228
237
|
return normalize_symbol(s)
|
|
229
238
|
return s
|
|
230
239
|
|
|
231
|
-
|
|
240
|
+
def _asset_type(bare: str) -> Any:
|
|
241
|
+
# _sym 后可能是 6 位裸码或 600000.SH 形态 — 都按前 3 位判
|
|
242
|
+
code6 = bare.split(".")[0][-6:] if "." in bare else bare[-6:]
|
|
243
|
+
if not (len(code6) == 6 and code6.isdigit()):
|
|
244
|
+
return AssetType.Stock # 非法 6 位 fallback, 不阻塞启动
|
|
245
|
+
pfx3 = code6[:3]
|
|
246
|
+
if pfx3.startswith(_ETF_PREFIXES):
|
|
247
|
+
return AssetType.Fund
|
|
248
|
+
if pfx3.startswith(_BOND_PREFIXES):
|
|
249
|
+
return AssetType.Fund
|
|
250
|
+
return AssetType.Stock
|
|
251
|
+
|
|
252
|
+
return [Instrument(symbol=_sym(s), asset_type=_asset_type(_sym(s))) for s in symbols if s.strip()]
|
|
232
253
|
|
|
233
254
|
|
|
234
255
|
def _recent_real_bars(symbols: list[str], days: int = 30) -> tuple[Any, str]:
|
|
@@ -770,4 +791,45 @@ def run_live(
|
|
|
770
791
|
f"market_broker={market_broker or '<single>'} "
|
|
771
792
|
f"symbols={symbols or '<all>'} duration={duration}", flush=True)
|
|
772
793
|
print(f"[info] gateway_options={gateway_options}", flush=True)
|
|
773
|
-
_akquant_run_live(**kwargs)
|
|
794
|
+
_akquant_run_live(**kwargs)
|
|
795
|
+
|
|
796
|
+
# ===== Round 18.62 P0-1: replay metrics 产出 =====
|
|
797
|
+
# live run 跑完后, 拿同一份 bars + strategy_cls 跑一次 mini-backtest 拿 metrics,
|
|
798
|
+
# 落 runs/{strategy_id}/replay_result.json (跟 cmd_run 同 13-key schema, to_hamuna_result).
|
|
799
|
+
# 用途: commiter cmd_commit --replay-result 拿此文件 vs 回测 result.json 算 sharpe diff.
|
|
800
|
+
# 仅 broker=replay 时跑 (其他 broker 真盘无意义, 也没 bars 数据).
|
|
801
|
+
if broker == "replay" and spec.mode == "class" and spec.strategy_cls is not None:
|
|
802
|
+
try:
|
|
803
|
+
from akquant import run_backtest as _akquant_run_backtest # noqa: F401
|
|
804
|
+
from ..akquant_schema_adapter import to_hamuna_result
|
|
805
|
+
from datetime import datetime as _dt
|
|
806
|
+
# strategy_id 来自 run_live kwargs 顶层 (Round 18.42+ commiter 必传);
|
|
807
|
+
# 若没有 (裸 live run 调) 退到 /tmp/replay_result.json, 不污染 runs/.
|
|
808
|
+
strategy_id = kwargs.get("strategy_id") or "adhoc"
|
|
809
|
+
out_dir = Path("runs") / strategy_id
|
|
810
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
811
|
+
out_path = out_dir / "replay_result.json"
|
|
812
|
+
# 用 replay_bars_df + strategy_cls 跑 mini-backtest. 同一份数据 → 可比.
|
|
813
|
+
mini_kwargs = dict(kwargs)
|
|
814
|
+
mini_kwargs.pop("live", None) # 防混入 live flag
|
|
815
|
+
# to_hamuna_result 需 akquant BacktestResult 对象; mini-backtest 走同一 strategy_cls
|
|
816
|
+
# + 同一份 bars, 拿 metrics 对象 → to_hamuna_result 落 13-key schema.
|
|
817
|
+
bt_result = _akquant_run_backtest(
|
|
818
|
+
strategy_cls=spec.strategy_cls,
|
|
819
|
+
bars=replay_bars_df,
|
|
820
|
+
initial_cash=initial_cash or 100000.0,
|
|
821
|
+
)
|
|
822
|
+
cfg = {"period": {"start": str(replay_bars_df['date'].min())[:10] if hasattr(replay_bars_df, 'columns') else "",
|
|
823
|
+
"end": str(_dt.now().date())}}
|
|
824
|
+
result_dict = to_hamuna_result(bt_result, cfg)
|
|
825
|
+
out_path.write_text(json.dumps(result_dict, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
826
|
+
m = result_dict.get("metrics", {})
|
|
827
|
+
print(f"[info] replay metrics: sharpe={m.get('sharpe'):.3f} "
|
|
828
|
+
f"total_return={m.get('total_return', 0):.2%} trades={len(result_dict.get('trades', []))} "
|
|
829
|
+
f"→ {out_path}", flush=True)
|
|
830
|
+
except ImportError:
|
|
831
|
+
# akquant 未装 / to_hamuna_result 不可用 — 跳过, 不阻塞 live run
|
|
832
|
+
print("[warn] P0-1: akquant / to_hamuna_result 未装, 跳过 replay metrics 产出", flush=True)
|
|
833
|
+
except Exception as e:
|
|
834
|
+
# mini-backtest 失败不致命 — live run 已成功, 只 warn
|
|
835
|
+
print(f"[warn] P0-1: replay metrics 产出失败 ({type(e).__name__}: {e}), 跳过", flush=True)
|
{hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/runtime/server_client.py
RENAMED
|
@@ -72,14 +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) -> dict:
|
|
76
|
-
"""PUT /api/v1/strategies/{id}/result → 上传 BacktestResult-shape dict.
|
|
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
|
+
|
|
81
|
+
Round 18.62 P0-1: replay_metrics (Sharpe/TotalReturn/WinRate/Trades/DiffPct/RunAt) —
|
|
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 强校验).
|
|
80
86
|
"""
|
|
81
87
|
payload = _sanitize_for_json(result)
|
|
82
|
-
|
|
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}
|
|
92
|
+
if replay_metrics is not None:
|
|
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}')
|
|
83
107
|
|
|
84
108
|
|
|
85
109
|
def create_strategy(name: str, code: str = '', params: dict | None = None) -> dict:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hamuna-quant-cli"
|
|
7
|
-
version = "0.1.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/akquant_schema_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli/cross_sectional_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.9 → hamuna_quant_cli-0.1.11}/hamuna_quant_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|