Python contains a lot of native and 3rd party modules that can save us a lot of time and effort when developing a strategy. If you are unfamiliar with Python modules, a module is a just another python script that contains useful functions and classes that have already been coded for you. Argparse is a good example of this. It is a module which allows us to specify command line arguments when running our scripts that can be used to change some parameters or logic in our code.
What is a command-line argument?
Since this tutorial is aimed at the beginner, it is worth briefly discussing what command line arguments actually are. When we run a python script, we can run it directly from an IDE (Integrated development environment, such as IDLE, the native development environment bundled with Python) or we can run the script from the command line. A command line argument is a program option that is written following the command/program name. For example:
1 2 3 |
cd /home/ ls -l python3 myStrategy.py |
The commands above are all simple examples of calling a program and providing it a single argument.
Command | Argument |
---|---|
cd | /home/ |
ls | -l |
python3 | myStrategy.py |
Python’s argparse module allows you to provide arguments to your script in the same way we can provide arguments to other system programs.
Argparse
Before we begin, Python has some great documentation on argparse with simple usage examples. The official “how to” docs can be found here:
https://docs.python.org/3/howto/argparse.html
My intention with this post is to try and add some value to the vast amount of tutorials in the wild by linking it to backtesting and showing some practical examples of how we can use it in our Backtrader scripts.
Common Use Cases
Argparse for me becomes useful after I have generated the “bare bones” of my strategy logic and I am happy with the initial backtest results. I then take a look at my code and decide which parameters it would be useful to change at run time. The same applies to the logic in the code. The reason I personally choose to do a bit later in the process is because I often find that after implementing a simple ‘proof of concept’ / bare bones script, the concept failed to be proved! So with that in mind, I try not to spend too much time on the polishing the table before it has legs.
Some good candidates for run-time options include:
- Instrument ticker / symbol
- Backtest date ranges
- Strategy parameters (e.g, risk, lookback period or whatever you have defined)
- Operation mode: E.g Backtest or Live Trading
Any parameter or logical decision “could” be a candidate for a run time option. However, I would caution against doing this for every parameter in the script at first. It will add a lot of lines to your code, increase complexity and is prone to fat finger errors.
The Code
For this tutorial, I will adapt the code written in the Backtrader: First Script post. The simple nature of the code will allow us to focus on playing with argparse module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
''' Author: www.backtest-rookies.com MIT License Copyright (c) 2017 backtest-rookies.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' ''' Pairs trade notes: When the correlation between two stocks weakens, enter a short on the outperforming stock and go long on the weaker stock. Alternatively measure convergence / divergence. ''' import backtrader as bt from datetime import datetime import argparse def parse_args(): parser = argparse.ArgumentParser(description='Argparse Example program\ for backtesting') parser.add_argument('instrument', help='The yahoo ticker of the instrument being tested') parser.add_argument('start', help='Starting date in YYYY-MM-DD format') parser.add_argument('end', help='Ending date in YYYY-MM-DD format') parser.add_argument('-p', '--period', default=21, type=int, help='Select the RSI lookback period') parser.add_argument('-r', '--rsi', choices=['SMA', 'EMA'], help='Select wether a simple moving average or exponential\ moving average is used in the RSI calcuation') return parser.parse_args() class firstStrategy(bt.Strategy): params = ( ('rsi', 'SMA'), ('period', 21), ) def __init__(self): if self.p.rsi == 'EMA': self.rsi = bt.indicators.RSI_EMA(self.data.close, period=self.p.period) else: self.rsi = bt.indicators.RSI_SMA(self.data.close, period=self.p.period) def next(self): if not self.position: if self.rsi < 30: self.buy(size=100) else: if self.rsi > 70: self.sell(size=100) #Get Args args = parse_args() #Convert date args to datetime objects fd = datetime.strptime(args.start, '%Y-%m-%d') td = datetime.strptime(args.end, '%Y-%m-%d') #Variable for our starting cash startcash = 10000 #Create an instance of cerebro cerebro = bt.Cerebro() #Add our strategy cerebro.addstrategy(firstStrategy, rsi=args.rsi, period=args.period) #Get Apple data from Yahoo Finance. data = bt.feeds.YahooFinanceData( dataname=args.instrument, fromdate = fd, todate = td, buffered= True ) #Add the data to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(startcash) # Run over everything cerebro.run() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('Final Portfolio Value: ${}'.format(portvalue)) print('P/L: ${}'.format(pnl)) #Finally plot the end results cerebro.plot(style='candlestick') |
Code Commentary
One of the great things about the argparse module is that it does more than just let you enter program parameters and options. It also automatically generates some simple user documentation and help prompts. Once setup, you can run your program with the –help option and argparse will display a help file based off the arguments you have added to the code.
1 |
python3 argparse-tutorial.py --help |
Running the above command will output:
If you have used shell programs in Linux or Mac OS, the output will look very familiar to you. In addition to this, if you enter an incorrect option or miss a required parameter, argparse will tell the you where the issue lies.
Now lets dig into the code. I choose to setup my script arguments inside the dedicated function parse_args(). Apart from being nicely compartmentalized, there are some good reasons for doing this such as being able to us the same script as a module without attempting to parse non-existent arguments. However that is a little out of scope of this post.
Digging into parse_args()
This function contains everything you need to setup and return arguments from the command-line. I have tried to provide a few different types of argument that are useful. As a result, I hope it is easy to copy and adapt to your own scripts. The parse_args() functions contains the following examples:
- Positional argument examples
- Optional Argument examples
- Examples which return strings (this is the default argument type)
- An example which returns a number using the type keyword
- An example that provides choices
Each argument added in parse_args() has a help keyword. It is from this, that the help text is automatically generated. If you never plan to have someone else use your script, you might decide not to include it. My personal preference is to always write a short help string describing the parameter. It just comes in useful for when you come back to a program after a few months and forgotten what each parameter does. There is no need to return the code in order to figure out how to run it!
Positional vs Optional Arguments
Positional arguments are ones which you must write in a fixed order following the command and do not use a flag/switch/keyword (.eg. “-p“, “–period“). Any argument that you provide a flag or keyword for will be classed as an optional argument. This can be slightly confusing as even if you set an argument’s keyword parameter “required=True“, it will still be classed as an optional argument in the help text.
Argument application in backtesting
So now we get to the bit which matters. How do we make use of these options in a relevant way? I imagine from just reading over each argument’s help strings, you will already have a clear picture of how we can use arguments in a meaningful way. Using arguments to apply the same strategy in different markets is a classic case. In backtesting, changing the date range we are focusing on is another. Some more advanced use cases might include turning logging on/off, putting the script into optimization mode (Optimize Strategies in Backtrader) or switching between live trading, practice accounts and backtesting modes.
Accessing arguments
Once we have called parse_args() in the script we can easily access an argument value through the returned args object. Calling args.start returns the positional argument for the start time. Similarly, calling args.period returns the optional period parameter.
Getting the right data types
By default, argparse returns strings unless we specify otherwise. If we need something other than a string, we should specify what we want using the type keyword. For example, this allows us to specify that a particular argument should be an integer. We do this in the code for the period argument.
1 2 3 4 |
parser.add_argument('-p', '--period', default=21, type=int, help='Select the RSI lookback period') |
As a bonus, argparse will also check the argument provided is the correct type and raise an error if it is not. This saves us from having to code a check in the script. As an example, if we provide a string to the –period argument, argparse will raise the following error:
1 |
argparse-tutorial.py: error: argument -p/--period: invalid int value: 'abc' |
Next, we have something more interesting. Unfortunately, not all ‘types’ are supported. This is especially true of types that are part of another module. A datetime object is a good example of this. The Yahoo data feed in Backtrader requires datetime objects to specify the start and end date of a backtest. As a result, we must handle the conversion in the script.
1 2 |
fd = datetime.strptime(args.start, '%Y-%m-%d') td = datetime.strptime(args.end, '%Y-%m-%d') |
In datetime’s strptime() function, the string ‘%Y-%m-%d‘ is defining the expected format of the string. This string means that strptime is expecting a YYYY-MM-DD format string to be provided to it. If the string is not in this format, a ValueError will be raised when you try to run the program.
1 |
ValueError: time data '2017-fwa-a1' does not match format '%Y-%m-%d' |
For more information regarding the various formatting options see:
https://docs.python.org/3.5/library/datetime.html#strftime-and-strptime-behavior
The last line that I think is worth commenting on is:
1 2 |
#Add our strategy cerebro.addstrategy(firstStrategy, rsi=args.rsi, period=args.period) |
You may wonder why I went to the effort of extending the Backtrader: First Script example to include strategy params and then load the arguments into cerebro when the strategy is initialized. After all I could have just written:
1 2 3 4 5 6 7 |
class firstStrategy(bt.Strategy): def __init__(self): if args.rsi == 'EMA': self.rsi = bt.indicators.RSI_EMA(self.data.close, period=args.period) else: self.rsi = bt.indicators.RSI_SMA(self.data.close, period=args.period) |
The reasoning is portability. I can now move this strategy class to any script without having to bring the baggage of my arguments with me. I may not want to have the same arguments in another script.
Final Note
During testing, I noticed that if you have a combination of setting the RSI to use a simple moving average with a period of 7, a ZeroDivisionError is raised. I am not convinced there is an issue with the code because longer periods work and a period of 7 also works with the exponential moving average. However if I do find an issue with the code, I will update this article accordingly.
Admittedly I’m a complete novice in python, and you could argue I should go way back and learn the basics before attempting this….
But I just cant get from one step to the next. If I copy the above script for the argparse and save it down, it does not allow me to call that script running the above command.
If I type in “python3 argparse-tutorial.py –help” and run it I get the following error:
———————————————————————————————-
C:\Users\xxxx\PycharmProjects\untitled\venv\Scripts\python.exe C:/Users/xxxx/PycharmProjects/untitled/venv/Lib/site-packages/backtrader/e.py
File “C:/Users/xxxx/PycharmProjects/untitled/venv/Lib/site-packages/backtrader/e.py”, line 1
python argparse-tutorial.py –help
^
SyntaxError: invalid syntax
Process finished with exit code 1
——————————————————————————————————-
I can’t understand what’s going wrong, it seems like there is a step missing in this tutorial, and the one for the Cyrpto data, which is what you do with the argparse script, and how you run another script querying it?
Sorry for the dumb question.
H
Thanks Henry…
For anyone else reading this and having a similar issue, Henry discovered the reason for the error and posted the answer here:
https://backtest-rookies.com/2018/03/08/download-cryptocurrency-data-with-ccxt/#comment-616