Compare flat fees versus split maker/taker fees to understand how fee structure impacts strategy performance.
Full Code
"""Compare flat fees vs maker/taker split fees on the same strategy."""
import horizon as hz
def fair_value(ctx: hz.Context) -> float:
return ctx.feed.price * 1.01
def quoter(ctx: hz.Context, fair: float):
if fair > ctx.feed.price + 0.02:
return hz.quotes(ctx.feed.price, spread=0.04, size=10)
# Sample data
import random
random.seed(42)
price = 0.50
data = []
for i in range(500):
price += random.gauss(0, 0.01)
price = max(0.01, min(0.99, price))
data.append({"timestamp": 1700000000 + i, "price": round(price, 4)})
# --- Flat fee ---
flat_result = hz.backtest(
name="flat-fee",
markets=["test"],
data=data,
pipeline=[fair_value, quoter],
paper_fee_rate=0.001,
)
# --- Split maker/taker ---
split_result = hz.backtest(
name="split-fee",
markets=["test"],
data=data,
pipeline=[fair_value, quoter],
paper_maker_fee_rate=0.0002,
paper_taker_fee_rate=0.002,
)
print("=== Flat Fee (10 bps) ===")
print(flat_result.summary())
print(f"Total fees: ${flat_result.metrics.total_fees:.4f}")
print("\n=== Split Fee (2 bps maker / 20 bps taker) ===")
print(split_result.summary())
print(f"Total fees: ${split_result.metrics.total_fees:.4f}")
# Inspect maker/taker breakdown
maker_fills = [f for f in split_result.trades if f.is_maker]
taker_fills = [f for f in split_result.trades if not f.is_maker]
print(f"\nMaker fills: {len(maker_fills)}")
print(f"Taker fills: {len(taker_fills)}")
How It Works
Flat fees (default)
A single paper_fee_rate is applied to every fill regardless of whether the order was resting in the book (maker) or crossing the spread (taker).
engine = Engine(paper_fee_rate=0.001) # 10 bps on all fills
Split maker/taker fees
Set different rates for each role. Makers add liquidity and typically pay lower fees. Takers remove liquidity and pay higher fees.
engine = Engine(
paper_maker_fee_rate=0.0002, # 2 bps for makers
paper_taker_fee_rate=0.002, # 20 bps for takers
)
You can also override just one side. The other falls back to paper_fee_rate:
engine = Engine(
paper_fee_rate=0.001, # 10 bps default
paper_taker_fee_rate=0.003, # 30 bps for takers only
)
# Makers still pay 10 bps
Checking fill type
Every Fill includes an is_maker boolean:
fills = engine.recent_fills()
for f in fills:
role = "maker" if f.is_maker else "taker"
print(f"{f.fill_id}: {role} fee={f.fee:.6f}")
When to Use Split Fees
- Backtesting market-making strategies where most fills should be maker fills at lower fees
- Modeling real exchange fee tiers (Polymarket charges 0 bps maker / 100 bps taker)
- Comparing strategy profitability under different fee structures
- Stress testing to see if your edge survives higher taker fees
In the paper exchange, maker/taker classification is based on the order price relative to the mid price at fill time. For more realistic classification using L2 orderbook data, use hz.backtest() with book_data which switches to the BookSim exchange.
Run It
python examples/maker_taker_fees.py