跳到主要内容Java 量化实战:股票详细信息实时获取与多数据源架构 | 极客日志Javajava算法
Java 量化实战:股票详细信息实时获取与多数据源架构
基于 SpringBoot 实现股票详细信息实时获取模块。通过定义 StockShowInfoDto 统一数据模型,对接财联社与腾讯两大公开数据源。采用主备切换架构保障稳定性,财联社为主,腾讯为备。核心涵盖请求构建、反爬处理、GBK 编码解析及异常降级策略。代码复用历史数据爬取基础,确保系统一致性,适合 Java 量化开发参考。
21772838011 浏览 在前期基础中,我们完成了自选股票维护功能。但对量化交易者而言,仅维护列表不够,还需实时掌握单只股票的关键信息,如当前价格、涨跌幅度、成交量等,这些是判断短期走势的核心依据。
本文聚焦股票详细信息获取模块,基于 SpringBoot 技术栈,实现两大核心目标:一是定义统一的股票详情数据模型(StockShowInfoDto),兼容不同数据源的字段映射;二是对接财联社、腾讯两大公开数据源,实现股票详情信息的实时爬取、解析与封装。同时复用历史数据爬取基础代码,保证系统一致性。
一、核心需求与数据模型设计
1.1 核心需求边界
股票详细信息获取的核心诉求是实时性、完整性与兼容性。结合量化交易场景,明确需求边界如下:
- 数据完整性:覆盖基础信息(代码、名称)、行情数据(开盘/收盘/最高/最低价)、交易数据(成交量、金额)、估值数据(市盈率)及辅助判断数据(涨跌幅、涨停状态);
- 多数据源兼容:对接财联社、腾讯两大数据源,支持自动切换,避免单一数据源失效;
- 实时性保障:通过 HTTP 请求实时爬取,确保获取最新行情;
- 异常兼容:爬取失败时返回 null 并记录日志,支持降级处理。
1.2 核心数据模型:StockShowInfoDto
为统一不同数据源的返回格式,定义 StockShowInfoDto 作为标准输出模型。通过自定义 getter 方法实现部分字段的自动计算(如涨停状态、涨跌幅度百分比转换)。
@Data
@Schema(description = "股票展示信息")
public class StockShowInfoDto implements Serializable {
@Schema(description = "股票的代码")
private String code;
@Schema(description = "股票的名称")
private String name;
@Schema(description = "日期")
private String currDate;
@Schema(description = "股票的全代码")
private String fullCode;
@Schema(description = "交易所类型")
private Integer exchange;
@Schema(description = "当前天")
private String date;
@Schema(description = "开盘价")
private BigDecimal openingPrice;
BigDecimal yesClosingPrice;
BigDecimal highestPrice;
BigDecimal lowestPrice;
BigDecimal closingPrice;
BigDecimal nowPrice;
tradingVolume;
BigDecimal tradingValue;
BigDecimal amplitude;
String amplitudeProportion;
BigDecimal changingProportion;
BigDecimal than;
BigDecimal peRatio;
;
Double amplitudeProportionDouble;
String webUrl;
Integer {
StockUtil.isZt(.code, .name, .amplitudeProportion) ? : ;
}
Double {
(StrUtil.isBlank(amplitudeProportion)) {
;
}
amplitudeProportion.replace(, );
Double.parseDouble(substring);
}
}
@Schema(description = "昨天的收盘价")
private
@Schema(description = "最高价格")
private
@Schema(description = "最低价格")
private
@Schema(description = "收盘价")
private
@Schema(description = "当前的价格")
private
@Schema(description = "成交量 (股)")
private
long
@Schema(description = "成交量金额")
private
@Schema(description = "涨跌幅度")
private
@Schema(description = "涨跌幅度百分比")
private
@Schema(description = "换手率")
private
@Schema(description = "量比")
private
@Schema(description = "市盈率")
private
@Schema(description = "是否涨停 1 为涨停 0 为不涨停")
private
Integer
zt
=
0
@Schema(description = "涨幅 double 形式")
private
@Schema(description = "webUrl 地址信息")
private
public
getZt
()
return
this
this
this
1
0
public
getAmplitudeProportionDouble
()
if
return
0.0d
String
substring
=
"%"
""
return
关键设计亮点在于自动计算字段,通过自定义 getter 方法实现 zt 和 amplitudeProportionDouble 的自动计算,无需上层代码处理。同时兼顾不同数据源的字段差异,通过 DTO 统一封装,屏蔽底层差异。
二、核心设计:多数据源架构与流程
为避免单一数据源失效导致功能不可用,设计主备数据源架构:以财联社作为主数据源(数据字段更完整),腾讯作为备用数据源(接口稳定性高)。核心流程遵循参数校验、构建请求、发送 HTTP 请求、解析响应数据、DTO 封装、自动计算字段。
三、核心实现:多数据源爬取与封装
下面拆解财联社(主数据源)和腾讯(备用数据源)的完整实现逻辑。
3.1 主数据源:财联社股票详情爬取实现
财联社提供公开的股票详情接口,返回 JSON 格式数据。需依赖 HTTP 工具类发送 GET 请求、JSON 工具类解析响应数据。
@Service
public class StockDetailServiceImpl implements StockDetailService {
@Override
public StockShowInfoDto getByCode(String fullCode) {
if (StrUtil.isBlank(fullCode)) {
log.error("财联社爬取股票详情失败:全代码为空");
return null;
}
try {
String url = MessageFormat.format(
"https://x-quote.cls.cn/quote/stock/basic?secu_code={0}&fields=open_px,av_px,high_px,low_px,change,change_px,down_price,change_3,change_5,qrr,entrust_rate,tr,amp,TotalShares,mc,NetAssetPS,NonRestrictedShares,cmc,business_amount,business_balance,pe,ttm_pe,pb,secu_name,secu_code,trade_status,secu_type,preclose_px,up_price,last_px&app=CailianpressWeb&os=web&sv=8.4.6&sign=9f8797a1f4de66c2370f7a03990d2737",
fullCode
);
Map<String, String> headerMap = buildDjrHeaderMap();
String content = HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(), url, headerMap);
JSONObject jsonObject = JSONUtil.parseObj(content);
JSONObject data = jsonObject.getJSONObject("data");
if (data == null) {
log.error("财联社爬取股票详情失败:响应数据中 data 节点为空,fullCode={}", fullCode);
return null;
}
StockShowInfoDto stockShowInfoDto = new StockShowInfoDto();
stockShowInfoDto.setCode(StrUtil.subSufByLength(fullCode, 6));
stockShowInfoDto.setName(data.getStr("secu_name"));
stockShowInfoDto.setCurrDate(DateUtil.now());
stockShowInfoDto.setFullCode(fullCode);
stockShowInfoDto.setDate(DateUtil.now());
stockShowInfoDto.setWebUrl("Data Source Link");
stockShowInfoDto.setOpeningPrice(data.getBigDecimal("open_px"));
stockShowInfoDto.setYesClosingPrice(data.getBigDecimal("preclose_px"));
stockShowInfoDto.setHighestPrice(data.getBigDecimal("high_px"));
stockShowInfoDto.setLowestPrice(data.getBigDecimal("low_px"));
stockShowInfoDto.setClosingPrice(data.getBigDecimal("last_px"));
stockShowInfoDto.setNowPrice(data.getBigDecimal("last_px"));
stockShowInfoDto.setTradingVolume(data.getLong("business_amount"));
stockShowInfoDto.setTradingValue(data.getBigDecimal("business_balance"));
stockShowInfoDto.setAmplitude(data.getBigDecimal("change_px"));
stockShowInfoDto.setAmplitudeProportion(BigDecimalUtil.mul100(data.getBigDecimal("change")));
stockShowInfoDto.setChangingProportion(BigDecimalUtil.mul100B(data.getBigDecimal("tr")));
stockShowInfoDto.setThan(BigDecimalUtil.mul100B(data.getBigDecimal("qrr")));
stockShowInfoDto.setPeRatio(data.getBigDecimal("ttm_pe"));
stockShowInfoDto.setZt(stockShowInfoDto.getZt());
return stockShowInfoDto;
} catch (Exception e) {
log.error("财联社爬取股票详情失败,fullCode={}", fullCode, e);
return null;
}
}
private Map<String, String> buildDjrHeaderMap() {
Map<String, String> headerMap = new HashMap<>();
headerMap.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headerMap.put("Referer", "https://www.cls.cn/");
headerMap.put("Accept", "application/json, text/plain, */*");
return headerMap;
}
}
关键注意点:必须模拟浏览器请求头,否则接口会拒绝返回数据。全代码处理需注意截取后 6 位作为纯代码。数值计算使用 BigDecimalUtil 工具类避免浮点数精度丢失。
3.2 备用数据源:腾讯股票详情爬取实现
腾讯股票接口返回特殊格式字符串,需自定义解析逻辑。该接口稳定性高,适合作为备用数据源。可复用历史数据爬取的基础方法。
@Resource
private DefaultProperties defaultProperties;
@Resource
private DailyTradingInfoParse dailyTradingInfoParse;
@Override
public StockShowInfoDto getNowInfo(String fullCode) {
if (StrUtil.isBlank(fullCode)) {
log.error("腾讯爬取股票详情失败:全代码为空");
return null;
}
try {
List<TxStockHistoryInfo> txStockHistoryInfos = parseTxMoneyYesHistory(Collections.singletonList(fullCode), DateTime.now());
if (CollUtil.isEmpty(txStockHistoryInfos)) {
log.error("腾讯爬取股票详情失败:解析后数据列表为空,fullCode={}", fullCode);
return null;
}
TxStockHistoryInfo txStockHistoryInfo = txStockHistoryInfos.get(0);
StockShowInfoDto stockShowInfoDto = new StockShowInfoDto();
stockShowInfoDto.setCode(txStockHistoryInfo.getCode());
stockShowInfoDto.setName(txStockHistoryInfo.getName());
stockShowInfoDto.setFullCode(fullCode);
stockShowInfoDto.setExchange(0);
stockShowInfoDto.setDate(DateUtil.now());
stockShowInfoDto.setOpeningPrice(txStockHistoryInfo.getOpeningPrice());
stockShowInfoDto.setYesClosingPrice(txStockHistoryInfo.getYesClosingPrice());
stockShowInfoDto.setHighestPrice(txStockHistoryInfo.getHighestPrice());
stockShowInfoDto.setLowestPrice(txStockHistoryInfo.getLowestPrice());
stockShowInfoDto.setClosingPrice(txStockHistoryInfo.getClosingPrice());
stockShowInfoDto.setNowPrice(txStockHistoryInfo.getNowPrice());
stockShowInfoDto.setTradingVolume(txStockHistoryInfo.getTradingVolume());
stockShowInfoDto.setTradingValue(txStockHistoryInfo.getTradingValue());
stockShowInfoDto.setAmplitude(txStockHistoryInfo.getAmplitude());
stockShowInfoDto.setAmplitudeProportion(BigDecimalUtil.toShowString2(txStockHistoryInfo.getAmplitudeProportion()));
stockShowInfoDto.setChangingProportion(txStockHistoryInfo.getChangingProportion());
stockShowInfoDto.setThan(txStockHistoryInfo.getThan());
stockShowInfoDto.setPeRatio(txStockHistoryInfo.getDynamicPriceRatio());
stockShowInfoDto.setZt(0);
return stockShowInfoDto;
} catch (Exception e) {
log.error("腾讯爬取股票详情失败,fullCode={}", fullCode, e);
return null;
}
}
@Override
public List<TxStockHistoryInfo> parseTxMoneyYesHistory(List<String> codeList, DateTime beforeLastWorking) {
String qParam = StrUtil.join(",", codeList);
String url = MessageFormat.format(defaultProperties.getTxMoneyHistoryUrl(), qParam);
try {
Map<String, String> header = new HashMap<>();
header.put("Referer", "http://qt.gtimg.cn");
String content = HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(), url, header, "gbk");
return dailyTradingInfoParse.parseTxMoneyHistory(content, codeList, beforeLastWorking);
} catch (Exception e) {
log.error("parseTxMoneyYesHistory 获取股票{}当前信息失败", qParam, e);
return null;
}
}
关键注意点:响应编码需指定为 GBK,否则会出现中文乱码。腾讯接口返回特殊格式字符串,需通过专用解析工具类自定义解析逻辑。
3.3 数据源切换策略实现
为实现主备数据源自动切换,新增数据源选择器方法。优先调用财联社接口,若返回 null 则自动调用腾讯接口;若均失败,返回 null 并记录日志。
@Override
public StockShowInfoDto getStockDetail(String fullCode) {
StockShowInfoDto mainSourceResult = getByCode(fullCode);
if (mainSourceResult != null) {
return mainSourceResult;
}
log.warn("财联社爬取失败,切换腾讯数据源,fullCode={}", fullCode);
StockShowInfoDto backupSourceResult = getNowInfo(fullCode);
if (backupSourceResult != null) {
return backupSourceResult;
}
log.error("主备数据源爬取均失败,fullCode={}", fullCode);
return null;
}
四、核心优化点与生产环境适配
上述实现已满足基础需求,但在生产环境中需补充以下优化点:
- 缓存优化:对股票详情数据添加 Redis 缓存,key 为
STOCK_DETAIL + fullCode,过期时间设为 5 分钟,减少重复爬取。
- 反爬机制适配:维护 User-Agent 池,每次请求随机选择,降低反爬风险。
- 请求频率控制:通过线程池 + 计数器限制爬取频率,避免被封禁 IP。
- 数据源健康监控:记录主备数据源的爬取成功率,低于阈值时发送告警。
- 字段校验增强:对 DTO 封装后的字段进行非空校验,若为空则填充默认值,避免空指针异常。
- 降级策略:当主备数据源均失败时,返回最近一次缓存的股票详情数据,提升用户体验。
五、总结
本文完成了量化系统数据展示层的核心模块——股票详细信息实时获取,实现了多数据源爬取、自动切换、数据封装的完整逻辑。后续将结合实时详情数据与自选股票功能,构建自己的股票实时查看系统。在高并发场景下,如何设计缓存策略兼顾性能与数据实时性,值得进一步探讨。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online