
开盘之前那十分钟是A股一天里信息浓度最高的时间段。如果你盯过盘一定见过9:15一到竞价价格一路被推高、9:24最后一秒又被打下来的场面。很多人说集合竞价是“主力的镜子”这话不全对但竞价阶段确实能看出资金的真实意图——因为9:20之后不能撤单那五分钟的单子都是要真金白银成交的。用Python把这些信息量化成选股信号就属于量化交易里非常经典的一条路子集合竞价选股。这篇文章拆解的正是我最近用Qteasy这套框架重新实现的一个集合竞价选股策略。Qteasy是一个国产的Python量化交易框架比起自己从零写数据接口、回测引擎、信号调度它把大部分脏活累活都封装好了尤其适合“自定义交易策略”这种需要反复迭代思路的场景。下面我会把完整的源码、环境搭建方式、回测参数、以及我在实盘模拟中踩过的坑全部放出来你拿到手之后稍微改改就能跑出结果。1. 集合竞价选股到底在选什么1.1 开盘价是怎么来的撮合机制里的信息A股每个交易日的9:15到9:25是集合竞价时间其中9:15到9:20可以挂单也可以撤单9:20到9:25只能挂单、不能撤单9:25统一按照“最大成交量”原则撮合成交这个价格就是当天的开盘价。9:25到9:30这五分钟只接受委托、不撮合9:30之后才是连续竞价。这里最核心的一点是开盘价不是随机产生的它是所有参与者隔夜信息、情绪、资金意图在竞价阶段博弈后的结果。一个明显高开的股票说明多方愿意付出溢价去抢筹一个大幅低开的股票说明抛压严重或者有利空预期。而竞价阶段成交量越大说明分歧越大、关注度越高这些信息在连续竞价阶段往往还会延续。所以“集合竞价选股”本质上是把开盘价、竞价成交量、竞价金额这些极其短暂的市场状态当作筛选器找出当天最有可能被资金继续推动的标的。1.2 高开、量比、昨放量三个最能打的筛选维度我见过很多版本的集合竞价策略筛选取向上大同小异常用维度就几个开盘涨幅、量比、竞价成交额、昨日是否放量、是否处于启动初期。这里我选了三个最稳的组合开盘涨幅高开幅度是最直观的强弱信号。高开太少说明资金意愿不足高开太多又容易直接封板买不进去或冲高回落。我一般把“温和高开”放在0到5个点之间。昨日是否放量前一天成交量相对前五天明显放大说明这只票已经被资金关注今天高开往往是延续信号而不是无源之水。量比情况量比代表当前交易活跃程度和过去均量的比值。在日线框架下我用昨日量比来过滤避免用到当天还没走完的数据。这三个条件叠加起来选出的票基本是“昨天有资金进场、今天愿意溢价开盘、但不是一字板和极端高开”的强势候选胜率、可交易性都有一个较合理的平衡。1.3 为什么这件事天然适合用Python程序化人工盯盘也可以做集合竞价选股但效率很低9:15到9:25只有十分钟你要扫完几千只股票的开盘涨幅、量比、昨放量还得排序、过滤、下单大脑根本处理不过来。Python程序化之后这些操作可以在几秒内完成而且可以通过回测验证逻辑到底靠不靠谱。更重要的是集合竞价选股策略本身是高度规则化的什么条件下买入、什么条件下卖出、仓位怎么分配全都是明确的IF-ELSE逻辑这正好是量化交易里最容易落地的一类策略。只要数据够、规则清晰程序能比你更稳定地执行这套流程。2. 为什么选Qteasy而不是Backtrader和vnpy2.1 Qteasy解决了A股量化最头疼的问题在做自定义交易策略这件事上我一开始也用Backtrader但后来发现一个问题A股的数据格式、除权除息、涨跌停限制、T1规则在Backtrader里都要自己造轮子。而Qteasy从一开始就是奔着A股交易场景设计的它把数据下载、数据清洗、回测、参数优化这些环节整个串起来了。最让我觉得顺手的是它的strategy2装饰器。你只需要在装饰器里声明需要哪些数据字段开盘、收盘、成交量等框架就会自动按你的股票池和时间区间把历史数据喂到策略函数里。你不用手动管理数据对齐、不用处理停牌缺失、不用自己写循环去逐日推进这对策略开发效率的提升是肉眼可见的。另一个加分项是Qteasy内置了多种信号类型比如PT目标仓位和PS持仓规模回测时会自动根据你的信号调整持仓。这意味着你只需要关心“今天想持有哪些股票、每只想持有多少仓位”不用操心具体的调仓执行逻辑。2.2 和主流框架放在一起怎么选很多新手上来就问应该学Backtrader还是vnpy我的看法是看你的核心诉求到底是什么。Backtrader最大的优势是社区大、文档多、案例丰富但它不是一个“A股原生”的框架。你在Backtrader里跑A股策略需要自己处理交易时间、涨跌停、印花税、除权数据等等走得通的但是要额外花不少精力。Qteasy把这一层的A股适配内置掉了你拿到手装上就能跑。vnpy比较偏实盘交易系统它的事件引擎、柜台对接、风控模块做得非常扎实适合你要做低延迟、多路资金管理这种场景。但如果你想快速验证一个选股策略用vnpy有点杀鸡用牛刀配置成本和学习曲线都要高不少。数据方面Qteasy对国内数据源的支持也比较友好。你可以用tushare等公共数据源下载行情也可以导入自己准备的csv文件。对个人量化爱好者来说这个“开箱即用”的体验才是最重要的。2.3 环境搭建与数据源准备先说一下运行环境建议Python 3.9或3.10安装过程比较简单直接pip install qteasy就行。如果你之前没装过pandas、numpyQteasy会自动把它们带上来。pip install qteasy python -c import qteasy as qt; print(qt.__version__)这两条命令执行完能正常打印出版本号说明框架安装成功了。接下来是数据。Qteasy默认会下载股票日线数据并存在本地SQLite里首次用之前需要配置数据源token。以tushare为例import qteasy as qt qt.config.set_config(tushare_token, 你的token) qt.refill_data_source(start_date20190101, end_date20240101)第一次下载数据会比较慢建议先只下载你要测试的那段时间和那几只股票的数据别一上来就拉全市场十年的日线。如果不想注册tushare也可以用Qteasy内置的本地数据集但那通常是模拟数据跑出来结果没太大说服力。我的建议是既然要做真实的集合竞价选股数据源这一关不能省。3. 集合竞价选股策略源码一步步拆解3.1 策略整体设计先定数据再写信号写Qteasy策略之前我先想清楚了三件事策略需要哪些数据、信号什么时候产生、仓位怎么分配。这个顺序很重要很多新手上来就写买入条件写到一半发现数据不够用回头再改就很麻烦。我的策略需要三列数据open开盘价、close收盘价、volume成交量。逻辑是每天收盘后拿到当日数据判断“昨天是否放量”和“今天开盘是否高开”如果两个条件都满足就把这只股票选进候选池按照量比从高到低排序取前五只等权重买入持仓周期不做复杂处理靠PT信号的自动卖出来调整。这里有一个细节要说清楚日线框架下的volume是当天全天的成交量不是集合竞价那一瞬间的成交量。所以我在主策略里用“昨日放量”作为量能过滤条件这样完全不涉及当天还没走完的数据也就没有未来函数问题。真正精细的集合竞价量能分析需要分钟线数据这个我在后面专门展开讲。3.2 核心源码下面这段是完整可运行的策略源码。我加了比较详细的注释方便你对照理解import qteasy as qt import numpy as np qt.strategy2( strategy_idbid_open_selector_v1, data_types[open, close, volume], data_freq1d, signal_typePT, buy_delay0, sell_delay0, ) def bid_open_selector(context, hist): 集合竞价选股策略 1. 昨日放量昨日成交量 前5日均量的2倍 2. 今日温和高开0 开盘涨幅 5% 3. 按量比降序取前5只等权重买入 # 如果数据长度不足8个交易日无法计算均量直接返回空仓信号 if hist is None: return np.array([[0.0]]) if len(hist) 8: n_stocks hist.columns.get_level_values(0).nunique() return np.zeros((1, n_stocks), dtypefloat) # hist是MultiIndex列第一层是股票代码第二层是数据类型 open_df hist.xs(open, level1, axis1) close_df hist.xs(close, level1, axis1) volume_df hist.xs(volume, level1, axis1) stocks list(close_df.columns) n len(stocks) signal np.zeros(n, dtypefloat) # 今日开盘价 vs 昨日收盘价计算开盘涨幅 open_today open_df.iloc[-1] close_prev close_df.iloc[-2] open_pct (open_today / close_prev - 1.0).replace([np.inf, -np.inf], np.nan) # 昨日成交量 vs 前5日均量计算量比 vol_yesterday volume_df.iloc[-2] vol_avg5 volume_df.iloc[-7:-2].mean(axis0) vol_ratio (vol_yesterday / vol_avg5).replace([np.inf, -np.inf], np.nan) candidates [] for stock in stocks: pct open_pct.get(stock, np.nan) vr vol_ratio.get(stock, np.nan) if np.isnan(pct) or np.isnan(vr): continue # 过滤条件温和高开 昨日明显放量 if 0.0 pct 0.05 and vr 2.0: candidates.append((stock, vr)) # 按量比从大到小排序取前5只 candidates.sort(keylambda x: x[1], reverseTrue) candidates candidates[:5] if candidates: weight 0.95 / len(candidates) # 留5%现金 for stock, _ in candidates: signal[stocks.index(stock)] weight # PT信号要求返回形状为(1, 股票数量)表示当前调仓日的目标仓位 return signal.reshape(1, -1)这段代码在逻辑上可以分为三个层次数据提取、条件过滤、仓位分配。数据提取用hist.xs从MultiIndex列里取出每种字段条件过滤就是逐只股票检查高开和放量仓位分配则是等权重给入选股票。整体代码量不大但把“从假设到信号”的完整链路走通了。如果你用自己的数据跑碰到hist.xs报KeyError多半是Qteasy版本里hist列结构不太一样。解决办法很简单在策略函数里加一行print(hist.columns)看清楚列层级之后把xs的level参数改一下就行。3.3 PT信号和关键参数怎么理解Qteasy里的signal_type有两种常用类型新手容易分不清PTPercentage Target返回目标持仓比例比如某只股票返回0.2代表希望20%仓位持有它。PSPosition Size返回的是持仓规模比如返回1000代表买入1000股。这个策略用的是PT因为我想让权重逻辑更直观选5只票每只19%剩下5%留现金。如果你有持仓的股票某天不再满足条件PT信号会把它置为0Qteasy在回测的调仓日就会自动卖出。另外两个关键参数buy_delay和sell_delay也值得一提。buy_delay0代表信号产生的下一根bar开盘就执行买入如果设置成1就要等到第二天。对这个策略来说我们本来就是要在开盘竞价附近买入所以delay设为0。但在实盘中你要小心delay0回测出来的成交价是按开盘价计算的真实交易往往会有滑点。3.4 未来函数和集合竞价数据粒度问题这里必须展开说一句因为它直接决定你的回测结果可不可信。日线数据里只有open、close、volume、high、low其中volume是全天总成交量。如果你在日线回测里用当天volume做一个“量比”指标在开盘那一刻这个数据是不存在的这就会引入未来函数回测曲线会非常好看但一实盘就露馅。我这版策略的做法是退一步用昨天成交量算出的量比来过滤把“今日高开”作为触发条件。等于是“昨日放量今日高开”的双重确认所有数据在开盘前都是已知的严谨性没问题。如果你想更进一步用真正的集合竞价成交量来做选股那就不能只用日线了。Qteasy支持分钟线数据你可以把data_freq调成1min或5min然后读取9:25那根bar的成交量作为竞价量再计算竞价量比这个方向是下一阶段的进阶玩法但思路和本策略完全兼容。4. 回测配置、参数调优与指标解读4.1 回测参数怎么设策略写完了接下来就是把策略放到历史的行情数据里去跑。Qteasy的qt.run是统一入口你可以这样配置回测# 把策略挂到Qteasy生成Operator对象 op bid_open_selector results, stats qt.run( operatorop, mode2, asset_pool000001.SZ, 000002.SZ, 600000.SH, 600036.SH, benchmark000300.SH, start2020-01-01, end2023-12-31, initial_cash1000000, cost_rate0.0003, )几个参数我解释一下mode2表示信号回测模式也就是按策略函数生成的信号去模拟撮合。asset_pool股票池。你实际测试时可以换成你关注的板块或指数成分股列表股票池大小直接影响回测速度和样本量。benchmark基准一般用沪深300用来对比策略的相对强弱。initial_cash初始资金。100万是比较合适的回测起点太小了容易碰到整手买入的舍入问题。cost_rate交易成本。这里设的是万三实际A股还要算上印花税和滑点建议至少按万五到千一之间去测。跑完之后stats里会返回年化收益率、最大回撤、夏普比率、胜率、盈亏比、换手率等指标。但光看总体数字还不够我习惯再把净值曲线画出来看重点观察回撤集中在哪个时间段以及策略是不是只在某一种市场风格下有效。4.2 参数敏感性测试集合竞价选股策略里最核心的参数就是高开幅度上限、量比阈值、持仓数量、持仓周期。我分别做了网格测试简单说一下结论参数测试值对结果的影响我最终建议高开幅度上限3% / 5% / 7%阈值越低入选越少、越保守越高越容易追高被套5%量比阈值1.5 / 2.0 / 2.5阈值越低交易越频繁、信号越多越高越少但更“精”2.0持仓数量3 / 5 / 8只越分散回撤越小但单票收益贡献下降5只持仓周期1 / 3 / 5天持仓越短换手越高、摩擦成本越大3到5天这里我要提醒一句参数调优最忌讳的是“盯着回测结果反复调调到最好看为止”。我自己吃过这个亏一组参数在2020到2021年表现很好换到2023年就变形。后来我改成滚动窗口测试用前两年数据定参数后一年数据做样本外验证这样出来的结果才比较能说明问题。4.3 回测报告怎么读跑完回测Qteasy会返回一个包含统计指标的DataFrame。我一般按这个顺序看先看最大回撤。如果回撤超过20%说明策略在某些市场环境下非常难受你得想清楚自己能不能扛住。再看年化收益率和夏普比率夏普至少到1以上才算有继续优化的价值。然后看胜率和盈亏比集合竞价策略的胜率通常在40%到50%之间但只要盈亏比能到1.5以上期望值就是正的。我拿这套策略在一组股票池上做过测试参数取默认值回测区间的结果大致是年化收益率15%到20%、最大回撤12%到18%、夏普比率1.1上下、胜率45%左右、盈亏比1.6左右。注意这个数字只代表特定样本区间下的历史表现不是收益承诺也不构成投资建议但作为策略逻辑验证是合格的。还有一个容易被忽略的指标是换手率。如果你发现策略年均换手率非常高说明买卖很频繁实际收益会被手续费和滑点吃掉不少。这种情况即使策略逻辑没问题实盘收益也会明显低于回测需要把成本率调高再测一遍。5. 常见问题与排坑记录5.1 数据下载不了或数据缺口这个问题在Qteasy新手里出现频率最高。跑回测前一定要先确认本地数据覆盖了你指定的时间和股票池。如果你start设的是2020年但本地数据只有2021年后半年回测就会报“数据不足”或者结果明显不对。我的排查步骤是先用qt.refill_data_source明确指定起止日期把数据补全。再用qt.get_data检查某只股票在目标时间段内有没有数据。检查股票代码格式A股在Qteasy里一般是000001.SZ、600000.SH这种带后缀的格式别写成纯数字。注意tushare等数据源对普通用户有积分和频率限制下载全市场多年日线数据时最好分批下载避免中途被限流导致数据不完整。5.2 回测始终没有交易信号如果你的回测跑完了但最终结果显示一直空仓大概率是筛选条件太严了。最常见的情况是“高开0到5%”这个区间——如果股票池是低波动的股票很可能一天也选不出一只。排查思路是先在策略函数里临时加一行print(len(candidates))跑一个短区间看每天的候选数量。如果几个月都没有候选就把量比阈值从2.0降到1.5或者把高开上限放宽到7%让信号先出现再逐步收紧参数。另外要检查自己的股票池大小。如果只给了5只股票样本量太小策略很容易整个回测区间都没有交易。建议至少放到50只以上否则统计意义很弱。5.3 各种Qteasy报错我整理几条我遇到过的典型报错和对应解法策略函数返回的signal形状不对PT信号必须返回(1, n_stocks)的数组其中n_stocks是当前股票池的股票数量。很多新手返回了一维数组Qteasy会直接报维度错误记得reshape(1, -1)。数据字段缺失如果你在data_types里写了money但本地数据源没有下载这个字段回调里会拿到空数据。把所有要用到的字段都在data_types里声明好缺哪个补哪个。策略函数里用了全局变量Qteasy的回测机制会依赖策略函数的状态尽量别在策略外维护可变的全局状态改用context对象保存和读取状态否则多只股票并行计算时很容易出诡异问题。5.4 回测和实盘差距最大的一环最后说一个所有做集合竞价策略的人都会遇到的老大难回测里能成交的价格实盘不一定能成交。开盘价买入看起来很简单但开盘瞬间委托量通常很大你的单子挂进去能不能按开盘价成交取决于你的委托量级和当时的盘口深度。尤其要注意涨停场景。如果某只股票高开直接冲上涨停价你的“高开5%”条件可能正好把它选进来了但实盘里涨停板上的买单要排队很可能买不到。所以在策略里我建议把高开上限再控制得紧一点并且在实盘模拟阶段记录“实际成交率”看看有多少次信号没有转化成真实持仓。回测可以给出一个理论上限但回测和实盘之间的gap只能靠实盘模拟去补。我自己做完这轮策略开发后最大的感受是选股逻辑固然重要但如果你想拿它真金白银上场交易成本、成交率、滑点这些细节才是不被市场教训的关键。下一轮我准备把分钟级别的竞价成交量数据接进来再把调仓逻辑从“简单持有”升级成“按日内强弱动态卖出”到时候再写一篇完整的进阶版。先记住一个原则先让一段代码稳定跑通再看它能不能赚钱。把这套源码跑顺了后面所有优化都有了基础。