Realistic backtest in one call¶
Start every new backtest from a venue-typed factory. One call wires the executor, the cross-margin account, the liquidation engine, the fee schedule, the funding schedule, the rate-limit policy, and the venue-availability hook to venue-realistic defaults.
Available venues¶
| Factory | Liquidation profile | Funding | Fees |
|---|---|---|---|
binance_um_futures |
Binance UM (Binance ADL) | 8h | 10-tier VIP ladder |
bybit_linear |
Bybit linear (Bybit ADL) | 8h | 6-tier VIP ladder |
okx_swap |
OKX swap (PnlRatio ADL) | 8h | 4-tier VIP ladder |
deribit |
Bybit linear (Bybit ADL) | 8h | LV1 maker rebate path |
The deribit row carries two placeholders: there is no
deribit-specific FundingSchedule or LiquidationEngine profile
yet, so the factory wires FundingSchedule::binance_um_futures()
(8h) and LiquidationEngine::bybit_linear(). Fees, rate limits, and
the pro_rata_with_fifo queue model are deribit-specific. Override
the funding and liquidation components if the placeholders matter
for your research.
String dispatch via VenueStack.from_venue("binance", account_id,
equity) is also available for codegen / agents that pick the
venue programmatically.
Drive the stack¶
The factory returns a fully-wired stack. From there:
# Open one or more positions on the cross-margin account.
acct.open_position(symbol=BTC, quantity=5.0, entry_price=50_000.0)
# On every tick of the strategy loop:
# 1) update marks (one per symbol you hold; the engine sets the
# mark for the called symbol automatically).
# 2) run the liquidation walk.
acct.set_mark(BTC, 48_000.0)
outcome = liq.on_mark(BTC, 48_000.0)
if outcome["liquidations_count"] > 0:
print("liquidated:", outcome["liquidated"])
# Multi-symbol: on_marks updates every attached account's mark for
# each (symbol, price) pair atomically, then walks liquidations once
# per symbol. Same aggregated outcome dict.
outcome = liq.on_marks([(BTC, 48_000.0), (ETH, 2_900.0)], ts_ns=now)
# Record fills through the fee schedule so the 30-day VIP tier
# tracks correctly (FeeSchedule reads aggregate notional from the
# account when bound — which the factory does for you).
fees.record_fill(ts_ns=now, notional=100_000.0)
fee_paid = fees.fee_for(ts_ns=now, notional=100_000.0, is_maker=False)
Why this is the default¶
There is no useful "unrealistic" backtest — omitting fees, funding,
liquidation, queue model, ack latency, or rate limits silently
produces optimistic numbers that do not survive contact with the
live venue. The bare SimulatedExecutor() constructor stays
available for unit tests of the executor itself, but research
backtests always go through a venue factory.
Attach a VenueStack to BacktestRunner¶
The examples above drive a VenueStack standalone: your own loop
calls acct.open_position, liq.on_mark, fees.record_fill, and so
on directly. BacktestRunner supports a second mode where it drives
the stack for you:
Once set_venue_stack is called, run_csv / run_bars /
run_tape(s) feed the stack's own executor instead of the
built-in one, so fills come from the venue's fill mechanics — queue
model and depth, iceberg refresh latency, venue availability, and
rate limits — rather than an instant-fill assumption. Fees come from
the stack's FeeSchedule as well: each fill is billed at the tier
the account's 30-day notional resolves to, with the fill's
maker/taker flag picking the side, instead of the flat fee_rate.
Funding and liquidation stay driven by explicit calls to liq and
funding, not by the replay loop. Pass None to revert to the
built-in executor.
BacktestRunner.set_executor(...) refuses a bare venue executor
(lrvx.VenueExecutor) with a pointer back to this page: the executor
alone does not carry the clock the runner has to advance to make ack
latency and queue timing progress, so set_venue_stack(stack) — which
hands over the whole stack — is the only supported path.
This attach path is currently Python-only: the C ABI exposes a
single-argument lrvx_backtest_runner_set_executor without the
clock the venue executor needs, so Codon and QuickJS strategies keep
driving a VenueStack standalone, the same way as in every other
section of this page.
Custom venues / overrides¶
The factory wires defaults. Each accessor returns the live component, so mutations after construction stick:
# Switch ADL ranking strategy.
stack.liquidation().set_adl_ranking("position_size")
# Lower insurance fund to test cascade behaviour.
stack.liquidation().set_insurance_fund_capital(1_000.0)
# Override the placeholder funding rate.
stack.funding().set_constant_rate(0.0001)
stack.executor() returns a VenueExecutor, not a
SimulatedExecutor. Its full surface is submit_order,
cancel_order, cancel_all, on_bar, on_trade, on_trade_qty,
fills_list, fill_count, set_rate_limit_policy,
clear_rate_limit_policy, and set_venue_availability. The queue,
slippage, iceberg, latency, and STP knobs are fixed by the factory
and are not reachable through it — build a SimulatedExecutor
directly (or use assemble_custom_venue below) when you need them.
Fully custom venue¶
For venues outside the canned set — or configurations where one
subsystem (fees, funding cadence, MM ladder) needs full
replacement rather than tuning — assemble_custom_venue wires
user-built subsystems into a venue-stack-shaped bundle:
import lrvx
acct = lrvx.Account(account_id=42, equity=10_000)
fees = lrvx.FeeSchedule()
fees.add_tier(0, 1.0, 3.0)
fees.add_tier(50_000, 0.5, 2.5)
# 4h cadence; interval is fixed at construction, there is no setter.
funding = lrvx.FundingSchedule.constant(4 * 3600 * 1_000_000_000, 0.0)
liq = lrvx.LiquidationEngine()
liq.add_tier(0.0, 0.004)
rate_limits = lrvx.RateLimitPolicy()
rate_limits.add_bucket("trading", 1_000_000_000, 50)
custom = lrvx.assemble_custom_venue(
account=acct, fees=fees, funding=funding, liquidation=liq,
rate_limits=rate_limits, venue_name="my_exchange",
)
# custom.executor(), custom.account(), custom.liquidation(), ...
The helper:
- Creates a fresh executor and installs the rate limits + venue availability on it
- Binds fees to the account so 30d notional aggregates correctly
- Attaches the account to liquidation and routes liquidation orders through the executor
CustomVenue mirrors most of VenueStack's accessors
(.executor(), .account(), .fees(), .funding(),
.liquidation()) but is not a drop-in replacement: it has no
.clock(), its availability accessor is .venue_availability()
rather than .venue(), and .executor() hands back a
SimulatedExecutor rather than a stack-owned VenueExecutor.
It also takes queue_model / queue_depth (default "full" / 8,
matching the built-in presets). Passing "none" leaves resting maker
orders unfillable while taker orders still fill, so the run looks
alive and reports no maker fills.
The C++ escape hatch is VenueStack::assemble(AssembleArgs&&).
Codon and QuickJS users assemble manually via the existing setter
API.
See also¶
- Cross-margin accounts — Account API details
- Liquidation and ADL — engine internals
- Perpetual funding — funding tape options
- Rate limits — per-endpoint budgets
- Venue downtime — outage policies