Skip to content
All library documents

Backtesting Close Orders with Daily Bars in Backtrader

Code backtrader

Summary

This example demonstrates how to simulate orders that execute at the close of daily bars in Backtrader. A sample strategy randomly chooses whether to open a long position or close an existing one, then reports when submitted orders complete. The random decisions illustrate order handling rather than a trading signal, so the example provides no evidence of profitability.

A custom data filter assigns a time near the end of the day to daily bars that lack a time field. This lets the backtest treat the bar as having passed the session end, and an optional broker setting marks the end-of-session bar accordingly. The command-line options support several CSV formats, date bounds, a session end time, and an optional time added to daily bars. The example is useful for studying Backtrader’s daily-bar timing and close-order mechanics, but its random entry and exit logic is only a demonstration.

Key ideas

  • The sample submits market orders configured to execute at the daily bar close.
  • A data filter can assign a time to daily bars that arrive without one.
  • An end-of-session setting and optional bar time help control daily-bar timing.
  • The random trading decisions demonstrate mechanics and do not establish a profitable strategy.

Tags

Full text
# close-daily.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 datetime
import random

import backtrader as bt
import backtrader.feeds as btfeeds

from backtrader.utils.py3 import with_metaclass


class St(bt.Strategy):
    def __init__(self):
        self.order = None

    def notify_order(self, order):
        curdtstr = self.data.datetime.datetime().strftime('%a %Y-%m-%d')
        if order.status in [order.Completed]:
            dtstr = bt.num2date(order.executed.dt).strftime('%a %Y-%m-%d')
            if order.isbuy():
                print('%s: BUY  EXECUTED, on:' % curdtstr, dtstr)
            else:  # Sell
                print('%s: SELL EXECUTED, on:' % curdtstr, dtstr)

            self.order = None

    def next(self):
        dtstr = self.data.datetime.datetime().strftime('%a %Y-%m-%d %H:%M:%S')
        # print('%s: data' % dtstr)
        if self.order:
            return

        if not random.randint(0, 5):  # roll a dice to decide entering/exit
            if self.position:
                print('%s: SELL CREATED' % dtstr)
                self.order = self.close(exectype=bt.Order.Close)
            else:  # no pending order
                print('%s: BUY  CREATED' % dtstr)
                self.order = self.buy(exectype=bt.Order.Close)


class SessionEndFiller(with_metaclass(bt.metabase.MetaParams, object)):
    '''This data filter simply adds the time given in param ``endtime`` to the
    current data datetime

    It is intended for daily bars which come from sources with no time
    indication and can be used to signal the bar is passed the end of the
    session

    The default value for ``endtime`` is 1 second before midnight 23:59:59
    '''
    params = (('endtime', datetime.time(23, 59, 59)),)

    def __call__(self, data):
        '''
        Params:
          - data: the data source to filter/process

        Returns:
          - False (always) because this filter does not remove bars from the
            stream
        '''
        # Get time of current (from data source) bar
        dtime = datetime.combine(data.datetime.date(), self.p.endtime)
        data.datetime[0] = data.date2num(dtime)
        return False


def runstrat():
    args = parse_args()

    cerebro = bt.Cerebro()
    cerebro.adddata(getdata(args))
    cerebro.addstrategy(St)
    if args.eosbar:
        cerebro.broker.seteosbar(True)

    cerebro.run()


def getdata(args):

    dataformat = dict(
        bt=btfeeds.BacktraderCSVData,
        visualchart=btfeeds.VChartCSVData,
        sierrachart=btfeeds.SierraChartCSVData,
        yahoo=btfeeds.YahooFinanceCSVData,
        yahoo_unreversed=btfeeds.YahooFinanceCSVData
    )

    dfkwargs = dict()
    if args.csvformat == 'yahoo_unreversed':
        dfkwargs['reverse'] = True

    if args.fromdate:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dfkwargs['fromdate'] = fromdate

    if args.todate:
        fromdate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dfkwargs['todate'] = todate

    if args.tend is not None:
        # internally only the "time" part is used
        dfkwargs['sessionend'] = datetime.datetime.strptime(args.tend, '%H:%M')

    dfkwargs['dataname'] = args.infile
    dfcls = dataformat[args.csvformat]

    data = dfcls(**dfkwargs)

    if args.filltime is not None:
        filltime = datetime.datetime.strptime(args.filltime, '%H:%M:%S').time()
        data.addfilter(SessionEndFiller, endtime=filltime)

    return data


def parse_args():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Sample for Close Orders with daily data')

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

    parser.add_argument('--csvformat', '-c', required=False, default='bt',
                        choices=['bt', 'visualchart', 'sierrachart',
                                 'yahoo', 'yahoo_unreversed'],
                        help='CSV Format')

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

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

    parser.add_argument('--eosbar', required=False, action='store_true',
                        help=('Consider a bar with the end of session time to'
                              'be the end of the session'))

    parser.add_argument('--tend', '-te',
                        default=None, required=False,
                        help='End time for the Session Filter (HH:MM)')

    parser.add_argument('--filltime', '-ftime',
                        default=None, required=False,
                        help='Add Time to daily bars (HH:MM:SS)')

    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.