Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/validation_baostock_20260716.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Baostock A-Share Public-Data Validation Report

> 2026-07-16 · Windows 11 · PyTorch 2.13.0+cpu · Python 3.12.7

## Command

```bash
python scripts/public_data_validation.py \
--source baostock \
--preset cn-large-25 \
--max-tickers 25 \
--cost-grid-bps 0,7,15,30 \
--bootstrap-samples 100
```

## Environment

| Item | Value |
|------|-------|
| OS | Windows 11 (10.0.26100) |
| Python | 3.12.7 |
| PyTorch | 2.13.0+cpu |
| CUDA | Not available |

## Data Coverage

| Field | Value |
|------|-------|
| Source | baostock |
| Tickers | 25 (SSE 15 + SZSE 10 A-share blue chips) |
| Date range | 2021-01-04 to 2024-12-31 |
| Trading days | 969 |
| Tradable ratio | 100% (all 25 tickers downloaded successfully) |
| Stocks with no data | none |

## Results (7 bps effective cost)

| strategy | ann_return | sharpe | max_dd | turnover | cost_drag |
| --- | ---: | ---: | ---: | ---: | ---: |
| equal_weight | +1.7% | 0.18 | 28.8% | 0.001 | 0.07% |
| momentum_20 | +0.2% | 0.14 | 41.6% | 0.179 | 24.2% |
| alpha101_mean | −23.2% | −1.02 | 73.1% | 0.533 | 72.2% |
| mlp_alpha101 | −11.9% | −0.81 | 53.5% | 0.272 | 36.8% |
| transformer_alpha101 | −6.0% | −0.38 | 35.3% | 0.282 | 38.2% |

All strategies negative or flat — consistent with A-share market conditions over 2021–2025.

## Cost sensitivity

At 30 bps, high-turnover strategies suffer disproportionately:
- alpha101_mean Sharpe drops from −0.21 (0 bps) → −3.62 (30 bps), cost drag 309%
- equal_weight Sharpe barely moves (0.185 → 0.182), cost drag only 0.3%

Bootstrap CIs (100 samples, block 20 days) included for all scenarios.

## Key packages

numpy==2.4.4 · pandas==3.0.2 · scipy==1.17.1 · scikit-learn==1.8.0 · torch==2.13.0+cpu · cvxpy==1.9.2 · scs==3.2.11 · click==8.4.1 · baostock==00.9.10
23 changes: 21 additions & 2 deletions scripts/public_data_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,21 @@
"EWT", "EWJ", "EWG", "EWU", "FXI", "MCHI", "INDA", "EWZ", "EWW", "EWC",
)

CN_LARGE_25 = (
"sh.600000", "sh.600036", "sh.600519", "sh.600585", "sh.600809",
"sh.600887", "sh.601012", "sh.601088", "sh.601166", "sh.601318",
"sh.601398", "sh.601668", "sh.601857", "sh.601888", "sh.601899",
"sz.000001", "sz.000002", "sz.000333", "sz.000651", "sz.000858",
"sz.002415", "sz.002594", "sz.300750", "sz.300059", "sz.300124",
)

PRESETS = {
"us-large-100": US_LARGE_100,
"etf-50": ETF_50,
"mixed-150": US_LARGE_100 + ETF_50,
"cn-large-25": CN_LARGE_25,
"etf-50": ETF_50,
"mixed-150": US_LARGE_100 + ETF_50,
}

DEFAULT_MODELS = ("equal_weight", "momentum_20", "alpha101_mean", "mlp_alpha101", "transformer_alpha101")
Expand Down Expand Up @@ -139,7 +150,7 @@ def _select_tickers(preset: str, tickers: str, max_tickers: int) -> tuple[str, .


def load_validation_panel(cfg: ValidationConfig) -> Panel:
"""Load either a public yfinance panel or a deterministic synthetic panel."""
"""Load a public-data, Baostock A-share, or deterministic synthetic panel."""
if cfg.source == "synthetic":
return make_synthetic_panel(
SyntheticConfig(
Expand All @@ -149,6 +160,14 @@ def load_validation_panel(cfg: ValidationConfig) -> Panel:
device=cfg.device,
)
)
if cfg.source == "baostock":
return make_panel(
source="baostock",
tickers=cfg.tickers,
start=cfg.start,
end=cfg.end,
device=cfg.device,
)
return make_panel(
source="yfinance",
tickers=cfg.tickers,
Expand Down Expand Up @@ -728,7 +747,7 @@ def _write_outputs(


@click.command()
@click.option("--source", type=click.Choice(["yfinance", "synthetic"]), default="yfinance", show_default=True)
@click.option("--source", type=click.Choice(["yfinance", "synthetic", "baostock"]), default="yfinance", show_default=True)
@click.option("--preset", type=click.Choice(sorted(PRESETS)), default="us-large-100", show_default=True)
@click.option("--tickers", default="", help="Comma-separated tickers. Overrides --preset.")
@click.option("--start", default="2021-01-01", show_default=True)
Expand Down
Loading