基于Java的AI开源量化交易平台搭建:从回测到实盘实践

发布时间:2026/10/4 13:13:28
基于Java的AI开源量化交易平台搭建:从回测到实盘实践 简介面向程序员与量化交易爱好者的Java版AI开源量化交易平台覆盖期货、股票、外汇、数字资产等场景功能上覆盖历史回放、策略研发、模拟交易与实盘交易既能全自动运行也支持半自动人工介入可替代文华、MC、金字塔等常用软件。整包共578个文件压缩包仅1.83MB核心为407个Java源文件后端业务与策略引擎均由Java驱动62个JavaScript、24个Vue文件构建Web管理界面14个XML与10个JSON分别承担配置定义与数据交换另有YML、Dockerfile及p12、pem等证书文件方便部署与安全连接。已有195人浏览学习。借助这套源码可梳理量化交易系统从行情接入、策略回测到实盘下单的完整链路理解前端展示与后端调度如何协作也能基于现有模块做二次开发快速搭建自己的全自动或半自动程序化交易环境。1. 基于JAVA的AI开源量化交易平台为什么值得从商业软件里跳出来如果你这几年一直在文华、MC、金字塔上写策略大概率有过这种憋屈策略代码被锁在供应商格式里换平台就得重写想接AI模型却发现没有开放接口回测跑得飞快实盘却像换了策略。这种“黑匣子体验”正是基于JAVA的AI开源量化交易平台想解决的。它把历史回放、策略研发、模拟交易、实盘交易拆成可审计模块既可以全自动跑单也可以只推送信号、由人确认后下单。适合一手写Java、又想摆脱商业平台锁定的个人交易者和量化小团队。下面按我落地这类平台的顺序展开。2. 平台核心架构JAVA生态里量化交易该有的拼图这类平台要同时应对全自动与半自动场景最忌讳把所有逻辑写在一个上帝类里。常见做法是分成五层接入层负责行情和交易接口服务层负责K线加工与切片策略层只产出信号路由层决定信号去向执行层负责下单并回报。JAVA生态里做这五层不需要太重的框架Spring Boot管配置TA4J做指标Disruptor或线程池做事件排队模型推理可以单独跑一个服务避免策略计算拖慢交易主链路。下面三个小节是我最看重的三块拼图。2.1 数据层行情接入与K线历史的存储选型数据层决定了策略能吃多新鲜的料。实盘行情一般是Tick级推送但策略模型大多数只用K线所以接入层要做的第一件事是分周期合K线。常见做法是用一个事件总线接收Tick快照按时间戳聚合成1分钟、5分钟、15分钟K线再向上衍生小时线和日线。JAVA里做这个事情很顺手ConcurrentHashMap维护每合约的聚合桶ScheduledExecutorService负责定时收口。聚合参数一般设为“T1”模式即一根K线结束后的下一个Tick才把K线推给策略这样才能避免收盘瞬间的噪声。存储选型上我一般会把Tick和分钟K线分开存。Tick数据量大适合列式存储或时序库字段只保留时间、最新价、成交量、持仓量分钟K线可以用一张明细表存结构类似这样CREATE TABLE bar_1m ( symbol VARCHAR(32) NOT NULL, bar_time TIMESTAMP NOT NULL, open_px DOUBLE NOT NULL, high_px DOUBLE NOT NULL, low_px DOUBLE NOT NULL, close_px DOUBLE NOT NULL, volume BIGINT NOT NULL, open_oi BIGINT NOT NULL, PRIMARY KEY (symbol, bar_time) );这张表的主键是“合约时间”好处是同一根K线重复写入时会冲突天然防止行情重复推送造成的数据脏写。配合TimescaleDB这类时序插件做超表能很轻松在分钟级数据上跑10年历史回放。参数上距离当前时间越近的K线越容易被修正尤其是盘后结算价调整。回放和研发建议都使用“已经收盘且不复权”的数据如果有复权需求期货品种按“涨跌停板调整”处理股票则按后复权。不要为了省空间把K线精度压到只保留浮点收盘价开高低、持仓量、成交额都是AI特征最容易用到的原始字段。2.2 策略层AI信号与规则信号如何在一个框架里共存策略层是这类平台的核心。很多开源项目会为“均线策略”“MACD策略”写死接口结果接入AI模型时又要改框架。我常用的做法是让所有策略实现同一个接口规则策略和AI策略都返回同一种信号对象。public record Signal(String strategyId, String symbol, Direction direction, double confidence, double lotsRatio, SignalSource source) { public enum Direction { BUY, SELL, CLOSE, HOLD } public enum SignalSource { RULE, AI } }confidence是策略对这次信号的置信度全自动模式下由风控过滤半自动模式下把它显示给交易员作为人工确认的参考。lotsRatio表示本次发单占当前可用资金的比例AI策略和规则策略共用这个结构后后续的信号合并、去重、路由都只需处理Signal不需要区分来源。接口定义我会尽量做薄只暴露三个方法public interface TradingStrategy { void onBar(Bar bar, BarSeries context); OptionalSignal onTick(Tick tick); void onPositionChanged(Position position); }onBar负责K线级别的决策onTick负责盘口级别的微调。半自动场景下通常只跑onBar就够了实盘依赖推送延迟较低时再开onTick。这里有个容易混淆的地方不是说AI策略就必须实时逐Tick推理很多AI信号其实是用小时级特征预测下一根K线方向盘中推理反而会把误报放大。为了兼顾规则和AI我会在策略内部再拆两个组件特征提取器和信号生成器。规则策略的特征提取器可能只是close - MA20AI策略的特征提取器则是一个标准化后的N维向量两者喂给不同的信号生成器都能产出Signal。这套设计的价值在于当你把策略从规则切到AI或者从AI切回规则路由层完全不用改动。2.3 决策与风控半自动场景下的“人机协同”设计全自动和半自动的区别不在策略而在信号路由。全自动模式下SignalRouter收到高置信度信号后直接送执行层半自动模式下信号先进一个确认队列交易员在确认台点击“接受”后才会下单。这里最常见的误区是半自动等于“自动弹窗”实际上半自动必须有超时机制信号有效时间默认30秒超时未确认就自动丢弃避免策略已经改变观点人工却还在犹豫上一个信号。风控不能只依赖人工尤其是半自动场景下AI可能会在短时间内给出多个方向相反的信号。我一般会在路由层和执行层之间加一层风控过滤器每个过滤器都实现同一个接口public interface RiskFilter { boolean reject(Signal signal, PortfolioView portfolio); }一个典型的过滤器组合是单笔止损限额、单日最大亏损限额、仓位集中度上限。比如SingleLossLimitFilter会计算当前信号可能造成的最大亏损根据止损价和持仓量如果超过账户权益的0.8%就直接拒绝而不是修改信号。这个“拒绝而不是修改”的原则很重要半自动模式下一旦风控改过信号方向人工和机器就分不清谁对谁错。需要说明的是这些过滤器在回测里同样要跑而且回测和实盘的参数必须保持一致。很多平台只在实盘时才想起风控回测用的是“理想账户”最后得出的资金曲线在实盘里基本无法复现。把风控写进回放引擎反而是这类开源平台比商业软件更容易做扎实的地方。此外半自动场景下人工确认台需要看到最简单的信息时间、品种、方向、建议仓位、置信度、依据摘要。不要把一堆特征图硬塞给操作者按我的经验人工确认决策时间超过10秒后交易员会开始依赖直觉而不是平台。这里还值得点一下AI Agent的思路——策略本质上是一个对环境做感知、决策、行动的Agent只是这个Agent可以被人接管。用JAVA写这套骨架时尽量保持接口小、责任单一后面换数据源、换模型都会轻松很多。3. 把最小可用平台跑起来从历史回放到模拟交易在空环境里搭一套能验证策略的骨架比研究复杂架构更能判断一个开源项目是否靠谱。下面这套流程是我会先跑通的最小闭环读取历史K线、推进时间轴、让策略逐根K线产生信号、用模拟账户撮合、记录资金曲线。这个闭环跑通后再往里面加AI模型和实盘接口。3.1 用 Spring Boot TA4J 搭一个策略回放骨架Spring Boot在这里不是必须的但它能把配置、日志、定时任务都统一管起来后续接实盘接口时也方便做依赖注入。TA4J则是JAVA生态里最常用的技术分析库提供BarSeries、Bar、指标计算等基础结构。回放骨架的第一步是把历史K线装载成BarSeries。// 从数据库读出Bar记录后按时间顺序装入TA4J序列 BarSeries series new BaseBarSeries(RB2410); for (BarRecord rec : barRecords) { Bar bar new BaseBar( Duration.ofSeconds(60), // K线周期 rec.open(), rec.high(), rec.low(), rec.close(), rec.volume(), // 成交量 rec.openInterest() // 持仓量 ); series.addBar(bar); }注意BaseBar的时间戳默认使用InstantK线周期用Duration表示。这里最容易出错的是时区国内期货用北京时间回放时如果直接取System.currentTimeMillis()会把凌晨夜盘时间算错。通常做法是统一使用Asia/Shanghai时区并在装载K线时把交易所时间转成Instant否则后续画资金曲线时日期会错位。参数上装载阶段先不要做什么过滤但可以提前把涨跌停限制、未成交合约的清理标记出来。回放时要严格使用“已经收盘后”的数据如果数据里有未完成的最后一段K线建议直接去掉不然策略会看到未来。3.2 用时间轴推进把历史回放“拨盘”转起来历史回放和回测不一样的地方在于回测常常一次性跑完全部K线而回放是逐根K线推进策略只能看到当前K线及之前的数据这样能模拟实盘的在线推理过程。下面这个ReplayEngine是核心。public class ReplayEngine { private final BarSeries series; private final TradingStrategy strategy; private final SimBroker broker; private int cursor 0; // 每根K线之间停顿的毫秒数用于控制回放速度 private long barIntervalMs 1000; public void runOneBar() { if (cursor series.getBarCount()) { stop(); return; } Bar bar series.getBar(cursor); strategy.onBar(bar, series.getSubSeries(0, cursor 1)); broker.checkPendingOrders(bar); cursor; } }getSubSeries(0, cursor 1)是关键传给策略的始终是从0到当前K线的子序列策略内部计算MA、RSI时不会看到未来数据。barIntervalMs1000表示每1000毫秒推一根1分钟K线相当于加速回放如果设成0就会以最快速度跑完全部历史适合做参数扫描。回放过程中还需要支持暂停、单步、手动结束。最简单的实现是把引擎挂到一个线程池里用volatile状态位控制暂停public void startLoop() { while (!stopped) { while (!paused cursor series.getBarCount()) { runOneBar(); sleep(barIntervalMs); } if (cursor series.getBarCount()) break; sleep(100); } }这个循环不是高性能方案但作为最小可用版本足够直观。等策略验证通过后可以再换成定时任务触发或者按Tick队列来驱动。回放的日志要带时间戳和当前K线的收盘价方便出问题时按图索骥。3.3 模拟撮合与资金曲线先验证逻辑再碰实盘策略产生信号后不能直接把信号当作成交。模拟撮合要尽量接近实盘规则最简单的模拟账户按K线收盘价成交用SimBroker维护仓位和资金。public class SimBroker { private final double multiplier; // 合约乘数 private final double feePerLot; // 单边手续费元/手 private final double slippageTick; // 滑点按最小变动价位 private double cash; private int position; public OrderResult execute(Signal signal, Bar fillBar) { double fillPrice signal.direction() Signal.Direction.BUY ? fillBar.getClosePrice() slippageTick * tickSize() : fillBar.getClosePrice() - slippageTick * tickSize(); int lots calcLots(signal.lotsRatio(), fillPrice); double fee lots * feePerLot; double turnover lots * multiplier * fillPrice; if (signal.direction() Direction.BUY) { position lots; cash - turnover fee; } else if (signal.direction() Direction.SELL) { position - lots; cash turnover - fee; } return new OrderResult(fillPrice, lots, fee); } }撮合参数至少有三个不能省合约乘数、手续费、滑点。国内期货大多数品种手续费按手数收比如螺纹钢每手几元股票和ETF按成交额比率收。滑点常常被忽略但AI策略尤其吃滑点因为模型利润往往来自很小的价差没有滑点假设的回测会比实盘好看很多。我一般把滑点设为1个最小变动价位周末回测再拉高到2个看策略是否仍然盈利。模拟撮合跑完所有K线后要按“每日收盘后权益”画资金曲线。计算方式为账户权益 现金 当前持仓按收盘价计算的市值 - 浮动手续费。不要在盘中每次Tick都重算权益那样会把回放速度拖慢而且对判断策略没有额外帮助。只要每天收盘时点把权益记录下来就能算出最大回撤、夏普比率、盈亏比这几个关键指标。让人惊喜的是这些指标在TA4J里都有现成分析模块可以直接复用。4. 策略研发与AI集成特征、训练、在线推理的落地参数规则策略的逻辑可以直接写在onBar里AI策略则需要一条更谨慎的流水线特征工程、模型训练、阈值确定、在线推理。很多开源平台会把“AI”做成噱头实际上只是调了一个外部接口。我们需要的是能回测、能审计的AI信号源。4.1 把K线变成特征矩阵常见因子与归一化参数模型不认K线只认特征向量。从原始BarSeries提取特征时我会先用下面这一组基础因子连续收益率、波动率、RSI、成交量变化率、乖离率。这五个因子在大多数品种上都有一定预测性而且计算成本低。public double[] buildFeatures(BarSeries series, int idx) { double[] f new double[5]; f[0] (series.getBar(idx).getClosePrice() - series.getBar(idx - 1).getClosePrice()) / series.getBar(idx - 1).getClosePrice(); // 单期收益率 f[1] standardDeviation(series, idx, 20); // 20根K线的滚动标准差 f[2] new RSIIndicator(series, 14).getValue(idx); // 14期RSI f[3] (double) series.getBar(idx).getVolume() / (averageVolume(series, idx, 5) 1e-6); // 量比 f[4] (series.getBar(idx).getClosePrice() - new SMAIndicator(series, 20).getValue(idx)) / new SMAIndicator(series, 20).getValue(idx); // 乖离率 return normalize(f, featureMean, featureStd); }normalize这一步是AI策略和规则策略的分水岭。归一化的均值featureMean和标准差featureStd必须在训练集上一次性算好并固化不能在回测过程中用滚动窗口实时更新。如果滚动更新模型在样本外的表现会被严重高估因为它在回放时偷偷“看了一眼”未来统计量。参数上RSI的周期常用14SMA窗口常用20波动率的滚动窗口可以选择20到60。不要把所有窗口都设成同一个值那样特征之间相关性过高模型容易学偏。特征矩阵保存为float[][]比较省内存10年分钟K线大概几百万行用double数组也不会太吃力。4.2 用模型预测作为信号源阈值与滑点怎么定模型输出通常是概率或分数要变成Signal必须设阈值。这个阈值不是随便定的它和手续费、滑点直接相关。假设模型对下一根K线上涨的概率输出为p设置买入信号的条件一般是p 0.6。如果阈值太低信号太多交易成本会吃掉利润阈值太高信号太少样本量和复利都会受影响。public OptionalSignal aiSignal(double probability, Bar currentBar, String symbol) { if (probability 0.60) { double lotsRatio 0.5; // 单次信号占用50%可用资金 return Optional.of(new Signal( ai-rf-v1, symbol, Direction.BUY, probability, lotsRatio, SignalSource.AI)); } else if (probability 0.40) { double lotsRatio 0.3; return Optional.of(new Signal( ai-rf-v1, symbol, Direction.SELL, 1 - probability, lotsRatio, SignalSource.AI)); } return Optional.empty(); }这里有个参数含义容易被误解confidence用的是概率值本身而lotsRatio是根据“凯利公式的1/4”折算出来的仓位。凯利公式会给出一个偏激进的仓位比例直接使用会导致回撤太大所以我会把它除以4这样在回测里得到的最大回撤更接近实盘的体验。AI策略的滑点成本比规则策略更高因为模型换仓频繁每个信号都要付出双边费用。假设在1分钟K线上做预测回测时至少把手续费和滑点都调高一倍再评估。训练时还要小心“同品种样本时间不独立”的问题。同一根1分钟K线的特征和下一根K线的标签天然有自相关性如果把全部样本随机切分训练集和测试集会严重高估准确率。正确做法是按时间段切分比如2020到2023年训练2024年测试而不是随机抽样。4.3 回测与实盘参数一致性能用复现才能上线策略研发的终点不是模型准确率而是实盘可复现。我在回测和实盘之间设了一份“参数一致性清单”每次上线都要逐项核对同一样本合约、同样的K线时间戳口径、同样的归一化参数、同样的手续费和滑点、同样的信号去重规则。任何一项不一致资金曲线就没有参考价值。一个便于核对的配置会放在版本管理里而不是写在代码里。下面这段用application.yml会显得更直观strategy: ai-threshold: 0.60 lots-ratio: 0.5 stop-loss-atr: 2.5 max-open-bars: 12 feature-window: 20 rsi-period: 14 normalization-file: classpath:norm/rb-feat-v3.json simulation: slippage-ticks: 1 fee-per-lot: 2.5 margin-rate: 0.12这份配置本身就是“后悔药”策略上线后如果表现不如预期先看配置而不是改代码。很多开源量化平台只做回测和实盘却忽略了参数版本管理导致同一个模型在回测和实盘时行为不同。我的做法是把配置、特征归一化参数、模型权重一起打进同一个发布包上线时打印指纹回测时也打印指纹两个指纹一致才允许继续。回测时的另一个隐蔽不一致是持仓周期。AI策略如果只是根据概率开仓但没有规定最大持仓K线数模型可能会在震荡行情里拿着亏损单很久。我一般会加max-open-bars12意思是12根K线后无论盈亏都强制平仓。这个参数虽然在规则策略里不常用但对AI信号尤其重要直接决定资金曲线的回撤深度。5. 实盘接口与全自动/半自动切换避坑与排查如果回放和模拟交易跑得很顺接下来的实盘接入才是真正劝退大多数人的地方。商业软件把实盘接口包得严严实实开源平台则把接口暴露给你坑也得自己踩。下面把我在JAVA开源量化平台里踩过的五个典型坑写清楚每条都按现象、原因、解决的顺序来。5.1 行情与交易接口的三个常见坑第一个坑回测盈利不错但接上实盘后连续亏损。现象是策略在实盘第一周就频繁止损资金曲线明显低于回测。原因通常是撮合凭据不同回测时在最优点附近撮合实盘则必须按对手价或最新价成交滑点比回测假设大得多。解决方法是把模拟撮合改为保守模式至少按“下一根K线开盘价1个tick”成交不考虑最优价。第二个坑行情接口偶发断线策略却在断线期间照常发单。现象是夜盘开盘后K线推送停了十几秒策略基于旧数据连发了三次加仓信号。原因是许多开源项目只在收到行情时更新K线没有做“行情不更新则暂停新信号”的保护。解决方法是给行情接口加心跳超时比如3秒内没有新Tick就把策略状态切到HALT所有信号只记录不执行。第三个坑AI模型推理延迟导致信号迟到。现象是盘中每分钟策略都跑一次模型但模型偶尔要花2秒才返回信号来的时候K线已经走完。原因是把模型推理放在了交易主线程上。解决方法是把AI推理放到单独的ExecutorService用最迟完成时间做限制超过500毫秒的推理结果直接丢弃避免用旧数据下单。这三个坑并不是只在自研平台出现商业软件也会遇到只是它们把原因藏起来了。用开源平台至少能看到日志能定位到是哪一行逻辑出了问题。5.2 全自动转半自动手滑止损的唯一解从全自动切到半自动最大的变化是“下单权”从策略手里交到了人手里。最容易出的坑是交易员收到信号后手滑点错了方向或者点了两次。现象就是多了一笔反向单而程序还认为策略没有持仓。原因在于半自动确认台没有做“信号幂等”。解决方法是给每个Signal分配一个signalId确认台收到人工点击后按signalId去重同一个信号只能确认一次。另外人工确认后不要直接调用下单接口而是生成一个“意图单”发给执行层执行层再与当前持仓核对方向意图与当前持仓方向相反且超出仓位限制时执行层拒绝并提示“反向手数超限”。这里还要养成一个习惯半自动模式下的止损单不能依赖人工。策略可以只负责发信号但止损必须是自动单或者至少是挂单。原因很简单人在开盘瞬间看到剧烈波动时很难理性判断止损价手滑和犹豫都会造成更大的亏损。所以开源平台即使在做半自动止损执行链路上也应该是自动的。5.3 重启恢复与挂单一致性开盘前的检查清单最后一个高频坑出现在程序重启后。现象是平台崩溃恢复后策略以为自己没有持仓而券商账户里其实还有上次残留的仓位于是策略再次开仓持仓翻倍。原因是本地持仓状态没有持久化或者没有在启动时先同步。解决方法是写一个“启动对账”流程开盘前先调用交易接口查询真实持仓和挂单然后与本地保存的持仓做差集不一致时禁止开新仓只允许平仓操作。这个流程不能省尤其在国内期货夜盘合约换月、结算后保证金变化都可能让本地状态失真。我一般会在开盘前跑一遍检查清单行情接口已连接交易接口登录成功后查询了账户权益持仓和挂单与本地一致风控参数加载成功策略信号路由处于“只提示不下单”的测试模式。测试模式跑满10分钟无异常后再切到全自动或半自动。这套流程看似保守但能拦截掉大部分重启后的人为失误。提示实盘切换前把模拟账户跑一天与实盘相同的合约和参数重点观察下单延迟、回报顺序、撤单响应时间。这些指标在回测里看不到。6. 用“日志回放固化参数”做一次上线前体检到这里平台已经能回测、模拟、实盘了但我还是不会直接上实盘。我养成了一个习惯上线前先做一次日志回放体检。具体做法是让平台在模拟交易模式下跑一整天把所有信号、下单请求、成交回报、风控拒绝记录都输出成JSON行然后把这个日志当作“实盘数据”重新喂给回放引擎。回放引擎会复算每一根K线比对新产生的信号和原日志中的信号是否完全一致。这个方法能抓出两类问题。一类是时序问题比如策略在实盘里看到的K线顺序和回放时不一致导致信号错位另一类是状态残留问题比如策略收到onPositionChanged后更新了内部状态但在回放里这个回调没有触发导致仓位计算不同。两者都会在JSON日志的对比中现出原形。比对工具不需要写得很复杂用jq就能筛选出信号不一致的时间点jq -r select(.typeSIGNAL) | [.time,.symbol,.direction,.signalId] | tsv sim-day.json sim-signals.tsv jq -r select(.typeSIGNAL) | [.time,.symbol,.direction,.signalId] | tsv replay-day.json replay-signals.tsv diff sim-signals.tsv replay-signals.tsv两条TSV文件要一模一样才说明回放引擎和模拟引擎行为一致。如果diff结果不为空我会先查信号ID再回到那根K线附近看日志上下文绝大多数问题都出在K线时间戳的时区或者SubSeries的截断边界上。固化的参数要和日志一起留档。特征均值、标准差、模型权重、阈值、滑点、手续费、风控限额全部打进一个带日期和git提交号的包里。这等于给每次实验都拍了张身份证一个月后再回来看某个资金曲线还能准确说出当时用的是哪组参数。注意日志里一定不要记录账户密码或会话密钥。JSON行只写时间、合约、价格、手数、信号类型就够了敏感字段单独存到安全的地方。我吃过一次亏升级特征库后忘记重算归一化参数回测曲线看起来很好实盘信号却全部偏向一边。后来养成了把归一化参数和模型权重一起提交的习惯再也没出过同类问题。量化交易最大的敌人不是模型不够强而是你根本不知道自己的策略在实盘里为什么变了样。先把日志回放和参数固化这两件事做扎实再上更多的AI模型和交易接口都不迟。希望帮到你。本文还有配套的精品资源点击获取