9.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A modular Chinese A-share quant research framework. Daily frequency only (Phase 1).
Commands
The env is managed with uv. uv sync creates .venv from pyproject.toml + uv.lock; uv run <cmd> runs inside it (no manual activation needed).
uv sync # create/refresh .venv from the lockfile (incl. dev group)
uv run python -m pytest tests/ -v # all tests
uv run python -m pytest tests/test_alpha.py -v # single file (test_alpha is network-free)
uv run python -m pytest tests/test_alpha.py::test_evaluate_alpha_keys -v # single test
# Pipeline — each phase is independent: reads parquet, writes parquet.
uv run python cli.py data download --universe csi500 --start-date 2017-01-01 # → data/daily_bars/csi500/ (month-partitioned)
uv run python cli.py alpha reversal --data-path data/daily_bars/<universe> # --data-path is the dataset DIR
uv run python cli.py alpha eval --alpha-path alphas/<file>.pq --data-path data/daily_bars/<universe>
uv run python cli.py combo combine --alpha-paths a.pq,b.pq --combo-name eq --method equal_weight
uv run python cli.py portfolio build --weights-path combos/eq.pq --data-path data/daily_bars/<universe> --booksize 10000000 --portfolio-name eq_10m
uv run python cli.py portfolio simulate --positions-path portfolio/eq_10m.pq --data-path data/daily_bars/<universe> --constraint suspension --constraint price_limit --constraint volume_cap --cost-bps 5 --slippage-bps 5
uv run python cli.py portfolio eval --positions-path portfolio/eq_10m.pq --data-path data/daily_bars/<universe>
Add a runtime dep with uv add <pkg>, a dev/test dep with uv add --dev <pkg> (both update pyproject.toml + uv.lock).
Note: tests/test_downloader.py hits the network (live baostock/akshare); tests/test_alpha.py and tests/test_portfolio.py are pure and fast.
Architecture: one decoupled pipeline
The system is a phase-based CLI (cli.py + pipeline/). Each phase communicates only through parquet files on disk, so phases can be run, cached, and inspected independently:
data → alpha → combo → portfolio build → portfolio simulate/eval
pipeline/data/— download daily bars for a universe →data/daily_bars/{universe}/month=YYYY-MM/*.pq(Hive-partitioned dataset; pass the{universe}dir as--data-path)pipeline/alpha/— compute one alpha's position weights from a data parquet →alphas/*.pq, andalpha evalto score itpipeline/combo/— combine several alpha parquets into one →combos/*.pqpipeline/portfolio/— construct tradable positions from alpha/combo weights, simulate next-open fills under A-share constraints, and evaluate target-weight research metrics
The pipeline reuses two top-level helper modules via a sys.path.insert at the top of pipeline/data/downloader.py: data/downloader.py (network download) and data/universe.py (constituent lists). This path hack is load-bearing — keep it.
Alphas are weights, not predictors
An alpha is a signed cross-sectional position weight: positive = long, negative = short. It is produced by applying a formula to the wide close matrix, then cross-sectional z-scoring per date (compute_alpha in pipeline/alpha/compute.py). Alphas are evaluated by return / Sharpe / turnover / max-drawdown in evaluate_alpha — interpreting the weight series as a portfolio. There is deliberately no IC/IR anywhere: those frame a signal as a return predictor, which this codebase does not do. Do not reintroduce IC-style evaluation.
Parquet schema contracts
pipeline/common/schema.py defines the column contracts that are the only interface between phases. Any new phase or alpha must conform:
DATA_COLUMNS(data output):symbol_id, symbol_name, date, open, high, low, close, preclose, volume, amount, vwap, turn, pctChg, tradestatus, isST, peTTM, pbMRQ, psTTM, pcfNcfTTM(vwap=amount/volumeis a raw-price daily VWAP, not on the adjusted OHLC scale under qfq/hfq). The richer fields are fetched only by the batch path (download_daily_batch→download_universe); single-symboldownload_dailykeeps the legacy 8-column schema thattests/test_downloader.pypins.ALPHA_COLUMNS(alpha output):symbol_id, date, alpha_name, weightCOMBO_COLUMNS(combo output):symbol_id, date, combo_name, weightPOSITION_COLUMNS(portfolio build output):symbol_id, date, portfolio_name, target_weight, target_value, target_shares, position_shares, position_value, priceFILL_COLUMNS(portfolio simulate fills):symbol_id, date, portfolio_name, prev_shares, target_shares, traded_shares, realized_shares, blocked, trade_costPNL_COLUMNS(portfolio simulate P&L):date, portfolio_name, gross_exposure, net_exposure, pnl, cost, turnover, n_positions
Data is stored long/tidy, not wide, as a Hive-partitioned dataset keyed by month=YYYY-MM (so reads of the dataset directory carry an extra month partition column, which _pivot_close ignores). Compute code pivots to wide (date index × symbol_id columns) internally via _pivot_close, where all formulas are vectorized column-wise.
Portfolio construction and execution
portfolio build accepts either alpha or combo weights (symbol_id, date, weight) and normalizes only finite-weight names with finite positive construction prices. target_* columns are continuous research targets; position_shares is the discretized + repaired integer book. If a date has zero gross target after filtering, construction logs a warning and carries the previous position_shares, while target fields remain 0.
portfolio simulate must execute position_shares, not continuous target_shares. It fills at the next available open and clips desired deltas through repeatable constraints (suspension, price_limit, volume_cap). portfolio eval uses target_weight for a continuous research view, so zero-gross carry dates remain flat there. Keep IC/IR out of portfolio metrics too.
Alphas: factory + plugin pattern
Each alpha is a class subclassing BaseAlpha (pipeline/alpha/base.py), living in its own module. It implements signal(close) -> wide DataFrame (the raw score); the base class's to_weights cross-sectionally z-scores that into position weights (override for custom normalization). Subclasses declare their own typed __init__ params (e.g. lookback, vol_window, or anything custom).
- Built-in alphas: one file each under
pipeline/alpha/library/(reversal.py,reversal_vol.py,momentum.py). Each uses the@register_alphadecorator and is imported bylibrary/__init__.pyso it self-registers. Add a built-in by dropping a module there + adding it to that__init__. - Factory:
pipeline/alpha/registry.py—register_alpha,get_alpha(name, **params)(forwards only the params the alpha's__init__accepts, via signature inspection),available_alphas(), andload_alpha_module(spec). - External alphas (authored outside this repo) are the point of the design: write a
@register_alpha class MyAlpha(BaseAlpha)in any.pyfile, then register it at runtime with--alpha-module path/to/file.py(or a dotted module path). Seeexamples/alphas/mean_reversion.pyfor a working example, andtests/test_alpha.py::test_load_external_alpha_module.
uv run python cli.py alpha list # registered alpha types
uv run python cli.py alpha list --alpha-module my_alpha.py # incl. an external one
uv run python cli.py alpha compute --alpha-module my_alpha.py \
--alpha-type my_alpha --alpha-name run1 --param decay=0.9 --data-path <data>.pq
compute_alpha(data, alpha_name, alpha_type, **params) in pipeline/alpha/compute.py resolves the class via get_alpha, applies .weights(), and melts to ALPHA_COLUMNS. --lookback/--vol-window are passed as params for convenience; arbitrary params go through repeatable --param name=value. evaluate_alpha (return/Sharpe/turnover, no IC) is unchanged.
Combo methods are still a plain dict registry: COMBO_METHODS in pipeline/combo/combine.py.
Data sources & symbol conventions
- baostock is the primary source, akshare the fallback (
data/downloader.py,download_daily(source="auto")tries baostock first). This ordering is intentional — akshare is less reliable on the deployment network. - Internal symbol format is
sh600000/sz000001(exchange prefix + code). baostock uses the dotted formsh.600000; akshare'sstock_zh_a_histwants the bare code (prefix stripped). Both download paths return the identical 8-column schema and map theadjustargument consistently (qfq/hfq/none → baostockadjustflagvia_BAOSTOCK_ADJUST). - baostock constituent queries (
get_hs300_stocks,get_zz500_stocksindata/universe.py) return columns in an unreliable order, sopipeline/data/downloader.py:_fix_baostock_columnsdetects them by value pattern, not position. - Universes accepted by
data download --universe:hs300,csi500,all/full(every listed A-share, ~5000, viaget_all_stocks→ baostockquery_all_stockfiltered to SH 6xxxxx/68xxxx + SZ 0xxxxx/3xxxxx, excluding indices & B-shares), a file path (one symbol per line), or a comma-separated symbol list. Bulk downloads usedownload_daily_batch(one baostock login for the whole run) rather than per-symboldownload_daily.
Code standards
- Type hints on public functions; Google-style docstrings; 4-space indentation.