Files
chinese-equity-quant/README.md
T
2026-06-16 15:55:30 +08:00

500 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Chinese Equity Quant Research Framework
A modular Chinese A-share quant research framework. Daily frequency only (Phase 1).
It is a **decoupled, file-based pipeline**: each phase reads parquet and writes
parquet, so phases run, cache, and inspect independently.
```
baostock (primary) one weight series
akshare (fallback) interpreted as a
│ portfolio
▼ ▲
┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ DATA │ │ ALPHA │ │ COMBO │ ┌────┴─────┐
│ download │─────▶│ compute │─────▶│ combine │ │ EVAL │
│ daily bars │ │ signal→weights│ │ merge alphas │ │ score it │
└──────┬───────┘ └───────┬───────┘ └───────┬───────┘ └────┬─────┘
│ │ │ │
▼ ▼ ▼ │
data/daily_bars/ alphas/*.pq combos/*.pq │
{universe}/ (ALPHA_COLUMNS) (COMBO_COLUMNS) │
month=YYYY-MM/*.pq │ │
(DATA_COLUMNS) │ │
└──────── price ───────┴───────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ PORTFOLIO │ │ SIMULATE │ PAPER TRADING
│ construct │──▶│ fills + costs│ │ forward / live │ TODO
│ positions │ │ + P&L │ execution
└──────┬───────┘ └──────────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ┘
portfolio/*.pq
(POSITION_COLUMNS)
Each phase reads parquet and writes parquet — run, cache, and inspect
independently. The only interface between phases is the parquet schema.
Solid boxes are implemented; dashed boxes are on the roadmap (see TODO below).
```
Data comes from **baostock (primary)** with **akshare (fallback)**.
## Install
The env is managed with [uv](https://docs.astral.sh/uv/). `uv sync` builds `.venv` from `pyproject.toml` + `uv.lock`; prefix commands with `uv run` (no manual activation needed).
```bash
uv sync
```
Backtrader is an optional dependency and is **not used by the current pipeline**.
Install it only for future experiments or adapter work:
```bash
uv sync --extra backtrader
```
## Quick start
```bash
# 1. Download daily bars for a few symbols (writes a month-partitioned dataset).
uv run python cli.py data download \
--universe sh600000,sz000001,sh600519 \
--start-date 2024-01-01 --end-date 2024-03-31 \
--output-dir data/daily_bars
# 2. Compute an alpha (position weights) from that data.
# --data-path is the dataset DIRECTORY ({output-dir}/{universe}).
uv run python cli.py alpha reversal \
--data-path "data/daily_bars/sh600000,sz000001,sh600519"
# 3. Evaluate it (return / Sharpe / turnover / drawdown).
uv run python cli.py alpha eval \
--alpha-path alphas/reversal_5d.pq \
--data-path "data/daily_bars/sh600000,sz000001,sh600519"
# 4. Build tradable integer positions from alpha or combo weights.
uv run python cli.py portfolio build \
--weights-path alphas/reversal_5d.pq \
--data-path "data/daily_bars/sh600000,sz000001,sh600519" \
--booksize 1000000 --portfolio-name reversal_port
# 5. Simulate next-open execution with A-share constraints, costs, and slippage.
uv run python cli.py portfolio simulate \
--positions-path portfolio/reversal_port.pq \
--data-path "data/daily_bars/sh600000,sz000001,sh600519" \
--constraint suspension --constraint price_limit --constraint volume_cap \
--cost-bps 5 --slippage-bps 5
# 6. Evaluate the constructed target weights as a continuous research portfolio.
uv run python cli.py portfolio eval \
--positions-path portfolio/reversal_port.pq \
--data-path "data/daily_bars/sh600000,sz000001,sh600519"
# Tests
uv run python -m pytest tests/ -v # alpha/portfolio tests are network-free; downloader tests hit the network
```
## CLI reference
All commands are subcommands of `uv run python cli.py`. Add `--help` to any of them.
### `data download` — fetch daily bars → partitioned parquet dataset
| Option | Default | Description |
| --- | --- | --- |
| `--universe` | `csi500` | `hs300`, `csi500`, `all` (~5000 A-shares), a file path (one symbol per line), or comma-separated symbols (`sh600000,sz000001`) |
| `--start-date` | `2017-01-01` | `YYYY-MM-DD` |
| `--end-date` | today | `YYYY-MM-DD` |
| `--output-dir` | `data/daily_bars` | Root for the dataset directory |
| `--symbols` | `0` | Max symbols to download (`0` = all) |
| `--chunk-size` | `300` | Symbols per durability flush (each flush appends `.pq` files) |
| `--adjust` | `qfq` | Price adjustment: `qfq` (forward), `hfq` (backward), `none` |
Writes a **Hive-partitioned dataset** at `{output_dir}/{universe}/month=YYYY-MM/*.pq`
(one partition per calendar month). The `{universe}` directory is rebuilt from
scratch on each run. Downloads stream under a single baostock session and flush
every `--chunk-size` symbols, so memory stays bounded and a crash keeps the
partitions already written. Pass the **dataset directory** (`{output_dir}/{universe}`)
as `--data-path` to later phases — `pd.read_parquet` reads the whole partitioned
set. Symbols use the internal `sh600000` / `sz000001` form (exchange prefix + code).
### `derived` — daily custom/derived data
Derived data is daily-only v1 research data keyed by `symbol_id,date`, with one
or more numeric value columns. It can come from user CSV/parquet files or Python
plugins, and is written as a single parquet file at `derived/{name}.pq`.
The validator normalizes `date` to the trading day, requires unique
`symbol_id,date` keys, rejects duplicate columns, and rejects non-numeric value
columns. Alpha computation consumes derived data through the existing
`--feature-path` flag.
```bash
# Validate a user file without writing output.
uv run python cli.py derived validate --input-path vendor_factor.csv
# Ingest CSV/parquet into the canonical derived/ layout.
uv run python cli.py derived ingest \
--input-path vendor_factor.csv \
--derived-name vendor_factor
# List built-in and external derived-data plugin types.
uv run python cli.py derived list
uv run python cli.py derived list --derived-module path/to/my_derived.py
# Compute a derived file from daily and/or minute inputs.
uv run python cli.py derived compute \
--minute-path data/minute_bars/sh600000 \
--daily-path data/daily_bars/sh600000 \
--derived-type minute_daily_summary \
--derived-name minute_summary
# Join derived columns into a feature-aware alpha.
uv run python cli.py alpha compute \
--data-path data/daily_bars/sh600000 \
--feature-path derived/minute_summary.pq \
--alpha-type my_feature_aware_alpha \
--alpha-name my_run
```
For compatibility, `feature list` and `feature compute` remain available and
delegate to the same derived-data registry. Existing `features/*.pq` files are
still valid `--feature-path` inputs when they satisfy the daily numeric contract.
### `alpha list` — show registered alpha types
```bash
uv run python cli.py alpha list
uv run python cli.py alpha list --alpha-module path/to/my_alpha.py # include an external alpha
```
### `alpha compute` — alpha class → weights parquet
| Option | Default | Description |
| --- | --- | --- |
| `--data-path` | (required) | Data parquet from `data download` |
| `--alpha-name` | (required) | Label stored in the `alpha_name` column / output filename |
| `--alpha-type` | (required) | Registry key of the alpha class (see `alpha list`) |
| `--output-dir` | `alphas` | Output directory |
| `--lookback` | `5` | Lookback days (passed to alphas that accept it) |
| `--vol-window` | `20` | Volatility window (passed to alphas that accept it) |
| `--feature-path` | — | Daily derived/feature parquet file or dataset to left-join on `symbol_id,date`; repeatable |
| `--alpha-module` | — | External module(s) to import first; repeatable. Dotted path or `.py` file |
| `--param` | — | Extra constructor param as `name=value`; repeatable |
Only the params an alpha's `__init__` accepts are forwarded, so passing extras
(e.g. `--vol-window` to a reversal alpha) is harmless.
```bash
uv run python cli.py alpha compute \
--data-path <data>.pq \
--alpha-type reversal_vol --alpha-name rv_5_20 \
--lookback 5 --vol-window 20
```
Shortcuts for the two most common built-ins:
```bash
uv run python cli.py alpha reversal --data-path <data>.pq --lookback 5
uv run python cli.py alpha reversal-vol --data-path <data>.pq --lookback 5 --vol-window 20
```
### `alpha eval` — score an alpha as a portfolio
```bash
uv run python cli.py alpha eval --alpha-path alphas/<name>.pq --data-path <data>.pq
```
Interprets the weights as a portfolio and reports cumulative return, annual
Sharpe, annual turnover, max drawdown, and hit rate; also dumps
`reports/<alpha_name>_eval.json`. There is deliberately **no IC/IR** — alphas are
position weights, not return predictors.
### `combo combine` — merge several alphas into one weight
| Option | Default | Description |
| --- | --- | --- |
| `--alpha-paths` | (required) | Comma-separated alpha parquet paths (≥ 2) |
| `--combo-name` | (required) | Label stored in the `combo_name` column / output filename |
| `--method` | `equal_weight` | Combination method (see `COMBO_METHODS`) |
| `--output-dir` | `combos` | Output directory |
```bash
uv run python cli.py combo combine \
--alpha-paths alphas/reversal_5d.pq,alphas/reversal_vol_5d_20d.pq \
--combo-name eq --method equal_weight
```
### `portfolio build` — weights → tradable positions
Turns alpha/combo weights into target weights, target yuan exposure, continuous
shares, and a lot-valid integer position book under A-share board rules.
Non-finite / non-positive construction prices are excluded before target
normalization. If a date has zero gross target after filtering, the previous
book is carried in `position_shares` and a warning is logged.
| Option | Default | Description |
| --- | --- | --- |
| `--weights-path` | (required) | Alpha or combo parquet with `symbol_id, date, weight` |
| `--data-path` | (required) | Data parquet file or partitioned dataset directory |
| `--booksize` | (required) | Target gross yuan exposure |
| `--portfolio-name` | (required) | Label stored in `portfolio_name` and output filename |
| `--price-field` | `close` | Data column used as construction price |
| `--output-dir` | `portfolio` | Output directory |
```bash
uv run python cli.py portfolio build \
--weights-path combos/eq.pq --data-path data/daily_bars/csi500 \
--booksize 10000000 --portfolio-name eq_10m
```
### `portfolio simulate` — constructed positions → fills + P&L
Executes the constructed `position_shares` book at the next available open,
clipping trades through repeatable constraints. It writes `fills/<name>.pq` and
`pnl/<name>.pq`. Trading costs use the simplified open-execution proportional
cash-cost model documented in
[`docs/portfolio_trading_cost_model.md`](docs/portfolio_trading_cost_model.md).
| Option | Default | Description |
| --- | --- | --- |
| `--positions-path` | (required) | Positions parquet from `portfolio build` |
| `--data-path` | (required) | Data parquet file or partitioned dataset directory |
| `--constraint` | — | Repeatable: `suspension`, `price_limit`, `volume_cap` |
| `--cost-bps` | `0.0` | Commission in basis points |
| `--slippage-bps` | `0.0` | Slippage in basis points |
| `--volume-frac` | `0.10` | Max traded value fraction for `volume_cap` |
| `--output-dir` | `.` | Base directory for `fills/` and `pnl/` |
```bash
uv run python cli.py portfolio simulate \
--positions-path portfolio/eq_10m.pq --data-path data/daily_bars/csi500 \
--constraint suspension --constraint price_limit --constraint volume_cap \
--cost-bps 5 --slippage-bps 5
```
### `portfolio eval` — score constructed target weights
```bash
uv run python cli.py portfolio eval \
--positions-path portfolio/eq_10m.pq --data-path data/daily_bars/csi500
```
Uses `target_weight` for a continuous research view: cumulative return,
annual Sharpe, annual turnover, max drawdown, Fitness, hit rate, and date count.
There is deliberately **no IC/IR**. Zero-gross carry dates remain flat in this
research view even though execution carries `position_shares`.
### `pqcat` — inspect a parquet file, like `cat`
Quickly dump any pipeline parquet (a single `.pq` file or a partitioned dataset
directory) to stdout, without writing a throwaway script.
| Option | Default | Description |
| --- | --- | --- |
| `-n, --head N` | — | Show only the first `N` rows |
| `-t, --tail N` | — | Show only the last `N` rows |
| `-c, --columns` | — | Comma-separated subset of columns to show |
| `--info` | off | Show shape + dtypes instead of the rows |
```bash
uv run python cli.py pqcat alphas/reversal_5d.pq # dump all rows
uv run python cli.py pqcat data/daily_bars/csi500 --info # shape + dtypes
uv run python cli.py pqcat data/daily_bars/csi500 -n 10 -c symbol_id,date,close,vwap
```
**Standalone command.** `tools/pqcat.py` has no repo dependencies, so it can be
run directly. Symlink it onto your `PATH` once and call `pqcat` from anywhere:
```bash
ln -sf "$(pwd)/tools/pqcat.py" ~/.local/bin/pqcat # ~/.local/bin must be on PATH
pqcat alphas/reversal_5d.pq --info
```
### `alphaview` — alpha weight(s) alongside bar data for one symbol
Join the bar dataset and one or more alpha parquet files on `(symbol, date)` and
print them side by side, so you can eyeball how a weight moves against price /
volume over a time range.
| Option | Default | Description |
| --- | --- | --- |
| `--data-path` | (required) | Bar dataset dir or parquet file |
| `--alpha-path` | (required) | Comma-separated alpha parquet path(s) — each `alpha_name` becomes a column |
| `--symbol` | (required) | Symbol id, e.g. `sh600000` |
| `--start-date` | — | `YYYY-MM-DD` (inclusive) |
| `--end-date` | — | `YYYY-MM-DD` (inclusive) |
| `-c, --columns` | `close,volume` | Comma-separated bar columns to show |
```bash
uv run python cli.py alphaview \
--data-path data/daily_bars/csi500 \
--alpha-path alphas/reversal_5d.pq,alphas/momentum_5d.pq \
--symbol sh600000 --start-date 2024-01-01 --end-date 2024-03-31 \
-c close,volume,vwap
```
Also standalone like `pqcat``ln -sf "$(pwd)/tools/alphaview.py" ~/.local/bin/alphaview`.
## Alphas: the factory / plugin interface
An **alpha** is a class that maps a wide close matrix (date index × `symbol_id`
columns) to **signed position weights** (positive = long, negative = short).
Every alpha subclasses `BaseAlpha` (`pipeline/alpha/base.py`) and is resolved by
name through the registry (`pipeline/alpha/registry.py`).
### Minimal alpha
```python
import pandas as pd
from pipeline.alpha.base import BaseAlpha
from pipeline.alpha.registry import register_alpha
@register_alpha
class MyAlpha(BaseAlpha):
name = "my_alpha" # unique registry key (required)
def __init__(self, lookback: int = 5):
self.lookback = lookback # declare whatever params you need
def signal(self, close: pd.DataFrame) -> pd.DataFrame:
# Raw score: wide (date × symbol_id), higher = stronger long, NaN where undefined.
return -close.pct_change(self.lookback)
```
That is the whole contract:
- `name` — the `--alpha-type` key; must be unique.
- `signal(close)` — the only required method; return a wide DataFrame.
- `to_weights(signal)` — provided by the base class: cross-sectionally z-scores
each date into weights (NaN → 0). **Override** it for a different scheme (rank,
dollar-neutral caps, etc.).
### Built-in alphas
One file per alpha under `pipeline/alpha/library/`:
| `--alpha-type` | Params | Description |
| --- | --- | --- |
| `reversal` | `lookback` | Negative trailing return (oversold scores high) |
| `reversal_vol` | `lookback`, `vol_window` | Reversal scaled by trailing volatility |
| `momentum` | `lookback` | Positive trailing return |
Add a built-in by dropping a module in `pipeline/alpha/library/` and importing it
from that package's `__init__.py`.
### Using an alpha written outside this repo
Write your `@register_alpha` class in any `.py` file, then register it at runtime
with `--alpha-module` (a `.py` path or an importable dotted module). See the
worked example in `examples/alphas/mean_reversion.py`:
```bash
uv run python cli.py alpha compute \
--alpha-module examples/alphas/mean_reversion.py \
--alpha-type mean_reversion --alpha-name mr20 \
--param window=20 \
--data-path <data>.pq
```
`mean_reversion` declares a `window` param (not `lookback`); `--param window=20`
supplies it and the unrelated `--lookback`/`--vol-window` defaults are ignored.
## Parquet schemas
The column contracts in `pipeline/common/schema.py` are the only interface
between phases (data is stored long/tidy):
- **data** (`DATA_COLUMNS`): `symbol_id, symbol_name, date, open, high, low, close, preclose, volume, amount, vwap, turn, pctChg, tradestatus, isST, peTTM, pbMRQ, psTTM, pcfNcfTTM`
(`vwap` = `amount / volume` — a **raw**-price daily VWAP, *not* on the adjusted
OHLC scale under `qfq`/`hfq`; `turn` is turnover %, `pctChg` daily % change,
`tradestatus`/`isST` are 0/1 flags, and `peTTM`/`pbMRQ`/`psTTM`/`pcfNcfTTM` are
baostock valuation ratios.)
- **derived** (`DERIVED_KEY_COLUMNS` + values): required keys `symbol_id, date`;
value columns are user/plugin-defined and must be numeric in v1.
- **alpha** (`ALPHA_COLUMNS`): `symbol_id, date, alpha_name, weight`
- **combo** (`COMBO_COLUMNS`): `symbol_id, date, combo_name, weight`
- **portfolio positions** (`POSITION_COLUMNS`): `symbol_id, date, portfolio_name, target_weight, target_value, target_shares, position_shares, position_value, price`
(`target_*` are continuous construction targets; `position_shares` is the
discretized + repaired integer book used by execution.)
- **fills** (`FILL_COLUMNS`): `symbol_id, date, portfolio_name, prev_shares, target_shares, traded_shares, realized_shares, blocked, trade_cost`
(`date` is the execution date, i.e. the next open after the target date.)
- **pnl** (`PNL_COLUMNS`): `date, portfolio_name, gross_exposure, net_exposure, pnl, cost, turnover, n_positions`
The data phase writes a month-partitioned dataset, so reading the dataset
directory yields an extra `month` (`YYYY-MM`) partition column on top of
`DATA_COLUMNS`; the alpha phase pivots by name and ignores it.
## Layout
- `cli.py` — entry point wiring the file-based phases together
- `pipeline/data/` — universe resolution + download → `data/daily_bars/{universe}/month=YYYY-MM/*.pq`
- `pipeline/derived/` — daily derived-data ingestion, validation, plugin registry,
and built-in derived computations → `derived/*.pq`
- `pipeline/alpha/``base.py` (`BaseAlpha`), `registry.py` (factory + plugin loader),
`library/` (built-in alphas), `compute.py` (`compute_alpha` / `evaluate_alpha`)
- `pipeline/features/` — compatibility wrappers for the derived-data registry
- `pipeline/combo/` — alpha combination → `combos/*.pq`
- `pipeline/portfolio/` — construction, A-share lot/limit rules, constraints,
reference next-open simulator, and research metrics
- `pipeline/common/schema.py` — parquet column contracts
- `data/downloader.py`, `data/universe.py` — baostock/akshare download + constituents
- `tools/pqcat.py` — standalone parquet inspector (`pqcat`), also wired as `cli.py pqcat`
- `tools/alphaview.py` — standalone alpha-vs-bar viewer (`alphaview`), also wired as `cli.py alphaview`
- `examples/alphas/` — example external alpha(s)
## Roadmap / current limits
The pipeline is implemented through portfolio construction and a reference
daily execution simulator. `alpha eval` remains a fast sanity check on raw
weights; use `portfolio build`, `portfolio simulate`, and `portfolio eval` for
constructed positions, fills/costs, P&L, and target-weight research metrics.
- [x] **Portfolio construction** — turn alpha/combo weights into continuous
targets and lot-valid integer positions under A-share board rules.
- [x] **Reference execution simulation** — next-open fills over constructed
`position_shares`, with suspension, price-limit, volume-cap, transaction-cost,
and slippage controls.
- [x] **Derived/custom daily data ("Level 2")** — ingest user CSV/parquet files
or compute plugin outputs as validated numeric daily datasets under
`derived/{name}.pq`; alpha joins continue through `--feature-path`.
- [ ] **Optional Backtrader adapter** — Backtrader is available as the
`backtrader` extra for possible future event-driven/broker-style experiments,
but it is not part of the current canonical portfolio workflow.
- [ ] **Forward / paper trading** — run the same construction logic on live
daily data, track simulated fills and a running P&L without real capital.
- [ ] **Intraday / microstructure data** — bid/ask prices & sizes, mid-price,
and intraday VWAP. These need a tick / L1L2 quote feed (typically a paid or
brokerage data tier); the free daily sources here only expose daily bars, so
this is a separate data phase rather than extra columns on the daily schema.
### Additional TODOs
The following items are intended extensions beyond the current daily
alpha-to-portfolio pipeline:
- **Long-only portfolio mode** — add a construction option that converts
alpha/combo weights into a long-only book while preserving existing lot,
price, suspension, and volume-cap handling.
- **Index-short hedging mode** — support portfolios that hold long A-share
names while shorting an index or index proxy for market exposure control.
- **Expanded universe presets** — add explicit universe aliases for CSI 300,
CSI 500, CSI 1000, and CSI 1800, while keeping file-based and comma-separated
custom universes available.
- **Categorical derived data** — extend the numeric-only derived-data v1 contract
to support categorical inputs such as industry classifications. In this
project, "Level 2" means customized second-level research data produced by
users or plugins; it does not necessarily mean exchange order-book/L2 quote
feeds.
- **Minute bar data** — continue extending the raw minute-bar and feature
workflow. The initial Baostock 5-minute download and daily feature plugin path
exist; intraday execution and replacing canonical daily bars remain out of
scope unless explicitly added later.
- **Industry data** — add industry classification inputs for filtering,
grouping, exposure reporting, neutralization, or industry-aware portfolio
construction.