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.
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:
| code | name | PE_TTM | ROETTM | ClosePrice | ChangePCT |
|---|---|---|---|---|---|
| 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:
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)
| Field | Value | Meaning |
|---|---|---|
| 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
| Group | Factor | Meaning | Unit |
|---|---|---|---|
| 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
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.
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.
No, /v1/screen is a paid endpoint. The 5 free endpoints are quote / kline / hot / market-overview / changedist — use those to verify connectivity first.
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).