Reading A-share shareholder counts: chip concentration
Holder count is the most direct public signal of "whose hands are the chips in": a falling count = concentration in fewer hands (often positive), a surging count = dispersion (retail crowding in). This guide covers the three tables, the fields, and a real case.
1. Why look at shareholder counts (30 seconds)
Short-term moves are driven by money; medium-term moves are decided by chip structure. The holder count is the clearest public signal of that structure:
- Count falling → chips moving from retail to a few hands (big money / institutions) — usually read as accumulation
- Count rising → chips dispersing (more retail taking over) — usually read as distribution / fading
- Average shares held = total shares / count, inversely related, also measures concentration
Note: "usually read" is not a hard signal — combine with price level and who is on both sides
2. One request, three tables
`/v1/shareholder` is a paid endpoint. One parameter: `code`. The `data` is an object with `tables` (three tables).
import requests
BASE = "https://api.ashareapi.com/v1"
H = {"Authorization": "Bearer ct-your-key"}
b = requests.get(f"{BASE}/shareholder",
params={"code": "sh600667"}, headers=H, timeout=60).json()
d = b["data"] # note: data is an object, not an array
for t in d["tables"]: # three tables, each with title/slug/rows
print("== [%s] %s ==" % (t["slug"], t["title"]))
for row in t["rows"][:5]:
print(" ", row)- `data` is an object (with `tables`), not a plain array — iterate with `data["tables"]`
- Do not use `body["structured"]` — shareholder data is a dict; that key is absent
3. What each of the three tables tells you
| Table (slug) | Content | Use |
|---|---|---|
| `top10_holders` | Top-10 shareholders | Who controls / whether big institutions are there |
| `top10_float_holders` | Top-10 float holders | Who is in the tradable float (closer to trading) |
| `holder_count` | Holder-count history | Chip concentration core: count trend over time |
`holder_count` is the most valuable — it is reported per quarter and shows the count trend (not a single point).
4. Reading the fields
| Field | Meaning | Unit | Note |
|---|---|---|---|
| holdShares | Shares held | shares | How many the holder owns |
| holdPct | Holding % | % | Share of total shares |
| holdChange | Holding change | shares | + = added / - = reduced / 0 = unchanged |
| totalSHNum | Total holder count | holders | Count at the period |
| avgHoldShares | Avg shares per holder | shares | Total shares / count (inverse to count) |
| date | As-of date | - | Report date (important: lags) |
Two counts: `totalSHNum` (total) and `aSHNum` (A-share) are usually equal; `avgHoldShares` / `aAvgHoldShares` likewise.
5. Real case: Tai Ji Industry (600667) count triples in a year
Live holder_count table returned 2026-09-29:
- Trend: 153.6k → 178.2k → 548.9k, a 3.5x jump; avg shares fell from 13,614 to 3,810
- Meaning: lots of new retail flooded in, chips moved from concentrated to heavily dispersed — a classic "high-level distribution / retailisation" pattern
- Combine with price: if price rose then stalled, "count surge + price stalling" is a distribution risk; if it happens at a low after volume expansion, it may start a new rotation (needs more context; not a conclusion here)
| Date | Holder count | Avg shares | Read |
|---|---|---|---|
| 2025-12-31 | 153,627 | 13,614 | Baseline |
| 2026-03-31 | 178,193 | 11,738 | +16%, chips starting to spread |
| 2026-06-30 | 548,947 | 3,810 | Count tripled — chips heavily dispersed |
Why this is a good case: it shows the proper use of the metric — read the trend, not a single point, and pair it with the price level.
6. Bulk scan (find "chips concentrating")
CODES = ["sh600667", "sh600519", "sz300750", "sh601318",
"sh600036", "sz000858", "sh600030", "sh601899"]
for code in CODES:
b = requests.get(f"{BASE}/shareholder", params={"code": code},
headers=H, timeout=60).json()
d = b.get("data") or {}
for t in d.get("tables", []):
if t.get("slug") != "holder_count":
continue
rows = t.get("rows", []) # latest first
if len(rows) < 2:
continue
latest = int(rows[0].get("totalSHNum") or 0)
prev = int(rows[1].get("totalSHNum") or 0)
if prev > 0 and latest < prev * 0.9: # count down >10% = concentrating
print("%s %s count %d → %d [concentrating]" % (
code, rows[0].get("date"), prev, latest))- Find concentration: `latest < prev * 0.9` (down >10% from prior) — chips moving to fewer hands
- Find dispersion (reverse): `latest > prev * 1.2` (up >20%) — retail crowding in (can be a avoid signal)
- Mind the lag: counts are quarterly (a 2026-06-30 count only appears by end of August) — not a "today" signal
7. Two common misreads (important)
- 1. Count data lags — quarterly, not real-time. Look at the `date` (as-of) field and do not treat it as the latest
- 2. Falling count is not always bullish, rising not always bearish — it is a mirror of chip structure. Combine with: price level (high-level dispersion is risky / low-level concentration may accumulate), who is on both sides (institution vs retail), and the overall market
FAQ
No. Disclosed per quarterly report (with lag). The date field marks the as-of date — use it to know which period the data is, not the latest.
Usually a positive "chip concentration" signal, but not absolute. Combine with price level: low-level concentration (may accumulate) differs from high-level concentration (may already have run). It is a structural mirror, not a buy/sell conclusion.
Top-10 holders are a single point (who holds now); holder count is a trend (how concentration is changing). They complement: top-10 shows "who is there", count shows "concentrating or dispersing".
shareholder returns multiple tables (top-10 / float / count), carried in an object with a tables array. Iterate with data["tables"], not body["structured"].
Last updated: 2026-09-29
Fields and structure from live ashareapi /v1/shareholder responses (2026-09-29 measured: three-table structure, field semantics); case data is the real holder_count series of Tai Ji Industry (600667) across 2025-12-31 / 2026-03-31 / 2026-06-30.