├── .gitignore ├── LICENSE ├── README.md ├── backtester ├── broker.py ├── csv │ ├── AAPL.csv │ ├── BRK-B.csv │ ├── CVX.csv │ └── KO.csv ├── data.py ├── events.py ├── loop.py ├── performance.py ├── portfolio.py └── strategy.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Douglas Denhartog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #quantstart-backtester 2 | 3 | This repository is a **hand-written (no copying and pasting here, noob!)**, slightly modified code, of an ~~eight~~seven-part* article series written by [Michael Halls-Moore](http://www.quantstart.com/about-mike/), "the guy behind QuantStart.com": 4 | 5 | 1. [Event-Driven Backtesting with Python - Part I](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-I) 6 | 2. [Event-Driven Backtesting with Python - Part II](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-II) 7 | 3. [Event-Driven Backtesting with Python - Part III](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-III) 8 | 4. [Event-Driven Backtesting with Python - Part IV](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-IV) 9 | 5. [Event-Driven Backtesting with Python - Part V](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-V) 10 | 6. [Event-Driven Backtesting with Python - Part VI](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-VI) 11 | 7. [Event-Driven Backtesting with Python - Part VII](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-VII) 12 | 8. [Event-Driven Backtesting with Python - Part VIII](http://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-VIII) 13 | 14 | ###Purpose 15 | I wanted to put together the code from the articles to better understand the event-based part of the code. The basic logic of `loop.py` is: 16 | 17 | 1. `update_data()` puts a `MarketEvent()` into the queue 18 | 2. `calculate_signals()` processes the `MarketEvent()` and puts a `SignalEvent()` into the queue 19 | 3. `update_signal()` processes an `OrderEvent()` 20 | 4. `execute_order()` puts a `FillEvent()` into the queue 21 | 5. `update_fill()` emits NO event so queue is Empty which breaks inner While loop 22 | 6. return to outer While loop 23 | 7. continue looping until `data.continue_backtest == False`, at which time loop will end after next `MarketEvent()` 24 | 25 | ###Notes 26 | 1. I say ~~eight~~seven-part article series because Part VIII is specifically for corresponding with Interactive Broker's API, which is beyond the scope of the academic exercise this repository represents. 27 | 2. It is important to note the aggregate of the code **as-is** from the series **does not work**. The code has missing logic and minor variable naming mismatches. 28 | 3. I tried to keep my variable/method naming conventions **very** similar to their originals so one can more easily compare my code to the articles 29 | 4. I took the liberty to make many minor logic changes (e.g. QuantStart's `Portfolio()` has `dict((k,v) for k, v in [(s, 0) for s in self.symbol_list]))` and I have `{symbol: 0 for symbol in self.symbol_list}` 30 | 5. The QuantStart code is full of opportunities for DRY and encapsulation improvements, some of which I made and plenty more I left alone 31 | -------------------------------------------------------------------------------- /backtester/broker.py: -------------------------------------------------------------------------------- 1 | #PYTHON 2 | from abc import ( 3 | ABCMeta, 4 | abstractmethod 5 | ) 6 | from datetime import date 7 | 8 | #PROJECT 9 | from .events import ( 10 | OrderEvent, 11 | FillEvent 12 | ) 13 | 14 | class BrokerMetaclass(metaclass=ABCMeta): 15 | @abstractmethod 16 | def execute_order(self, event): 17 | raise NotImplementedError 18 | 19 | 20 | class BacktestBroker(BrokerMetaclass): 21 | def __init__(self, event_queue): 22 | self.event_queue = event_queue 23 | 24 | def execute_order(self, event): 25 | """Converts OrderEvents into FillEvents""" 26 | if isinstance(event, OrderEvent): 27 | signal = FillEvent( 28 | event.symbol, 29 | date.today(), 30 | event.quantity, 31 | event.direction, 32 | None 33 | ) 34 | self.event_queue.put(signal) 35 | -------------------------------------------------------------------------------- /backtester/csv/AAPL.csv: -------------------------------------------------------------------------------- 1 | date,open,high,low,close,volume,adj_close 1/16/15,107.03,107.58,105.2,105.99,77052500,105.99 1/15/15,110,110.06,106.66,106.82,60014000,106.82 1/14/15,109.04,110.49,108.5,109.8,48956600,109.8 1/13/15,111.43,112.8,108.91,110.22,67091900,110.22 1/12/15,112.6,112.63,108.8,109.25,49650800,109.25 1/9/15,112.67,113.25,110.21,112.01,53699500,112.01 1/8/15,109.23,112.15,108.7,111.89,59364500,111.89 1/7/15,107.2,108.2,106.7,107.75,40105900,107.75 1/6/15,106.54,107.43,104.63,106.26,65797100,106.26 1/5/15,108.29,108.65,105.41,106.25,64285500,106.25 1/2/15,111.39,111.44,107.35,109.33,53204600,109.33 12/31/14,112.82,113.13,110.21,110.38,41403400,110.38 12/30/14,113.64,113.92,112.11,112.52,29881500,112.52 12/29/14,113.79,114.77,113.7,113.91,27598900,113.91 12/26/14,112.1,114.52,112.01,113.99,33721000,113.99 12/24/14,112.58,112.71,112.01,112.01,14479600,112.01 12/23/14,113.23,113.33,112.46,112.54,26028400,112.54 12/22/14,112.16,113.49,111.97,112.94,45167500,112.94 12/19/14,112.26,113.24,111.66,111.78,88429800,111.78 12/18/14,111.87,112.65,110.66,112.65,59006200,112.65 12/17/14,107.12,109.84,106.82,109.41,53411800,109.41 12/16/14,106.37,110.16,106.26,106.75,60790700,106.75 12/15/14,110.7,111.6,106.35,108.23,67218100,108.23 12/12/14,110.46,111.87,109.58,109.73,56028100,109.73 12/11/14,112.26,113.8,111.34,111.62,41401700,111.62 12/10/14,114.41,114.85,111.54,111.95,44565300,111.95 12/9/14,110.19,114.3,109.35,114.12,60208000,114.12 12/8/14,114.1,114.65,111.62,112.4,57664900,112.4 12/5/14,115.99,116.08,114.64,115,38318900,115 12/4/14,115.77,117.2,115.29,115.49,42044500,115.49 12/3/14,115.75,116.35,115.11,115.93,43063400,115.93 12/2/14,113.5,115.75,112.75,114.63,59075100,114.63 12/1/14,118.81,119.25,111.27,115.07,83814000,115.07 -------------------------------------------------------------------------------- /backtester/csv/BRK-B.csv: -------------------------------------------------------------------------------- 1 | date,open,high,low,close,volume,adj_close 1/16/15,147.2,149.26,146.56,149.21,4303100,149.21 1/15/15,148.53,148.53,146.8,147.58,2892200,147.58 1/14/15,147.27,148.37,146.42,147.82,4802000,147.82 1/13/15,149.95,150.99,147.77,148.63,3899600,148.63 1/12/15,149.96,150.12,147.95,148.28,5887200,148.28 1/9/15,151.65,151.69,149.26,149.47,3358900,149.47 1/8/15,150.6,151.37,150.51,151.37,4282100,151.37 1/7/15,147.94,149.14,147.65,148.88,4156400,148.88 1/6/15,147.64,148.53,146.11,146.84,4116100,146.84 1/5/15,148.81,149,146.78,147,4168800,147 1/2/15,151.5,151.6,148.5,149.17,3436400,149.17 12/31/14,152.2,152.67,150.15,150.15,3182200,150.15 12/30/14,151.35,152.22,151.19,152.09,2001300,152.09 12/29/14,151.02,152.26,151.02,152.05,2677100,152.05 12/26/14,151.66,152.13,151.25,151.35,1889300,151.35 12/24/14,152.43,152.44,151.52,151.54,1612200,151.54 12/23/14,152.19,152.7,151.68,151.75,3472800,151.75 12/22/14,151.98,152.55,151.09,151.9,5919500,151.9 12/19/14,152.33,152.66,150.83,151.56,9337600,151.56 12/18/14,151.16,152.74,150.15,152.67,7839000,152.67 12/17/14,145.76,148.63,145.53,148.45,4838800,148.45 12/16/14,145.37,148.73,145,145.28,5009300,145.28 12/15/14,147.97,148.42,144.75,146.12,4727900,146.12 12/12/14,149.31,149.89,146.3,146.46,5198200,146.46 12/11/14,149.79,151.27,149.5,150.13,3626600,150.13 12/10/14,150.36,151.26,148.82,149.08,5559000,149.08 12/9/14,151.45,151.45,148.91,150.46,4550600,150.46 12/8/14,150.88,152.94,150.65,151.94,5263600,151.94 12/5/14,150.38,150.68,150.1,150.68,2777300,150.68 12/4/14,149.96,150.15,149.23,150.05,2418500,150.05 12/3/14,150.15,150.49,149.51,149.73,3004100,149.73 12/2/14,149.28,150,148.66,149.87,3179700,149.87 12/1/14,148.61,149.94,148.18,148.52,3317000,148.52 -------------------------------------------------------------------------------- /backtester/csv/CVX.csv: -------------------------------------------------------------------------------- 1 | date,open,high,low,close,volume,adj_close 1/16/15,102.47,105.14,102.47,105.12,12557800,105.12 1/15/15,103.9,104.94,102.53,102.67,9664600,102.67 1/14/15,103.2,104.41,101.83,103.9,14116900,103.9 1/13/15,106.92,107.66,103.19,104.2,13277700,104.2 1/12/15,107.47,107.47,105.56,105.88,8912300,105.88 1/9/15,110.14,110.22,107.67,108.21,9584500,108.21 1/8/15,109.19,110.44,108.6,110.41,8650800,110.41 1/7/15,109.25,109.73,107.51,107.94,10353800,107.94 1/6/15,107.87,109.02,106.48,108.03,11591600,108.03 1/5/15,110.96,111.2,107.44,108.08,11758100,108.08 1/2/15,111.63,113,110.85,112.58,5898800,112.58 12/31/14,111.65,113.31,111.53,112.18,6411800,112.18 12/30/14,112.93,113.65,112.15,113.11,5659500,113.11 12/29/14,113.44,114.38,112.78,113.32,6043000,113.32 12/26/14,113.93,114.35,112.82,113.25,4380400,113.25 12/24/14,113.66,114.14,112.07,113.47,4536500,113.47 12/23/14,112.76,114.45,112.32,113.95,8092000,113.95 12/22/14,112.36,112.99,111.07,112.03,9433500,112.03 12/19/14,109.53,112.96,108.5,112.93,15723300,112.93 12/18/14,108.01,109.03,105.49,109.03,13533000,109.03 12/17/14,102.18,106.59,102.01,106.02,13354000,106.02 12/16/14,100.5,104.47,100.15,101.7,12722200,101.7 12/15/14,103.13,103.91,100.42,100.86,13144200,100.86 12/12/14,103.76,104.35,102.37,102.38,12013600,102.38 12/11/14,104.97,107.27,104.3,104.91,10293600,104.91 12/10/14,106.22,106.25,103.07,104.86,15073400,104.86 12/9/14,106.17,108.15,106.13,107.01,11053000,107.01 12/8/14,109.89,109.94,106.41,106.8,13809000,106.8 12/5/14,111.79,112.29,110.71,110.87,7663400,110.87 12/4/14,113.08,113.08,111.01,112.28,8100700,112.28 12/3/14,114.74,114.82,113.15,113.71,8049800,113.71 12/2/14,111.27,114.56,110.89,114.02,8513500,114.02 12/1/14,109.38,112.49,108.68,111.73,13400100,111.73 -------------------------------------------------------------------------------- /backtester/csv/KO.csv: -------------------------------------------------------------------------------- 1 | date,open,high,low,close,volume,adj_close 1/16/15,42.36,42.59,42.24,42.53,15075700,42.53 1/15/15,42.56,42.86,42.17,42.38,11193100,42.38 1/14/15,42.08,42.6,42.07,42.56,13447600,42.56 1/13/15,42.83,43.24,42.45,42.63,12529500,42.63 1/12/15,43.07,43.2,42.46,42.64,11415800,42.64 1/9/15,43.47,43.56,42.95,43.03,12733500,43.03 1/8/15,43.18,43.57,43.1,43.51,21743600,43.51 1/7/15,42.8,43.11,42.58,42.99,13412300,42.99 1/6/15,42.41,42.94,42.24,42.46,16897500,42.46 1/5/15,42.69,42.97,42.08,42.14,26292600,42.14 1/2/15,42.26,42.4,41.8,42.14,9921100,42.14 12/31/14,42.92,42.94,42.22,42.22,9369500,42.22 12/30/14,42.74,42.99,42.65,42.76,9222000,42.76 12/29/14,42.8,43.06,42.49,42.86,8694500,42.86 12/26/14,42.97,43.3,42.93,42.96,6466900,42.96 12/24/14,43.1,43.23,42.92,42.94,6405900,42.94 12/23/14,42.54,43.14,42.47,42.97,13411300,42.97 12/22/14,42.14,42.44,42.09,42.35,11190900,42.35 12/19/14,42.44,42.79,41.89,41.95,24009500,41.95 12/18/14,41.86,42.39,41.75,42.39,18030700,42.39 12/17/14,40.44,41.76,40.38,41.55,20223900,41.55 12/16/14,40.2,41.31,39.8,40.39,23475400,40.39 12/15/14,41.13,41.18,40.56,40.57,23378500,40.57 12/12/14,41.39,41.61,40.87,40.91,18432400,40.91 12/11/14,41.62,42.01,41.5,41.53,16201500,41.53 12/10/14,42.04,42.24,41.56,41.6,18324100,41.6 12/9/14,42.15,42.53,41.67,42.04,25714600,42.04 12/8/14,43.51,43.63,43.09,43.14,12802300,43.14 12/5/14,43.5,43.61,43.2,43.53,12536900,43.53 12/4/14,43.62,43.84,43.37,43.5,13779700,43.5 12/3/14,44.42,44.44,43.76,43.8,15246500,43.8 12/2/14,44.37,44.67,44.23,44.54,9862400,44.54 12/1/14,44.18,44.77,44.13,44.55,10065700,44.55 -------------------------------------------------------------------------------- /backtester/data.py: -------------------------------------------------------------------------------- 1 | #PYTHON 2 | from abc import ( 3 | ABCMeta, 4 | abstractmethod 5 | ) 6 | from datetime import datetime 7 | from os.path import ( 8 | dirname, 9 | join, 10 | realpath 11 | ) 12 | 13 | #PACKAGES 14 | import pandas 15 | 16 | #PROJECT 17 | from events import MarketEvent 18 | 19 | 20 | class DataMetaclass(metaclass=ABCMeta): 21 | @abstractmethod 22 | def get_latest_data( 23 | self, 24 | symbol, 25 | quantity 26 | ): 27 | raise NotImplementedError 28 | 29 | @abstractmethod 30 | def update_data(self): 31 | raise NotImplementedError 32 | 33 | 34 | class HistoricCSVDataHandler(DataMetaclass): 35 | def __init__( 36 | self, 37 | event_queue, 38 | symbol_list, 39 | ): 40 | self.event_queue = event_queue 41 | self.symbol_list = symbol_list 42 | 43 | self.symbol_data = {} 44 | self.latest_symbol_data = {} 45 | self.continue_backtest = True 46 | 47 | self.file_type = 'csv' 48 | self.folder_name = join( 49 | dirname(realpath(__file__)), 50 | self.file_type 51 | ) 52 | 53 | self.initial_symbol_data() 54 | 55 | def initial_symbol_data(self): 56 | combined_index = None 57 | for symbol in self.symbol_list: 58 | #load data into pandas.DataFrame 59 | #for EACH symbol 60 | self.symbol_data[symbol] = pandas.io.parsers.read_csv( 61 | join( 62 | self.folder_name, 63 | '{file_name}.{file_extension}'.format( 64 | file_name=symbol, 65 | file_extension=self.file_type 66 | ) 67 | ), 68 | header=0, 69 | index_col=0, 70 | names=[ 71 | 'datestamp', 72 | 'open', 73 | 'high', 74 | 'low', 75 | 'close', 76 | 'volume', 77 | 'adj_close' 78 | ] 79 | ) 80 | 81 | #unionize index 82 | if combined_index is None: 83 | combined_index = self.symbol_data[s].index 84 | else: 85 | combined_index.union(self.symbol_data[symbol].index) 86 | 87 | for symbol in self.symbol_list: 88 | #iterrows() creates a generator 89 | self.symbol_data[symbol] = self.symbol_data[symbol].reindex( 90 | reindex=combined_index, 91 | method='pad' 92 | ).iterrows() 93 | 94 | def new_data_generator( 95 | self, 96 | symbol 97 | ): 98 | for row in self.symbol_list[symbol]: 99 | yield tuple([ 100 | symbol, 101 | datetime.strptime( 102 | row['datestamp'], 103 | '%m/%d/%y' 104 | ).date(), 105 | row['open'], 106 | row['high'], 107 | row['low'], 108 | row['volume'], 109 | row['adj_close'] 110 | ]) 111 | 112 | def get_latest_data( 113 | self, 114 | symbol, 115 | quantity 116 | ): 117 | try: 118 | return self.latest_symbol_data[symbol][-quantity:] 119 | except KeyError: 120 | print('{symbol} is not a valid symbol'.format(symbol=symbol)) 121 | 122 | def update_latest_data(self): 123 | for symbol in self.symbol_list: 124 | try: 125 | data = self.new_data_generator(symbol).next() 126 | except: 127 | self.continue_backtest = False 128 | 129 | if data is not None and len(data) > 0: 130 | self.latest_symbol_data[symbol].append(data) 131 | 132 | self.event_queue.put(MarketEvent()) 133 | -------------------------------------------------------------------------------- /backtester/events.py: -------------------------------------------------------------------------------- 1 | class AbstractEvent: 2 | pass 3 | 4 | 5 | class MarketEvent(AbstractEvent): 6 | pass 7 | 8 | 9 | class SignalEvent(AbstractEvent): 10 | def __init__( 11 | self, 12 | symbol, 13 | datestamp, 14 | signal_type 15 | ): 16 | """Initializes a SignalEvent 17 | 18 | signal_type == 'LONG','SHORT' 19 | """ 20 | self.symbol = symbol 21 | self.datestamp = datestamp 22 | self.signal_type = signal_type 23 | 24 | 25 | class OrderEvent(AbstractEvent): 26 | def __init__( 27 | self, 28 | symbol, 29 | order_type, 30 | quantity, 31 | direction 32 | ): 33 | """Initializes an OrderEvent 34 | 35 | order_type == 'MARKET','LIMIT','STOP','STOPLIMT' 36 | direction == 'BUY','SELL' 37 | """ 38 | self.symbol = symbol 39 | self.order_type = order_type 40 | self.quantity = quantity 41 | self.direction = direction 42 | 43 | def __repr__(self): 44 | return '{cls}({d})'.format( 45 | cls=self.__class__, 46 | d=self.__dict__ 47 | ) 48 | 49 | 50 | class FillEvent(AbstractEvent): 51 | def __init__( 52 | self, 53 | symbol, 54 | datestamp, 55 | quantity, 56 | direction, 57 | fill_cost, 58 | commission=None 59 | ): 60 | """Initializes a FillEvent 61 | 62 | direction == 'BUY','SELL' 63 | commission: is $ per share 64 | """ 65 | self.symbol = symbol 66 | self.datestamp = datestamp 67 | self.quantity = quantity 68 | self.direction = direction 69 | self.fill_cost = fill_cost 70 | self.commission = commission 71 | -------------------------------------------------------------------------------- /backtester/loop.py: -------------------------------------------------------------------------------- 1 | #PYTHON 2 | import Queue 3 | import time 4 | 5 | #PROJECT 6 | from .events import ( 7 | MarketEvent, 8 | SignalEvent, 9 | OrderEvent, 10 | FillEvent 11 | ) 12 | from .data import HistoricCSVDataHandler 13 | from .strategy import BuyAndHoldStrategy 14 | from .portfolio import Portfolio 15 | from .broker import ExecutionHandler 16 | 17 | #MODULE 18 | event_queue = Queue.Queue() 19 | data = HistoricCSVDataHandler() 20 | strategy = BuyAndHoldStrategy() 21 | portfolio = Portfolio() 22 | broker = ExecutionHandler() 23 | 24 | while True: 25 | if data.continue_backtest is True: 26 | data.update_latest_data() 27 | else: 28 | break 29 | 30 | while True: 31 | try: 32 | event = event_queue.get(block=False) 33 | except Queue.Empty: 34 | break 35 | 36 | if event is not None: 37 | if isinstance(event, MarketEvent): 38 | strategy.calculate_signals(event) 39 | portfolio.update_timeindex(event) 40 | elif isinstance(event, SignalEvent): 41 | portfolio.update_signal(event) 42 | elif isinstance(event, OrderEvent): 43 | broker.execute_order(event) 44 | elif isinstance(event, FillEvent): 45 | portfolio.update_fill(event) 46 | 47 | time.sleep(10*60) 48 | -------------------------------------------------------------------------------- /backtester/performance.py: -------------------------------------------------------------------------------- 1 | #PACKAGES 2 | import numpy 3 | import pandas 4 | 5 | 6 | def create_sharpe_ratio( 7 | returns, 8 | periods=252 9 | ): 10 | return ( 11 | numpy.sqrt(periods) * 12 | numpy.mean(returns) / 13 | numpy.std(returns) 14 | ) 15 | 16 | 17 | def create_drawdowns(equity_curve): 18 | high_watermark = [0] 19 | curve_index = equity_curve.index 20 | drawdown = pandas.Series(index=curve_index) 21 | duration = pandas.Series(index=curve_index) 22 | 23 | for i in range(1, len(curve_index)): 24 | current_high_watermark = max( 25 | high_watermark[i-1], 26 | equity_curve[i] 27 | ) 28 | high_watermark.append(current_high_watermark) 29 | 30 | drawdown[i] = high_watermark[i] - equity_curve[i] 31 | duration[i] = 0 if drawdown[i] == 0 else duration[i-1] + 1 32 | 33 | return drawdown.max(), duration.max() 34 | -------------------------------------------------------------------------------- /backtester/portfolio.py: -------------------------------------------------------------------------------- 1 | #PYTHON 2 | from abc import ( 3 | ABCMeta, 4 | abstractmethod 5 | ) 6 | from math import copysign 7 | 8 | #PROJECT 9 | from .events import ( 10 | SignalEvent, 11 | OrderEvent 12 | ) 13 | from .performance import ( 14 | create_sharpe_ratio, 15 | create_drawdowns 16 | ) 17 | 18 | 19 | class PortfolioMetaclass(metaclass=ABCMeta): 20 | @abstractmethod 21 | def update_signal(self, event): 22 | raise NotImplementedError 23 | 24 | @abstractmethod 25 | def update_fill(self, event): 26 | raise NotImplementedError 27 | 28 | 29 | class BacktestPortfolio(PortfolioMetaclass): 30 | def __init__( 31 | self, 32 | event_queue, 33 | data, 34 | start_date, 35 | initial_capital 36 | ): 37 | self.event_queue = event_queue 38 | self.data = data 39 | self.start_date = start_date 40 | self.initial_capital = initial_capital 41 | self.equity_curve = None 42 | 43 | self.symbol_list = self.data.symbol_list 44 | 45 | self.all_positions = self.calculate_all_positions() 46 | self.current_positions = {symbol: 0 for symbol in self.symbol_list} 47 | 48 | self.all_holdings = self.calculate_all_holdings() 49 | self.current_holdings = self.calculate_current_holdings() 50 | 51 | def calculate_all_positions(self): 52 | positions = {symbol: 0 for symbol in self.symbol_list} 53 | positions['datestamp'] = self.start_date 54 | return positions 55 | 56 | def calculate_all_holdings(self): 57 | holdings = {symbol: 0 for symbol in self.symbol_list} 58 | holdings['datestamp'] = self.start_date 59 | holdings['cash'] = self.initial_capital 60 | holdings['commission'] = 0 61 | holdings['total'] = self.initial_capital 62 | return holdings 63 | 64 | def calculate_current_holdings(self): 65 | holdings = {symbol: 0 for symbol in self.symbol_list} 66 | holdings['cash'] = self.initial_capital 67 | holdings['commission'] = 0 68 | holdings['total'] = self.initial_capital 69 | return holdings 70 | 71 | def update_timeindex(self, event): 72 | data = { 73 | symbol: self.data.get_latest(data, symbol) 74 | for symbol in self.symbol_list 75 | } 76 | datestamp = data[self.symbol_list[0]][0][1] 77 | 78 | #positions 79 | positions = { 80 | symbol: self.current_positions[symbol] 81 | for symbol in self.symbol_list 82 | } 83 | positions['datestamp'] = datestamp 84 | self.all_positions.append(positions) 85 | 86 | #holdings 87 | holdings = {symbol: 0 for symbol in self.symbol_list} 88 | holdings['datestamp'] = datestamp 89 | holdings['cash'] = self.current_holdings['cash'] 90 | holdings['commission'] = self.current_holdings['commission'] 91 | holdings['total'] = self.current_holdings['cash'] 92 | 93 | for symbol in self.symbol_list: 94 | market_value = self.current_positions[symbol] + data[symbol][0][5] 95 | holdings[symbol] = market_value 96 | holdings['total'] += market_value 97 | 98 | self.all_holdings.append(holdings) 99 | 100 | def update_positions_post_fill(self, event): 101 | direction = 1 if event.direction == 'BUY' else -1 102 | self.current_positions[event.symbol] += copysign(event.quantity, direction) 103 | 104 | def update_holdings_post_fill(self, event): 105 | direction = 1 if event.direction == 'BUY' else -1 106 | fill_cost = self.data.get_latest_data(event.symbol)[0][5] 107 | cost = copysign(fill_cost, direction) * event.quantity 108 | self.current_holdings[event.symbol] += cost 109 | self.current_holdings['cash'] -= cost + event.commission 110 | self.current_holdings['total'] -= cost + event.commission 111 | 112 | def update_fill(self, event): 113 | if isinstance(event, FillEvent): 114 | self.update_positions_post_fill(event) 115 | self.update_holdings_post_fill(event) 116 | 117 | def create_order_event(self, event): 118 | if isinstance(event, SignalEvent) 119 | direction = 'BUY' if event.signal_type == 'LONG' else 'SELL' 120 | return OrderEvent( 121 | event.symbol, 122 | 'MARKET', 123 | 100, 124 | direction 125 | ) 126 | 127 | def update_signal(self, event): 128 | if isinstance(event, SignalEvent): 129 | self.event_queue.put(self.create_order_event(event)) 130 | 131 | def calculate_equity_curve_dataframe(self): 132 | curve = pandas.DataFrame(self.all_holdings) 133 | curve.set_index('datestamp', inplace=True) 134 | curve['returns'] = curve['total'].pct_change() 135 | curve['equity_curve'] = (1 + curve['returns']).cumprod() 136 | self.equity_curve = curve 137 | 138 | def output_summary_stats(self): 139 | total_return = self.equity_curve['equity_curve'][-1] 140 | drawdown, duration = create_drawdowns(self.equity_curve['equity_curve']) 141 | 142 | return [ 143 | ('Total return', (total_return - 1) * 100), 144 | ('Sharpe ratio', create_sharpe_ratio(self.equity_curve['returns'])), 145 | ('Max drawdown', drawdown * 100), 146 | ('Drawdown duration', duration) 147 | ] 148 | -------------------------------------------------------------------------------- /backtester/strategy.py: -------------------------------------------------------------------------------- 1 | #PYTHON 2 | from abc import ( 3 | ABCMeta, 4 | abstractmethod 5 | ) 6 | 7 | #PROJECT 8 | from event import ( 9 | MarketEvent, 10 | SignalEvent 11 | ) 12 | 13 | 14 | class StrategyMetaclass(metaclass=ABCMeta): 15 | @abstractmethod 16 | def calculate_signals(self): 17 | raise NotImplementedError 18 | 19 | 20 | class BuyAndHoldStrategy(StrategyMetaclass): 21 | def __init__( 22 | self, 23 | data, 24 | event_queue 25 | ): 26 | self.data = data 27 | self.symbol_list = self.data.symbol_list 28 | self.event_queue = event_queue 29 | self.bought = {symbol: False for symbol in self.symbol_list} 30 | 31 | def calculate_signals( 32 | self, 33 | event 34 | ): 35 | if isinstance(event, MarketEvent): 36 | for symbol in self.symbol_list: 37 | data = self.data.get_latest_data(symbol)[0] 38 | if data is not None and len(data) > 0: 39 | signal = SignalEvent( 40 | symbol, 41 | data[1], 42 | 'LONG' 43 | ) 44 | self.event_queue.put(signal) 45 | self.bought[symbol] = True 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distribute==0.6.34 2 | numpy==1.9.1 3 | pandas==0.15.2 4 | python-dateutil==2.4.0 5 | pytz==2014.10 6 | six==1.9.0 7 | --------------------------------------------------------------------------------