QMT 交易日判断完全指南:三级递进判断今天是不是 A 股交易日(附核心思路与关键代码片段)

这是一份写给 QMT(迅投极速策略交易系统)个人用户 的交易日判断实战指南。
全文分四块:先讲清原理(为什么”判断交易日”比你想的要多想一层)、再给实现思路与关键代码片段(核心逻辑不长,把思路讲透,剩下的自己搭)、然后讲两种典型用法(策略启动时判断 + 定时任务)、最后给排错表和完整免责声明。
代码片段基于 QMT 内置 Python 3.6,面向实盘策略使用。


一、这份指南给你什么

先按你的情况,挑一条阅读路径:

你是谁 建议路径
刚接触 QMT 从 §2 读起,搞懂”为什么不能只判断周末”
已会写策略,想看核心逻辑 直奔 §4 关键代码片段 + §6 两种用法
自己写的时候跑不出预期结果 跳到 §8 排错表
第一次看我的文章 先看 §9 免责声明,再决定怎么用

要解决的是一个非常具体、几乎每个实盘策略都会遇到的问题:

今天是周六、是国庆节、还是正常的交易日?我的策略今天到底该不该干活?

这个判断看起来简单,但写错的位置、错的时点、错的数据前提,都会让你的策略在休市日空跑报错,或者在交易日该干活的时候安静地待机——两种情况都不好排查。

本文不给”复制粘贴就能用”的一整份封装模块,而是把判断逻辑拆成几个独立片段讲清楚。原因很实在:交易日判断只有十几行核心逻辑,真正值钱的是”为什么这么排”;你自己组装一遍,以后接任何策略都不用再回来翻文章。


二、为什么”判断交易日”是实盘策略的第一行代码

先看三个真实场景:

场景 1:定时任务在周末被触发。
你用 run_time 注册了一个每天 09:25 的定时任务。QMT 的定时器在周末和节假日照样会触发——它不懂交易所日历。如果你的回调里直接读行情、查持仓,轻则拿到过期数据,重则直接报错。

场景 2:临时休市日没有日历兜底。
周末之外,还有春节、国庆等法定节假日,以及交易所的临时休市安排。只靠”今天几号”这种本地规则没法覆盖,必须有交易所日历做最终裁决。

场景 3:调休周末。
A股周末从不交易,但法定节假日和调休安排每年都在变。只判断周末,遇到国庆、春节就会误判;完全依赖日历接口,又会在每次回调里白白多调一次接口。

所以正确的姿势是分层递进:先用零成本的办法把周末秒排除,剩下的工作日再交给交易所日历裁决。这就是下一节的内容。


三、原理:三级递进判断

判断一个日期是否为 A 股交易日,我们分三级来做,每一级都是”能早退就早退”:

第 0 级:日期格式校验

传入的日期先归一化成 YYYYMMDD,格式不对直接抛异常。这一级是为了防御——防止 2026-10-5、20261005 (带空格)这类脏数据一路带到底,最后得到一个莫名其妙的结论。

第 1 级:周末快速过滤(零成本)

Python 的 datetime.weekday() 返回 0=周一 ... 5=周六, 6=周日。>= 5 就是周末。

关键认知:A 股周六周日从不交易(哪怕调休上班日也是休市的)。所以”周末 → 一定不是交易日”这条规则不存在例外,可以放心地直接返回 False,不需要查任何接口。

这一级的价值是性能:周末触发的定时任务、启动在周末的策略,在这里就结束了,一次日历接口都不用调。

第 2 级:交易日历校验(权威裁决)

调用 QMT 的 ContextInfo.get_trading_dates,取”目标日期往前 N 个交易日”:

  • 如果目标日期是交易日 → 返回列表的最后一个元素恰好等于它;
  • 如果目标日期不是交易日(法定节假日等)→ 列表最后一格早于它。

所以判断条件就是一行:dates[-1] == date_str。

两个使用前提,缺一不可:

  1. 只能在 after_init / handlebar / run_time 回调中调用,不能放在 init 里——QMT 的机制是 init 执行完成前,交易日历类接口尚未就绪。
  2. 客户端必须先下载”节假日数据”(菜单:数据管理 → 节假日数据),否则日历不完整,法定节假日可能被误判。这个数据建议每年年初更新一次。

判断流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
传入日期
│
▼
[第0级] 格式校验 ──非法──► 抛异常(拒绝带病运行)
│ 合法
▼
[第1级] 是周六/周日? ──是──► False(零成本早退)
│ 否(工作日)
▼
[第2级] 查交易日历(带缓存)
│
├─ 日历为空 ──► False + 打印警告(多为节假日数据未下载)
│
└─ dates[-1] == 目标日期?
├─ 是 ──► True(交易日)
└─ 否 ──► False(法定节假日等)

四、实现思路与关键代码片段

先看整体形状。你最终写出来的东西,职责上应该就这四个部分——结构清楚比代码短更重要:

1
2
3
4
5
判断入口 is_trading_day(日期)
├─ 第0步 日期归一化 normalize_date → 'YYYYMMDD',非法直接抛
├─ 第1步 周末过滤 is_weekend → 命中即返回 False
├─ 第2步 日历查询 get_calendar → 命中缓存优先,否则调接口
└─ 第3步 结果裁决 dates[-1] == 目标日期

下面按这四步给关键片段。

文件编码提醒:QMT 内置 Python 要求源文件为 GBK 编码,文件第一行必须有 # coding:gbk。下面的片段你要单独存成文件时,注意这一点。

片段 1:第 0 级 · 日期归一化

这一段的要点是”宽容输入、严格输出”——允许用户传 2026-10-05 这种写法,但内部统一成 YYYYMMDD:

1
2
3
4
5
6
7
8
9
10
11
12
# coding:gbk
import time
import datetime

def _normalize_date(date_str):
# 兼容 '20261005' / '2026-10-05' / '2026/10/05' / '2026.10.05' 四种写法
s = str(date_str).strip().replace('-', '').replace('/', '').replace('.', '')
if len(s) != 8 or not s.isdigit():
raise ValueError(u"日期格式不正确: %r(应为 YYYYMMDD)" % date_str)
# 再用 strptime 做一次"真实日期"校验(如 20261332 会被拦截)
datetime.datetime.strptime(s, '%Y%m%d')
return s

片段 2:第 1 级 · 周末快速过滤

三行说完的事,但它是整个判断里性价比最高的一层:

1
2
3
4
5
def _is_weekend(date_str):
# Python 约定: weekday() 返回 0=周一 ... 5=周六, 6=周日
# >= 5 即周末。纯本地计算,不产生任何行情/数据接口调用
d = datetime.datetime.strptime(date_str, '%Y%m%d')
return d.weekday() >= 5

片段 3:第 2 级 · 交易日历查询与裁决

核心是接口调用 + 一个判断条件。注意 count 必须大于 0,period 用 '1d':

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def _get_calendar(C, end_date):
# get_trading_dates(股票代码, 开始日期, 结束日期, 个数, 周期)
# 开始日期传 '' 表示不启用;结束日期 = 要判断的日期;
# 个数取 N(建议 30 左右);周期 '1d' →
# 返回形如 ['20260914', '20260915', ...] 的字符串列表
return C.get_trading_dates('000001.SH', '', end_date, 30, '1d')


def _check_by_calendar(C, date_str):
dates = _get_calendar(C, date_str)
if not dates:
# 日历不可用(最常见原因:节假日数据未下载)。
# 保守返回 False,但必须打印,避免"静默误判"
print(u"交易日历不可用,请先下载『数据管理 → 节假日数据』")
return False
# 是交易日 → 列表最后一格恰好是它;不是 → 最后一格早于它
return dates[-1] == date_str

参考标的为什么写死 '000001.SH'(上证指数)而不是用”当前图代码”?因为沪深两市共享同一套交易日历,而指数历史最完整、永不退市,行为最确定。你要改成 '399001.SZ' 结果也一样。

片段 4:缓存 —— 思路比代码重要

get_trading_dates 单次调用并不慢,但盘中回调一天可能触发上千次。加缓存的原则很简单:

1
2
3
4
查询前:目标日期已在缓存里? → 直接返回
查询后:结果非空? → 存入缓存
结果为空? → 不缓存(留给用户"补下数据后立刻恢复"的机会)
缓存满了? → 整体清空(日历几乎不变,重查代价可忽略)

对应到代码就几行,用一个模块级字典即可:

1
2
3
4
5
6
7
8
9
10
11
12
_CALENDAR_CACHE = {}          # { 'YYYYMMDD': [该日期前最近 N 个交易日, ...] }

def _get_calendar_cached(C, end_date):
if end_date in _CALENDAR_CACHE: # 命中缓存,绝大多数调用停在这里
return _CALENDAR_CACHE[end_date]
dates = C.get_trading_dates('000001.SH', '', end_date, 30, '1d')
if not dates: # 空结果不缓存
return []
if len(_CALENDAR_CACHE) >= 50: # 上限保护,防内存增长
_CALENDAR_CACHE.clear()
_CALENDAR_CACHE[end_date] = dates
return dates

片段 5:把它们拼起来(判断入口的骨架)

1
2
3
4
5
6
7
8
9
10
def is_trading_day(C, date_str=''):
# 缺省 = QMT 所在机器的"今天"(部署机即北京时间)
if not date_str:
date_str = time.strftime('%Y%m%d')
date_str = _normalize_date(date_str) # 第 0 级

if _is_weekend(date_str): # 第 1 级:零成本早退
return False

return _check_by_calendar(C, date_str) # 第 2 级:日历裁决

到这里,核心逻辑已经完整了。你会发现它总共不到 40 行——难的不是写不出来,而是知道为什么这么排:为什么周末必须先判、为什么日历空结果要打印、为什么不传日期时要小心”今天是哪台机器的今天”。这三条都在 §5 里展开。


五、几个容易做错的设计点

5.1 值得留出的几个可配置项

自己组装的时候,建议把这些做成模块顶部的常量,别散落在函数里——以后改一处就够:

可配置项 建议默认值 作用 什么时候改
参考标的 000001.SH 查日历用的标的 基本不用改;沪深日历一致
周末快捷开关 开 周末零成本早退 需要严格逐日历裁决时可临时关闭
日历缓存天数 30 一次缓存多少个交易日 需要更长历史回看时调大
缓存容量上限 50 缓存多少个不同日期 长期运行的策略防内存增长

5.2 为什么周末判断要放在日历查询之前

反过来写也能跑,但代价是:每个周末触发的定时任务、每次周末启动的策略,都要白调一次日历接口。更重要的是语义清晰——周末不是交易日,这是不需要问任何人的确定性事实,本地能定的结论不要交给接口。

5.3 为什么日历为空时不能静默返回

is_trading_day 返回 False 有三种可能:周末、节假日、日历坏了。前两种是正常业务,第三种是环境故障。如果不打印提示,第三种会被静默吞掉——你以为今天是节假日,其实是数据没下载,这种误排查起来极其痛苦。所以正确做法是:保守返回 False + 显式打印原因。

5.4 不传日期时,”今天”是哪来的

缺省取的是 time.strftime('%Y%m%d')——QMT 所在机器的系统日期。两个注意点:

  1. 确保部署 QMT 的机器日期、时区正确(国内机器默认北京时间,一般无需处理)。
  2. 如果要判断的不是今天——比如收盘后判断”明天是不是交易日”,决定明早的定时任务要不要干活——显式传日期即可('YYYYMMDD'),其余判断流程完全相同。

5.5 关于”独立模块”

如果你希望它像工具库一样被复用,把上面这些片段存成一个独立文件(比如 trading_day.py)放在策略同目录,主策略里 import 即可——这也是我本地一直在用的组织方式,好处是以后每个新策略第一行就能用上它。这一步是工程习惯问题,留给你按自己的目录规范处理。


六、两种典型用法(实盘)

场景 1:策略启动时判断今天该不该干活

在 after_init 里调用,非交易日直接让策略待机:

1
2
3
4
5
6
def after_init(C):
# ⚠️ 不要放在 init 里:init 阶段交易日历接口尚未就绪
if is_trading_day(C):
print(u"今天是交易日,开始执行今日任务") # ← 你的启动逻辑
else:
print(u"今天不是交易日(周末或节假日),策略待机")

场景 2:定时任务回调先过交易日关

run_time 注册的定时回调,在周末和节假日照样会被触发,所以回调的第一件事就是判断:

1
2
3
4
5
6
7
8
9
def after_init(C):
# 注册每日 09:25 的回调:period='1nDay' = 每 1 天触发一次
C.run_time('my_daily_task', '1nDay', '2026-10-06 09:25:00')


def my_daily_task(C):
if not is_trading_day(C):
return # 非交易日直接返回
print(u"交易日 09:25 任务执行") # ← 你的开盘前任务

落地三步走

  1. 先下数据:QMT 界面 → 数据管理 → 节假日数据,确认已下载(每年年初记得更新)。这一步不做,后面全白搭。
  2. 先自测:单独写一个最小策略,在模拟模式下把 is_trading_day(C) 的返回值打印出来,逐个验证:一个已知交易日、一个周六、一个法定节假日,三个都对再往下走。
  3. 再接入:把你的判断函数嵌进真实策略的 after_init 和定时回调里。

建议给自己写一个自检函数:用同一个查询逻辑去查”今天”,如果返回空列表,说明日历不可用,打印一句明确提示。这一步能省掉你未来大量排查时间。


七、进阶:一个可扩展的方向

目前回答的是”是不是交易日”。如果你想进一步区分”今天是交易日,但现在是盘前 / 盘中 / 盘后”,可以在判断为交易日的前提下,用 time.localtime() 对当前时刻做区间判断(如 09:30–11:30、13:00–15:00 为盘中)。思路与本文一致:本地计算能解决的先本地解决,接口调用放在最后。这个留给你作为练习。


八、常见问题排查表

现象 最可能原因 解决办法
调用无结果 / 报接口错误 在 init 里调用了日历接口 移到 after_init / handlebar / run_time 回调中
法定节假日被判断为”交易日” 节假日数据未下载或过期 数据管理 → 节假日数据,下载并每年年初更新
交易日被判断为”非交易日”且有警告打印 日历不可用(数据未下载) 按打印提示下载节假日数据后重试
判断未来日期结果不对 本地日历未覆盖该未来日期 更新节假日数据;或推迟到临近再判断
自己写的模块 import 失败 模块与主策略不在同一目录 将两个文件放到同一策略目录
中文注释乱码 文件被保存为 UTF-8 用 GBK 编码重新保存,确认首行 # coding:gbk

九、免责声明(请务必完整阅读)

阅读本文,即视为你已完整阅读、理解并同意以下全部条款。

1. 性质声明:本文及文中示例代码片段仅供编程学习与技术研究使用,不构成任何投资建议、荐股意见或收益承诺。 证券市场有风险,任何交易决策及其后果均由使用者独立作出并独立承担。

2. 模拟测试要求(重要): 在将本文思路或代码片段用于实盘环境之前,你必须在 QMT「模拟信号模式」或「模拟柜台」中进行充分测试,验证逻辑、数据前提、异常处理均符合预期。本文作者/发布者无法也不承诺对你的特定环境进行测试。未经充分模拟验证直接接入实盘所导致的一切后果,由使用者自行承担全部责任。

3. 数据准确性声明: 本文涉及的交易日历数据来源于 QMT 客户端本地数据。节假日数据未下载、未及时更新、交易所临时调整休市安排、客户端故障等,均可能导致判断结果错误。使用者应自行建立对判断结果的复核机制,不得将判断结果作为交易决策的唯一依据。

4. 无担保声明: 本文内容与示例代码按”现状”提供,不附带任何明示或默示的担保,包括但不限于对适销性、特定用途适用性、不侵犯第三方权利的担保。作者/发布者不保证示例代码无缺陷、不中断、不出错,也不保证读者自行实现的版本能达到预期效果。

5. 责任限制: 在任何情况下,作者/发布者均不对因使用或无法使用本文内容、示例代码而产生的任何直接、间接、附带、特殊、惩罚性损失(包括但不限于交易亏损、机会损失、数据丢失、停机损失)承担责任。即使作者/发布者已被告知此类损失的可能性。

6. 内容定性声明确认: 本文及其中的示例代码属于计算机编程技术内容,不构成证券投资咨询或投资顾问服务。作者不提供代客理财、代办交易、跟单操作、个股推荐或买卖时机建议等任何服务。若你因此类需求联系作者,请另行咨询具备中国证监会业务许可的持牌机构。

7. 版权声明: 本文版权归作者所有。本文免费分享,欢迎个人学习使用,也欢迎注明出处后自由转发;但未经书面授权,不得用于商业培训、付费社群、二次售卖,或以原创名义转载。发现侵权行为,作者保留追究法律责任的权利。

8. 合规使用: 使用者承诺遵守国家法律法规及所在券商的相关规定,不得将本文内容用于任何违法违规用途。


十、写在最后

交易日判断是策略体系里最不起眼、却每天都在被调用的一块地基。它写对的标志是你感觉不到它的存在;它写错的标志,是某个周末早上你收到一堆莫名其妙的报错。

核心逻辑不到 40 行,希望这篇文章给你的是”为什么这么排”,而不只是”复制哪一段”。把这段逻辑收进你自己的策略工具箱,以后每个新策略,第一行就是它。

⚠️ 最后再强调一次:任何代码在实盘使用前,请务必在模拟模式下充分测试。 市场有风险,交易需谨慎。