How to make a crypto backtest charge fees and funding

Four engines, four ways to charge maker and taker fees and perpetual funding — and the settings each one falls back to when you give it nothing.

Give the engine your own tier's maker and taker rates rather than its default, and charge funding from the venue's settled history at mark-price notional. Freqtrade does both from exchange data in futures mode; NautilusTrader and LEAN settle funding only from records you load; hftbacktest charges maker and taker per fill and has no funding at all. Then check which side of a settlement instant your bar-open fills land on.

The short way

Use Freqtrade in futures mode, and type in your own fee. It is the one engine in this category that downloads the funding history itself and charges it without being asked.

freqtrade download-data --trading-mode futures --timeframes 5m --pairs BTC/USDT:USDT
freqtrade backtesting --fee 0.0005

The first command pulls the price candles and, because the mode is futures, the mark and funding_rate series beside them. The backtest takes its mode from the configuration rather than a flag, so the config file needs "trading_mode": "futures" as well. It then charges each open trade the funding rate times the mark price times its size at every settlement it holds through, so the payment follows the mark rather than your entry price — which is how the venue computes it too: Binance defines the funding amount as the nominal value of the position, and the nominal value as mark price times contract size.

The second flag is the one people leave out. Without it Freqtrade reads the fee from the exchange's market info, takes the maker and the taker rate, and uses whichever is higher, which it logs as the "worst case fee from exchange (lowest tier)". That is a sensible default for a strategy that takes liquidity and a wrong one for anything else, and neither number is your account's if you have a volume tier or a token discount. --fee is a single ratio, applied once on entry and once on exit.

That is the whole short way for a strategy that crosses the spread. If your result depends on earning the maker rate, or on a venue Freqtrade cannot fetch funding from, read on.

What the options are

Freqtrade, when crossing the spread is the strategy. Covered above. What it cannot express is two rates: one fee covers entries and exits, maker and taker alike, so a strategy whose margin is the gap between the two is being charged the wrong shape of cost whichever number you pick.

NautilusTrader, when you want both rates and are willing to bring the data. NautilusTrader takes a maker and a taker rate per simulated venue and charges each fill by the side it actually took. Funding it settles properly, but only from FundingRateUpdate records you construct, and its documentation is exact about when one becomes a payment: at next_funding_ns if the record carries it, otherwise only when ts_event lands on the interval boundary, and otherwise never — "updates without a boundary remain strategy data". So build them from the venue's settled history with the boundary written in:

from decimal import Decimal
from nautilus_trader.model import FundingRateUpdate, InstrumentId

PERP = InstrumentId.from_str("BTCUSDT-PERP.BINANCE")

def to_update(row):  # one row of Binance GET /fapi/v1/fundingRate
    settle_ns = row["fundingTime"] * 1_000_000  # milliseconds to nanoseconds
    return FundingRateUpdate(
        instrument_id=PERP,
        rate=Decimal(row["fundingRate"]),
        ts_event=settle_ns - 1,
        ts_init=settle_ns - 1,
        next_funding_ns=settle_ns,
    )

Stamping the record a nanosecond before the settlement and naming the settlement explicitly means the payment happens at the venue's own timestamp, whatever the symbol's interval was that day. A positive rate debits longs and credits shorts, and the payment moves realised profit and the account balance together.

LEAN, when the maker-or-taker decision should follow the order. LEAN's Binance fee models charge the maker rate only to a limit order that was post-only or not marketable when it arrived, and the taker rate to everything else — the venue's own rule, applied per order. The rates are constructor arguments with first-tier defaults, and a comment in the code says why: it does "not model 30-day volume, so we use the first tier". Pass yours. Funding is read from a two-column CSV per symbol under cryptofuture/<market>/margin_interest/ in the data folder, one line per settlement, 20221213 00:00:00,0.00010000. Write it from the venue's history the same way you would for Nautilus.

hftbacktest, when the maker rate is the edge. hftbacktest charges maker or taker by how each fill happened — resting in the simulated queue, or crossing on arrival — rather than by the order type you asked for, and its fee builder states that "a negative fee represents rebates". If you never call one, both rates are zero:

asset = BacktestAsset().trading_value_fee_model(-0.00005, 0.0004)  # maker rebate, taker fee

It has no funding model. For a quoting strategy that holds inventory for seconds that is often fine; for one that holds across a settlement, the funding leg has to be computed separately from the recorded position series and subtracted.

Where this breaks

A fill at the settlement instant is charged by one engine and not by another. Candle engines fill at bar boundaries, and on any timeframe that divides eight hours some bar opens exactly at 08:00:00. Freqtrade sums every funding row from the trade's open time onward, inclusive, so a trade opened on that bar pays the 08:00 settlement. LEAN schedules the first payment for the next boundary after the position opens, so the same trade opened at 08:00:00 skips it and pays at 16:00. The venue is no help in settling the argument: Binance says there is a 15-second deviation in the actual funding time, and that a position opened at 08:00:05 may still be charged. On a strategy that enters around settlements the difference is one payment per trade, systematically, in one direction.

A missing row is a free settlement. Freqtrade joins the funding series to the mark series on timestamp and keeps only the rows present in both, so a settlement with no mark candle at that instant drops out of the sum and is charged nothing. When the funding history does not reach back far enough, the documented workaround is the futures_funding_rate setting, which fills the gap with one constant. It fills only the stretch before the first real row; a hole in the middle of the series is still charged nothing. The docs recommend zero and say plainly that "this will mean your backtests are inaccurate" for those periods. Either way nothing in the output tells you which trades paid real funding and which paid a fill value. Count your funding rows against the number of settlements in the range before you run anything.

The notional is not always the mark. Binance charges the rate on mark price times size. Freqtrade does the same from its mark series. LEAN's funding model multiplies the rate by the holding's value at the security's current price, which in a backtest built on trade bars is the last trade, not a mark. Usually the two are close; in a dislocation, which is when funding is largest, they are not. See mark price for why the venue keeps them apart.

A normalised funding series is the wrong input. Some aggregators rescale rates to a common per-eight-hour figure so venues can be compared side by side, which is the job described in how to get funding rates and open interest. An engine wants the opposite: the rate each settlement actually paid, at the time it actually paid it. Feed a per-eight-hour figure into an hourly settlement schedule and the engine charges eight times the funding, every hour.

Maker is an outcome, not a setting. A limit order priced through the book fills immediately and pays the taker fee; LEAN encodes exactly that rule, and it is the one the venue applies. Typing the maker rate into an engine that takes one fee for everything, as Freqtrade does, gives it to every fill, including the ones that crossed. And the rate itself is your tier's, not the schedule's first line: Freqtrade's docs note that volume and balance discounts are "not visible to ccxt", and LEAN's code says it does not model 30-day volume at all.

This page makes the cost model honest. It does not touch the fill rule, the mark price that liquidation reads, or the settlement schedule an engine assumes when it has no data — those are the subject of what a crypto backtest silently assumes, and none of them is fixed by the steps above.

If you outgrow this

If the problem is writing funding files by hand for LEAN, QuantConnect Cloud is the same engine with the margin-rate datasets already loaded — the piece the open-source engine leaves to you, and the one whose absence fails silently.

If the problem is funding history that stops too early, the settled series is a public endpoint on Binance, Bybit and OKX, and Binance also publishes it in its flat-file archive, under a non-commercial licence that the card spells out. Beyond that it is an archive purchase: Tardis.dev sells funding beside the tick data, which matters if you also want the book for a fill model.

If the problem is that the maker rate is the whole result, a candle engine cannot answer it at any fee setting, because it cannot tell you whether the resting order would have been reached. That is hftbacktest's question, at the price of tick data and a funding leg you write yourself.

And if the problem is the rest of the cost model — the fill rule, liquidation, a mark price the engine never received — that is the guide linked above, not a fee setting.

The tools named above

In the order this page puts them in, which is an editorial judgement and not a ranking anyone paid for.

  1. Freqtrade

    Futures mode downloads mark and funding-rate candles and charges funding at mark price. The fee defaults to the worse of maker and taker, at the lowest tier.

    Write the strategy in Python, backtest it, then run the same class live.

    FreeFree tierOpen source

  2. NautilusTrader

    Maker and taker rates per venue through a fee model, and funding settled at each boundary from FundingRateUpdate records you build from venue history.

    Event-driven Rust engine that settles perpetual funding at the venue boundary.

    FreeFree tierOpen source

  3. LEAN

    Maker or taker decided by whether the limit order was marketable, first-tier rates unless you pass yours, and funding read from a CSV in the data folder.

    The engine behind QuantConnect, Apache-2.0 and runnable on your own machine.

    FreeFree tierOpen source

  4. HftBacktest

    Maker and taker decided by the queue simulation, rebates as negative fees — and no funding model, so the funding leg is yours to add.

    Tick-by-tick backtesting that models order queue position and feed and order latency.

    FreeFree tierOpen source

FAQ

Which fee should I put in a crypto backtest, maker or taker?

Whichever each fill would actually have paid. A limit order that rests pays maker and one that crosses the book on arrival pays taker, which is the rule LEAN applies per order. If your engine takes a single rate, as Freqtrade does, use the taker rate unless the strategy only ever rests — Freqtrade's own default picks the higher of the two for that reason.

Does Freqtrade include funding fees in backtests?

In futures mode, yes. Downloading data with the futures trading mode also fetches mark and funding-rate series, and the backtest charges the rate times the mark price times the position size at each settlement a trade holds through. Where the funding history starts later than the backtest, the futures_funding_rate setting fills that leading stretch with a constant, and the docs recommend zero. A gap in the middle of the history is not filled and is charged nothing.

How do I add funding to NautilusTrader?

Build FundingRateUpdate records from the venue's settled funding history and add them as data. A record only becomes a payment at a boundary — next_funding_ns if you set it, otherwise when ts_event lands on the interval — so set next_funding_ns to the settlement time the venue reported. A positive rate then debits longs and credits shorts at that instant.

Why would two engines charge different funding on the same trade?

Three common reasons. They disagree on whether a position opened exactly at the settlement instant pays it — Freqtrade charges it, LEAN waits for the next one. They multiply the rate by different prices, mark in Freqtrade and the last price in LEAN. And a gap in the funding history is either skipped or filled with a constant, depending on the engine and how it was configured.

Sources

  1. Backtesting — supplying a custom fee value — Freqtrade, read
  2. Trading with leverage — unavailable funding rates — Freqtrade, read
  3. freqtrade/optimize/backtesting.py, set_fee — Freqtrade, read
  4. freqtrade/exchange/exchange.py, combine_funding_and_mark and calculate_funding_fees — Freqtrade, read
  5. Backtest accounts and margin — Nautech Systems, read
  6. FundingRateUpdate — Nautech Systems, read
  7. Common/Orders/Fees/BinanceFeeModel.cs — QuantConnect, read
  8. Common/Orders/Fees/BinanceFuturesFeeModel.cs — QuantConnect, read
  9. Common/Securities/CryptoFuture/BinanceFutureMarginInterestRateModel.cs — QuantConnect, read
  10. Common/Data/Market/MarginInterestRate.cs, GetSource — QuantConnect, read
  11. py-hftbacktest/src/lib.rs, BacktestAsset fee models — hftbacktest, read
  12. Introduction to Binance Futures Funding Rates — Binance,

The catalogue next door

This page names a handful of products. The rest of them are in Backtesting & Research Libraries, each filled in against the same schema, with the fields to narrow it yourself.

Last updated . Corrected in place — an endpoint that moves is a bug on this page, not a new post.