Guide

Factor screening for China A-shares with one API call

Factor screening turns financial and market metrics into conditions and filters the whole market. This guide covers three things: how to write the expression, how to read the result, and why you must run a data health check afterwards (otherwise it is easy to mistake an outlier for an opportunity).

1. What factor screening is (30 seconds)

A factor is a computable metric: `PE_TTM` (price/earnings), `ROETTM` (return on equity), `DividendRatioTTM` (dividend yield), `DebtAssetsRatio` (debt ratio) and so on. Factor screening writes these into conditions and filters the whole market of 5,000+ stocks.

It replaces manually reading financial statements — turning an idea like "cheap + profitable + low debt" into a reproducible, backtestable rule.

  • Reproducible: the same conditions give the same result every time
  • Backtestable: with `--date` you can screen on a past day variation of factor values
  • Not investment advice: screening only filters by objective conditions — being selected does not mean you should buy

2. Quick start: one request for low-PE, high-ROE

`/v1/screen` is a paid endpoint (needs a Key). The most common form is `expr` with a multi-factor intersection — `intersect([...])` means all conditions must hold.

Python: PE between 0 and 20, ROE above 15%, sorted by ROE
import requests

BASE = "https://api.ashareapi.com/v1"
H = {"Authorization": "Bearer ct-your-key"}   # Key: https://ashareapi.com/pricing

r = requests.get(
    f"{BASE}/screen",
    params={
        "expr": "intersect([PE_TTM > 0, PE_TTM < 20, ROETTM > 15])",
        "orderby": "ROETTM",
        "desc": "1",
        "limit": 5,
    },
    headers=H,
    timeout=30,
)
body = r.json()
if not body.get("ok"):
    raise RuntimeError(body)                  # upstream failure returns ok=false (not counted)

for row in body["structured"]:                # structured = machine-readable rows
    print(row["code"], row["name"], row["PE_TTM"], row["ROETTM"])
  • `PE_TTM > 0` is a required guard — loss-making companies have negative PE and would otherwise appear as "low PE"
  • Percentage factors take plain numbers: `ROETTM > 15` means "above 15%" (not `0.15`)
  • Values are strings ("14.33") — call `float()` before arithmetic

3. What the result looks like (live run, 2026-09-29)

Real output of the request above (limit=5); `structured` is the machine-readable form:

sz000656
Jinke Property
0.36
883.0546
1.25
3.31
sz300972
Wanchen Group
14.33
77.2634
151.22
-2.81
sh600132
Chongqing Brewery
15.54
73.3501
37.30
-0.80
sh688331
RemeGen
11.28
70.0198
116.31
0.84
sz001309
Demingli
12.32
69.7845
370.69
-0.16

Do not celebrate yet — the first row (PE_TTM=0.36, ROETTM=883%) is exactly the kind of value you need to inspect. The next section shows how.

4. The 3-step data health check (important)

A factor value is a number a machine computed — not the company true operating picture. When you see extremes (very low PE, very high ROE, huge growth), cross-check with other fields from the same financial statement:

Python: health-check screened names (ex-non-recurring / multi-metric / leverage)
def health_check(code: str) -> None:
    # Inspect factor values: multi-metric ROE, ex-non-recurring, leverage, current profit
    r = requests.get(f"{BASE}/finance", params={"code": code, "limit": 1},
                     headers=H, timeout=30)
    tables = r.json()["data"]        # 3 tables: income / balance sheet / cash flow
    income, balance = tables[0][0], tables[1][0]

    print(code)
    print("  ROE variants:", income.get("ROE"), "/", income.get("ROETTM"),
          "/", income.get("ROEWeighted"), "/ ex-non-recurring", income.get("ROECut"))
    print("  Profit current:", income.get("NPParentCompanyOwners"),
          " TTM:", income.get("NPParentCompanyOwnersTTM"))
    print("  Debt ratio:", balance.get("DebtAssetsRatio"), "%  equity:",
          balance.get("TotalShareholderEquity"))

    try:
        if float(income.get("ROECut") or 0) < 0:
            print("  [!] ex-non-recurring ROE is negative - profit may be non-recurring")
        if float(balance.get("DebtAssetsRatio") or 0) > 70:
            print("  [!] debt ratio above 70% - ROE may be leveraged")
    except (TypeError, ValueError):
        pass

health_check("sz000656")
  • 1) Check ex-non-recurring: `ROECut` and `NPDeductNonRecurringPL`. If `ROETTM` is high but `ROECut` is negative, profit likely comes from non-recurring items (asset sales, debt restructuring), not the core business
  • 2) Check multi-metric consistency: the same statement carries `ROE` / `ROETTM` / `ROEWeighted` / `ROECut`. If they differ by orders of magnitude (e.g. 0.44 vs 883), one of them is distorted — do not use it directly
  • 3) Check leverage and the current period: `DebtEquityRatio` / `DebtAssetsRatio` (leverage inflates ROE) plus `NPParentCompanyOwners` (current-period profit). If TTM profit is far from the current period, the TTM value is suspect

5. Real case: what that 883% ROE actually is

The screened name sz000656 Jinke Property (PE_TTM 0.36 / ROETTM 883%) — one health check reveals the problem. From the same statement:

  • The verdict is not a hunch: within one statement, ROE=0.44 vs ROETTM=883 (~2000x), ex-non-recurring is negative, and TTM vs current profit diverge — the bases contradict each other, so this ROETTM should not be used as-is
  • Correct handling: this does not mean "the stock is bad", it means "this factor value is unreliable" — switch basis (use `ROECut`) or skip it
  • This is exactly why the check is mandatory: the API faithfully returns every basis; whether to exclude is a rule the consumer applies (we give no buy/sell conclusions)
ROETTM
883.05
The one used by the screener (extremely high)
ROE
0.44
Same metric, different basis (~2000x apart)
ROECut
-1.71
Ex-non-recurring ROE (negative = core loss)
NPParentCompanyOwnersTTM
36.87 bn CNY
Net profit TTM
NPParentCompanyOwners
0.019 bn CNY
Current-period profit (far from TTM)
DebtEquityRatio
200.67%
Debt-to-equity (high leverage)
ClosePrice
1.25 CNY
Share price (near par value)

6. Common factors cheat sheet

Valuation
PE_TTM
Price/earnings TTM (needs > 0)
x
Valuation
PB
Price/book (no PB_TTM)
x
Valuation
PS_TTM
Price/sales TTM
x
Valuation
TotalMV
Total market value
CNY
Valuation
DividendRatioTTM
Dividend yield TTM
%
Profitability
ROETTM
Return on equity TTM
percent
Profitability
ROECut
Ex-non-recurring ROE (health check)
percent
Profitability
GrossIncomeRatioTTM
Gross margin TTM
percent
Growth
NPParentCompanyYOY
Net profit YoY
%
Growth
OperatingRevenueGrowRate
Revenue growth
%
Leverage
DebtAssetsRatio
Debt-to-assets
%
Leverage
DebtEquityRatio
Debt-to-equity
%
Cash flow
NetOperateCashFlowTTM
Operating cash flow TTM
CNY

All 85 factors (6 valuation / 2 market / 38 profitability / 32 balance sheet / 14 cash flow) and 14 templates — see "Expression syntax and factor dictionary".

FAQ

Can I buy directly from screening results?

No. Screening only filters the market by objective conditions; being selected means "meets the rule", not "worth buying". It replaces the first step of manual statement reading. This guide only covers data retrieval and verification, not investment advice.

Why do I get extremely low PE and extremely high ROE?

Extremes often come from basis issues. Cross-check with ROECut (ex-non-recurring), other ROE bases, and DebtEquityRatio (leverage) from the same statement. When bases contradict, the factor value is not usable. Sections 4-5 give the full method and a real case.

Is screening available on the free tier?

No, /v1/screen is a paid endpoint. The 5 free endpoints are quote / kline / hot / market-overview / changedist — use those to verify connectivity first.

Why are numeric fields strings?

To avoid losing precision. Call float() before comparing or calculating; direct addition concatenates strings.

Last updated: 2026-09-29

Field list and strategies taken from live responses of ashareapi /v1/screen and /v1/finance (verified 2026-09-29); factor semantics and the health-check method come from measured verification (85 factors, 2026-08-31).