You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This tracks a structured performance campaign on pybroker that I've been running over the last few days. It resulted in 1 benchmarking-infrastructure PR and 6 non-invasive perf vectors, all measurable via asv (Airspeed Velocity) with per-PR evidence.
Everything listed here is ready to review. All 6 vector PRs pass the existing CI (ruff + mypy + pytest on 3.11 + 3.12). All PRs are independent — you can merge any subset in any order (modulo the one explicit V7 → V2 stack). Merging #224 first makes the other diffs collapse to just the vector change.
I also have a list of invasive follow-ups that the non-invasive set deliberately leaves alone — see the bottom.
None of the vectors strictly require any specific order. The only real dependency is #230 (V7) → #226 (V2) because V7 modifies the capture_bar body that V2 introduces.
V1: cache=True on every @njit decorator #225 V1 — cache=True everywhere. Peak RSS win is the repeatable signal; warm CI time is flat here, but cross-process cold-start benefit is real.
V5: in-memory L1 cache in front of diskcache #227 V5 — in-memory L1. Micro-bench shows 100×; macro is flat because the reference scenario is dominated by non-cache work. Still a free win for any cache-heavy workflow.
No Decimal → float conversions. Portfolio / Entry / Position keep their exact-arithmetic semantics.
No change to public method signatures. The only signature change is Portfolio.capture_bar(date, df) → capture_bar(date, col_scope, sym_end_index), both internal.
No removed behavior. Full test suite (4074 tests) stays green on every branch.
No new required dependencies. asv is a dev-only dep; the runtime install_requires is unchanged.
Invasive options I deliberately did not pursue here (happy to discuss in follow-up if interest):
V3 redesign — parallel model training (see next section).
V4 — @njit the portfolio hot path. Would port capture_bar + check_stops to Numba. Needs Position / Entry state on float64 (API-visible precision change) + NamedTuple → np.record. Multi-day scope. I attempted a scoped version (float internals, Decimal write-back) locally; it regressed because the Decimal(str(round(x, 10))) reconstruction on every call outweighs the Decimal arithmetic it replaces. Documented as a negative result. Real V4 requires committing to float throughout Portfolio state.
Columnar batch backtest. Rewrite the backtest as vectorized ops where possible. Fork-scale change; not attempted.
Why V3 was left out
My original plan included a V3 "parallel walkforward windows" vector using joblib.Parallel. Before implementing I audited _run_walkforward (strategy.py:1333–1418) and found:
Portfolio is passed by reference to backtest_executions each window (line 1404) and mutated — cash, open positions, orders, trades accumulate window N → window N+1.
Sessions dict (line 1348) is per-symbol state the user can stash across windows; also mutated.
Signals are appended across windows.
Walkforward windows are not independent. Parallelizing the window loop would break portfolio continuity. So V3 as-drafted was dropped.
A redesigned version — V3-rev: parallel model training, sequential backtest — is possible. Pre-compute all (train_idx, test_idx) splits, train models in parallel via joblib.Parallel, then run the test phase sequentially with the pre-trained models. That preserves portfolio continuity while parallelizing the work most likely to dominate for ML users (self.train_models(...)).
I didn't include V3-rev here because:
It only helps model-heavy strategies. For indicator/signal-only strategies (the reference bench, and likely the common case), training is a no-op and the vector delivers nothing.
The measurement is harder — the reference walkforward has no models, so a meaningful V3-rev benchmark needs a separate model-based scenario. Happy to add one if you want to pursue V3-rev.
What deserves more data
Several vectors are verified per-call wins that don't move the current reference scenario's wall-clock, because that scenario isn't the shape where they shine.
If you want these surfaced at the macro level, possibilities include:
A dense-position scenario (10+ concurrent symbols with open positions at all times) — would move V2 / V7 further and surface V9 more.
An ML walkforward scenario with a cheap sklearn model — would surface V3-rev if pursued, plus give V1 a real cold-start amortization.
A cache-enabled scenario that exercises indicator re-reads — would surface V5 on the macro level.
Offers
Extend benchmarks — glad to add scenarios if the coverage above doesn't match your mental model of "workloads pybroker should stay fast on". Suggest a workflow shape and I'll add a class.
Rebase / reshape / split / squash any PR per review comments.
Summary
This tracks a structured performance campaign on pybroker that I've been running over the last few days. It resulted in 1 benchmarking-infrastructure PR and 6 non-invasive perf vectors, all measurable via
asv(Airspeed Velocity) with per-PR evidence.Everything listed here is ready to review. All 6 vector PRs pass the existing CI (ruff + mypy + pytest on 3.11 + 3.12). All PRs are independent — you can merge any subset in any order (modulo the one explicit
V7 → V2stack). Merging #224 first makes the other diffs collapse to just the vector change.I also have a list of invasive follow-ups that the non-invasive set deliberately leaves alone — see the bottom.
PRs
asv.conf.json,benchmarks/,.github/actions/setup-pybroker/,.github/workflows/asv-pr.yml,docs/source/benchmarking.rstcache=Trueon every@njitsrc/pybroker/vect.py,src/pybroker/eval.pyPortfolio.capture_barportfolio.py,strategy.py,tests/test_portfolio.pysrc/pybroker/cache.py,tests/test_cache.py,benchmarks/bench_backtest.pyCacheHitmicro; flat on macroPriceScope.fetchsrc/pybroker/scope.py,benchmarks/bench_backtest.pyPriceScopeFetch: 208 → 66 ms (3.1×)ExecContext.long_pos/short_possrc/pybroker/context.py,benchmarks/bench_backtest.pylong_pos232 → 79 ms (3.0×),short_pos218 → 77 ms (2.8×)capture_bar(stacks on #226)src/pybroker/portfolio.pyRecommended merge order
None of the vectors strictly require any specific order. The only real dependency is #230 (V7) → #226 (V2) because V7 modifies the
capture_barbody that V2 introduces.Suggested order by risk/reward:
fetch_dictcall.cache=Trueeverywhere. Peak RSS win is the repeatable signal; warm CI time is flat here, but cross-process cold-start benefit is real.long_pos/short_pos. Cleanup-grade polish.Non-invasive vs invasive
Everything above is non-invasive:
Decimal→floatconversions.Portfolio/Entry/Positionkeep their exact-arithmetic semantics.Portfolio.capture_bar(date, df)→capture_bar(date, col_scope, sym_end_index), both internal.asvis a dev-only dep; the runtimeinstall_requiresis unchanged.Invasive options I deliberately did not pursue here (happy to discuss in follow-up if interest):
@njitthe portfolio hot path. Would portcapture_bar+check_stopsto Numba. NeedsPosition/Entrystate onfloat64(API-visible precision change) +NamedTuple→np.record. Multi-day scope. I attempted a scoped version (float internals, Decimal write-back) locally; it regressed because theDecimal(str(round(x, 10)))reconstruction on every call outweighs the Decimal arithmetic it replaces. Documented as a negative result. Real V4 requires committing to float throughout Portfolio state.Why V3 was left out
My original plan included a V3 "parallel walkforward windows" vector using
joblib.Parallel. Before implementing I audited_run_walkforward(strategy.py:1333–1418) and found:backtest_executionseach window (line 1404) and mutated — cash, open positions, orders, trades accumulate window N → window N+1.Walkforward windows are not independent. Parallelizing the window loop would break portfolio continuity. So V3 as-drafted was dropped.
A redesigned version — V3-rev: parallel model training, sequential backtest — is possible. Pre-compute all
(train_idx, test_idx)splits, train models in parallel viajoblib.Parallel, then run the test phase sequentially with the pre-trained models. That preserves portfolio continuity while parallelizing the work most likely to dominate for ML users (self.train_models(...)).I didn't include V3-rev here because:
What deserves more data
Several vectors are verified per-call wins that don't move the current reference scenario's wall-clock, because that scenario isn't the shape where they shine.
cache=TrueWalkforwardProperColdin #224 clears.nbibetween iterationsCacheHitmicro + #227'sWalkforwardWithCachePriceScope.fetchPriceScopeFetchmicro in #228long_posctx.long_pos()/short_pos()frequentlyExecContextPosmicro in #229If you want these surfaced at the macro level, possibilities include:
Offers
Happy to iterate.
🤖 Generated with Claude Code