Quant Trading for Programmers 07: Build A Clean A-Share Stock Universe First
· 20 min read · Views --
Last updated on

Quant Trading for Programmers 07: Build A Clean A-Share Stock Universe First

Author: Alex Xiang


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.

Cover image for article 7 in Quant Trading for Programmers

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:

Stock-universe foundation-check output

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?