wbt 0.2.1

Weight-based backtesting engine for quantitative trading
# wbt v0.2.0

> Release date: 2026-05-17
> Crate tag: `crate-v0.2.0` · Python tag: `v0.2.0`

## Summary

MINOR release adding five evaluation/utility helpers migrated from `czsc`, now exposed at the `wbt` top level. All additions are backward-compatible; no public API was removed or renamed.

## What's new

### New public APIs

- `cal_yearly_days(dts)` — infer yearly trading-day count from a date series (Rust core).
- `rolling_daily_performance(df, ret_col, window=252, min_periods=100, yearly_days=None)` — rolling-window daily performance (Rust core).
- `weights_simple_ensemble(df, weight_cols, method="mean", only_long=False, **kwargs)` — ensemble multiple strategy weights (`mean` / `vote` / `sum_clip`). `sum_clip` mode accepts `clip_min=-1, clip_max=1` via kwargs.
- `cal_trade_price(df, digits=None, **kwargs)` — TWAP / VWAP and next-bar trade-price table grouped by symbol. kwargs accept `windows=(5, 10, 15, 20, 30, 60)` and `copy=True`.
- `log_strategy_info(strategy, df)` — pretty-print per-symbol weight summaries via loguru.

Numerical results are aligned with the czsc reference (see `python/tests/test_compare_with_czsc_script.py`).

### Infrastructure

- Bridge Rust `log` crate output to Python `logging` via `pyo3-log` (so Rust-emitted warnings reach loguru `InterceptHandler`).
- Add `loguru` as a runtime dependency.
- CI: bump `manylinux` to `2_28` for Linux wheel builds.

## Fixes

- **`weights_simple_ensemble` no longer mutates the input DataFrame.** The function now copies internally and returns a new DataFrame. Previously it wrote `df["weight"] = ...` directly on the caller's DataFrame, which could silently pollute upstream data in multi-strategy pipelines.

## Doc fixes

- `cal_trade_price` and `weights_simple_ensemble` signatures in both READMEs corrected to reflect kwargs (`windows`, `copy`, `clip_min`, `clip_max`).
- README docstring of `rolling_daily_performance` clarifies that the first `min_periods` rows are skipped (`output_rows = max(0, n - min_periods)`).

## Compatibility

- `BREAKING CHANGE`: none.
- Python: requires ≥ 3.10 (unchanged).
- Rust: edition 2024 (unchanged).
- `pyo3 = 0.28` / `numpy = 0.28` (unchanged).

## Release checklist outcome

This release was gated by `docs/release_check.md`. Highlights:

- **§1 SemVer**: 5 backward-compatible API additions in the 0.x phase → MINOR bump (`0.1.8 → 0.2.0`).
- **§4 LLM full-repo review** (mandatory): performed against `v0.1.8..HEAD`. Findings and disposition:

  | ID | Level | Finding | Disposition |
  |----|-------|---------|-------------|
  | H1 | High | `cal_yearly_days` keeps truncated edge years in `max()` | **Argued as false positive.** Matches czsc reference behavior; `test_compare_with_czsc_script.py` (end-to-end equivalence) passes — diverging would break the contract this release ships against. |
  | H2 | High | `weights_simple_ensemble` mutates input `df` | **Fixed** in `fa8e472` (copy + regression test `test_input_df_not_mutated`). |
  | M1 | Medium | `rolling_daily_performance` `min_periods` semantics opaque | **Already documented** correctly in Python docstring; no change. |
  | M2 | Medium | `cal_trade_price` `sub` slice SettingWithCopy | **Latent only**: top-level `df = df.copy()` prevents trigger today. Deferred to next patch as defensive cleanup. |
  | M3 | Medium | `cal_trade_price` `sym_digits` NaN-propagation fragile | Functional; deferred to next patch as a refactor. |
  | L1 | Low | `log_strategy_info` lacks input column guard | Deferred to next patch. |
  | L2 | Low | `cal_yearly_days` UTC vs local-time edge offset | Deferred; document that callers should pass date-only or UTC-normalized inputs. |

- **§5 doc-vs-code consistency**: two README signature mismatches found and fixed (commit `2042919`); code held as ground truth.

## Known issues (carried forward to next patch)

- `cal_trade_price`: add explicit `sub = sub.copy()` and robust `sym_digits` inference.
- `log_strategy_info`: add `assert "symbol" in df.columns and "dt" in df.columns` for friendly errors.
- `cal_yearly_days`: clarify in docstring that inputs are interpreted as UTC dates (callers in non-UTC zones with non-zero time-of-day may see a one-day shift near UTC boundary).