Simulator performance¶
The fast path in one paragraph¶
When you call :meth:Simulator.run_weights, :meth:Simulator.run_orders, or :meth:Simulator.run_signals with Fundcloud's built-in cost, slippage, and execution models, the simulator dispatches to a Rust kernel that runs the full deterministic bar loop with the GIL released — no Python callback per bar, no pandas method dispatch, all work happens on flat NumPy arrays through a PyO3 boundary. The pure-Python fallback at fundcloud.kernels._sim_pyfallback is the parity reference: the two backends agree to atol=1e-10 across every (cost × slippage × execution) combination (tests/unit/test_sim_parity.py enforces this).
Speedup (bench/bench_sim.py)¶
size (bars × assets) path wall (s) speedup
─────────────────────────────────────────────────────────────────
500 × 5 run_weights py=0.005 rust=0.003 2.0×
2000 × 10 run_weights py=0.024 rust=0.007 3.4×
5000 × 20 run_weights py=0.105 rust=0.020 5.3×
10000 × 30 run_weights py=0.300 rust=0.049 6.2×
Speedup scales with panel size because the Rust loop has a smaller per-bar constant than the NumPy-driven Python fallback. The Python fallback is itself NumPy-array-based (no pandas .iloc per bar), which is why the speedup sits around 2–6× rather than the 100× seen when replacing a pandas-row loop directly.
Run the benchmark yourself:
When is the Rust kernel engaged?¶
Three entry points route to Rust:
Simulator.run_weights(target_weights)— rebalance at each row oftarget_weightsSimulator.run_orders(orders)— execute an explicitts/asset/side/qtylogSimulator.run_signals(entries, exits)— boolean entry/exit panels
Not run_strategy — that path calls a Python BaseStrategy.decide(ctx) per bar and can't release the GIL per iteration. It stays on the original Python loop.
Rust dispatch is gated on all three of (costs, slippage, execution) being built-ins:
| Model | Built-in |
|---|---|
costs |
NoCost, FixedBps, PerShare |
slippage |
NoSlippage, HalfSpread |
execution |
NextBarOpen, SameBarClose |
Any custom subclass silently falls back to the original Python _drive loop — the simulator stays correct, you just don't get the Rust speedup.
Verify the fast path is active¶
from fundcloud.kernels import HAS_RUST, _core
print(HAS_RUST, hasattr(_core, "sim_run_weights"))
# → True True ← Rust kernel installed
If HAS_RUST is False, your wheel was built without the Rust extension — the simulator still runs correctly on the NumPy fallback. Rebuild the Rust extension with:
Architecture¶
Simulator.run_weights ─┬─ built-in models?
│ │
│ ├─ yes → _run_weights_fast ─┐
│ │ │
│ │ fundcloud.kernels._sim dispatcher
│ │ │
│ │ ├─ HAS_RUST → Rust kernel
│ │ │ (crates/fundcloud-core/src/sim.rs)
│ │ │
│ │ └─ fallback → _sim_pyfallback.py
│ │ (NumPy loop; parity reference)
│ │
│ └─ no → _drive (original Python loop with per-bar callback)
The Rust kernel and the NumPy fallback share the exact same loop structure (drain pending → submit new orders → mark-to-market), same enum-tagged config, same struct-of-arrays output. Bugs fixed in one almost always apply to the other by close inspection; tests/unit/test_sim_parity.py enforces that they agree before a release ships.