做量化开发的都懂,交易日期判断看似是小细节,却能直接决定策略成败!🤯 比如:
“周末误触发策略执行,导致空单挂单失败”“漏算法定节假日,抓取到空数据拖垮整个同步任务”“计算前后交易日时,重复过滤节假日效率极低”…… 这些坑,几乎每个量化开发者都踩过。
交易日期判断的核心,本质是“区分周末+过滤法定节假日”,但手动维护节假日列表、重复编写判断逻辑,不仅耗时费力,还容易出错。尤其是A股节假日每年更新,手动同步更是麻烦到崩溃。
今天带来Java量化系列第40篇实战——交易日期判断全流程落地方案,从节假日数据定时同步、实体类封装,到工具类一站式适配多场景需求、对外接口开发,全套可运行代码直接奉上,覆盖“判断是否交易日、查询前后交易日、统计区间交易日”等所有高频场景,彻底解决量化开发中日期判断的痛点。
一、核心认知:交易日期判断的关键要点🎯
先理清核心逻辑,避免开发走弯路,明确量化场景下交易日期判断的核心诉求:
- • 🔹 核心判断逻辑:交易日 = 非周末(非周六、周日)+ 非法定节假日,二者缺一不可。
- • 🔹 数据支撑:需维护法定节假日列表,每年更新一次,避免手动录入出错;通过定时任务自动同步,无需人工干预。
- • 🔹 高频场景:判断指定日期是否为交易日、获取前后N个交易日、统计区间内所有交易日、查询最近交易日列表(适配策略执行、数据抓取等场景)。
- • 🔹 核心优势:封装通用工具类,一键复用所有日期判断逻辑;定时同步节假日数据,适配每年节假日调整;对外提供标准化接口,支撑多服务调用。
关键提醒:A股交易日遵循“周末休市、法定节假日休市”规则,无特殊调休补班(调休上班日若为周末,仍不算交易日),本文方案完全贴合A股规则。
二、核心实现:从数据同步到工具类封装全流程⚙️
整套方案分为“实体类封装-节假日数据定时同步-DateHelper工具类开发-对外接口开发”四步,每一步都附完整代码+详细注释,所有代码可直接导入项目运行,无需额外修改。
2.1 实体类封装:HolidayCalendarDo(数据存储核心)
用于存储法定节假日数据,关联数据库表holiday_calendar,区分日期类型(交易日/周末/法定节假日),为后续判断提供数据支撑,完整代码如下:
@Data@EqualsAndHashCode(callSuper=false)@TableName("holiday_calendar")// 关联数据库表publicclassHolidayCalendarDoimplementsSerializable{privatestaticfinallongserialVersionUID=1L;/** 主键自增 */@TableId(value="id",type=IdType.AUTO)privateIntegerid;/** 法定日期,不开盘(存储节假日日期) */@TableField("holiday_date")privateDateholidayDate;/** 当前年(用于按年筛选,提升查询效率) */@TableField("curr_year")privateIntegercurrYear;/** 日期类型 3为法定节假日 */@TableField("date_type")privateIntegerdateType;}设计亮点:按年存储节假日数据,查询时可精准筛选对应年份的节假日,避免全表扫描;日期类型字段只放置 法定节假日。
2.2 定时任务:每年自动同步节假日数据(解放双手)
核心需求:每年1月1日自动同步当年法定节假日数据,避免重复同步,无需人工维护。通过syncYear方法调用第三方接口获取数据,批量保存到数据库,完整代码如下:
/** * 同步指定年份的节假日数据(定时任务每年1月1日执行) * @param year 需同步的年份 * @return 同步结果(成功/失败/已存在) */publicOutputResult<Void>syncYear(Integeryear){// 1. 校验该年份数据是否已存在,避免重复同步HolidayQueryParamqueryParam=newHolidayQueryParam();queryParam.setYear(year);List<HolidayCalendarDo>holidayCalendarDoList=holidayCalendarDomainService.listByCondition(queryParam);if(!CollUtil.isEmpty(holidayCalendarDoList)){log.info(">>>已经存在 {}年的假期数据,不需要同步",year);returnOutputResult.buildAlert(ResultCode.HOLIDAY_EXISTS);}Map<?,?>data;try{// 2. 调用第三方接口获取当年节假日数据(接口返回格式:年份->日期映射,0=工作日,非0=节假日)data=restTemplate.getForObject("http://tool.bitefu.net/jiari/?d="+year,Map.class);}catch(Exceptione){log.error("获取同步假期数据时出现异常",e);returnOutputResult.buildFail();}// 3. 解析接口返回数据,封装为实体类列表Map<String,Integer>dateInfo=(Map<String,Integer>)data.get(String.valueOf(year));List<HolidayCalendarDo>list=dateInfo.entrySet().stream().filter(entry->entry.getValue()!=0)// 筛选出节假日(非0即为节假日).map(entry->{Datedate;try{// 格式化日期(接口返回日期为MMdd,拼接年份转为yyyyMMdd格式)date=DateUtils.parseDate(year+entry.getKey(),"yyyyMMdd");}catch(ParseExceptione){thrownewIllegalArgumentException(e);}HolidayCalendarDoholidayCalendarDo=newHolidayCalendarDo();holidayCalendarDo.setHolidayDate(date);holidayCalendarDo.setCurrYear(year);holidayCalendarDo.setDateType(3);// 标记为法定节假日(date_type=3)returnholidayCalendarDo;}).collect(Collectors.toList());// 4. 批量保存节假日数据到数据库holidayCalendarDomainService.saveBatch(list);returnOutputResult.buildSucc();}定时任务配置说明(关键):
// 定时任务注解,每年1月1日0点10分执行,同步当年节假日数据@Scheduled(cron="0 10 0 1 1 ?")publicvoidsyncHolidayData(){intcurrentYear=LocalDate.now().getYear();syncYear(currentYear);}避坑重点:第三方接口返回日期为MMdd格式,需拼接年份转为yyyyMMdd再解析,避免日期格式错误;添加异常捕获,防止接口调用失败导致定时任务崩溃。
对应的地址是:
http://tool.bitefu.net/jiari/?d=2026返回内容是:
{2026:{1001:2,1002:2,1003:2,1004:1,1005:1,1006:1,1007:1,0101:2,0102:1,0103:1,0215:1,0216:2,0217:2,0218:2,0219:2,0220:1,0221:1,0222:1,0223:1,0404:1,0405:2,0406:1,0501:2,0502:2,0503:1,0504:1,0505:1,0619:2,0620:1,0621:1,0925:2,0926:1,0927:1}}3. 核心工具类:DateHelper(一站式日期判断解决方案)
封装所有交易日期相关工具方法,覆盖量化开发全场景,无需重复编写判断逻辑,直接调用即可。核心方法分类讲解,完整代码附注释:
3.1 基础判断:是否为交易日
核心方法isWorkingDay,判断指定日期是否为交易日(非周末+非法定节假日),适配策略执行、数据抓取等基础场景:
/** * 判断指定时间是否为工作日(非周末且不在节假日列表中) * @param currDate 指定的时间(为null则使用当前系统日期) * @return 当前时间是否是工作日, 是为 true, 否则为 false */publicbooleanisWorkingDay(DatecurrDate){if(currDate==null){currDate=DateUtil.date();}// 1. 查询当前年份的节假日列表(按年查询,提升效率)List<String>holidayDateList=holidayCalendarService.listHolidayDateByYear(DateUtil.year(currDate));// 2. 先判断是否为周末,是则直接返回falseif(DateUtil.isWeekend(currDate)){returnfalse;}// 3. 判断是否在节假日列表中,不在则为交易日StringformatDate=DateUtil.format(currDate,Const.SIMPLE_DATE_FORMAT);return!holidayDateList.contains(formatDate);}3.2 场景化工具:前后交易日查询
覆盖“获取前N个交易日、后N个交易日、排除当天的前后交易日”等高频场景,以两个核心方法为例,其余方法可直接复用:
/** * 查询距离指定日期N天前的工作日日期(包含当天,不包含假期与周末) * @param date 日期 * @param days 天数 * @return N天前的工作日 */publicDategetBeforeWorkingDateByDay(Datedate,intdays){returnDateUtil.beginOfDay(getBeforeWorkingByCount(date,days,true).getFirst());}/** * 查询距离指定日期N天前的工作日日期(不包含当天) * @param date 日期 * @param days 天数 * @return N天前的工作日(不含当天) */publicDategetBeforeWorkingDateByDayRemoveToday(Datedate,intdays){returnDateUtil.beginOfDay(getBeforeWorkingByCountRemoveToday(date,days,true).getFirst());}3.3 进阶工具:区间交易日统计
计算两个日期之间的所有交易日,适配区间数据统计、策略回测等场景,核心方法如下:
/** * 计算两个日期之间的所有工作日(包含起始日期) * @param startDate 开始日期 * @param endDate 结束日期 * @return 工作日的日期列表 */publicList<Date>betweenWorkDay(DatestartDate,DateendDate){if(!(startDate!=null&&endDate!=null)){returnCollections.emptyList();}// 格式化日期为当天开始/结束时间,避免时间戳干扰startDate=DateUtil.beginOfDay(startDate);endDate=DateUtil.endOfDay(endDate);List<Date>result=newArrayList<>();// 循环遍历区间内所有日期,筛选出交易日while(endDate.after(startDate)){if(isWorkingDay(startDate)){result.add(DateUtil.beginOfDay(startDate));}startDate=DateUtil.offsetDay(startDate,1);// 日期加1天}returnresult;}工具类亮点:所有方法均做了空值处理和效率优化,按年查询节假日列表避免全表扫描;日期格式化统一,规避时间戳干扰,适配多场景调用。
3.4 对外接口开发:支撑多服务调用
基于工具类开发标准化对外接口,覆盖前端查询、其他服务调用等场景,接口文档清晰,直接对接业务,核心接口代码如下:
/** * 判断指定日期是否为交易日(核心接口) * @param day 日期字符串(格式:yyyy-MM-dd) * @return 1=是交易日,0=非交易日 */@Operation(summary="查询某天是否是交易日")@GetMapping("/getTradeDay/{day}")publicOutputResult<Integer>getTradeDay(@PathVariable("day")Stringday){Datedate=DateUtil.parse(day);returnOutputResult.buildSucc(dateHelper.isWorkingDay(date)?1:0);}/** * 查询最近十天的交易日日期列表 * @return 最近10个交易日的日期字符串列表(格式:yyyy-MM-dd) */@Operation(summary="查询最近十天的交易日日期列表")@GetMapping("/getTenTradeDay")publicOutputResult<List<String>>getTenTradeDay(){returnholidayCalendarBusiness.getTenTradeDay();}/** * 查询日期段内的交易日日期列表 * @param dateRo 日期对象(包含开始日期、结束日期) * @return 区间内所有交易日日期列表 */@Operation(summary="查询日期段内的交易日日期列表")@PostMapping("/getTradeDay")publicOutputResult<List<String>>getTradeDay(@RequestBodyDateRodateRo){returnholidayCalendarBusiness.getTradeDay(dateRo);}/** * 查询前N天的交易日日期(不含当天) * @param days 天数 * @return 前N天的交易日日期 */@Operation(summary="查询前几天的对应日期")@GetMapping("/getBeforeLastDay/{days}")publicOutputResult<Date>getBeforeLastDay(@PathVariableIntegerdays){returnholidayCalendarBusiness.getBeforeLastDay(days);}接口使用场景:前端页面交易日筛选、量化策略调度(仅在交易日执行)、数据同步任务(仅抓取交易日数据)、回测系统日期校验等,通用性极强。
大家可以通过:
https://stock-api.apifox.cn/api-391134756看响应的数据,进行理解
四、实战场景:工具类+接口的落地案例📊
结合量化开发高频场景,举例说明如何使用本文方案,帮你快速落地:
场景1:量化策略仅在交易日执行
// 策略执行入口publicvoidexecuteStrategy(){// 1. 判断当前日期是否为交易日if(!dateHelper.isWorkingDay(null)){log.info("今日非交易日,不执行策略");return;}// 2. 执行策略逻辑(选股、下单、复盘等)strategyService.run();}场景2:抓取最近10个交易日的股票数据
// 数据抓取入口publicvoidsyncStockData(){// 1. 查询最近10个交易日OutputResult<List<String>>result=holidayCalendarBusiness.getTenTradeDay();if(!result.isSuccess()||CollUtil.isEmpty(result.getData())){log.error("查询交易日失败,无法同步数据");return;}// 2. 循环抓取每个交易日的股票数据List<String>tradeDays=result.getData();tradeDays.forEach(day->{stockDataService.syncDailyData(day);// 抓取单个交易日数据});}场景3:统计区间内交易日数量(回测场景)
// 回测统计入口publicintcountTradeDays(StringstartDateStr,StringendDateStr){// 1. 计算区间内所有交易日List<String>tradeDays=dateHelper.betweenWorkDay(startDateStr,endDateStr);// 2. 返回交易日数量returntradeDays.size();}五、避坑指南:8个实战踩雷教训⚠️(必看)
交易日期判断看似简单,实则暗藏很多细节坑,整理了实战中踩过的雷,帮你少走弯路:
坑1:日期格式不统一,导致判断失效
✅ 原因:工具类中日期格式化与数据库存储格式不一致(如工具类用yyyy-MM-dd,数据库用yyyyMMdd),导致节假日匹配失败。
✅ 解决:统一日期格式为yyyy-MM-dd(Const.SIMPLE_DATE_FORMAT定义),数据库与工具类保持一致。
坑2:定时任务执行时机错误,遗漏节假日
✅ 原因:定时任务未在1月1日执行,或执行时第三方接口未更新当年节假日数据,导致数据缺失。
✅ 解决:定时任务设置为1月1日0点10分执行(避开接口更新高峰期),添加执行日志,失败时触发告警。
坑3:未处理跨年节假日,查询失败
✅ 原因:12月查询次年1月节假日时,仅查询当年节假日列表,导致跨年节假日未被过滤。
✅ 解决:工具类中添加跨年处理逻辑(如12月查询时,同时加载当年和次年节假日数据),参考getAfterWorkingByCount方法实现。
坑4:频繁查询数据库,性能瓶颈
✅ 原因:每次判断交易日都查询数据库,高并发场景下导致服务卡顿。
✅ 解决:将当年节假日列表缓存到Redis,设置过期时间为1年,查询时先查缓存,再查数据库。
坑5:日期为null未处理,报空指针异常
✅ 原因:未处理currDate=null的情况,直接调用日期工具类方法,导致空指针。
✅ 解决:所有工具类方法均添加空值处理,默认使用当前系统日期。
坑6:第三方接口调用失败,未做兜底
✅ 原因:第三方接口宕机或超时,未做降级处理,导致定时任务崩溃。
✅ 解决:添加接口重试机制(最多重试3次),失败时手动同步接口数据,避免任务中断。
坑7:混淆“交易日”与“工作日”
✅ 原因:误将调休上班日当作交易日(调休上班日若为周末,仍不算交易日)。
✅ 解决:严格遵循“非周末+非法定节假日”双重判断,第三方接口已处理调休逻辑,无需额外开发。
坑8:区间交易日统计遗漏起始日期
✅ 原因:循环判断时未包含起始日期,导致统计结果少1天。
✅ 解决:将起始日期格式化为本日开始时间(DateUtil.beginOfDay(startDate)),确保起始日期被纳入判断。
五、福利领取:完整可运行代码包免费送🎁
为了帮大家快速落地,我整理了本次交易日期判断实战的完整可运行代码包,包含:
- • ① holiday_calendar 对应的sql 语句 和近三年的数据。
- • ② DateHelper工具类完整代码(所有日期判断方法);
- • ③ 对外接口完整代码+接口文档;
私信回复【交易日期判断】,即可免费领取!所有代码均可直接导入项目运行,无需修改,帮你节省1天以上开发时间,彻底搞定量化开发中日期判断的所有难题。
下期预告✨
本次我们搞定了交易日期判断的全流程方案,下期将聚焦 股票池,获取涨停股票,跌停股票 等数据 为量化策略筑牢数据基础,敬请期待!
结尾互动
你在量化开发中,还遇到过哪些日期判断的坑?欢迎在评论区留言讨论!
如果觉得这篇文章对你有帮助,别忘了点赞+在看+转发,让更多量化开发者告别日期判断的困扰~ 关注我,持续解锁Java量化实战干货!🚀