Value screening in practice & outlier investigation
The previous three guides covered syntax, factors and ready-made strategies. This is the full walkthrough: write a four-condition value screen, run it, then investigate the top result in depth to show how a "looks like free money" value is verified as unusable step by step.
1. Rules first: four dimensions of value screening
Value investing is not "the lower the PE the better" — it is meeting several conditions at once. My four-dimension rule (one representative factor each):
import requests
BASE = "https://api.ashareapi.com/v1"
H = {"Authorization": "Bearer ct-your-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 hits (limit-capped)" % len(rows))
for i, row in enumerate(rows, 1):
print("%2d. %s %-8s PE=%-7s ROE=%-10s debt=%-8s"
% (i, row["code"], row["name"], row["PE_TTM"],
row["ROETTM"], row.get("DebtAssetsRatio", "-")))| Dimension | Factor | Condition | Why |
|---|---|---|---|
| Valuation | PE_TTM | < 20 (and > 0) | Cheap; > 0 excludes losses |
| Profitability | ROETTM | > 15 | High capital efficiency |
| Leverage | DebtAssetsRatio | < 60 | Exclude leverage-inflated ROE |
| Cash flow | NetOperateCashFlowTTM | > 0 | Profits backed by cash |
2. Results (live run, 2026-09-29)
Sorted by ROE descending, so extremes surface first:
| # | code | name | PE_TTM | ROETTM |
|---|---|---|---|---|
| 1 | sz000656 | Jinke Property | 0.36 | 883.0546 |
| 2 | sz300972 | Wanchen Group | 14.33 | 77.2634 |
| 3 | sh600132 | Chongqing Brewery | 15.54 | 73.3501 |
| 4 | sh688331 | RemeGen | 11.28 | 70.0198 |
Rows 2-4 look plausible (PE 11-16x, ROE 70%+). But row 1 — PE 0.36 / ROE 883% — is on another scale entirely. Full investigation below.
3. The investigation: four steps to the root cause
Principle: do not judge a single number; check whether other fields in the same statement corroborate it. Four steps:
CODE = "sz000656"
r = requests.get(f"{BASE}/finance", params={"code": CODE, "limit": 1}, headers=H, timeout=40)
tables = r.json()["data"] # [income, balance sheet, cash flow]
income, balance = tables[0][0], tables[1][0]
print("=== Step 2: ROE bases ===")
for k in ("ROE", "ROETTM", "ROEWeighted", "ROECut"):
print(" %-14s %s" % (k, income.get(k)))
print("=== Step 3: current vs TTM ===")
print(" current profit ", income.get("NPParentCompanyOwners"))
print(" TTM profit ", income.get("NPParentCompanyOwnersTTM"))
print("=== Step 4: cross-verification ===")
print(" ex-non-recurring profit", income.get("NPDeductNonRecurringPL"))
print(" debt-to-equity ", balance.get("DebtEquityRatio"))
print(" shareholders equity ", balance.get("TotalShareholderEquity"))
print(" total liabilities ", balance.get("TotalLiability"))- Step 1: pull the raw statement — ignore the screen output, call `/v1/finance` for the full income statement + balance sheet
- Step 2: compare bases — the same metric often has several bases (`ROE` / `ROETTM` / `ROEWeighted` / `ROECut`); check whether magnitudes agree
- Step 3: current vs TTM — compare `NPParentCompanyOwners` (current) with `NPParentCompanyOwnersTTM` (trailing)
- Step 4: cross-verify — check ex-non-recurring (`ROECut`), leverage (`DebtEquityRatio`) and price as independent evidence
4. Result: all four steps say "this value is unusable"
- `ROE`(0.44) / `ROEWeighted`(0.45) / `ROECut`(-1.71) — three bases agree (all in the "barely profitable to loss-making" range)
- Only `ROETTM`(883) stands alone on the other side — an extreme minority value like this is usually the miscalculated or special-item one
- `NPParentCompanyOwnersTTM`(36.87 bn) vs current (0.019 bn) differ ~1900x → TTM is contaminated by a one-off item
- Conclusion: `ROETTM` is unusable (if ROE is needed, use `ROECut` or `ROEWeighted`)
| Step | Field | Value | Conclusion |
|---|---|---|---|
| 2 | ROETTM | 883.05 | The value used by the screen |
| 2 | ROE | 0.44 | Same metric, other basis — ~2000x apart |
| 2 | ROEWeighted | 0.45 | Weighted basis — agrees with ROE, contradicts ROETTM |
| 2 | ROECut | -1.71 | Ex-non-recurring negative — core business loss-making |
| 3 | NPParentCompanyOwners (current) | 0.019 bn CNY | Normal magnitude |
| 3 | NPParentCompanyOwnersTTM | 36.87 bn CNY | ~1900x from current — clearly anomalous |
| 4 | NPDeductNonRecurringPL | -0.071 bn CNY | Ex-non-recurring loss, corroborates ROECut |
| 4 | DebtEquityRatio | 200.67% | High leverage |
| 4 | ClosePrice | 1.25 CNY | Near par value (market disagrees too) |
Reasoning chain:
5. Turn the investigation into a reusable function
One-off checks are worse than rules. The function below encodes the logic above so it can run over any screen output:
def check_factor_health(code: str) -> list:
# Returns a list of risk notes; empty list = nothing obvious found
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
# 1) negative ex-non-recurring ROE -> profit may be non-recurring
if f(income, "ROECut") < 0:
flags.append("ex-non-recurring ROE negative")
# 2) ROE bases differ by an order of magnitude -> one basis is distorted
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) vs ROETTM(%.2f) inconsistent" % (roe, roettm))
# 3) TTM profit far from current period
cur, ttm = f(income, "NPParentCompanyOwners"), f(income, "NPParentCompanyOwnersTTM")
if cur > 0 and ttm and (ttm > cur * 10 or cur > ttm * 10):
flags.append("current(%.2f) vs TTM(%.2f) diverging" % (cur, ttm))
# 4) high leverage
if f(balance, "DebtAssetsRatio") > 70:
flags.append("debt ratio %.1f%% (high)" % f(balance, "DebtAssetsRatio"))
return flags
for row in body["structured"][:10]:
flags = check_factor_health(row["code"])
print("%s %-8s %s" % (row["code"], row["name"], " | ".join(flags) or "OK"))6. Live output (same screened batch)
sz000656 Jinke Property ROE(0.44) vs ROETTM(883.05) inconsistent | ex-non-recurring ROE negative | current(0.19) vs TTM(368.66) diverging | debt ratio 200.7% (high)
sz300972 Wanchen Group OK
sh600132 Chongqing Brewery OK
sh688331 RemeGen OK
sz001309 Demingli OKThe four rules caught the outlier precisely and did not flag the others — that is the value of turning checks into rules: screen once, health-check in bulk, only inspect the flagged ones.
7. Four cautions
- These rules are examples, not gospel — adjust thresholds (e.g. 70% debt) to your own risk appetite; we provide method, not investment advice
- Flagged is not the same as "do not buy" — a flag means "this factor value deserves a manual look", not a conclusion
- Health checks do not cover all risks — they find data-level contradictions only, not industry, policy or governance risk
- Revisit the rules periodically — new anomaly types emerge with use; keep adding rules (another reason to encode them)
FAQ
Sorting by a single factor puts extremes first. They may be genuine opportunities or basis problems — this guide walks the four-step investigation that distinguishes them.
The 70% debt and 10x magnitude figures are examples. Tune them to your risk appetite and track which flags later proved to be real problems, calibrating over time.
No. It does two things: narrow the market to dozens (screening) and surface data contradictions (health check). The remaining judgement — industry, competition, management, valuation — is not in the data and is yours to make.
A Key (/v1/screen and /v1/finance are both paid endpoints). Every table and code output is reproduced from live responses, so you can run them as-is.
Last updated: 2026-09-29
All numbers and code output come from live ashareapi /v1/screen and /v1/finance responses (reproduced 2026-09-29); the health-check design follows measured verification notes (2026-08-31).