1. 为什么直接调接口拿数据总是不顺手
很多人第一次接触金融数据接口,脑子里想的都是“我传个参数,它返回 JSON,我解析一下完事”。真上手 finviz-api 这类封装库之后才发现,事情没那么简单。筛选器(Screener)这个模块看起来只是拼几个查询条件,但实际用起来,参数之间的耦合、字段命名的隐晦、分页与限流的边界,处处都是坑。我自己最开始用的时候,写了一个“筛选市盈率低于 20、市值大于 100 亿”的脚本,跑出来结果只有十几条,跟网页上手动筛选出来的几百条对不上,排查了大半天才发现是参数拼写和默认值的问题。
这篇内容就是围绕 finviz-api 的筛选器高级用法展开的。我会从最基础的请求构造讲起,把参数体系拆开揉碎,再结合几个真实的筛选场景,把“怎么拼条件”“怎么处理返回”“怎么避开常见陷阱”这几件事讲透。适合已经跑通过基础 Demo、但想进一步把筛选器用出花来的朋友。如果你还没装过这个库,也没关系,我会把环境准备和最小可运行示例一并带上,照着敲就能跑。
先给一个最直观的结论:finviz-api 的筛选器本质上是对一套固定查询参数的封装,它的能力边界完全由底层支持的字段决定。你不需要去猜它能不能做某个筛选,只需要搞清楚它支持哪些字段、每个字段接受什么值、多个条件之间是 AND 还是 OR。把这三点吃透,剩下的就是组合和调试的体力活了。
2. 环境准备与最小可运行示例
2.1 安装与依赖确认
finviz-api 是一个纯 Python 库,安装本身没什么门槛。我习惯用虚拟环境隔离,避免跟系统里的其他包打架。
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install finviz-api装完之后,建议先确认一下版本。这个库更新不算特别频繁,但不同版本之间筛选器支持的字段可能有细微差异。我遇到过某个字段在旧版本里不生效、升级后正常的情况,所以版本确认这一步别省。
import finviz_api print(finviz_api.__version__)如果输出版本号没有报错,说明基础环境没问题。接下来就可以构造第一个筛选请求了。
2.2 第一个筛选请求:从最简单的条件开始
不要一上来就堆一堆条件,那样出了问题你根本不知道是哪个参数导致的。我的习惯是先跑一个单条件请求,确认链路通了,再逐步加条件。
from finviz_api import Screener screener = Screener() result = screener.get_screener( filters=["fa_pe_u20"], # 市盈率低于 20 table="Overview", order="-marketcap" ) print(result)这里有几个点值得说明。filters参数接受一个字符串列表,每个字符串代表一个筛选条件。fa_pe_u20这个写法里,fa是 fundamental analysis 的缩写,pe是市盈率,u20表示 under 20。这种“字段_操作_数值”的命名规则是 finviz 筛选器的核心语法,后面我会专门用一节来拆解。
table参数决定返回哪些列。Overview是最常用的概览表,包含代码、公司名、行业、市值、市盈率等基础字段。order参数控制排序,-marketcap表示按市值降序,前面的减号代表降序,不加就是升序。
跑通这个请求之后,你会拿到一个包含若干条记录的列表。如果返回为空,先别急着怀疑代码,去网页端手动筛一下同样的条件,看看是不是本身就没有符合条件的标的。这一步能帮你快速区分“代码问题”和“数据问题”。
2.3 返回结构长什么样
第一次拿到返回结果的人,很容易被它的结构搞懵。它不是一个扁平的字典列表,而是带有层级和元信息的结构。我建议你先把返回结果打印出来,用type()和keys()逐层看一遍。
print(type(result)) # 通常是 list 或 dict,取决于版本和调用方式 if isinstance(result, list): print(len(result)) print(result[0].keys() if result else "empty")搞清楚返回结构之后,你才能正确地提取字段、做后续处理。我见过有人直接把返回结果当 DataFrame 用,结果各种报错,其实就是没先看一眼结构。花两分钟看清楚,能省后面两小时的调试。
3. 筛选器参数体系拆解:字段、操作符与数值
3.1 命名规则背后的逻辑
finviz 筛选器的参数命名不是随便定的,它有一套相对固定的模式:类别_字段_操作数值。理解这个模式,你就能自己推导出很多没在文档里明写的参数。
以fa_pe_u20为例,拆开来看:
fa:类别前缀,代表基本面(fundamental analysis)。类似的还有ta(技术面)、sh(做空数据)、geo(地区)等。pe:具体字段,这里是市盈率。u20:操作符加数值,u代表 under(低于),20是阈值。
再比如ta_rsi_o70,ta是技术面,rsi是相对强弱指标,o70表示 over 70(高于 70)。这种命名方式一旦掌握,你看到一个新参数就能大致猜出它的含义,不用每次都翻文档。
3.2 常用操作符对照
操作符是筛选器里最容易出错的地方,因为不同字段支持的操作符不完全一样。下面这张表是我在实际使用中整理出来的常用操作符,覆盖了绝大多数场景。
| 操作符 | 含义 | 示例 | 说明 |
|---|---|---|---|
| u | under,低于 | fa_pe_u20 | 数值型字段常用 |
| o | over,高于 | ta_rsi_o70 | 数值型字段常用 |
| b | between,介于 | fa_pe_b10to20 | 需要两个数值,用 to 连接 |
| e | equal,等于 | sh_short_e10 | 精确匹配 |
| l | less,小于 | fa_debt_eq_l0.5 | 部分字段用 l 而非 u |
| g | greater,大于 | fa_gross_margin_g50 | 部分字段用 g 而非 o |
这里有个坑要特别注意:u和l、o和g在某些字段上是可以互换的,但在另一些字段上不行。我的经验是,如果某个参数报错或者不生效,先试试换一个同义操作符。这个规律文档里没写,是我反复试出来的。
3.3 数值的写法与单位
数值部分的写法也有讲究。百分比类字段通常直接写数字,比如毛利率 50% 写成g50,不需要加百分号。金额类字段则要注意单位,有的按百万,有的按十亿,写错了结果会差好几个数量级。
# 市值大于 100 亿(假设单位是十亿) filters=["fa_marketcap_o10"] # 股息率高于 3% filters=["fa_div_yield_o3"] # 市盈率介于 10 到 20 之间 filters=["fa_pe_b10to20"]我建议每次写数值之前,先去网页端手动筛一次,看看它显示的数值格式是什么,然后照着写。这是最稳妥的办法,比猜单位靠谱得多。
3.4 多条件组合:AND 是默认逻辑
当你传入多个 filter 时,它们之间默认是 AND 关系。也就是说,所有条件必须同时满足。
filters=[ "fa_pe_u20", # 市盈率低于 20 "fa_marketcap_o10", # 市值大于 100 亿 "fa_div_yield_o2" # 股息率高于 2% ]这三个条件叠加,筛出来的就是“市盈率低、市值大、股息高”的标的。如果你想要 OR 逻辑,finviz 筛选器本身不直接支持在单个请求里做 OR,需要拆成多个请求然后合并结果。这一点后面会专门讲。
4. 实战场景一:构建一个多因子价值筛选器
4.1 需求拆解与条件设计
假设我想找一批“估值合理、盈利稳定、有一定分红”的标的。这个需求翻译成筛选条件,大概是这样几条:
- 市盈率低于行业平均水平,先设一个绝对阈值,比如 25
- 市净率低于 3,避免估值过高
- 净资产收益率高于 10%,确保盈利质量
- 股息率高于 1.5%,有分红意愿
- 市值大于 50 亿,过滤掉太小的标的
把这些条件写成代码:
from finviz_api import Screener screener = Screener() value_stocks = screener.get_screener( filters=[ "fa_pe_u25", "fa_pb_u3", "fa_roe_o10", "fa_div_yield_o1.5", "fa_marketcap_o5" ], table="Overview", order="-marketcap" )4.2 为什么这样设置阈值
阈值不是拍脑袋定的。市盈率 25 是一个比较宽松的价值线,能覆盖到一些成长性尚可但估值不算离谱的标的。市净率 3 是为了排除那些靠重资产堆出来的低 ROE 公司。ROE 10% 是很多专业机构筛选优质公司的底线。股息率 1.5% 则是为了确保公司有回馈股东的习惯,同时不至于把阈值设太高导致结果太少。
这些数字你可以根据自己的偏好调整。关键是理解每个条件在过滤什么,而不是照抄。我见过有人直接把网上的参数抄过来,结果筛出来的东西完全不符合自己的投资逻辑,那就是没理解条件背后的含义。
4.3 结果验证与迭代
跑出结果之后,不要直接就用。先看数量,如果只有三五条,说明条件太严了,可以适当放宽某个阈值。如果几百条,说明条件太松,需要收紧。
我通常会做一个简单的分布统计,看看筛出来的标的在各个维度上的分布情况,判断条件是否合理。
import pandas as pd df = pd.DataFrame(value_stocks) print(df[["Ticker", "Market Cap", "P/E", "ROE", "Div Yield"]].describe())通过描述性统计,你能快速看出这批标的的估值中枢、盈利水平分布,从而判断筛选条件是否达到了预期效果。如果发现某个维度的分布特别集中,说明那个条件可能设得太死,可以考虑放宽。
4.4 一个容易忽略的细节:行业中性化
纯粹的数值筛选有一个问题:某些行业天然市盈率低、股息率高,比如公用事业和金融。如果你不加限制,筛出来的结果可能全是这几个行业的标的,失去了分散配置的意义。
解决办法是在筛选条件里加入行业限制,或者筛完之后按行业分组,每个行业取前几名。finviz 筛选器支持按行业筛选,具体参数可以在网页端的行业下拉框里找到对应的值。
# 排除金融行业(假设行业参数为 fa_sector) filters=[ "fa_pe_u25", "fa_pb_u3", "fa_roe_o10", "fa_div_yield_o1.5", "fa_marketcap_o5", # 行业排除需要根据实际支持的参数来写 ]行业中性化这一步,很多人会忽略,但它对最终结果的影响很大。尤其是做组合配置的时候,行业集中度过高会带来额外的风险。
5. 实战场景二:技术面与基本面结合的动量筛选
5.1 技术面筛选器的可用字段
finviz 的技术面筛选器(ta前缀)支持的字段包括 RSI、布林带位置、均线位置、成交量变化等。这些字段单独用意义不大,但跟基本面结合,能筛出一些有意思的标的。
比如我想找“基本面不差、近期有动量”的标的,可以这样组合:
momentum_stocks = screener.get_screener( filters=[ "fa_pe_u30", # 估值不算离谱 "fa_roe_o8", # 盈利尚可 "ta_rsi_o50", # RSI 高于 50,有向上动量 "ta_rsi_u70", # 但没到超买 "ta_sma20_o1" # 价格在 20 日均线之上 ], table="Overview", order="-change" )5.2 RSI 区间的选择逻辑
RSI 高于 50 说明近期买盘力量占优,低于 70 则是为了避免追高。这个 50 到 70 的区间是我比较常用的,它代表“有动量但还没过热”。如果你更激进,可以放宽到 40 到 80;更保守就收紧到 55 到 65。
这里有个实操心得:RSI 的参数在不同市场环境下表现差异很大。趋势市里 RSI 可以长时间维持在 70 以上,震荡市里则很少突破 65。所以这个区间不是固定的,需要根据市场状态动态调整。我一般会先跑一个宽区间,看看结果数量,再决定要不要收紧。
5.3 均线条件的写法
ta_sma20_o1这个条件的意思是“价格在 20 日均线之上”。这里的o1不是“高于 1”,而是一个布尔标记,表示“满足该条件”。类似的还有ta_sma50_o1、ta_sma200_o1。
如果你想找“价格同时在 20 日和 50 日均线之上”的标的,就把两个条件都加上:
filters=[ "ta_sma20_o1", "ta_sma50_o1" ]这种多均线条件在趋势跟踪策略里很常见。它的逻辑是:短期均线和中期均线都向上,说明趋势比较健康。
5.4 技术面筛选的局限性
必须说清楚一点:技术面筛选器只能告诉你“当前状态”,不能预测未来。RSI 高不代表一定会继续涨,均线之上也不代表不会跌破。它只是一个过滤工具,帮你缩小关注范围,最终的判断还是要靠你自己。
我见过有人把技术面筛选当成交易信号,筛出来就直接买,结果可想而知。筛选器是起点,不是终点。这个认知很重要,能帮你避免很多不必要的损失。
6. 分页、排序与结果导出的工程化处理
6.1 分页机制与请求节奏
finviz 筛选器的返回结果有数量限制,通常一次最多返回几十到上百条。如果你的筛选条件比较宽松,结果可能超过这个限制,就需要分页获取。
分页的实现方式取决于库的版本。有的版本支持page参数,有的则需要通过调整筛选条件来间接实现。我建议先确认你用的版本支持哪种方式。
# 假设支持 page 参数 all_results = [] for page in range(1, 6): result = screener.get_screener( filters=["fa_pe_u20"], table="Overview", page=page ) if not result: break all_results.extend(result)分页的时候要注意请求节奏。连续快速请求可能会触发限制,建议在每次请求之间加一个短暂的等待。
import time for page in range(1, 6): result = screener.get_screener(...) all_results.extend(result) time.sleep(1) # 等待 1 秒,避免请求过于密集这个等待时间不是固定的,可以根据实际情况调整。如果发现请求失败率上升,就适当加长。
6.2 排序参数的灵活运用
order参数决定了返回结果的排序方式。常用的排序字段包括市值、涨跌幅、市盈率、成交量等。降序用减号前缀,升序不加。
# 按市值降序 order="-marketcap" # 按涨跌幅升序(跌幅最大的在前) order="change" # 按市盈率升序(估值最低的在前) order="pe"排序字段的选择会影响你看到的结果。比如你想找“近期跌幅较大但基本面不差”的标的,就可以按涨跌幅升序排,然后在前几页里找符合基本面条件的。这种用法比单纯按市值排要有针对性得多。
6.3 导出为 CSV 与后续处理
拿到结果之后,通常需要导出做进一步分析。用 pandas 可以很方便地完成这个步骤。
import pandas as pd df = pd.DataFrame(all_results) df.to_csv("screener_results.csv", index=False) print(f"导出 {len(df)} 条记录")导出之后,你可以在 Excel 或者 pandas 里做更复杂的筛选、排序、可视化。我习惯把每次筛选的结果都存一份,加上日期标记,这样过一段时间可以回看当时的筛选逻辑是否有效。
from datetime import datetime filename = f"screener_{datetime.now().strftime('%Y%m%d')}.csv" df.to_csv(filename, index=False)这个习惯看起来简单,但长期积累下来,你会有一套自己的历史筛选数据集,对复盘和优化筛选条件很有帮助。
6.4 异常处理与重试机制
网络请求不可能永远成功。加一层异常处理和重试机制,能让你的脚本更健壮。
import time def fetch_with_retry(screener, filters, max_retries=3): for attempt in range(max_retries): try: return screener.get_screener(filters=filters, table="Overview") except Exception as e: print(f"第 {attempt + 1} 次尝试失败: {e}") time.sleep(2 ** attempt) # 指数退避 return []指数退避的意思是每次重试的等待时间翻倍。第一次等 1 秒,第二次等 2 秒,第三次等 4 秒。这样既能给服务端喘息的时间,又不会让你的脚本卡太久。这个模式在处理任何网络请求时都适用,值得养成习惯。
7. 那些文档里不会写的踩坑记录
7.1 参数拼写错误导致的静默失败
最让人头疼的坑不是报错,而是不报错但结果不对。我有一次把fa_pe_u20写成了fa_pe_under20,脚本正常运行,返回了一堆结果,但仔细一看,筛选条件根本没生效,返回的是全量数据。
这种静默失败很难发现,因为代码不报错,结果看起来也“像那么回事”。我的应对办法是:每次写完筛选条件,先跑一个小样本,手动核对几条记录的字段值是否真的满足条件。如果发现有不满足的,说明某个参数没生效。
# 验证筛选条件是否生效 for stock in result[:5]: pe = stock.get("P/E") print(f"{stock['Ticker']}: P/E = {pe}") # 手动确认 pe 是否真的低于 20这个验证步骤花不了多少时间,但能帮你避免很多后续的麻烦。
7.2 数值单位不一致的陷阱
前面提过数值单位的问题,这里再强调一次。不同字段的单位可能不一样,市值可能是十亿,成交量可能是百万,百分比字段可能直接是数字。写条件之前,一定要先确认单位。
我的做法是:先在网页端手动筛一次,把鼠标悬停在结果上,看看它显示的原始数值是什么格式,然后照着写。比如网页显示市值是“12.5B”,那你在代码里写o12就是对的(假设单位是十亿)。如果网页显示的是“12,500M”,那单位就是百万,写法就不一样了。
7.3 筛选条件过多导致结果为空
条件越多,结果越少,这是必然的。但有时候你会发现,明明每个条件单独跑都有结果,合在一起就空了。这通常是因为条件之间的交集太小。
解决办法是逐步增加条件,每加一个就看一眼结果数量。如果加某个条件之后结果骤降,说明这个条件太严了,需要放宽。
conditions = [ "fa_pe_u25", "fa_pb_u3", "fa_roe_o10", "fa_div_yield_o1.5", "fa_marketcap_o5" ] for i in range(1, len(conditions) + 1): result = screener.get_screener(filters=conditions[:i], table="Overview") print(f"前 {i} 个条件: {len(result)} 条结果")这个逐步增加的方法能帮你快速定位是哪个条件把结果卡死了。定位到之后,要么放宽阈值,要么去掉这个条件。
7.4 返回字段缺失的处理
不是所有标的都有完整的字段数据。有些小盘股可能没有股息率,有些亏损公司没有市盈率。这些缺失值在返回结果里可能表现为None、"-"或者空字符串。
处理的时候要做好判断,避免因为一个缺失值导致整个脚本崩溃。
for stock in result: pe = stock.get("P/E") if pe and pe != "-": # 正常处理 pass else: # 跳过或特殊处理 continue这种防御性编程的习惯,在处理任何外部数据时都值得养成。数据源不会永远给你干净完整的数据,你的代码要能应对各种意外情况。
7.5 请求频率与稳定性
虽然 finviz 的筛选器接口相对稳定,但频繁请求仍然可能触发限制。我的经验是,单次会话里连续请求不要超过十几次,每次之间加一秒左右的间隔。如果需要大量请求,考虑分批进行,中间休息几分钟。
另外,不要在短时间内重复请求完全相同的条件。如果你需要多次使用同一批数据,把它缓存下来,而不是每次都重新请求。
import json # 缓存结果 with open("cache.json", "w") as f: json.dump(result, f) # 下次直接读缓存 with open("cache.json", "r") as f: result = json.load(f)这个缓存机制在调试阶段特别有用。你可以反复处理同一批数据,而不用每次都去请求接口,既省时间又降低触发限制的风险。
8. 把筛选器用出花来的几个进阶思路
8.1 多条件 OR 的模拟实现
前面说过,finviz 筛选器默认是 AND 逻辑,不直接支持 OR。但你可以通过多次请求加合并来模拟 OR。
比如你想找“市盈率低于 15 或者股息率高于 5%”的标的,可以拆成两个请求:
result_a = screener.get_screener(filters=["fa_pe_u15"], table="Overview") result_b = screener.get_screener(filters=["fa_div_yield_o5"], table="Overview") # 合并去重 seen = set() merged = [] for stock in result_a + result_b: ticker = stock["Ticker"] if ticker not in seen: seen.add(ticker) merged.append(stock) print(f"合并后共 {len(merged)} 条")这个方法的代价是请求次数翻倍,但能实现更灵活的筛选逻辑。如果 OR 条件比较多,可以考虑用集合运算来合并多个结果集。
8.2 筛选结果的二次加工
筛选器返回的是原始数据,你可以在此基础上做二次加工。比如计算一个简单的综合评分,把市盈率、ROE、股息率加权求和,然后按评分排序。
def score(stock): pe = float(stock.get("P/E", 999) or 999) roe = float(stock.get("ROE", 0) or 0) div = float(stock.get("Div Yield", 0) or 0) # 简单的加权评分,权重可以根据偏好调整 return -pe * 0.3 + roe * 0.5 + div * 0.2 scored = sorted(result, key=score, reverse=True) for stock in scored[:10]: print(f"{stock['Ticker']}: score = {score(stock):.2f}")这个评分函数只是一个示例,你可以根据自己的投资逻辑设计更复杂的评分体系。关键是把筛选器的输出当作原材料,而不是最终产品。
8.3 定时任务与自动化
如果你有一套固定的筛选逻辑,可以把它做成定时任务,每天自动跑一次,把结果发到你的邮箱或者保存到数据库。
import schedule import time def daily_screen(): result = screener.get_screener( filters=["fa_pe_u25", "fa_roe_o10"], table="Overview" ) df = pd.DataFrame(result) df.to_csv(f"daily_{datetime.now().strftime('%Y%m%d')}.csv", index=False) print(f"完成,共 {len(df)} 条") schedule.every().day.at("09:00").do(daily_screen) while True: schedule.run_pending() time.sleep(60)这个自动化流程能帮你节省大量手动操作的时间。不过要注意,定时任务里的筛选条件不要设得太复杂,否则一旦某个环节出问题,排查起来会比较麻烦。建议先用简单的条件跑通流程,再逐步增加复杂度。
8.4 与其他数据源的交叉验证
finviz 的数据有它的优势,但也不是万能的。我习惯把筛选结果跟其他数据源做交叉验证,比如用另一个接口查一下同样的标的,看看财务数据是否一致。如果发现明显差异,就以更权威的数据源为准。
这个交叉验证的步骤不是必须的,但在做重要决策之前,多一层验证总是好的。尤其是涉及具体财务数据的时候,不同数据源之间的差异可能比你想象的要大。
8.5 筛选逻辑的版本管理
最后分享一个我自己的习惯:把每次调整的筛选条件都记录下来,形成一个版本历史。这样当你发现某个版本的筛选效果特别好或者特别差的时候,可以回溯到当时的条件设置。
# 用一个简单的字典记录筛选配置 config = { "version": "v3", "date": "2024-01-15", "filters": ["fa_pe_u25", "fa_roe_o10", "fa_div_yield_o1.5"], "note": "放宽了市盈率阈值,从20调到25" }这个习惯看起来有点繁琐,但当你积累了几十个版本的筛选配置之后,回头看会非常有价值。它能帮你理解什么样的条件组合在什么样的市场环境下更有效,从而不断优化自己的筛选策略。
筛选器只是工具,真正决定结果的是你背后的筛选逻辑和对市场的理解。工具用得再熟,逻辑不对也是白搭。反过来,逻辑清晰的人,哪怕用最基础的功能,也能筛出有价值的东西。希望这些内容能帮你把 finviz-api 的筛选器真正用起来,而不是停留在“跑通 Demo”的阶段。