├── README.md ├── backtest.py ├── backtest.pyc ├── create_lagged_series.py ├── create_lagged_series.pyc ├── csv ├── .DS_Store ├── aapl.csv ├── plot_performance.py └── spy.csv ├── data.py ├── data.pyc ├── equity.csv ├── event.py ├── event.pyc ├── execution.py ├── execution.pyc ├── ib_execution.py ├── mac.py ├── performance.py ├── performance.pyc ├── portfolio.py ├── portfolio.pyc ├── snp_forecast.py ├── strategy.py └── strategy.pyc /README.md: -------------------------------------------------------------------------------- 1 | # event-driven-backtesting 2 | The strategy-backtesting repository will hold the event driven python backtester. 3 | This program will test algorithmic strategies and provide interaction to a fake Interactive Brokers portfolio. 4 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Dec 8 19:19:12 2015 4 | 5 | @author: djunh 6 | """ 7 | from __future__ import print_function 8 | 9 | import datetime 10 | import pprint 11 | try: 12 | import Queue as queue 13 | except ImportError: 14 | import queue 15 | import time 16 | 17 | 18 | class Backtest(object): 19 | """ 20 | Enscapsulates the settings and components for carrying out 21 | an event-driven backtest. 22 | """ 23 | 24 | def __init__( 25 | self, csv_dir, symbol_list, initial_capital, 26 | heartbeat, start_date, data_handler, 27 | execution_handler, portfolio, strategy 28 | ): 29 | """ 30 | Initialises the backtest. 31 | 32 | Parameters: 33 | csv_dir - The hard root to the CSV data directory. 34 | symbol_list - The list of symbol strings. 35 | intial_capital - The starting capital for the portfolio. 36 | heartbeat - Backtest "heartbeat" in seconds 37 | start_date - The start datetime of the strategy. 38 | data_handler - (Class) Handles the market data feed. 39 | execution_handler - (Class) Handles the orders/fills for trades. 40 | portfolio - (Class) Keeps track of portfolio current and prior positions. 41 | strategy - (Class) Generates signals based on market data. 42 | """ 43 | self.csv_dir = csv_dir 44 | self.symbol_list = symbol_list 45 | self.initial_capital = initial_capital 46 | self.heartbeat = heartbeat 47 | self.start_date = start_date 48 | 49 | self.data_handler_cls = data_handler 50 | self.execution_handler_cls = execution_handler 51 | self.portfolio_cls = portfolio 52 | self.strategy_cls = strategy 53 | 54 | self.events = queue.Queue() 55 | 56 | self.signals = 0 57 | self.orders = 0 58 | self.fills = 0 59 | self.num_strats = 1 60 | 61 | self._generate_trading_instances() 62 | 63 | def _generate_trading_instances(self): 64 | """ 65 | Generates the trading instance objects from 66 | their class types. 67 | """ 68 | print( 69 | "Creating DataHandler, Strategy, Portfolio and ExecutionHandler" 70 | ) 71 | self.data_handler = self.data_handler_cls(self.events, self.csv_dir, self.symbol_list) 72 | self.strategy = self.strategy_cls(self.data_handler, self.events) 73 | self.portfolio = self.portfolio_cls(self.data_handler, self.events, self.start_date, 74 | self.initial_capital) 75 | self.execution_handler = self.execution_handler_cls(self.events) 76 | 77 | def _run_backtest(self): 78 | """ 79 | Executes the backtest. 80 | """ 81 | i = 0 82 | while True: 83 | i += 1 84 | print(i) 85 | # Update the market bars 86 | if self.data_handler.continue_backtest == True: 87 | self.data_handler.update_bars() 88 | else: 89 | break 90 | 91 | # Handle the events 92 | while True: 93 | try: 94 | event = self.events.get(False) 95 | except queue.Empty: 96 | break 97 | else: 98 | if event is not None: 99 | if event.type == 'MARKET': 100 | self.strategy.calculate_signals(event) 101 | self.portfolio.update_timeindex(event) 102 | 103 | elif event.type == 'SIGNAL': 104 | self.signals += 1 105 | self.portfolio.update_signal(event) 106 | 107 | elif event.type == 'ORDER': 108 | self.orders += 1 109 | self.execution_handler.execute_order(event) 110 | 111 | elif event.type == 'FILL': 112 | self.fills += 1 113 | self.portfolio.update_fill(event) 114 | 115 | time.sleep(self.heartbeat) 116 | 117 | def _output_performance(self): 118 | """ 119 | Outputs the strategy performance from the backtest. 120 | """ 121 | self.portfolio.create_equity_curve_dataframe() 122 | 123 | print("Creating summary stats...") 124 | stats = self.portfolio.output_summary_stats() 125 | 126 | print("Creating equity curve...") 127 | print(self.portfolio.equity_curve.tail(10)) 128 | pprint.pprint(stats) 129 | 130 | print("Signals: %s" % self.signals) 131 | print("Orders: %s" % self.orders) 132 | print("Fills: %s" % self.fills) 133 | 134 | def simulate_trading(self): 135 | """ 136 | Simulates the backtest and outputs portfolio performance. 137 | """ 138 | self._run_backtest() 139 | self._output_performance() -------------------------------------------------------------------------------- /backtest.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/backtest.pyc -------------------------------------------------------------------------------- /create_lagged_series.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Dec 8 20:59:21 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | 9 | import datetime 10 | 11 | import numpy as np 12 | import pandas as pd 13 | from pandas.io.data import DataReader 14 | 15 | 16 | def create_lagged_series(symbol, start_date, end_date, lags=5): 17 | """ 18 | This creates a pandas DataFrame that stores the 19 | percentage returns of the adjusted closing value of 20 | a stock obtained from Yahoo Finance, along with a 21 | number of lagged returns from the prior trading days 22 | (lags defaults to 5 days). Trading volume, as well as 23 | the Direction from the previous day, are also included. 24 | """ 25 | 26 | # Obtain stock information from Yahoo Finance 27 | ts = DataReader( 28 | symbol, "yahoo", 29 | start_date-datetime.timedelta(days=365), 30 | end_date 31 | ) 32 | 33 | # Create the new lagged DataFrame 34 | tslag = pd.DataFrame(index=ts.index) 35 | tslag["Today"] = ts["Adj Close"] 36 | tslag["Volume"] = ts["Volume"] 37 | 38 | # Create the shifted lag series of prior trading period close values 39 | for i in range(0,lags): 40 | tslag["Lag%s" % str(i+1)] = ts["Adj Close"].shift(i+1) 41 | 42 | # Create the returns DataFrame 43 | tsret = pd.DataFrame(index=tslag.index) 44 | tsret["Volume"] = tslag["Volume"] 45 | tsret["Today"] = tslag["Today"].pct_change()*100.0 46 | 47 | # If any of the values of percentage returns equal zero, set them to 48 | # a small number (stops issues with QDA model in scikit-learn) 49 | for i,x in enumerate(tsret["Today"]): 50 | if (abs(x) < 0.0001): 51 | tsret["Today"][i] = 0.0001 52 | 53 | # Create the lagged percentage returns columns 54 | for i in range(0,lags): 55 | tsret[ 56 | "Lag%s" % str(i+1) 57 | ] = tslag["Lag%s" % str(i+1)].pct_change()*100.0 58 | 59 | # Create the "Direction" column (+1 or -1) indicating an up/down day 60 | tsret["Direction"] = np.sign(tsret["Today"]) 61 | tsret = tsret[tsret.index >= start_date] 62 | 63 | return tsret -------------------------------------------------------------------------------- /create_lagged_series.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/create_lagged_series.pyc -------------------------------------------------------------------------------- /csv/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/csv/.DS_Store -------------------------------------------------------------------------------- /csv/plot_performance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Dec 8 21:19:51 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | import os.path 9 | 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | import pandas as pd 13 | 14 | 15 | if __name__ == "__main__": 16 | data = pd.io.parsers.read_csv( 17 | "equity.csv", header=0, 18 | parse_dates=True, index_col=0 19 | ).sort() 20 | 21 | # Plot three charts: Equity curve, 22 | # period returns, drawdowns 23 | fig = plt.figure() 24 | # Set the outer colour to white 25 | fig.patch.set_facecolor('white') 26 | 27 | # Plot the equity curve 28 | ax1 = fig.add_subplot(311, ylabel='Portfolio value, %') 29 | data['equity_curve'].plot(ax=ax1, color="blue", lw=2.) 30 | plt.grid(True) 31 | 32 | # Plot the returns 33 | ax2 = fig.add_subplot(312, ylabel='Period returns, %') 34 | data['returns'].plot(ax=ax2, color="black", lw=2.) 35 | plt.grid(True) 36 | 37 | # Plot the returns 38 | ax3 = fig.add_subplot(313, ylabel='Drawdowns, %') 39 | data['drawdown'].plot(ax=ax3, color="red", lw=2.) 40 | plt.grid(True) 41 | 42 | # Plot the figure 43 | plt.show() -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # data.py 5 | 6 | from __future__ import print_function 7 | 8 | from abc import ABCMeta, abstractmethod 9 | import datetime 10 | import os, os.path 11 | 12 | import numpy as np 13 | import pandas as pd 14 | 15 | from event import MarketEvent 16 | ''' 17 | TO DO: 18 | impliment more robust data handler using quandl, yahoo etc. 19 | ''' 20 | 21 | class DataHandler(object): 22 | """ 23 | DataHandler is an abstract base class providing an interface for 24 | all subsequent (inherited) data handlers (both live and historic). 25 | 26 | The goal of a (derived) DataHandler object is to output a generated 27 | set of bars (OHLCVI) for each symbol requested. 28 | 29 | This will replicate how a live strategy would function as current 30 | market data would be sent "down the pipe". Thus a historic and live 31 | system will be treated identically by the rest of the backtesting suite. 32 | """ 33 | 34 | __metaclass__ = ABCMeta 35 | 36 | @abstractmethod 37 | def get_latest_bar(self, symbol): 38 | """ 39 | Returns the last bar updated. 40 | """ 41 | raise NotImplementedError("Should implement get_latest_bar()") 42 | 43 | @abstractmethod 44 | def get_latest_bars(self, symbol, N=1): 45 | """ 46 | Returns the last N bars updated. 47 | """ 48 | raise NotImplementedError("Should implement get_latest_bars()") 49 | 50 | @abstractmethod 51 | def get_latest_bar_datetime(self, symbol): 52 | """ 53 | Returns a Python datetime object for the last bar. 54 | """ 55 | raise NotImplementedError("Should implement get_latest_bar_datetime()") 56 | 57 | @abstractmethod 58 | def get_latest_bar_value(self, symbol, val_type): 59 | """ 60 | Returns one of the Open, High, Low, Close, Volume or OI 61 | from the last bar. 62 | """ 63 | raise NotImplementedError("Should implement get_latest_bar_value()") 64 | 65 | @abstractmethod 66 | def get_latest_bars_values(self, symbol, val_type, N=1): 67 | """ 68 | Returns the last N bar values from the 69 | latest_symbol list, or N-k if less available. 70 | """ 71 | raise NotImplementedError("Should implement get_latest_bars_values()") 72 | 73 | @abstractmethod 74 | def update_bars(self): 75 | """ 76 | Pushes the latest bars to the bars_queue for each symbol 77 | in a tuple OHLCVI format: (datetime, open, high, low, 78 | close, volume, open interest). 79 | """ 80 | raise NotImplementedError("Should implement update_bars()") 81 | 82 | 83 | class HistoricCSVDataHandler(DataHandler): 84 | """ 85 | HistoricCSVDataHandler is designed to read CSV files for 86 | each requested symbol from disk and provide an interface 87 | to obtain the "latest" bar in a manner identical to a live 88 | trading interface. 89 | """ 90 | 91 | def __init__(self, events, csv_dir, symbol_list): 92 | """ 93 | Initialises the historic data handler by requesting 94 | the location of the CSV files and a list of symbols. 95 | 96 | It will be assumed that all files are of the form 97 | 'symbol.csv', where symbol is a string in the list. 98 | 99 | Parameters: 100 | events - The Event Queue. 101 | csv_dir - Absolute directory path to the CSV files. 102 | symbol_list - A list of symbol strings. 103 | """ 104 | self.events = events 105 | self.csv_dir = csv_dir 106 | self.symbol_list = symbol_list 107 | 108 | self.symbol_data = {} 109 | self.latest_symbol_data = {} 110 | self.continue_backtest = True 111 | self.bar_index = 0 112 | 113 | self._open_convert_csv_files() 114 | 115 | def _open_convert_csv_files(self): 116 | """ 117 | Opens the CSV files from the data directory, converting 118 | them into pandas DataFrames within a symbol dictionary. 119 | 120 | For this handler it will be assumed that the data is 121 | taken from Yahoo. Thus its format will be respected. 122 | """ 123 | comb_index = None 124 | for s in self.symbol_list: 125 | # Load the CSV file with no header information, indexed on date 126 | self.symbol_data[s] = pd.io.parsers.read_csv( 127 | os.path.join(self.csv_dir, '%s.csv' % s), 128 | header=0, index_col=0, parse_dates=True, 129 | names=[ 130 | 'datetime', 'open', 'high', 131 | 'low', 'close', 'volume', 'adj_close' 132 | ] 133 | ).sort_index() 134 | 135 | # Combine the index to pad forward values 136 | if comb_index is None: 137 | comb_index = self.symbol_data[s].index 138 | else: 139 | comb_index.union(self.symbol_data[s].index) 140 | 141 | # Set the latest symbol_data to None 142 | self.latest_symbol_data[s] = [] 143 | 144 | # Reindex the dataframes 145 | for s in self.symbol_list: 146 | self.symbol_data[s] = self.symbol_data[s].\ 147 | reindex(index=comb_index, method='pad').iterrows() 148 | 149 | def _get_new_bar(self, symbol): 150 | """ 151 | Returns the latest bar from the data feed. 152 | """ 153 | for b in self.symbol_data[symbol]: 154 | yield b 155 | 156 | def get_latest_bar(self, symbol): 157 | """ 158 | Returns the last bar from the latest_symbol list. 159 | """ 160 | try: 161 | bars_list = self.latest_symbol_data[symbol] 162 | except KeyError: 163 | print("That symbol is not available in the historical data set.") 164 | raise 165 | else: 166 | return bars_list[-1] 167 | 168 | def get_latest_bars(self, symbol, N=1): 169 | """ 170 | Returns the last N bars from the latest_symbol list, 171 | or N-k if less available. 172 | """ 173 | try: 174 | bars_list = self.latest_symbol_data[symbol] 175 | except KeyError: 176 | print("That symbol is not available in the historical data set.") 177 | raise 178 | else: 179 | return bars_list[-N:] 180 | 181 | def get_latest_bar_datetime(self, symbol): 182 | """ 183 | Returns a Python datetime object for the last bar. 184 | """ 185 | try: 186 | bars_list = self.latest_symbol_data[symbol] 187 | except KeyError: 188 | print("That symbol is not available in the historical data set.") 189 | raise 190 | else: 191 | return bars_list[-1][0] 192 | 193 | def get_latest_bar_value(self, symbol, val_type): 194 | """ 195 | Returns one of the Open, High, Low, Close, Volume or OI 196 | values from the pandas Bar series object. 197 | """ 198 | try: 199 | bars_list = self.latest_symbol_data[symbol] 200 | except KeyError: 201 | print("That symbol is not available in the historical data set.") 202 | raise 203 | else: 204 | return getattr(bars_list[-1][1], val_type) 205 | 206 | def get_latest_bars_values(self, symbol, val_type, N=1): 207 | """ 208 | Returns the last N bar values from the 209 | latest_symbol list, or N-k if less available. 210 | """ 211 | try: 212 | bars_list = self.get_latest_bars(symbol, N) 213 | except KeyError: 214 | print("That symbol is not available in the historical data set.") 215 | raise 216 | else: 217 | return np.array([getattr(b[1], val_type) for b in bars_list]) 218 | 219 | def update_bars(self): 220 | """ 221 | Pushes the latest bar to the latest_symbol_data structure 222 | for all symbols in the symbol list. 223 | """ 224 | for s in self.symbol_list: 225 | try: 226 | bar = next(self._get_new_bar(s)) 227 | except StopIteration: 228 | self.continue_backtest = False 229 | else: 230 | if bar is not None: 231 | self.latest_symbol_data[s].append(bar) 232 | self.events.put(MarketEvent()) 233 | -------------------------------------------------------------------------------- /data.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/data.pyc -------------------------------------------------------------------------------- /event.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # event.py 5 | 6 | from __future__ import print_function 7 | 8 | 9 | class Event(object): 10 | """ 11 | Event is base class providing an interface for all subsequent 12 | (inherited) events, that will trigger further events in the 13 | trading infrastructure. 14 | """ 15 | pass 16 | 17 | 18 | class MarketEvent(Event): 19 | """ 20 | Handles the event of receiving a new market update with 21 | corresponding bars. 22 | """ 23 | 24 | def __init__(self): 25 | """ 26 | Initialises the MarketEvent. 27 | """ 28 | self.type = 'MARKET' 29 | 30 | 31 | class SignalEvent(Event): 32 | """ 33 | Handles the event of sending a Signal from a Strategy object. 34 | This is received by a Portfolio object and acted upon. 35 | """ 36 | 37 | def __init__(self, strategy_id, symbol, datetime, signal_type, strength): 38 | """ 39 | Initialises the SignalEvent. 40 | 41 | Parameters: 42 | strategy_id - The unique ID of the strategy sending the signal. 43 | symbol - The ticker symbol, e.g. 'GOOG'. 44 | datetime - The timestamp at which the signal was generated. 45 | signal_type - 'LONG' or 'SHORT'. 46 | strength - An adjustment factor "suggestion" used to scale 47 | quantity at the portfolio level. Useful for pairs strategies. 48 | """ 49 | self.strategy_id = strategy_id 50 | self.type = 'SIGNAL' 51 | self.symbol = symbol 52 | self.datetime = datetime 53 | self.signal_type = signal_type 54 | self.strength = strength 55 | 56 | 57 | class OrderEvent(Event): 58 | """ 59 | Handles the event of sending an Order to an execution system. 60 | The order contains a symbol (e.g. GOOG), a type (market or limit), 61 | quantity and a direction. 62 | """ 63 | 64 | def __init__(self, symbol, order_type, quantity, direction): 65 | """ 66 | Initialises the order type, setting whether it is 67 | a Market order ('MKT') or Limit order ('LMT'), has 68 | a quantity (integral) and its direction ('BUY' or 69 | 'SELL'). 70 | 71 | TODO: Must handle error checking here to obtain 72 | rational orders (i.e. no negative quantities etc). 73 | 74 | Parameters: 75 | symbol - The instrument to trade. 76 | order_type - 'MKT' or 'LMT' for Market or Limit. 77 | quantity - Non-negative integer for quantity. 78 | direction - 'BUY' or 'SELL' for long or short. 79 | """ 80 | self.type = 'ORDER' 81 | self.symbol = symbol 82 | self.order_type = order_type 83 | self.quantity = quantity 84 | self.direction = direction 85 | 86 | def print_order(self): 87 | """ 88 | Outputs the values within the Order. 89 | """ 90 | print( 91 | "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" % 92 | (self.symbol, self.order_type, self.quantity, self.direction) 93 | ) 94 | 95 | 96 | class FillEvent(Event): 97 | """ 98 | Encapsulates the notion of a Filled Order, as returned 99 | from a brokerage. Stores the quantity of an instrument 100 | actually filled and at what price. In addition, stores 101 | the commission of the trade from the brokerage. 102 | 103 | TODO: Currently does not support filling positions at 104 | different prices. This will be simulated by averaging 105 | the cost. 106 | """ 107 | 108 | def __init__(self, timeindex, symbol, exchange, quantity, 109 | direction, fill_cost, commission=None): 110 | """ 111 | Initialises the FillEvent object. Sets the symbol, exchange, 112 | quantity, direction, cost of fill and an optional 113 | commission. 114 | 115 | If commission is not provided, the Fill object will 116 | calculate it based on the trade size and Interactive 117 | Brokers fees. 118 | 119 | Parameters: 120 | timeindex - The bar-resolution when the order was filled. 121 | symbol - The instrument which was filled. 122 | exchange - The exchange where the order was filled. 123 | quantity - The filled quantity. 124 | direction - The direction of fill ('BUY' or 'SELL') 125 | fill_cost - The holdings value in dollars. 126 | commission - An optional commission sent from IB. 127 | """ 128 | self.type = 'FILL' 129 | self.timeindex = timeindex 130 | self.symbol = symbol 131 | self.exchange = exchange 132 | self.quantity = quantity 133 | self.direction = direction 134 | self.fill_cost = fill_cost 135 | 136 | # Calculate commission 137 | if commission is None: 138 | self.commission = self.calculate_ib_commission() 139 | else: 140 | self.commission = commission 141 | 142 | def calculate_ib_commission(self): 143 | """ 144 | Calculates the fees of trading based on an Interactive 145 | Brokers fee structure for API, in USD. 146 | 147 | This does not include exchange or ECN fees. 148 | 149 | Based on "US API Directed Orders": 150 | https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2 151 | """ 152 | full_cost = 1.3 153 | if self.quantity <= 500: 154 | full_cost = max(1.3, 0.013 * self.quantity) 155 | else: # Greater than 500 156 | full_cost = max(1.3, 0.008 * self.quantity) 157 | return full_cost 158 | -------------------------------------------------------------------------------- /event.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/event.pyc -------------------------------------------------------------------------------- /execution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Nov 15 11:50:36 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | from abc import ABCMeta, abstractmethod 11 | import datetime 12 | try: 13 | import Queue as queue 14 | except ImportError: 15 | import queue 16 | 17 | from event import FillEvent, OrderEvent 18 | 19 | 20 | class ExecutionHandler(object): 21 | """ 22 | The ExecutionHandler abstract class handles the interaction 23 | between a set of order objects generated by a Portfolio and 24 | the ultimate set of Fill objects that actually occur in the 25 | market. 26 | 27 | The handlers can be used to subclass simulated brokerages 28 | or live brokerages, with identical interfaces. This allows 29 | strategies to be backtested in a very similar manner to the 30 | live trading engine. 31 | """ 32 | 33 | __metaclass__ = ABCMeta 34 | 35 | @abstractmethod 36 | def execute_order(self, event): 37 | """ 38 | Takes an Order event and executes it, producing 39 | a Fill event that gets placed onto the Events queue. 40 | 41 | Parameters: 42 | event - Contains an Event object with order information. 43 | """ 44 | raise NotImplementedError("Should implement execute_order()") 45 | 46 | 47 | class SimulatedExecutionHandler(ExecutionHandler): 48 | """ 49 | The simulated execution handler simply converts all order 50 | objects into their equivalent fill objects automatically 51 | without latency, slippage or fill-ratio issues. 52 | 53 | This allows a straightforward "first go" test of any strategy, 54 | before implementation with a more sophisticated execution 55 | handler. 56 | """ 57 | 58 | def __init__(self, events): 59 | """ 60 | Initialises the handler, setting the event queues 61 | up internally. 62 | 63 | Parameters: 64 | events - The Queue of Event objects. 65 | """ 66 | self.events = events 67 | 68 | def execute_order(self, event): 69 | """ 70 | Simply converts Order objects into Fill objects naively, 71 | i.e. without any latency, slippage or fill ratio problems. 72 | 73 | Parameters: 74 | event - Contains an Event object with order information. 75 | """ 76 | if event.type == 'ORDER': 77 | fill_event = FillEvent( 78 | datetime.datetime.utcnow(), event.symbol, 79 | 'ARCA', event.quantity, event.direction, None 80 | ) 81 | self.events.put(fill_event) -------------------------------------------------------------------------------- /execution.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/execution.pyc -------------------------------------------------------------------------------- /ib_execution.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Nov 16 20:39:56 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | 9 | from __future__ import print_function 10 | 11 | import datetime 12 | import time 13 | 14 | from ib.ext.Contract import Contract 15 | from ib.ext.Order import Order 16 | from ib.opt import ibConnection, message 17 | 18 | from event import FillEvent, OrderEvent 19 | from execution import ExecutionHandler 20 | 21 | 22 | class IBExecutionHandler(ExecutionHandler): 23 | """ 24 | Handles order execution via the Interactive Brokers 25 | API, for use against accounts when trading live 26 | directly. 27 | """ 28 | 29 | def __init__( 30 | self, events, order_routing="SMART", currency="USD" 31 | ): 32 | """ 33 | Initialises the IBExecutionHandler instance. 34 | 35 | Parameters: 36 | events - The Queue of Event objects. 37 | """ 38 | self.events = events 39 | self.order_routing = order_routing 40 | self.currency = currency 41 | self.fill_dict = {} 42 | 43 | self.tws_conn = self.create_tws_connection() 44 | self.order_id = self.create_initial_order_id() 45 | self.register_handlers() 46 | 47 | def _error_handler(self, msg): 48 | """Handles the capturing of error messages""" 49 | # Currently no error handling. 50 | print("Server Error: %s" % msg) 51 | 52 | def _reply_handler(self, msg): 53 | """Handles of server replies""" 54 | # Handle open order orderId processing 55 | if msg.typeName == "openOrder" and \ 56 | msg.orderId == self.order_id and \ 57 | not self.fill_dict.has_key(msg.orderId): 58 | self.create_fill_dict_entry(msg) 59 | # Handle Fills 60 | if msg.typeName == "orderStatus" and \ 61 | msg.status == "Filled" and \ 62 | self.fill_dict[msg.orderId]["filled"] == False: 63 | self.create_fill(msg) 64 | print("Server Response: %s, %s\n" % (msg.typeName, msg)) 65 | 66 | def create_tws_connection(self): 67 | """ 68 | Connect to the Trader Workstation (TWS) running on the 69 | usual port of 7496, with a clientId of 100. 70 | The clientId is chosen by us and we will need 71 | separate IDs for both the execution connection and 72 | market data connection, if the latter is used elsewhere. 73 | """ 74 | tws_conn = ibConnection() 75 | tws_conn.connect() 76 | return tws_conn 77 | 78 | def create_initial_order_id(self): 79 | """ 80 | Creates the initial order ID used for Interactive 81 | Brokers to keep track of submitted orders. 82 | """ 83 | # There is scope for more logic here, but we 84 | # will use "1" as the default for now. 85 | return 1 86 | 87 | def register_handlers(self): 88 | """ 89 | Register the error and server reply 90 | message handling functions. 91 | """ 92 | # Assign the error handling function defined above 93 | # to the TWS connection 94 | self.tws_conn.register(self._error_handler, 'Error') 95 | 96 | # Assign all of the server reply messages to the 97 | # reply_handler function defined above 98 | self.tws_conn.registerAll(self._reply_handler) 99 | 100 | def create_contract(self, symbol, sec_type, exch, prim_exch, curr): 101 | """Create a Contract object defining what will 102 | be purchased, at which exchange and in which currency. 103 | 104 | symbol - The ticker symbol for the contract 105 | sec_type - The security type for the contract ('STK' is 'stock') 106 | exch - The exchange to carry out the contract on 107 | prim_exch - The primary exchange to carry out the contract on 108 | curr - The currency in which to purchase the contract""" 109 | contract = Contract() 110 | contract.m_symbol = symbol 111 | contract.m_secType = sec_type 112 | contract.m_exchange = exch 113 | contract.m_primaryExch = prim_exch 114 | contract.m_currency = curr 115 | return contract 116 | 117 | def create_order(self, order_type, quantity, action): 118 | """Create an Order object (Market/Limit) to go long/short. 119 | 120 | order_type - 'MKT', 'LMT' for Market or Limit orders 121 | quantity - Integral number of assets to order 122 | action - 'BUY' or 'SELL'""" 123 | order = Order() 124 | order.m_orderType = order_type 125 | order.m_totalQuantity = quantity 126 | order.m_action = action 127 | return order 128 | 129 | def create_fill_dict_entry(self, msg): 130 | """ 131 | Creates an entry in the Fill Dictionary that lists 132 | orderIds and provides security information. This is 133 | needed for the event-driven behaviour of the IB 134 | server message behaviour. 135 | """ 136 | self.fill_dict[msg.orderId] = { 137 | "symbol": msg.contract.m_symbol, 138 | "exchange": msg.contract.m_exchange, 139 | "direction": msg.order.m_action, 140 | "filled": False 141 | } 142 | 143 | def create_fill(self, msg): 144 | """ 145 | Handles the creation of the FillEvent that will be 146 | placed onto the events queue subsequent to an order 147 | being filled. 148 | """ 149 | fd = self.fill_dict[msg.orderId] 150 | 151 | # Prepare the fill data 152 | symbol = fd["symbol"] 153 | exchange = fd["exchange"] 154 | filled = msg.filled 155 | direction = fd["direction"] 156 | fill_cost = msg.avgFillPrice 157 | 158 | # Create a fill event object 159 | fill = FillEvent( 160 | datetime.datetime.utcnow(), symbol, 161 | exchange, filled, direction, fill_cost 162 | ) 163 | 164 | # Make sure that multiple messages don't create 165 | # additional fills. 166 | self.fill_dict[msg.orderId]["filled"] = True 167 | 168 | # Place the fill event onto the event queue 169 | self.events.put(fill_event) 170 | 171 | def execute_order(self, event): 172 | """ 173 | Creates the necessary InteractiveBrokers order object 174 | and submits it to IB via their API. 175 | 176 | The results are then queried in order to generate a 177 | corresponding Fill object, which is placed back on 178 | the event queue. 179 | 180 | Parameters: 181 | event - Contains an Event object with order information. 182 | """ 183 | if event.type == 'ORDER': 184 | # Prepare the parameters for the asset order 185 | asset = event.symbol 186 | asset_type = "STK" 187 | order_type = event.order_type 188 | quantity = event.quantity 189 | direction = event.direction 190 | 191 | # Create the Interactive Brokers contract via the 192 | # passed Order event 193 | ib_contract = self.create_contract( 194 | asset, asset_type, self.order_routing, 195 | self.order_routing, self.currency 196 | ) 197 | 198 | # Create the Interactive Brokers order via the 199 | # passed Order event 200 | ib_order = self.create_order( 201 | order_type, quantity, direction 202 | ) 203 | 204 | # Use the connection to the send the order to IB 205 | self.tws_conn.placeOrder( 206 | self.order_id, ib_contract, ib_order 207 | ) 208 | 209 | # NOTE: This following line is crucial. 210 | # It ensures the order goes through! 211 | time.sleep(1) 212 | 213 | # Increment the order ID for this session 214 | self.order_id += 1 -------------------------------------------------------------------------------- /mac.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Dec 8 19:47:21 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | 9 | from __future__ import print_function 10 | 11 | import datetime 12 | 13 | import numpy as np 14 | import pandas as pd 15 | import statsmodels.api as sm 16 | 17 | from strategy import Strategy 18 | from event import SignalEvent 19 | from backtest import Backtest 20 | from data import HistoricCSVDataHandler 21 | from execution import SimulatedExecutionHandler 22 | from portfolio import Portfolio 23 | 24 | 25 | class MovingAverageCrossStrategy(Strategy): 26 | """ 27 | Carries out a basic Moving Average Crossover strategy with a 28 | short/long simple weighted moving average. Default short/long 29 | windows are 100/400 periods respectively. 30 | """ 31 | 32 | def __init__( 33 | self, bars, events, short_window=100, long_window=400 34 | ): 35 | """ 36 | Initialises the Moving Average Cross Strategy. 37 | 38 | Parameters: 39 | bars - The DataHandler object that provides bar information 40 | events - The Event Queue object. 41 | short_window - The short moving average lookback. 42 | long_window - The long moving average lookback. 43 | """ 44 | self.bars = bars 45 | self.symbol_list = self.bars.symbol_list 46 | self.events = events 47 | self.short_window = short_window 48 | self.long_window = long_window 49 | 50 | # Set to True if a symbol is in the market 51 | self.bought = self._calculate_initial_bought() 52 | 53 | def _calculate_initial_bought(self): 54 | """ 55 | Adds keys to the bought dictionary for all symbols 56 | and sets them to 'OUT'. 57 | """ 58 | bought = {} 59 | for s in self.symbol_list: 60 | bought[s] = 'OUT' 61 | return bought 62 | 63 | def calculate_signals(self, event): 64 | """ 65 | Generates a new set of signals based on the MAC 66 | SMA with the short window crossing the long window 67 | meaning a long entry and vice versa for a short entry. 68 | 69 | Parameters 70 | event - A MarketEvent object. 71 | """ 72 | if event.type == 'MARKET': 73 | for s in self.symbol_list: 74 | bars = self.bars.get_latest_bars_values( 75 | s, "adj_close", N=self.long_window 76 | ) 77 | bar_date = self.bars.get_latest_bar_datetime(s) 78 | if bars is not None and bars != []: 79 | short_sma = np.mean(bars[-self.short_window:]) 80 | long_sma = np.mean(bars[-self.long_window:]) 81 | 82 | symbol = s 83 | dt = datetime.datetime.utcnow() 84 | sig_dir = "" 85 | 86 | if short_sma > long_sma and self.bought[s] == "OUT": 87 | print("LONG: %s" % bar_date) 88 | sig_dir = 'LONG' 89 | signal = SignalEvent(1, symbol, dt, sig_dir, 1.0) 90 | self.events.put(signal) 91 | self.bought[s] = 'LONG' 92 | elif short_sma < long_sma and self.bought[s] == "LONG": 93 | print("SHORT: %s" % bar_date) 94 | sig_dir = 'EXIT' 95 | signal = SignalEvent(1, symbol, dt, sig_dir, 1.0) 96 | self.events.put(signal) 97 | self.bought[s] = 'OUT' 98 | 99 | 100 | if __name__ == "__main__": 101 | csv_dir = '/Users/djunh/programming/backtester/csv' 102 | symbol_list = ['AAPL'] 103 | initial_capital = 100000.0 104 | heartbeat = 0.0 105 | start_date = datetime.datetime(1990, 1, 1, 0, 0, 0) 106 | 107 | backtest = Backtest( 108 | csv_dir, symbol_list, initial_capital, heartbeat, 109 | start_date, HistoricCSVDataHandler, SimulatedExecutionHandler, 110 | Portfolio, MovingAverageCrossStrategy 111 | ) 112 | backtest.simulate_trading() -------------------------------------------------------------------------------- /performance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Nov 15 12:06:44 2015 4 | 5 | @author: djunh 6 | """ 7 | from __future__ import print_function 8 | 9 | import numpy as np 10 | import pandas as pd 11 | 12 | 13 | def create_sharpe_ratio(returns, periods=252): 14 | """ 15 | Create the Sharpe ratio for the strategy, based on a 16 | benchmark of zero (i.e. no risk-free rate information). 17 | 18 | Parameters: 19 | returns - A pandas Series representing period percentage returns. 20 | periods - Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc. 21 | """ 22 | return np.sqrt(periods) * (np.mean(returns)) / np.std(returns) 23 | 24 | 25 | def create_drawdowns(pnl): 26 | """ 27 | Calculate the largest peak-to-trough drawdown of the PnL curve 28 | as well as the duration of the drawdown. Requires that the 29 | pnl_returns is a pandas Series. 30 | 31 | Parameters: 32 | pnl - A pandas Series representing period percentage returns. 33 | 34 | Returns: 35 | drawdown, duration - Highest peak-to-trough drawdown and duration. 36 | """ 37 | 38 | # Calculate the cumulative returns curve 39 | # and set up the High Water Mark 40 | hwm = [0] 41 | 42 | # Create the drawdown and duration series 43 | idx = pnl.index 44 | drawdown = pd.Series(index = idx) 45 | duration = pd.Series(index = idx) 46 | 47 | # Loop over the index range 48 | for t in range(1, len(idx)): 49 | hwm.append(max(hwm[t-1], pnl[t])) 50 | drawdown[t]= (hwm[t]-pnl[t]) 51 | duration[t]= (0 if drawdown[t] == 0 else duration[t-1]+1) 52 | return drawdown, drawdown.max(), duration.max() -------------------------------------------------------------------------------- /performance.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/performance.pyc -------------------------------------------------------------------------------- /portfolio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Nov 15 10:08:16 2015 4 | 5 | @author: djunh 6 | """ 7 | from __future__ import print_function 8 | 9 | import datetime 10 | from math import floor 11 | try: 12 | import Queue as queue 13 | except ImportError: 14 | import queue 15 | 16 | import numpy as np 17 | import pandas as pd 18 | 19 | from event import FillEvent, OrderEvent 20 | from performance import create_sharpe_ratio, create_drawdowns 21 | 22 | 23 | class Portfolio(object): 24 | """ 25 | The Portfolio class handles the positions and market 26 | value of all instruments at a resolution of a "bar", 27 | i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD. 28 | 29 | The positions DataFrame stores a time-index of the 30 | quantity of positions held. 31 | 32 | The holdings DataFrame stores the cash and total market 33 | holdings value of each symbol for a particular 34 | time-index, as well as the percentage change in 35 | portfolio total across bars. 36 | """ 37 | 38 | def __init__(self, bars, events, start_date, initial_capital=100000.0): 39 | """ 40 | Initialises the portfolio with bars and an event queue. 41 | Also includes a starting datetime index and initial capital 42 | (USD unless otherwise stated). 43 | 44 | Parameters: 45 | bars - The DataHandler object with current market data. 46 | events - The Event Queue object. 47 | start_date - The start date (bar) of the portfolio. 48 | initial_capital - The starting capital in USD. 49 | """ 50 | self.bars = bars 51 | self.events = events 52 | self.symbol_list = self.bars.symbol_list 53 | self.start_date = start_date 54 | self.initial_capital = initial_capital 55 | 56 | self.all_positions = self.construct_all_positions() 57 | self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) 58 | 59 | self.all_holdings = self.construct_all_holdings() 60 | self.current_holdings = self.construct_current_holdings() 61 | 62 | def construct_all_positions(self): 63 | """ 64 | Constructs the positions list using the start_date 65 | to determine when the time index will begin. 66 | """ 67 | d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) 68 | d['datetime'] = self.start_date 69 | return [d] 70 | 71 | def construct_all_holdings(self): 72 | """ 73 | Constructs the holdings list using the start_date 74 | to determine when the time index will begin. 75 | """ 76 | d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] ) 77 | d['datetime'] = self.start_date 78 | d['cash'] = self.initial_capital 79 | d['commission'] = 0.0 80 | d['total'] = self.initial_capital 81 | return [d] 82 | 83 | def construct_current_holdings(self): 84 | """ 85 | This constructs the dictionary which will hold the instantaneous 86 | value of the portfolio across all symbols. 87 | """ 88 | d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] ) 89 | d['cash'] = self.initial_capital 90 | d['commission'] = 0.0 91 | d['total'] = self.initial_capital 92 | return d 93 | 94 | def update_timeindex(self, event): 95 | """ 96 | Adds a new record to the positions matrix for the current 97 | market data bar. This reflects the PREVIOUS bar, i.e. all 98 | current market data at this stage is known (OHLCV). 99 | 100 | Makes use of a MarketEvent from the events queue. 101 | """ 102 | latest_datetime = self.bars.get_latest_bar_datetime(self.symbol_list[0]) 103 | 104 | # Update positions 105 | # ================ 106 | dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) 107 | dp['datetime'] = latest_datetime 108 | 109 | for s in self.symbol_list: 110 | dp[s] = self.current_positions[s] 111 | 112 | # Append the current positions 113 | self.all_positions.append(dp) 114 | 115 | # Update holdings 116 | # =============== 117 | dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) 118 | dh['datetime'] = latest_datetime 119 | dh['cash'] = self.current_holdings['cash'] 120 | dh['commission'] = self.current_holdings['commission'] 121 | dh['total'] = self.current_holdings['cash'] 122 | 123 | for s in self.symbol_list: 124 | # Approximation to the real value 125 | market_value = self.current_positions[s] * \ 126 | self.bars.get_latest_bar_value(s, "adj_close") 127 | dh[s] = market_value 128 | dh['total'] += market_value 129 | 130 | # Append the current holdings 131 | self.all_holdings.append(dh) 132 | 133 | # ====================== 134 | # FILL/POSITION HANDLING 135 | # ====================== 136 | 137 | def update_positions_from_fill(self, fill): 138 | """ 139 | Takes a Fill object and updates the position matrix to 140 | reflect the new position. 141 | 142 | Parameters: 143 | fill - The Fill object to update the positions with. 144 | """ 145 | # Check whether the fill is a buy or sell 146 | fill_dir = 0 147 | if fill.direction == 'BUY': 148 | fill_dir = 1 149 | if fill.direction == 'SELL': 150 | fill_dir = -1 151 | 152 | # Update positions list with new quantities 153 | self.current_positions[fill.symbol] += fill_dir*fill.quantity 154 | 155 | def update_holdings_from_fill(self, fill): 156 | """ 157 | Takes a Fill object and updates the holdings matrix to 158 | reflect the holdings value. 159 | 160 | Parameters: 161 | fill - The Fill object to update the holdings with. 162 | """ 163 | # Check whether the fill is a buy or sell 164 | fill_dir = 0 165 | if fill.direction == 'BUY': 166 | fill_dir = 1 167 | if fill.direction == 'SELL': 168 | fill_dir = -1 169 | 170 | # Update holdings list with new quantities 171 | fill_cost = self.bars.get_latest_bar_value( 172 | fill.symbol, "adj_close" 173 | ) 174 | cost = fill_dir * fill_cost * fill.quantity 175 | self.current_holdings[fill.symbol] += cost 176 | self.current_holdings['commission'] += fill.commission 177 | self.current_holdings['cash'] -= (cost + fill.commission) 178 | self.current_holdings['total'] -= (cost + fill.commission) 179 | 180 | def update_fill(self, event): 181 | """ 182 | Updates the portfolio current positions and holdings 183 | from a FillEvent. 184 | """ 185 | if event.type == 'FILL': 186 | self.update_positions_from_fill(event) 187 | self.update_holdings_from_fill(event) 188 | 189 | def generate_naive_order(self, signal): 190 | """ 191 | Simply files an Order object as a constant quantity 192 | sizing of the signal object, without risk management or 193 | position sizing considerations. 194 | 195 | Parameters: 196 | signal - The tuple containing Signal information. 197 | """ 198 | order = None 199 | 200 | symbol = signal.symbol 201 | direction = signal.signal_type 202 | strength = signal.strength 203 | 204 | mkt_quantity = 100 205 | cur_quantity = self.current_positions[symbol] 206 | order_type = 'MKT' 207 | 208 | if direction == 'LONG' and cur_quantity == 0: 209 | order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY') 210 | if direction == 'SHORT' and cur_quantity == 0: 211 | order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL') 212 | 213 | if direction == 'EXIT' and cur_quantity > 0: 214 | order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL') 215 | if direction == 'EXIT' and cur_quantity < 0: 216 | order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY') 217 | return order 218 | 219 | def update_signal(self, event): 220 | """ 221 | Acts on a SignalEvent to generate new orders 222 | based on the portfolio logic. 223 | """ 224 | if event.type == 'SIGNAL': 225 | order_event = self.generate_naive_order(event) 226 | self.events.put(order_event) 227 | 228 | # ======================== 229 | # POST-BACKTEST STATISTICS 230 | # ======================== 231 | 232 | def create_equity_curve_dataframe(self): 233 | """ 234 | Creates a pandas DataFrame from the all_holdings 235 | list of dictionaries. 236 | """ 237 | curve = pd.DataFrame(self.all_holdings) 238 | curve.set_index('datetime', inplace=True) 239 | curve['returns'] = curve['total'].pct_change() 240 | curve['equity_curve'] = (1.0+curve['returns']).cumprod() 241 | self.equity_curve = curve 242 | 243 | def output_summary_stats(self): 244 | """ 245 | Creates a list of summary statistics for the portfolio. 246 | """ 247 | total_return = self.equity_curve['equity_curve'][-1] 248 | returns = self.equity_curve['returns'] 249 | pnl = self.equity_curve['equity_curve'] 250 | 251 | sharpe_ratio = create_sharpe_ratio(returns, periods=252*60*6.5) 252 | drawdown, max_dd, dd_duration = create_drawdowns(pnl) 253 | self.equity_curve['drawdown'] = drawdown 254 | 255 | stats = [("Total Return", "%0.2f%%" % ((total_return - 1.0) * 100.0)), 256 | ("Sharpe Ratio", "%0.2f" % sharpe_ratio), 257 | ("Max Drawdown", "%0.2f%%" % (max_dd * 100.0)), 258 | ("Drawdown Duration", "%d" % dd_duration)] 259 | 260 | self.equity_curve.to_csv('equity.csv') 261 | return stats -------------------------------------------------------------------------------- /portfolio.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/portfolio.pyc -------------------------------------------------------------------------------- /snp_forecast.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Dec 8 20:40:22 2015 4 | 5 | @author: djunh 6 | """ 7 | 8 | from __future__ import print_function 9 | 10 | import datetime 11 | 12 | import pandas as pd 13 | from sklearn.qda import QDA 14 | 15 | from strategy import Strategy 16 | from event import SignalEvent 17 | from backtest import Backtest 18 | from data import HistoricCSVDataHandler 19 | from execution import SimulatedExecutionHandler 20 | from portfolio import Portfolio 21 | from create_lagged_series import create_lagged_series 22 | 23 | 24 | class SPYDailyForecastStrategy(Strategy): 25 | """ 26 | S&P500 forecast strategy. It uses a Quadratic Discriminant 27 | Analyser to predict the returns for a subsequent time 28 | period and then generated long/exit signals based on the 29 | prediction. 30 | """ 31 | def __init__(self, bars, events): 32 | self.bars = bars 33 | self.symbol_list = self.bars.symbol_list 34 | self.events = events 35 | self.datetime_now = datetime.datetime.utcnow() 36 | 37 | self.model_start_date = datetime.datetime(2001,1,10) 38 | self.model_end_date = datetime.datetime(2005,12,31) 39 | self.model_start_test_date = datetime.datetime(2005,1,1) 40 | 41 | self.long_market = False 42 | self.short_market = False 43 | self.bar_index = 0 44 | 45 | self.model = self.create_symbol_forecast_model() 46 | 47 | def create_symbol_forecast_model(self): 48 | # Create a lagged series of the S&P500 US stock market index 49 | snpret = create_lagged_series( 50 | self.symbol_list[0], self.model_start_date, 51 | self.model_end_date, lags=5 52 | ) 53 | 54 | # Use the prior two days of returns as predictor 55 | # values, with direction as the response 56 | X = snpret[["Lag1","Lag2"]] 57 | y = snpret["Direction"] 58 | 59 | # Create training and test sets 60 | start_test = self.model_start_test_date 61 | X_train = X[X.index < start_test] 62 | X_test = X[X.index >= start_test] 63 | y_train = y[y.index < start_test] 64 | y_test = y[y.index >= start_test] 65 | 66 | model = QDA() 67 | model.fit(X_train, y_train) 68 | return model 69 | 70 | def calculate_signals(self, event): 71 | """ 72 | Calculate the SignalEvents based on market data. 73 | """ 74 | sym = self.symbol_list[0] 75 | dt = self.datetime_now 76 | 77 | if event.type == 'MARKET': 78 | self.bar_index += 1 79 | if self.bar_index > 5: 80 | lags = self.bars.get_latest_bars_values( 81 | self.symbol_list[0], "returns", N=3 82 | ) 83 | pred_series = pd.Series( 84 | { 85 | 'Lag1': lags[1]*100.0, 86 | 'Lag2': lags[2]*100.0 87 | } 88 | ) 89 | pred = self.model.predict(pred_series) 90 | if pred > 0 and not self.long_market: 91 | self.long_market = True 92 | signal = SignalEvent(1, sym, dt, 'LONG', 1.0) 93 | self.events.put(signal) 94 | 95 | if pred < 0 and self.long_market: 96 | self.long_market = False 97 | signal = SignalEvent(1, sym, dt, 'EXIT', 1.0) 98 | self.events.put(signal) 99 | 100 | 101 | if __name__ == "__main__": 102 | csv_dir = '/Users/djunh/programming/backtester/csv' 103 | symbol_list = ['SPY'] 104 | initial_capital = 100000.0 105 | heartbeat = 0.0 106 | start_date = datetime.datetime(2006,1,3) 107 | 108 | backtest = Backtest( 109 | csv_dir, symbol_list, initial_capital, heartbeat, 110 | start_date, HistoricCSVDataHandler, SimulatedExecutionHandler, 111 | Portfolio, SPYDailyForecastStrategy 112 | ) 113 | backtest.simulate_trading() -------------------------------------------------------------------------------- /strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sun Nov 15 09:03:45 2015 4 | 5 | @author: djunh 6 | """ 7 | from __future__ import print_function 8 | 9 | from abc import ABCMeta, abstractmethod 10 | import datetime 11 | try: 12 | import Queue as queue 13 | except ImportError: 14 | import queue 15 | 16 | import numpy as np 17 | import pandas as pd 18 | 19 | from event import SignalEvent 20 | 21 | 22 | class Strategy(object): 23 | """ 24 | Strategy is an abstract base class providing an interface for 25 | all subsequent (inherited) strategy handling objects. 26 | 27 | The goal of a (derived) Strategy object is to generate Signal 28 | objects for particular symbols based on the inputs of Bars 29 | (OHLCV) generated by a DataHandler object. 30 | 31 | This is designed to work both with historic and live data as 32 | the Strategy object is agnostic to where the data came from, 33 | since it obtains the bar tuples from a queue object. 34 | """ 35 | 36 | __metaclass__ = ABCMeta 37 | 38 | @abstractmethod 39 | def calculate_signals(self): 40 | """ 41 | Provides the mechanisms to calculate the list of signals. 42 | """ 43 | raise NotImplementedError("Should implement calculate_signals()") -------------------------------------------------------------------------------- /strategy.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djunh1/event-driven-backtesting/9e869db801403af622145d35f798cdef3fd94c1e/strategy.pyc --------------------------------------------------------------------------------