mirror of
https://github.com/alphabench/raptorbt.git
synced 2026-11-02 07:56:12 +00:00
Plain words: when raptorbt was handed something it could not interpret -- an option type it could not parse, a direction that was neither long nor short, a correlation matrix that is not mathematically valid -- it quietly picked a default and returned numbers that looked completely normal. Five such cases. All of them now raise an error naming the bad input. These share the most dangerous shape a financial library can have. Not a crash, not an obviously silly figure, but a smooth well-formed result computed from something other than what the caller asked for. No metric, no equity curve, no risk check downstream could tell the difference. ## What was wrong - **`max_trades` defaulted to 50 and truncated the run.** It `break`s out of the tick loop and reports as if the tape ended there. Measured on 1,000,000 ticks: 50 trades covering 0.81% of the data, total return -0.12% against a true -14.13%, max drawdown 0.124% against a true 14.13%. A 114x understatement of the one number a risk check reads. Now unlimited by default; the knob remains for anyone who explicitly wants a truncated run. - **A correlation matrix that is not positive definite was silently repaired.** Cholesky patched a negative pivot with `sqrt(|diag|)` and a zero pivot with `0.0`, then returned Ok. On an indefinite 3-asset matrix (smallest eigenvalue -0.8) the result was `var_95 = 0` and `probability_of_loss = 0` -- a risk model reporting no risk at all. `covariance.rs::chol_strict` already refuses and its comment says it is "deliberately unlike monte_carlo::cholesky"; the Monte Carlo path was the wrong half of that pair. The identity-matrix fallback below it was dead code (cholesky had no Err path) and is gone -- substituting one would have made every asset independent, the most optimistic assumption a risk model can make. - **An unparseable option type became a Call.** `OptionType::from_code` says in so many words that "defaulting an unrecognised code to Call would price a put as a call", and both PyO3 call sites then did exactly that. An iron condor whose put legs failed to parse became a four-leg call structure. `batch_spread_backtest` multiplied it across a whole sweep. - **`run_options_backtest` parsed three enums with a catch-all arm.** `option_type="PUT"` selected a long CALL -- the mirror image of the intended payoff -- while the identical string is accepted by `run_spread_backtest`. The same call meant two different things depending on which function you entered through. Now case-insensitive and closed; the documented defaults still work. - **`direction` fell back to Long.** Six call sites did `from_int(d).unwrap_or(Long)`, so a book encoded `0`/`1` instead of `-1`/`1` backtested entirely long, flipping the sign of the P&L on every short. In the basket and portfolio runners the parse is per instrument, so one bad row turned a leg of a market-neutral book into a doubled long. Two of the five were found by reading, three by an audit sweep prompted by the first. The audit also confirmed the optimizer's `max_iter` handling is correct (it returns SolverFailed with the iteration count rather than passing off a non-converged answer) and that no Rust `Default` diverges from its PyO3 default. ## Also here `benches/python/` -- the harness behind every published performance figure, so a claim can be re-run rather than trusted. Its README records why the 0.7.0 numbers are larger than the 0.6.4 ones: a different harness, not a slower engine. Same-data A/B of the published 0.6.4 wheel against this build gives 71.0us vs 70.5us on 1,000 bars with identical results, so 0.7.0 is marginally faster. The harness also encodes the two traps that produce flattering-but-wrong numbers: measure the release build, and raise `max_trades` so tick runs actually reach the end of the array. Version bumped to 0.7.0 in Cargo.toml and pyproject.toml. NOT yet published, so the backend requirement floors stay where they are until the wheel is on PyPI. ## Verification 460 Rust tests, 463 Python tests through the built wheel, clippy clean under -D warnings, fmt clean. tests/python/test_refuses_ambiguous_input.py adds 44 cases covering all five defects plus the valid inputs that must keep working. User-facing consequence: code that was relying on any of these silent fallbacks now raises a ValueError telling you which argument is wrong. That is a break, and an intentional one -- the alternative was continuing to hand people confident numbers computed from inputs the engine did not understand. Tick backtests that used the default `max_trades` will return different, correct results; Renko and spread numbers are unaffected by this commit.