hamuna-quant-cli 0.1.24__tar.gz → 0.1.26__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.24 → hamuna_quant_cli-0.1.26}/PKG-INFO +1 -1
  2. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/__init__.py +1 -1
  3. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/__main__.py +1 -1
  4. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/qmt_broker.py +94 -1
  5. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/discipline.py +119 -0
  6. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
  7. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/pyproject.toml +1 -1
  8. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/MANIFEST.in +0 -0
  9. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/README.md +0 -0
  10. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/README.md +0 -0
  11. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_market_fallback.py +0 -0
  12. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_metrics_15.py +0 -0
  13. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
  14. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
  15. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_runner.py +0 -0
  16. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
  17. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/base_strategy.py +0 -0
  18. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
  19. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/__init__.py +0 -0
  20. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/loader.py +0 -0
  21. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/qmt_market.py +0 -0
  22. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/runner.py +0 -0
  23. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/server_market_client.py +0 -0
  24. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
  25. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
  26. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/qmt_translator.py +0 -0
  27. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/__init__.py +0 -0
  28. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/backtest.py +0 -0
  29. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/cache.py +0 -0
  30. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/http_client.py +0 -0
  31. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/s3client.py +0 -0
  32. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/server_client.py +0 -0
  33. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/scripts/server.json +0 -0
  34. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
  35. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
  36. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
  37. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/requires.txt +0 -0
  38. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
  39. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/setup.cfg +0 -0
  40. {hamuna_quant_cli-0.1.24 → hamuna_quant_cli-0.1.26}/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.24
3
+ Version: 0.1.26
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.24"
15
+ __version__ = "0.1.26"
@@ -9,7 +9,7 @@ Usage:
9
9
  hamuna_quant_cli qmt-translate <strategy.py> <cfg.json>
10
10
  hamuna_quant_cli commit <strategy_id> --strategy ... --result ... --config ... --params ...
11
11
  hamuna_quant_cli dataset {list|fetch|manifest}
12
- hamuna_quant_cli live run <strategy.py> --mode paper --broker qmt ...
12
+ hamuna_quant_cli live run <strategy.py> --mode broker_live --broker qmt ...
13
13
 
14
14
  PYTHONPATH: 不需要 (pip install hamuna-quant-cli 后全局可用).
15
15
  """
@@ -66,7 +66,9 @@ def _import_akquant_gateway():
66
66
  # (transient failure 自动重试; 4xx/业务错误不重试)
67
67
  # ============================================================
68
68
  import json
69
+ import os
69
70
  import time
71
+ from dataclasses import dataclass
70
72
  from typing import Any
71
73
 
72
74
  import requests
@@ -159,6 +161,36 @@ class _HTTP:
159
161
  return self._request("POST", route, payload=payload)
160
162
 
161
163
 
164
+ # ============================================================
165
+ # Round 33+2: broker 硬 cap — 装在 place_order 入口, 1万/笔 + 5万/总 + 10 笔/日.
166
+ # env override: HAMUNA_LIVE_MAX_ORDER_AMOUNT / HAMUNA_LIVE_MAX_TOTAL_AMOUNT /
167
+ # HAMUNA_LIVE_MAX_DAILY_ORDERS (空字符串/未设走默认).
168
+ # 模拟信号模式下仍生效 (broker 入口最后兜底, 不依赖 QMT 模式).
169
+ # ============================================================
170
+ @dataclass(frozen=True)
171
+ class _HardCap:
172
+ """实盘下单硬 cap — 单笔/累计/笔数 3 维."""
173
+ max_order_amount: float = 10_000.0 # 单笔金额 ≤ 1 万
174
+ max_total_amount: float = 50_000.0 # 累计金额 ≤ 5 万
175
+ max_daily_orders: int = 10 # 单日 ≤ 10 笔
176
+
177
+ @classmethod
178
+ def from_env(cls) -> "_HardCap":
179
+ def _f(env: str, default: float) -> float:
180
+ raw = os.environ.get(env, "").strip()
181
+ return float(raw) if raw else default
182
+
183
+ def _i(env: str, default: int) -> int:
184
+ raw = os.environ.get(env, "").strip()
185
+ return int(raw) if raw else default
186
+
187
+ return cls(
188
+ max_order_amount=_f("HAMUNA_LIVE_MAX_ORDER_AMOUNT", cls.max_order_amount),
189
+ max_total_amount=_f("HAMUNA_LIVE_MAX_TOTAL_AMOUNT", cls.max_total_amount),
190
+ max_daily_orders=_i("HAMUNA_LIVE_MAX_DAILY_ORDERS", cls.max_daily_orders),
191
+ )
192
+
193
+
162
194
  # ============================================================
163
195
  # 安全码映射: akquant 侧用 sh600000 / sz000001 / 600000.XSHG, bridge 接受 600000.SH / 600000.XSHG.
164
196
  # 转换原则: 已是 .XSHG/.XSHE 形式直接 to_qmt; sh/sz 前缀剥掉; 裸 6 位按首字 (5/6/7/9→SH, 0/1/2/3→SZ) 加 .SH/.SZ.
@@ -273,6 +305,17 @@ def _make_trader_gateway():
273
305
  self._account_id = account_id
274
306
  self._account_type = account_type
275
307
  self._connected = False
308
+ # Round 33+2: 装 broker 硬 cap (默认 1万/笔 + 5万/总 + 10 笔/日, env override).
309
+ self._cap = _HardCap.from_env()
310
+ self._day_orders: list[float] = [] # 今日已下金额 (price * qty) 累积
311
+ self._day_reset_at: str = time.strftime("%Y-%m-%d")
312
+ # round 33+2: cap 启动 log 让用户/测试一眼看到生效.
313
+ print(
314
+ f"[cap] order={self._cap.max_order_amount:.0f} "
315
+ f"total={self._cap.max_total_amount:.0f} "
316
+ f"daily={self._cap.max_daily_orders}",
317
+ flush=True,
318
+ )
276
319
 
277
320
  # ----- 生命周期 -----
278
321
  def connect(self) -> None:
@@ -287,7 +330,9 @@ def _make_trader_gateway():
287
330
  "account_id": self._account_id,
288
331
  "account_type": self._account_type,
289
332
  })
290
- if not isinstance(set_resp, dict) or not set_resp.get("ok"):
333
+ # Round 33+2: /set_account 实测返裸 dict ({account_id, account_type, applied},
334
+ # 不带 {ok, value, ...} 信封) — 兼容: ok 字段缺省视为 True, 仅当 ok=False 抛.
335
+ if not isinstance(set_resp, dict) or set_resp.get("ok") is False:
291
336
  raise BrokerHTTPError(
292
337
  f"connect: /set_account 返回异常: {set_resp!r}"
293
338
  )
@@ -333,10 +378,56 @@ def _make_trader_gateway():
333
378
  }),
334
379
  )
335
380
 
381
+ # ----- 硬 cap (Round 33+2) -----
382
+ def _rollover_day(self) -> None:
383
+ """跨日清零 _day_orders (本机时区)."""
384
+ today = time.strftime("%Y-%m-%d")
385
+ if today != self._day_reset_at:
386
+ self._day_orders.clear()
387
+ self._day_reset_at = today
388
+
389
+ def _check_cap(self, req) -> None:
390
+ """hard cap 入口检查 — 任一超 cap 立刻 raise, 不发任何 HTTP.
391
+
392
+ 笔数 cap 总是检查; 金额 cap 在 price * qty > 0 时检查 (市价单 amount=0 跳过).
393
+ """
394
+ try:
395
+ qty = float(req.quantity or 0.0)
396
+ except (TypeError, ValueError):
397
+ qty = 0.0
398
+ try:
399
+ price = float(req.price or 0.0)
400
+ except (TypeError, ValueError):
401
+ price = 0.0
402
+ amount = price * qty
403
+
404
+ if amount > self._cap.max_order_amount:
405
+ raise BrokerHTTPError(
406
+ f"[hard-cap] 单笔金额 {amount:.2f} > {self._cap.max_order_amount:.0f} "
407
+ f"(sym={req.symbol} qty={qty} price={price})"
408
+ )
409
+ self._rollover_day()
410
+ if len(self._day_orders) + 1 > self._cap.max_daily_orders:
411
+ raise BrokerHTTPError(
412
+ f"[hard-cap] 单日笔数 {len(self._day_orders) + 1} > "
413
+ f"{self._cap.max_daily_orders}"
414
+ )
415
+ if amount > 0 and sum(self._day_orders) + amount > self._cap.max_total_amount:
416
+ raise BrokerHTTPError(
417
+ f"[hard-cap] 累计金额 {sum(self._day_orders) + amount:.2f} > "
418
+ f"{self._cap.max_total_amount:.0f}"
419
+ )
420
+
421
+ def _record_order(self, price: float, qty: int) -> None:
422
+ """HTTP /place_order 成功后记一笔 (供累计金额 cap)."""
423
+ if price > 0 and qty > 0:
424
+ self._day_orders.append(float(price * qty))
425
+
336
426
  # ----- 下单 -----
337
427
  def place_order(self, req): # type: ignore[override]
338
428
  if not self._connected:
339
429
  raise BrokerHTTPError("place_order: 未 connect, 先调 start()/connect()")
430
+ self._check_cap(req) # Round 33+2: 硬 cap 在所有真实动作前
340
431
  # Round 18.63.18: paper mock 路径已删 — QMT 客户端原生模拟信号模式
341
432
  # (GUI 手动切), CLI 永远发真 passorder, 模拟由 QMT 侧承担.
342
433
  ak = _import_akquant_gateway()
@@ -371,6 +462,8 @@ def _make_trader_gateway():
371
462
  "client_order_id": req.client_order_id,
372
463
  }
373
464
  data = self._http.post("/place_order", payload)
465
+ # Round 33+2: HTTP 成功才记 cap (金额 cap 累计用). price=0 市价单不记金额.
466
+ self._record_order(price, amount)
374
467
  order_id = str(data.get("order_id") or data.get("order_ref") or "")
375
468
  if order_id:
376
469
  self.record_broker_order(order_id, req.client_order_id)
@@ -33,6 +33,11 @@ class DisciplineError:
33
33
  return f'ERROR: rule={self.rule} line={self.line} {self.msg}'
34
34
 
35
35
 
36
+ # 2026-09-11 用户拍板 A: warn-only rules 永远不 abort cmd_check (default 不破坏现有策略).
37
+ # 现有 rule 命名约定: 用 rule name 区分 severity, 不改 DisciplineError 形态.
38
+ _WARN_ONLY_RULES: set[str] = {"on_bar_duplicate_dispatch"} # Rule 20
39
+
40
+
36
41
  # QMT 残留标识符 (出现即报 — v2 走 akquant, 任何 QMT-specific 调用都该走 v1)
37
42
  _QMT_GLOBALS: dict[str, str] = {
38
43
  'passorder': 'QMT 报单函数 (akquant 用 self.buy / self.sell)',
@@ -592,6 +597,70 @@ def _rule_position_sizing(tree: ast.Module) -> list[DisciplineError]:
592
597
  _SIGNAL_HOOKS = ("on_bar", "on_timer", "on_cross_section")
593
598
 
594
599
 
600
+ # ---- Rule 20 (2026-09-11 用户拍板): broker_live 1m SSE 同根 bar 多次推送,
601
+ # on_bar 必须 dedup. 永远 warn-only (用户拍板 A: 不破坏现有策略).
602
+ # Round 33+2 e2e 实测: vol=0 占位 + 后续 vol 增量, 同一 ts 触发 on_bar 多次.
603
+ # 真实策略无 dedup = 同根重复下单. 见 auditor.md §2.13.
604
+ def _rule_on_bar_duplicate_dispatch(tree: ast.Module) -> list[DisciplineError]:
605
+ """Rule 20 (2026-09-11 用户拍板, warn-only).
606
+
607
+ 检测: 策略类 `bar_interval ∈ {1m, 5m}` (Rule 15 联动) + `on_bar` 函数体前 3 行
608
+ **无** `bar.timestamp == self.X` 模式 → emit `on_bar_duplicate_dispatch`.
609
+
610
+ 不拦: `bar_interval` 不在 {1m, 5m} (日线/周线实盘不重复推送); `on_bar` 缺 dedup
611
+ 但 emit 后被主流程 warn-only 过滤掉 (cmd_check 不 abort, 仅 stderr 提示).
612
+ """
613
+ errs: list[DisciplineError] = []
614
+ for node in ast.walk(tree):
615
+ if not isinstance(node, ast.ClassDef):
616
+ continue
617
+ bar_interval = None
618
+ for stmt in node.body:
619
+ if (
620
+ isinstance(stmt, ast.Assign)
621
+ and len(stmt.targets) == 1
622
+ and isinstance(stmt.targets[0], ast.Name)
623
+ and stmt.targets[0].id == "bar_interval"
624
+ and isinstance(stmt.value, ast.Constant)
625
+ and isinstance(stmt.value.value, str)
626
+ ):
627
+ bar_interval = stmt.value.value
628
+ if bar_interval not in {"1m", "5m"}:
629
+ continue
630
+ on_bar = next(
631
+ (s for s in node.body if isinstance(s, ast.FunctionDef) and s.name == "on_bar"),
632
+ None,
633
+ )
634
+ if on_bar is None:
635
+ continue
636
+ # on_bar 函数体前 3 行 (line 1 是 docstring 时, 跳过它)
637
+ body_stmts = [s for s in on_bar.body if not (
638
+ isinstance(s, ast.Expr) and isinstance(s.value, ast.Constant)
639
+ and isinstance(s.value.value, str)
640
+ )][:3]
641
+ try:
642
+ body_src = ast.unparse(ast.Module(body=body_stmts, type_ignores=[]))
643
+ except Exception:
644
+ body_src = ""
645
+ if "bar.timestamp" in body_src and "==" in body_src and "return" in body_src:
646
+ continue
647
+ errs.append(DisciplineError(
648
+ rule="on_bar_duplicate_dispatch",
649
+ line=on_bar.lineno,
650
+ msg=(
651
+ f"bar_interval={bar_interval!r} 但 on_bar 前 3 行无 "
652
+ f"`bar.timestamp == self.X: return` dedup 模式. "
653
+ f"Round 33+2 e2e 实测 broker_live SSE 推同根 bar 多次 "
654
+ f"(vol=0 占位 + 后续 vol 增量). 无 dedup = 同根重复下单. "
655
+ f"修复: on_bar 首行加 "
656
+ f"`if bar.timestamp == self._last_bar_ts: return; "
657
+ f"self._last_bar_ts = bar.timestamp`. "
658
+ f"另: on_start 加 `self._last_bar_ts: int = 0`."
659
+ ),
660
+ ))
661
+ return errs
662
+
663
+
595
664
  def _rule_cash_guard(tree: ast.Module) -> list[DisciplineError]:
596
665
  """Rule 13 (2026-08-29): 信号买入前必须确认剩余资金.
597
666
 
@@ -677,6 +746,9 @@ def check_discipline(source: str, config: dict) -> list[DisciplineError]:
677
746
  errs += _rule_get_history_nan_guard(tree)
678
747
  errs += _rule_position_sizing(tree) # Rule 12 (2026-08-29)
679
748
  errs += _rule_cash_guard(tree) # Rule 13 (2026-08-29): buy 前资金守卫
749
+ errs += _rule_on_bar_duplicate_dispatch(tree) # Rule 20 (2026-09-11 用户拍板, warn-only)
750
+ # 过滤 warn-only rules (用户拍板 A: 永远不 abort, 仅 emit 给用户看)
751
+ errs = [e for e in errs if e.rule not in _WARN_ONLY_RULES]
680
752
  return errs
681
753
 
682
754
 
@@ -873,6 +945,53 @@ class WithSizing(Strategy):
873
945
  assert not any(e.rule == 'position_sizing_missing' for e in errs), f'有 total_pct 应放行: {errs}'
874
946
  print(f'OK: total_pct 仓位声明放行')
875
947
 
948
+ # Rule 20 (2026-09-11 用户拍板, warn-only): broker_live 1m 同根 bar 多次推送.
949
+ # 违规: bar_interval='1m' 但 on_bar 无 dedup → AST emit, 主流程 warn-only 过滤掉 (0 errs)
950
+ bad_dedup = '''
951
+ from akquant import Strategy
952
+ class MyStrat(Strategy):
953
+ bar_interval = "1m"
954
+ def on_bar(self, bar):
955
+ self.buy(bar.symbol, 100)
956
+ '''
957
+ errs_raw = _rule_on_bar_duplicate_dispatch(ast.parse(bad_dedup))
958
+ assert any(e.rule == 'on_bar_duplicate_dispatch' for e in errs_raw), \
959
+ f'Rule 20 应 emit AST 错: {errs_raw}'
960
+ print(f'OK: Rule 20 AST 检测 emit (bar_interval=1m, on_bar 无 dedup)')
961
+ errs = check_discipline(bad_dedup, {})
962
+ assert not any(e.rule == 'on_bar_duplicate_dispatch' for e in errs), \
963
+ f'Rule 20 应被 warn-only 过滤 (主流程不 abort): {errs}'
964
+ print(f'OK: Rule 20 warn-only 过滤掉 (cmd_check 不 exit 3)')
965
+
966
+ # 合规: on_bar 首行 dedup → AST 放行
967
+ good_dedup = '''
968
+ from akquant import Strategy
969
+ class MyStrat(Strategy):
970
+ bar_interval = "1m"
971
+ def on_start(self):
972
+ self._last_bar_ts: int = 0
973
+ def on_bar(self, bar):
974
+ if bar.timestamp == self._last_bar_ts:
975
+ return
976
+ self._last_bar_ts = bar.timestamp
977
+ self.buy(bar.symbol, 100)
978
+ '''
979
+ errs_raw = _rule_on_bar_duplicate_dispatch(ast.parse(good_dedup))
980
+ assert not errs_raw, f'合规 dedup 应放行: {errs_raw}'
981
+ print(f'OK: Rule 20 合规 dedup 放行 (bar.timestamp == self._last_bar_ts: return)')
982
+
983
+ # 不拦: bar_interval='1d' (日线不重复推送)
984
+ daily_dedup = '''
985
+ from akquant import Strategy
986
+ class DailyStrat(Strategy):
987
+ bar_interval = "1d"
988
+ def on_bar(self, bar):
989
+ self.buy(bar.symbol, 100)
990
+ '''
991
+ errs_raw = _rule_on_bar_duplicate_dispatch(ast.parse(daily_dedup))
992
+ assert not errs_raw, f'bar_interval=1d 应放行: {errs_raw}'
993
+ print(f'OK: Rule 20 bar_interval=1d 放行 (日线不重复推送)')
994
+
876
995
 
877
996
  if __name__ == '__main__':
878
997
  _selfcheck()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hamuna-quant-cli
3
- Version: 0.1.24
3
+ Version: 0.1.26
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.24"
7
+ version = "0.1.26"
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"