Guide

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):

Python: four-condition value screen
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", "-")))
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:

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:

Python: four-step investigation (all evidence at once)
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`)
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:

Python: reusable health check (returns risk flags)
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)

Output
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        OK

The 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

Why are the top PE / ROE values so extreme?

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.

How should the thresholds be chosen?

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.

Does this guarantee good stocks?

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.

What do I need to run these examples?

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).