简介:面向程序员的开源量化交易平台,使用Java和人工智能技术构建,覆盖期货、股票、外汇、数字货币等多类市场,支持历史回放、策略研发、模拟交易与实盘交易,兼顾全自动和半自动模式,可替代文华财经、MC、金字塔等商业软件,适合有一定代码基础的开发者直接使用。压缩包共579个文件,核心为408个Java源码文件,前端以Vue和JavaScript为主,工程内还包含多种配置格式、容器化部署文件与自动运维脚本,便于快速导入开发环境运行调试,整包体积仅1.83MB,结构紧凑清晰。资源已有380人学习下载,对想搭建个人量化系统或研究策略编写的程序员来说,可参考价值较高。通过源码可以梳理行情接入、策略回测、订单管理及前端展示的实现脉络,也能基于现有模块进行二次开发,改造出适合自己的自动或半自动交易工具。
1. 用 Java 做量化自动交易:为什么说它能顶替文华、MC 和金字塔
一个做期货的 Java 开发问我:Python 回测了半年的策略,怎么落到实盘?文华写模型可以,但公式语言一碰复杂逻辑就难受;MultiCharts 功能强,代码和数据却都是黑匣子。这套基于 Java 的开源量化交易平台,解决的就是从策略研发到自动交易的全链路问题。历史回放、策略编写、模拟盘验证、实盘执行全部在一个系统里完成,行情端覆盖期货 CTP、股票、外汇和数字资产,既能全自动跑策略,也能半自动等人工确认后下单。它的直接对标就是文华、MC、金字塔这类商业软件,但策略代码跑在 JVM 生态里,你手里那些并发、Maven、Spring 的积累全都用得上。适合两类人:一是有 Java 底子、想自己掌控策略逻辑的程序员;二是受够了闭源平台函数限制、想要策略代码彻底可控的量化从业者。
2. 平台架构与核心模块:从行情接入到策略落地的完整链路
2.1 五个核心模块拆解:行情、引擎、风控、订单、账户
整个系统不是一个大而全的单体,而是按交易链路拆成五个核心模块:行情接入、策略引擎、风控、订单路由、账户与持仓。这个拆分方式是典型的"回放和实盘复用同一套内核"的设计,也是它敢说顶替商业软件的基础。
行情接入模块负责把不同市场的行情源统一成两个对象:Tick 和 Bar。CTP 期货柜台是主动查询加回报推送,行情密度高,成交回报走单独回调;证券行情有 Level-1 和 Level-2 的差异,前者三秒一个快照,后者有逐笔委托;外汇和数字资产基本都是 WebSocket 增量推送,字段命名方式和交易时段又不一样。如果让策略层分别处理这些差异,策略代码就废了。平台的处理方式是在中间加一层归一化:不管底层是什么协议,交到策略手里的只有"最新一笔变化"和"合成好的K线"两个对象。这样一套策略,改改参数就能在不同市场间复用,这是架构层面最值得借鉴的一点。
策略引擎是核心模块,但它做的事情很少:订阅行情、维护K线序列、调用策略回调、把策略产生的信号交给下一步。关键设计在于,回放模式和实盘模式跑的是同一个引擎,只是数据源不同。回放模式的数据来自历史行情文件,实盘模式的数据来自实时行情,引擎通过一个数据源接口隔离两种来源。策略里的 onTick、onBar 回调完全感知不到数据是从文件里读的还是从交易所实时推来的。这个约束保证了回测行为和实盘行为的高度一致,从根源上减少"回测一套、实盘另一套"的落差。
风控模块必须排在订单路由之前。内置的风控项通常包括:单合约最大持仓限制、单日最大亏损额、下单频率限制(比如每秒最多一单)、撤单次数限制。这些风控在回放模式里默认关闭,在模拟盘和实盘按需打开。企业应用场景还会多一层多账户风控,比如同一标的所有子账户的总持仓不允许超过某个额度。
订单路由模块负责把策略信号翻译成交易所指令。不同市场的指令字段差异很大:CTP 要填合约代码、开平标志、投机或套保标志;证券要填股东代码、席位信息;数字资产要填交易对和价格精度。统一订单层把这些差异封装在路由接口后面,策略只发一个目标仓位和价格约束,订单层自己做拆单、补单和状态跟踪。半自动模式下的人工确认下单,也是走这一层。
账户与持仓模块维护每个策略子账户的权益、保证金、持仓和浮动盈亏。拆子账户的原因很实际:同一个平台上可能同时跑多个策略,资金要隔离核算,不能让一个策略的亏损拖垮另一个策略的风控判定。这个模块同时是重启后恢复持仓状态的依据,后面避坑章里会专门说。
2.2 部署结构:前端工程、Java 进程与 Dockerfile
看资源包的文件结构,顶层有 .browserslistrc、index.css 这类前端构建配置,还有 Dockerfile 和 lombok.config。能判断出这套系统是"前端管理界面 + 后端 Java 服务"的标准形态。前端负责策略列表、回放进度、持仓面板、人工确认下单界面;后端负责行情、引擎、风控和订单路由。
部署时常见做法是:前端构建产物装进 Nginx 容器,后端 Java 打成可执行 jar 放进另一个容器,用 docker-compose 定义网络和启动顺序。多阶段构建是标准姿势,把你自己的 Dockerfile 改成下面这样:
FROM maven:3.9-eclipse-temurin-17 AS builder COPY . /src RUN mvn -pl backend -am package -DskipTests FROM eclipse-temurin:17-jre COPY --from=builder /src/backend/target/quant-server.jar /app/quant-server.jar ENTRYPOINT ["java", "-Xmx2g", "-jar", "/app/quant-server.jar"]多阶段构建的好处是把编译环境和运行环境分开。第一阶段用 Maven 容器做构建,第二阶段只复制打好的 jar 到精简 JRE 镜像里,镜像体积小很多,也没有多余的 Maven 依赖。lombok.config 是编译期注解处理的配置文件,运行时完全用不到,所以不用复制进第二阶段。
JVM 参数里 -Xmx2g 是堆内存上限,量化平台要缓存K线和持仓数据,堆给太小会频繁 Full GC;但也不要一次性给到 8g,JVM 预留的内存和操作系统的页缓存叠加,反而会造成内存浪费。量级上,如果你是几千个合约的行情订阅加分钟级K线,2g 到 4g 够用;要做日内 Tick 级回放并缓存大量历史分笔,建议单独给回放进程开大堆。
2.3 交易日状态机:回放和实盘共用的骨架
很多第一次接触量化平台的人会忽略一个基本问题:交易不是连续运行的。期货有日盘和夜盘,夜盘次日凌晨收盘,跨自然日的日切点不是零点而是凌晨两三点;证券有集合竞价和连续竞价;数字资产才是 7x24 小时。平台的运行骨架是一个交易日状态机,用枚举状态描述当前时段:PRE_OPEN、AUCTION、TRADING、CLOSED。
所有依赖时间的任务都要先问状态机现在是不是 TRADING,再决定要不要执行。回放模式并不特殊,它用历史行情自带的时间戳推断当时处在哪个时段。这个状态机是后面避坑章里"夜盘定时任务错乱"问题的根因,这里先记住:任何定时器、风控检查、信号执行,都不要自己用 System.currentTimeMillis() 判断交易时间,统一走状态机。
3. 历史回放与策略研发:把回测系统跑通的关键参数
3.1 回放引擎:Tick 驱动与 Bar 驱动两种模式
历史回放是这个平台最常用的功能。回放引擎支持两种驱动方式:Tick 驱动和 Bar 驱动。Tick 驱动是一笔一笔地推进,每个 tick 触发一次策略回调,撮合粒度最细,结果最接近实盘,但回放速度慢,适合日内策略的最终验证。Bar 驱动是按K线推进,一根K线合成完触发一次 onBar,回放速度极快,适合中长线策略的批量筛选。
一个容易忽略的细节是:Bar 驱动下,策略拿到的是"已经收盘的K线",还是"正在形成的K线"?许多回测系统存在未来函数,就是因为把当根K线收盘前的数据提前给了策略。平台默认的做法是把K线分成"已确认"和"形成中"两类,onBar 回调只在K线确认收盘后触发。你自己写策略时也要遵循这个习惯,不要用实时快照去计算当根K线的均线值再决定下单,那在回放里会得到虚高的胜率。
3.2 用策略骨架跑通第一轮回测:双均线例子
平台里常见的策略写法是继承策略基类,重写行情回调。下面是一个最简单的双均线策略骨架:
public class MaCrossStrategy extends StrategyBase { private final int fastPeriod = 10; private final int slowPeriod = 30; private final BarSeries series = new BarSeries(); @Override public void onBar(Bar bar) { // 只处理已确认收盘的K线 if (!bar.isClosed()) { return; } series.add(bar); if (series.size() < slowPeriod + 1) { return; } double fastMa = series.ma(fastPeriod); double slowMa = series.ma(slowPeriod); double prevFast = series.maPrevious(1, fastPeriod); double prevSlow = series.maPrevious(1, slowPeriod); if (prevFast <= prevSlow && fastMa > slowMa) { buy("ma_cross", 1); // 金叉,开多一手 } else if (prevFast >= prevSlow && fastMa < slowMa) { sell("ma_cross", 1); // 死叉,平多 } } }这段代码的关键点有三个。第一,onBar 开头先判断 bar.isClosed(),这就是上面说的避免未来函数。第二,series.maPrevious 拿到的是上一根K线的均线值,用"上一根和当前根的关系"来判断交叉,而不是用当前值大于均线这种近似写法。第三,buy 里的 "ma_cross" 是策略信号名,下单记录里会带上这个名字,回放结束后可以按信号名统计各笔交易的盈亏分布。
参数上是固定的 fastPeriod=10、slowPeriod=30,实际使用时要做成可配置项,从配置中心或属性文件读入,而不是写死在代码里。这样后面做参数扫描时不用重新编译。
3.3 回测参数设置的五个关键项
跑回放前,参数面板里有几个值直接决定回测结果可信度。整理成一张常用参数表:
| 参数项 | 推荐设置 | 说明 |
|---|---|---|
| 撮合模式 | next_bar_open 或 tick | bar.close 成交是回测高收益的最大来源 |
| 滑点 | 至少 1 跳,日内高频建议 2 跳 | 期货按跳计,股票按最小价位变动 |
| 手续费 | 按交易所标准 + 期货公司加收部分 | 按手数和按成交额两套都要填对 |
| 初始资金 | 按实际可投入资金,不要为凑仓位调小 | 调小资金会放大杠杆,虚增收益率 |
| 信号统计 | 按 ma_cross 这类信号名分组 | 分不清信号来源,优化时无从下手 |
撮合模式是最容易埋雷的一项。bar.close 成交的意思是"这一根K线收盘价就是你成交的价格",但你的信号是收盘后确认的,实际成交价只能出现在下一根K线。两者在趋势行情里差距不大,在震荡行情里差距巨大。我自己的习惯是:粗筛选用 bar 驱动 + next_bar_open,最后验证用 tick 驱动,两次结果对不上就回去查代码,绝不轻信第一次回测的曲线。
4. 实盘接入与自动交易:CTP、证券、数字资产的统一订单层
4.1 统一订单层的桥接逻辑
实盘接入是这套平台真正的分水岭。回测做得再漂亮,接不进实盘就只是个研究工具。平台在订单层做了统一抽象,把下单选价、撤单、查持仓的状态机收敛成一套通用接口,底层对接不同的交易通道。
CTP、证券、数字资产这三类通道的差异非常大。CTP 要求先认证再登录,登录成功后还要确认结算单,然后才能请求持仓和订阅行情;证券柜台接口通常要求本地维护会话和股东代码,下单前要查可买数量;数字资产交易所普遍限制下单频率,行情和交易是两套独立的 API Key。统一订单层要做三件事:限频、超时重查、幂等。限频保证任何策略都打不爆交易所的接口配额;超时重查解决"请求发出去了但没收到回报"的悬案;幂等保证同一个请求不会被重复执行。第三点在半自动场景里特别重要,后面避坑章会讲一个真实翻车案例。
4.2 CTP 接入参数与初始化顺序
期货 CTP 的接入参数固定这么几个:BrokerID、UserID、Password、AppID、AuthCode、行情前置地址、交易前置地址。以 SimNow 测试环境为例,常见配置是行情前置 tcp://180.168.146.187:10130,交易前置 tcp://180.168.146.187:10131。实盘时换成期货公司给你的柜台地址。
TradingApi api = new TradingApi(); api.subscribePrivateTopic(THOST_TERT_QUICK); api.subscribePublicTopic(THOST_TERT_QUICK); api.registerFront(tradeFrontUrl); api.init(); // 回调里按顺序执行: OnFrontConnected -> ReqAuthenticate -> ReqUserLogin // -> OnRspUserLogin -> ReqSettlementInfoConfirm -> ReqQryInvestorPosition初始化顺序是 CTP 接入最玄学的地方,顺序错了回报的错号都不一样。先订阅私有和公有主题,这决定了你收不收得到别人的成交回报;再注册前置地址并 init。连接成功后,回调里必须严格按认证、登录、确认结算、查询持仓的顺序走。认证和登录之间要有回报确认,不要在 OnFrontConnected 里同时发认证和登录请求,很多 CTP 版本会直接拒绝并发登录。
AppID 和 AuthCode 是新一代 CTP 强制要求的,只填 UserID 和 Password 会一直报"客户端认证失败"。这个错号坑了不少人,后面避坑章会再提一次。
4.3 半自动模式:信号确认再下单
半自动模式是这套平台兼顾专业和风控的设计。策略引擎持续计算信号,但不下单,把信号推到待确认列表,前端界面弹出来,人工点确认后,订单层才组装指令发出去。信号从生成到确认是有时效的,常见做法是设 60 秒有效期,超时自动作废。
这里的关键在于:人工确认只决定"要不要下",不参与"怎么下"。下单的合约、价格、手数、开平标志全部由策略信号带过来,界面不能改。这样既保留了人工否决权,又避免操作者在紧张行情里填错数字。半自动模式很适合刚开始做实盘的人,先让策略给建议、自己盯几周,确认信号质量和执行逻辑没问题,再切全自动。
5. 实盘前的避坑排查:五个最容易翻车的地方
5.1 回测收益曲线很漂亮,实盘却连续亏损
现象:回测年化翻倍,实盘跑两个月不仅没赚,还亏掉了回测回撤的三倍。
原因:回测撮合用了 bar.close 成交,等于假设信号出现的瞬间就能按收盘价成交;实际上信号是收盘后才确认的,成交只能发生在下一根K线。遇到跳空行情,你按前一根K线收盘价挂单,实际成交价差出去好几个跳。如果策略里还用了当根K线的实时快照计算指标,那就是未来函数,回测的高收益大部分是幻觉。
解决:撮合模式改成 next_bar_open,滑点加至少 1 跳,主流合约建议 2 跳。固定参数后重新跑一遍,如果收益曲线大幅缩水,不要觉得可惜,那是你原本就要付的成本。最终上实盘前,用 tick 驱动再验证一轮。
5.2 CTP 连接上了但收不到行情
现象:OnFrontConnected 回调触发了,登录也提示成功,但订阅合约之后没有任何行情回报。
原因:八成是行情前置地址配错了,或者订阅后没有等订阅回报就急着发单。SimNow 的行情和交易前置是两个独立端口,把交易地址填到行情接口里,能连上但永远是空数据。
解决:打开行情回报日志,确认 OnRspSubMarketData 里返回了成功,再标记订阅完成;订阅完成之前,策略引擎不要尝试下单。另外确认当前时间是不是交易时段,CTP 在非交易时段不推送 tick,但会推送离线K线,别把离线K线当成实时行情。
5.3 重启后持仓对不上,平仓信号被吞
现象:程序重启后,内部持仓被重置为 0。此时策略发出平仓信号,引擎发现持仓为 0,直接忽略了离场指令,行情随后反向,利润全吐回去。
原因:持仓只存在内存里,启动时没有做持仓恢复。实盘中程序重启是必然会发生的事,不是概率问题。
解决:启动流程里强制加一步"持仓同步"——从交易所拉一次实际持仓,和本地记录比对,以交易所为准更新缓存,等确认一致后,策略引擎才进入 TRADING 状态。这一步不能跳过,也别指望交易所有推送告诉你重启期间发生了什么。
5.4 夜盘时段的定时任务错乱
现象:某些风控任务固定在晚上 10 点跑,但夜盘品种交易到凌晨,第二天早上程序补跑任务,发出无效指令。
原因:定时任务用的是系统本地时间,没有对齐交易时段状态机。夜盘跨自然日,按日期跑的定时器必然错乱。
解决:所有时间敏感任务统一挂到交易日状态机上,用交易所时间判断当前时段。平台提供的 MarketSession 状态机直接用就行,不要再自己写 cron 表达式去匹配期货夜盘,那个边界条件多到你测不完。
5.5 半自动确认出现重复下单
现象:前端确认按钮点了一次,后端却下了两组单。查日志发现是 WebSocket 重连后重放了确认消息。
原因:订单请求没有幂等键。网络重连、前端重试、双击确认,任何一个情况都会让同一个信号被执行多次。
解决:每笔订单生成唯一的 clientOrderId,下单前先查这个 id 是否已经处理过,内存和 Redis 各做一次判断。处理过的请求直接返回已成功,不再重复组装订单。从那以后我再也不信"用户不会双击"这种假设,全自动和半自动都必须有幂等兜底。
6. 进阶:把 AI 信号接进策略引擎:基于 ONNX Runtime 的买入阈值实例
平台预留的行情特征计算层可以直接给 AI 模型用。我常用的方案是:Python 训练模型,导出 ONNX 格式,Java 侧用 ONNX Runtime 加载推理。这样训练和实盘推理解耦,Python 生态负责复杂的特征工程,Java 侧只做前向计算。
try (OrtSession session = OrtSession.create(env, modelPath)) { OnnxTensor input = OnnxTensor.createTensor(env, featureArray); OrtSession.Result result = session.run( Collections.singletonMap("input", input)); float[][] scores = (float[][]) result.get(0).getValue(); if (scores[0][0] >= 0.8) { orderService.buy("ml_signal", 1); } }这段代码的逻辑是:把特征数组喂给模型,拿到一个 0 到 1 之间的分数,超过阈值就触发买入信号。模型输入的特征数组必须在 Java 侧实时计算,所以特征标准化用的均值和方差要固化到模型里,或者作为常量存到配置文件,不要在 Java 侧临时算。
这里有个坑值得单独说:ONNX 模型的输入维度是固定的,特征数组的顺序和 Python 训练时完全一致,差一个字段推理结果就全偏了。我实习过两轮因为特征错位导致的假信号,后来定下一条规矩:每次接模型,强制走一遍三段对齐——Python 训练环境里对某一段历史数据的预测值、Java 推理对同一段数据的输出、回放引擎里实际触发的信号,三个值必须完全一致,才允许上模拟盘。
这个流程看着笨,但确实拦下了很多问题。模型分数在 0.6 到 0.8 之间时不要下单,只在稳定超过 0.8 时执行。阈值太高会错过行情,阈值太低会被噪声骗,你可以统计历史预测分布来定,让阈值卡在样本的 90 分位左右。希望帮到你。
本文还有配套的精品资源,点击获取