Skip to content

Commit 41287aa

Browse files
committed
feat: TradingKernel — 系统控制平面
唯一主循环 + 单一事实来源 + 失败自治. 核心: - TradingKernel.cycle(): 一次完整运行周期 - TradingKernel.run_forever(): 永不结束的主循环 - SystemState: 单一事实来源 (资金/持仓/状态/模式) - SystemClock: 时间一致性 (market/system/broker time) - Mode transitions: normal → safe → halt → recovery - 组件注册: register(name, component) 统一接入 替换 daemon/runner.py 成为新的系统入口. 从零件集合变成 autonomous trading system.
1 parent 4be8ef9 commit 41287aa

2 files changed

Lines changed: 296 additions & 0 deletions

File tree

kernel/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""TradingKernel — alpha-v1.0 系统控制平面.
2+
3+
把零件装成机器。从 "collection of components" 变成 "autonomous trading organism"。
4+
"""
5+
6+
from .kernel import TradingKernel
7+
8+
__all__ = ["TradingKernel"]

kernel/kernel.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
"""TradingKernel — alpha-v1.0 系统控制平面.
2+
3+
唯一主循环. 单一事实来源. 失败自治.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import logging
9+
import time
10+
import traceback
11+
from dataclasses import dataclass, field
12+
from datetime import datetime, timedelta
13+
from enum import Enum
14+
from typing import Any, Callable
15+
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"
34+
35+
36+
@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
47+
equity: float = 10_000_000
48+
equity_peak: float = 10_000_000
49+
daily_pnl: float = 0.0
50+
51+
# 持仓
52+
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+
73+
@property
74+
def drawdown(self) -> float:
75+
if self.equity_peak <= 0:
76+
return 0.0
77+
return (self.equity_peak - self.equity) / self.equity_peak
78+
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+
114+
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)
131+
132+
# ── 主循环 ──
133+
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):
203+
"""永不结束的主循环."""
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:
211+
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()
221+
222+
def stop(self):
223+
"""停止系统."""
224+
self._running = False
225+
self.state.status = SystemStatus.STOPPED
226+
logger.info("Kernel stopped. Cycles: %d, Errors: %d",
227+
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+
# ── 报告 ──
270+
271+
def report(self) -> str:
272+
dd = self.state.drawdown
273+
return (
274+
f"\n{'=' * 60}\n"
275+
f" TradingKernel — alpha-v1.0\n"
276+
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"
287+
f"{'=' * 60}"
288+
)

0 commit comments

Comments
 (0)