Backtrader OANDA Feed and Order Handling Example
Summary
This example script demonstrates how to connect Backtrader to OANDA data and optionally use an OANDA broker. It defines a sample strategy that logs market data and a simple moving average, tracks data and order notifications, and can submit sample buy or sell orders. Optional behaviors include bracket orders, order cancellation after a configured number of bars, counter-trades, and stopping after a specified number of live data lines. Command-line options configure data symbols, timeframes, resampling or replay, execution type, stake, and account connection mode.
The strategy is primarily a framework and integration example, not a tested trading system. Trading is disabled by default, and the sample order logic does not use the moving average as an entry signal. The excerpt is incomplete and provides no results, profitability analysis, or risk evaluation. It is useful for understanding data-feed and order lifecycle wiring, but its sample trades should not be treated as a market-tested method.
Key ideas
- The script connects Backtrader to OANDA market data and can optionally configure the broker.
- It logs data, indicator values, order updates, and trade notifications.
- Sample trading is disabled by default and can submit market, limit, or bracket orders.
- Command-line options control data handling, order behavior, and live operation.
- The example provides no evidence of trading performance.
Tags
Full text
# oandatest.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
# The above could be sent to an independent module
import backtrader as bt
from backtrader.utils import flushfile # win32 quick stdout flushing
StoreCls = bt.stores.OandaStore
DataCls = bt.feeds.OandaData
# BrokerCls = bt.brokers.OandaBroker
class TestStrategy(bt.Strategy):
params = dict(
smaperiod=5,
trade=False,
stake=10,
exectype=bt.Order.Market,
stopafter=0,
valid=None,
cancel=0,
donotcounter=False,
sell=False,
usebracket=False,
)
def __init__(self):
# To control operation entries
self.orderid = list()
self.order = None
self.counttostop = 0
self.datastatus = 0
# Create SMA on 2nd data
self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod)
print('--------------------------------------------------')
print('Strategy Created')
print('--------------------------------------------------')
def notify_data(self, data, status, *args, **kwargs):
print('*' * 5, 'DATA NOTIF:', data._getstatusname(status), *args)
if status == data.LIVE:
self.counttostop = self.p.stopafter
self.datastatus = 1
def notify_store(self, msg, *args, **kwargs):
print('*' * 5, 'STORE NOTIF:', msg)
def notify_order(self, order):
if order.status in [order.Completed, order.Cancelled, order.Rejected]:
self.order = None
print('-' * 50, 'ORDER BEGIN', datetime.datetime.now())
print(order)
print('-' * 50, 'ORDER END')
def notify_trade(self, trade):
print('-' * 50, 'TRADE BEGIN', datetime.datetime.now())
print(trade)
print('-' * 50, 'TRADE END')
def prenext(self):
self.next(frompre=True)
def next(self, frompre=False):
txt = list()
txt.append('Data0')
txt.append('%04d' % len(self.data0))
dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
txt.append('{:f}'.format(self.data.datetime[0]))
txt.append('%s' % self.data.datetime.datetime(0).strftime(dtfmt))
txt.append('{:f}'.format(self.data.open[0]))
txt.append('{:f}'.format(self.data.high[0]))
txt.append('{:f}'.format(self.data.low[0]))
txt.append('{:f}'.format(self.data.close[0]))
txt.append('{:6d}'.format(int(self.data.volume[0])))
txt.append('{:d}'.format(int(self.data.openinterest[0])))
txt.append('{:f}'.format(self.sma[0]))
print(', '.join(txt))
if len(self.datas) > 1 and len(self.data1):
txt = list()
txt.append('Data1')
txt.append('%04d' % len(self.data1))
dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
txt.append('{}'.format(self.data1.datetime[0]))
txt.append('%s' % self.data1.datetime.datetime(0).strftime(dtfmt))
txt.append('{}'.format(self.data1.open[0]))
txt.append('{}'.format(self.data1.high[0]))
txt.append('{}'.format(self.data1.low[0]))
txt.append('{}'.format(self.data1.close[0]))
txt.append('{}'.format(self.data1.volume[0]))
txt.append('{}'.format(self.data1.openinterest[0]))
txt.append('{}'.format(float('NaN')))
print(', '.join(txt))
if self.counttostop: # stop after x live lines
self.counttostop -= 1
if not self.counttostop:
self.env.runstop()
return
if not self.p.trade:
return
if self.datastatus and not self.position and len(self.orderid) < 1:
if not self.p.usebracket:
if not self.p.sell:
# price = round(self.data0.close[0] * 0.90, 2)
price = self.data0.close[0] - 0.005
self.order = self.buy(size=self.p.stake,
exectype=self.p.exectype,
price=price,
valid=self.p.valid)
else:
# price = round(self.data0.close[0] * 1.10, 4)
price = self.data0.close[0] - 0.05
self.order = self.sell(size=self.p.stake,
exectype=self.p.exectype,
price=price,
valid=self.p.valid)
else:
print('USING BRACKET')
price = self.data0.close[0] - 0.05
self.order, _, _ = self.buy_bracket(size=self.p.stake,
exectype=bt.Order.Market,
price=price,
stopprice=price - 0.10,
limitprice=price + 0.10,
valid=self.p.valid)
self.orderid.append(self.order)
elif self.position and not self.p.donotcounter:
if self.order is None:
if not self.p.sell:
self.order = self.sell(size=self.p.stake // 2,
exectype=bt.Order.Market,
price=self.data0.close[0])
else:
self.order = self.buy(size=self.p.stake // 2,
exectype=bt.Order.Market,
price=self.data0.close[0])
self.orderid.append(self.order)
elif self.order is not None and self.p.cancel:
if self.datastatus > self.p.cancel:
self.cancel(self.order)
if self.datastatus:
self.datastatus += 1
def start(self):
if self.data0.contractdetails is not None:
print('-- Contract Details:')
print(self.data0.contractdetails)
header = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume',
'OpenInterest', 'SMA']
print(', '.join(header))
self.done = False
def runstrategy():
args = parse_args()
# Create a cerebro
cerebro = bt.Cerebro()
storekwargs = dict(
token=args.token,
account=args.account,
practice=not args.live
)
if not args.no_store:
store = StoreCls(**storekwargs)
if args.broker:
if args.no_store:
broker = BrokerCls(**storekwargs)
else:
broker = store.getbroker()
cerebro.setbroker(broker)
timeframe = bt.TimeFrame.TFrame(args.timeframe)
# Manage data1 parameters
tf1 = args.timeframe1
tf1 = bt.TimeFrame.TFrame(tf1) if tf1 is not None else timeframe
cp1 = args.compression1
cp1 = cp1 if cp1 is not None else args.compression
if args.resample or args.replay:
datatf = datatf1 = bt.TimeFrame.Ticks
datacomp = datacomp1 = 1
else:
datatf = timeframe
datacomp = args.compression
datatf1 = tf1
datacomp1 = cp1
fromdate = None
if args.fromdate:
dtformat = '%Y-%m-%d' + ('T%H:%M:%S' * ('T' in args.fromdate))
fromdate = datetime.datetime.strptime(args.fromdate, dtformat)
DataFactory = DataCls if args.no_store else store.getdata
datakwargs = dict(
timeframe=datatf, compression=datacomp,
qcheck=args.qcheck,
historical=args.historical,
fromdate=fromdate,
bidask=args.bidask,
useask=args.useask,
backfill_start=not args.no_backfill_start,
backfill=not args.no_backfill,
tz=args.timezone
)
if args.no_store and not args.broker: # neither store nor broker
datakwargs.update(storekwargs) # pass the store args over the data
data0 = DataFactory(dataname=args.data0, **datakwargs)
data1 = None
if args.data1 is not None:
if args.data1 != args.data0:
datakwargs['timeframe'] = datatf1
datakwargs['compression'] = datacomp1
data1 = DataFactory(dataname=args.data1, **datakwargs)
else:
data1 = data0
rekwargs = dict(
timeframe=timeframe, compression=args.compression,
bar2edge=not args.no_bar2edge,
adjbartime=not args.no_adjbartime,
rightedge=not args.no_rightedge,
takelate=not args.no_takelate,
)
if args.replay:
cerebro.replaydata(data0, **rekwargs)
if data1 is not None:
rekwargs['timeframe'] = tf1
rekwargs['compression'] = cp1
cerebro.replaydata(data1, **rekwargs)
elif args.resample:
cerebro.resampledata(data0, **rekwargs)
if data1 is not None:
rekwargs['timeframe'] = tf1
rekwargs['compression'] = cp1
cerebro.resampledata(data1, **rekwargs)
else:
cerebro.adddata(data0)
if data1 is not None:
cerebro.adddata(data1)
if args.valid is None:
valid = None
else:
valid = datetime.timedelta(seconds=args.valid)
# Add the strategy
cerebro.addstrategy(TestStrategy,
smaperiod=args.smaperiod,
trade=args.trade,
exectype=bt.Order.ExecType(args.exectype),
stake=args.stake,
stopafter=args.stopafter,
valid=valid,
cancel=args.cancel,
donotcounter=args.donotcounter,
sell=args.sell,
usebracket=args.usebracket)
# Live data ... avoid long data accumulation by switching to "exactbars"
cerebro.run(exactbars=args.exactbars)
if args.exactbars < 1: # plotting is possible
if args.plot:
pkwargs = dict(style='line')
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='Test Oanda integration')
parser.add_argument('--exactbars', default=1, type=int,
required=False, action='store',
help='exactbars level, use 0/-1/-2 to enable plotting')
parser.add_argument('--stopafter', default=0, type=int,
required=False, action='store',
help='Stop after x lines of LIVE data')
parser.add_argument('--no-store',
required=False, action='store_true',
help='Do not use the store pattern')
parser.add_argument('--debug',
required=False, action='store_true',
help='Display all info received from source')
parser.add_argument('--token', default=None,
required=True, action='store',
help='Access token to use')
parser.add_argument('--account', default=None,
required=True, action='store',
help='Account identifier to use')
parser.add_argument('--live', default=None,
required=False, action='store',
help='Go to live server rather than practice')
parser.add_argument('--qcheck', default=0.5, type=float,
required=False, action='store',
help=('Timeout for periodic '
'notification/resampling/replaying check'))
parser.add_argument('--data0', default=None,
required=True, action='store',
help='data 0 into the system')
parser.add_argument('--data1', default=None,
required=False, action='store',
help='data 1 into the system')
parser.add_argument('--timezone', default=None,
required=False, action='store',
help='timezone to get time output into (pytz names)')
parser.add_argument('--bidask', default=None,
required=False, action='store_true',
help='Use bidask ... if False use midpoint')
parser.add_argument('--useask', default=None,
required=False, action='store_true',
help='Use the "ask" of bidask prices/streaming')
parser.add_argument('--no-backfill_start',
required=False, action='store_true',
help='Disable backfilling at the start')
parser.add_argument('--no-backfill',
required=False, action='store_true',
help='Disable backfilling after a disconnection')
parser.add_argument('--historical',
required=False, action='store_true',
help='do only historical download')
parser.add_argument('--fromdate',
required=False, action='store',
help=('Starting date for historical download '
'with format: YYYY-MM-DD[THH:MM:SS]'))
parser.add_argument('--smaperiod', default=5, type=int,
required=False, action='store',
help='Period to apply to the Simple Moving Average')
pgroup = parser.add_mutually_exclusive_group(required=False)
pgroup.add_argument('--replay',
required=False, action='store_true',
help='replay to chosen timeframe')
pgroup.add_argument('--resample',
required=False, action='store_true',
help='resample to chosen timeframe')
parser.add_argument('--timeframe', default=bt.TimeFrame.Names[1],
choices=bt.TimeFrame.Names,
required=False, action='store',
help='TimeFrame for Resample/Replay')
parser.add_argument('--compression', default=1, type=int,
required=False, action='store',
help='Compression for Resample/Replay')
parser.add_argument('--timeframe1', default=None,
choices=bt.TimeFrame.Names,
required=False, action='store',
help='TimeFrame for Resample/Replay - Data1')
parser.add_argument('--compression1', default=None, type=int,
required=False, action='store',
help='Compression for Resample/Replay - Data1')
parser.add_argument('--no-takelate',
required=False, action='store_true',
help=('resample/replay, do not accept late samples'))
parser.add_argument('--no-bar2edge',
required=False, action='store_true',
help='no bar2edge for resample/replay')
parser.add_argument('--no-adjbartime',
required=False, action='store_true',
help='no adjbartime for resample/replay')
parser.add_argument('--no-rightedge',
required=False, action='store_true',
help='no rightedge for resample/replay')
parser.add_argument('--broker',
required=False, action='store_true',
help='Use Oanda as broker')
parser.add_argument('--trade',
required=False, action='store_true',
help='Do Sample Buy/Sell operations')
parser.add_argument('--sell',
required=False, action='store_true',
help='Start by selling')
parser.add_argument('--usebracket',
required=False, action='store_true',
help='Test buy_bracket')
parser.add_argument('--donotcounter',
required=False, action='store_true',
help='Do not counter the 1st operation')
parser.add_argument('--exectype', default=bt.Order.ExecTypes[0],
choices=bt.Order.ExecTypes,
required=False, action='store',
help='Execution to Use when opening position')
parser.add_argument('--stake', default=10, type=int,
required=False, action='store',
help='Stake to use in buy operations')
parser.add_argument('--valid', default=None, type=float,
required=False, action='store',
help='Seconds to keep the order alive (0 means DAY)')
parser.add_argument('--cancel', default=0, type=int,
required=False, action='store',
help=('Cancel a buy order after n bars in operation,'
' to be combined with orders like Limit'))
# 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 (escape the quotes if needed):\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__':
runstrategy()
```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.