|
1 | 1 | """TradingKernel — alpha-v1.0 系统控制平面. |
2 | 2 |
|
3 | | -唯一主循环. 单一事实来源. 失败自治. |
| 3 | +Single control plane. 唯一主循环. 失败自治. |
4 | 4 | """ |
5 | 5 |
|
6 | 6 | from __future__ import annotations |
|
9 | 9 | import time |
10 | 10 | import traceback |
11 | 11 | from dataclasses import dataclass, field |
12 | | -from datetime import datetime, timedelta |
13 | | -from enum import Enum |
14 | | -from typing import Any, Callable |
| 12 | +from datetime import datetime |
| 13 | +from typing import Any |
15 | 14 |
|
16 | | -import numpy as np |
17 | | -import pandas as pd |
18 | | - |
19 | | -logger = logging.getLogger(__name__) |
20 | | - |
21 | | -# ── 系统状态 ── |
22 | | - |
23 | | -class SystemMode(Enum): |
24 | | - NORMAL = "normal" |
25 | | - SAFE = "safe" # 降级运行 (只监控, 不交易) |
26 | | - HALT = "halt" # 停止交易, 等待恢复 |
27 | | - RECOVERY = "recovery" # 正在恢复 |
28 | | - |
29 | | -class SystemStatus(Enum): |
30 | | - INIT = "init" |
31 | | - RUNNING = "running" |
32 | | - ERROR = "error" |
33 | | - STOPPED = "stopped" |
| 15 | +logger = logging.getLogger("kernel") |
34 | 16 |
|
35 | 17 |
|
36 | 18 | @dataclass |
37 | | -class SystemState: |
38 | | - """系统单一事实来源.""" |
39 | | - |
40 | | - # 时间 |
41 | | - market_time: datetime | None = None |
42 | | - system_time: datetime | None = None |
43 | | - broker_time: datetime | None = None |
44 | | - |
45 | | - # 资金 |
46 | | - cash: float = 10_000_000 |
| 19 | +class KernelState: |
| 20 | + """系统运行时状态.""" |
| 21 | + running: bool = True |
| 22 | + mode: str = "LIVE" # LIVE / SAFE / HALT |
| 23 | + last_tick: float = 0.0 |
| 24 | + cycle_count: int = 0 |
| 25 | + errors: list[str] = field(default_factory=list) |
| 26 | + alerts: list[str] = field(default_factory=list) |
47 | 27 | equity: float = 10_000_000 |
48 | 28 | equity_peak: float = 10_000_000 |
49 | | - daily_pnl: float = 0.0 |
50 | | - |
51 | | - # 持仓 |
| 29 | + cash: float = 10_000_000 |
52 | 30 | n_positions: int = 0 |
53 | | - gross_exposure: float = 0.0 |
54 | | - |
55 | | - # 状态 |
56 | | - mode: SystemMode = SystemMode.NORMAL |
57 | | - status: SystemStatus = SystemStatus.INIT |
58 | | - last_cycle: str = "" |
59 | | - cycle_count: int = 0 |
60 | | - errors: list[dict] = field(default_factory=list) |
61 | | - alerts: list[dict] = field(default_factory=list) |
62 | | - |
63 | | - # 调仓 |
64 | | - last_rebalance: str = "" |
65 | | - next_rebalance: str = "" |
66 | | - days_to_rebalance: int = 0 |
67 | | - |
68 | | - def update_equity(self, total: float): |
69 | | - self.equity = total |
70 | | - if total > self.equity_peak: |
71 | | - self.equity_peak = total |
72 | 31 |
|
73 | 32 | @property |
74 | 33 | def drawdown(self) -> float: |
75 | 34 | if self.equity_peak <= 0: |
76 | 35 | return 0.0 |
77 | 36 | return (self.equity_peak - self.equity) / self.equity_peak |
78 | 37 |
|
79 | | - def summary(self) -> str: |
80 | | - return ( |
81 | | - f"State | mode={self.mode.value} status={self.status.value} " |
82 | | - f"equity={self.equity:>.0f} dd={self.drawdown:.2%} " |
83 | | - f"cycle={self.cycle_count} errors={len(self.errors)}" |
84 | | - ) |
85 | | - |
86 | | - |
87 | | -# ── 时钟 ── |
88 | | - |
89 | | -@dataclass |
90 | | -class SystemClock: |
91 | | - """系统时间一致性管理器.""" |
92 | | - |
93 | | - market_offset: float = 0.0 # market_time - system_time |
94 | | - broker_offset: float = 0.0 # broker_time - system_time |
95 | | - |
96 | | - def sync(self, market_time: datetime | None = None, broker_time: datetime | None = None): |
97 | | - now = datetime.now() |
98 | | - if market_time: |
99 | | - self.market_offset = (market_time - now).total_seconds() |
100 | | - if broker_time: |
101 | | - self.broker_offset = (broker_time - now).total_seconds() |
102 | | - |
103 | | - @property |
104 | | - def market_now(self) -> datetime: |
105 | | - return datetime.now() + timedelta(seconds=self.market_offset) |
106 | | - |
107 | | - @property |
108 | | - def broker_now(self) -> datetime: |
109 | | - return datetime.now() + timedelta(seconds=self.broker_offset) |
110 | | - |
111 | | - |
112 | | -# ── 内核 ── |
113 | 38 |
|
114 | 39 | class TradingKernel: |
115 | | - """系统控制平面. 唯一主循环. 决策中枢.""" |
116 | | - |
117 | | - def __init__(self, initial_capital: float = 10_000_000): |
118 | | - self.state = SystemState(cash=initial_capital, equity=initial_capital) |
119 | | - self.clock = SystemClock() |
120 | | - self.components: dict[str, Any] = {} |
121 | | - self._running = False |
122 | | - self._cycle_interval = 60 # seconds |
123 | | - self._last_cycle_time: float = 0.0 |
124 | | - |
125 | | - # ── 组件注册 ── |
126 | | - |
127 | | - def register(self, name: str, component: Any): |
128 | | - """注册系统组件.""" |
129 | | - self.components[name] = component |
130 | | - logger.info("Component registered: %s", name) |
| 40 | + """系统控制平面. 唯一主循环. 全系统串联.""" |
| 41 | + |
| 42 | + def __init__(self, data_engine=None, signal_engine=None, risk_engine=None, |
| 43 | + execution_engine=None, monitor=None, reconciliation=None): |
| 44 | + self.data = data_engine |
| 45 | + self.signal = signal_engine |
| 46 | + self.risk = risk_engine |
| 47 | + self.exec = execution_engine |
| 48 | + self.monitor = monitor |
| 49 | + self.recon = reconciliation |
| 50 | + self.state = KernelState() |
131 | 51 |
|
132 | 52 | # ── 主循环 ── |
133 | 53 |
|
134 | | - def cycle(self) -> dict[str, Any]: |
135 | | - """一次完整运行周期.""" |
136 | | - result: dict[str, Any] = {"status": "ok", "alerts": []} |
137 | | - |
138 | | - try: |
139 | | - self.state.system_time = datetime.now() |
140 | | - self.state.cycle_count += 1 |
141 | | - self.state.last_cycle = self.state.system_time.strftime("%Y-%m-%d %H:%M:%S") |
142 | | - |
143 | | - # 1. 数据 |
144 | | - data = self._get_data() |
145 | | - if data is None: |
146 | | - self._transition(SystemMode.SAFE, "data_unavailable") |
147 | | - result["alerts"].append("data_unavailable") |
148 | | - return result |
149 | | - |
150 | | - # 2. 策略 |
151 | | - engine = self.components.get("engine") |
152 | | - if engine: |
153 | | - engine.run_once(data.get("last_date")) |
154 | | - self.state.n_positions = len(engine.state.positions) |
155 | | - self.state.cash = engine.state.cash |
156 | | - self.state.update_equity(engine.state.current_equity) |
157 | | - |
158 | | - # 3. 风控 |
159 | | - safety = self.components.get("safety") |
160 | | - if safety: |
161 | | - checks = safety.check_all( |
162 | | - equity=self.state.equity, |
163 | | - peak_equity=self.state.equity_peak, |
164 | | - cash=self.state.cash, |
165 | | - daily_pnl=self.state.daily_pnl, |
166 | | - positions={}, |
167 | | - ) |
168 | | - failed = [c for c in checks if not c.passed] |
169 | | - if failed: |
170 | | - result["alerts"].extend(f"{c.name}: {c.message}" for c in failed) |
171 | | - if safety.kill_switch_triggered: |
172 | | - self._transition(SystemMode.HALT, "kill_switch") |
173 | | - result["status"] = "killed" |
174 | | - return result |
175 | | - |
176 | | - # 4. 对账 |
177 | | - recon = self.components.get("reconciliation") |
178 | | - if recon: |
179 | | - pass # 需要 broker 接入后启用 |
180 | | - |
181 | | - # 5. 状态检查 |
182 | | - dd = self.state.drawdown |
183 | | - if dd > 0.25: |
184 | | - self._transition(SystemMode.SAFE, f"drawdown_warning:{dd:.2%}") |
185 | | - result["alerts"].append(f"drawdown={dd:.2%}") |
186 | | - |
187 | | - self.state.status = SystemStatus.RUNNING |
188 | | - self.state.mode = SystemMode.NORMAL |
189 | | - |
190 | | - except Exception as e: |
191 | | - logger.error("Cycle failed: %s\n%s", e, traceback.format_exc()) |
192 | | - self.state.errors.append({ |
193 | | - "time": str(datetime.now()), |
194 | | - "error": str(e), |
195 | | - "trace": traceback.format_exc(), |
196 | | - }) |
197 | | - self._transition(SystemMode.RECOVERY, str(e)[:100]) |
198 | | - result["status"] = "error" |
199 | | - |
200 | | - return result |
201 | | - |
202 | | - def run_forever(self, interval: int = 3600): |
| 54 | + def run(self): |
203 | 55 | """永不结束的主循环.""" |
204 | | - self._running = True |
205 | | - self._cycle_interval = interval |
206 | | - self.state.status = SystemStatus.RUNNING |
207 | | - |
208 | | - logger.info("Kernel started. Interval: %ds", interval) |
209 | | - |
210 | | - while self._running: |
| 56 | + logger.info("TradingKernel started") |
| 57 | + while self.state.running: |
211 | 58 | try: |
212 | | - self.cycle() |
213 | | - time.sleep(self._cycle_interval) |
214 | | - except KeyboardInterrupt: |
215 | | - logger.info("Kernel stopped by user") |
216 | | - self.stop() |
217 | | - break |
218 | | - except Exception: |
219 | | - logger.error("Kernel crashed: %s", traceback.format_exc()) |
220 | | - self._recover() |
| 59 | + self._tick() |
| 60 | + except Exception as e: |
| 61 | + logger.exception("Kernel crash: %s", e) |
| 62 | + self.state.errors.append(str(e)) |
| 63 | + self._enter_safe_mode() |
| 64 | + |
| 65 | + def _tick(self): |
| 66 | + """单次运行周期.""" |
| 67 | + now = time.time() |
| 68 | + self.state.last_tick = now |
| 69 | + self.state.cycle_count += 1 |
| 70 | + |
| 71 | + # 1. Data |
| 72 | + market_data = self.data.get_latest() if self.data else None |
| 73 | + |
| 74 | + # 2. Signal |
| 75 | + signal = None |
| 76 | + if self.signal and market_data is not None: |
| 77 | + signal = self.signal.generate(market_data) |
| 78 | + |
| 79 | + # 3. Risk |
| 80 | + risk_ok = True |
| 81 | + if self.risk and signal is not None: |
| 82 | + risk_ok = self.risk.check(signal, market_data) |
| 83 | + if not risk_ok: |
| 84 | + logger.warning("Risk block — skipping execution") |
| 85 | + if self.monitor: |
| 86 | + self.monitor.log_event("RISK_BLOCK") |
| 87 | + return |
| 88 | + |
| 89 | + # 4. Execution |
| 90 | + fills = None |
| 91 | + if self.exec and signal is not None: |
| 92 | + orders = self.exec.generate_orders(signal) |
| 93 | + fills = self.exec.execute(orders) |
| 94 | + |
| 95 | + # 5. Reconciliation |
| 96 | + if self.recon and fills: |
| 97 | + self.recon.update(fills) |
| 98 | + |
| 99 | + # 6. Monitoring |
| 100 | + if self.monitor: |
| 101 | + self.monitor.update(market_data=market_data, signal=signal, |
| 102 | + fills=fills, state=self.state) |
| 103 | + |
| 104 | + # 7. Safety |
| 105 | + self._post_tick_safety() |
| 106 | + |
| 107 | + # ── 安全 ── |
| 108 | + |
| 109 | + def _post_tick_safety(self): |
| 110 | + if self.risk and self.risk.drawdown_exceeded(): |
| 111 | + self._enter_safe_mode() |
| 112 | + if self.risk and self.risk.daily_loss_limit_hit(): |
| 113 | + self._enter_halt() |
| 114 | + |
| 115 | + def _enter_safe_mode(self): |
| 116 | + logger.critical("SAFE MODE") |
| 117 | + self.state.mode = "SAFE" |
| 118 | + self.state.alerts.append("safe_mode") |
| 119 | + if self.exec: |
| 120 | + self.exec.cancel_all() |
| 121 | + |
| 122 | + def _enter_halt(self): |
| 123 | + logger.critical("HALT MODE") |
| 124 | + self.state.mode = "HALT" |
| 125 | + self.state.running = False |
| 126 | + self.state.alerts.append("halt") |
| 127 | + if self.exec: |
| 128 | + self.exec.cancel_all() |
221 | 129 |
|
222 | 130 | def stop(self): |
223 | | - """停止系统.""" |
224 | | - self._running = False |
225 | | - self.state.status = SystemStatus.STOPPED |
226 | | - logger.info("Kernel stopped. Cycles: %d, Errors: %d", |
| 131 | + """手动停止.""" |
| 132 | + logger.info("Kernel stopping. Cycles: %d, Errors: %d", |
227 | 133 | self.state.cycle_count, len(self.state.errors)) |
228 | | - |
229 | | - # ── 内部 ── |
230 | | - |
231 | | - def _get_data(self) -> dict | None: |
232 | | - """获取最新数据.""" |
233 | | - data_rel = self.components.get("data") |
234 | | - if data_rel: |
235 | | - try: |
236 | | - result = data_rel.connect() |
237 | | - if result is not None: |
238 | | - return {"last_date": datetime.now()} |
239 | | - except Exception as e: |
240 | | - logger.warning("Data unavailable: %s", e) |
241 | | - return None |
242 | | - |
243 | | - # 从 engine 获取数据 |
244 | | - engine = self.components.get("engine") |
245 | | - if engine and engine._returns is not None: |
246 | | - return {"last_date": engine._returns.index[-1]} |
247 | | - return None |
248 | | - |
249 | | - def _transition(self, mode: SystemMode, reason: str): |
250 | | - """模式切换.""" |
251 | | - old = self.state.mode |
252 | | - self.state.mode = mode |
253 | | - logger.info("Mode transition: %s -> %s (%s)", old.value, mode.value, reason) |
254 | | - self.state.alerts.append({ |
255 | | - "time": str(datetime.now()), |
256 | | - "from": old.value, |
257 | | - "to": mode.value, |
258 | | - "reason": reason, |
259 | | - }) |
260 | | - |
261 | | - def _recover(self): |
262 | | - """崩溃恢复.""" |
263 | | - self._transition(SystemMode.RECOVERY, "auto_recovery") |
264 | | - logger.info("Recovery attempt...") |
265 | | - time.sleep(5) |
266 | | - self.state.errors = [] |
267 | | - self._transition(SystemMode.NORMAL, "recovered") |
268 | | - |
269 | | - # ── 报告 ── |
| 134 | + self.state.running = False |
270 | 135 |
|
271 | 136 | def report(self) -> str: |
272 | 137 | dd = self.state.drawdown |
273 | 138 | return ( |
274 | 139 | f"\n{'=' * 60}\n" |
275 | 140 | f" TradingKernel — alpha-v1.0\n" |
276 | 141 | f"{'=' * 60}\n" |
277 | | - f" Status: {self.state.status.value}\n" |
278 | | - f" Mode: {self.state.mode.value}\n" |
279 | | - f" Equity: {self.state.equity:>12,.2f}\n" |
280 | | - f" Drawdown: {dd*100:>10.2f}%\n" |
281 | | - f" Positions: {self.state.n_positions:>6d}\n" |
282 | | - f" Cycles: {self.state.cycle_count:>6d}\n" |
283 | | - f" Alerts: {len(self.state.alerts):>6d}\n" |
284 | | - f" Errors: {len(self.state.errors):>6d}\n" |
285 | | - f" Next rebal: {self.state.next_rebalance}\n" |
286 | | - f" Mode hist: {','.join(a['to'] for a in self.state.alerts[-5:])}\n" |
| 142 | + f" Mode: {self.state.mode}\n" |
| 143 | + f" Cycles: {self.state.cycle_count}\n" |
| 144 | + f" Equity: {self.state.equity:>12,.2f}\n" |
| 145 | + f" Drawdown: {dd*100:>10.2f}%\n" |
| 146 | + f" Positions: {self.state.n_positions:>6d}\n" |
| 147 | + f" Alerts: {len(self.state.alerts)}\n" |
| 148 | + f" Errors: {len(self.state.errors)}\n" |
287 | 149 | f"{'=' * 60}" |
288 | 150 | ) |
0 commit comments