Skip to main content
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