# 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 ` runs inside it (no manual activation needed). ```bash 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/ # --data-path is the dataset DIR uv run python cli.py alpha eval --alpha-path alphas/.pq --data-path data/daily_bars/ 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/ --booksize 10000000 --portfolio-name eq_10m uv run python cli.py portfolio simulate --positions-path portfolio/eq_10m.pq --data-path data/daily_bars/ --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/ ``` Add a runtime dep with `uv add `, a dev/test dep with `uv add --dev ` (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_batch` → `download_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. ## 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.py` — `register_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`. ```bash 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 .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.