Piotroski F-Score 9 分选股法:如何用 FMP 接口一键排查美股财务健康度?

在美股基本面量化与价值选股(Value Investing)中,芝加哥大学会计学教授 Joseph Piotroski 于 2000 年提出的 Piotroski F-Score(皮奥特罗斯基 9 分法) 是过滤“价值陷阱”最常用的财务健康度标尺。该模型从盈利能力、资本结构与运营效率三个维度提取 9 个二元财务信号(满足记 1 分、不满足记 0 分),用于识别基本面正在发生边际改善的标的。

但在工程实盘中直接调用 FMP 官方的 Financial Scores(财务健康评分接口,/stable/financial-scores)与 Financial Scores Bulk(财务评分批量接口,/stable/scores-bulk)时,开发者往往会遇到三个具体问题:第一,为什么按教科书年度公式手算出来的分数,常与接口返回的 piotroskiScore 相差 1~2 分?第二,为什么英伟达(NVDA)与波音(BA)在最新截面上同为 6 分,底层失分结构却完全不同?第三,在全市场 18,046 条美元财报评分记录中,为何有 33.27% 的 8~9 分高分记录同时处于 altmanZScore < 1.81 区间?

本文基于 quote-fmp.com 网关拉取的 10 家美股代表性公司最近 8 个已结账季度三大报表(共 80 个公司季度切片)及全市场 62,886 条批量评分截面摘要(数据快照截至 2026-11-04,留存于同目录脱敏核对摘要 JSON),逐项拆解 FMP 的 8 季度滚动复算规则、批量初筛的噪音边界,并给出可复现的单股 9 项复算与对账 Python 代码。


一、 FMP 的 Piotroski 9 分是怎么算出来的?8 季度滚动口径与规则边界

许多量化研究者的第一个困惑是:拉取两期 10-K 年报(period=annual)按 Piotroski (2000) 原论文公式复算,结果在非四季报期间与 /stable/financial-scores 返回的 piotroskiScore 频繁出入。

需要首先厘清的是,Piotroski (2000) 原论文是以年度财报为基础定义的:其总资产收益率(ROA)与经营现金流均按期初总资产缩放,杠杆率考核长期债务占平均总资产比例的年度变动,且第 7 项股权融资信号(EQ_OFFER)考核“过去一年是否未发行普通股”(未增发且股数持平时在原论文中得 1 分)。而对照本站 财务指标计算公式全集、FMP 官方文档的九项文字定义以及本文对 10 家公司 80 个季度 Income Statement(利润表接口)、Balance Sheet Statement(资产负债表接口) 和 Cash Flow Statement(现金流量表接口) 的实盘复算,FMP 接口采用的是最近 8 个季度(Q0..Q7,其中 Q0 为最新结账季、Q4 为去年同季)滚动窗口,且在 F2 与 F6 两项上采用了不同于 ΔROA 与负债率的判定规则:

维度与分值题号与考核指标本文验证的 FMP 8 季滚动复算口径 (Q0..Q7)规则特征及与仅替换 F2/F6 对照口径的差异
盈利能力 (4 分)F1 总资产收益率 · F2 盈利水平F1:sum(NI[0:4]) / totalAssets[0] > 0;F2:sum(NI[0:4]) > 0FMP 官方定义将 F2 设为净利大于 0(NI_TTM > 0),使 F1 与 F2 同向绑定;若在同滚动窗口下将 F2 替换为 ΔROA_TTM > 0,ROA 高位微降的公司会少 1 分
盈利能力 (续)F3 经营现金流 · F4 应计盈余质量F3:sum(OCF[0:4]) > 0;F4:sum(OCF[0:4]) > sum(NI[0:4])要求滚动 4 季经营现金流为正且高于滚动 4 季净利润,过滤应计项目偏高、现金含量不足的账面盈利
资本结构 (3 分)F5 流动比率 · F6 长期债务 · F7 股本变动F5:CR[0] > CR[4];F6:longTermDebt[0] < longTermDebt[4];F7:weightedAverageShsOut[0] < weightedAverageShsOut[4]F6 直接比较长期债务绝对金额(若替换为长期债务占总资产比 LTD / TA 同比下降,扩表期公司得分会不同);注意 F7 用严格小于判断,未增发但股数持平或零债务 0 < 0 均会判 0 分
运营效率 (2 分)F8 毛利率同比 · F9 总资产周转率F8:GM[0] > GM[4](单季同比);F9:(sum(Rev[0:4]) / TA[0]) > (sum(Rev[4:8]) / TA[4])F8 比较最新单季与去年同季毛利率;F9 比较当前 TTM 与去年同期 TTM 的总资产周转率(以各自期末总资产作分母)

把这套规则转化为工程直觉,有两条核心纪律需要在写选股器前明确:


二、 10 家美股单期截面实盘对账:为什么苹果拿满 9 分,而英伟达和波音同为 6 分?

我们先调用 Statements(财务报表接口) 分类下的 Financial Scores(财务健康评分接口,/stable/financial-scores),观察苹果(AAPL)与波音(BA)的原生返回结构(数据快照截至 2026-11-04):

// GET /stable/financial-scores?symbol=AAPL 及 symbol=BA (取数时间 2026-11-04)
[
  { "symbol": "AAPL", "reportedCurrency": "USD", "altmanZScore": 12.479213455651779, "piotroskiScore": 9, "workingCapital": 3942000000,
    "totalAssets": 404548000000, "retainedEarnings": 13700000000, "ebit": 159237000000, "marketCap": 4843808342000, "totalLiabilities": 292455000000, "revenue": 478834000000 },
  { "symbol": "BA", "reportedCurrency": "USD", "altmanZScore": 1.499309458800008, "piotroskiScore": 6, "workingCapital": 15311000000,
    "totalAssets": 170739000000, "retainedEarnings": 16429000000, "ebit": 10305000000, "marketCap": 137339499101, "totalLiabilities": 164177000000, "revenue": 94913000000 }
]

为检验表一的 8 季度滚动复算口径,我们拉取苹果(AAPL)、美光(MU)、AMD、英伟达(NVDA)、微软(MSFT)、特斯拉(TSLA)、波音(BA)、英特尔(INTC)、谷歌(GOOGL)与超微电脑(SMCI)共 10 家公司最近 8 个季度的三大报表,逐项计算 F1..F9 并与接口最新截面原值比对(数据快照截至 2026-11-04):

标的 (最新财季 / 披露日 filingDate)F-Score (FMP 接口分 vs 仅替换 F2/F6 的滚动对照分)盈利 4 项 (F1..F4 得分与 OCF-NI 差额)资本结构 3 项 (F5..F7 得分与核心变动)效率 2 项 (F8..F9) 与 altmanZScore
AAPL (FY26 Q4 / 2026-11-03)9 分 vs 9 分1,1,1,1(4 分 · 现金流超净利 +129.22 亿美元)1,1,1(3 分 · 流动比 +0.13x,股本缩减 -1.65%)1,1(2 分 · 毛利率 +0.71pp) · Z = 12.48
MU (FY26 Q4 / 2026-10-09)8 分 vs 8 分1,1,1,1(4 分 · 现金流超净利 +47.06 亿美元)1,1,0(2 分 · 长期债降 -112.25 亿美元,股本 +1.25%)1,1(2 分 · 毛利率 +42.09pp) · Z = 13.78
AMD (FY26 Q3 / 2026-11-04)7 分 vs 7 分1,1,1,1(4 分 · 现金流超净利 +15.04 亿美元)1,0,0(1 分 · 长期债增 +47.32 亿美元,股本 +0.62%)1,1(2 分 · 毛利率 +2.20pp) · Z = 19.12
NVDA (FY27 Q2 / 2026-08-26)6 分 vs 5 分1,1,1,0(3 分 · 净利超现金流 -585.19 亿美元)1,0,1(2 分 · 长期债增 +239.00 亿美元,股本 -0.72%)1,0(1 分 · 周转率降 -0.2279x) · Z = 37.59
MSFT (FY27 Q1 / 2026-10-28)6 分 vs 6 分1,1,1,1(4 分 · 现金流超净利 +532.47 亿美元)0,1,1(2 分 · 流动比降 -0.18x,股本 -0.09%)0,0(0 分 · 毛利率 -1.25pp) · Z = 9.42
TSLA (FY26 Q3 / 2026-10-22)6 分 vs 5 分1,1,1,1(4 分 · 现金流超净利 +154.92 亿美元)0,1,0(1 分 · 流动比降 -0.33x,股本 +0.40%)1,0(1 分 · 毛利率 +1.60pp) · Z = 15.31
BA (FY26 Q3 / 2026-10-27)6 分 vs 6 分1,1,1,0(3 分 · 净利超现金流 -36.44 亿美元)0,1,0(1 分 · 长期债降 -32.60 亿美元,股本 +3.21%)1,1(2 分 · 毛利率转正 +19.17pp) · Z = 1.50
INTC (FY26 Q3 / 2026-10-30)5 分 vs 6 分0,0,1,1(2 分 · TTM 净亏损 -120.32 亿美元)1,0,0(1 分 · 长期债额增但占总资产比降 -1.28pp)1,1(2 分 · 毛利率 +4.75pp) · Z = 2.58
GOOGL (FY26 Q3 / 2026-10-29)5 分 vs 5 分1,1,1,0(3 分 · 净利超现金流 -561.43 亿美元)1,0,0(1 分 · 长期债增 +1,034.95 亿美元)1,0(1 分 · 周转率降 -0.2490x) · Z = 9.72
SMCI (FY27 Q1 / 2026-11-03)4 分 vs 5 分1,1,0,0(2 分 · TTM 经营现金流 -63.26 亿美元)0,0,0(0 分 · 长期债额增但占总资产比降 -13.79pp)1,1(2 分 · 毛利率 +3.43pp) · Z = 3.62

在本次核验的 10 家公司最新时点截面上,按表一公式复算的 9 项总分与 /stable/financial-scores 返回的 piotroskiScore 10 家全部吻合(需要说明的是,10 家样本总分一致验证了该 8 季滚动公式在常规美股报表上的复现能力,但不能反证 FMP 内部所有未公开边界分支)。拆解具体子项,可以看到三个核心实战特征:

1. 扩表期的周转率/应计项失分 vs 经营现金流转负的失分(以 NVDA 与 SMCI 为例)

表面上看,英伟达(NVDA)的 F-Score 为 6 分,仅比超微电脑(SMCI,4 分)高 2 分。但穿透到底层报表数字,两者的失分结构截然不同:

2. 为什么波音(BA)也能拿 6 分?用同接口的 altmanZScore 补齐存量负债观察视角

对比表中同为 6 分的英伟达(NVDA)、微软(MSFT)、特斯拉(TSLA)与波音(BA),体现出单用 Piotroski F-Score 选股的局限:F-Score 大部分指标衡量的是“同比边际变化方向(Delta)”,而非“资产负债表存量负债水平(Level)”。

波音(BA)在 2026Q3 的单季毛利率从去年同期的 -10.21% 修复至 +8.96%(F8 得 1 分),长期债务从 446.11 亿美元缩减至 413.51 亿美元(F6 得 1 分),总资产周转率从 0.5383x 微升至 0.5559x(F9 得 1 分),加上 TTM 扭亏使得 F1、F2、F3 各得 1 分,合计拿到 6 分。此时配合 /stable/financial-scores 同屏返回的 altmanZScore,即可直观对比存量偿债缓冲:


三、 全市场 /stable/scores-bulk 快照统计:62,886 条记录的分层分布与四类干扰源

如果要对全市场评分截面做批量统计,调用 Bulk(批量数据接口) 分类下的 Financial Scores Bulk(财务评分批量接口,/stable/scores-bulk)可在单次 HTTP 请求中拉回全库 62,886 条评分记录。

我们对 2026-11-04 拉取的 62,886 条原始 CSV 记录按报表币种、F-Score 分档与 Altman Z-Score 阈值做分层汇总(统计摘要见同目录脱敏核对 JSON),可以看到仅靠评分数值排序会混入多类非目标记录:

快照统计层级 (2026-11-04 截面)记录总数8~9 分高分记录数 (占比)0~2 分低分记录数 (占比)8~9 分记录的 Z-Score 阈值切分与分布特征
全库原始 CSV 流 (/stable/scores-bulk)62,886 条2,880 条 (4.58%,其中 9 分 404 条占 0.64%)12,491 条 (19.86%,含 0 分 617 条)31.53%(908 / 2,880)满足 Z < 1.81;混有非美元财报及全球多市场跨币种原币金额
第一层:限定美元财报 (reportedCurrency == "USD")18,046 条1,013 条 (5.61%,其中 9 分 177 条、8 分 836 条)4,594 条 (25.46%,含 0 分 237 条)33.27%(337 / 1,013)满足 Z < 1.81,18.76%(190 条)位于 1.81~2.99 区间
第二层:美元 8~9 分且叠加 Z > 2.99 阈值486 条 (待进一步清洗的候选记录)486 条 (占美元 8~9 分记录的 47.98%)已按阈值过滤全部满足 F >= 8 且 Z > 2.99;尚未按上市交易所、证券类型与行业联表剔除

在基于 /stable/scores-bulk 构建实盘选股漏斗时,必须注意上表揭示的四项数据边界(尤其注意表三的 486 条仅是满足美元财报、F >= 8、Z > 2.99 的候选记录数,并非已完成美股普通股清洗后的最终股票数):

  1. 格式边界:Bulk 接口返回 CSV 文本流而非 JSON 数组:与常规 /stable/* 接口返回 JSON 不同,/stable/scores-bulk 与 Company Profile Bulk(公司概况批量接口,/stable/profile-bulk)均直接返回带表头的 CSV 文本流(详见 FMP 数据 API 全面测评),在 Python 中须用 csv.DictReader(io.StringIO(text)) 流式解析,直接调用 .json() 会报错。
  2. 币种与上市地边界:reportedCurrency 仅代表财报记账币种,不等于美股交易所挂牌:以台积电美股 ADR(TSM)为例,其 piotroskiScore 为 8 分、altmanZScore 为 16.52,但其 reportedCurrency 为新台币 TWD(marketCap 亦按同币种折算为 68.97 万亿新台币);反之,部分非美交易所挂牌公司或 OTC 凭证也会采用 USD 披露财报。若不先校验 reportedCurrency 就直接按 float(row["marketCap"]) > 10e9 过滤市值,会把大批以日元(JPY)、新台币(TWD)计价的中小盘记录误判为百亿美元大盘股。
  3. 为什么美元 8~9 分记录中有 33.27%(337 / 1,013)落入 altmanZScore < 1.81? 这里的 33.27% 仅表示低于 1.81 阈值的记录占比,绝不能解释为破产风险发生率。核查存档中的这 337 条记录,可见四类典型成因:

四、 Python 实战代码:10 家美股 9 项得分逐位复算与双向勾稽断言

下面给出仅依赖 Python 标准库的单股 9 项复算与对账脚本(54 行)。脚本从 https://quote-fmp.com/stable 拉取单股财务评分与最近 8 个季度的三大报表,在校验三表 8 季财期严格对齐与分母非零后,逐位算出 F1..F9 九个二元得分项及 altmanZScore,并通过 assert 断言与接口返回值比对。

考虑到试用档密钥配额为 20 RPM(每分钟 20 次请求),而遍历 10 家公司需发起 40 次请求,脚本内置了请求间隔与 HTTP 429 分钟级窗口退避。运行前请设置环境变量 export FMP_KEY="你的密钥"(Windows PowerShell 使用 $env:FMP_KEY="你的密钥"):

import json, os, time, urllib.error, urllib.request

BASE_URL, API_KEY = "https://quote-fmp.com/stable", os.environ["FMP_KEY"]


def fmp_get(path: str, **params) -> list[dict]:
    params["apikey"] = API_KEY
    url = f"{BASE_URL}/{path}?" + "&".join(f"{k}={v}" for k, v in params.items())
    for attempt in range(3):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": "fmpquote-fscore/1.0"})
            with urllib.request.urlopen(req, timeout=20) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            if e.code == 429 and attempt < 2:
                wait_s = max(1.0, float(e.headers.get("Retry-After", "60")))
                time.sleep(wait_s)
                continue
            raise


def audit_piotroski_9(sym: str, pace_sec: float = 0.0) -> dict:
    sc = fmp_get("financial-scores", symbol=sym)[0]
    inc = fmp_get("income-statement", symbol=sym, period="quarter", limit=8)
    bal = fmp_get("balance-sheet-statement", symbol=sym, period="quarter", limit=8)
    cf = fmp_get("cash-flow-statement", symbol=sym, period="quarter", limit=8)
    if pace_sec > 0: time.sleep(pace_sec)
    dates = [x["date"] for x in inc]
    assert len(dates) == 8 and dates == [x["date"] for x in bal] == [x["date"] for x in cf], f"财期未对齐: {sym}"
    ta0, ta4, tl0 = bal[0]["totalAssets"], bal[4]["totalAssets"], sc["totalLiabilities"]
    assert ta0 > 0 and ta4 > 0 and tl0 > 0 and inc[0]["revenue"] > 0 and inc[4]["revenue"] > 0, f"分母异常: {sym}"
    ni_ttm, ocf_ttm = sum(x["netIncome"] for x in inc[:4]), sum(x["operatingCashFlow"] for x in cf[:4])
    rev_ttm, rev_prev = sum(x["revenue"] for x in inc[:4]), sum(x["revenue"] for x in inc[4:8])
    cr0, cr4 = bal[0]["totalCurrentAssets"] / bal[0]["totalCurrentLiabilities"], bal[4]["totalCurrentAssets"] / bal[4]["totalCurrentLiabilities"]
    gm0, gm4 = inc[0]["grossProfit"] / inc[0]["revenue"], inc[4]["grossProfit"] / inc[4]["revenue"]
    flags = [
        int((ni_ttm / ta0) > 0), int(ni_ttm > 0), int(ocf_ttm > 0), int(ocf_ttm > ni_ttm),
        int(cr0 > cr4), int(bal[0]["longTermDebt"] < bal[4]["longTermDebt"]),
        int(inc[0]["weightedAverageShsOut"] < inc[4]["weightedAverageShsOut"]),
        int(gm0 > gm4), int((rev_ttm / ta0) > (rev_prev / ta4)),
    ]
    z_calc = (1.2 * sc["workingCapital"] + 1.4 * sc["retainedEarnings"] + 3.3 * sc["ebit"] + 1.0 * sc["revenue"]) / ta0 + 0.6 * sc["marketCap"] / tl0
    assert sum(flags) == sc["piotroskiScore"] and abs(z_calc - sc["altmanZScore"]) < 1e-6, f"口径不一致: {sym}"
    return {"symbol": sym, "date": dates[0], "f_score": sum(flags), "flags": flags, "z_score": round(z_calc, 2)}


for ticker in ["AAPL", "MU", "AMD", "NVDA", "MSFT", "TSLA", "BA", "INTC", "GOOGL", "SMCI"]:
    print(audit_piotroski_9(ticker, pace_sec=1.2))

根据上述函数返回结果整理如下,10 家公司均通过三表财季对齐检查与 assert 双向断言(例如 AAPL 输出 {'symbol': 'AAPL', 'date': '2026-09-26', 'f_score': 9, 'flags': [1, 1, 1, 1, 1, 1, 1, 1, 1], 'z_score': 12.48},NVDA 输出 {'symbol': 'NVDA', 'date': '2026-07-26', 'f_score': 6, 'flags': [1, 1, 1, 0, 1, 0, 1, 1, 0], 'z_score': 37.59};完整 80 个季度的三表输入与复算值均已归档于同目录实测证据 JSON)。


五、 常见问答(FAQ)

1. /stable/financial-scores 只返回当前最新一期的评分快照,做历史回测时能否直接把 limit=8 改大来生成历史序列?

不能仅靠修改 limit 参数实现。首先,第四节的 audit_piotroski_9 固定读取最新截面切片 [:4] 与 [4:8];若拉取 limit=40(10 年季报),需要编写滑动窗口循环,在确认三表历史财期逐季对齐后,按偏移量 i 依次截取 [i:i+4] 与 [i+4:i+8],且 40 个连续季度最多只能生成 33 个完整 8 季滚动 F-Score 窗口。其次,历史各期的 Altman Z-Score 第四项 D = marketCap / totalLiabilities 必须关联对应评估日的历史市值,不能复用最新评分接口里的 sc["marketCap"]。最后,在回测严谨性上,须区分“基于现有报表重建历史滚动序列”与“严格 Point-in-Time(PIT)回测”:按每期财报的 acceptedDate(SEC 接收时间戳)截断仅能避免在财报披露前提前使用该期数据,但无法消除上市公司事后会计重述(Restatement)对历史报表数值的覆盖(回测防前视与退市样本处理可参考 消除美股回测幸存者偏差实战)。若需观察 FMP 预计算的 DCF、ROE、ROA、D/E、P/E、P/B 六维字母评级(S+ 至 D-)历史序列,可配合调用 Analyst(分析师数据接口) 分类下的 Ratings Snapshot(评级快照接口,/stable/ratings-snapshot)与 Historical Ratings(历史财务评分接口,/stable/ratings-historical)。

2. 为什么部分长期大额回购的消费或医药股 Altman Z-Score 偏低,筛选时该如何结合现金流指标交叉核验?

成熟期现金牛公司在长期执行大额股票回购与现金分红时,会通过两条路径影响 Altman Z-Score:一是当期分红或注销式回购冲减留存收益(retainedEarnings),直接压低第二项 B = retainedEarnings / totalAssets(例如表一中 AAPL 2025 财年末至 2026 财年一季末的 retainedEarnings 曾一度为负值);二是若公司通过发行债券筹资回购股票,会推高分母总负债(totalLiabilities),从而压低第四项 D = marketCap / totalLiabilities(注意 D 项分子是总市值而非账面股东权益)。实盘初筛非金融股时,对 piotroskiScore >= 8 且 OCF_TTM > NI_TTM > 0 的公司,可联动 Key Metrics TTM(TTM 关键指标接口,/stable/key-metrics-ttm)核验自由现金流收益率(freeCashFlowYieldTTM)与净债务/EBITDA(netDebtToEBITDATTM),将“高现金流支撑的回购型公司”与“经营失血的高负债公司”区分开来。如需在本地复现上述 10 家美股 9 项得分复算代码,可前往 快速接入指南 免费申领试用 Key(20 RPM / 0.3 GiB,滚动 30 天额度,支持国内直连与微信扫码开通),5 分钟内即可跑通单股财务健康度核验流程。

登录后可领取FMP Ultimate档15天试用 Key,覆盖全量接口。免费试用 15 天 →