做盘中监控的兄弟应该都有体会早上九点半一开盘你想快速搞清楚今天全市场哪些票封死了涨停只靠眼睛盯行情软件翻板块翻完第一屏前面几只早就开板了。这个需求我最早是手工处理后来痛定思痛直接用程序把沪深A股全市场五千多只股票的最新行情一次性拉下来实时计算涨跌幅再按不同板块的涨跌停规则筛出涨停股。整套流程跑顺之后从开盘到拿到第一版涨停池基本三秒内完成。这整套思路依托的是某券商提供的MiniQMT量化交易客户端它带了一套本地化Python行情接口能在自己机器上直接拉全市场快照数据。MiniQMT这个方案的好处在于行情数据不是从网页接口爬来的而是从官方终端本地服务读取稳定性好、字段齐全且支持个股批量获取。适合做盘中实时扫描、板块异动监控、涨停股筛选这一类数据量大的活。文章本身不构成任何投资建议只讨论技术和工程实现。下面我会把从环境准备、接口调用、涨跌停规则处理到最终筛选涨停池的完整过程都拆开讲一遍尤其是字段单位、板块阈值、浮点误差这些容易踩坑的细节都会单独拿出来说。不管是刚接触MiniQMT的新手还是已经跑过一些策略的开发者应该都能在里面找到有用的东西。1. 需求拆解与方案选型为什么要走全量快照这条路1.1 这个需求的真实使用场景我最早遇到这个需求是想做一个盘中自动提醒每过几秒扫一遍全市场一旦出现新的涨停股马上弹窗并把代码、名称、当前涨幅整理出来。这和“打开行情软件手动翻涨幅榜”是完全不同的玩法因为人工翻榜有严重的滞后尤其涨停潮来得猛的时候等你翻完一圈最早的几只可能已经炸板。程序化扫描解决的核心问题有三个第一是覆盖全沪深A股全部纳入不漏票第二是速度快全市场拉一遍数据要控制在几秒内第三是规则准不同板块、不同股票状态的涨停阈值不一样不能一刀切用10%。这三个要求决定了后面所有的技术选型。1.2 MiniQMT与普通行情方案的区别在哪里市面上获取A股行情的路径不少但要做全市场扫描大部分方案都有明显的短板。免费的HTTP行情接口通常限制请求频率一次只能查一只或少数几只股票要把五千多只票全部刷一遍光是次数限制就能把你卡死。网页爬虫则面临数据格式不稳定、频率限制、页面改版等问题而且跑到一半断掉是家常便饭。券商客户端手动导出的方式更不现实只能作为事后数据核对。MiniQMT这种本地终端方案逻辑上相当于在你自己电脑上跑了一个行情服务Python代码直接向本地服务要数据。它最大的特点就是支持一次性传入几千个股票代码然后批量返回这些股票的最新快照。一次调用拿到全市场行情这正好是实时扫描场景最需要的。另外本地服务的数据频次和字段完整度也比普通网页接口高盘中盯盘足够了。1.3 全量快照与逐票查询的性能差异很多人一开始习惯写循环一只一只查行情这在股票数量少的时候没问题但全市场场景下绝对不能这么干。假设一只股票查询耗时0.1秒五千只就是500秒等查完最后一票第一票的数据早就过时了扫描程序变成“历史回放”完全失去实时性。MiniQMT的全量快照接口本质上是本地终端已经维护了一份全市场的最新行情表代码只是把这张表整体取回来。一次网络传输几MB数据耗时基本在几百毫秒到一两秒之间。速度提升来自“本地持有数据”这个架构而不是你的代码写得有多高效。所以方案选型时不管接口长什么样核心原则就一条能用一条批量指令拿全量绝不写循环逐票拉取。2. 动手前的核心认知行情数据与涨跌停规则2.1 行情快照里到底有哪些关键字段在写第一行代码之前先要搞清楚MiniQMT行情接口返回的每一笔数据里有什么。从实用角度全市场筛选涨停股只需要少量字段股票代码、最新价格、昨收价格、最高价、股票名称、时间戳。但行情快照实际返回的字段远不止这些还包括开盘价、成交量、成交额、换手率、买卖盘口等。不同版本和环境下返回的数据结构可能是一个对象也可能是一个字典。第一次拿到数据不要急着写逻辑先直接打印一只股票的完整返回把字段名和值看清楚。我最开始就吃过这个亏想当然用了其中一个字段名结果取出来是None代码跑半天全在摆烂。先打印原始数据是最笨也最稳妥的排查方式。2.2 价格字段的单位陷阱数值不是你想的那么简单这是MiniQMT新手最容易踩的坑没有之一。行情接口返回的价格字段很多情况下并不是以“元”为单位的浮点数而是放大过后的整数。常见的情况是价格放大了10000倍比如最新价11.23元接口返回的是112300。有些字段可能放大倍数不同甚至有的版本直接返回浮点元不同环境差异很大。处理这个问题的标准做法是拿到一只股票的数据后先对比一下它的现价和行情软件上的显示反推出缩放倍数。我的习惯是写一个价格格式化函数把原始值除以对应的缩放倍数再进入后续计算这样即使以后换了环境只需要改一个参数。调试阶段多花五分钟确认单位能避免后续所有筛选结果出现量级错误。2.3 A股涨跌幅限制到底怎么分A股市场不是所有股票都用同一个涨跌幅。主板、创业板、科创板、北交所、ST股、新股规则都不一样。筛选涨停股如果不区分这些情况结果肯定不准。我列一个常用的判断逻辑表股票类型代码前缀示例涨跌幅限制沪市主板600、601、603、60510%深市主板000、001、002、00310%创业板300、30120%科创板688、68920%北交所8开头、4开头、920等30%ST、*ST股名称含“ST”字样5%新股上市首日或部分次新无涨跌幅限制或特殊规则不适用这个表是整个筛选逻辑的地基。实际编码时先用代码前缀判断板块再判断名称里是否带ST标记两个条件叠加才能决定阈值。北交所的代码规则在不同时期有调整如果你的股票清单来源里包含北交所一定要单独处理别让它掉进默认的10%分支里。2.4 涨停价的计算与判断口径涨停价不是简单用“昨收乘以1.1”就行它有一个按最小价格变动单位取整的规则。A股股票价格最小变动单位是0.01元所以涨停价的计算公式是涨停价 四舍五入(昨收价 × (1 涨跌幅限制比例), 到分)。比如昨收10元、10%限制涨停价是11.00元昨收10.05元、10%限制精确值是11.055元取整到分就是11.06元。判断一只股票是否涨停就是把最新价和计算出的涨停价做比较但这里有个浮点数精度问题。Python里浮点运算可能出现10.9999999这种结果直接比较相等会失败。我判断时习惯加一个极小容差最新价 涨停价 - 0.001 就认为达到了涨停价。如果要区分“封死涨停”和“曾经触板”还要结合最高价字段判断这个后面的代码里会展示。3. 实操第一步拿到沪深A股股票清单3.1 用板块接口获取全量代码筛选涨停股的第一步是把沪深A股的全部股票代码拿到手。MiniQMT提供了板块相关的接口可以获取某个板块下的所有股票代码。常见写法是传入板块名称返回一个字符串列表。from xtquant import xtdata stock_list xtdata.get_stock_list_in_sector(沪深A股) print(股票总数:, len(stock_list)) print(stock_list[:20])不同环境下板块名称可能不完全一样有的叫“沪深A股”有的叫“全部A股”实际以客户端支持列表为准。如果拿到的板块列表为空可以先用接口查看当前支持哪些板块名称再从中挑一个数据范围正确、包含沪深两市的板块。第一次跑通这一步整个项目就完成了一半。3.2 清洗清单剔除不参与正常涨跌停的股票拿到的股票清单不能直接用它里面可能混着停牌股、退市整理股、上市首日新股这些股票要么没有正常行情要么涨跌幅规则特殊直接混进筛选会污染结果。建议在进入行情拉取前先做一层清洗。从代码前缀上先明确自己需要哪些市场。沪深A股通常已经排除北交所但还是要检查一遍别拿默认结果赌。停牌股可以靠成交量字段过滤成交量长时间为零的股票直接剔除因为停牌股即使最新价还在也不存在封板与否的意义。上市首日和次新股要单独处理它们的涨跌幅限制可能放开不能套用常规阈值。清洗阶段不用做太复杂的过滤目标是保证进入筛选的每一只股票都适用于“主板10%、创业板科创板20%、ST 5%”这套规则。我习惯先把清单按代码前缀分组打印出各分组数量确认数据范围没有异常后再继续。4. 实操第二步拉取全市场实时快照4.1 一次性批量拉取的正确姿势拿到股票清单后核心操作就是用批量接口拉全市场行情快照。MiniQMT里通常是调用get_full_tick把整个股票代码列表一次性传进去返回一个以代码为键、行情数据为值的字典。代码如下tick_dict xtdata.get_full_tick(stock_list) print(返回数据条数:, len(tick_dict)) # 取一只股票看看原始数据结构 sample_code stock_list[0] sample_tick tick_dict.get(sample_code) print(sample_code, sample_tick)这一步跑得顺不顺利取决于本地行情服务是否在线。MiniQMT程序要保持在登录状态本地服务断开的话获取到的数据可能为空或者长时间不更新。返回字典的长度通常应该和传入的股票数量差不多如果差太多优先排查连接状态而不是怀疑代码写错。4.2 实时轮询的间隔怎么选全市场扫描不可能只跑一次盘中需要循环刷新。循环逻辑本身不复杂关键是轮询间隔怎么定。间隔太短本地服务和网络带宽压力大而且行情数据本身有频率上限拉再快也是同一份数据间隔太长又会错过快速上板的过程。我实测下来全市场扫描的间隔设在3到5秒比较合理。这个频率足以捕捉大多数涨停异动又不会让自己电脑的资源占用高得离谱。完整轮询可以用while循环加time.sleep实现注意在循环里做异常捕获避免某个时刻数据异常导致程序直接退出。import time from datetime import datetime while True: try: tick_dict xtdata.get_full_tick(stock_list) now datetime.now().strftime(%H:%M:%S) print([{0}] 获取行情 {1} 条.format(now, len(tick_dict))) # 这里先只打印数据量确认轮询稳定后再加筛选逻辑 except Exception as e: print(轮询异常:, e) time.sleep(3)4.3 数据有效性校验在全市场轮询中数据有效性校验不能省。有的股票因为停牌或数据异常最新价可能是0或者昨收为0这些数据进入涨跌幅计算会直接报错或产生荒谬结果。我在计算前会加几个基础判断最新价必须大于0昨收必须大于0没有有效昨收的股票谈不上涨停。还要留意行情时间戳如果拉到的数据还是昨天收盘的盘中扫描就完全失真了。校验逻辑看起来简单但正是这些细节决定了程序在真实交易时间里能不能稳定跑完一整天。5. 实操第三步计算涨跌幅并筛选涨停股5.1 涨跌幅的计算与展示计算涨跌幅本身不复杂公式就是“最新价相对于昨收的百分比变化”但要注意浮点数精度和除零保护。代码如下def calc_change_pct(last_price, last_close): if last_close 0: return 0.0 return (last_price - last_close) / last_close * 100计算结果建议保留两位小数方便打印和保存。这里有一个值得注意的点有些时候我们看到的软件涨幅是四舍五入后的但筛选用的是未取整的精确值两者在极少数情况下会有差异。判断涨停时直接用原始浮点数计算展示时才做格式化不要为了好看影响判断逻辑。5.2 分板块涨停阈值判断函数这一段是整个筛选逻辑的心脏。把前面讲的板块规则、ST判断、涨停价计算全部放进一个独立函数里输入一只股票的代码、名称、最新价、昨收输出它是否涨停。代码如下def is_limit_up(code, name, last_price, last_close): # 有效价格判断 if last_price 0 or last_close 0: return False # 判断 ST限制幅度 5% upper_name name.upper() if ST in upper_name: limit_pct 5 # 创业板和科创板限制幅度 20% elif code.startswith((300, 301, 688, 689)): limit_pct 20 # 沪深主板限制幅度 10% elif code.startswith((600, 601, 603, 605, 000, 001, 002, 003)): limit_pct 10 else: # 北交所等默认给 30%按需调整 limit_pct 30 # 计算涨停价四舍五入到分 limit_price round(last_close * (1 limit_pct / 100), 2) # 考虑浮点误差最新价达到涨停价即判定 if last_price limit_price - 0.001: return True return False这段代码有一个隐藏细节涨停价的计算用了Python内置的round函数取两位小数。A股计价最小单位是分确定涨停价时必须按交易规则取整如果直接用浮点数比较遇到“10.055元”这种价格判断结果会出偏差。这些交易规则层面的细节比接口调用本身更影响筛选结果的准确性。5.3 全市场扫描并输出涨停池把前面所有环节组装起来完整的全市场扫描逻辑就出来了。遍历行情字典逐票计算涨跌幅和涨停状态符合条件就收进涨停池def scan_limit_up_pool(stock_list): tick_dict xtdata.get_full_tick(stock_list) limit_up_pool [] for code, tick in tick_dict.items(): name tick.get(name, ) # 根据环境确认字段名和缩放倍数 last_price tick.get(lastPrice, 0) / 10000 last_close tick.get(lastClose, 0) / 10000 change_pct calc_change_pct(last_price, last_close) if is_limit_up(code, name, last_price, last_close): limit_up_pool.append((code, name, round(change_pct, 2))) # 按涨幅降序排列 limit_up_pool.sort(keylambda x: x[2], reverseTrue) return limit_up_pool返回结果后打印或者保存就非常方便了。我通常会把涨停池按涨幅排个序反正都是涨停涨幅高低代表的是封板先后和封单强度对当日复盘很有参考价值。6. 常见问题与排查技巧实录6.1 股票列表返回空或者数量不对这个情况我在初期遇到过不止一次。板块名称传错、本地服务没初始化、客户端版本不同都可能导致返回空列表。排查顺序建议这样走先确认客户端已经正常登录并连接行情再用接口查看当前环境支持的板块名称列表逐个试直到找到能返回完整沪深A股清单的板块。记住数据初始化阶段宁可多打印两行日志不要在后续流程里猜测问题根源。6.2 涨停股筛不出来或者老是误报筛选结果不对九成问题出在规则判断而不是数据接口上。检查顺序如下有没有按代码前缀区分不同板块的涨跌幅限制有没有处理ST股涨停价有没有按0.01元取整比较时有没有留浮点误差的余量。还有一个隐蔽问题行情接口返回的昨收字段可能是调整过的昨收而不是普通昨收数据口径不同会让计算基准完全变掉。一旦结果和行情软件对不上先用一两只明显的涨停股和跌停股做追查把中间量全部打印出来一步就能定位问题。6.3 程序跑一段时间后突然崩掉或卡死全市场轮询程序长时间运行最容易出问题的点有三个本地行情连接断开、内存持续增长、日志刷太多把磁盘写满。连接断开后get_full_tick可能返回空字典而不是抛异常程序还在一本正经地“扫描空数据”。我习惯在每个轮询周期检查返回条数如果连续几次明显少于正常值就主动重新初始化连接并告警。日志方面盘中只打印摘要和异常不要每个周期打印五千条明细否则磁盘很快就满了这就是个纯工程问题很烦但不难解决。6.4 行情数据更新不及时或明显滞后有时候程序没报错但晚出来的涨停池明显比行情软件慢半拍这种滞后很可能不是网络问题而是本地行情服务的数据源本身没有实时刷新。这跟客户端的数据订阅模式有关不同环境下快照数据的推送频率并不一样。遇到这种情况我会先确认客户端的行情模式设置再检查自己轮询间隔是否太短以至于拿到的都是缓存数据。把间隔调到3秒以上多数情况下能减轻这个问题。但要注意某些环境下实时快照的刷新机制和订阅个股的Level-1推送不是同一套路径不要用订阅个股的逻辑去套全市场快照。6.5 收盘后的数据复用与落盘盘中筛选出来的涨停池如果直接丢弃等于浪费了一份很有价值的复盘数据。我建议每轮扫描都把结果追加写入本地CSV文件格式很简单时间戳、涨停数量、涨停股票代码列表。一天跑下来你得到的不只是当时看的那一眼而是一整天的涨停演化轨迹。收盘后可以回看哪些票在哪个时间点封板中途有没有开板哪个时段涨停家数激增这些数据对后续复盘帮助非常大。def save_pool_to_csv(now, limit_up_pool): codes ;.join([item[0] for item in limit_up_pool]) with open(limit_up_pool.csv, a, encodingutf-8) as f: f.write({0},{1},{2}\n.format(now, len(limit_up_pool), codes))这个做法成本极低但会让你的扫描程序从“看一眼的工具”升级成“能留痕的记录仪”。7. 我的实操体会与几个扩展方向个人用下来的体会是全市场涨停扫描的核心从来不只是“把数据拉下来”而是把交易规则准确地翻译成代码判断逻辑。两个我最初没想到的问题实际占用了大量调试时间一是价格字段的缩放单位不统一二是涨停价按分取整导致判断误差。这两个问题解决了整个筛选逻辑的准确度会一下子拉起来。调试阶段建议先不要急着盯涨停池而是把程序挂在那里跑一个上午日志全开让它记录每次扫描到的涨停数量。收盘后跟行情软件的历史记录做对比确认准确率足够高再慢慢加上弹窗提醒、语音播报、联动下单这类功能。直接拿未经验证的筛选结果去驱动自动交易风险太大了。这个项目后面的扩展空间也不小。可以给涨停池加上“封单金额”字段判断封板强度区分一字板和尾盘偷袭可以把当天涨停池和昨日涨停池做对比统计晋级率还可以把涨停股票按概念板块归类识别当天资金炒作的主线方向。MiniQMT的本地行情能力足够支撑这些玩法只要你把第一版全市场扫描跑稳了后面都是叠加功能的事。