实战:价值投资筛选与异常值排查
前面三篇讲了语法、因子、现成方案。这篇是完整实战:写一个四条件的价值筛选 → 跑出结果 → 对筛出来的第一名做完整排查,展示"看着像捡钱的值"是怎么一步步被验证为"不可用"的。
一、先定规则:价值筛选的四个维度
价值投资不是"PE 越低越好",而是同时满足多个条件才算"便宜且不差"。我用的四维规则(每个维度各选一个代表因子):
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 降序,所以极端值必然排在最前):
| # | code | name | PE_TTM | ROETTM |
|---|---|---|---|---|
| 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% 明显是另一个量级。下面完整排查它。
三、排查流程(四步定位根因)
排查的原则是:不看单项数字,看同一份财报里其他字段是否与它互相印证。四步走:
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 元 | 面值附近(市场也不认) |
关键推理链:
五、把排查做成可复用的函数
一次排查不如固化成规则。下面这个函数把上面的判断逻辑写成代码,可以对任何筛选结果批量跑:
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%)应按自己的风险偏好调整;我们只提供方法,不提供投资建议
- "有标记"≠"不能买" —— 标记的含义是"这个因子值需要人工再看一眼",不是结论
- 体检不覆盖所有风险 —— 它只能发现"数据层面的矛盾",发现不了行业风险、政策风险、治理问题
- 同一套规则要定期回看 —— 随着使用会发现新类型异常,持续补充规则(这也是它值得固化为代码的原因)
常见问题
按单因子排序时,极端值天然排最前。可能是真机会,也可能是口径问题——本文演示了完整的四步排查流程来判断属于哪种。
本文的 70% 负债率 / 10 倍量级差都是示例。建议按自己的风险偏好调整,并在使用中记录"哪些标记后来被验证是真问题",逐步校准。
不能。它只做两件事:把全市场缩到几十只(筛选)、把有数据矛盾的挑出来(体检)。剩余判断(行业、竞争、管理层、估值合理性)不在数据层面,需要你自己完成。
一个 Key(/v1/screen 与 /v1/finance 都是付费端点)。文档里的表格与代码均为线上真实返回复现,可直接照跑。
最后更新: 2026-09-29
所有表格数字与代码输出均来自 ashareapi /v1/screen 与 /v1/finance 的线上真实返回(2026-09-29 实测复现);体检规则设计参考实测验证记录(2026-08-31)。