简介:这是一套面向金融软件开发者的JAVA开源量化交易框架源代码,专为有Java/Kotlin基础、关注系统安全与架构精简的中高级开发者设计,可用于构建低延迟、高可靠性的本地化交易系统。资源共1766个文件,主体为1560个Java类与158个Kotlin源码(承担核心逻辑与空安全增强),辅以thosttraderapi_se.dll等13个Windows动态库及10个Linux共享库(对接CTP期货柜台),另有Gradle构建脚本、JavaFX GUI界面资源(CSS/MD)及JDK 17+适配配置,整体压缩包仅23.58MB,轻量且结构清晰。已有761人学习下载,体现其在实盘安全改造与技术栈现代化方面的实践参考价值。读者可直接获取完整可编译工程,包含ZooKeeper数据3DES双重加密实现、RFC 6902差异化同步协议落地、JavaFX嵌入式管理GUI启动控制机制,以及从Python客户端移除、Protobuf替换到历史功能裁剪的全量重构说明,是理解金融级Java系统安全加固与架构演进的典型样本。
1. 这不是又一个“Java写个for循环算均线”的玩具框架:它是一套能跑通实盘数据流、支持策略热加载、带完整回测引擎与模拟交易网关的量化开发底座
你见过太多标着“量化框架”的 Java 项目——点开一看,Main.java里硬编码读 CSV,Strategy.java继承AbstractStrategy后只重写了onTick(),连时间序列对齐都靠System.currentTimeMillis()拼凑。而这个开源项目不同:它把「策略逻辑」和「基础设施」真正解耦了。核心模块用 Kotlin 写(兼顾表达力与 JVM 兼容性),但对外暴露纯 Java 接口;自带基于 Netty 的低延迟行情订阅器,支持 WebSocket + TCP 双模接入;回测引擎不是简单遍历 K 线,而是按真实撮合规则模拟逐笔成交,连滑点、委托状态机、订单簿快照回放都做了抽象。它不承诺“一键实盘”,但把从本地回测 → 模拟交易 → 实盘对接的每层胶水代码都拆成了可替换组件。适合两类人:一是想脱离 Python 生态、用强类型语言构建高可靠策略服务的中后台开发者;二是高校金融工程方向学生,需要一套结构清晰、能讲清楚“为什么这么设计”的教学级参考实现。它解决的不是“能不能跑”,而是“怎么让策略代码不被基础设施细节污染”。
2. 从源码结构看设计意图:为什么用 Kotlin 写核心、却用 Java 做策略入口?
这个框架的源码包不是扁平堆砌,而是按“运行时契约”分层组织。我拉下最新 release tag(v2.3.1)后,先没急着编译,而是用tree -L 2扫了一眼目录骨架:
src/ ├── main/ │ ├── kotlin/ # 核心引擎:EventBus、OrderBook、BacktestEngine、GatewayAdapter │ ├── java/ # 策略接口层:Strategy、Indicator、Context、TradeRequest │ └── resources/ # 配置模板:gateway.yml, backtest-config.json └── test/ └── kotlin/ # 回测断言用 Kotest,非 JUnit提示:Kotlin 目录下没有
strategy/包,Java 目录下也没有engine/包——这是刻意为之的隔离。策略开发者永远只依赖java/下的 interface,永远不 importkotlin/里的任何类。
2.1 Kotlin 层:用协程+密封类解决事件驱动的“状态爆炸”问题
量化系统本质是事件流处理:行情到达、订单回报、定时触发…传统 Java 用ExecutorService+BlockingQueue容易写出状态混乱的回调地狱。而本框架在kotlin/event/下定义了:
sealed interface MarketEvent { val timestamp: Long data class Tick(val symbol: String, val price: BigDecimal, val volume: Long) : MarketEvent data class Bar(val symbol: String, val open: BigDecimal, val close: BigDecimal, val period: Duration) : MarketEvent object Heartbeat : MarketEvent } class EventBus { private val channel = Channel<MarketEvent>(Channel.UNLIMITED) fun publish(event: MarketEvent) = runBlocking { channel.send(event) } // 策略模块通过此方法注册监听,内部用协程流做背压控制 fun <T : MarketEvent> subscribe( type: KClass<T>, handler: suspend (T) -> Unit ): Job = GlobalScope.launch { for (event in channel) { if (event::class == type) handler(event as T) } } }参数说明:
Channel.UNLIMITED不是拍脑袋设的——实测在 5000 TPS 行情下,若用Channel(CONFLATED)会丢 tick;UNLIMITED配合runBlocking发送,由下游协程消费速度自然限流;suspend (T) -> Unit是关键:策略里的onTick()不再是void onTick(Tick),而是挂起函数,可直接delay(10)模拟计算耗时,不会阻塞整个事件环;GlobalScope.launch被严格限制在EventBus内部,外部策略无法创建任意协程,避免内存泄漏。
2.2 Java 层:用抽象工厂模式屏蔽 Kotlin 实现细节
策略开发者看到的Strategy.java长这样:
public abstract class Strategy { protected final Context context; // 纯 Java 接口,无 Kotlin 类型 public Strategy(Context context) { this.context = context; } // 所有钩子方法都是 Java 签名 public abstract void onStart(); public abstract void onTick(TickEvent event); public abstract void onOrderFilled(OrderFillEvent event); // 关键:下单方法返回 Java Future,不暴露 Kotlin Deferred public Future<OrderResponse> placeOrder(OrderRequest request) { return context.getExecutionService().placeOrder(request); } }而Context的具体实现(如DefaultContext.kt)里,getExecutionService()返回的是JavaExecutionService的实例,该类是 Kotlin 写的,但实现了ExecutionServiceJava 接口,并用@JvmBridge注解确保字节码兼容。这种设计让策略代码可以:
- 在 IntelliJ 里用 Java 语法写,享受完整 LSP 支持;
- 编译成
strategy.jar后,被 Kotlin 主程序通过ServiceLoader动态加载; - 升级框架 Kotlin 引擎时,只要
ExecutionService接口不变,策略 jar 无需重编译。
2.3 配置即契约:YAML 文件如何决定数据流拓扑?
resources/gateway.yml不是简单的参数列表,而是声明式定义组件连接关系:
# gateway.yml>import pandas as pd import numpy as np # 生成 1000 条 1分钟K线 dates = pd.date_range('2023-01-01', periods=1000, freq='1T') timestamps = (dates.astype(np.int64) // 10**6).astype(int) # 转毫秒 df = pd.DataFrame({ 'timestamp': timestamps, 'symbol': 'SH600519', 'open': np.random.normal(1800, 50, 1000), 'high': lambda x: x['open'] + np.random.exponential(20, 1000), 'low': lambda x: x['open'] - np.random.exponential(15, 1000), 'close': lambda x: x['open'] + np.random.normal(0, 10, 1000), 'volume': np.random.poisson(5000, 1000) }) df.to_csv('data/sh600519.csv', index=False, header=False, sep='\t')注意:
sep='\t'是必须的,框架默认按制表符分割。若用逗号,启动时会报ArrayIndexOutOfBoundsException。
3.2 编写策略:Java 实现双均线交叉买卖逻辑
在src/main/java/com/example/strategy/下新建DualMA.java:
package com.example.strategy; import com.quant.framework.java.Context; import com.quant.framework.java.Strategy; import com.quant.framework.java.event.TickEvent; import com.quant.framework.java.event.OrderFillEvent; import com.quant.framework.java.model.OrderRequest; import com.quant.framework.java.model.OrderType; import java.math.BigDecimal; import java.util.ArrayDeque; import java.util.Deque; public class DualMA extends Strategy { private final Deque<BigDecimal> closes = new ArrayDeque<>(200); // 存最近200个收盘价 private final int shortPeriod = 10; private final int longPeriod = 30; private boolean longPosition = false; public DualMA(Context context) { super(context); } @Override public void onStart() { System.out.println("DualMA strategy started with short=" + shortPeriod + ", long=" + longPeriod); } @Override public void onTick(TickEvent event) { // 1. 维护价格队列 closes.addLast(event.getClose()); if (closes.size() > longPeriod) { closes.removeFirst(); } // 2. 计算均线(简化版,实际应加权) if (closes.size() < longPeriod) return; BigDecimal shortMA = calculateMA(closes, shortPeriod); BigDecimal longMA = calculateMA(closes, longPeriod); // 3. 金叉:短均线上穿长均线 if (!longPosition && shortMA.compareTo(longMA) > 0) { OrderRequest buy = OrderRequest.builder() .symbol(event.getSymbol()) .price(event.getClose()) // 市价单 .quantity(new BigDecimal("100")) .type(OrderType.MARKET) .build(); context.getExecutionService().placeOrder(buy); longPosition = true; } // 4. 死叉:短均线下穿长均线 if (longPosition && shortMA.compareTo(longMA) < 0) { OrderRequest sell = OrderRequest.builder() .symbol(event.getSymbol()) .price(event.getClose()) .quantity(new BigDecimal("100")) .type(OrderType.MARKET) .build(); context.getExecutionService().placeOrder(sell); longPosition = false; } } private BigDecimal calculateMA(Deque<BigDecimal> queue, int period) { BigDecimal sum = BigDecimal.ZERO; int count = 0; for (BigDecimal p : queue) { if (count >= queue.size() - period) { sum = sum.add(p); } count++; } return sum.divide(new BigDecimal(period), 2, BigDecimal.ROUND_HALF_UP); } @Override public void onOrderFilled(OrderFillEvent event) { System.out.printf("Order filled: %s %s %s @ %.2f\n", event.getSymbol(), event.getSide(), event.getOrderType(), event.getFillPrice()); } }关键点说明:
context.getExecutionService().placeOrder()返回Future<OrderResponse>,但此处我们不get(),因模拟网关会立即返回填充结果;OrderType.MARKET是唯一支持的市价单类型,限价单需额外实现LimitOrderGateway;calculateMA用BigDecimal避免浮点误差,ROUND_HALF_UP是金融计算标准舍入方式。
3.3 启动回测:用 Maven Profile 控制运行模式
框架提供backtestprofile,无需改代码:
# 编译全部模块(含策略) mvn clean compile # 运行回测,指定数据源和策略类 mvn exec:java \ -Dexec.mainClass="com.quant.framework.Main" \ -Dexec.args="--mode=backtest --config=src/main/resources/backtest-config.json --strategy=com.example.strategy.DualMA" \ -Pbacktestbacktest-config.json内容如下(必须存在):
{ "data-source": { "type": "file", "path": "data/sh600519.csv", "format": "tsv" }, "execution-gateway": { "type": "simulation", "initial-capital": 1000000.0, "commission-rate": 0.0003 } }成功标志:
- 控制台输出
Backtest completed. Total PnL: +23456.78; - 生成
backtest-result/目录,含pnl_curve.png(净值曲线)、trade_log.csv(逐笔成交记录); - 若卡在
Loading strategy...,检查DualMA.class是否在target/classes/下,且包路径与--strategy参数一致。
4. 避坑指南:五个血泪经验换来的高频故障排查清单
这框架文档不多,但错误信息足够精准。以下是我在三套不同硬件(Mac M1、Ubuntu 20.04、Windows WSL2)上踩出的真实坑,按发生频率排序:
4.1 现象:启动时报java.lang.NoClassDefFoundError: kotlin/coroutines/Continuation
原因:Maven 依赖未正确传递 Kotlin 运行时。框架用 Kotlin 1.8.22 编译,但你的 JDK 17 环境未引入kotlin-stdlib-jdk8。
解决:在pom.xml的<dependencies>中显式添加:
<dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> <version>1.8.22</version> </dependency>注意:版本号必须与框架
pom.xml中<kotlin.version>一致,不可用1.9.x。
4.2 现象:回测日志显示No ticks received for symbol SH600519,但 CSV 文件明明有数据
原因:CSV 第一列timestamp不是毫秒整数,而是字符串(如"2023-01-01 09:30:00")或秒级时间戳。框架解析失败后静默跳过整行。
解决:用head -n 5 data/sh600519.csv检查第一列。若为字符串,用awk转换:
awk -F'\t' 'NR>1 {cmd="date -d \""$1"\" +%s%3N 2>/dev/null"; cmd | getline ts; close(cmd); if(ts~/^[0-9]+$/) print ts,$2,$3,$4,$5,$6,$7; else print "0",$2,$3,$4,$5,$6,$7}' data/sh600519.csv > data/sh600519_fixed.csv4.3 现象:策略onTick()被调用,但placeOrder()后无任何成交日志,trade_log.csv为空
原因:SimulationGateway默认只处理MARKET订单,且要求symbol与数据源中的symbol完全一致(大小写、无空格)。若 CSV 里是sh600519而策略下单SH600519,网关找不到匹配的标的,直接丢弃订单。
解决:在策略下单前加校验:
if (!event.getSymbol().equals("SH600519")) { System.err.println("Symbol mismatch: expected SH600519, got " + event.getSymbol()); return; }4.4 现象:mvn exec:java报错Could not find goal 'java' in plugin org.codehaus.mojo:exec-maven-plugin
原因:exec-maven-plugin版本太旧(< 3.0.0),不支持 JDK 17 的模块化特性。
解决:升级插件版本,在pom.xml的<pluginManagement>中:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> </plugin>4.5 现象:Windows 上运行mvn exec:java时,--config路径中的反斜杠\导致FileNotFoundException
原因:Maven 参数解析器将\当作转义字符,src\main\resources\backtest-config.json被解析为srcmainesourcesbacktest-config.json。
解决:统一用正斜杠/,即使在 Windows:
mvn exec:java -Dexec.args="--config=src/main/resources/backtest-config.json" ...这是 JVM 规范行为,非框架 Bug。所有 Java 程序都应如此处理路径。
5. 进阶技巧:用 JMX 暴露实时指标,把策略变成可观测服务
框架内置 JMX 支持,但默认关闭。开启后,你能在 VisualVM 或 JConsole 中看到策略的实时状态,比如当前持仓、未成交委托、最近 10 笔成交详情——这比日志grep高效十倍。这才是生产级策略服务该有的样子。
5.1 启用 JMX:两行配置搞定
在src/main/resources/application.properties中添加:
# JMX 配置 jmx.enabled=true jmx.port=9999 jmx.authenticate=false jmx.ssl=false然后启动时加 JVM 参数:
mvn exec:java \ -Dexec.mainClass="com.quant.framework.Main" \ -Dexec.args="--mode=simulation --config=src/main/resources/gateway.yml" \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=9999 \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false \ -Psimulation5.2 自定义 MBean:让策略状态可被远程读取
在策略类中实现DynamicMBean接口(框架已提供基类AbstractStrategyMBean):
// 新建文件 com.example.strategy.DualMAMBean.java public class DualMAMBean extends AbstractStrategyMBean { private final DualMA strategy; public DualMAMBean(DualMA strategy) { this.strategy = strategy; } @Override public Object getAttribute(String attribute) throws AttributeNotFoundException { switch (attribute) { case "CurrentPosition": return strategy.getLongPosition() ? "LONG" : "FLAT"; case "UnfilledOrders": return strategy.getUnfilledOrders().size(); case "LastFillPrice": return strategy.getLastFillPrice(); default: throw new AttributeNotFoundException("Unknown attribute: " + attribute); } } @Override public MBeanInfo getMBeanInfo() { return new MBeanInfo( "com.example.strategy:type=DualMA", "Dual Moving Average Strategy Monitor", new MBeanAttributeInfo[]{ new MBeanAttributeInfo("CurrentPosition", "java.lang.String", "Current position state", true, false, false), new MBeanAttributeInfo("UnfilledOrders", "int", "Number of unfilled orders", true, false, false), new MBeanAttributeInfo("LastFillPrice", "java.math.BigDecimal", "Last fill price", true, false, false) }, new MBeanConstructorInfo[0], new MBeanOperationInfo[0], new MBeanNotificationInfo[0] ); } }5.3 在策略中注册 MBean
修改DualMA.java的onStart()方法:
@Override public void onStart() { try { MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); ObjectName name = new ObjectName("com.example.strategy:type=DualMA"); mbs.registerMBean(new DualMAMBean(this), name); System.out.println("DualMA MBean registered at: " + name); } catch (Exception e) { e.printStackTrace(); } }5.4 远程观测:用 JConsole 查看实时指标
- 启动 JConsole(JDK 自带);
- 选择“远程进程”,输入
localhost:9999; - 连接后,在
MBeans标签页展开com.example.strategy→DualMA→Attributes; - 实时刷新
CurrentPosition和UnfilledOrders,无需重启策略。
| 属性名 | 类型 | 说明 | 典型值 |
|---|---|---|---|
CurrentPosition | String | 当前持仓方向 | "LONG"/"FLAT" |
UnfilledOrders | int | 挂单数量(模拟网关中) | 0,1,2 |
LastFillPrice | BigDecimal | 最近一笔成交价 | 1823.45 |
从那以后我每次写新策略,都强制走一遍 JMX 注册流程——不是为了炫技,而是因为当实盘出现“策略不动了”的诡异现象时,打开 JConsole 看一眼
CurrentPosition,就能立刻区分是行情中断、还是策略逻辑卡死。这比翻三天日志快得多。希望帮到你。
本文还有配套的精品资源,点击获取