Quant Trading for Programmers 07: Build A Clean A-Share Stock Universe First
Quant Trading for Programmers 07: Build A Clean A-Share Stock Universe First
Article 6 added database schema and migration checks. Now we can start handling stock master data.
This article does one thing: convert a raw A-share list into a clean, stable, reusable stock universe. This step is easy to underestimate. If the universe is stitched together casually, later strategy returns, backtest coverage, and paper-trading observation will all be polluted.

A Stock Universe Is Not A Stock List
A stock list is the raw output from a data source. A stock universe is the boundary of “which symbols this research run is allowed to consider”.
Those are very different things.
Raw lists may contain inconsistent symbol formats, duplicate rows, ST names, delisting-board names, missing industries, or missing market suffixes. A stock universe should block those problems before they enter strategy code.
Normalize Symbol Format First
A-share symbols often appear in mixed formats:
600519
600519.SH
300750.sz
000001.SZ
Strategy code and database tables should not accept that chaos. Chapter 7 adds app/stock_universe.py, and the first step is to normalize symbols into 000000.SH/SZ:
def normalize_a_share_symbol(value: str) -> str | None:
text = str(value or "").strip().upper()
if not text:
return None
if "." in text:
code, market = text.split(".", 1)
else:
code = text
market = "SH" if code.startswith(("6", "9")) else "SZ"
if not re.fullmatch(r"\d{6}", code):
return None
if market not in {"SH", "SZ"}:
return None
return f"{code}.{market}"
This is not a perfect exchange-detection system, but it is explicit enough for the current Shanghai/Shenzhen universe. If Beijing Stock Exchange support is added later, it can be extended here instead of scattering symbol-format logic across strategies.
Filter ST And Delisted Names Early
For early practical work, ST and delisting-board names should not be mixed into the public stock universe.
The reason is simple: their trading rules, risk exposure, and liquidity constraints are more special. Before the normal A-share path is stable, mixing high-risk boundaries into it makes debugging noisy.
def is_tradeable_a_share_name(name: str) -> bool:
normalized = str(name or "").strip().upper()
if not normalized:
return False
return "退市" not in normalized and not normalized.startswith(("ST", "*ST"))
This is not investment advice. It is an engineering boundary. Once the platform has finer risk classification, these stocks can be moved into separate research universes.
Convert Raw Rows Into Candidate Objects
Chapter 7 defines StockCandidate:
@dataclass(frozen=True)
class StockCandidate:
symbol: str
name: str
market: str
sector: str
lot_size: int = 100
source: str = "manual"
Then stock_candidate_from_row() accepts different field names:
candidate = stock_candidate_from_row(
{"code": "600036", "股票名称": "招商银行", "行业": "银行", "source": "eastmoney"}
)
The result becomes:
600036.SH / 招商银行 / SH / 银行 / source=eastmoney
This belongs in a pure function. Vendor fields may change, but if the entry function adapts, later database writes and strategy research do not need to change.
Build The Public Stock Universe
The public-universe builder is short:
def build_public_universe(rows: Iterable[dict], limit: int = 500, default_source: str = "manual") -> list[StockCandidate]:
out: list[StockCandidate] = []
seen: set[str] = set()
for row in rows:
candidate = stock_candidate_from_row(row, default_source=default_source)
if not candidate or candidate.symbol in seen:
continue
out.append(candidate)
seen.add(candidate.symbol)
if len(out) >= limit:
break
return out
It performs three explicit actions: filter bad data, deduplicate by symbol, and stop after reaching the limit.
The early public-universe size is 500 stocks. There is nothing magical about 500. It keeps local backtests, coverage checks, and article examples fast. After the pipeline stabilizes, it can expand to the full market.
The Universe Must Be Explainable
Building a universe is not enough. We also need to explain what it looks like.
universe_summary() summarizes by market, sector, and source:
{
"count": 3,
"by_market": {"SH": 1, "SZ": 2},
"by_source": {"eastmoney": 2, "qveris": 1},
"sample": ["600519.SH", "000001.SZ", "300750.SZ"],
}
This kind of summary will later enter data-quality reports. It is not for decoration. Before a backtest, it helps answer: is the universe accidentally concentrated in one sector? Did fallback data leak in? Does market distribution look abnormal?
Runnable Foundation Check
The stock universe must be explainable. The current project uses this command to reproduce the foundation capabilities from articles 01-08:
uv run python -m scripts.chapter_examples foundation-check
The output for this chapter is:

The sample input contains duplicate symbols and an ST name. The final universe keeps only 600519.SH and 000001.SZ. The summary also shows market, sector, and source distribution, which is more useful for diagnosis than only printing a stock list.
Hands-On Task
Clone the chapter 7 code:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-07
uv sync --extra dev
uv run pytest
Run only the stock-universe tests:
uv run pytest tests/test_stock_universe.py
The full chapter 7 test suite passes with 155 passed, still with only the existing FastAPI deprecation warning.
Chapter Update And Repository
This chapter adds:
app/stock_universe.py.- A-share symbol normalization, ST/delisting filters, deduplication, size limits, and universe summaries.
tests/test_stock_universe.py, covering key stock-universe boundaries.
Repository:
https://github.com/ax2/zi-quant-platform
Code for this chapter:
git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-07
uv sync --extra dev
uv run pytest tests/test_stock_universe.py
Summary
The stock universe is the research boundary of a quant system.
This article does not write a strategy yet. It first turns symbol normalization, ST/delisting filters, deduplication, size limits, and source summaries into testable code. The next article continues downward: after we have a stock universe, how do we clean raw K-line data into consistent daily bars?
Follow ZiCode on WeChat
If this post was useful, you can follow later updates on WeChat as well.
X / Twitter
Follow @ax2_zicode
Faster technical notes, short thoughts, and new-post alerts are posted on X.
More in this column
- Quant Trading for Programmers 11: From One Stock To A Portfolio Backtest
- Quant Trading for Programmers 10: Run The First Minimal Backtest Loop
- Quant Trading for Programmers 09: From K-Lines To The First Explainable Factor Signal
- Quant Trading for Programmers 08: Clean Raw K-Lines Into Trustworthy Market Bars
- Quant Trading for Programmers 06: Make The Database Schema Explicit First
- Quant Trading for Programmers 05: Data Matters More Than Strategy
- Quant Trading for Programmers 04: How A-Share Trading Rules Change Code
- Quant Trading for Programmers 03: Core Concepts In Quant Trading