Java开源AI量化平台:事件驱动打通回测与实盘
2026/9/15 21:13:24 网站建设 项目流程

简介:一套基于 Java 的人工智能开源量化交易平台源码包,面向有编程基础的量化开发者和交易团队,覆盖期货、股票、外汇、数字货币等场景,具备历史回放、策略研发、模拟交易、实盘交易功能,可替代文华财经、MC 与金字塔软件,兼顾全自动与半自动运行方式。压缩包共 578 个文件、约 1.83MB,以 Java、Vue、JS、XML、JSON、YML 等源码和配置为主,分别对应后端引擎、前端界面、行情接入与多环境部署,另有 Dockerfile、证书与部署脚本,工程结构完整,便于二次编译和使用。资料还包含前后端交互流程、典型策略示例、CTP 等通道对接参考及部署脚本,可帮助开发者在本地搭建完整量化系统,用历史数据回放验证策略后,再逐步升级到模拟盘与实盘。已有 191 人学习下载,适合希望深入程序化交易或自建量化平台的技术人员参考。

1. JAVA系AI量化开源平台:为什么敢说替掉文华、MC与金字塔

用过文华、MultiCharts或金字塔的人,大概都被同一件事卡过:策略脚本只能在自家平台运行,想接一个机器学习模型,得把行情导成CSV外部算完再导回;想加半自动的人工确认环节,脚本绕来绕去也只能做到弹窗提示。JAVA系开源AI量化交易平台把这层纸捅破了——策略用Java写成,历史回放、模拟交易、实盘交易共用一套代码,回测信号直接流转到交易通道,半自动模式下信号先进待确认队列,人工确认后才真正下单。AI在这里不是噱头,特征计算、模型预测都能以Java库或独立服务方式嵌进策略流水线,不再需要外部算完再导回的笨办法。接下来的内容按选型、回测、交易接入、迁移四条线展开,目标读者是被商业平台绑住、想转开源JAVA方案的中高级量化开发者。

2. 事件驱动引擎与行情数据模型:JAVA量化平台能回测也能实盘的底座

2.1 回测和实盘为什么要共用一套事件循环

很多商业平台的历史回测和实时交易走的是两条不同的调用链:回测时平台按收盘价算信号,实盘时信号触发逻辑由另一个调度器控制,两头口径经常对不上。JAVA系量化平台普遍采用事件驱动架构,一个统一的事件循环从数据来源里不断取出新事件(新的K线、新的逐笔、新的委托回报),分发给注册的监听器。回测时数据来源是CSV文件或数据库,按时间顺序把历史数据重放出来;实盘时数据来源是交易柜台推送,事件到达顺序就是真实行情产生顺序。策略代码里始终只看到onBar、onTick回调,底层是文件还是网络,策略本身不需要关心。

这样的好处是,策略在回测里的每一次下单、撤单、止损行为,和实盘里执行的逻辑是同一段字节码,不存在“回测用收盘价、实盘用最新价”这种口径漂移。代价是引擎层要处理时间对齐、数据缺失和事件排序,这些复杂度停留在框架内部,不会泄露到每份策略文件里。选型时优先看这个事件循环是否支持回测与实盘共用,而不是看它封装了多少现成指标。

2.2 行情对象与事件接口:用不可变对象承载每一根K线

行情数据模型是整个平台的地基。核心对象是K线(Bar)和逐笔(Tick),平台之间的差别往往就体现在字段的语义上。建议的Bar模型要把所有字段在构造时一次性赋值,只提供getter,做成不可变对象:

public final class Bar { private final String symbol; private final LocalDateTime time; private final double open; private final double high; private final double low; private final double close; private final double volume; private final long openInterest; public Bar(String symbol, LocalDateTime time, double open, double high, double low, double close, double volume, long openInterest) { this.symbol = symbol; this.time = time; this.open = open; this.high = high; this.low = low; this.close = close; this.volume = volume; this.openInterest = openInterest; } public LocalDateTime getTime() { return time; } public double getClose() { return close; } public boolean isBull() { return close >= open; } }

把Bar做成final类并由构造器一次性赋值,是为了让K线对象可以在多线程之间安全传递。回测引擎经常用多线程并行跑多个品种或多个策略实例,如果Bar允许setClose(),一个线程修改最新价,另一个线程的均线计算就会错乱,而且这种错乱几乎无法复现。策略代码全部只读Bar的引用,等于绕开整类并发问题。

事件监听接口用JDK 8的default方法实现,策略类只覆盖自己关心的事件,不需要实现空方法:

public interface EventListener { default void onBar(Bar bar) {} default void onTick(Tick tick) {} default void onOrderUpdate(OrderStatus status) {} }

2.3 数据源抽象:回放文件与实时行情只差一个实现类

在事件驱动架构里,数据源被抽象成可以按时间顺序消费的对象。回测逐根拉取历史K线,实盘订阅实时推送,这两件事收敛为同一个接口之后,策略研发阶段用CSV回放,上线前切到实时订阅,策略代码一行都不用改:

public interface BarSource extends AutoCloseable { Optional<Bar> nextBar(); // 回测用:按时间顺序取下一根 void subscribe(String symbol, BarListener listener); // 实盘用:注册K线更新回调 void close() throws IOException; }

选择数据粒度时参考下表。商品期货主力合约的Tick量级在每天几万到二十万条之间,这个量级直接决定回放速度和内存占用:

数据粒度单品种单日数据量相对实时回放速度典型适用策略
Tick级5万-20万条0.5-2倍高频做市、盘口博弈
1分钟Bar300-600条50-200倍日内动量、突破
5分钟及以上50-150条500倍以上中低频趋势、跨日策略

Tick级回放很难比实时更快,因为每一条都要触发策略计算;如果只是研发日线级别策略,没必要把Tick全量喂进引擎,按1分钟甚至5分钟K线回放即可,速度和内存能省下一个量级。这里说的“回放速度”是指引擎消化历史数据的速度,不是网络传输速度。

2.4 滑点与手续费模型:回测里最容易被低估的两个参数

回测报告好看、实盘却亏损,九成问题在滑点和手续费。常见做法是把滑点建模成最小报价单位的倍数,以螺纹钢为例tick size是1元/吨,买入按现价加1个tick成交,卖出减1个tick:

public class FixedTickSlippage { private final int ticks; public FixedTickSlippage(int ticks) { this.ticks = ticks; } public double apply(OrderRequest request, double currentPrice, double tickSize) { if (request.getSide() == Side.BUY) { return currentPrice + ticks * tickSize; } return currentPrice - ticks * tickSize; } }

做多时在现价上方成交,表示追价买入;做空时在现价下方成交。ticks参数很敏感,趋势策略日内交易频繁,通常设1-2个tick;如果策略假设限价单总能被动成交,回测里设0个tick会导致实盘成交率严重偏离。手续费按成交金额的万分比配置,不同渠道费率不同,回测前从结算单里把真实费率拿出来填进去,而不是用平台默认值。保证金比例影响可用资金与爆仓判断,商品期货一般在12%-15%区间。

提示:滑点和手续费参数在策略研发阶段就要固定。反复修改这两个参数去拟合回测结果,实盘前再换回“合理值”,等于自己骗自己。

3. 历史回放与策略研发:跑通JAVA量化平台的第一个回测闭环

3.1 策略基类:onBar里只写逻辑,不碰执行通道

新平台上的第一个策略建议从基类开始。基类封装下单、持仓管理这些通用能力,子类只覆写与行情相关的回调:

public abstract class BaseStrategy implements EventListener { private final String symbol; private double position; protected BaseStrategy(String symbol) { this.symbol = symbol; } @Override public final void onBar(Bar bar) { if (!bar.getSymbol().equals(symbol)) return; onBarForSymbol(bar); } protected abstract void onBarForSymbol(Bar bar); protected void buy(double quantity, String tag) { ExecutionContext.get().submit( OrderRequest.market(symbol, Side.BUY, quantity, tag)); } protected void sell(double quantity, String tag) { ExecutionContext.get().submit( OrderRequest.market(symbol, Side.SELL, quantity, tag)); } }

onBar声明为final,隔离框架行为与子类逻辑;子类只写onBarForSymbol,并且通过buy/sell提交市价单。ExecutionContext封装了当前运行环境,回测时提交给撮合器,实盘时提交给通道,这是回测与实盘共用代码的关键。

3.2 最小可复现的双均线示例

双均线策略足够说明整条链路。窗口参数从构造器传入,窗口内用ArrayDeque维护最近N根收盘价:

public class DualMaStrategy extends BaseStrategy { private final int fastPeriod; private final int slowPeriod; private final ArrayDeque<Double> fastPrices = new ArrayDeque<>(); private final ArrayDeque<Double> slowPrices = new ArrayDeque<>(); public DualMaStrategy(String symbol, int fastPeriod, int slowPeriod) { super(symbol); this.fastPeriod = fastPeriod; this.slowPeriod = slowPeriod; } @Override protected void onBarForSymbol(Bar bar) { double close = bar.getClose(); fastPrices.addLast(close); slowPrices.addLast(close); if (fastPrices.size() > fastPeriod) fastPrices.removeFirst(); if (slowPrices.size() > slowPeriod) slowPrices.removeFirst(); if (slowPrices.size() < slowPeriod) return; double fastMa = average(fastPrices); double slowMa = average(slowPrices); if (fastMa > slowMa && position() == 0) { buy(1, "ma_cross_up"); } else if (fastMa < slowMa && position() > 0) { sell(position(), "ma_cross_down"); } } private double average(ArrayDeque<Double> deque) { return deque.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); } }

注意position()来自基类,基类在每次委托回报到达时更新持仓,策略自身不要维护整数持仓。这段代码覆盖了策略研发里最常见的三个动作:维护滑窗、计算指标、按条件下单。

回测启动采用构建器模式,把数据源、区间、资金和费率一次性传给引擎:

BacktestEngine engine = BacktestEngine.builder() .barSource(new CsvBarSource("data/rb2510_5m.csv")) .start(LocalDate.of(2023, 1, 1)) .end(LocalDate.of(2024, 12, 31)) .initialCapital(1_000_000) .commissionRate(0.00015) // 单边万分之1.5 .slippageTicks(1) // 单边1个最小变动价位 .marginRate(0.15) // 保证金比例15% .strategy(new DualMaStrategy("rb2510", 20, 60)) .build(); engine.run(); engine.generateReport().forEach(System.out::println);

新手最容易漏掉的是结束日期和滑点tick数。start/end至少要覆盖一个完整的多空周期,滑点从1个tick起步,不要设0:

参数含义推荐取值方式
start/end回测起止日期至少覆盖一个完整牛熊周期
initialCapital初始资金按实盘账户金额填
commissionRate手续费率从券商结算单取真实值
slippageTicks滑点tick数高频按2,趋势按1
marginRate保证金率按品种实际水平12%-15%

3.3 回测报告怎么看:先看回撤,再看收益

回测引擎输出一堆指标后,不要只盯年化收益。建议按这个顺序检查:先看最大回撤和回撤持续时长,再看手续费占毛利润的比例,最后看盈亏比与胜率。比如收益回撤比1.5、胜率38%但盈亏比3.5的信号,说明策略靠少数几笔大盈利扛住了大量小止损,这种策略对滑点极其敏感,实盘前要把滑点参数提到2个tick重新跑一遍。

指标含义参考标准
最大回撤净值从峰值回落的幅度控制在20%以内较好
收益回撤比年化收益/最大回撤大于2算及格
夏普比率单位波动率的超额回报大于1.5可上线
手续费占毛利润比例交易成本消耗超过30%必须降频

3.4 历史回放模式:把某一天的分时重新播放出来

除了区间回测,单日历史回放是排查信号异常的主力功能。指定合约和日期,引擎把那一天从开盘到收盘的行情按设定倍速重新播放,某根K线触发开仓信号时,暂停下来看当时的指标值和盘口,比事后翻日志直观得多:

replay: symbol: rb2510 date: 2024-05-06 speed: 20x startTime: 21:00:00

20倍速大约三分钟放完一天,足够看清信号与K线形态的对应关系。如果发现信号比预期晚一根K线,优先检查均线窗口是否把当前K线排除在外,这是从脚本平台迁移过来的第一个高频坑。

4. 模拟交易与实盘交易接入:全自动与半自动的双模式设计

4.1 交易通道接口:下单、撤单、查询三件套

策略在回测里跑出正收益后,下一步是搬到模拟盘。模拟盘与实盘共享同一份策略代码,差异只在通道实现。接口定义得越薄,切换成本越低,一般只需要三个方法:

public interface ExecutionGateway { String submit(OrderRequest request) throws OrderRejectedException; void cancel(String orderId); OrderStatus query(String orderId); PositionSnapshot position(String symbol); }

submit返回委托编号,撤单和查询都用它定位委托。模拟盘实现内部用行情快照按对手价撮合,基本立刻成交;实盘实现对接期货公司柜台,行情与交易是两条独立链路,委托回报通过单独的线程异步推给策略。回测、模拟、实盘三个环境共用接口,策略代码不需要感知环境差异。

4.2 全自动模式:信号与下单之间必须有一道风控闸门

全自动的难点不是下单,而是防呆。常见做法是在信号处理与通道之间加一个风控组件,所有订单先过闸门再发出:

public class RiskGate { private final RiskConfig config; private final double maxDailyLossRatio; private final double initialCapital; private final AtomicBoolean paused = new AtomicBoolean(false); public RiskGate(RiskConfig config, double maxDailyLossRatio, double initialCapital) { this.config = config; this.maxDailyLossRatio = maxDailyLossRatio; this.initialCapital = initialCapital; } public boolean allow(OrderRequest request, double todayPnl) { if (paused.get()) return false; if (todayPnl <= -maxDailyLossRatio * initialCapital) { paused.set(true); // 触发当日熔断 return false; } if (request.getQuantity() > config.getMaxOrderQuantity()) return false; return true; } }

allow()先查暂停开关,再判断当日累计亏损,最后限制单笔手数。一旦触发最大亏损就熔断当天所有订单,而不是只拦当前这一单。风控参数从配置读,不要在代码里写死。模拟盘阶段建议先按下面一档跑两周,确认没有误触发再放宽:

参数建议初值说明
maxDailyLossRatio5%当日亏损到达初始资金的5%即暂停
maxOrderQuantity10手单笔最大下单量
maxOpenPositions5个品种同时持有的品种数上限
pauseMinutes60分钟熔断后恢复到可下单状态的最短间隔

风控参数在模拟盘和实盘必须一致,否则模拟盘风控正常、实盘风控失控的情况很容易发生。任何风控触达都要有告警通知,代码里不能悄悄吞掉异常。

4.3 半自动模式:信号进入待确认队列,人工确认后再下单

半自动解决的是“模型给出信号但交易员保留最终决定权”的场景。信号不直接进通道,而先进待确认队列,交易员在界面上看到信号摘要和K线快照后再决定是否提交。这里最大的坑是重复确认,队列必须用原子删除实现:

public class ConfirmQueue { private final ConcurrentHashMap<String, Signal> pending = new ConcurrentHashMap<>(); private final ExecutionGateway gateway; public ConfirmQueue(ExecutionGateway gateway) { this.gateway = gateway; } public void push(Signal signal) { pending.putIfAbsent(signal.getId(), signal); } public boolean confirm(String signalId) { Signal signal = pending.remove(signalId); if (signal == null) { return false; // 重复确认或信号已被撤销 } gateway.submit(signal.toOrderRequest()); return true; } public void dismiss(String signalId) { pending.remove(signalId); } }

confirm()用ConcurrentHashMap.remove原子取出信号,两个线程同时点击确认只有一个能拿到对象并下单,另一个拿到null直接返回,从根上避免重复加仓位。半自动模式对信号对象的完整性要求比全自动更高,信号里要带触发时间、当时的K线快照和指标读数,交易员才能在几秒内做判断。

4.4 模拟盘转到实盘的切换清单

模拟盘跑通只是第一步。模拟与实盘有四个容易被忽略的差异,而且每一个都会让策略表现明显变差。实盘有排队、拒单和部分成交,策略代码要能处理OrderStatus里所有非预期状态;实盘存在集合竞价和盘中休息,模拟盘的连续撮合会让策略在错误时段反复下单;手续费和保证金要从真实结算单读取,不能用模拟盘假定的费率;涨跌停附近的委托,模拟盘可能照常撮合,实盘柜台会直接拒单。

差异点模拟盘实盘
成交速度立即按对手价有排队和部分成交
交易时段可能连续撮合遵循交易所作息
手续费/保证金手动配置按真实结算单
涨跌停委托可能照常成交直接拒单

切换顺序建议是:模拟盘跑满两周且无风控告警,拿一手小仓位实盘跑一周,每天把策略报表与结算单对一遍,重点核对委托编号、成交价格、手续费三个字段。三者一致后再逐步放大仓位,不要一次性全量上线。

5. 指标口径、手数换算与AI辅助:从旧平台迁移的适配技巧

5.1 指标口径对齐:慢一根K线就是另一套策略

从文华、MC、金字塔迁移策略,最隐蔽的差异是指标的最新值是否包含当前K线。在JAVA平台的事件驱动模型里,onBar收到的Bar就是当前正在形成的这一根,窗口计算要包含它;如果沿旧平台习惯把当前K线排除在窗口外,所有信号会整体滞后一根K线,回测曲线完全变样。迁移后的第一件事不是跑全量回测,而是选连续三个交易日做逐K线回放,把旧平台与JAVA平台的信号发生时间逐笔对照。只有每个信号都对得上,策略才算真正迁移成功。

5.2 手数与价格精度:一张映射表解决品种换算

手数单位也要重新核对。商品期货螺纹钢一手是10吨,平台内部持仓按张计算时,显示数字会和旧平台的“手”差一个合约乘数。价格精度方面,螺纹钢最小变动价位是1元,原油是0.1元,委托价格如果比tick还细会被直接拒单。迁移时建立一张品种映射表,把旧平台代码、新平台代码、合约乘数、价格精度、交易时段放在同一张表里,每次要换合约先查表,而不是靠记忆。

5.3 用AI大模型辅助生成策略骨架:给模型的约束条件

AI编程工具能省掉大量重复的Java代码,但生成的策略要能挂进现有事件循环,必须在提示词里把约束写死。下面这套提示词模板可以直接改参数复用:

请生成Java类,继承BaseStrategy,实现onBarForSymbol回调。 策略规则:5分钟K线,收盘价上穿布林带中轨且RSI(14)低于70时开多; 止损设入场后最近20根K线最低价下方1个tick; 盈利目标为2倍ATR(14)。 约束:不使用第三方指标库,指标全部手写; 参数从构造函数传入;下单只能调用buy/sell方法; 处理窗口不满时的边界情况,不能出现空指针。

“参数从构造函数传入”保证策略可配置,“不能出现空指针”保证接入回测引擎不会在跑了一整天后崩掉。AI生成后先用第3章的最小回测工程验证编译和持仓净值,再把收益率曲线与手工迁移版本对比。十分钟内筛掉九成不可靠的生成代码,剩下那成再花半天做逐K线比对,迁移效率能提高不少。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询