Skip to content
All library documents

Modeling Fees, Slippage, Partial Fills, and Buying Power in Backtests

Article Strategy library · Author: QuantConnect

Summary

This example demonstrates how to customize transaction assumptions in a backtest by supplying separate models for fees, slippage, order fills, and buying power. Its sample algorithm trades an equity, while the custom fee calculation applies a minimum charge or a price-and-quantity-based fee, and the slippage estimate grows with order size. The buying-power example accepts every order, illustrating how a model can override the usual constraint.

The fill examples show two approaches: a market fill model that uses seeded randomness to simulate partial fills over multiple events, and a simpler model for market, stop-market, and limit orders based on quote or bar prices. The example helps explain where execution assumptions enter a backtest, but its formulas and behaviors are illustrative rather than calibrated evidence of real market conditions. In particular, always granting buying power and simplified bar-based fill checks can make simulated results unrealistic. Researchers should base such models on relevant venue, instrument, and order data.

Key ideas

  • Backtests can assign custom logic to fees, slippage, fills, and buying-power checks.
  • A fill model can simulate partial execution and retain the unfilled quantity for later fills.
  • The example fee and slippage formulas are configurable assumptions rather than empirical estimates.
  • Simplified stop and limit fill rules use bar highs and lows or available quotes.
  • An always-sufficient buying-power model can permit orders that would not be feasible in practice.

Tags

Full text
# CustomModelsAlgorithm


# CustomModelsAlgorithm









Demonstration of using custom fee, slippage, fill, and buying power models for modelling transactions in backtesting.
    QuantConnect allows you to model all orders as deeply and accurately as you need.

Demonstration of using custom fee, slippage, fill, and buying power models for modelling transactions in backtesting. QuantConnect allows you to model all orders as deeply and accurately as you need.

## Source (Apache-2.0)

```python
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *
import random

### <summary>
### Demonstration of using custom fee, slippage, fill, and buying power models for modelling transactions in backtesting.
### QuantConnect allows you to model all orders as deeply and accurately as you need.
### </summary>
### <meta name="tag" content="trading and orders" />
### <meta name="tag" content="transaction fees and slippage" />
### <meta name="tag" content="custom buying power models" />
### <meta name="tag" content="custom transaction models" />
### <meta name="tag" content="custom slippage models" />
### <meta name="tag" content="custom fee models" />
class CustomModelsAlgorithm(QCAlgorithm):
    '''Demonstration of using custom fee, slippage, fill, and buying power models for modelling transactions in backtesting.
    QuantConnect allows you to model all orders as deeply and accurately as you need.'''

    def initialize(self):
        self.set_start_date(2013,10,1)   # Set Start Date
        self.set_end_date(2013,10,31)    # Set End Date
        self.security = self.add_equity("SPY", Resolution.HOUR)
        self.spy = self.security.symbol

        # set our models
        self.security.set_fee_model(CustomFeeModel(self))
        self.security.set_fill_model(CustomFillModel(self))
        self.security.set_slippage_model(CustomSlippageModel(self))
        self.security.set_buying_power_model(CustomBuyingPowerModel(self))

    def on_data(self, data):
        open_orders = self.transactions.get_open_orders(self.spy)
        if len(open_orders) != 0: return

        if self.time.day > 10 and self.security.holdings.quantity <= 0:
            quantity = self.calculate_order_quantity(self.spy, .5)
            self.log(f"MarketOrder: {quantity}")
            self.market_order(self.spy, quantity, True)   # async needed for partial fill market orders

        elif self.time.day > 20 and self.security.holdings.quantity >= 0:
            quantity = self.calculate_order_quantity(self.spy, -.5)
            self.log(f"MarketOrder: {quantity}")
            self.market_order(self.spy, quantity, True)   # async needed for partial fill market orders

# If we want to use methods from other models, you need to inherit from one of them
class CustomFillModel(ImmediateFillModel):
    def __init__(self, algorithm):
        super().__init__()
        self.algorithm = algorithm
        self.absolute_remaining_by_order_id = {}
        self.random = Random(387510346)

    def market_fill(self, asset, order):
        absolute_remaining = order.absolute_quantity

        if order.id in self.absolute_remaining_by_order_id.keys():
            absolute_remaining = self.absolute_remaining_by_order_id[order.id]

        fill = super().market_fill(asset, order)
        absolute_fill_quantity = int(min(absolute_remaining, self.random.next(0, 2*int(order.absolute_quantity))))
        fill.fill_quantity = np.sign(order.quantity) * absolute_fill_quantity

        if absolute_remaining == absolute_fill_quantity:
            fill.status = OrderStatus.FILLED
            if self.absolute_remaining_by_order_id.get(order.id):
                self.absolute_remaining_by_order_id.pop(order.id)
        else:
            absolute_remaining = absolute_remaining - absolute_fill_quantity
            self.absolute_remaining_by_order_id[order.id] = absolute_remaining
            fill.status = OrderStatus.PARTIALLY_FILLED
        self.algorithm.log(f"CustomFillModel: {fill}")
        return fill

class CustomFeeModel(FeeModel):
    def __init__(self, algorithm):
        super().__init__()
        self.algorithm = algorithm

    def get_order_fee(self, parameters):
        # custom fee math
        fee = max(1, parameters.security.price
                  * parameters.order.absolute_quantity
                  * 0.00001)
        self.algorithm.log(f"CustomFeeModel: {fee}")
        return OrderFee(CashAmount(fee, "USD"))

class CustomSlippageModel:
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def get_slippage_approximation(self, asset, order):
        # custom slippage math
        slippage = asset.price * 0.0001 * np.log10(2*float(order.absolute_quantity))
        self.algorithm.log(f"CustomSlippageModel: {slippage}")
        return slippage

class CustomBuyingPowerModel(BuyingPowerModel):
    def __init__(self, algorithm):
        super().__init__()
        self.algorithm = algorithm

    def has_sufficient_buying_power_for_order(self, parameters):
        # custom behavior: this model will assume that there is always enough buying power
        has_sufficient_buying_power_for_order_result = HasSufficientBuyingPowerForOrderResult(True)
        self.algorithm.log(f"CustomBuyingPowerModel: {has_sufficient_buying_power_for_order_result.is_sufficient}")
        return has_sufficient_buying_power_for_order_result

# The simple fill model shows how to implement a simpler version of
# the most popular order fills: Market, Stop Market and Limit
class SimpleCustomFillModel(FillModel):
    def __init__(self):
        super().__init__()

    def _create_order_event(self, asset, order):
        utc_time = Extensions.convert_to_utc(asset.local_time, asset.exchange.time_zone)
        return OrderEvent(order, utc_time, OrderFee.ZERO)

    def _set_order_event_to_filled(self, fill, fill_price, fill_quantity):
        fill.status = OrderStatus.FILLED
        fill.fill_quantity = fill_quantity
        fill.fill_price = fill_price
        return fill

    def _get_trade_bar(self, asset, order_direction):
        trade_bar = asset.cache.get_data(TradeBar)
        if trade_bar: return trade_bar

        # Tick-resolution data doesn't have TradeBar, use the asset price
        price = asset.price
        return TradeBar(asset.local_time, asset.symbol, price, price, price, price, 0)

    def market_fill(self, asset, order):
        fill = self._create_order_event(asset, order)
        if order.status == OrderStatus.CANCELED: return fill

        return self._set_order_event_to_filled(fill,
            asset.cache.ask_price \
                if order.direction == OrderDirection.BUY else asset.cache.bid_price,
            order.quantity)

    def stop_market_fill(self, asset, order):
        fill = self._create_order_event(asset, order)
        if order.status == OrderStatus.CANCELED: return fill

        stop_price = order.stop_price
        trade_bar = self._get_trade_bar(asset, order.direction)

        if order.direction == OrderDirection.SELL and trade_bar.low < stop_price:
            return self._set_order_event_to_filled(fill, stop_price, order.quantity)

        if order.direction == OrderDirection.BUY and trade_bar.high > stop_price:
            return self._set_order_event_to_filled(fill, stop_price, order.quantity)

        return fill

    def limit_fill(self, asset, order):
        fill = self._create_order_event(asset, order)
        if order.status == OrderStatus.CANCELED: return fill

        limit_price = order.limit_price
        trade_bar = self._get_trade_bar(asset, order.direction)

        if order.direction == OrderDirection.SELL and trade_bar.high > limit_price:
            return self._set_order_event_to_filled(fill, limit_price, order.quantity)

        if order.direction == OrderDirection.BUY and trade_bar.low < limit_price:
            return self._set_order_event_to_filled(fill, limit_price, order.quantity)

        return fill

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.