炒股的朋友都懂一个道理:看盘先看势,顺势而为才能少踩坑。这里的“势”,核心就是大盘指数的动态——上证指数定方向,深证成指看成长,创业板指察情绪,沪深300判权重。但市面上的行情软件要么广告多,要么数据延迟,想实时掌握核心指数动态总差口气。
本文作为Java量化系列的第十篇,将带大家打造专属大盘指数实时监控神器:核心是工作日交易时段(9:00-11:00、13:00-14:00)每10秒触发一次抓取(Cron表达式精准控制),同步东方财富/财联社双数据源的指数数据,实时缓存到Redis,让你毫秒级获取最新价、涨跌幅等关键信息,为交易决策筑牢数据基础!
一、为什么要做专属指数实时监控?3大痛点直击股民刚需
可能有朋友会问:“市面上行情软件这么多,为啥还要自己造轮子?” 其实做专属监控,正是为了解决商用软件的3大核心痛点:
- 数据延迟高:免费行情软件普遍有3-15秒延迟,短线交易中,这几秒可能就是盈利与亏损的差距;
- 广告干扰多:打开软件全是开户、投顾广告,想快速看个指数还要层层跳转;
- 定制化不足:无法根据自己的交易策略自定义指数预警(比如上证指数跌破3900触发提醒),也不能对接自己的量化系统。
而我们打造的这套监控系统,不仅能实现10秒级实时抓取,还能无缝对接前九篇搭建的量化框架,后续可轻松扩展指数预警、策略触发等高级功能,真正做到“数据为自己所用”。
二、核心需求与技术选型:稳定、高效、低延迟
2.1 核心需求拆解
- 定时抓取:工作日9:00-11:00、13:00-14:00时段,每10秒抓取一次核心指数数据(Cron:1/10 * 9,10,11,13,14 ? * 1-5);
- 多源保障:支持东方财富、财联社双数据源抓取,避免单一数据源故障导致监控中断;
- 数据完整:获取上证指数、深证成指、创业板指、沪深300等核心指数的最新价、涨跌幅、涨跌额等关键信息;
- 实时缓存:抓取的数据实时存入Redis,保证后续查询、使用的毫秒级响应;
- 异常兼容:单个数据源抓取失败时不影响整体功能,自动返回空列表避免程序崩溃。
2.2 核心技术选型
结合需求特点,选型聚焦“高效、稳定、低延迟”三大核心:
- 定时调度:Spring Scheduler,轻量高效,支持精准Cron表达式配置,无需额外部署中间件;
- 数据抓取:HttpClient,自定义请求头模拟浏览器访问,避免被目标网站拦截;
- 数据解析:FastJSON,高效解析JSON格式的响应数据,适配东方财富、财联社的返回结构;
- 缓存存储:Redis,内存级存储,支持key-value快速读写,完美适配实时数据的存储需求;
- 多源切换:通过接口封装实现数据源动态切换,东方财富为主、财联社为备,提升系统稳定性。
三、核心实现(一):定时调度与主流程设计
整个系统的核心入口是定时任务,通过精准的Cron配置控制抓取时机,再串联“数据源选择→数据抓取→数据筛选→缓存存储”全流程。
3.1 Cron表达式解析
本次使用的Cron表达式:1/10 * 9,10,11,13,14 ? * 1-5,逐位拆解核心逻辑:
- 1/10:从第1秒开始,每10秒执行一次(比如1秒、11秒、21秒…触发);
- *:任意分钟都执行;
- 9,10,11,13,14:仅在9点、10点、11点、13点、14点这几个小时执行;
- ?:日位不指定(因星期位已限定,避免冲突);
- *:任意月份都执行;
- 1-5:仅周一到周五(工作日)执行,避开周末休市。
3.2 主流程核心代码
主流程方法updateStockIndexPrice负责串联全流程,支持通过type参数筛选特定类型的指数(如仅抓取上证指数),灵活适配不同使用场景:
/**
* 定时更新股票指数价格(核心主流程)
* @param type 指数类型:1=上证,2=沪深,3=创业板,4=沪深300(null则抓取全部)
*/
@Override
public void updateStockIndexPrice(Integer type) {
// 1. 选择数据源抓取指数数据(优先东方财富,可按需切换财联社)
List<StockIndexInfo> stockIndexList = extCrawlerService.findStockIndex();
// 备用:财联社数据源(单一数据源故障时启用)
// List<StockIndexInfo> stockIndexList = crawlerDrjService.findStockIndex();
// 2. 按类型筛选指数(如仅需上证指数,传入type=1)
if (type != null) {
stockIndexList = stockIndexList.stream()
.filter(index -> type.equals(index.getType()))
.collect(Collectors.toList());
}
// 3. 数据为空则直接返回,避免无效操作
if (CollUtil.isEmpty(stockIndexList)) {
log.info("本次抓取未获取到有效指数数据");
return;
}
// 4. 遍历数据,存入Redis缓存
for (StockIndexInfo stockIndexInfo : stockIndexList) {
stockCacheService.updateStockIndex(stockIndexInfo);
}
log.info("指数数据更新完成,共更新{}条数据", stockIndexList.size());
}
123456789101112131415161718192021222324252627282930
四、核心实现(二):多数据源抓取与数据解析
数据抓取是系统的核心能力,我们分别实现东方财富和财联社的抓取逻辑,通过统一接口返回数据,保证后续流程的兼容性。
4.1 东方财富数据源(主力推荐)
东方财富的指数接口返回数据完整、稳定性高,是我们的主力数据源。核心逻辑是“构造请求URL→发送GET请求→解析JSON响应→封装实体类”。
4.1.1 抓取核心代码
/**
* 东方财富指数数据抓取
* @return 核心指数列表
*/
@Override
public List<StockIndexInfo> findStockIndex() {
// 1. 构造请求URL(通过配置文件注入基础URL,动态拼接回调参数)
String url = "https://push2.eastmoney.com/api/qt/clist/get?pi=0&pz=100&po=1&np=1&fields=f1,f2,f3,f4,f12,f13,f14&fltt=2&invt=2&ut=433fd2d0e98eaf36ad3d5001f088614d&fs=i:1.000001,i:0.399001,i:0.399006,i:1.000300&cb=jQuery112405795797323925824_1676954612820&_=";
try {
// 2. 发送GET请求(自定义请求头,模拟浏览器访问)
String content = HttpUtil.sendGet(
HttpClientConfig.proxyNoUseCloseableHttpClient(),
url + MyDateUtil.getTimezone(), // 拼接时间戳,避免缓存
buildDfHeaderMap() // 构造东方财富专属请求头
);
// 3. 清洗响应数据(去除jQuery回调包裹,保留纯JSON)
content = content.substring("jQuery112405795797323925824_1676954612820".length() + 1);
content = content.substring(0, content.length() - 2);
// 4. 解析JSON数据,封装为StockIndexInfo列表
return stockInfoParser.parseStockIndex(content);
} catch (Exception e) {
log.error("东方财富指数抓取失败,URL:{}", url, e);
return Collections.emptyList();
}
}
123456789101112131415161718192021222324252627
4.1.2 响应数据解析
东方财富的响应是jQuery回调包裹的JSON,解析时需先清洗数据,再提取data.diff中的指数列表,核心解析代码如下:
/**
* 解析东方财富指数JSON数据
* @param content 清洗后的纯JSON字符串
* @return 封装后的StockIndexInfo列表
*/
@Override
public List<StockIndexInfo> parseStockIndex(String content) {
// 1. 解析为JSON对象
JSONObject jsonObject = JSONObject.parseObject(content);
JSONObject data = jsonObject.getJSONObject("data");
if (ObjectUtils.isEmpty(data)) {
return Collections.emptyList();
}
// 2. 获取指数列表数组(data.diff)
JSONArray jsonArray = data.getJSONArray("diff");
if (jsonArray.size() <= 0) {
return Collections.emptyList();
}
// 3. 遍历数组,封装为StockIndexInfo
List<StockIndexInfo> result = new ArrayList<>(6);
jsonArray.forEach(n -> {
JSONObject tempObject = JSONObject.parseObject(n.toString());
StockIndexInfo indexInfo = new StockIndexInfo();
indexInfo.setCode(tempObject.getString("f12")); // 指数编码(如000001)
indexInfo.setName(tempObject.getString("f14")); // 指数名称(如上证指数)
indexInfo.setNowPrice(tempObject.getString("f2")); // 最新价
indexInfo.setNowProportion(tempObject.getString("f3") + "%"); // 涨跌幅(加%符号)
indexInfo.setSubPrice(tempObject.getString("f4")); // 涨跌额
indexInfo.setType(convertTypeByCode(indexInfo.getCode())); // 转换为统一类型(1-4)
result.add(indexInfo);
});
return result;
}
1234567891011121314151617181920212223242526272829303132333435
4.1.3 东方财富响应示例
清洗前的响应数据(包含jQuery回调):
jQuery112405795797323925824_1676954612820({"rc":0,"rt":6,"svr":183124518,"lt":1,"full":1,"dlmkts":"","data":{"total":4,"diff":[{"f1":2,"f2":3917.36,"f3":0.69,"f4":26.91,"f12":"000001","f13":1,"f14":"上证指数"},{"f1":2,"f2":13332.73,"f3":1.47,"f4":192.52,"f12":"399001","f13":0,"f14":"深证成指"},{"f1":2,"f2":3191.98,"f3":2.23,"f4":69.74,"f12":"399006","f13":0,"f14":"创业板指"},{"f1":2,"f2":4611.62,"f3":0.95,"f4":43.44,"f12":"000300","f13":1,"f14":"沪深300"}]}});
1
可以看到,响应中包含4个核心指数的完整数据,字段f2(最新价)、f3(涨跌幅)、f4(涨跌额)正是我们需要的核心信息。
4.2 财联社数据源(备用)
财联社数据源作为备用,其接口返回格式更简洁,无需清洗直接解析即可。核心优势是反爬机制宽松,适合作为东方财富故障时的备用方案:
/**
* 财联社指数数据抓取(备用数据源)
* @return 核心指数列表
*/
@Override
public List<StockIndexInfo> findStockIndex() {
try {
// 财联社指数接口(固定参数,包含上证、深证、创业板等核心指数)
String url = "https://x-quote.cls.cn/v2/quote/a/web/stocks/basic?app=CailianpressWeb&fields=secu_name,secu_code,trade_status,change,change_px," +
"last_px&os=web&secu_codes=sh000001,sz399001,sh000905,sz399006&sv=8.4.6&sign=7ddfd2eef7564087ff01a1782c724f43";
// 发送GET请求(自定义财联社请求头)
String content = HttpUtil.sendGet(
HttpClientConfig.proxyNoUseCloseableHttpClient(),
url,
buildDjrHeaderMap()
);
// 解析JSON数据
JSONObject jsonObject = JSONUtil.parseObj(content);
JSONObject data = jsonObject.getJSONObject("data");
Set<String> indexCodes = data.keySet();
List<StockIndexInfo> indexInfos = new ArrayList<>();
// 遍历指数编码,封装实体类
for (String code : indexCodes) {
JSONObject indexObj = data.getJSONObject(code);
StockIndexInfo indexInfo = new StockIndexInfo();
indexInfo.setCode(indexObj.getStr("secu_code")); // 指数编码(如sh000001)
indexInfo.setName(indexObj.getStr("secu_name")); // 指数名称
indexInfo.setNowPrice(indexObj.getStr("last_px")); // 最新价
// 涨跌幅:将小数转换为百分比(如0.0069→0.69%)
indexInfo.setNowProportion(BigDecimalUtil.mul100(indexObj.getBigDecimal("change")));
indexInfo.setSubPrice(indexObj.getStr("change_px")); // 涨跌额
indexInfos.add(indexInfo);
}
return indexInfos;
} catch (Exception e) {
log.error("财联社指数抓取失败", e);
return new ArrayList<>();
}
}
private Map<String,String> buildDjrHeaderMap() {
Map<String,String> headerMap = new HashMap<>(4);
headerMap.put("Host","x-quote.cls.cn");
headerMap.put("Origin","https://www.cls.cn");
headerMap.put("Referer","https://www.cls.cn/");
return headerMap;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
五、核心实现(三):Redis实时缓存与数据实体设计
抓取到的指数数据需要实时存入Redis,保证后续查询的高效性;同时通过统一的StockIndexInfo实体类,规范不同数据源的返回格式。
5.1 数据实体设计:StockIndexInfo
实体类封装了指数的核心信息,支持不同数据源的统一适配,同时通过getDate()方法自动获取当前时间,无需手动设置:
@Data
public class StockIndexInfo implements Serializable {
private String code; // 指数编码(如000001、399001)
private String name; // 指数名称(如上证指数、深证成指)
private String nowPrice; // 最新价
private String nowProportion; // 涨幅度(带%符号)
private String subPrice; // 涨跌额
private String openPrice; // 开盘价
private String highPrice; // 最高价
private String lowPrice; // 最低价
private String yesClosePrice; // 昨日收盘价
private String change; // 涨跌比例(小数)
private Integer type; // 指数类型:1=上证,2=沪深,3=创业板,4=沪深300
private String date; // 数据更新时间
private Integer limitUp; // 涨停状态
private Integer limitDown; // 跌停状态
private Integer limitLevel; // 涨跌停级别
private String tradingValue; // 成交额
// 自动获取当前时间作为更新时间
public String getDate() {
return DateUtil.now();
}
}
123456789101112131415161718192021222324252627
5.2 Redis缓存核心代码
缓存逻辑简洁高效:以“固定前缀+指数编码”作为Redis的key,将StockIndexInfo实体直接存入Redis,后续查询时通过编码即可快速获取:
/**
* 将指数数据存入Redis缓存
* @param stockIndexInfo 指数数据实体
*/
public void updateStockIndex(StockIndexInfo stockIndexInfo) {
// 构造Redis的key:STOCK_PRICE_zs_指数编码(如STOCK_PRICE_zs_000001)
String key = Const.STOCK_PRICE + "zs_" + stockIndexInfo.getCode();
// 存入Redis(默认无过期时间,下次抓取自动覆盖)
redisUtil.set(key, stockIndexInfo);
log.debug("指数数据缓存完成,key:{},数据:{}", key, stockIndexInfo);
}
1234567891011
缓存后的Redis数据示例(key:STOCK_PRICE_zs_000001):
{
"code": "000001",
"name": "上证指数",
"nowPrice": "3917.36",
"nowProportion": "0.69%",
"subPrice": "26.91",
"type": 1,
"date": "2025-12-22 09:30:01"
}
123456789
六、核心优化:让监控系统更稳定、更实用
基础功能实现后,我们还需要做3个关键优化,让系统更适配生产环境,更符合实际使用需求:
6.1 多数据源故障自动切换
通过添加数据源健康检查逻辑,当东方财富抓取失败次数达到阈值时,自动切换到财联社数据源,避免监控中断:
/**
* 智能选择数据源(故障自动切换)
* @return 可用的指数数据列表
*/
private List<StockIndexInfo> selectAvailableDataSource() {
// 先尝试东方财富数据源
List<StockIndexInfo> dfData = extCrawlerService.findStockIndex();
if (CollUtil.isNotEmpty(dfData)) {
return dfData;
}
// 东方财富失败,切换到财联社数据源
log.warn("东方财富数据源抓取失败,切换到财联社数据源");
List<StockIndexInfo> drjData = crawlerDrjService.findStockIndex();
if (CollUtil.isNotEmpty(drjData)) {
return drjData;
}
// 双数据源均失败,记录严重错误
log.error("东方财富、财联社双数据源均抓取失败,本次监控中断");
return Collections.emptyList();
}
12345678910111213141516171819202122
6.2 指数类型精准匹配
通过convertTypeByCode方法,将不同数据源的指数编码统一转换为1-4的类型标识,方便后续筛选和使用:
/**
* 根据指数编码转换为统一类型
* @param code 指数编码
* @return 统一类型:1=上证,2=沪深,3=创业板,4=沪深300
*/
private Integer convertTypeByCode(String code) {
switch (code) {
case "000001":
return 1; // 上证指数
case "399001":
return 2; // 深证成指
case "399006":
return 3; // 创业板指
case "000300":
return 4; // 沪深300
default:
return 0; // 其他指数
}
}
12345678910111213141516171819
6.3 新增指数预警扩展接口
预留预警扩展接口,后续可根据自己的交易策略添加预警逻辑(如指数跌破/突破指定点位时发送邮件/钉钉提醒):
/**
* 指数预警逻辑(扩展接口)
* @param indexInfo 最新指数数据
*/
private void indexWarn(StockIndexInfo indexInfo) {
// 示例:上证指数跌破3900触发预警
if ("000001".equals(indexInfo.getCode())) {
BigDecimal nowPrice = new BigDecimal(indexInfo.getNowPrice());
if (nowPrice.compareTo(new BigDecimal("3900")) < 0) {
notifyService.sendDingTalkNotify(
String.format("【指数预警】上证指数跌破3900点,当前价:%s", indexInfo.getNowPrice())
);
}
}
}
123456789101112131415
七、最终效果:10秒级更新,毫秒级响应
部署完成后,系统将在工作日交易时段自动运行,实现三大核心效果:
- 实时更新:每10秒抓取一次最新指数数据,数据延迟控制在1秒内,远超免费行情软件;
- 稳定可靠:双数据源自动切换,单一数据源故障不影响监控;
- 高效响应:Redis缓存保障查询响应时间≤10ms,后续对接量化策略、前端展示都毫无压力。
后续可基于这套系统,轻松扩展出“指数趋势图表”“个性化预警提醒”“策略自动触发”等高级功能,让指数数据真正为你的交易决策服务。
八、系列文章预告
本文完成了量化系统的“实时数据监控层”搭建,实现了核心指数的10秒级抓取与缓存,为后续策略执行提供了精准、实时的数据支撑。下一篇文章,我们将正式进入量化策略实战环节,基于前面搭建的数据体系,实现“均线交叉策略”的自动执行与回测,让你的量化系统真正具备实战交易能力!
最后,留一个思考问题:如果需要监控更多细分指数(如科创50、中证500),你会如何修改当前的抓取逻辑?欢迎在评论区交流你的解决方案~