Commissions and slippage¶
QTrade ships four Commission implementations covering the patterns
most strategies need. All live in qtrade.core.commission and share
the abstract base Commission.
from qtrade.core import (
NoCommission, # default: 0
PercentageCommission, # X% of order value
FixedCommission, # flat fee per order
SlippageCommission, # X% slippage on order value
)
Pass an instance to Backtest(..., commission=...) or
TradingEnv(..., commission=...). commission=None is equivalent to
NoCommission().
The interface¶
Commission.calculate_commission(order_size: int, fill_price: float) -> float
Returns the commission in account currency for a single fill. The
Broker subtracts it from cash at fill time. Sign of order_size is
preserved — use abs(order_size) inside if you want symmetric fees on
buys and sells.
To roll your own:
from qtrade.core.commission import Commission
class TieredCommission(Commission):
"""0.1% under $10k, 0.05% above (per fill)."""
def calculate_commission(self, order_size, fill_price):
notional = abs(order_size) * fill_price
rate = 0.001 if notional < 10_000 else 0.0005
return notional * rate
Built-in implementations¶
NoCommission¶
NoCommission()
Returns 0.0 always. The default if you don’t pass anything. Use it
for clean comparisons against Buy & Hold, or when modeling commission-free
brokers.
PercentageCommission¶
PercentageCommission(percentage=0.001) # 0.1%
Charges abs(order_size) * fill_price * percentage.
The standard model for stocks (commissions quoted as bps of notional) and crypto spot (taker fees are typically 0.05–0.1%).
FixedCommission¶
FixedCommission(fixed_fee=1.0) # $1 per fill
Returns a flat fee regardless of size or price. Realistic for futures (where the exchange charges per-contract) and for discount brokers that quote a flat ticket charge.
For futures with multiple commission components (exchange + clearing +
broker), sum them up: FixedCommission(fixed_fee=2.50).
SlippageCommission¶
SlippageCommission(slippage_percentage=0.0005) # 5 bps
Mathematically identical to PercentageCommission, but the intent is
different — this models the price impact of crossing the spread,
not a venue fee. Use it when:
You want to add slippage on top of an explicit commission.
You want the cost to scale with notional but separate from “fees” in reporting.
# Crypto spot: 0.1% taker fee + 0.05% slippage
class CryptoCost(Commission):
def __init__(self):
self.fee = PercentageCommission(0.001)
self.slip = SlippageCommission(0.0005)
def calculate_commission(self, order_size, fill_price):
return self.fee.calculate_commission(order_size, fill_price) \
+ self.slip.calculate_commission(order_size, fill_price)
Picking the right model¶
Asset class |
Typical setup |
|---|---|
US equities (retail) |
|
US equities (institutional) |
|
Futures |
|
FX |
|
Crypto spot |
|
Crypto perps |
|
If unsure, run the backtest both with and without commission. The gap tells you how cost-sensitive your strategy is — high-turnover strategies often look great pre-cost and lose money post-cost.
A note on the built-in Contract specs¶
The qtrade.contracts module ships specs for common futures (GC_COMEX,
ES_CME, CL_NYMEX, etc.) that bundle a multiplier and a default
margin_ratio. The multiplier values are part of the contract design
and rarely change. The margin_ratio values are conventional starting
points only — actual margin is set by the exchange (SPAN) and your
broker, and floats over time. Verify against your account before
trusting backtest leverage numbers.
To override, either tweak with dataclasses.replace:
from dataclasses import replace
from qtrade.contracts import GC_COMEX
MY_GC = replace(GC_COMEX, margin_ratio=0.07)
…or define your own:
from qtrade.contracts import Contract
SHFE_AU = Contract(multiplier=1000, margin_ratio=0.08, name="SHFE Gold")
What’s not modeled¶
Maker/taker asymmetry. All fills are charged at one rate. To model maker fees on limit orders, write a custom
Commissionand branch on whether the order has alimitprice.Spread. The Broker fills market orders at
Close(or nextOpen). The bid/ask spread isn’t modeled —SlippageCommissionis the closest approximation.Funding rates / borrow costs. Not modeled. For long-running short positions or perp funding, you’d need to subtract from
cashmanually in your strategy’son_bar_close.Per-asset commission. A single
Commissioninstance applies to all assets in a multi-asset backtest. If you need different rates per asset, write a customCommissionthat branches on the order’s_fill_priceor wrap it in your strategy logic.