Files
chinese-equity-quant/CLAUDE.md
T
2026-06-10 15:41:38 +08:00

9.9 KiB
Raw Blame History

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).

Backtrader is optional (uv sync --extra backtrader) and is not used by the current pipeline. Keep portfolio simulate as the canonical backtest/execution path unless an explicit future adapter is requested.

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, and alpha eval to score it
  • pipeline/combo/ — combine several alpha parquets into one → combos/*.pq
  • pipeline/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/volume is 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_batchdownload_universe); single-symbol download_daily keeps the legacy 8-column schema that tests/test_downloader.py pins.
  • ALPHA_COLUMNS (alpha output): symbol_id, date, alpha_name, weight
  • COMBO_COLUMNS (combo output): symbol_id, date, combo_name, weight
  • POSITION_COLUMNS (portfolio build output): symbol_id, date, portfolio_name, target_weight, target_value, target_shares, position_shares, position_value, price
  • FILL_COLUMNS (portfolio simulate fills): symbol_id, date, portfolio_name, prev_shares, target_shares, traded_shares, realized_shares, blocked, trade_cost
  • PNL_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.

Trading cost uses the simplified open-execution proportional cash-cost model in docs/portfolio_trading_cost_model.md: abs(traded_shares * open) * (cost_bps + slippage_bps) / 10000. Slippage is cash cost only; do not also adjust execution prices for slippage.

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_alpha decorator and is imported by library/__init__.py so it self-registers. Add a built-in by dropping a module there + adding it to that __init__.
  • Factory: pipeline/alpha/registry.pyregister_alpha, get_alpha(name, **params) (forwards only the params the alpha's __init__ accepts, via signature inspection), available_alphas(), and load_alpha_module(spec).
  • External alphas (authored outside this repo) are the point of the design: write a @register_alpha class MyAlpha(BaseAlpha) in any .py file, then register it at runtime with --alpha-module path/to/file.py (or a dotted module path). See examples/alphas/mean_reversion.py for a working example, and tests/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 form sh.600000; akshare's stock_zh_a_hist wants the bare code (prefix stripped). Both download paths return the identical 8-column schema and map the adjust argument consistently (qfq/hfq/none → baostock adjustflag via _BAOSTOCK_ADJUST).
  • baostock constituent queries (get_hs300_stocks, get_zz500_stocks in data/universe.py) return columns in an unreliable order, so pipeline/data/downloader.py:_fix_baostock_columns detects them by value pattern, not position.
  • Universes accepted by data download --universe: hs300, csi500, all/full (every listed A-share, ~5000, via get_all_stocks → baostock query_all_stock filtered 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 use download_daily_batch (one baostock login for the whole run) rather than per-symbol download_daily.

Code standards

  • Type hints on public functions; Google-style docstrings; 4-space indentation.