KamijouTouma 是一个面向 A 股中低频策略的量化交易系统。名字听起来像某位“幻想杀手”,但项目本身一点也不魔法:行情要先同步,数据要能对得上,策略要能回测,订单要有状态,风控要在下单前真的拦得住。少任何一环,屏幕上那条漂亮的收益曲线都可能只是幻觉。(:з」∠)
本文按仓库最近一次提交的实现快照来记录,重点是当前代码已经搭出的边界,不把仍在演进中的功能包装成生产承诺。量化系统会随着数据源、QMT 后端和交易接口调整而变化,真正运行前仍应重新检查配置、数据覆盖和执行状态。
我做这个项目时最在意的事情其实就一件:不要只写一个能产生信号的策略 Demo,而是把“数据从哪里来、经过什么处理、为什么生成这个信号、订单有没有真的执行”全部放进同一条可以追踪的链路里。
先看全貌:一条信号要经过多少站
项目的主干是 Rust workspace,前端看板用 React + Vite,QMT 数据源通过 Python gRPC 桥接层接入。画成一张不太浪漫、但很诚实的地图,大概是这样:
1 | QMT / 外部行情 |
这里每个箭头都不是装饰。QMT 适配层解决“外部数据长什么样”,数据管道解决“系统内部愿意相信什么样的数据”,策略引擎解决“在这些数据上是否产生信号”,交易引擎解决“信号如何变成订单”,风控管理解决“这个订单是否允许出去”。要是把它们全塞进一个 main.rs 或一个脚本里,第一天会很爽,第三个月就会开始怀疑人生。
Workspace 的边界:按交易问题拆,而不是按文件类型拆
Rust workspace 里有一组按领域划分的 crate。这种拆法看上去比“utils、services、controllers”啰嗦,但它逼着我把问题说清楚:
common放共享配置、错误类型、交易日历,以及跨模块使用的行情、订单和组合数据结构。indicators负责 SMA、EMA、MACD、RSI、BOLL,也放收益、回撤等绩效计算,省得每个策略各写一份略有差异的指标公式。universe负责多因子选股和股票池生成。策略从候选标的开始,而不是每次都自己遍历全市场。strategy-engine定义策略 trait、注册中心和 DSL 宏,让策略只关注信号规则,不必知道数据库连接或 HTTP 路由。backtest-engine提供回测主循环、模拟券商、资金和持仓更新,以及结果报告。trading-engine负责模拟与实盘交易、订单状态机和组合变化,risk-manager在订单进入执行链路前做限额检查。data-pipeline处理行情获取、清洗、时序存储、缓存和事件总线;news-analyzer则把新闻解析和情绪分析放在独立边界中。
这样一来,“选什么股票”“什么时候产生信号”“这个数量能不能下”“成交后组合变成什么样”就不再是同一个函数里的四个 if。对研究代码来说,这种边界可能显得有点重;对要长期运行的系统来说,它反而是给未来的自己留一条逃生通道。
一条行情是怎样变成策略输入的
QMT 数据不会直接进策略。packages/py-bridge 里的 Python 服务通过 gRPC 暴露统一接口,Rust 侧把外部字段转换成内部行情结构,再由数据同步服务写入 TimescaleDB,实时消费需要的状态放到 Redis。
TimescaleDB 适合存按时间排列的 K 线、指标输入这些时序数据;Redis 更像快速通道,放缓存、事件或短生命周期的运行状态。数据库和缓存分工之后,策略回看历史数据不会和实时状态读取混成一团,出了问题也好定位。
这条链路里还有一个特别容易被忽略的判断:数据“有几行”不等于数据“已经准备好”。目标交易日的日线或 5 分钟数据没覆盖完整,策略没信号可能只是因为输入缺失;某个服务返回了 HTTP 200,也不等于下游拿到了完整数据。所以项目在数据同步、数据库覆盖范围、策略信号和交易结果之间保留了可以分别验证的状态。
我很喜欢这个约束,因为它会阻止人把“没有发生任何事”直接解释成“策略今天判断为空”。量化系统最可怕的 bug 往往不是崩溃,而是安静地给出一个看起来很合理的错误结论。
指标、股票池和策略:把研究逻辑放在能复用的位置
指标模块目前包含常用趋势和摆动指标。它们不是为了让界面上多几条线,而是为了让回测、策略运行和报告计算用同一套输入约定。股票池模块再在此之上处理多因子筛选,把“市场范围”和“信号条件”分开。
策略引擎通过统一接口和注册中心承载策略,项目里有动量轮动、MACD 趋势择时这些示例。策略只需要回答一个问题:在给定的行情、指标和组合状态下,当前是买入、卖出、持有,还是暂时什么都不做。它不应该自己打开 Redis,也不应该知道前端页面跑在哪个端口。
这种分离还有个实际好处:同一策略可以被回测引擎调用,也可以被模拟交易调用。只要输入数据模型和订单意图保持一致,研究阶段和运行阶段就不会因为复制粘贴而慢慢分叉。
回测不是收益曲线生成器
回测最容易写成一个循环:读一根 K 线,算一下指标,条件满足就把收益加上去。但真实交易里还有订单未成交、资金冻结、持仓变化、撤单、滑点和手续费。项目用模拟券商与订单状态机把这些概念显式化,让策略生成的是“交易意图”,而不是直接修改账户余额。
回测引擎负责推进时间、调用策略、模拟订单和更新组合;交易引擎保留相近的订单和组合语义,接入模拟或实盘执行通道。风控管理站在订单真正发出之前,检查数量、金额、持仓、限额和其他策略外约束。换句话说,策略可以提出一个大胆的想法,但最后下不下单,不是策略一句话说了算的。
这套设计也让问题好复盘:
- 数据层能回答“当时到底有没有这根 K 线”。
- 策略层能回答“基于这些输入为什么产生了信号”。
- 订单层能回答“信号是否创建成订单、订单经历了什么状态”。
- 风控层能回答“如果被拒绝,是哪条规则拒绝的”。
这四个答案比一张最终收益图值钱。收益图只告诉你结果,状态链路才告诉你下一次该改哪里。
Axum 与 Dashboard:把“系统正在做什么”显示出来
后端用 Axum 提供 REST 和 WebSocket。REST 适合策略、组合、回测和管理操作;WebSocket 推送行情、策略状态和交易事件。前端用 React 19、Vite 6、Tailwind CSS 4、Zustand,加上轻量级 K 线图表组件,把历史数据、实时状态和管理入口放进同一个看板。
看板的意义不只是好看。开发时我需要知道数据同步有没有在跑、当前策略是否运行、订单是不是卡在某个中间状态、组合的现金和持仓有没有跟着成交变化。一个页面把这些信息放在一起,排查问题时就不用在终端、数据库和浏览器之间来回切换,少一点“到底是哪一层没动”的猜谜游戏。
项目还保留了分别启动 Py-Bridge、数据同步服务、API Server 和 Dashboard 的脚本,比如 scripts/start_web.ps1,README 里也写了 Rust、Python、前端各自的测试和 lint 入口。多语言项目最容易变成“每一层都能单独跑,但合在一起谁也不知道怎么跑”,启动脚本和验证命令就是用来填这条鸿沟的。
配置和启动:真正的系统不会只靠一个按钮
开发时通常要面对四类进程:Python 的 QMT / gRPC 桥、Rust 数据同步服务、Axum API,还有 Vite Dashboard。它们之间有先后关系:桥接层要先连上数据源,数据服务要能写库,API 才能读到一致的状态,前端最后才有东西可展示。
这也是为什么启动脚本要单独维护,而不是只在 README 里写四段命令。脚本要处理工作目录、环境变量、后台进程和停止动作;启动顺序错了,前端可能正常打开,却只看到空列表。页面“能访问”和系统“已经准备好”是两回事,调试时必须分开。
数据库也不是一块没有边界的黑板。开发数据、参考数据和交易相关数据按逻辑用途隔离,测试或回填时不应该随手把实验结果写进运行数据。Redis 里的缓存和事件状态同样要知道生命周期:哪些数据可以过期,哪些状态必须落库,哪些消息丢了以后需要重新同步。这个问题没有哪个 Rust trait 能一键解决,只能靠明确的配置和验证入口。
一次“没有信号”的排查顺序
我现在更愿意把策略没产生信号当成一个需要拆解的事件,而不是直接写成“今天不满足条件”。排查顺序大致是:
- 先确认目标交易日有没有完整的日线或 5 分钟覆盖,最后一根数据的时间是否合理。
- 再确认数据同步任务是否真正写完,有没有失败批次、空结果或被截断的标的。
- 然后检查指标输入有没有足够的历史窗口,股票池是不是空的,策略状态有没有启动。
- 最后才轮到信号条件本身,以及信号是不是被风控或订单状态机拦下了。
这条顺序看着保守,却能避免把数据缺口误判成策略结论。一次接口返回成功、一个服务进程还活着,甚至数据库里躺着几千行数据,都不能独立证明目标交易日已经准备完毕。量化系统最需要的不是“看起来健康”,而是能把完成条件写成一组可重复的检查。
回测、模拟和实盘之间,哪些东西应该一样
三种运行方式不可能完全相同:回测没有真实交易柜台,模拟盘没有真实网络延迟,实盘还会面对拒单、部分成交和交易时段限制。但订单意图、订单状态、持仓变更和资金变化,最好用相近的领域模型。
策略生成一个买入意图之后,回测交给模拟券商,模拟交易交给模拟执行器,真实环境再交给 QMT 交易适配器;差异集中在执行层,而不是让每个策略各写一份“如果现在是实盘”的分支。这样回测报告和运行日志才有机会用相同的字段互相对照。
当然,共用概念不代表回测就等于实盘。滑点、成交延迟、涨跌停、停牌、手续费和可买数量仍然要显式建模。我更愿意先把这些差异放在看得见的位置,而不是用一条漂亮曲线把它们藏起来。
做完以后,最重要的收获不是某个指标
KamijouTouma 目前更像一座持续施工的实验场,而不是一个可以闭眼交给它管钱的黑盒。它让我越来越确定:量化系统的难点很少只在 MACD、动量或某个新奇因子上,真正麻烦的是数据缺口、时间边界、订单状态、异常恢复,还有那些“看起来成功但其实没完成”的中间状态。
所以这个项目给我的最大提醒是——策略只是链路中的一站。把每一站的输入、输出、失败方式和验证方式写清楚,系统才开始有一点工程的样子。至于收益嘛,先让数据每天准时到站再说。(:з」∠)