在前两篇文章中,我们先后完成了股票基础列表同步和每日日K数据自动同步,搭建了量化系统的“基础数据层”和“每日增量数据层”。但对于量化策略回测而言,仅有当日和近期的日K数据远远不够——我们需要获取一只股票过去数月、数年的历史K线数据,才能验证策略在不同市场环境下的有效性。
本文作为Java量化系列的第三篇,将聚焦“历史K线数据同步”核心功能,基于SpringBoot 3.3.8 + Mybatis-Plus + Mysql8.0技术栈,实现从东方财富网站爬取指定股票的历史K线数据(支持自定义时间范围和K线类型),并完成数据解析、格式转换与批量存储。同样适合三年以上Java开发经验,希望完善量化系统历史数据层的工程师参考。
一、核心需求与设计考量
历史K线数据同步的核心诉求是“灵活性”“完整性”“可复用性”,结合量化策略回测的场景特性,我们明确了以下需求与设计原则:
1.1 核心需求
- 支持多K线类型:可同步日K、周K、月K等不同周期的历史数据(通过参数控制,本文以日K为例);
- 时间范围可控:支持自定义同步的起始时间(如从2025年10月10日开始),直至最新交易日;
- 代码适配性强:支持A股所有可用股票的历史数据同步,需实现股票代码的标准化转换(如001318→0.001318,适配东方财富接口格式);
- 异常兼容:针对接口返回的特殊格式(JSONP)进行解析,处理网络异常、数据缺失等问题;
- 批量高效存储:解析后的历史数据批量存入Mysql,兼顾存储效率与数据一致性。
1.2 技术选型补充与接口分析
在之前技术栈基础上,针对本次历史K线同步需求,补充以下关键设计:
- 数据来源:东方财富历史K线接口(
https://push2his.eastmoney.com/api/qt/stock/kline/get),支持自定义股票代码、时间范围、K线类型,数据完整性高; - 格式解析:JSONP格式解析(接口返回为JSONP包裹的JSON数据,需先剥离JSONP前缀后缀);
- 代码转换:自定义
StockCodeHelper工具类,实现A股代码到东方财富接口要求的标准化格式转换(如上海股票600000→1.600000,深圳股票001318→0.001318); - 接口防爬:添加自定义请求头(含Host、Cookie等字段),模拟浏览器请求,避免被接口拦截;
- 批量存储:复用Mybatis-Plus的
saveBatch方法,指定批次大小(如100条/批),提升存储效率。
核心接口分析(东方财富历史K线接口):
接口URL模板:https://push2his.eastmoney.com/api/qt/stock/kline/get?cb={0}&secid={1}&ut=fa5fd1943c7b386f172d6893dbfba10b&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt={2}&fqt=1&beg={3}&end=20500101&lmt=800&_=
关键参数说明:
cb:JSONP回调函数名(固定值,如jQuery35105361642636114103_1690247737667);secid:标准化股票代码(如0.001318代表深圳股票001318,1.600000代表上海股票600000);klt:K线周期类型(101=日K,102=周K,103=月K,1=分钟K等);beg:起始时间(格式为yyyyMMdd,如20251010代表2025年10月10日);end:结束时间(固定为20500101,代表获取到最新交易日);lmt:单次获取最大条数(默认800条,足够覆盖单只股票数年的日K数据)。
二、核心流程设计:从接口调用到数据存储
本次历史K线数据同步的完整流程可概括为:接口参数准备 → 股票代码标准化转换 → JSONP格式接口调用 → JSONP数据解析(剥离前缀后缀) → JSON数据提取与字段映射 → DTO转DO格式转换 → Mysql批量存储。下面我们按流程逐步拆解实现细节。
三、核心实现:分步拆解关键代码
数据表与 Do 均与之前是一致的, 为了避免读者忘记,再写一下, 后续文章均是以这个 数据表和 Do 为准。
Mysql 数据表:
CREATE TABLE `stock_history_30` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id自增',
`code` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '股票的编码',
`name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '股票的名称',
`curr_date` timestamp NULL DEFAULT NULL COMMENT '当天的日期不包括周六周天',
`highest_price` decimal(18,4) DEFAULT NULL COMMENT '最高价格',
`lowest_price` decimal(18,4) DEFAULT NULL COMMENT '最低价格',
`closing_price` decimal(18,4) DEFAULT NULL COMMENT '收盘价',
`opening_price` decimal(18,4) DEFAULT NULL COMMENT '开盘价',
`yesClosing_price` decimal(18,4) DEFAULT NULL COMMENT '前收盘',
`highest_time` timestamp NULL DEFAULT NULL COMMENT '最高价格所在的时间',
`lowest_time` timestamp NULL DEFAULT NULL COMMENT '最低价格所在的时间',
`open_percent` decimal(18,4) DEFAULT NULL COMMENT '开盘价比例',
`highest_percent` decimal(18,4) DEFAULT NULL COMMENT '最高价比例',
`lowest_percent` decimal(18,4) DEFAULT NULL COMMENT '最低价比例',
`zt` tinyint(1) DEFAULT NULL COMMENT '是否涨停 1为涨停 0为不涨停',
`tProportion` decimal(18,4) DEFAULT NULL COMMENT '做T的比例',
`amplitude` decimal(18,4) DEFAULT NULL COMMENT '涨跌额',
`amplitude_proportion` decimal(18,4) DEFAULT NULL COMMENT '涨跌幅',
`trading_volume` decimal(18,4) DEFAULT NULL COMMENT '成交量',
`trading_value` decimal(18,4) DEFAULT NULL COMMENT '成交金额',
`out_dish` int DEFAULT NULL COMMENT '外盘数量',
`inner_dish` int DEFAULT NULL COMMENT '内盘数量',
`changing_proportion` decimal(18,4) DEFAULT NULL COMMENT '换手率',
`than` decimal(18,4) DEFAULT NULL COMMENT '量比',
`avg_price` decimal(18,4) DEFAULT NULL COMMENT '均价',
`market` decimal(18,4) DEFAULT NULL COMMENT '市值',
`lt_market` decimal(18,4) DEFAULT NULL COMMENT '流通市值',
`static_price_ratio` decimal(18,4) DEFAULT NULL COMMENT '静态市盈率',
`dynamic_price_ratio` decimal(18,4) DEFAULT NULL COMMENT '动态市盈率',
`ttm_price_ratio` decimal(18,4) DEFAULT NULL COMMENT 'TTM 市盈率',
`buy_hand` int DEFAULT NULL COMMENT '买的 前五手',
`sell_hand` int DEFAULT NULL COMMENT '卖的 前五手',
`appoint_than` varchar(18) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '委比',
`flag` tinyint(1) DEFAULT '1' COMMENT '1为正常 0为删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_stock_history_1` (`code`,`curr_date`) USING BTREE,
KEY `curr_date` (`curr_date`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5945467 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='股票的历史交易记录表';
StockHistoryDo:
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("stock_history_30")
public class StockHistoryDo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id自增
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 股票的编码
*/
@TableField("code")
private String code;
/**
* 股票的名称
*/
@TableField("name")
private String name;
/**
* 当天的日期不包括周六周天
*/
@TableField("curr_date")
private Date currDate;
/**
* 最低价格
*/
@TableField("lowest_price")
private BigDecimal lowestPrice;
/**
* 开盘价
*/
@TableField("opening_price")
private BigDecimal openingPrice;
/**
* 前收盘
*/
@TableField("yesClosing_price")
private BigDecimal yesClosingPrice;
/**
* 涨跌额
*/
@TableField("amplitude")
private BigDecimal amplitude;
/**
* 涨跌幅
*/
@TableField("amplitude_proportion")
private BigDecimal amplitudeProportion;
/**
* 成交量
*/
@TableField("trading_volume")
private BigDecimal tradingVolume;
/**
* 成交金额
*/
@TableField("trading_value")
private BigDecimal tradingValue;
/**
* 收盘价
*/
@TableField("closing_price")
private BigDecimal closingPrice;
/**
* 最高价格
*/
@TableField("highest_price")
private BigDecimal highestPrice;
/**
* 是否涨停 1为涨停 0为不涨停
*/
@TableField("zt")
private Integer zt;
/**
* 最低价所处的时间
*/
@TableField("lowest_time")
private Date lowestTime;
/**
* 最高价所处的时间
*/
@TableField("highest_time")
private Date highestTime;
/**
* 开盘价比例
*/
@TableField("open_percent")
private BigDecimal openPercent;
/**
* 最低价比例
*/
@TableField("lowest_percent")
private BigDecimal lowestPercent;
/**
* 最高价比例
*/
@TableField("highest_percent")
private BigDecimal highestPercent;
/**
* (最高点- 最低点) / 昨日收盘价 *100
*/
@TableField("tProportion")
private BigDecimal tProportion;
/**
* 外盘数量
*/
@TableField("out_dish")
private Integer outDish;
/**
* 内盘数量
*/
@TableField("inner_dish")
private Integer innerDish;
/**
* 换手率
*/
@TableField("changing_proportion")
private BigDecimal changingProportion;
/**
* 量比
*/
@TableField("than")
private BigDecimal than;
/**
* 均价
*/
@TableField("avg_price")
private BigDecimal avgPrice;
/**
* 市值,亿单位
*/
@TableField("market")
private BigDecimal market;
/**
* 流通市值,亿单位
*/
@TableField("lt_market")
private BigDecimal ltMarket;
/**
* 静态市盈率
*/
@TableField("static_price_ratio")
private BigDecimal staticPriceRatio;
/**
* 动态市盈率
*/
@TableField("dynamic_price_ratio")
private BigDecimal dynamicPriceRatio;
/**
* TTM 市盈率
*/
@TableField("ttm_price_ratio")
private BigDecimal ttmPriceRatio;
/**
* 买的 前五手
*/
@TableField("buy_hand")
private Integer buyHand;
/**
* 卖的 前五手
*/
@TableField("sell_hand")
private Integer sellHand;
/**
* 委比
*/
@TableField("appoint_than")
private String appointThan;
/**
* 1为正常 0为删除
*/
@TableField("flag")
private Integer flag;
@TableField(exist = false)
private String webUrl;
@TableField(exist = false)
private String aiSendMessage;
public String getAiSendMessage() {
return DateUtil.format(currDate, Const.SIMPLE_DATE_FORMAT) + " " +openingPrice + ","+ highestPrice + "," + lowestPrice + ","+ closingPrice + "," + tradingVolume;
}
}
3.1 核心入口:同步接口与流程编排
首先实现历史K线同步的核心入口接口(采用POST请求,支持后续扩展为批量同步多只股票),负责指定股票代码、调用工具类转换代码格式、获取历史K线数据、格式转换与批量存储。核心方法为asyncKData()。
核心代码实现:
@RestController
@RequestMapping("/stock/history")
public class StockHistoryController {
@Resource
private CrawlerStockService crawlerStockService;
@Resource
private StockCodeHelper stockCodeHelper;
@Resource
private StockHistoryAssembler stockHistoryAssembler;
@Resource
private StockHistoryDomainService stockHistoryDomainService;
@Operation(description = "同步指定股票历史K线数据")
@PostMapping("/asyncKData")
public OutputResult asyncKData() {
// 示例:同步股票001318的历史K线数据(可扩展为接收前端传入的股票代码列表)
String code = "001318";
log.info("开始同步股票 {} 的历史K线数据", code);
try {
// 1. 股票代码标准化转换(适配东方财富接口格式:001318→0.001318)
String standardCode = stockCodeHelper.convertCode(code);
// 2. 调用服务获取历史K线数据(klt=101代表日K,beg=20251010代表起始时间)
OutputResult<List<TxStockHistoryInfo>> dataResult = crawlerStockService.kDataList(standardCode, 101, "20251010");
List<TxStockHistoryInfo> kDataList = dataResult.getData();
if (CollUtil.isEmpty(kDataList)) {
log.info("未获取到股票 {} 的历史K线数据", code);
return OutputResult.buildSucc("未获取到历史K线数据");
}
log.info("成功获取股票 {} 的历史K线数据,共 {} 条", code, kDataList.size());
// 3. DTO转DO:将TxStockHistoryInfo转换为StockHistoryDo(适配数据库表结构)
List<StockHistoryDo> stockHistoryDos = new ArrayList<>();
for (TxStockHistoryInfo txStockHistoryInfo : kDataList) {
StockHistoryDo stockHistoryDo = stockHistoryAssembler.txInfoToDo(txStockHistoryInfo);
// 设置交易日期(从DTO的日期对象中获取)
stockHistoryDo.setCurrDate(txStockHistoryInfo.getCurrDateObj());
stockHistoryDos.add(stockHistoryDo);
}
// 4. 批量保存到Mysql(批次大小100,避免SQL过长)
stockHistoryDomainService.saveBatch(stockHistoryDos, 100);
log.info("股票 {} 历史K线数据同步并保存完成", code);
return OutputResult.buildSucc("历史K线数据同步成功");
} catch (Exception e) {
log.error("股票 {} 历史K线数据同步失败", code, e);
return OutputResult.buildFail("历史K线数据同步失败:" + e.getMessage());
}
}
}
关键注意点:
- 代码扩展性:当前示例固定同步股票001318,实际开发中可修改为接收前端传入的
codeList参数,实现多只股票批量同步; - 参数可配置性:K线类型(
klt)和起始时间(beg)可抽取为配置项(如存入application.yml),避免硬编码; - 异常处理:通过try-catch捕获整个流程的异常,记录详细日志并返回友好提示,便于问题排查。
3.2 核心工具:股票代码标准化转换
东方财富接口要求股票代码为“市场标识.股票代码”格式(如上海市场1、深圳市场0),需实现convertCode方法完成格式转换,同时兼容债券等特殊标的的代码处理。
核心代码实现:
@Component
public class StockCodeHelper {
/**
* 转换标准股票代码格式(适配东方财富接口:上海股票→1.代码,深圳股票→0.代码)
*
* @param code 原始股票代码(如001318、600000)
* @return 标准化后的带市场前缀的代码(如0.001318、1.600000)
*/
public String convertCode(String code) {
// 1. 校验入参:为空或已为标准化格式(含.),直接返回
if (!StrUtil.isNotBlank(code) || code.contains(".")) {
return code;
}
String validateCode = code;
// 3. 判断股票市场类型(上海/深圳),拼接标准化格式
StockCodeType stockCodeType = StockCodeType.getTypeByStockCode(validateCode);
return StockCodeType.SH.equals(stockCodeType) ? "1." + code : "0." + code;
}
}
关键补充说明:
StockCodeType枚举类:需自定义该枚举,通过股票代码前缀判断市场类型(如60开头→上海SH,00开头→深圳SZ,30开头→创业板SZ等);
public enum StockCodeType {
SH(1, "上海"),
SZ(2, "深圳"),
CY(3, "创业板"),
BJ(4, "北京板"),
OTHER(5, "未知"),
;
private Integer code;
private String desc;
private StockCodeType(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 获取交易的方法
*
* @param code
* @return
*/
public static StockCodeType getTypeByCode(Integer code) {
if (code == null) {
return null;
}
for (StockCodeType configCodeType : StockCodeType.values()) {
if (configCodeType.code.equals(code)) {
return configCodeType;
}
}
return null;
}
/**
* 获取交易的方法
*
* @param code
* @return
*/
public static StockCodeType getTypeByStockCode(String code) {
// 如果以 60 开头
if (code.startsWith("68")) {
return StockCodeType.BJ;
} else if (code.startsWith("6")) {
return StockCodeType.SH;
} else if (code.startsWith("0")) {
return StockCodeType.SZ;
} else if (code.startsWith("1")) {
return StockCodeType.OTHER;
} else if (code.startsWith("5")) {
return StockCodeType.OTHER;
}else if (code.startsWith("3")) {
return StockCodeType.CY;
} else if (code.startsWith("83")) {
return StockCodeType.BJ;
} else {
return StockCodeType.OTHER;
}
}
public static Boolean isTradeType(String code) {
// 如果以 60 开头
StockCodeType stockCodeType = getTypeByStockCode(code);
return stockCodeType.equals(StockCodeType.SH) || stockCodeType.equals(StockCodeType.SZ)
|| stockCodeType.equals(StockCodeType.CY);
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
3.3 数据爬取:东方财富JSONP接口调用
实现kDataList方法调用东方财富历史K线接口,核心处理JSONP格式请求、自定义请求头、数据获取与初步清洗(剥离JSONP前缀后缀)。
核心代码实现:
@Service
public class CrawlerStockServiceImpl implements CrawlerStockService {
@Resource
private CrawlerService crawlerService;
@Override
public OutputResult<List<TxStockHistoryInfo>> kDataList(String code, Integer type, String beg) {
try {
// 调用爬虫服务获取历史K线数据
List<TxStockHistoryInfo> kDataList = crawlerService.kDataList(code, type, beg);
return OutputResult.buildSucc(kDataList);
} catch (Exception e) {
log.error("获取股票 {} 历史K线数据失败(类型:{},起始时间:{})", code, type, beg, e);
return OutputResult.buildFail("获取历史K线数据失败");
}
}
}
@Service
public class CrawlerServiceImpl implements CrawlerService {
// JSONP回调函数名(接口固定返回该回调包裹的JSON数据,需固定此值)
private static final String KDATA_CB = "jQuery35105361642636114103_1690247737667";
@Override
public List<TxStockHistoryInfo> kDataList(String code, Integer type, String beg) {
try {
// 1. 拼接历史K线接口URL(替换cb、secid、klt、beg参数)
String kdataUrl = "https://push2his.eastmoney.com/api/qt/stock/kline/get?cb={0}&secid={1}" +
"&ut=fa5fd1943c7b386f172d6893dbfba10b&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt={2}" +
"&fqt=1&beg={3}&end=20500101&lmt=800&_=";
String url = MessageFormat.format(kdataUrl, KDATA_CB, code, type, beg);
// 2. 构建请求头:模拟浏览器请求,避免被接口拦截
Map<String, String> header = new HashMap<>();
header.put("Host", "push2his.eastmoney.com");
header.put("Cookie", "qgqp_b_id=ec2d8007963808c47e3e13c6ab114c63; st_nvi=XXXXXXXqxg-2"); // 实际使用时替换为有效Cookie
header.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");
// 3. 发送GET请求:获取JSONP格式的响应内容(添加时间戳参数避免缓存)
String content = HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(), url + MyDateUtil.getTimezone());
log.info("东方财富历史K线接口返回内容:{}", content);
// 4. JSONP格式清洗:剥离回调函数前缀(KDATA_CB + "(")和后缀(");")
content = content.substring(KDATA_CB.length() + 1); // 去掉前缀:KDATA_CB(
content = content.substring(0, content.length() - 2); // 去掉后缀:);
// 5. 调用解析服务,将清洗后的JSON数据转换为TxStockHistoryInfo列表
List<TxStockHistoryInfo> txStockHistoryInfos = stockInfoParser.kDataList(content);
return CollUtil.isEmpty(txStockHistoryInfos) ? Collections.emptyList() : txStockHistoryInfos;
} catch (Exception e) {
log.error("同步股票 {} 历史K线数据失败(类型:{},起始时间:{})", code, type, beg, e);
// 异常统计:自定义埋点,便于监控接口可用性
globalWebExceptionHandlerAspect.addException();
return null;
}
}
}
关键注意点:
- JSONP格式处理:接口返回内容为
KDATA_CB(JSON数据);格式,需通过字符串截取剥离前缀后缀,转换为标准JSON格式后再解析; - 请求头配置:必须添加
Host和Cookie字段(Cookie需替换为有效值,可通过浏览器抓包获取),否则接口会返回403或空数据; - 缓存避免:URL末尾添加
MyDateUtil.getTimezone()(获取当前时间戳),避免接口返回缓存数据;
Cookie 的获取,可以 登录东方财富, 然后输入股票编码,再 F12 进行获取到。 如下图:
3.4 数据解析:JSON转实体类字段映射
清洗后的JSON数据中,历史K线数据嵌套在data.klines字段中(为JSON数组,每条元素为单根K线的字符串,字段间用逗号分隔)。需实现kDataList方法提取数据并映射为TxStockHistoryInfoDTO。
核心代码实现:
@Service
public class StockInfoParserImpl implements StockInfoParser {
@Override
public List<TxStockHistoryInfo> kDataList(String content) {
// 1. 将清洗后的JSON字符串转换为JSONObject
JSONObject jsonObject = JSONObject.parseObject(content);
// 2. 提取data字段(核心数据所在字段)
JSONObject data = jsonObject.getJSONObject("data");
if (ObjectUtils.isEmpty(data)) {
log.info("历史K线数据解析:data字段为空");
return Collections.emptyList();
}
// 3. 提取股票基本信息:代码和名称
String code = data.getString("code");
String name = data.getString("name");
// 4. 提取klines字段(K线数据数组,每条为单根K线的字符串)
JSONArray jsonArray = data.getJSONArray("klines");
if (jsonArray.size() <= 0) {
log.info("股票 {} 未获取到历史K线数据", code);
return Collections.emptyList();
}
// 5. 遍历解析每条K线数据
List<TxStockHistoryInfo> result = new ArrayList<>(jsonArray.size());
String finalCode = code;
String finalName = name;
jsonArray.forEach(n -> {
String singleContent = n.toString();
if (StrUtil.isNotBlank(singleContent)) {
// 按逗号拆分单根K线数据(字段顺序:日期,开盘价,收盘价,最高价,最低价,成交量,成交额,涨跌额,涨跌幅,振幅,换手率...)
String[] splitArr = singleContent.split(Const.JOB_PARAM_SPLIT); // Const.JOB_PARAM_SPLIT为逗号分隔符
TxStockHistoryInfo txStockHistoryInfo = new TxStockHistoryInfo();
// 基础字段映射
txStockHistoryInfo.setCode(finalCode); // 股票代码
txStockHistoryInfo.setName(finalName); // 股票名称
txStockHistoryInfo.setCurrDate(splitArr[0]); // 交易日期(格式:yyyy-MM-dd)
txStockHistoryInfo.setCurrDateObj(DateUtil.parse(splitArr[0], Const.SIMPLE_DATE_FORMAT)); // 转换为日期对象
// 价格字段映射(BigDecimal类型,避免精度丢失)
txStockHistoryInfo.setOpeningPrice(new BigDecimal(splitArr[1])); // 开盘价
txStockHistoryInfo.setClosingPrice(new BigDecimal(splitArr[2])); // 收盘价
txStockHistoryInfo.setHighestPrice(new BigDecimal(splitArr[3])); // 最高价
txStockHistoryInfo.setLowestPrice(new BigDecimal(splitArr[4])); // 最低价
// 成交量(Long类型)、成交额(BigDecimal类型)
txStockHistoryInfo.setTradingVolume(Long.parseLong(splitArr[5])); // 成交量(单位:股)
txStockHistoryInfo.setTradingValue(new BigDecimal(splitArr[6])); // 成交额(单位:元)
// 衍生指标映射
txStockHistoryInfo.setZf(new BigDecimal(splitArr[7])); // 涨跌额
txStockHistoryInfo.setAmplitudeProportion(new BigDecimal(splitArr[8])); // 涨跌幅(%)
txStockHistoryInfo.setAmplitude(new BigDecimal(splitArr[9])); // 振幅(%)
txStockHistoryInfo.setChangingProportion(new BigDecimal(splitArr[10])); // 换手率(%)
result.add(txStockHistoryInfo);
}
});
return result;
}
}
关键注意点:
- 字段顺序确认:
klines数组中每条字符串的字段顺序是固定的(日期→开盘价→收盘价→…),需通过东方财富接口文档或抓包确认,避免映射错误; - 类型转换:价格、涨跌额等字段需转换为
BigDecimal类型(避免浮点数精度丢失),成交量转换为Long类型(避免超出Integer范围); - 日期处理:将字符串格式的日期(如2025-10-10)转换为
Date对象,便于后续数据库存储和策略回测时的日期筛选; - 空值容错:通过
StrUtil.isNotBlank(singleContent)过滤空数据,避免空指针异常。
四、核心优化点与生产环境适配
上述实现已满足单只股票历史K线数据同步的基础需求,但在生产环境中需补充以下优化点,确保系统稳定性、效率与数据完整性:
- 批量同步优化:扩展接口支持多只股票批量同步,采用线程池异步处理单只股票的同步流程,提升整体同步效率;
- 数据去重机制:同步前先查询数据库中已存在的历史数据,按“股票代码+交易日期”作为唯一键,避免重复插入(可通过Mybatis-Plus的
insertOrUpdateBatch方法实现); - 分页获取适配:当单只股票历史数据超过800条时(
lmt=800),需实现分页获取逻辑(通过调整beg参数为上一批次的最后一个日期); - 多K线类型支持:封装
klt参数为枚举类(如KlineType.DAY=101、KlineType.WEEK=102),支持日K、周K、月K等多类型同步; - 监控与告警:集成监控工具(如Prometheus + Grafana)监控同步成功率、同步数据量等指标,同步失败时通过钉钉/邮件告警;
- 接口切换预案:准备备用历史数据接口(如雪球、同花顺接口),当东方财富接口不可用时自动切换,提升系统可用性。
五、系列文章预告
本文完成了历史K线数据的同步功能,至此我们已搭建起量化系统的“完整数据层”(基础列表+每日增量+历史存量)。
下一篇文章将进行 自选股票列表的处理,用于对基础的数据进行同步。
最后,留一个思考问题:在多只股票批量同步历史K线数据时,如何设计线程池参数(核心线程数、最大线程数、队列大小),才能在提升同步效率的同时,避免触发东方财富接口的限流机制?欢迎在评论区交流~