The vector engine
The vectorized time-by-symbol portfolio kernel — a deliberately narrow target-weight path with next-open fills on shared cash, reconciled bit-for-bit against the event engine on the path they share.
crates/vike-backtest/src/vector_engine.rs is the multi-asset time×symbol portfolio kernel: the
vectorized fast path for cross-sectional and target-weight strategies, an exact port of the Python
core/portfolio_fastsim.py::_portfolio_kernel and fast_portfolio_backtest. Where the event
engine steps a strategy object through callbacks, this kernel takes matrices and runs one pass.
The scope is the parity boundary
The header draws the line deliberately narrowly. In scope is the cash_gate=False, market-order,
target-weight path only: next-open fills, column-order shared-cash application — cash may go
negative; there is no gate, no reduce-first, no weight priority — taker fees, slippage, funding
and cost-basis averaging. Out of scope, and the reason to use the event engine instead: stops and
brackets, the cash gate, volume and position caps, leverage capping, membership masks,
liquidation, sub-bars and sizers.
The inputs are row-major Matrix values of shape (T, S): opens, closes, funding and
target_weights, plus the timestamp vector, the fee and slippage scalars, initial cash and one
contract multiplier. VectorBacktestEngine::run is the one public entry point; the free function
fast_portfolio_backtest is the implementation detail and the parity-gate target. S = 1 is the
single-symbol case — there is no separate single engine.
The target-weights contract
weights[t, s] is the weight decided at bar t, from closes up to and including t, and
realized at t+1's open. NaN means "no rebalance decided" for that symbol and bar — nothing
is queued. A weight of 0.0 for a held symbol drives a full close. The last bar's row is never
acted on, because there is no next open to fill at.
Per bar t, the kernel does four things in order:
- Fill the deltas queued at
t−1atopens[t, s], in column order, on the shared cash: the fill price is the adverse-moved open, the fee is charged on the filled notional, the signed notional moves cash, and the sharedTradeFoldsteps this symbol's cost basis and emits a closed-portionTradeon a reduce, close or flip. - Charge funding on held positions, marked at the close.
- Mark equity once — a single shared snapshot used for both the curve and the next sizing decision.
- Decide the next deltas from this bar's target weights:
w · eq / close − pos, with a dead-band of1e-12below which nothing is queued, and aclose == 0.0guard mirroring the Python_engine_target's non-positive-price guard. Skipped on the last bar.
The output is the shared BacktestResult: the equity curve, final equity, the trade count and,
when build_trades is set, the trade list, plus net funding paid.
The maker/taker law, and a dead parameter
Every fill this kernel produces is a rebalance crossing the market at the next bar's open. There
is no resting-limit path, so is_maker is always false and every fill is charged taker_fee.
That agrees with the event engine's own classification — maker if and only if the order kind is
Limit — for the same strategy verb this kernel mirrors, SimBroker::strategy_order_target_percent,
which only ever submits market rebalances. The two engines are reconciliation partners precisely
because neither has a maker-classified fill on this path.
maker_fee is threaded through and dead
crates/vike-backtest/src/vector_engine.rs's fast_portfolio_backtest records it as a local trap:
the kernel classifies every fill as taker, so its maker_fee parameter is dead at every call site.
It exists so a future maker path must consciously wire it rather than silently defaulting to
taker_fee by omission. The engine-kernel parity test's fee-schedule-armed row proves the two
engines agree bit-for-bit under a schedule with maker ≠ taker precisely because this kernel never
reads its maker side.
Reconciliation with the event engine
Engine-versus-kernel agreement is a second oracle, gated as a test in
crates/vike-backtest/tests/parity/engine_kernel_parity.rs. The kernel shares the event engine's
cost primitives — adverse_fill_price, fee and funding_charge from
the cost model — and the same TradeFold, so the comparison is
of two loops over one arithmetic, not two implementations of it.
The equivalence holds only on the path both engines have. crates/vike-backtest/src/engine/sim_broker.rs's
EngineParams documentation names, knob by knob, what the kernel does not consult: the market
impact model (it has no per-fill market context), the funding source closure (it takes funding
as a fully materialized matrix), the binary-resolution and option-expiry settlement sources, the
variation-margin cadence and the latency model. Reconciliation is meaningful only with every one of
those left at its default, which is what the parity gate runs.
When to reach for it
If the strategy is a matrix of weights — a rotation, a cross-sectional rank, a fixed allocation rebalanced on a schedule — and you already hold the aligned open/close/funding matrices, this kernel answers with one pass and no callbacks. The moment the strategy needs to see a fill, rest an order, protect a position with a stop, or be told it cannot afford a trade, it belongs on the portfolio engine.
The portfolio engine
The one event engine — aligned per-symbol bars stepped on one shared cash account, orders filling at the next open, a fixed per-step order of gates and settlements, and a tick path that k-way-merges recorded events.
The sim broker
The backtest fill engine and the context every strategy handler receives — its per-symbol state, the order verbs that all but one cross the live risk gate, the one fill-application path, and the diagnostics a run leaves on it.