hamuna-quant-cli 0.1.25__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.
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/__init__.py +1 -1
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/qmt_broker.py +94 -1
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/discipline.py +119 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/PKG-INFO +1 -1
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/pyproject.toml +1 -1
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/MANIFEST.in +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/README.md +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/README.md +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/__main__.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_market_fallback.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_metrics_15.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_test_akquant_parity.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_data_adapter.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_runner.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_schema_adapter.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/base_strategy.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/cross_sectional_helpers.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/__init__.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/loader.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/qmt_market.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/runner.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/live/server_market_client.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/prebuilt_downloader.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/prebuilt_resolver.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/qmt_translator.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/__init__.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/backtest.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/cache.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/http_client.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/s3client.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/server_client.py +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/scripts/server.json +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/SOURCES.txt +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/dependency_links.txt +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/entry_points.txt +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/requires.txt +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/top_level.txt +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/setup.cfg +0 -0
- {hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/tests/test_live_safety.py +0 -0
|
@@ -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
|
-
|
|
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()
|
|
@@ -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.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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/_test_akquant_parity.py
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_data_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/akquant_schema_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/cross_sectional_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/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
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli/runtime/server_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{hamuna_quant_cli-0.1.25 → hamuna_quant_cli-0.1.26}/hamuna_quant_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|