Model venue volume-tiered maker / taker fees¶
A flat fee rate buries a real PnL driver. For a market-maker running $10M+ daily volume, the difference between VIP-0 and VIP-3 is the entire margin. Strategies that look profitable at VIP-0 may be net-negative once the rebate ladder kicks in, and vice-versa.
FeeSchedule ships a volume-tiered ladder that mirrors how real
venues bill: add tiers with (min 30-day notional, maker bps, taker
bps), record every fill, and the schedule resolves the active tier
from the 30-day rolling sum on each lookup.
Configure¶
"""Track volume tiers and compute realistic fees per fill."""
import flox_py as flox
# Canned profile: Binance UM futures 10-tier ladder.
sched = flox.FeeSchedule.binance_um_futures()
# Or build manually:
custom = flox.FeeSchedule()
custom.add_tier(min_notional_30d=0, maker_bps=2.0, taker_bps=4.0)
custom.add_tier(min_notional_30d=1_000_000, maker_bps=1.0, taker_bps=3.0)
custom.add_tier(min_notional_30d=10_000_000, maker_bps=-0.5, taker_bps=2.0)
ts = 1_000_000_000
# A taker fill of 1000 USDT at the Regular tier costs 0.4 USDT.
fee = sched.fee_for(ts_ns=ts, notional=1000.0, is_maker=False)
print(f"taker fee @ tier 0: {fee:.4f} USDT")
# Push 1M notional through and re-query — tier advances, fee drops.
sched.record_fill(ts, 1_200_000.0)
fee2 = sched.fee_for(ts_ns=ts, notional=1000.0, is_maker=False)
print(f"after 1.2M volume: tier={sched.current_tier_index()}, fee={fee2:.4f}")
# Maker rebate kicks in at VIP-9.
sched.record_fill(ts, 1_000_000_000.0)
print(f"current tier={sched.current_tier_index()}, "
f"rolling 30d notional={sched.rolling_notional_30d():,.0f}")
# Maker fill at top tier — negative number means received rebate.
maker_fee = sched.fee_for(ts_ns=ts, notional=10_000.0, is_maker=True)
print(f"maker rebate @ top tier: {maker_fee:+.4f} USDT")
Canned profiles¶
| Profile | Tiers | Top maker rate |
|---|---|---|
binance_um_futures |
10 | -0.005% rebate |
bybit_linear |
6 | -0.005% rebate |
okx_swap |
4 | 0% |
deribit |
2 | -0.010% rebate above LV-1 |
Numbers reflect published VIP brackets at the time of writing. Tune for your actual tier; venue schedules drift.
Aggregating volume across symbols¶
record_fill accumulates into the schedule's own 30-day window. When
the schedule is bound to an Account, record_fill pushes into the
account instead and tier resolution reads the account's aggregate
counter, so volume across every symbol advances the same tier:
sched.bind_account(account) # tier now tracks account.rolling_notional_30d()
sched.clear_account_binding() # back to the schedule's own window
The VenueStack factories bind the account for you.
current_bps(now_ns) returns the active (maker_bps, taker_bps)
pair without computing a fee.
Fee sign convention¶
Returned fee is positive when the account pays, negative when the account receives (maker rebate). Integrate as:
So a maker rebate (negative fee) adds to equity.
Tier transition log¶
tier_transition_ts_ns() returns the timestamps of every tier
change recorded by record_fill. Useful for post-trade attribution
("which fills happened at each tier"):
transitions = sched.tier_transition_ts_ns()
print(f"changed tier {len(transitions)} times during backtest")
Notes¶
- The rolling window is exactly 30 days (
30 * 24 * 3600 * 1e9ns). Notional ages out on the next call after the cutoff. - Tiers are stored sorted ascending by
min_notional_30d. Insert order does not matter; the schedule sorts on everyadd_tier. - The active tier is resolved on every
fee_for/current_tier_indexcall (cheap — linear walk of typically < 10 tiers). reset_rolling()clears the 30-day window and the transition log (keeps the tier definitions).- For research that wants to disable fees entirely, instantiate an
empty
FeeSchedule()—fee_forreturns 0 for every fill.