Skip to content
All library documents

Testing Percentage and Fixed Slippage in an SMA Crossover Strategy

Code backtrader

Summary

This Backtrader example shows how to model execution slippage in a simple moving average crossover strategy. It compares a fast and slow SMA, generates signals when they cross, and lets the user choose long-only or long-short trading. The strategy reports completed orders with their execution prices, making the effect of configured slippage visible in the trade log.

Slippage can be set as either a percentage or a fixed amount. Options control whether slippage applies at the next open, whether prices are capped to the bar range, and whether simulated prices may fall outside that range. The example can also restrict the input data by date, set starting cash, change SMA periods, and plot results. It is a framework demonstration rather than a performance study: it provides no comparative results, market impact model, or evidence that the crossover is profitable. Its bar-based settings also simplify real execution conditions.

Key ideas

  • The example generates trading signals when fast and slow simple moving averages cross.
  • Slippage can be configured as a percentage or a fixed price amount.
  • Options determine how simulated slippage interacts with bar extremes and next-open fills.
  • Completed orders are reported with execution size and price.
  • The example demonstrates configuration and does not establish strategy profitability.

Tags

Full text
# slippage.py


```py
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import collections
import datetime
import itertools

import backtrader as bt


class SMACrossOver(bt.Indicator):
    lines = ('signal',)
    params = (('p1', 10), ('p2', 30),)

    def __init__(self):
        sma1 = bt.indicators.SMA(period=self.p.p1)
        sma2 = bt.indicators.SMA(period=self.p.p2)
        self.lines.signal = bt.indicators.CrossOver(sma1, sma2)


class SlipSt(bt.SignalStrategy):
    opcounter = itertools.count(1)

    def notify_order(self, order):
        if order.status == bt.Order.Completed:
            t = ''
            t += '{:02d}'.format(next(self.opcounter))
            t += ' {}'.format(order.data.datetime.datetime())
            t += ' BUY ' * order.isbuy() or ' SELL'
            t += ' Size: {:+d} / Price: {:.2f}'
            print(t.format(order.executed.size, order.executed.price))


def runstrat(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()
    cerebro.broker.set_cash(args.cash)

    dkwargs = dict()
    if args.fromdate is not None:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dkwargs['fromdate'] = fromdate

    if args.todate is not None:
        todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dkwargs['todate'] = todate

    # if dataset is None, args.data has been given
    data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs)
    cerebro.adddata(data)

    cerebro.signal_strategy(SlipSt)
    if not args.longonly:
        stype = bt.signal.SIGNAL_LONGSHORT
    else:
        stype = bt.signal.SIGNAL_LONG

    cerebro.add_signal(stype, SMACrossOver, p1=args.period1, p2=args.period2)

    if args.slip_perc is not None:
        cerebro.broker.set_slippage_perc(args.slip_perc,
                                         slip_open=args.slip_open,
                                         slip_match=not args.no_slip_match,
                                         slip_out=args.slip_out)

    elif args.slip_fixed is not None:
        cerebro.broker.set_slippage_fixed(args.slip_fixed,
                                          slip_open=args.slip_open,
                                          slip_match=not args.no_slip_match,
                                          slip_out=args.slip_out)

    cerebro.run()
    if args.plot:
        pkwargs = dict(style='bar')
        if args.plot is not True:  # evals to True but is not True
            npkwargs = eval('dict(' + args.plot + ')')  # args were passed
            pkwargs.update(npkwargs)

        cerebro.plot(**pkwargs)


def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Slippage')

    parser.add_argument('--data', required=False,
                        default='../../datas/2005-2006-day-001.txt',
                        help='Specific data to be read in')

    parser.add_argument('--fromdate', required=False, default=None,
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', required=False, default=None,
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--cash', required=False, action='store',
                        type=float, default=50000,
                        help=('Cash to start with'))

    parser.add_argument('--period1', required=False, action='store',
                        type=int, default=10,
                        help=('Fast moving average period'))

    parser.add_argument('--period2', required=False, action='store',
                        type=int, default=30,
                        help=('Slow moving average period'))

    parser.add_argument('--longonly', required=False, action='store_true',
                        help=('Long only strategy'))

    pgroup = parser.add_mutually_exclusive_group(required=False)
    pgroup.add_argument('--slip_perc', required=False, default=None,
                        type=float,
                        help='Set the value for commission percentage')

    pgroup.add_argument('--slip_fixed', required=False, default=None,
                        type=float,
                        help='Set the value for commission percentage')

    parser.add_argument('--no-slip_match', required=False, action='store_true',
                        help=('Match by capping slippage at bar ends'))

    parser.add_argument('--slip_out', required=False, action='store_true',
                        help=('Disable capping and return non-real prices'))

    parser.add_argument('--slip_open', required=False, action='store_true',
                        help=('Slip even if match price is next open'))

    # Plot options
    parser.add_argument('--plot', '-p', nargs='?', required=False,
                        metavar='kwargs', const=True,
                        help=('Plot the read data applying any kwargs passed\n'
                              '\n'
                              'For example:\n'
                              '\n'
                              '  --plot style="candle" (to plot candles)\n'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()


if __name__ == '__main__':
    runstrat()

```

Shown in full with attribution under the source's licence. Licence: GPL-3.0

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