diff --git a/docs/validation_baostock_20260716.md b/docs/validation_baostock_20260716.md new file mode 100644 index 0000000..9695e5a --- /dev/null +++ b/docs/validation_baostock_20260716.md @@ -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 diff --git a/scripts/public_data_validation.py b/scripts/public_data_validation.py index 5b8a765..68b8dad 100644 --- a/scripts/public_data_validation.py +++ b/scripts/public_data_validation.py @@ -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") @@ -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( @@ -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, @@ -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)