每天收盘后强迫自己翻一遍全市场的股票池,是我以前最容易拖延的一件事。几千只标的挨个看K线、对比量能、翻公告,做完基本就到深夜了,而且今天看完明天全忘,筛选标准全凭当时的情绪。后来我干脆用Python写了一套自动化筛选潜力股的工具:爬虫负责把公开的行情数据抓下来,pandas负责清洗和计算指标,最后按照我自己的规则输出一份候选名单。这套流程稳定跑了几个月,它不保证选出的一定涨,但至少把我从“凭感觉盯盘”里彻底解放了出来,每天收盘后十分钟就能拿到一份客观、可复现的筛选报告。这篇文章就把这套系统的完整思路、踩过的坑和关键技术细节分享出来,给同样想用爬虫技术做选股辅助的同学一条可以复现的路线。
先说清楚一件事:我这里说的“潜力股”,指的是技术面上资金关注度在提升、趋势结构转好、尚未被市场充分注意的标的,而不是什么“内幕消息股”或“必涨代码”。选股这件事永远存在不确定性,工具能帮你缩小观察范围、保持纪律性,但不可能替你消除风险。我的定位很简单:让数据来承担重复劳动,让人来做最终判断。
1. 为什么我把选股思路从“盯盘”改成了“写代码”
手动选股这件事,听起来很“专业”,真做起来漏洞百出。我先说自己最直接的三个痛点,你可以对照一下是否也有类似感受。
第一个痛点是覆盖面不够。一个人盯着行情软件,能认真看完的大概也就几十只热门股。但全市场几千只股票,大量波动温和、没有上热门的标的根本不可能靠肉眼覆盖。偏偏很多潜在机会就藏在无人问津的角落里。没有工具的年代,这基本靠运气。
第二个痛点是标准不固定。同样是“放量突破”,昨天我觉得量比大于2算放量,今天可能觉得大于3才算;同样是“均线多头排列”,我看得太细就什么都选不出来,看太松又选出一堆乱七八糟的。标准飘忽不定,结果就是筛选完全没有可复现性,更谈不上后续复盘改进。
第三个痛点是时间碎片化。白天要上班,晚上才有空复盘,等把所有数据翻完,脑子已经成一团浆糊了。第二天早上可能又因为起晚了没来得及看行情,整个节奏都是乱的。
这三个痛点指向同一个解决方案:把筛选过程程序化。行情数据是公开的,技术指标计算是标准化的,筛选规则是可以写成代码的——既然这些都是确定性的工作,为什么不让Python来做?
所以我搭建这套系统的目标很明确:每天自动拉取公开行情数据,计算我关心的技术因子,把全市场按统一规则过滤一遍,输出一份简明报告,我只需要在结果基础上做二次判断。
1.1 这套工具不能做什么,必须先说清
我在最初写这套系统的时候,确实对自动化有很高的期望,但跑了两个月之后,我逐渐认清了一个边界:爬虫选股能解决“数据获取和规则执行”的问题,但解决不了“判断”的问题。
举个例子,我的筛选规则可以捕捉到某只标的底部放量、均线拐头,这些都是可以被量化表达的结构特征。但它无法知道这家公司是不是正在经历经营危机,也无法预测突发消息对股价的冲击。技术指标本质上是对历史数据的数学加工,它描述的是“过去发生过什么”,而不是“未来一定发生什么”。
所以在我这套系统里,筛选结果只是“候选池”,不是“买入指令”。候选池的意义在于缩小你的注意力范围,把几百个符合技术特征的标的筛到几个,然后你再去做基本面查证、消息面核实、仓位规划这些机器不擅长的事。
1.2 适合谁参考,不适合谁参考
这套方案最适合两类人:一类是有编程基础、想用数据辅助决策的个人投资者,你可以把它当成一个练手和技术验证项目;另一类是本身就做量化交易的开发者,可以拿这套框架做策略原型,快速验证自己的想法。
反过来,如果你完全不懂Python,或者期待这套系统能替你“躺着赚钱”,那还是趁早绕道。爬虫选股花时间的是前期搭建和调试,不是天天盯着屏幕操作;规则越是自动化,越要求你对股票市场本身有基本认知,知道哪些指标有意义、哪些会被市场结构变化干扰。
2. 数据源选型与取数链路设计
写爬虫选股系统,第一个要解决的就是数据从哪来。这一步如果地基没打好,后面所有指标计算和筛选都是空中楼阁。
2.1 行情数据获取的三种常见路径
我在调研阶段对比过三条路:手写请求去爬公开财经网页、用社区维护的开源行情接口库、购买商业数据服务。
手写爬虫爬网页的问题在于页面结构太脆弱。财经网站的DOM结构隔几个月改一次版,你辛苦写的解析代码马上就失效;而且网页接口经常有限流机制,抓太频繁容易被封。这条路适合临时抓少量数据,不适合做成每天跑的稳定系统。
商业数据服务稳定性和数据质量最好,客服响应也快,但有成本门槛。如果你想做高频回测、拿毫秒级数据,这是正路;但如果每天只是收盘后拉一次日线数据,花这个钱其实有点浪费。
开源行情接口库是我最终的选择。这类库把底层的数据接口请求、响应解析、格式转换都封装好了,你只需要调用函数就能拿到干净的DataFrame,而且大多数数据源是公开免费的数据,合规风险小。社区维护比较活跃,偶尔接口变动也会有人及时更新适配。
我自己的偏好是:拿来主义能用就用,但必须留一层“防腐层”。也就是说,我不直接在核心代码里到处调用接口,而是把所有取数操作封装到一个独立的数据模块里。万一将来接口变了,我只需要改这一个模块,筛选逻辑一行都不用动。
2.2 三层取数链路:抓取层、缓存层、计算层
有了数据源,接下来是设计架构。我的代码组织分成三层:
第一层是抓取层,负责跟行情源打交道。它做的事情很纯粹:给定一批股票代码和时间范围,返回对应的行情数据。
第二层是缓存层,这是很多刚开始写爬虫的人最容易忽略的。股票数据是典型的时序数据,今天的全量历史行情和昨天几乎一模一样,只有最后一天的数据是新的。如果你每天都把几千只股票的历史数据全量抓一遍,既慢又容易被限流。我的做法是把每次抓到的数据按股票代码分文件缓存到本地,下次优先读缓存,只对缺失的日期做增量更新。
第三层是计算层,所有技术指标的计算都在这层完成。计算层不要直接接触数据源,它只需要从缓存层读到的干净DataFrame。
这样的分层逻辑,好处是非常好排查问题。哪天看到报告数据不对,先判断是抓取问题还是缓存问题还是计算问题,顺着层定位就行,不用在几千行代码里找线索。
2.3 存储选型:parquet 为什么比 CSV 更适合
数据存本地,最自然的想法是存CSV。CSV的优点是人眼可直接读、任何工具都能打开,但缺点也很明显:文件大、读写慢、类型信息丢失。
比如日期字段,CSV存进去是字符串,读出来还得解析;数值字段可能有精度损失;几千只股票每只存一个CSV文件,占用的磁盘空间也相当可观。我一开始用的就是CSV,后来数据量上来之后明显感觉力不从心。
后来我切换到parquet格式。parquet是列式存储,同样数据体积只有CSV的四分之一左右,读取速度要快好几倍,而且数据类型完全保留,读出来就是原生DataFrame,不需要做格式转换。配合Pandas的read_parquet和to_parquet方法,代码几乎没有学习成本。
我的数据目录结构是这样的:
market_data/ ├── stock_list.parquet # 全市场股票清单 ├── daily/ │ ├── 000001.parquet # 每只股票一个文件 │ ├── 000002.parquet │ └── ... └── index/ └── benchmark.parquet # 大盘指数数据,用来算相对强度这种按股票拆文件的组织方式,增量更新非常方便。每次收盘后只需要遍历一遍股票池,对每只股票检查最新日期,把缺失的数据追加上去就行。
3. 从全市场清单到因子计算:核心实现
选股的前提是把全市场的数据都拉下来。这一段我将拆开来讲,先讲怎么拿到股票清单并过滤,再讲行情数据的关键预处理——复权,最后讲我用到的基础因子计算。
3.1 获取股票基础清单并过滤三种标的
首先要有一个全市场股票清单。开源行情接口库一般都会提供获取全部股票列表的函数,返回内容包括股票代码、名称、上市日期等基础信息。
清单拿到手之后,不能直接开抓,必须先过滤掉三类不适合进入筛选池的标的:
第一类是带ST和退市风险标记的股票。这些标的技术面经常是失真的,连续跌停、流动性枯竭,普通技术指标完全失效。我的规则很简单:股票名称里含ST或者退字样的,直接剔除。
第二类是上市时间过短的次新股。新股上市初期没有足够的历史数据来计算均线、波动率等指标,而且上市前几天的价格波动剧烈,被资金情绪主导,不适合纳入规则筛选。我一般要求上市满250个交易日左右,也就是大约一年,保证至少能算出完整的中长期均线。
第三类是流动性极差的标的。比如日均成交额长期低于某个阈值的,这些股票就算触发技术信号,也很难在合理价位成交,执行层面意义不大。
3.2 前复权数据:不处理就是给自己挖坑
所有做技术分析的人都知道复权很重要,但实际操作中很容易犯错。分红送转会在除权除息日给股价留下一个不自然的大缺口:比如一只股票10送10,除权日股价直接“腰斩”,但持有人的资产并没有变化。如果不做复权处理,均线、相对强度、突破等所有技术指标的计算都会被这些假缺口严重扭曲。
复权分为前复权和后复权。前复权是保持最新价格不变,历史价格往前调整;后复权是保持首发价格不变,后续价格往后调整。日常技术分析用前复权基本是行业共识,因为最新价格就是盘面上看到的真实成交价,历史价格经过调整后形成的K线最能反映真实的趋势结构。
我的取数逻辑里,所有日线数据都要求返回前复权后的开高低收价格。同时在缓存字段里存一个原始未复权收盘价,这样将来如果要算真实的涨跌幅、统计除权除息事件,还有据可查。
3.3 我实际计算的基础因子:MA、成交量均量、RS 与波动率
拿到干净的前复权行情数据后,我开始计算因子。筛选不一定需要花哨的因子,我实际用的都是非常经典的技术指标,但我在计算时注意了一些细节。
均线族是最核心的因子。我计算了MA5、MA10、MA20、MA60四条均线,分别对应短中长三个周期。关键不只是均线本身的值,而是它们的排列关系:如果MA5大于MA10大于MA20大于MA60,说明短期均线在上、长期均线在下,这就是经典的多头排列形态,代表趋势结构偏强。
成交量均量用于识别资金的关注度变化。我计算了5日均量和20日均量。均量的意思是平滑掉单日的偶然波动,看清成交量的趋势性变化。
相对强度RS是我非常看重的一个因子。它的计算方式很简单:用个股区间涨跌幅减去同期基准指数的涨跌幅。当RS为正且持续扩大,说明这只股票在过去一段时间内跑赢了大盘,有资金在特定方向上形成合力。
波动率则用于控制风险。我计算的是过去20个交易日收益率的标准差,年化之后得到一个百分比。波动率太高的票,就算趋势不错,进场后的持仓体验也常常很折磨,所以我会对波动率设置一个上限。
下面是我计算这些因子的核心代码片段,逻辑写出来供你参考,实际使用时再按你选的库做适配:
import pandas as pd import numpy as np def calculate_factors(df: pd.DataFrame) -> pd.DataFrame: """输入前复权日线数据,输出包含基础因子的DataFrame""" result = df.copy() # 均线族 for window in [5, 10, 20, 60]: result[f"MA{window}"] = result["close"].rolling(window).mean() # 成交量均量 result["VOL_MA5"] = result["volume"].rolling(5).mean() result["VOL_MA20"] = result["volume"].rolling(20).mean() # 日收益率与波动率 result["return"] = result["close"].pct_change() result["volatility"] = result["return"].rolling(20).std() * np.sqrt(252) # 相对强度(需要在外部传入基准同期涨跌幅) # result["rs"] = result["close"].pct_change(60) - benchmark_return_60d # 最近的因子值: 取最后一行的快照 latest = result.iloc[-1] return result, latest这里有个细节:Pandas的rolling计算会自动忽略缺失值,但上市不满60个交易日的股票,算不出MA60。所以我在计算前又做了一层清洗:过滤掉历史长度不足的数据。
4. 把“潜力股”翻译成可执行的筛选规则
数据准备好之后,整个项目最核心的部分来了:什么叫“潜力股”?这个概念很虚,必须把它翻译成一条条可执行的规则。这一步做得好不好,直接决定筛选结果的质量。
4.1 我的四维筛选框架
我把“潜力股”拆解成四个可观测的维度:
趋势结构:均线多头排列,价格在中短期均线之上运行。这个维度衡量的是标的整体方向是否向上。
量能配合:最近成交量显著高于过去20日的平均成交量,且价格上涨伴随着放量。这个维度衡量是否有关注度、是否有资金参与。
相对强度:过去一段时间走势强于大盘。这个维度用来筛选真正有资金合力的标的,而不只是跟着大盘反弹的弱势股。
风险约束:波动率不能过高,距离阶段高点不能回撤太多。这个维度用于排除情绪过度亢奋的标的。
四个维度缺一不可。只有趋势没有量能,可能是无量的空涨,持续性存疑;只有量能没有趋势,可能是下跌途中的反弹,资金在出货而非进场;趋势量能都好但相对强度不行,说明它只是跟随大盘,没有独立性;前面三条全占但波动率失控,持仓风险又太大。
4.2 规则打分卡与代码实现
为了让筛选结果可排序,我用打分制而不是简单的“是/否通过”。每个维度设置若干子条件,每满足一条得一分,最后按总分排序取前N名。下面是打分逻辑的简化版:
def score_stock(latest: pd.Series) -> int: """ 对某只股票的最新因子快照进行打分 总分范围0-10分,分数越高代表技术结构越受关注 """ score = 0 # 维度一: 趋势结构 (满分4分) if latest["MA5"] > latest["MA10"]: score += 1 if latest["MA10"] > latest["MA20"]: score += 1 if latest["MA20"] > latest["MA60"]: score += 1 if latest["close"] > latest["MA20"]: score += 1 # 维度二: 量能配合 (满分2分) vol_ratio = latest["VOL_MA5"] / latest["VOL_MA20"] if vol_ratio > 1.5: score += 1 if latest["close"] > latest["open"] and vol_ratio > 1.2: score += 1 # 维度三: 相对强度 (满分2分) if latest["rs_60d"] > 0: score += 1 if latest["rs_60d"] > 0.1: score += 1 # 维度四: 风险约束 (满分2分) if latest["volatility"] < 0.6: score += 1 if latest["drawdown_20d"] > -0.15: score += 1 return score打分规则写清楚之后,我按照总分从高到低排序,取前10到20名进入候选池。你看,整个筛选过程完全透明——每一分都是可解释的,每一只入选的股票都能说清楚它强在哪里、缺陷在哪里。
4.3 为什么我用组合规则而不是单一指标
如果你只用单一指标选股,比如只看MACD金叉,你会得到几百甚至上千个信号,而且大量信号是重叠的、噪音很多。单一指标本质上只是从一个角度观察市场,而市场走势是多重力量叠加的结果。
组合规则的核心价值不在于“更准”,而在于“更稳”。它能滤掉大量没有实际意义的假信号。比如一只股票仅仅因为前几天跌幅过大,今天出现技术性反弹,单一指标可能提示“超跌反弹”,但加上均线结构和相对强度过滤之后,这种信号就自动被排除了,因为它不满足趋势向上的大前提。
说白了,组合打分相当于给候选股票做了一次多维度体检,只有各项指标协调匹配的标的才能进入最终名单。用这种方法筛出来的池子,后续人工复盘的命中率和效率,比我之前纯凭感觉挑高得多。
5. 流水线化:从手动查数到每日自动出报告
有了筛选规则,还只是解决了“能选”的问题。真正让这套系统发挥价值的,是把它变成每天自动运行的流水线。这一章我分享调度、异常处理和报告输出的具体设计。
5.1 调度策略:每天收盘后增量更新
股票数据是低频数据,不需要实时的全市场推送。我设置的调度策略是:每个交易日收盘后跑一次主流程。这样既不浪费计算资源,也不会因为频繁请求被数据源限制。
主流程大致分为五步:
- 更新股票清单
- 增量拉取每只股票的最新日线数据到本地缓存
- 批量计算技术因子
- 对所有股票打分排序
- 生成报告并推送给自己
在Linux系统上我用cron做定时触发,在Windows上用任务计划程序也行。关键是执行时间要设在收盘清算完成后,避免数据还是半成品。
5.2 异常处理:单只股票失败绝不能拖垮全流程
全市场几千只股票,每天总会在某些标的上遇到数据异常,比如临时停牌、接口返回空值、字段格式不对。最开始我写代码时,一只股票报错就把整个循环中断了,前面算了半天的数据全部白费。
后来我改成:任何一只股票的处理都包在独立的try块里,失败就记录日志,继续下一只。全部跑完之后,再汇总失败列表,统一重试一次;如果重试还失败,就把问题标的标记为“数据异常”,自动排除在当日筛选之外。
这个改动看似简单,但对稳定性提升巨大。运行几个月,流程再没有因为单点问题整体崩过。
5.3 输出一份“说人话”的报告
报告是整个系统的最终产出。如果输出是一堆代码和数字,那只是把问题从“手动盯盘”换成了“手动看代码”,没本质区别。我必须让报告一目了然。
我的报告用Markdown格式生成,核心结构长这样:
# 候选池报告 2026-XX-XX ## 高优先级(得分≥8) 1. 标的A(代码: DEMO001) - 得分: 9/10 - 入选原因: 均线多头排列,5日均量为20日均量的1.8倍,相对强度跑赢大盘12% - 风险提示: 近期涨幅较大,距离20日高点回撤仅2% 2. 标的B(代码: DEMO002) - 得分: 8/10 - 入选原因: 放量突破20日均线,波动率处于健康区间 - 风险提示: 相对强度接近0,需要进一步观察 ## 中优先级(得分≥6) ...重点是把“它为什么入选”讲清楚,而不只是堆指标。我在每条候选后面都附带入选原因和风险提示,这样我每次复盘报告时,不需要再翻行情软件确认入选逻辑,直接就能判断“这条规则是不是该调整了”。
6. 回测、误区和几个实战中反复踩的坑
系统能跑起来只是第一步。逻辑写完了、报告出来了,如果不去验证这套筛选规则在历史上到底靠不靠谱,那它迟早会变成一种“自我感觉良好”的仪式。最后这部分我聊聊回测方法和我在实际项目里踩过的一些坑。
6.1 回测到底该怎么测才不算自我欺骗
很多人做回测,是用今天的代码去测试昨天已经涨过的股票——这不叫回测,叫“往回看”。
正确的做法是:站在历史某个时点,只用当时已经产生的数据做筛选,然后看这个筛选结果的后续表现。比如我来测“均线多头排列且放量”这个规则,就要选取某个历史日期,用截至那一天的收盘数据计算指标,再观察该日期之后5个交易日、10个交易日的走势,统计入选标的的平均涨幅和胜率。
这个过程需要严格避免前视偏差:不能用未来数据计算任何指标,不能把后来才知道的除权信息混入当时的计算。具体到代码实现,就是当你计算某天的MA20时,只能用当天及之前的数据;当你用前复权数据回测历史时,要清楚前复权因子本身用了后来的价格信息,这在小范围回测中可以接受,但分析时要留意这个偏差。
我做了一个简化版回测脚本:选取过去一年每个交易日作为模拟“策略启动日”,跑当日筛选,记录候选池股票在后续10日的表现,汇总胜率和平均收益。跑完之后我对筛选规则是否值得信任就有了大概的底。
6.2 三个容易误导自己的偏差
幸存者偏差是第一个大坑。我一开始的数据清洗把退市股全剔除了,但这意味着我的股票池里只包含“活下来”的股票。用这样的数据回测,结果天然偏向乐观,因为真正跌到退市的标的根本没在池子里。处理办法是:回测时使用历史时点的全量股票列表,包括后来退市的。
前视偏差刚才已经提过,还有一个容易忽略的版本——财报数据的延迟发布。如果你用财务因子选股,要知道财报公布日期和报告期之间有几个月时差,直接拿报告期数据回测,相当于提前知道了财报内容。
过拟合是第三个坑。调参数的时候,规则表现稍微不理想,你就想“把波动率上限从0.6改成0.4试试”“把放量倍数从1.5改成1.8再跑一遍”,来回折腾十几轮,回测结果肯定越调越好看,但这只是因为你在记忆历史数据,不代表未来的规则有效性。我的做法是每次参数调整都只在一个固定的验证集上测,且严格控制调整次数。
6.3 我在这个系统里踩过的具体坑
说完大原则,说几个非常具体、不跑代码根本不会发现的坑。
第一个坑是复权数据混用。我最初抓取日线数据的时候,接口返回的默认是未复权数据。我算出MA20多头排列,结果买入后发现股价突然“跳空低开”,形态全乱了。后来排查发现是除权除息导致的。解决方式就是前面说的,统一使用前复权数据做指标计算,并把复权参数显式写进代码,避免不同调用之间默认值不一致。
第二个坑是停牌股误判。一只停牌一个月的股票,价格每天“横盘”,这种价格稳定会让技术指标失真。均线系统在停牌期间不会变化,但成交量萎缩到零,会被某些规则误判成“缩量整理”。我加了一个停牌检测:当天成交量为零或者成交额低于阈值的,直接从选股池剔除,并标注为停牌。
第三个坑是一字涨停板。一字涨停时成交量极小,价格大幅上涨,这时候量能指标显示“缩量上涨”,而缩量上涨被很多人理解为健康形态,但实际上是因为根本买不进去,成交自然稀少。如果系统把这个当成强势信号,第二天追高会很被动。应对方式是识别一字板:开盘价等于收盘价且接近涨停幅度的,单调高量能评分。
第四个坑是次新股的数据波动。前面说了要过滤上市不足一年的股票,但即使满足了天数条件,次新股因为没有长期价格中枢,计算出来的波动率和均线结构也常和成熟标的差异巨大。我的做法是在报告中给次新股单独打标签,让它们在候选池里单独分组展示。
6.4 数据采集的合规与频率问题
最后必须要提的是数据采集的分寸感。所有行情数据接口都有访问频率的限制,我严格控制请求间隔,绝不高频并发冲击服务端,也绝不使用绕过限制的手段。爬虫技术本身是中性工具,关键在于使用方式是否合规、是否尊重服务提供方的规则。
我在代码里给请求加了一个限速器,每次请求之间至少间隔一定的毫秒数,这样全市场几千只股票的增量更新虽然会慢一些,但非常稳,这个稳定性比极致的速度重要得多。
回到开头说的目标——把重复劳动交给代码,把时间留给人做判断。这套系统到现在已经迭代了好几版,从最初裸调接口的脚本,变成了带缓存、带打分、带回测的完整小工程。最有价值的一点不是它选出的股票有多准,而是它让我被迫把自己的判断规则清晰地写下来,面对“我到底凭什么认为这是一只潜力股”这个问题,写清楚之后,每一个逻辑漏洞都逃不过回测的检验。
最后一个实用技巧:报告里一定要记录每一只候选股的“入选原因”和“后续走势”,每周复盘一次。系统运行久了,你会积累下大量“入选后大涨”和“入选后大跌”的案例。这些案例是优化规则最可靠的素材,比任何书上教的指标用法都贴近你自己的市场认知。这正是爬虫技术选股相对手动盯盘最大的价值——它可以不断积累数据,让你对筛选规则的理解越来越精确。