A funding carry across two venues
Open a delta-neutral perp carry from recorded funding rates on two venues, and watch the fee table decide whether the differential was ever worth collecting.
A funding carry is long the venue that pays and short the venue that charges, sized so the two legs' price exposure cancels. What is left is the funding differential — and a round-trip taker cost that has to come out of it first. This tutorial runs that trade over recorded funding on two venues and shows where the cost comes from.
The sign law, once
crates/vike-strategy/src/funding_carry.rs's header freezes the convention so no page has to re-derive it. On every wired perp venue a positive funding_rate means longs pay shorts. Per interval, per unit notional, a long position's cashflow is −funding_rate and a short's is +funding_rate. So a carry that longs venue L and shorts venue S collects funding_S − funding_L — the gross differential, non-negative exactly when the long leg sits on the lower rate, which is what rank_best_carry orients every pair to.
The controller never books that money. The header is explicit: the runtime already folds Event::Funding settlements into account cash through vike_exec::Account::apply_funding, so this strategy only decides.
What the cost is
roundtrip_taker_cost(long_taker, short_taker) is 2 × (long_taker + short_taker) — each leg crosses as a taker on entry and exit, so four crossings. net_carry_edge(gross, cost, hold_periods) amortises that one-time cost over an assumed holding of hold_periods funding intervals: hold_periods × gross − cost. The default hold_periods = 1.0 is the conservative single-period breakeven — the pair must clear its entire round-trip cost out of one interval's differential.
rank_best_carry scores every unordered venue pair rather than just the extremes, and its doc says why: because the round-trip cost is per venue, a fat differential onto a high-fee venue can lose to a thinner one onto cheap venues, so the max-gross pair is not always the max-net pair. ## Where the taker rate comes from
FundingCarryController::quotes pulls each venue's taker fee from the static per-venue registry, vike_model::fee_schedule_for(venue).maker_taker_rates().1. That table is crates/vike-model/src/fees.rs, published tier-0 with no token discount, each row cited: binance spot 10 / 10 bps, binance-perp 2 / 5, bybit 2 / 5.5, okx 2 / 5, hyperliquid 1.5 / 4.5, aster spot 0.5 / 4 — the last of which is a measurement rather than a claim, swept live across twelve pairs on 2026-08-05, all twelve identical.
On a backtest, this static row is the whole answer
vike_mount::resolve_fee_schedule prefers a venue's real fetched rates, but only consults ReconClient::fetch_fee_rates when a reconcile client was built, which is gated on VIKE_RECONCILE=1 and off by default. The fees.rs row therefore decides on essentially every paper and backtest mount. Before the binance-perp row existed, a BTCUSDT.P mount was charged the spot 10/10 — five times the maker fee.
The run
crates/vike-backtest/tests/funding_carry_demo.rs is the end-to-end fixture, and its profile is the shape to copy. Two [[data.series]] entries under their own venues, engine.attach_funding = true, and a [strategy.params.venues] table mapping each symbol to the venue it routes under:
[data]
kind = "bar"
interval = "1h"
[[data.series]]
venue = "binance"
symbol = "BTCBIN"
[[data.series]]
venue = "okx"
symbol = "BTCOKX"
[engine]
cash = 100000.0
attach_funding = true
[strategy]
name = "funding_carry"
[strategy.params]
qty = 1.0
entry_threshold = 0.0
time_limit_ms = 14400000
[strategy.params.venues]
BTCBIN = "binance"
BTCOKX = "okx"The fixture seeds binance funding at +0.02 and okx at −0.01 every 8 hours: a gross differential of 0.03 against a round-trip cost of 2 × (0.001 + 0.0005) = 0.003, so the net edge clears an entry_threshold of 0.0 comfortably. Those rates are a deliberately fat fixture, not a market observation — the point is that the arithmetic is visible.
Three behaviours are pinned there, and they are the three you should reproduce:
- Two-leg, with
symbolleft empty: a leg opens on both carry venues from one mount, long the low-funding one and short the high. - Single-leg, with
symbol = "BTCBIN": only that symbol's leg opens, while the cross-venue book still informs the decision — the other venue is observed and never traded. - Inert, with no
venuesmap: both series route to one default venue, the funding book never reaches two venues,rank_best_carryreturnsNone, and the run ends with zero trades and zero funding accrued. The venue map is what unlocks the carry.
Attaching the funding series
engine.attach_funding window-joins the store's funding series onto the price bars for the same (venue, symbol), so the bar-loop accrual charges once per interval rather than once per bar. Four properties from its own doc are worth carrying:
- It is bar mode only — a profile that sets it with
data.kind = "tick"is rejected byBacktestProfile::validate, not ignored. - The price interval must be no coarser than the funding cadence. If more than one event lands in a price bar's window only the last is kept, and the collision is logged loudly.
- Events predating the first price bar are dropped, with a count on the warning.
- On but with no stored funding data is a warning and unchanged bars, never an error — so a silent zero-funding run is a thing to check for, not to assume away.
The limitation to plan around
The differential-compression and flip exit is built and tested (should_close_carry, precedence Flipped → ProfitTarget → Compressed) but is not wired through today's seam: Controller::evaluate is invited only while a pair is flat, so a held carry exits through its leg's TripleBarrier — the time_limit_ms in the profile above — not through a funding re-check. That is why the fixture sets a four-hour time limit. Plan the exit as a barrier, and read hold_periods as what it is: a constructor knob the live-params bag has no field for, so a re-tune leaves it untouched.
Next: funding carry for the parameter table, the controller seam for how a controller becomes a strategy, and run profiles for the rest of [engine].
Pairs stat-arb with a cost gate
Run the pairs_zscore template over two real Binance symbols with its round-trip cost floor armed, and read the negative result it exists to produce.
The grid template on paper
Mount the grid strategy on the production live core with the paper exchange behind it, and understand every refusal the profile has to pass first.