hamuna-quant-cli 0.1.0.dev93__py3-none-any.whl

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.
@@ -0,0 +1,121 @@
1
+ """hamuna_quant_cli.live.loader — strategy.py 加载 + 校验.
2
+
3
+ 可独立跑 (`python -m hamuna_quant_cli.live.loader --help` 不强依赖 akquant).
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import importlib.util
8
+ import inspect
9
+ import sys
10
+ import types
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+
16
+ # 策略可识别的函数式回调名 (functional mode) — 顺序无意义, 仅做存在性检查
17
+ FUNCTIONAL_HOOKS = ("initialize", "on_bar", "on_order", "on_trade", "on_timer", "on_broker_connected")
18
+
19
+
20
+ class StrategyLoadError(Exception):
21
+ """strategy.py 加载 / 校验失败."""
22
+
23
+
24
+ @dataclass
25
+ class StrategySpec:
26
+ mode: Literal["class", "functional"]
27
+ # class mode
28
+ strategy_cls: type | None = None
29
+ # functional mode
30
+ callbacks: dict[str, Any] | None = None
31
+ module: types.ModuleType | None = None # 留着, 后续 run_live 之外的扩展用
32
+
33
+
34
+ def _import_strategy_module(path: Path) -> types.ModuleType:
35
+ """用 importlib 加载 strategy.py (不污染 sys.modules)."""
36
+ if not path.is_file():
37
+ raise StrategyLoadError(f"策略文件不存在: {path}")
38
+ spec = importlib.util.spec_from_file_location(f"_hamuna_strategy_user_{path.stem}", path)
39
+ if spec is None or spec.loader is None:
40
+ raise StrategyLoadError(f"无法构造 import spec: {path}")
41
+ module = importlib.util.module_from_spec(spec)
42
+ # 用户顶层副作用 (网络请求/重 I/O) 应当避免 — 不在 loader 责任范围, 透传即可
43
+ try:
44
+ spec.loader.exec_module(module)
45
+ except Exception as e:
46
+ raise StrategyLoadError(f"strategy.py 顶层执行失败: {type(e).__name__}: {e}") from e
47
+ return module
48
+
49
+
50
+ def _is_akquant_strategy_subclass(obj: Any) -> bool:
51
+ """宽松判定: 类是否继承自 akquant.Strategy.
52
+
53
+ 用 duck typing 而不是直接 import akquant.Strategy 比较 — loader 必须能在
54
+ akquant 未装的环境跑通 (CI / stub 测试). 走 duck check: 类必须有 on_bar /
55
+ buy / sell 等 akquant 协议方法, 且其 MRO 中有 'Strategy' 这个名字.
56
+ """
57
+ if not inspect.isclass(obj):
58
+ return False
59
+ # MRO 中存在名为 'Strategy' 的基类 — 适配 `from akquant import Strategy` 与
60
+ # `import akquant; class X(akquant.Strategy)` 两种写法
61
+ has_strategy_base = any(base.__name__ == "Strategy" for base in obj.__mro__)
62
+ if not has_strategy_base:
63
+ return False
64
+ # duck check: 必须有协议方法 on_bar
65
+ return hasattr(obj, "on_bar") and callable(getattr(obj, "on_bar"))
66
+
67
+
68
+ def load_strategy(path: Path, class_name: str = "Strategy") -> StrategySpec:
69
+ """加载 strategy.py → StrategySpec.
70
+
71
+ class_name: class mode 模式下优先要找的策略类名 (默认 `Strategy`).
72
+ 优先 class mode; 若找不到目标类, fallback 到 functional mode (顶层函数 hook).
73
+ """
74
+ module = _import_strategy_module(path)
75
+
76
+ # —— class mode ——
77
+ # 找【模块内定义】的 akquant.Strategy 子类。老逻辑 getattr(module, 'Strategy')
78
+ # 会命中 `from akquant import Strategy` 挂到模块命名空间的【基类本身】
79
+ # (所有模板都这么写) → 用户类从来没被加载过, on_bar 全走基类空实现
80
+ # (2026-08-19 实测: live run 0 trades + compute_factors 检测恒 False)。
81
+ # 判定: __module__ == 用户模块名 (排除 akquant / 其它 import 的类)。
82
+ cls = None
83
+ if class_name:
84
+ cand = getattr(module, class_name, None)
85
+ if (cand is not None and _is_akquant_strategy_subclass(cand)
86
+ and getattr(cand, "__module__", None) == module.__name__):
87
+ cls = cand
88
+ if cls is None:
89
+ for name, obj in vars(module).items():
90
+ if (not name.startswith("_")
91
+ and _is_akquant_strategy_subclass(obj)
92
+ and getattr(obj, "__module__", None) == module.__name__):
93
+ cls = obj
94
+ break
95
+ if cls is not None:
96
+ return StrategySpec(mode="class", strategy_cls=cls, module=module)
97
+
98
+ # —— functional mode ——
99
+ callbacks: dict[str, Any] = {}
100
+ for hook in FUNCTIONAL_HOOKS:
101
+ fn = getattr(module, hook, None)
102
+ if fn is not None and callable(fn):
103
+ callbacks[hook] = fn
104
+
105
+ if not callbacks:
106
+ # 既找不到 class 也没找到函数式 hook — 列文件里有什么帮用户排错
107
+ declared = [n for n, o in vars(module).items() if not n.startswith("_") and (inspect.isclass(o) or callable(o))]
108
+ hint = f"文件里看到: {', '.join(declared[:8])}" if declared else "文件为空或全以下划线开头"
109
+ raise StrategyLoadError(
110
+ f"{path}: 未找到合法 strategy.\n"
111
+ f" · class mode: 类 `{class_name}(akquant.Strategy)` 不存在或不是 akquant.Strategy 子类\n"
112
+ f" · functional mode: 顶层函数 {', '.join(FUNCTIONAL_HOOKS)} 一个都没找到\n"
113
+ f" · {hint}"
114
+ )
115
+
116
+ if "on_bar" not in callbacks:
117
+ raise StrategyLoadError(
118
+ f"{path}: functional mode 缺少 `on_bar(ctx, bar)` — akquant.run_live 必须有它"
119
+ )
120
+
121
+ return StrategySpec(mode="functional", callbacks=callbacks, module=module)