Replace the old signal/strategy/backtest modules with a decoupled
data → alpha → combo pipeline (parquet between phases, .pq extension).
Alphas:
- BaseAlpha + @register_alpha factory/plugin registry; one file per
built-in (reversal, reversal_vol, momentum); external alphas via
--alpha-module. Alphas are z-scored position weights, not predictors.
Data:
- baostock primary / akshare fallback, treated consistently.
- New --universe all (~5000 A-shares via query_all_stock, filtered).
- login-once batch downloader; empty-string OHLCV coerced to NaN.
- Month-partitioned dataset {output_dir}/{universe}/month=YYYY-MM/*.pq
with chunked durability flushes; --data-path is the dataset dir.
CLI logs at INFO by default (--log-level) so progress is visible.
Docs (README, CLAUDE.md) updated incl. pipeline diagram and roadmap
TODOs for portfolio construction / backtest / paper trading.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 KiB
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 ───────┴───────────────────────────────────────┘
│
▼ (planned — not yet implemented)
┌ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐
PORTFOLIO BACKTEST PAPER TRADING
│ construct │ │ simulate │ │ forward / live │ TODO
positions fills + costs execution
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ┘
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
pip install -r requirements.txt
Quick start
# 1. Download daily bars for a few symbols (writes a month-partitioned dataset).
python3 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}).
python3 cli.py alpha reversal \
--data-path "data/daily_bars/sh600000,sz000001,sh600519"
# 3. Evaluate it (return / Sharpe / turnover / drawdown).
python3 cli.py alpha eval \
--alpha-path alphas/reversal_5d.pq \
--data-path "data/daily_bars/sh600000,sz000001,sh600519"
# Tests
python3 -m pytest tests/ -v # tests/test_alpha.py is network-free; test_downloader.py hits the network
CLI reference
All commands are subcommands of python3 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).
alpha list — show registered alpha types
python3 cli.py alpha list
python3 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) |
--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.
python3 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:
python3 cli.py alpha reversal --data-path <data>.pq --lookback 5
python3 cli.py alpha reversal-vol --data-path <data>.pq --lookback 5 --vol-window 20
alpha eval — score an alpha as a portfolio
python3 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 |
python3 cli.py combo combine \
--alpha-paths alphas/reversal_5d.pq,alphas/reversal_vol_5d_20d.pq \
--combo-name eq --method equal_weight
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
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-typekey; 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:
python3 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, volume, amount - alpha (
ALPHA_COLUMNS):symbol_id, date, alpha_name, weight - combo (
COMBO_COLUMNS):symbol_id, date, combo_name, weight
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 three phases togetherpipeline/data/— universe resolution + download →data/daily_bars/{universe}/month=YYYY-MM/*.pqpipeline/alpha/—base.py(BaseAlpha),registry.py(factory + plugin loader),library/(built-in alphas),compute.py(compute_alpha/evaluate_alpha)pipeline/combo/— alpha combination →combos/*.pqpipeline/common/schema.py— parquet column contractsdata/downloader.py,data/universe.py— baostock/akshare download + constituentsexamples/alphas/— example external alpha(s)
Roadmap (not yet implemented)
The pipeline currently ends at combo, and alpha eval only interprets a weight
series as a portfolio for quick scoring (return / Sharpe / turnover / drawdown).
It is not a true backtest — there is no transaction-cost, slippage, or
execution modeling. The following phases are planned but not built yet:
- Portfolio construction — turn combo weights into target positions (gross/net exposure caps, per-name and sector limits, capital allocation, rebalance schedule).
- Backtesting — event-driven simulation over the constructed positions
with realistic fills, transaction costs, slippage, and borrow constraints;
richer P&L / risk attribution than
alpha eval. - Forward / paper trading — run the same construction logic on live daily data, track simulated fills and a running P&L without real capital.
Until these land, treat alpha eval as a fast sanity check on a weight series,
not a performance estimate.