Document implemented portfolio workflow

This commit is contained in:
Yuxuan Yan
2026-06-10 15:04:34 +08:00
parent 98a4f99300
commit 459336b6cc
2 changed files with 120 additions and 24 deletions
+15 -2
View File
@@ -19,23 +19,27 @@ uv run python cli.py data download --universe csi500 --start-date 2017-01-01 #
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` is pure and fast.
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
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.
@@ -49,9 +53,18 @@ An **alpha** is a signed cross-sectional **position weight**: positive = long, n
- `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).