Skip to content

Performance campaign: asv infrastructure + 6 non-invasive vectors (2.5–3× wins; PR#224–#230) #231

Description

@Pirat83

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 → 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.


PRs

PR Vector Files Headline result Significant?
#224 asv infrastructure + 11 benchmark classes asv.conf.json, benchmarks/, .github/actions/setup-pybroker/, .github/workflows/asv-pr.yml, docs/source/benchmarking.rst Infrastructure only; no code path changes N/A
#225 V1 cache=True on every @njit src/pybroker/vect.py, src/pybroker/eval.py peak RSS 281 M → 245 M (-13%) Yes (mem)
#226 V2 integer indexing in Portfolio.capture_bar portfolio.py, strategy.py, tests/test_portfolio.py Walkforward: 413 → 164 ms (2.5×); Cold: 389 → 169 ms (2.3×) Yes — biggest macro win
#227 V5 in-memory L1 in front of diskcache src/pybroker/cache.py, tests/test_cache.py, benchmarks/bench_backtest.py ~100× on CacheHit micro; flat on macro Micro only
#228 V8 fast round + enum cache in PriceScope.fetch src/pybroker/scope.py, benchmarks/bench_backtest.py PriceScopeFetch: 208 → 66 ms (3.1×) Yes (micro)
#229 V9 inline ExecContext.long_pos / short_pos src/pybroker/context.py, benchmarks/bench_backtest.py long_pos 232 → 79 ms (3.0×), short_pos 218 → 77 ms (2.8×) Yes (micro)
#230 V7 fetch batch in capture_bar (stacks on #226) src/pybroker/portfolio.py +17% on top of V2; cumulative 2.8× vs baseline Yes

Recommended 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_bar body that V2 introduces.

Suggested order by risk/reward:

  1. Add asv benchmark suite + PR-regression workflow #224 — asv infrastructure. Lightest review. Once merged, every other PR's diff shrinks to just the vector change.
  2. V2: integer indexing in Portfolio.capture_bar (2.5x warm, 2.3x cold) #226 V2 — the biggest clean macro win (2.5× on the reference walkforward scenario).
  3. V7: batch col_scope.fetch in capture_bar (stacks on #226) #230 V7 — ratchets V2 to 2.8× via a single batched fetch_dict call.
  4. V1: cache=True on every @njit decorator #225 V1cache=True everywhere. Peak RSS win is the repeatable signal; warm CI time is flat here, but cross-process cold-start benefit is real.
  5. 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.
  6. V8: fast round + enum cache in PriceScope.fetch (3.1x micro) #228 V8 — order-pipeline cleanup (fast round + enum cache).
  7. V9: inline ExecContext.long_pos / short_pos (3x micro) #229 V9 — method-call inlining on long_pos / short_pos. Cleanup-grade polish.

Non-invasive vs invasive

Everything above is non-invasive:

  • No Decimalfloat 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) + NamedTuplenp.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.

Vector Shows where Already covered by added benches?
V1 cache=True Multi-process cold starts (walkforward × joblib pool) Partially — WalkforwardProperCold in #224 clears .nbi between iterations
V5 L1 cache Cache-enabled workflows (indicator/model cache on during dev/grid-search) Yes — #224's CacheHit micro + #227's WalkforwardWithCache
V8 fast round / enum cache Order-heavy strategies, many symbols hitting PriceScope.fetch Yes — PriceScopeFetch micro in #228
V9 inline long_pos Strategies that call ctx.long_pos()/short_pos() frequently Yes — ExecContextPos micro in #229

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.
  • Address the checkbox comment on Add asv benchmark suite + PR-regression workflow #224 if you want to make decisions now on Python matrix / PR-check semantics / historical dashboard.
  • Open a design thread on V4 (njit portfolio) or V3-rev (parallel model training) if you want to pursue the invasive ones.

Happy to iterate.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions