hamuna-quant-cli 0.1.6__tar.gz → 0.1.8__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.6 → hamuna_quant_cli-0.1.8}/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/__init__.py +1 -1
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/__main__.py +46 -12
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/live/runner.py +69 -4
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/pyproject.toml +1 -1
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/MANIFEST.in +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/README.md +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/README.md +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/_market_fallback.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/_metrics_15.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/akquant_runner.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/base_strategy.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/live/__init__.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/live/loader.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/live/qmt_broker.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/live/qmt_market.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/qmt_translator.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/__init__.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/backtest.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/cache.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/discipline.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/http_client.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/s3client.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/runtime/server_client.py +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/scripts/server.json +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/requires.txt +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/setup.cfg +0 -0
- {hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/tests/test_live_safety.py +0 -0
|
@@ -30,6 +30,37 @@ def _load_config(path: str) -> dict:
|
|
|
30
30
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _load_params_arg(arg: str) -> dict | None:
|
|
34
|
+
"""Round 18.48+: --params 既支持 inline JSON (以 '{' 开头) 又支持文件路径.
|
|
35
|
+
|
|
36
|
+
历史 CLI / 回测脚本传文件路径 (`hamuna_quant_cli run --params ./params.json`).
|
|
37
|
+
desktop Rust 调 live run 时通过 `read_to_string` 把 `<name>.params.json` 读成
|
|
38
|
+
String 后 inline 传过来 (`--params '<json>'`), 旧 cli 把 inline 当文件路径
|
|
39
|
+
直接 `Path.read_text()` → FileNotFoundError (本轮 bug 报告).
|
|
40
|
+
|
|
41
|
+
协议: 以 '{' (允许前导空白) 开头 → inline JSON parse; 否则 → 文件路径 read.
|
|
42
|
+
Caller 决定 None vs raise: cmd_commit 容忍 None + 走 schema warning,
|
|
43
|
+
cmd_live_run 期望非空否则 raise FileNotFoundError.
|
|
44
|
+
"""
|
|
45
|
+
if not arg:
|
|
46
|
+
return None
|
|
47
|
+
stripped = arg.lstrip()
|
|
48
|
+
if stripped.startswith("{"):
|
|
49
|
+
import json as _json
|
|
50
|
+
try:
|
|
51
|
+
return _json.loads(arg)
|
|
52
|
+
except _json.JSONDecodeError as e:
|
|
53
|
+
raise ValueError(f"--params inline JSON 解析失败: {e}") from e
|
|
54
|
+
p = Path(arg)
|
|
55
|
+
if not p.exists():
|
|
56
|
+
raise FileNotFoundError(f"--params 不存在: {arg}")
|
|
57
|
+
import json as _json
|
|
58
|
+
try:
|
|
59
|
+
return _json.loads(p.read_text(encoding="utf-8"))
|
|
60
|
+
except _json.JSONDecodeError as e:
|
|
61
|
+
raise ValueError(f"--params 解析失败: {arg} ({e})") from e
|
|
62
|
+
|
|
63
|
+
|
|
33
64
|
def _parse_symbols(s: str) -> list[str]:
|
|
34
65
|
"""CSV symbols → 裸码 list (A 股 dataset stockCode 不带 .SH/.SZ/.BJ 后缀)."""
|
|
35
66
|
out: list[str] = []
|
|
@@ -72,18 +103,17 @@ def cmd_run(args) -> int:
|
|
|
72
103
|
# --params (params.json 固定 schema) → strategy_params 段注入 cfg,
|
|
73
104
|
# akquant_runner 展开注入 engine kwargs. 与 `live run --params` 同源:
|
|
74
105
|
# 同一份 params.json 驱动回测 + 实盘, 参数约定不再分裂 (Round 18.13).
|
|
106
|
+
# Round 18.48+: 既支持文件路径也支持 inline JSON (见 _load_params_arg).
|
|
75
107
|
if getattr(args, "params", None):
|
|
76
|
-
params_path = Path(args.params)
|
|
77
|
-
if not params_path.exists():
|
|
78
|
-
print(f"--params 不存在: {args.params}", file=sys.stderr)
|
|
79
|
-
return 2
|
|
80
|
-
import json as _json
|
|
81
108
|
try:
|
|
82
|
-
params_doc =
|
|
83
|
-
except
|
|
84
|
-
print(f"--params
|
|
109
|
+
params_doc = _load_params_arg(args.params) or {}
|
|
110
|
+
except FileNotFoundError as e:
|
|
111
|
+
print(f"--params 不存在: {e}", file=sys.stderr)
|
|
85
112
|
return 2
|
|
86
|
-
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
print(f"--params 解析失败: {e}", file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
sp = params_doc.get("strategy_params")
|
|
87
117
|
if isinstance(sp, dict):
|
|
88
118
|
cfg["strategy_params"] = sp
|
|
89
119
|
else:
|
|
@@ -91,7 +121,7 @@ def cmd_run(args) -> int:
|
|
|
91
121
|
# Round 18.14: risk_config 段 → cfg['risk_config'] → runner 透传
|
|
92
122
|
# run_backtest(risk_config={"max_position_pct": 0.1}) 原生消费 (账户级风控,
|
|
93
123
|
# 引擎 hasattr 校验 + 未知字段仅 warning). skill 桌面编辑的实盘风控真正生效.
|
|
94
|
-
rc =
|
|
124
|
+
rc = params_doc.get("risk_config")
|
|
95
125
|
if isinstance(rc, dict):
|
|
96
126
|
# 与既有 cfg['risk_config'] 合并 (显式 cfg 优先)
|
|
97
127
|
merged = dict(cfg.get("risk_config") or {})
|
|
@@ -814,9 +844,13 @@ def cmd_live_run(args) -> int:
|
|
|
814
844
|
print(f"[error] strategy.py 加载失败: {e}", file=sys.stderr)
|
|
815
845
|
return 3
|
|
816
846
|
try:
|
|
817
|
-
import json as _json
|
|
818
847
|
symbols = [s.strip() for s in args.symbols.split(",")] if args.symbols else None
|
|
819
|
-
|
|
848
|
+
# Round 18.48+: --params 既支持 inline JSON 也支持文件路径 (desktop rust read_to_string 传 inline)
|
|
849
|
+
try:
|
|
850
|
+
params = _load_params_arg(args.params) if args.params else None
|
|
851
|
+
except (FileNotFoundError, ValueError) as e:
|
|
852
|
+
print(f"[error] --params: {e}", file=sys.stderr)
|
|
853
|
+
return 2
|
|
820
854
|
# 只取 strategy_params 段 (schema 顶层: backtest / strategy_params / risk_config / meta)
|
|
821
855
|
sp = (params or {}).get("strategy_params")
|
|
822
856
|
run_live(
|
|
@@ -282,10 +282,10 @@ def configure_logging(level: str, log_file: Path | None) -> None:
|
|
|
282
282
|
def _with_live_history_depth(
|
|
283
283
|
cls: type, params: dict | None = None, calendar: list[str] | None = None
|
|
284
284
|
) -> type:
|
|
285
|
-
"""class mode 注入 set_history_depth + 交易日历重排定时器 — live
|
|
286
|
-
(engine.py:4637) 那样自动开 history tracking, 策略 on_bar 里
|
|
287
|
-
"History tracking is not enabled" (2026-08-19 实测). depth
|
|
288
|
-
warmup_period, 未设默认 120.
|
|
285
|
+
"""class mode 注入 set_history_depth + 交易日历重排定时器 + 5 类日志 hook — live
|
|
286
|
+
引擎不像回测 (engine.py:4637) 那样自动开 history tracking, 策略 on_bar 里
|
|
287
|
+
get_history 直接抛 "History tracking is not enabled" (2026-08-19 实测). depth
|
|
288
|
+
取策略 on_start 设的 warmup_period, 未设默认 120.
|
|
289
289
|
|
|
290
290
|
calendar: 真实交易日列表 (YYYY-MM-DD, 从 bars_df / bridge 历史提取). 非空时
|
|
291
291
|
wrapper 在 on_start 里按日历注册周五 14:55 的 schedule() 定时器 (payload
|
|
@@ -299,6 +299,20 @@ def _with_live_history_depth(
|
|
|
299
299
|
末尾 `model_cls(**kwargs)`, 盖 __init__ 传 kwargs 不回灌 — 2026-08-25
|
|
300
300
|
实测缺口), 因此 wrapper 保留 `__new__` override 做 params 注入.
|
|
301
301
|
|
|
302
|
+
5 类日志 hook (Round 18.55, 适应任何 class-mode 策略, 不改用户代码):
|
|
303
|
+
- [on_bar_entry] 替代 [signal_calc] — 每次 on_bar / on_tick 入口自动打
|
|
304
|
+
(含 sym / price / bar timestamp), 取代用户策略里手写
|
|
305
|
+
"我正在计算 MA5/MA20" 的样板. 用户策略内部指标计算仍可
|
|
306
|
+
用 self.log('[signal_calc] ma_fast=X ma_slow=Y') 加细节.
|
|
307
|
+
- [signal_trigger] on_order 触发前自动打 (side/sym/qty/price), 替代用户
|
|
308
|
+
自己写 "我要下单了" — 这是 risk_check / pos_check /
|
|
309
|
+
capital_check 通过后才触发, 所以 [signal_trigger] 出现 =
|
|
310
|
+
风控放行.
|
|
311
|
+
- [signal_filled] on_trade 回调自动打 (sym/qty/price), 等同成交回报.
|
|
312
|
+
- [risk_check] / [pos_check] / [capital_check] 由用户自己写 self.log(...)
|
|
313
|
+
(看不到 user 内部逻辑, wrapper 没法自动打; 例见 example_strategy.py
|
|
314
|
+
Round 18.52 的 _risk_check + 5 tick 节流).
|
|
315
|
+
|
|
302
316
|
ponytail: 只包 class mode; functional mode 用户自己管 get_history (ctx 侧).
|
|
303
317
|
日历定时器只覆盖周五重排; 非周五策略自己 on_timer 空转.
|
|
304
318
|
"""
|
|
@@ -338,6 +352,57 @@ def _with_live_history_depth(
|
|
|
338
352
|
print(f"[warn] set_history_depth({depth}) 失败: {e}", flush=True)
|
|
339
353
|
return ret_val
|
|
340
354
|
|
|
355
|
+
# ========== Round 18.55 5 类日志 hook ==========
|
|
356
|
+
# hook 在 super().xxx 调用前后自动打, 不破坏 user 的 on_bar / on_tick 逻辑.
|
|
357
|
+
# 用 try/except 包住 user 抛出的异常 — wrapper 的 log 失败不能让策略崩.
|
|
358
|
+
|
|
359
|
+
def on_bar(self, bar: _Any) -> _Any: # type: ignore[override]
|
|
360
|
+
try:
|
|
361
|
+
sym = getattr(bar, "symbol", "?")
|
|
362
|
+
ts = getattr(bar, "timestamp", 0)
|
|
363
|
+
close = float(getattr(bar, "close", 0.0) or 0.0)
|
|
364
|
+
from datetime import datetime as _dt2
|
|
365
|
+
ts_str = _dt2.fromtimestamp(ts / 1e9).strftime("%H:%M") if ts else "?"
|
|
366
|
+
print(f"[on_bar_entry] sym={sym} close={close:.3f} ts={ts_str}",
|
|
367
|
+
flush=True)
|
|
368
|
+
except Exception: # noqa: BLE001
|
|
369
|
+
pass
|
|
370
|
+
return super().on_bar(bar)
|
|
371
|
+
|
|
372
|
+
def on_tick(self, tick: _Any) -> _Any: # type: ignore[override]
|
|
373
|
+
try:
|
|
374
|
+
sym = getattr(tick, "symbol", "?")
|
|
375
|
+
price = float(getattr(tick, "price", 0.0) or 0.0)
|
|
376
|
+
print(f"[on_tick_entry] sym={sym} price={price:.3f}", flush=True)
|
|
377
|
+
except Exception: # noqa: BLE001
|
|
378
|
+
pass
|
|
379
|
+
return super().on_tick(tick)
|
|
380
|
+
|
|
381
|
+
def on_order(self, order: _Any) -> _Any: # type: ignore[override]
|
|
382
|
+
try:
|
|
383
|
+
side = getattr(order, "side", "?")
|
|
384
|
+
sym = getattr(order, "symbol", "?")
|
|
385
|
+
qty = int(getattr(order, "quantity", 0) or 0)
|
|
386
|
+
price = float(getattr(order, "price", 0.0) or 0.0)
|
|
387
|
+
print(f"[signal_trigger] side={side} sym={sym} qty={qty} "
|
|
388
|
+
f"price={price:.3f} (风控/持仓/资金 已通过)",
|
|
389
|
+
flush=True)
|
|
390
|
+
except Exception: # noqa: BLE001
|
|
391
|
+
pass
|
|
392
|
+
return super().on_order(order)
|
|
393
|
+
|
|
394
|
+
def on_trade(self, trade: _Any) -> _Any: # type: ignore[override]
|
|
395
|
+
try:
|
|
396
|
+
sym = getattr(trade, "symbol", "?")
|
|
397
|
+
qty = int(getattr(trade, "quantity", 0) or 0)
|
|
398
|
+
price = float(getattr(trade, "price", 0.0) or 0.0)
|
|
399
|
+
side = getattr(trade, "side", "?")
|
|
400
|
+
print(f"[signal_filled] side={side} sym={sym} qty={qty} "
|
|
401
|
+
f"price={price:.3f}", flush=True)
|
|
402
|
+
except Exception: # noqa: BLE001
|
|
403
|
+
pass
|
|
404
|
+
return super().on_trade(trade)
|
|
405
|
+
|
|
341
406
|
_Wrapped.__name__ = cls.__name__
|
|
342
407
|
_Wrapped.__qualname__ = cls.__qualname__
|
|
343
408
|
return _Wrapped
|
|
@@ -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.8"
|
|
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.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli/akquant_schema_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/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
|
|
File without changes
|
{hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.6 → hamuna_quant_cli-0.1.8}/hamuna_quant_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|