ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

finviz-api筛选器高级用法:从URL到DataFrame的量化选股实战

finviz-api筛选器高级用法:从URL到DataFrame的量化选股实战 1. 为什么需要认真对待 finviz-api 的筛选器做量化选股或者美股数据研究的人大概率都绕不开 finviz 这个站点。它的筛选器Screener能把基本面、技术面、行业属性几十个维度组合起来做条件过滤页面响应快、字段全手动点几下就能筛出一批标的。但手动操作有两个绕不过去的坎一是每次都要重复点选二是没法把筛选结果直接喂给下游的分析脚本。finviz-api 这个库就是来解决这个问题的。它把 finviz 的筛选参数封装成 Python 对象让你用代码描述筛选条件然后一次性把结果拉回来直接变成结构化数据。标题里说的从URL到数据指的正是这个链路你构造的筛选条件最终会拼成一个带查询参数的 URL库去请求这个 URL解析返回的 HTML 表格最后交给你一个可以继续处理的列表。这篇文章面向的是已经会用 pandas 做基础数据处理、想把手动筛选流程自动化的朋友。我会把筛选器的参数体系拆开讲清楚重点放在那些文档里一笔带过、但实际用起来很容易踩坑的高级用法上比如多条件组合的逻辑、字段值的精确写法、分页与限流、以及怎么把结果稳定地落成 DataFrame。全文基于我自己的实操经验代码可以直接抄去改。2. 筛选器的参数体系与核心思路拆解2.1 筛选器本质上是一组键值对finviz 的筛选器 URL 长这样https://finviz.com/screener.ashx?v111fcap_large,fa_div_o,sh_avgvol_o1000ft4o-marketcap。拆开看v111是视图编号f后面跟的是筛选条件ft是信号过滤o是排序字段。finviz-api 做的事情就是让你不用手拼这个字符串而是用finviz.Filters里的常量去组合。理解这一点很关键你写的每一个筛选条件最终都会变成 URL 里的一个 token。所以当结果不符合预期时第一件事不是怀疑库有 bug而是把库实际请求的 URL 打印出来看看 token 拼得对不对。我习惯在调试阶段把screener.url打出来核对这个习惯帮我省了大量排查时间。2.2 为什么用常量而不是裸字符串finviz-api 把每个筛选条件都定义成了Filters类下的属性比如Filters.MarketCap.LARGE、Filters.Dividend.YIELD_OVER_3。这些常量的值其实就是 finviz 内部的 token 字符串。用常量的好处有三个一是 IDE 能补全不用去背 token二是拼错了会直接报 AttributeError而不是悄悄拼出一个无效 URL 返回空结果三是版本升级时 token 变了库会跟着更新你的代码不用动。我见过有人图省事直接写字符串cap_large短期能跑但一旦 finviz 调整了某个字段的命名这种硬编码就会静默失效——返回的是空列表而不是报错排查起来非常痛苦。所以我的建议是能用常量就用常量实在找不到对应常量再去查 token。2.3 视图编号决定了返回哪些列v这个参数很多人会忽略但它直接决定了你拿到的数据有哪些列。常用的几个视图视图编号含义典型列111概览Overview代码、公司名、行业、市值、PE、价格、涨跌幅、成交量121估值Valuation加上 P/B、P/S、PEG、EPS 等131财务Financial加上股息、ROA、ROE、利润率等141所有权Ownership机构持股、内部持股比例151表现Performance各周期涨跌幅161技术TechnicalRSI、均线、Beta、ATR如果你需要股息率就必须用 131 或更靠后的视图用 111 是拿不到这个字段的。这一点在文档里没有强调但实际做筛选时经常需要根据目标字段反推视图编号。我的做法是先确定要哪些列再选最小的能满足需求的视图编号避免拉回一堆用不上的数据。3. 高级筛选条件的写法与实操要点3.1 多条件组合的默认逻辑是 AND当你传入多个筛选条件时finviz 默认用 AND 连接也就是所有条件都要满足。比如同时筛大盘股和股息率大于3%返回的是两个条件都成立的标的。这个逻辑符合直觉但要注意同一个维度内的多个值也是 AND这往往不是你想要的。举个例子如果你想筛市值是大盘或者中盘直觉上会写[Filters.MarketCap.LARGE, Filters.MarketCap.MID]但这样拼出来的 URL 是cap_large,cap_midfinviz 会理解为市值既是大盘又是中盘结果必然是空。正确的做法是用f参数之外的方式或者干脆分两次请求再合并。这是新手最容易踩的坑之一。3.2 数值型条件的区间写法很多筛选条件支持区间比如市值、价格、成交量。finviz-api 里对应的常量通常有_UNDER、_OVER、_BETWEEN这类后缀。以成交量为例如Filters.AverageVolume.OVER_1000K表示日均成交量超过 100 万股。注意这里的单位是 K千股不是金额。如果你需要自定义区间比如价格在 10 到 50 之间标准常量可能覆盖不到这时候有两个办法一是用最接近的常量组合二是直接构造 token。我一般优先用常量因为区间边界 finviz 是固定的几档自己拼 token 容易拼出无效值。实测下来finviz 对价格区间的档位划分比较粗如果你的策略需要精细区间建议拉回数据后用 pandas 二次过滤而不是在筛选器层面死磕。3.3 信号过滤ft 参数的独立用法ft是信号过滤和f是两套体系。f筛的是基本面和技术面的静态属性ft筛的是当前是否出现某个信号比如今日跳空高开突破 50 日均线RSI 超卖等。这两个参数可以同时用效果是叠加。finviz-api 里信号过滤通过finviz.Filters.Signal或者直接传ft参数实现。我常用的一个组合是f筛出基本面达标的池子ft再筛出其中技术面出现买点的标的。这样两步走比把所有条件堆在一起更清晰也方便你分别调试——先确认基本面池子对不对再确认信号过滤有没有生效。注意信号过滤的结果和当天行情强相关同一个脚本今天跑和明天跑结果可能完全不同。做回测时不要依赖ft它只反映此刻的状态没有历史快照。3.4 排序与分页的正确处理o参数控制排序比如o-marketcap表示按市值降序oprice表示按价格升序。负号代表降序这个规则要记牢。排序字段必须是当前视图里存在的列否则会报错或者被忽略。分页是另一个容易被忽略的点。finviz 每页默认返回 20 条超过的部分需要翻页。finviz-api 的Screener对象在迭代时会自动处理分页但如果你用screener.data直接取可能只拿到第一页。我的做法是明确用循环迭代screener对象让库自己去翻页这样不会漏数据。同时要注意翻页过程中如果筛选结果在变化比如盘中可能出现重复或遗漏做严肃分析时建议在收盘后跑。4. 从筛选到 DataFrame 的完整实操流程4.1 环境准备与基础调用先装库然后跑一个最小可用的例子。这里我用一个大盘股 股息率超 3% 日均成交量超 100 万的组合来演示。from finviz.screener import Screener filters [ cap_large, fa_div_o3, sh_avgvol_o1000, ] stock_list Screener(filtersfilters, tableValuation, order-marketcap) for stock in stock_list: print(stock[Ticker], stock[Price], stock[Dividend])这里tableValuation对应视图 121order-marketcap是降序。跑通之后你会看到一批标的和它们的估值字段。如果返回空先检查 filters 里的 token 是不是有效——把stock_list.url打出来手动在浏览器里打开看看 finviz 怎么响应。4.2 把结果转成 DataFrameScreener对象本身可以迭代每个元素是一个字典。转 DataFrame 最直接的方式是import pandas as pd df pd.DataFrame(stock_list.data) print(df.shape) print(df.columns.tolist())但这里有个细节stock_list.data只包含当前已加载的页。如果你要全部数据得先确保迭代完成或者用stock_list.data之前手动触发加载。我的习惯是写一个辅助函数把迭代结果收集成列表再转 DataFrame这样最稳rows [dict(stock) for stock in stock_list] df pd.DataFrame(rows)转完之后数值列还是字符串需要做类型转换。比如Price、Market Cap、Volume这些列finviz 返回的是带单位或百分号的字符串得自己清洗。这一步没有捷径只能针对每个列写转换逻辑。4.3 数值列的清洗与单位换算finviz 返回的数值有几个常见的坑市值带B十亿或M百万后缀成交量带K或M百分比带%涨跌幅带正负号。清洗逻辑我一般写成这样def parse_number(val): if pd.isna(val) or val -: return None val str(val).replace(%, ).replace(,, ) multiplier 1 if val.endswith(B): multiplier 1e9 val val[:-1] elif val.endswith(M): multiplier 1e6 val val[:-1] elif val.endswith(K): multiplier 1e3 val val[:-1] try: return float(val) * multiplier except ValueError: return None这个函数能覆盖大部分情况但要注意有些列本身就是纯数字字符串不带单位直接 float 就行。建议先df.head()看一眼原始值再决定清洗策略不要盲目套用。4.4 筛选条件的动态构造实际项目里筛选条件往往不是写死的而是根据策略参数动态生成。比如用户输入一个市值下限你要把它映射到 finviz 的档位。这时候就需要一个映射表用户输入亿美元对应 token 3cap_micro3 - 20cap_small20 - 100cap_mid100 - 1000cap_large 1000cap_mega这种映射的好处是把连续参数离散化到 finviz 支持的档位避免拼出无效 token。代价是精度损失但筛选器本来就是粗筛精筛交给 pandas 做这个分工是合理的。5. 常见问题与排查技巧实录5.1 返回空结果的排查顺序空结果是最常见的问题排查要按顺序来不要乱试。我的固定流程是打印screener.url手动在浏览器打开确认 finviz 本身有没有结果。如果浏览器有结果但代码没有检查是不是分页没迭代完。如果浏览器也没结果逐个删减筛选条件定位是哪个条件导致的。检查视图编号和排序字段是否匹配比如用 111 视图却按dividend排序就会出问题。这个顺序能覆盖 90% 的空结果场景。剩下 10% 通常是网络问题或者 finviz 临时调整了页面结构等一会儿再试或者升级库版本。5.2 字段缺失与列名变化finviz 偶尔会调整返回的列名比如把P/E改成PE或者某个字段在某些视图下不返回。这时候stock[P/E]会抛 KeyError。稳妥的写法是用.get()pe stock.get(P/E) or stock.get(PE)另外不同视图返回的列不一样如果你在代码里硬编码了某个列名换视图就会崩。建议在转 DataFrame 之后先打印df.columns确认一遍再写后续逻辑。5.3 请求频率与稳定性finviz 对请求频率是有隐性限制的短时间内大量请求可能被临时拒绝。我的经验是单次筛选尽量把条件写全减少请求次数而不是分很多次小请求。如果确实需要多次请求中间加个time.sleep(1)到2秒的间隔实测下来比较稳。还有一个技巧把常用的筛选结果缓存到本地 CSV下次跑的时候先读缓存只有缓存过期或者条件变了才重新请求。这样既省请求又加快了调试速度。5.4 常见问题速查表现象可能原因解决方向返回空列表条件互斥或 token 无效打印 URL 手动验证逐个删条件KeyError列名变化或视图不匹配用 .get()确认视图编号数据只有 20 条分页未迭代用 for 循环迭代 Screener 对象数值无法计算字符串带单位写清洗函数处理 B/M/K/%请求被拒频率过高加 sleep合并请求用缓存结果与网页不一致盘中数据变化收盘后跑或固定时间点跑5.5 几个我踩过的坑第一个坑是把信号过滤当成静态条件。有次我写了个脚本用ft筛突破均线的标的回测时发现结果好得离谱后来才意识到ft反映的是运行那一刻的状态回测里根本不该用它。这个教训让我养成了区分静态筛选和动态信号的习惯。第二个坑是忽略视图编号导致字段缺失。早期我图省事全用 111 视图结果想算股息率时发现根本没这列白白多写了一套请求逻辑。后来我固定用 131 视图做基本面筛选字段全省心。第三个坑是数值清洗不彻底。有一次市值列里混进了-表示无数据我的清洗函数没处理导致整列变成 object 类型后续排序全乱。从那以后我的清洗函数第一行就是判断空值和-。6. 把筛选器接入实际工作流的思路6.1 筛选器在量化流程中的位置筛选器适合做粗筛也就是从几千只标的里快速缩小到一个几百只的候选池。它的优势是快、字段全、不用自己维护数据源劣势是条件粒度粗、无法做复杂计算。所以合理的分工是finviz 粗筛出候选池pandas 做精细过滤和因子计算最后再交给策略逻辑。我自己的流程是这样的每天收盘后跑一次筛选器把候选池存成 CSV第二天开盘前读 CSV用本地数据做二次过滤盘中只做监控不再请求筛选器。这样既利用了 finviz 的数据广度又避免了实时请求的不稳定。6.2 多组筛选条件的批量执行如果你有多个策略每个策略对应一组筛选条件可以写成一个配置列表循环执行strategies [ {name: dividend, filters: [cap_large, fa_div_o3]}, {name: growth, filters: [cap_mid, fa_epsyoy_o20]}, {name: value, filters: [cap_large, fa_pe_u20]}, ] for s in strategies: result Screener(filterss[filters], tableValuation) rows [dict(x) for x in result] pd.DataFrame(rows).to_csv(f{s[name]}.csv, indexFalse)这种批量执行要注意请求间隔别一口气跑十几个策略容易被限流。我一般每个策略之间 sleep 两秒跑完一轮也就几十秒可以接受。6.3 结果落库与增量更新候选池如果每天都要用建议落成本地数据库而不是 CSV。CSV 的问题是每次全量覆盖没法做历史对比。用 SQLite 就很合适轻量、无需额外服务还能按日期查询历史候选池。落库的时候加一个snapshot_date字段这样你可以对比昨天筛出来的和今天筛出来的有什么变化哪些是新进池的、哪些被剔除了。这个变化本身就是一个有用的信号比单看某一天的快照信息量大得多。6.4 一个完整的可复用脚本骨架把上面的东西串起来一个可复用的脚本大概长这样import time import pandas as pd from finviz.screener import Screener def run_screen(filters, tableValuation, order-marketcap): result Screener(filtersfilters, tabletable, orderorder) rows [dict(x) for x in result] return pd.DataFrame(rows) def clean_df(df): # 针对具体列做清洗这里省略细节 return df if __name__ __main__: filters [cap_large, fa_div_o3, sh_avgvol_o1000] df run_screen(filters) df clean_df(df) df.to_csv(candidates.csv, indexFalse) print(f共筛出 {len(df)} 只标的)这个骨架的好处是职责清晰run_screen只管拉数据clean_df只管清洗主流程只管编排。后续要加策略、加缓存、加落库都在主流程里扩展不会把拉数据和清洗的逻辑搅在一起。7. 关于筛选器精度与数据时效的个人体会finviz 筛选器的档位设计是它的特点也是它的限制。市值、成交量、股息率这些字段都是分档的不是连续值。这意味着你没法精确表达市值在 50 亿到 80 亿之间这种需求只能选最接近的档位然后拉回来用 pandas 精筛。我一开始觉得这是缺陷用久了反而觉得合理——筛选器的定位就是快速缩小范围精度交给下游各司其职。数据时效方面筛选器反映的是请求那一刻的状态盘中会变。如果你的策略对时效敏感建议固定在一个时间点跑比如收盘后半小时这时候数据已经稳定。我试过盘中跑和收盘后跑结果差异不小尤其是涉及涨跌幅和成交量的条件。所以做严肃分析时我坚持收盘后跑把结果存下来第二天用存下来的数据而不是第二天早上重新请求。最后分享一个小技巧如果你需要某个字段但不确定它在哪个视图里可以先用 111 视图拉一只标的看返回的字典有哪些键再对照 finviz 网页上的列名去猜视图编号。这个方法比翻文档快实测几次就能摸清字段和视图的对应关系。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表