教程

实战:价值投资筛选与异常值排查

前面三篇讲了语法、因子、现成方案。这篇是完整实战:写一个四条件的价值筛选 → 跑出结果 → 对筛出来的第一名做完整排查,展示"看着像捡钱的值"是怎么一步步被验证为"不可用"的。

一、先定规则:价值筛选的四个维度

价值投资不是"PE 越低越好",而是同时满足多个条件才算"便宜且不差"。我用的四维规则(每个维度各选一个代表因子):

Python:四条件价值筛选
import requests

BASE = "https://api.ashareapi.com/v1"
H = {"Authorization": "Bearer ct-你的Key"}

EXPR = ("intersect([PE_TTM > 0, PE_TTM < 20, ROETTM > 15, "
        "DebtAssetsRatio < 60, NetOperateCashFlowTTM > 0])")

r = requests.get(f"{BASE}/screen",
                 params={"expr": EXPR, "orderby": "ROETTM", "desc": "1", "limit": 10},
                 headers=H, timeout=40)
body = r.json()
assert body["ok"], body

rows = body["structured"]
print("命中 %d 只(最多返回 limit 条)" % len(rows))
for i, row in enumerate(rows, 1):
    print("%2d. %s %-8s PE=%-7s ROE=%-10s 负债=%-8s"
          % (i, row["code"], row["name"], row["PE_TTM"],
             row["ROETTM"], row.get("DebtAssetsRatio", "-")))
估值
PE_TTM
< 20(且 > 0)
便宜;> 0 排除亏损
盈利
ROETTM
> 15
赚钱效率高
杠杆
DebtAssetsRatio
< 60
排除高杠杆撑起来的 ROE
现金流
NetOperateCashFlowTTM
> 0
利润有现金支撑

二、结果(2026-09-29 实跑)

跑出来会看到类似的排序(ROE 降序,所以极端值必然排在最前):

1
sz000656
金科股份
0.36
883.0546
2
sz300972
万辰集团
14.33
77.2634
3
sh600132
重庆啤酒
15.54
73.3501
4
sh688331
荣昌生物
11.28
70.0198

第 2~4 名看起来都在合理范围(PE 11~16 倍、ROE 70%+)。但第 1 名 PE=0.36 / ROE=883% 明显是另一个量级。下面完整排查它。

三、排查流程(四步定位根因)

排查的原则是:不看单项数字,看同一份财报里其他字段是否与它互相印证。四步走:

Python:四步排查(一次拿全部证据)
CODE = "sz000656"

r = requests.get(f"{BASE}/finance", params={"code": CODE, "limit": 1}, headers=H, timeout=40)
tables = r.json()["data"]                 # [利润表, 资产负债表, 现金流]
income, balance = tables[0][0], tables[1][0]

print("=== 第二步:ROE 多口径对比 ===")
for k in ("ROE", "ROETTM", "ROEWeighted", "ROECut"):
    print("  %-14s %s" % (k, income.get(k)))

print("=== 第三步:当期 vs TTM ===")
print("  当期归母净利   ", income.get("NPParentCompanyOwners"))
print("  归母净利 TTM   ", income.get("NPParentCompanyOwnersTTM"))

print("=== 第四步:交叉验证 ===")
print("  扣非归母净利   ", income.get("NPDeductNonRecurringPL"))
print("  产权比率       ", balance.get("DebtEquityRatio"))
print("  股东权益       ", balance.get("TotalShareholderEquity"))
print("  总负债         ", balance.get("TotalLiability"))
  • 第一步:拉原始财务报表 —— 不看筛选结果,直接查 `/v1/finance`,拿到利润表 + 资产负债表全部字段
  • 第二步:多口径对比 —— 同一指标往往有多个口径(`ROE` / `ROETTM` / `ROEWeighted` / `ROECut`),比对是否量级一致
  • 第三步:当期 vs TTM —— 对比 `NPParentCompanyOwners`(当期)与 `NPParentCompanyOwnersTTM`(滚动),看是否脱节
  • 第四步:交叉验证 —— 用扣非(`ROECut`)、杠杆(`DebtEquityRatio`)、股价做独立印证

四、排查结果:四步全部指向"该值不可用"

  • `ROE`(0.44) / `ROEWeighted`(0.45) / `ROECut`(-1.71) 三个口径互相一致(都在"几乎不赚钱到亏损"区间)
  • 只有 `ROETTM`(883) 单独站在另一边 —— 少数派且极端的那个,通常是算错或含特殊项的那个
  • `NPParentCompanyOwnersTTM`(368.7 亿) 与当期(0.19 亿) 相差 1900 倍 → TTM 被某个一次性事项污染
  • 结论:`ROETTM` 不可用(若一定要用 ROE,应改用 `ROECut` 或 `ROEWeighted`)
第二步
ROETTM
883.05
筛选使用的值
第二步
ROE
0.44
同指标另一口径 —— 相差 2000 倍
第二步
ROEWeighted
0.45
加权口径 —— 与 ROE 一致,与 ROETTM 矛盾
第二步
ROECut
-1.71
扣非为负 —— 主营不赚钱
第三步
NPParentCompanyOwners(当期)
0.19 亿
正常量级
第三步
NPParentCompanyOwnersTTM
368.7 亿
与当期相差 1900 倍 —— 明显异常
第四步
NPDeductNonRecurringPL
-0.71 亿
扣非亏损,印证 ROECut
第四步
DebtEquityRatio
200.67%
高杠杆
第四步
ClosePrice
1.25 元
面值附近(市场也不认)

关键推理链:

五、把排查做成可复用的函数

一次排查不如固化成规则。下面这个函数把上面的判断逻辑写成代码,可以对任何筛选结果批量跑:

Python:可复用的体检函数(返回风险标记)
def check_factor_health(code: str) -> list:
    # 返回风险提示列表;空列表 = 未发现明显异常
    r = requests.get(f"{BASE}/finance", params={"code": code, "limit": 1},
                     headers=H, timeout=40)
    tables = r.json()["data"]
    income, balance = tables[0][0], tables[1][0]
    flags = []

    def f(d, k):
        try:
            return float(d.get(k) or 0)
        except (TypeError, ValueError):
            return 0.0

    # ① 扣非为负 -> 利润可能来自非经常性损益
    if f(income, "ROECut") < 0:
        flags.append("扣非 ROE 为负(利润或含非经常性损益)")

    # ② 多口径 ROE 量级不一致 -> 其中某个口径失真
    roe, roettm = f(income, "ROE"), f(income, "ROETTM")
    if roettm and roe and (abs(roettm) > abs(roe) * 10 or abs(roe) > abs(roettm) * 10):
        flags.append("ROE(%.2f) 与 ROETTM(%.2f) 量级不一致" % (roe, roettm))

    # ③ TTM 与当期净利脱节
    cur, ttm = f(income, "NPParentCompanyOwners"), f(income, "NPParentCompanyOwnersTTM")
    if cur > 0 and ttm and (ttm > cur * 10 or cur > ttm * 10):
        flags.append("当期净利(%.2f) 与 TTM(%.2f) 脱节" % (cur, ttm))

    # ④ 高杠杆
    if f(balance, "DebtAssetsRatio") > 70:
        flags.append("资产负债率 %.1f%%(偏高)" % f(balance, "DebtAssetsRatio"))

    return flags


for row in body["structured"][:10]:
    flags = check_factor_health(row["code"])
    mark = " ".join(flags) if flags else "OK"
    print("%s %-8s %s" % (row["code"], row["name"], mark))

六、实测输出(同一批筛选结果)

输出
sz000656 金科股份  ROE(0.44) 与 ROETTM(883.05) 量级不一致 | 扣非 ROE 为负(利润或含非经常性损益) | 当期净利(0.19) 与 TTM(368.66) 脱节 | 资产负债率 200.7%(偏高)
sz300972 万辰集团  OK
sh600132 重庆啤酒  OK
sh688331 荣昌生物  OK
sz001309 德明利    OK

四步规则精准命中了那个异常值,且没有误伤其余标的 —— 这就是"把排查固化成规则"的价值:筛一次,批量体检,只看有标记的。

七、使用注意(四条)

  • 体检规则是示例,不是标准答案 —— 阈值(如负债率 70%)应按自己的风险偏好调整;我们只提供方法,不提供投资建议
  • "有标记"≠"不能买" —— 标记的含义是"这个因子值需要人工再看一眼",不是结论
  • 体检不覆盖所有风险 —— 它只能发现"数据层面的矛盾",发现不了行业风险、政策风险、治理问题
  • 同一套规则要定期回看 —— 随着使用会发现新类型异常,持续补充规则(这也是它值得固化为代码的原因)

常见问题

为什么筛出来第一名的 PE / ROE 那么极端?

按单因子排序时,极端值天然排最前。可能是真机会,也可能是口径问题——本文演示了完整的四步排查流程来判断属于哪种。

体检规则里的阈值怎么定?

本文的 70% 负债率 / 10 倍量级差都是示例。建议按自己的风险偏好调整,并在使用中记录"哪些标记后来被验证是真问题",逐步校准。

这套方法能保证筛出好股票吗?

不能。它只做两件事:把全市场缩到几十只(筛选)、把有数据矛盾的挑出来(体检)。剩余判断(行业、竞争、管理层、估值合理性)不在数据层面,需要你自己完成。

要跑这些例子需要什么?

一个 Key(/v1/screen 与 /v1/finance 都是付费端点)。文档里的表格与代码均为线上真实返回复现,可直接照跑。

最后更新: 2026-09-29

所有表格数字与代码输出均来自 ashareapi /v1/screen 与 /v1/finance 的线上真实返回(2026-09-29 实测复现);体检规则设计参考实测验证记录(2026-08-31)。