hamuna-quant-cli 0.1.23__tar.gz → 0.1.25__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.23 → hamuna_quant_cli-0.1.25}/PKG-INFO +3 -6
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/README.md +2 -5
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/__init__.py +1 -1
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/__main__.py +5 -9
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/qmt_broker.py +9 -218
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/qmt_market.py +16 -17
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/runner.py +52 -141
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/PKG-INFO +3 -6
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/pyproject.toml +1 -1
- hamuna_quant_cli-0.1.25/tests/test_live_safety.py +42 -0
- hamuna_quant_cli-0.1.23/tests/test_live_safety.py +0 -52
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/MANIFEST.in +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/README.md +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/_market_fallback.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/_metrics_15.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/akquant_runner.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/base_strategy.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/__init__.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/loader.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/server_market_client.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/qmt_translator.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/__init__.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/backtest.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/cache.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/discipline.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/http_client.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/s3client.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/server_client.py +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/scripts/server.json +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/requires.txt +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
- {hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hamuna-quant-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.25
|
|
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
|
|
@@ -130,11 +130,8 @@ hamuna_quant_cli upload --name "v2_smoke" --config /tmp/c.json --code /tmp/s.py
|
|
|
130
130
|
```bash
|
|
131
131
|
# 1) 写策略 (同上)
|
|
132
132
|
|
|
133
|
-
# 2)
|
|
134
|
-
|
|
135
|
-
--mode paper --broker replay --symbols sh600000,sz600036 --duration 30s
|
|
136
|
-
|
|
137
|
-
# 3) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
133
|
+
# 2) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
134
|
+
# Round 18.63.18: paper 模式已删 — 模拟由 QMT 客户端原生「模拟信号模式」承担
|
|
138
135
|
hamuna_quant_cli live run /tmp/s.py \
|
|
139
136
|
--mode broker_live --broker qmt --market-broker qmt_market \
|
|
140
137
|
--symbols sh600000,sz600036 \
|
|
@@ -91,11 +91,8 @@ hamuna_quant_cli upload --name "v2_smoke" --config /tmp/c.json --code /tmp/s.py
|
|
|
91
91
|
```bash
|
|
92
92
|
# 1) 写策略 (同上)
|
|
93
93
|
|
|
94
|
-
# 2)
|
|
95
|
-
|
|
96
|
-
--mode paper --broker replay --symbols sh600000,sz600036 --duration 30s
|
|
97
|
-
|
|
98
|
-
# 3) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
94
|
+
# 2) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
95
|
+
# Round 18.63.18: paper 模式已删 — 模拟由 QMT 客户端原生「模拟信号模式」承担
|
|
99
96
|
hamuna_quant_cli live run /tmp/s.py \
|
|
100
97
|
--mode broker_live --broker qmt --market-broker qmt_market \
|
|
101
98
|
--symbols sh600000,sz600036 \
|
|
@@ -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
|
|
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
|
"""
|
|
@@ -909,7 +909,6 @@ def cmd_live_run(args) -> int:
|
|
|
909
909
|
log_level=args.log_level,
|
|
910
910
|
log_file=args.log_file,
|
|
911
911
|
market_broker=args.market_broker,
|
|
912
|
-
replay_days=args.replay_days,
|
|
913
912
|
params=sp if isinstance(sp, dict) else None,
|
|
914
913
|
)
|
|
915
914
|
except LiveRunError as e:
|
|
@@ -1048,10 +1047,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1048
1047
|
live_sub = live_p.add_subparsers(dest="subcmd", required=True)
|
|
1049
1048
|
run_p = live_sub.add_parser("run", help="运行一个 strategy.py")
|
|
1050
1049
|
run_p.add_argument("strategy", type=Path, help="strategy.py 路径")
|
|
1051
|
-
run_p.add_argument("--mode", choices=["
|
|
1052
|
-
help="trading_mode (
|
|
1053
|
-
run_p.add_argument("--broker", default="
|
|
1054
|
-
help="broker 名称:
|
|
1050
|
+
run_p.add_argument("--mode", choices=["broker_live"], default="broker_live",
|
|
1051
|
+
help="trading_mode (Round 18.63.18: 仅 broker_live, paper 已删)")
|
|
1052
|
+
run_p.add_argument("--broker", default="qmt",
|
|
1053
|
+
help="broker 名称: qmt / 自定义 (replay 已删)")
|
|
1055
1054
|
run_p.add_argument("--market-broker", default=None,
|
|
1056
1055
|
help="独立行情 broker (e.g. qmt_market)")
|
|
1057
1056
|
run_p.add_argument("--symbols", default=None,
|
|
@@ -1064,9 +1063,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1064
1063
|
help="strategy.py 里的类名")
|
|
1065
1064
|
run_p.add_argument("--gateway-options", default=None,
|
|
1066
1065
|
help="k=v 字典, 透传给 run_live")
|
|
1067
|
-
run_p.add_argument("--replay-days", type=int, default=30,
|
|
1068
|
-
help="broker=replay 时取最近 N 天真实日线 (默认 30; "
|
|
1069
|
-
"策略 warmup 超窗口需加大, e.g. 90)")
|
|
1070
1066
|
run_p.add_argument("--initial-cash", type=float, default=None)
|
|
1071
1067
|
run_p.add_argument("--log-level", default="INFO",
|
|
1072
1068
|
choices=["DEBUG", "INFO", "WARNING", "ERROR"])
|
|
@@ -84,11 +84,6 @@ class BrokerHTTPError(RuntimeError):
|
|
|
84
84
|
"""bridge_server 返回 ok=False 或 HTTP 状态非 2xx."""
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def _emit_paper(msg: str) -> None:
|
|
88
|
-
"""paper 模式日志 — flush=True 保证 akquant 拉日志实时看到, 不被缓冲."""
|
|
89
|
-
print(f"[PAPER] {msg}", flush=True)
|
|
90
|
-
|
|
91
|
-
|
|
92
87
|
class _HTTP:
|
|
93
88
|
def __init__(self, base_url: str, timeout: float = 30.0) -> None: # ponytail: 默认 30s 容忍 TCP connect 偶发超时 — ceiling = qmt_market.poll_interval (30s), 再高就盖过 poll cycle; Retry 自动重试已覆盖偶发抖动 (urllib3.Retry, 见 _RETRY_*)
|
|
94
89
|
self.base_url = base_url.rstrip("/")
|
|
@@ -267,12 +262,9 @@ def _make_trader_gateway():
|
|
|
267
262
|
account_id: str = "",
|
|
268
263
|
account_type: str = "stock",
|
|
269
264
|
timeout: float = 5.0,
|
|
270
|
-
paper: bool = True,
|
|
271
265
|
) -> None:
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
不发真 HTTP, 走本地 mock. 显式真下单必须 paper=False.
|
|
275
|
-
"""
|
|
266
|
+
"""Round 18.63.18: paper 参数已删 — QMT 客户端原生模拟信号模式,
|
|
267
|
+
CLI 永远发真 passorder (模拟由 QMT GUI 切)."""
|
|
276
268
|
super().__init__()
|
|
277
269
|
ak = _import_akquant_gateway()
|
|
278
270
|
self._BrokerCapability = ak["BrokerCapability"]
|
|
@@ -281,13 +273,6 @@ def _make_trader_gateway():
|
|
|
281
273
|
self._account_id = account_id
|
|
282
274
|
self._account_type = account_type
|
|
283
275
|
self._connected = False
|
|
284
|
-
self._paper = paper
|
|
285
|
-
self._paper_order_seq = 0 # 仅 paper 模式用, mock 订单号递增
|
|
286
|
-
if paper:
|
|
287
|
-
_emit_paper(
|
|
288
|
-
f"qmt broker paper 模式启动 — account={account_id} "
|
|
289
|
-
f"place_order / cancel_order 不发真 HTTP, mock 返 PAPER-NNNNNN"
|
|
290
|
-
)
|
|
291
276
|
|
|
292
277
|
# ----- 生命周期 -----
|
|
293
278
|
def connect(self) -> None:
|
|
@@ -352,16 +337,8 @@ def _make_trader_gateway():
|
|
|
352
337
|
def place_order(self, req): # type: ignore[override]
|
|
353
338
|
if not self._connected:
|
|
354
339
|
raise BrokerHTTPError("place_order: 未 connect, 先调 start()/connect()")
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
self._paper_order_seq += 1
|
|
358
|
-
order_id = f"PAPER-{self._paper_order_seq:06d}"
|
|
359
|
-
self.record_broker_order(order_id, req.client_order_id)
|
|
360
|
-
_emit_paper(
|
|
361
|
-
f"place_order 跳过真下单: symbol={req.symbol} side={req.side} "
|
|
362
|
-
f"qty={req.quantity} price={req.price} → mock order_id={order_id}"
|
|
363
|
-
)
|
|
364
|
-
return order_id
|
|
340
|
+
# Round 18.63.18: paper mock 路径已删 — QMT 客户端原生模拟信号模式
|
|
341
|
+
# (GUI 手动切), CLI 永远发真 passorder, 模拟由 QMT 侧承担.
|
|
365
342
|
ak = _import_akquant_gateway()
|
|
366
343
|
side = str(req.side).upper()
|
|
367
344
|
# bridge 的 amount 字段 — QMT 用 int, 但 akquant quantity 是 float, 截断到 int.
|
|
@@ -417,9 +394,6 @@ def _make_trader_gateway():
|
|
|
417
394
|
def cancel_order(self, broker_order_id: str) -> None:
|
|
418
395
|
if not self._connected:
|
|
419
396
|
raise BrokerHTTPError("cancel_order: 未 connect")
|
|
420
|
-
if self._paper:
|
|
421
|
-
_emit_paper(f"cancel_order 跳过真撤单: broker_order_id={broker_order_id}")
|
|
422
|
-
return
|
|
423
397
|
self._http.post("/cancel_order", {
|
|
424
398
|
"account_id": self._account_id,
|
|
425
399
|
"account_type": self._account_type,
|
|
@@ -644,13 +618,14 @@ def build_qmt(
|
|
|
644
618
|
qmt_account_id (必填)
|
|
645
619
|
qmt_account_type (default "stock")
|
|
646
620
|
qmt_timeout (default 5.0 秒)
|
|
647
|
-
|
|
621
|
+
|
|
622
|
+
Round 18.63.18 (用户拍板): qmt_paper 已删 — QMT 客户端原生模拟信号模式,
|
|
623
|
+
CLI 永远发真 passorder, 模拟由 QMT GUI 切.
|
|
648
624
|
"""
|
|
649
625
|
base_url = kwargs.get("qmt_base_url") or kwargs.get("base_url") or "http://127.0.0.1:9000"
|
|
650
626
|
account_id = kwargs.get("qmt_account_id") or kwargs.get("account_id") or ""
|
|
651
627
|
account_type = kwargs.get("qmt_account_type") or kwargs.get("account_type") or "stock"
|
|
652
628
|
timeout = float(kwargs.get("qmt_timeout") or kwargs.get("timeout") or 30.0) # ponytail: 跟 _HTTP.timeout 默认 30s 保持一致 (caller 显式传 timeout=5.0 会盖过这里默认, 但 build_qmt 不传 timeout 时必须 30s 才治网络抖动)
|
|
653
|
-
paper = _qmt_paper_default(kwargs.get("qmt_paper"))
|
|
654
629
|
if not account_id:
|
|
655
630
|
raise ValueError(
|
|
656
631
|
"hamuna_qmt_broker: 缺少 qmt_account_id — 透传给 run_live(..., qmt_account_id='...')"
|
|
@@ -659,27 +634,16 @@ def build_qmt(
|
|
|
659
634
|
QmtTraderGateway = _make_trader_gateway()
|
|
660
635
|
trader = QmtTraderGateway(
|
|
661
636
|
base_url=base_url, account_id=account_id, account_type=account_type,
|
|
662
|
-
timeout=timeout,
|
|
637
|
+
timeout=timeout,
|
|
663
638
|
)
|
|
664
639
|
return ak["GatewayBundle"](
|
|
665
640
|
market_gateway=None, # 行情走 akquant DataFeed (无 push)
|
|
666
641
|
trader_gateway=trader,
|
|
667
642
|
trader_capabilities=trader.get_capabilities(),
|
|
668
|
-
metadata={"broker": "qmt", "bridge": "bullettrade_compat"
|
|
643
|
+
metadata={"broker": "qmt", "bridge": "bullettrade_compat"},
|
|
669
644
|
)
|
|
670
645
|
|
|
671
646
|
|
|
672
|
-
def _qmt_paper_default(v: Any) -> bool:
|
|
673
|
-
"""CLI 字符串 → paper bool. 默认 True (安全).
|
|
674
|
-
显式 "0"/"false"/"no"/"off" → False (真下单); 其它 (含 None/""/"1"/"true") → True (paper)."""
|
|
675
|
-
if v is None or v == "":
|
|
676
|
-
return True
|
|
677
|
-
s = str(v).strip().lower()
|
|
678
|
-
if s in ("0", "false", "no", "off"):
|
|
679
|
-
return False
|
|
680
|
-
return True
|
|
681
|
-
|
|
682
|
-
|
|
683
647
|
# 注册 (side-effect import). akquant 未装时不抛 — 让 --help 仍能跑.
|
|
684
648
|
def _safe_register() -> None:
|
|
685
649
|
try:
|
|
@@ -690,176 +654,3 @@ def _safe_register() -> None:
|
|
|
690
654
|
|
|
691
655
|
|
|
692
656
|
_safe_register()
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
# ============================================================
|
|
696
|
-
# self-check — 不依赖 akquant, 起本地 mock bridge_server, 验证 HTTP shape
|
|
697
|
-
# ============================================================
|
|
698
|
-
if __name__ == "__main__":
|
|
699
|
-
import threading
|
|
700
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
701
|
-
|
|
702
|
-
class _MockBridge(BaseHTTPRequestHandler):
|
|
703
|
-
def log_message(self, fmt, *args):
|
|
704
|
-
pass # 静音
|
|
705
|
-
|
|
706
|
-
def _read_json(self):
|
|
707
|
-
length = int(self.headers.get("Content-Length") or 0)
|
|
708
|
-
return json.loads(self.rfile.read(length).decode("utf-8")) if length else {}
|
|
709
|
-
|
|
710
|
-
def _send(self, code, payload):
|
|
711
|
-
body = json.dumps(payload).encode("utf-8")
|
|
712
|
-
self.send_response(code)
|
|
713
|
-
self.send_header("Content-Type", "application/json")
|
|
714
|
-
self.send_header("Content-Length", str(len(body)))
|
|
715
|
-
self.end_headers()
|
|
716
|
-
self.wfile.write(body)
|
|
717
|
-
|
|
718
|
-
def do_GET(self):
|
|
719
|
-
if self.path.startswith("/health"):
|
|
720
|
-
self._send(200, {"ok": True, "value": {"status": "ok"}})
|
|
721
|
-
return
|
|
722
|
-
if self.path.startswith("/orders"):
|
|
723
|
-
self._send(200, {"ok": True, "value": {"orders": [{
|
|
724
|
-
"order_id": "ORD1", "security": "600000.SH", "raw_status": 6,
|
|
725
|
-
"filled": 100, "price": 10.5, "amount": 100,
|
|
726
|
-
"qmt_user_order_id": "coid1",
|
|
727
|
-
}]}})
|
|
728
|
-
return
|
|
729
|
-
if self.path.startswith("/trades"):
|
|
730
|
-
self._send(200, {"ok": True, "value": {"trades": [{
|
|
731
|
-
"trade_id": "TRD1", "order_id": "ORD1", "security": "600000.SH",
|
|
732
|
-
"amount": 100, "price": 10.5, "side": "BUY",
|
|
733
|
-
"qmt_user_order_id": "coid1",
|
|
734
|
-
}]}})
|
|
735
|
-
return
|
|
736
|
-
if self.path.startswith("/flaky"):
|
|
737
|
-
# transient retry 验证: 前 _RETRY_TOTAL 次返 503, 之后 200.
|
|
738
|
-
# 计数器放 server (handler 每请求新实例, 放 self 会每次清零 → 永不 200)
|
|
739
|
-
n = getattr(self.server, "_flaky_n", 0)
|
|
740
|
-
if n < _RETRY_TOTAL:
|
|
741
|
-
self.server._flaky_n = n + 1
|
|
742
|
-
self._send(503, {"ok": False, "code": "TRY_AGAIN"})
|
|
743
|
-
return
|
|
744
|
-
self._send(200, {"ok": True, "value": {"attempts": n + 1}})
|
|
745
|
-
return
|
|
746
|
-
self._send(404, {"ok": False, "code": "NOT_FOUND"})
|
|
747
|
-
|
|
748
|
-
def do_POST(self):
|
|
749
|
-
data = self._read_json()
|
|
750
|
-
if "/set_account" in self.path:
|
|
751
|
-
# Round 18.63.12: connect() 先 POST /set_account 设全局账户 —
|
|
752
|
-
# mock 同步返回 ok, 否则 paper 模式 connect 抛异常.
|
|
753
|
-
self._send(200, {"ok": True, "value": {
|
|
754
|
-
"account_id": data.get("account_id"),
|
|
755
|
-
"account_type": data.get("account_type") or "stock",
|
|
756
|
-
}})
|
|
757
|
-
return
|
|
758
|
-
if "/account" in self.path:
|
|
759
|
-
self._send(200, {"ok": True, "value": {
|
|
760
|
-
"account_id": data.get("account_id"), "account_type": "stock",
|
|
761
|
-
"available_cash": 50000.0, "cash": 100000.0, "total_value": 150000.0,
|
|
762
|
-
}})
|
|
763
|
-
return
|
|
764
|
-
if "/positions" in self.path:
|
|
765
|
-
self._send(200, {"ok": True, "value": {"positions": [{
|
|
766
|
-
"security": "600000.SH", "amount": 100, "closeable_amount": 100,
|
|
767
|
-
"avg_cost": 10.0,
|
|
768
|
-
}]}})
|
|
769
|
-
return
|
|
770
|
-
if "/place_order" in self.path:
|
|
771
|
-
self._send(200, {"ok": True, "value": {
|
|
772
|
-
"order_id": "ORD2", "order_ref": "ORD2", "security": "600000.SH",
|
|
773
|
-
"side": data.get("side"), "amount": data.get("amount"),
|
|
774
|
-
"price": data.get("price"),
|
|
775
|
-
}})
|
|
776
|
-
return
|
|
777
|
-
if "/cancel_order" in self.path:
|
|
778
|
-
self._send(200, {"ok": True, "value": {"cancelled": True}})
|
|
779
|
-
return
|
|
780
|
-
self._send(404, {"ok": False, "code": "NOT_FOUND"})
|
|
781
|
-
|
|
782
|
-
server = HTTPServer(("127.0.0.1", 0), _MockBridge)
|
|
783
|
-
port = server.server_address[1]
|
|
784
|
-
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
785
|
-
print(f"[self-check] mock bridge_server at http://127.0.0.1:{port}")
|
|
786
|
-
|
|
787
|
-
# 1. symbol 映射 (无 akquant 依赖)
|
|
788
|
-
assert _to_qmt_symbol("sh600000") == "600000.SH"
|
|
789
|
-
assert _to_qmt_symbol("600000.XSHG") == "600000.SH"
|
|
790
|
-
assert _to_qmt_symbol("sz000001") == "000001.SZ"
|
|
791
|
-
assert _to_qmt_symbol("000001.XSHE") == "000001.SZ"
|
|
792
|
-
assert _from_qmt_symbol("600000.XSHG") == "600000.SH"
|
|
793
|
-
assert _from_qmt_symbol("600000.SH") == "600000.SH"
|
|
794
|
-
assert _from_qmt_symbol("000001.XSHE") == "000001.SZ"
|
|
795
|
-
assert _from_qmt_symbol("000001.SZ") == "000001.SZ"
|
|
796
|
-
print("[self-check] OK symbol 映射")
|
|
797
|
-
|
|
798
|
-
# 2. HTTP client — 6 个 endpoint shape 验证
|
|
799
|
-
http = _HTTP(f"http://127.0.0.1:{port}", timeout=2.0)
|
|
800
|
-
acct = http.post("/account", {"account_id": "ACCT", "account_type": "stock"})
|
|
801
|
-
assert acct["available_cash"] == 50000.0, acct
|
|
802
|
-
pos = http.post("/positions", {"account_id": "ACCT"})
|
|
803
|
-
assert pos["positions"][0]["security"] == "600000.SH", pos
|
|
804
|
-
order = http.post("/place_order", {
|
|
805
|
-
"account_id": "ACCT", "security": "600000.SH", "side": "BUY",
|
|
806
|
-
"amount": 100, "price": 10.0,
|
|
807
|
-
})
|
|
808
|
-
assert order["order_id"] == "ORD2", order
|
|
809
|
-
http.post("/cancel_order", {"account_id": "ACCT", "order_id": "ORD2"})
|
|
810
|
-
orders = http.get("/orders", {"account_id": "ACCT", "order_id": "ORD1"})
|
|
811
|
-
assert orders["orders"][0]["raw_status"] == 6
|
|
812
|
-
trades = http.get("/trades", {"account_id": "ACCT"})
|
|
813
|
-
assert trades["trades"][0]["trade_id"] == "TRD1"
|
|
814
|
-
print("[self-check] OK HTTP client shape")
|
|
815
|
-
|
|
816
|
-
# 2.5 retry — /flaky 前 pending 次 503, Retry 自动重试后 200 (返回 attempts=total+1)
|
|
817
|
-
flaky = http.get("/flaky", {"account_id": "ACCT"})
|
|
818
|
-
assert isinstance(flaky, dict) and flaky.get("attempts") == _RETRY_TOTAL + 1, flaky
|
|
819
|
-
print("[self-check] OK transient retry (503 自动重试后成功)")
|
|
820
|
-
|
|
821
|
-
# 3. akquant 协议 — 检查 TraderGatewayBase 子类化 + 方法齐
|
|
822
|
-
try:
|
|
823
|
-
ak = _import_akquant_gateway()
|
|
824
|
-
except ImportError as e:
|
|
825
|
-
print(f"[self-check] akquant 未装 ({e.__cause__ or e}) — 跳过协议层验证")
|
|
826
|
-
else:
|
|
827
|
-
QmtTraderGateway = _make_trader_gateway()
|
|
828
|
-
# 实例化 (不连, 只查 shape). paper=False 让 e2e 走真 HTTP place_order 路径.
|
|
829
|
-
gw = QmtTraderGateway(
|
|
830
|
-
base_url=f"http://127.0.0.1:{port}", account_id="ACCT", account_type="stock",
|
|
831
|
-
timeout=2.0, paper=False,
|
|
832
|
-
)
|
|
833
|
-
for name in (
|
|
834
|
-
"connect", "disconnect", "start", "place_order", "cancel_order",
|
|
835
|
-
"query_order", "query_trades", "query_account", "query_positions",
|
|
836
|
-
"get_capabilities", "heartbeat", "on_order", "on_trade",
|
|
837
|
-
"on_execution_report", "record_broker_order", "client_order_id_for",
|
|
838
|
-
):
|
|
839
|
-
assert hasattr(gw, name), f"missing {name}"
|
|
840
|
-
cap = gw.get_capabilities()
|
|
841
|
-
assert cap.broker_name == "qmt"
|
|
842
|
-
assert "qmt_bridge" in cap.features
|
|
843
|
-
print("[self-check] OK akquant 协议点齐 (TraderGatewayBase 子类)")
|
|
844
|
-
# 4. e2e: 真实 connect / place / cancel / query
|
|
845
|
-
gw.connect()
|
|
846
|
-
a = gw.query_account()
|
|
847
|
-
assert a.account_id == "ACCT" and a.available_cash == 50000.0, a
|
|
848
|
-
ps = gw.query_positions()
|
|
849
|
-
assert len(ps) == 1 and ps[0].symbol == "600000.SH", ps
|
|
850
|
-
oid = gw.place_order(ak["UnifiedOrderRequest"](
|
|
851
|
-
client_order_id="coid-test", symbol="600000.XSHG", side="buy",
|
|
852
|
-
quantity=100, price=10.0, order_type="Limit",
|
|
853
|
-
))
|
|
854
|
-
assert oid == "ORD2", oid
|
|
855
|
-
gw.cancel_order(oid)
|
|
856
|
-
snap = gw.query_order("ORD1")
|
|
857
|
-
# raw_status=6 → Filled, 但 query_order 不一定拿到 coid 反查 (mock 返回 ORD1 没在 record 里)
|
|
858
|
-
# 测试 record_broker_order + 反查
|
|
859
|
-
gw.record_broker_order("ORD1", "coid-test")
|
|
860
|
-
snap2 = gw.query_order("ORD1")
|
|
861
|
-
assert snap2 is not None and snap2.symbol == "600000.SH", snap2
|
|
862
|
-
print("[self-check] OK e2e connect→place→cancel→query")
|
|
863
|
-
|
|
864
|
-
server.shutdown()
|
|
865
|
-
print("[self-check] ALL PASSED")
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import hamuna_qmt_market # noqa: F401
|
|
10
10
|
run_live(market_broker="qmt_market", trader_broker="qmt", ...,
|
|
11
11
|
qmt_base_url="http://127.0.0.1:9000",
|
|
12
|
-
qmt_account_id="8888888888"
|
|
12
|
+
qmt_account_id="8888888888")
|
|
13
13
|
|
|
14
14
|
bridge_server 端点:
|
|
15
15
|
GET /data/snapshot?securities=<sym1>,<sym2>,... → 全推实时 tick (QMT get_full_tick)
|
|
@@ -29,7 +29,7 @@ akquant MarketGateway 接口:
|
|
|
29
29
|
"今日累计" bar 持续更新直到收盘 — QMT 默认行为, 适合日线择时).
|
|
30
30
|
|
|
31
31
|
Round 33 双源验证 (server 数据源):
|
|
32
|
-
dual_source="off" : 单源 QMT (
|
|
32
|
+
dual_source="off" : 单源 QMT (默认).
|
|
33
33
|
dual_source="verify": 双源 QMT + server 并行拉, 不一致 server 为主 + log warn.
|
|
34
34
|
broker_live 默认开 (runner.py _live_safety_defaults).
|
|
35
35
|
dual_source="fallback": QMT 主, server 备; server 仅在 QMT 失败时启用.
|
|
@@ -40,8 +40,6 @@ Round 33 双源验证 (server 数据源):
|
|
|
40
40
|
设计原则:
|
|
41
41
|
- thin client: 翻译 + HTTP, 不存业务逻辑
|
|
42
42
|
- 跟 broker 同 _HTTP / register / factory 套路 (requests.Session + Retry transient 自动重试)
|
|
43
|
-
- paper 模式: 不下单, 行情全真 (snapshot/history 都是只读) — paper e2e 仍能验
|
|
44
|
-
真桥 7 个端点 (5 交易 + 2 行情)
|
|
45
43
|
"""
|
|
46
44
|
from __future__ import annotations
|
|
47
45
|
|
|
@@ -50,7 +48,7 @@ __version__ = "0.1.0"
|
|
|
50
48
|
# ============================================================
|
|
51
49
|
# 共享 HTTP client — 跟 broker 模块同一份, 不重复实现
|
|
52
50
|
# ============================================================
|
|
53
|
-
from .qmt_broker import _HTTP, BrokerHTTPError
|
|
51
|
+
from .qmt_broker import _HTTP, BrokerHTTPError # noqa: F401 re-used
|
|
54
52
|
from .server_market_client import ServerMarketClient, OneMinuteAggregator
|
|
55
53
|
|
|
56
54
|
import threading
|
|
@@ -86,7 +84,7 @@ def _make_market_gateway() -> Any:
|
|
|
86
84
|
poll_interval: float = 30.0, # 默认 30s/帧 (≈2/min) — 用户决策: 不打 QMT 限流; 想更快自己 CLI 透传
|
|
87
85
|
timeout: float = 5.0,
|
|
88
86
|
symbols: list[str] | None = None, # 启动时 auto-subscribe (来自 build_qmt_market 的 symbols)
|
|
89
|
-
subscribe: bool =
|
|
87
|
+
subscribe: bool = True, # Round 18.63.18: paper 已删, broker_live 恒 True — 透传给 /data/history
|
|
90
88
|
# Round 33: server 数据源 + 双源验证
|
|
91
89
|
server_base_url: str | None = None, # None → 不开 server 源 (单源 QMT)
|
|
92
90
|
server_api_key: str | None = None, # None → runtime.http_client._load_token() 自动读
|
|
@@ -484,9 +482,8 @@ def _make_market_gateway() -> Any:
|
|
|
484
482
|
def _fetch_bars(self, symbol: str, count: int | None = None) -> list[dict[str, Any]]:
|
|
485
483
|
"""GET /data/history?security=...&period=1d&count=N[&subscribe=T/F] → list of bar dict (旧→新).
|
|
486
484
|
|
|
487
|
-
subscribe: paper
|
|
488
|
-
|
|
489
|
-
避免 count=-1 + 空时间窗只返本地最新 1 条 snapshot 的问题.
|
|
485
|
+
subscribe: Round 18.63.18 paper 已删 — broker_live 恒 True 让 QMT
|
|
486
|
+
实时推新 bar, 避免 count=-1 + 空时间窗只返本地最新 1 条 snapshot 的问题.
|
|
490
487
|
|
|
491
488
|
Round 33 双源 (dual_source="verify"): QMT 历史 bar 为主, server
|
|
492
489
|
(容维 /StockBars 通过 backend /market/bars) 拉同样窗口. period=1m
|
|
@@ -496,8 +493,8 @@ def _make_market_gateway() -> Any:
|
|
|
496
493
|
cnt = count if count is not None else self._bar_count
|
|
497
494
|
params = {"security": qmt_sym, "period": self._period,
|
|
498
495
|
"count": str(cnt), "fq": "qfq"}
|
|
499
|
-
# Round 32: 显式带 subscribe, 不靠 bridge 默认 (已翻 True).
|
|
500
|
-
# →
|
|
496
|
+
# Round 32: 显式带 subscribe, 不靠 bridge 默认 (已翻 True). broker_live
|
|
497
|
+
# → true (实时推新 bar); 显式 market_subscribe=0 可关 (纯历史拉取).
|
|
501
498
|
params["subscribe"] = "true" if self._subscribe else "false"
|
|
502
499
|
resp = self._http.get("/data/history", params)
|
|
503
500
|
value = _unwrap_envelope(resp)
|
|
@@ -784,10 +781,11 @@ def build_qmt_market(feed: Any, symbols: list[str], use_aggregator: bool, **kwar
|
|
|
784
781
|
market_bar_count (default 20) — warmup 拉几根
|
|
785
782
|
market_poll (default 30.0) — 轮询秒数 (用户决策 2026-08-18: ≥30s)
|
|
786
783
|
market_timeout (default 5.0) — HTTP 超时
|
|
787
|
-
market_subscribe (default
|
|
788
|
-
(paper
|
|
784
|
+
market_subscribe (default True) — /data/history subscribe= 参数
|
|
785
|
+
(Round 18.63.18: paper 已删, broker_live
|
|
786
|
+
恒 True; 显式 "0"/"false" 可关纯历史拉取)
|
|
789
787
|
透传方式: --gateway-options
|
|
790
|
-
"market_subscribe=
|
|
788
|
+
"market_subscribe=0" (关)
|
|
791
789
|
server_base_url (default None) — Round 33: server 数据源 base URL
|
|
792
790
|
(None → 不开 server 源)
|
|
793
791
|
server_api_key (default None) — Round 33: 显式 API key; None → 自动
|
|
@@ -805,9 +803,10 @@ def build_qmt_market(feed: Any, symbols: list[str], use_aggregator: bool, **kwar
|
|
|
805
803
|
bar_count = int(kwargs.get("market_bar_count") or kwargs.get("bar_count") or 20)
|
|
806
804
|
poll = float(kwargs.get("market_poll") or kwargs.get("poll_interval") or 30.0)
|
|
807
805
|
timeout = float(kwargs.get("market_timeout") or kwargs.get("timeout") or 30.0) # ponytail: 跟 _HTTP.timeout 默认 30s 保持一致 (caller 显式传 timeout=5.0 会盖过这里默认, 但 build_qmt_market 不传 timeout 时必须 30s 才治网络抖动)
|
|
808
|
-
# market_subscribe:
|
|
809
|
-
|
|
810
|
-
|
|
806
|
+
# market_subscribe: Round 18.63.18 paper 已删, broker_live 恒 True (实时推新 bar);
|
|
807
|
+
# 显式 "0"/"false"/"no"/"off" 可关闭 (纯历史拉取, 防误开 QMT 订阅).
|
|
808
|
+
subscribe_raw = kwargs.get("market_subscribe") or kwargs.get("subscribe") or "1"
|
|
809
|
+
subscribe = not str(subscribe_raw).strip().lower() in ("0", "false", "no", "off", "")
|
|
811
810
|
# Round 33 双源 kwargs
|
|
812
811
|
server_base_url = kwargs.get("server_base_url")
|
|
813
812
|
server_api_key = kwargs.get("server_api_key")
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import sys
|
|
12
|
-
import json # Round 18.62 P0-1: replay metrics 落盘
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
from typing import Any
|
|
15
14
|
|
|
@@ -133,9 +132,8 @@ def _fetch_bridge_history(bridge_url: str, sym: str, period: str, count: int,
|
|
|
133
132
|
bridge 不通 / 超时 / 非 200 直接 raise LiveRunError, 不静默退化 (用户原话:
|
|
134
133
|
策略出错直接报错停止, 不走 mock 兜底).
|
|
135
134
|
|
|
136
|
-
subscribe:
|
|
137
|
-
|
|
138
|
-
count=-1 + start/end 全空时只返回本地最新 1 条 snapshot 的问题.
|
|
135
|
+
subscribe: broker_live 恒 True (Round 18.63.18: paper 已删) — 让 QMT 实时
|
|
136
|
+
推新 bar, 避免 count=-1 + start/end 全空时只返回本地最新 1 条 snapshot 的问题.
|
|
139
137
|
"""
|
|
140
138
|
from urllib.parse import urlencode
|
|
141
139
|
from urllib.request import urlopen, Request
|
|
@@ -145,7 +143,7 @@ def _fetch_bridge_history(bridge_url: str, sym: str, period: str, count: int,
|
|
|
145
143
|
canonical = canonical[2:]
|
|
146
144
|
params = {"security": canonical, "period": period, "count": str(count), "fq": "qfq"}
|
|
147
145
|
# Round 32: bridge_server subscribe 默认翻 True (qmt-docs 官方对齐) 后, 这里不能
|
|
148
|
-
# 省掉 query 靠默认 —
|
|
146
|
+
# 省掉 query 靠默认 — 显式带 subscribe 防误开/误关 QMT 订阅
|
|
149
147
|
# (Round 27 300 只批量防卡护栏从 bridge 默认移到调用端显式 false).
|
|
150
148
|
params["subscribe"] = "true" if subscribe else "false"
|
|
151
149
|
q = urlencode(params)
|
|
@@ -192,7 +190,7 @@ def _fetch_bridge_history_batch(bridge_url: str, symbols: list[str], period: str
|
|
|
192
190
|
优先使用 bridge_server 的批量 multi 接口 (一次 HTTP 拉多只), 大幅减少
|
|
193
191
|
逐只 HTTP 开销 (300 只从 ~27s 降到 ~1s). 失败时返回 {} 让调用方回退逐只.
|
|
194
192
|
|
|
195
|
-
subscribe 语义同 _fetch_bridge_history (
|
|
193
|
+
subscribe 语义同 _fetch_bridge_history (broker_live 恒 True, Round 18.63.18).
|
|
196
194
|
"""
|
|
197
195
|
from urllib.parse import urlencode
|
|
198
196
|
from urllib.request import urlopen, Request
|
|
@@ -252,7 +250,7 @@ def _fetch_live_factors(bridge_url: str, universe: list[str], period: str = "1d"
|
|
|
252
250
|
4. 单只失败的片回退逐只 (兼容旧 bridge_server)
|
|
253
251
|
|
|
254
252
|
subscribe 透传给 _fetch_bridge_history / _fetch_bridge_history_batch —
|
|
255
|
-
|
|
253
|
+
broker_live 恒 True (Round 18.63.18: paper 已删).
|
|
256
254
|
"""
|
|
257
255
|
import pandas as _pd
|
|
258
256
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -317,16 +315,15 @@ def _fetch_live_factors(bridge_url: str, universe: list[str], period: str = "1d"
|
|
|
317
315
|
return factors
|
|
318
316
|
|
|
319
317
|
|
|
320
|
-
def _build_instruments(symbols: list[str] | None
|
|
318
|
+
def _build_instruments(symbols: list[str] | None) -> list[Any] | None:
|
|
321
319
|
"""symbols 列表 → akquant.Instrument 列表. None → None (全市场, 让策略自行 subscribe).
|
|
322
320
|
|
|
323
|
-
normalize=True: 裸码 → 带后缀 (replay 数据来自 prebuilt, stockCode 是 '600000.SH'
|
|
324
|
-
形态, instrument symbol 必须一致才匹配 ReplayMarketGateway 的订阅过滤).
|
|
325
|
-
|
|
326
321
|
Round 18.59: 按 6 位代码前缀推断 AssetType — akquant 0.3.x AssetType 枚举没 ETF 也没 Bond,
|
|
327
322
|
510/511/512/515/159 (沪/深 ETF) → Fund; 113/127/128 (可转债) → Fund; 其他 6 位 → Stock.
|
|
328
323
|
之前硬写 AssetType.Stock 让 351 个 ETF 被 broker 内部按 asset_type 分流时丢弃 → universe=[].
|
|
329
324
|
ponytail: akquant 0.4.x 若新增 AssetType.ETF / AssetType.Bond 则此映射需重审.
|
|
325
|
+
|
|
326
|
+
Round 18.63.18: normalize 参数已删 (曾用于 replay prebuilt 裸码归一, replay 已删).
|
|
330
327
|
"""
|
|
331
328
|
if not symbols:
|
|
332
329
|
return None
|
|
@@ -336,14 +333,8 @@ def _build_instruments(symbols: list[str] | None, normalize: bool = False) -> li
|
|
|
336
333
|
_ETF_PREFIXES = ("510", "511", "512", "515", "159")
|
|
337
334
|
_BOND_PREFIXES = ("113", "127", "128") # 可转债 (沪 113 / 深 127 / 沪 128)
|
|
338
335
|
|
|
339
|
-
def _sym(s: str) -> str:
|
|
340
|
-
if normalize:
|
|
341
|
-
from ..akquant_schema_adapter import normalize_symbol
|
|
342
|
-
return normalize_symbol(s)
|
|
343
|
-
return s
|
|
344
|
-
|
|
345
336
|
def _asset_type(bare: str) -> Any:
|
|
346
|
-
#
|
|
337
|
+
# 6 位裸码或 600000.SH 形态 — 都按前 3 位判
|
|
347
338
|
code6 = bare.split(".")[0][-6:] if "." in bare else bare[-6:]
|
|
348
339
|
if not (len(code6) == 6 and code6.isdigit()):
|
|
349
340
|
return AssetType.Stock # 非法 6 位 fallback, 不阻塞启动
|
|
@@ -354,38 +345,7 @@ def _build_instruments(symbols: list[str] | None, normalize: bool = False) -> li
|
|
|
354
345
|
return AssetType.Fund
|
|
355
346
|
return AssetType.Stock
|
|
356
347
|
|
|
357
|
-
return [Instrument(symbol=
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def _recent_real_bars(symbols: list[str], days: int = 30) -> tuple[Any, str]:
|
|
361
|
-
"""最近 N 天真实日线 → replay bars DataFrame (akquant dataframe_to_bars 格式).
|
|
362
|
-
|
|
363
|
-
数据源 = 本地 prebuilt bundle (真实历史, 与回测同源; bundle 覆盖到最新交易日).
|
|
364
|
-
返 (df, src): df 列 = date/open/high/low/close/volume/symbol + "股票代码" (多标的
|
|
365
|
-
识别必需, dataframe_to_bars normalize.py 只认这个列名).
|
|
366
|
-
|
|
367
|
-
Raises:
|
|
368
|
-
LiveRunError: 无 --symbols / 数据不可达 (提示先 dataset fetch).
|
|
369
|
-
"""
|
|
370
|
-
from datetime import datetime, timedelta
|
|
371
|
-
|
|
372
|
-
from ..prebuilt_resolver import resolve
|
|
373
|
-
|
|
374
|
-
if not symbols:
|
|
375
|
-
raise LiveRunError(
|
|
376
|
-
"broker=replay 需要 --symbols 限定回放标的 (从本地 prebuilt 取最近真实数据)"
|
|
377
|
-
)
|
|
378
|
-
end = datetime.now().strftime("%Y%m%d")
|
|
379
|
-
start = (datetime.now() - timedelta(days=days)).strftime("%Y%m%d")
|
|
380
|
-
df, src = resolve(symbols, start, end)
|
|
381
|
-
if df is None or len(df) == 0:
|
|
382
|
-
raise LiveRunError(
|
|
383
|
-
f"最近 {days} 天无 bar 数据 (universe={symbols}, {start}~{end}, src={src}). "
|
|
384
|
-
f"先 `hamuna_quant_cli dataset fetch --symbols {','.join(symbols)}` 下载 prebuilt"
|
|
385
|
-
)
|
|
386
|
-
out = df.copy()
|
|
387
|
-
out["股票代码"] = out["symbol"]
|
|
388
|
-
return out, src
|
|
348
|
+
return [Instrument(symbol=s, asset_type=_asset_type(s)) for s in symbols if s.strip()]
|
|
389
349
|
|
|
390
350
|
|
|
391
351
|
def configure_logging(level: str, log_file: Path | None) -> None:
|
|
@@ -417,7 +377,7 @@ def _with_live_history_depth(
|
|
|
417
377
|
wrapper 在 on_start 里按日历注册周五 14:55 的 schedule() 定时器 (payload
|
|
418
378
|
'rebalance') — 这正是 akquant live 缺的横截面周频触发:live 不注入
|
|
419
379
|
_trading_days (on_cross_section 静默死) 且 schedule_daily 堕入真实墙钟
|
|
420
|
-
(replay
|
|
380
|
+
(replay 已删 18.63.18), 手动 schedule() 绝对 ts 是唯一通路 (2026-08-27 探针实测触发).
|
|
421
381
|
|
|
422
382
|
params: 用户实盘参数 (params.json strategy_params) — 作构造 kwargs 注入.
|
|
423
383
|
live 端 _build_strategy_instance 是 `cls()` 无参实例化, 用户参数经
|
|
@@ -562,14 +522,13 @@ def _friday_date_strings(calendar: list[str] | None) -> list[str]:
|
|
|
562
522
|
def _calendar_from_df(df: Any, symbols: list[str] | None = None) -> list[str] | None:
|
|
563
523
|
"""从实盘 startup 拉到的历史数据提取真实交易日列表 (YYYY-MM-DD 去重排序).
|
|
564
524
|
|
|
565
|
-
|
|
566
|
-
- replay: 单 bars_df (列含 date/时间)
|
|
525
|
+
形态 (Round 18.63.18: replay 已删, 只剩 bridge):
|
|
567
526
|
- bridge: {sym: DataFrame} dict (列名 bridge 透传, 可能 date/stime/time/datetime)
|
|
568
527
|
|
|
569
528
|
返回 None 表示无可用日期列 (跳过日历注入). 日期列探测优先级
|
|
570
529
|
date > datetime > stime > time. bridge /data/history 的 bar 无 date 列,
|
|
571
530
|
stime 是 'YYYYMMDD' 字符串 (真机实测 2026-08-28), time 是毫秒时间戳 —
|
|
572
|
-
time 必须 unit='ms' 解析, 否则 to_datetime 默认 ns 单位得 1970-01-01 垃圾日 (Round 18.23
|
|
531
|
+
time 必须 unit='ms' 解析, 否则 to_datetime 默认 ns 单位得 1970-01-01 垃圾日 (Round 18.23 的坑).
|
|
573
532
|
"""
|
|
574
533
|
import datetime as _dt
|
|
575
534
|
import pandas as _pd
|
|
@@ -620,20 +579,21 @@ def _live_safety_defaults(
|
|
|
620
579
|
gateway_options: dict[str, str],
|
|
621
580
|
duration: str,
|
|
622
581
|
) -> str:
|
|
623
|
-
"""P0-
|
|
582
|
+
"""P0-2 实盘安全默认: broker_live+qmt 永久运行 + dual_source 验证.
|
|
624
583
|
|
|
625
|
-
|
|
584
|
+
Round 18.63.18 (用户拍板): 去掉 qmt_paper mock 下单路径 — QMT 客户端原生
|
|
585
|
+
有「模拟信号模式」(GUI 手动切), CLI 永远发真 passorder, 模拟由 QMT 侧承担.
|
|
586
|
+
返回修正后的 duration.
|
|
626
587
|
"""
|
|
627
588
|
if mode == "broker_live" and broker == "qmt":
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
)
|
|
589
|
+
# Round 33: broker_live 默认开 dual_source=verify (QMT + server 端双源验证).
|
|
590
|
+
# server_base_url 不显式传 → qmt_market 里 server=None → 退化为单源 QMT (旧路径).
|
|
591
|
+
# user 想开 server 验证 → --gateway-options "dual_source=verify,server_base_url=http://..."
|
|
592
|
+
# 或 env HAMUNA_SERVER 让 server_market_client 自动读.
|
|
593
|
+
if gateway_options.get("dual_source") is None:
|
|
594
|
+
gateway_options["dual_source"] = "verify"
|
|
595
|
+
print("[info] broker_live 默认 dual_source=verify (QMT+server 双源); "
|
|
596
|
+
"传 dual_source=off 可关闭", flush=True)
|
|
637
597
|
|
|
638
598
|
if duration in ("1h", "0", "0s", "0m", "0h", ""):
|
|
639
599
|
# Round 18.63.14: "0"/"1h" = 永久运行. 之前返回 "0" 会被 akquant
|
|
@@ -644,15 +604,6 @@ def _live_safety_defaults(
|
|
|
644
604
|
print("[info] broker_live 默认 duration=0 (永久运行); 如需限时显式传 --duration", flush=True)
|
|
645
605
|
return ""
|
|
646
606
|
print(f"[warn] broker_live 使用显式 duration={duration} — 到期自动停止", flush=True)
|
|
647
|
-
|
|
648
|
-
# Round 33: broker_live 默认开 dual_source=verify (QMT + server 端双源验证).
|
|
649
|
-
# server_base_url 不显式传 → qmt_market 里 server=None → 退化为单源 QMT (旧路径).
|
|
650
|
-
# user 想开 server 验证 → --gateway-options "dual_source=verify,server_base_url=http://..."
|
|
651
|
-
# 或 env HAMUNA_SERVER 让 server_market_client 自动读.
|
|
652
|
-
if gateway_options.get("dual_source") is None:
|
|
653
|
-
gateway_options["dual_source"] = "verify"
|
|
654
|
-
print("[info] broker_live 默认 dual_source=verify (QMT+server 双源); "
|
|
655
|
-
"传 dual_source=off 可关闭", flush=True)
|
|
656
607
|
return duration
|
|
657
608
|
|
|
658
609
|
|
|
@@ -668,14 +619,13 @@ def run_live(
|
|
|
668
619
|
log_level: str,
|
|
669
620
|
log_file: Path | None,
|
|
670
621
|
market_broker: str | None,
|
|
671
|
-
replay_days: int = 30,
|
|
672
622
|
params: dict | None = None,
|
|
673
623
|
) -> None:
|
|
674
624
|
"""翻译 + 转发到 akquant.run_live.
|
|
675
625
|
|
|
676
626
|
spec: loader 加载出的 StrategySpec (class 或 functional)
|
|
677
|
-
mode: "
|
|
678
|
-
broker: "
|
|
627
|
+
mode: "broker_live" (Round 18.63.18: paper 已删 — QMT 原生模拟信号模式)
|
|
628
|
+
broker: "qmt" / 自定义 broker 名字 (replay 已删)
|
|
679
629
|
market_broker: 独立行情 broker id (e.g. "qmt_market"); None=单 broker
|
|
680
630
|
当 market_broker 设了, akquant 要求 trader_broker 也设 (二者成对, broker 字段被忽略).
|
|
681
631
|
我们固定配对: market_broker="qmt_market" + trader_broker="qmt".
|
|
@@ -692,43 +642,13 @@ def run_live(
|
|
|
692
642
|
resolved_mp = _resolve_market_period(spec, gateway_options, params)
|
|
693
643
|
if resolved_mp is not None:
|
|
694
644
|
gateway_options["market_period"] = resolved_mp
|
|
695
|
-
#
|
|
696
|
-
replay_bars_df = None
|
|
697
|
-
# 真实交易日历 (YYYY-MM-DD, 从 bars_df / bridge 历史提取) — 注入 strategy wrapper
|
|
645
|
+
# 真实交易日历 (YYYY-MM-DD, 从 bridge 历史提取) — 注入 strategy wrapper
|
|
698
646
|
# 做周五 14:55 重排定时器 (Round 18.22: live 无 _trading_days, schedule_daily 墙钟死)
|
|
699
647
|
rebalance_calendar: list[str] | None = None
|
|
700
648
|
|
|
701
|
-
#
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
"broker=replay + trading_mode=broker_live 不兼容 — replay 只有行情, "
|
|
705
|
-
"无交易通道, 用 broker_live 会抛 ValueError. 改用 mode=paper 或换 broker."
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
# broker=replay → 自动注入最近 N 天真实数据 (本地 prebuilt), paper 撮合走引擎.
|
|
709
|
-
# bounded_event_total 让回放完自动停, 不依赖 --duration 墙钟.
|
|
710
|
-
if broker == "replay":
|
|
711
|
-
bars_df, replay_src = _recent_real_bars(symbols, days=replay_days)
|
|
712
|
-
replay_bars_df = bars_df # 供 v2 architecture compute_factors 复用
|
|
713
|
-
instruments = _build_instruments(sorted(bars_df["symbol"].unique()), normalize=True)
|
|
714
|
-
gateway_options["bars"] = bars_df
|
|
715
|
-
print(
|
|
716
|
-
f"[info] replay 数据: {replay_src} · {len(bars_df)} bars · "
|
|
717
|
-
f"{sorted(bars_df['symbol'].unique())} (最近 {replay_days} 天)",
|
|
718
|
-
flush=True,
|
|
719
|
-
)
|
|
720
|
-
if len(bars_df) < 60:
|
|
721
|
-
print(
|
|
722
|
-
f"[warn] bars < 60 — 有 warmup 的策略可能全程不触发 on_bar (0 交易). "
|
|
723
|
-
f"可加 --replay-days 扩大窗口 (e.g. --replay-days 180)",
|
|
724
|
-
flush=True,
|
|
725
|
-
)
|
|
726
|
-
else:
|
|
727
|
-
instruments = _build_instruments(symbols)
|
|
728
|
-
# run_live 始终要求至少 1 个 instrument (不是可选) — 不传 --symbols 时
|
|
729
|
-
# 用占位 sh600000, paper 模式 broker=qmt 不真下单, smoke 能跑通.
|
|
730
|
-
if not instruments:
|
|
731
|
-
instruments = _build_instruments(["sh600000"])
|
|
649
|
+
# Round 18.63.18 (用户拍板): broker=replay 本地回放已删 — QMT 客户端原生
|
|
650
|
+
# 模拟信号模式承担"无真盘验证", CLI live run 只走 broker_live+qmt 真下单.
|
|
651
|
+
instruments = _build_instruments(symbols)
|
|
732
652
|
configure_logging(log_level, log_file)
|
|
733
653
|
|
|
734
654
|
# broker="qmt" → import 触发 register_broker("qmt", ...), 校验 qmt_account_id 必填.
|
|
@@ -771,10 +691,7 @@ def run_live(
|
|
|
771
691
|
# functional mode / 无 bridge_url 跳过 (兼容老策略).
|
|
772
692
|
if spec.mode == "class" and spec.strategy_cls is not None:
|
|
773
693
|
qmt_base_url = gateway_options.get("qmt_base_url")
|
|
774
|
-
|
|
775
|
-
# 两者都走 compute_factors / filter_symbols (v2 architecture).
|
|
776
|
-
has_replay_data = replay_bars_df is not None
|
|
777
|
-
if qmt_base_url or has_replay_data:
|
|
694
|
+
if qmt_base_url:
|
|
778
695
|
try:
|
|
779
696
|
strat_inst = spec.strategy_cls()
|
|
780
697
|
except Exception as e:
|
|
@@ -802,30 +719,24 @@ def run_live(
|
|
|
802
719
|
# 策略直接跳过 → calendar=[] → fridays=[] → 0 个周五百排定时器 (实测 bn9d2aa9u).
|
|
803
720
|
rebalance_calendar: list[str] | None = None
|
|
804
721
|
raw_factors: dict[str, Any] = {} # 兜底初值: 下面 if 不走时空 dict
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
raise
|
|
824
|
-
except Exception as e:
|
|
825
|
-
raise LiveRunError(
|
|
826
|
-
f"实盘拉历史失败 (rebalance_calendar): {type(e).__name__}: {e} "
|
|
827
|
-
f"(用户原话: 出错直接报错停止, 不走 mock 兜底)"
|
|
828
|
-
) from e
|
|
722
|
+
try:
|
|
723
|
+
# broker_live 必须 subscribe=True 才能让 QMT 实时推新 bar
|
|
724
|
+
# (避免 count=-1 + 空时间窗只返本地最新 1 条 snapshot 的问题).
|
|
725
|
+
raw_factors = _fetch_live_factors(qmt_base_url, symbols, subscribe=True)
|
|
726
|
+
rebalance_calendar = _calendar_from_df(raw_factors, symbols)
|
|
727
|
+
if rebalance_calendar:
|
|
728
|
+
print(
|
|
729
|
+
f"[info] 交易日历注入: {len(rebalance_calendar)} days, "
|
|
730
|
+
f"{len(_friday_date_strings(rebalance_calendar))} 周五 → 14:55 重排定时器",
|
|
731
|
+
flush=True,
|
|
732
|
+
)
|
|
733
|
+
except LiveRunError:
|
|
734
|
+
raise
|
|
735
|
+
except Exception as e:
|
|
736
|
+
raise LiveRunError(
|
|
737
|
+
f"实盘拉历史失败 (rebalance_calendar): {type(e).__name__}: {e} "
|
|
738
|
+
f"(用户原话: 出错直接报错停止, 不走 mock 兜底)"
|
|
739
|
+
) from e
|
|
829
740
|
|
|
830
741
|
if not wants_precompute:
|
|
831
742
|
print(
|
|
@@ -837,7 +748,7 @@ def run_live(
|
|
|
837
748
|
factors: dict[str, Any] = {}
|
|
838
749
|
if hasattr(strat_inst, "compute_factors"):
|
|
839
750
|
# 实盘 startup: 拉 N sym × N bar 历史, 拼 {sym: DataFrame} 喂 compute_factors
|
|
840
|
-
# qmt: bridge /data/history
|
|
751
|
+
# qmt: bridge /data/history (Round 18.63.18: replay 已删).
|
|
841
752
|
# raw_factors / rebalance_calendar 已在上面分支统一拉, 此处复用.
|
|
842
753
|
try:
|
|
843
754
|
factors = strat_inst.compute_factors(raw_factors) or {}
|
|
@@ -882,7 +793,7 @@ def run_live(
|
|
|
882
793
|
instruments = _build_instruments(filtered)
|
|
883
794
|
else:
|
|
884
795
|
print(
|
|
885
|
-
"[info] class mode 无 qmt_base_url
|
|
796
|
+
"[info] class mode 无 qmt_base_url; 跳过 compute_factors / filter_symbols",
|
|
886
797
|
flush=True,
|
|
887
798
|
)
|
|
888
799
|
elif spec.mode == "functional":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hamuna-quant-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.25
|
|
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
|
|
@@ -130,11 +130,8 @@ hamuna_quant_cli upload --name "v2_smoke" --config /tmp/c.json --code /tmp/s.py
|
|
|
130
130
|
```bash
|
|
131
131
|
# 1) 写策略 (同上)
|
|
132
132
|
|
|
133
|
-
# 2)
|
|
134
|
-
|
|
135
|
-
--mode paper --broker replay --symbols sh600000,sz600036 --duration 30s
|
|
136
|
-
|
|
137
|
-
# 3) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
133
|
+
# 2) qmt 实盘 (需配 broker=qmt + market_broker=qmt_market + gateway_options)
|
|
134
|
+
# Round 18.63.18: paper 模式已删 — 模拟由 QMT 客户端原生「模拟信号模式」承担
|
|
138
135
|
hamuna_quant_cli live run /tmp/s.py \
|
|
139
136
|
--mode broker_live --broker qmt --market-broker qmt_market \
|
|
140
137
|
--symbols sh600000,sz600036 \
|
|
@@ -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.25"
|
|
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"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""P0-2 实盘安全默认测试 (2026-08; Round 18.63.18 更新).
|
|
2
|
+
|
|
3
|
+
验证 hamuna_quant_cli.live.runner._live_safety_defaults:
|
|
4
|
+
- broker_live + qmt + duration=1h (CLI 默认) → 改为 "" (永久)
|
|
5
|
+
- broker_live + qmt + 显式 duration=2h → 保留 2h
|
|
6
|
+
- broker_live + qmt → 默认注入 dual_source=verify
|
|
7
|
+
- Round 18.63.18 (用户拍板): qmt_paper 已删 — QMT 客户端原生模拟信号模式,
|
|
8
|
+
CLI 永远发真 passorder. 不再强制/拒绝 qmt_paper, gateway_options 里出现
|
|
9
|
+
该键也不做任何处理 (build_qmt 忽略未知 kwargs).
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# 确保能 import 到 hamuna_quant_cli (仓库根)
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
sys.path.insert(0, str(ROOT))
|
|
17
|
+
|
|
18
|
+
from hamuna_quant_cli.live.runner import _live_safety_defaults # noqa: E402
|
|
19
|
+
|
|
20
|
+
def test_broker_live_qmt_default_duration_permanent():
|
|
21
|
+
opts = {}
|
|
22
|
+
duration = _live_safety_defaults("broker_live", "qmt", opts, "1h")
|
|
23
|
+
assert duration == "", "broker_live 默认 duration=1h 应改为 '' (永久运行)"
|
|
24
|
+
assert opts.get("dual_source") == "verify", "broker_live+qmt 默认开双源验证"
|
|
25
|
+
|
|
26
|
+
def test_broker_live_qmt_explicit_duration_preserved():
|
|
27
|
+
opts = {}
|
|
28
|
+
duration = _live_safety_defaults("broker_live", "qmt", opts, "2h")
|
|
29
|
+
assert duration == "2h", "显式 duration 应保留"
|
|
30
|
+
|
|
31
|
+
def test_broker_live_qmt_paper_key_ignored():
|
|
32
|
+
# Round 18.63.18: qmt_paper 概念已删, 出现也不报错 (QMT 原生模拟信号模式)
|
|
33
|
+
opts = {"qmt_paper": "1"}
|
|
34
|
+
duration = _live_safety_defaults("broker_live", "qmt", opts, "1h")
|
|
35
|
+
assert duration == ""
|
|
36
|
+
assert opts.get("dual_source") == "verify"
|
|
37
|
+
|
|
38
|
+
def test_other_mode_untouched():
|
|
39
|
+
opts = {}
|
|
40
|
+
duration = _live_safety_defaults("broker_live", "other", opts, "1h")
|
|
41
|
+
assert opts == {}, "非 qmt broker 不应注入 dual_source"
|
|
42
|
+
assert duration == "1h"
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"""P0-1/P0-2 实盘安全默认测试 (2026-08).
|
|
2
|
-
|
|
3
|
-
验证 hamuna_quant_cli.live.runner._live_safety_defaults:
|
|
4
|
-
- broker_live + qmt + 未显式 qmt_paper → 强制注入 "0" (真实下单)
|
|
5
|
-
- broker_live + qmt + 显式 qmt_paper=1 → LiveRunError
|
|
6
|
-
- broker_live + qmt + duration=1h (CLI 默认) → 改为 "0" (永久)
|
|
7
|
-
- broker_live + qmt + 显式 duration=2h → 保留 2h
|
|
8
|
-
- paper / 非 qmt → 不改 gateway_options, duration 原样
|
|
9
|
-
"""
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
# 确保能 import 到 hamuna_quant_cli (仓库根)
|
|
14
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
15
|
-
sys.path.insert(0, str(ROOT))
|
|
16
|
-
|
|
17
|
-
from hamuna_quant_cli.live.runner import LiveRunError, _live_safety_defaults # noqa: E402
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_broker_live_qmt_injects_real_trading():
|
|
21
|
-
opts = {}
|
|
22
|
-
duration = _live_safety_defaults("broker_live", "qmt", opts, "1h")
|
|
23
|
-
assert opts["qmt_paper"] == "0", "broker_live+qmt 必须强制真实下单"
|
|
24
|
-
assert duration == "0", "broker_live 默认 duration=1h 应改为 0 (永久)"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_broker_live_qmt_explicit_paper_raises():
|
|
28
|
-
import pytest
|
|
29
|
-
opts = {"qmt_paper": "1"}
|
|
30
|
-
with pytest.raises(LiveRunError, match="qmt_paper=1"):
|
|
31
|
-
_live_safety_defaults("broker_live", "qmt", opts, "1h")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_broker_live_qmt_explicit_duration_preserved():
|
|
35
|
-
opts = {}
|
|
36
|
-
duration = _live_safety_defaults("broker_live", "qmt", opts, "2h")
|
|
37
|
-
assert opts["qmt_paper"] == "0"
|
|
38
|
-
assert duration == "2h", "显式 duration 应保留"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_paper_mode_untouched():
|
|
42
|
-
opts = {}
|
|
43
|
-
duration = _live_safety_defaults("paper", "qmt", opts, "1h")
|
|
44
|
-
assert opts == {}, "paper 模式不应注入 qmt_paper"
|
|
45
|
-
assert duration == "1h"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_broker_live_non_qmt_untouched():
|
|
49
|
-
opts = {}
|
|
50
|
-
duration = _live_safety_defaults("broker_live", "replay", opts, "1h")
|
|
51
|
-
assert opts == {}, "非 qmt broker 不应注入"
|
|
52
|
-
assert duration == "1h"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/_test_akquant_parity.py
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/akquant_data_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/akquant_schema_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/cross_sectional_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/live/server_market_client.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
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli/runtime/server_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.23 → hamuna_quant_cli-0.1.25}/hamuna_quant_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|