├── LICENSE ├── examples.py ├── README.md ├── utils.py └── backtesting.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abhay Pawar 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 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | from backtesting import trading_env, trade, trade_benchmark 2 | from utils import get_ohlc_data 3 | 4 | wait_period = 15 5 | sell_thresh = 0.2 6 | stop_loss_thresh = 0.2 7 | min_prob_buy = 0.4 8 | n_stocks_per_day = 5 9 | single_stock_exposure = 6000 10 | min_preds_rank = 10 11 | 12 | data_bt = get_ohlc_data() 13 | 14 | # Define trading enviroment which has access to the price data through data_bt 15 | env = trading_env(data_bt, commission=0, slippage=0.1) 16 | period = len(env.date_index) 17 | 18 | # trade implements a specific trading strategy. It interacts with env (trading environment) 19 | # place buy/sell orders. 20 | trade(env, period = None, wait_period = wait_period, sell_thresh = sell_thresh, 21 | stop_loss_thresh = stop_loss_thresh, min_prob_buy = min_prob_buy, n_stocks_per_day = n_stocks_per_day, 22 | single_stock_exposure = single_stock_exposure, min_preds_rank = min_preds_rank, verbose=False) 23 | 24 | # Compute the performance metrics 25 | env.compute_test_results() 26 | 27 | # For running the benchmark strategy of buy and hold 28 | env = trading_env(data_bt, commission=0, slippage=0.0) 29 | trade_benchmark(env) 30 | env.compute_test_results() 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trading_backtesting 2 | Example of implementing a backtesting framework from scratch. 3 | 4 | ### Why? 5 | Implementing backtesting is seen as something very complicated, but actually it is not! 6 | My main motivation for writing this backtesting framework from scratch was flexibility and optimization. 7 | I found it very difficult to work with existing backtesting for mutli-stocks strategies and they 8 | were pretty slow. 9 | 10 | ### Implementation details 11 | This code implements a general purpose trading environment through `trading_env` in backtesting.py. 12 | This trading enviroment has access to the OHLC price dataframe passed during initialization. You interact with it 13 | to place buy/sell orders. The enviroment keeps track of all the data during backtesting that is required for computing 14 | the final performance metrics. 15 | 16 | The `trade` function in backtesting.py implements my trading strategy which is a multi-stock strategy. 17 | 18 | ### Usage Details 19 | examples.py shows how to use these to run backtesting on my trading strategy and the benchmark strategy. 20 | 21 | ### Some random thoughts 22 | I wouldn't recommend using this code as is for your backtesting purpose. I would definitely try out existing 23 | frameworks and if they don't work, only then I would go for writing one from scratch like this. I landed on this piece of 24 | code after going through several iterations. I tried out several ways to optimize the code by vectorization and 25 | parallelization. But, ultimately the simple idea of iterating through each trading day worked the best. The final 26 | piece of code doesn't seem that complicated, but my thought process took a fairly long and winding road to reach there :D. 27 | 28 | It was fun though! And that is what matters the most. 29 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | def get_ohlc_data(): 5 | # Not implemented 6 | return () 7 | 8 | def get_pct_change(val1, val2): 9 | return (val2-val1)/val1 10 | 11 | def get_SR(monthly_returns, yearly_expected_return=0.0, row_duration=1): 12 | # row_duration = number of days between consecutive records 13 | # Generally there are 252 trading days in a year 14 | yearly_return_multiplier = (252)/row_duration 15 | yearly_expected_return_duration = yearly_expected_return/yearly_return_multiplier 16 | 17 | #monthly_returns = [(1+val)**yearly_return_multiplier-1 for val in monthly_returns] 18 | #monthly_returns = [val*yearly_return_multiplier for val in monthly_returns] 19 | numerator = np.mean(monthly_returns) - yearly_expected_return_duration 20 | sharpe_ratio = numerator/np.std(monthly_returns) 21 | sharpe_ratio = sharpe_ratio*np.sqrt(yearly_return_multiplier) 22 | 23 | return sharpe_ratio 24 | 25 | 26 | def get_sortino(monthly_returns, yearly_expected_return=0.0, row_duration=1, mar=0.0): 27 | # mar: minimim acceptable return over full duration 28 | yearly_return_multiplier = (252)/row_duration 29 | mar_duration = mar/yearly_return_multiplier 30 | yearly_expected_return_duration = yearly_expected_return/yearly_return_multiplier 31 | #monthly_returns = [(1+val)**yearly_return_multiplier-1 for val in monthly_returns] 32 | #monthly_returns = np.array(monthly_returns)*yearly_return_multiplier 33 | 34 | numerator = (np.mean(monthly_returns)-yearly_expected_return_duration) 35 | duration = len(monthly_returns) 36 | monthly_returns = [val-mar_duration for val in monthly_returns] 37 | monthly_returns = [val*val for val in monthly_returns if val < 0] 38 | down_std = np.sqrt(np.sum(monthly_returns)/duration) 39 | sortino = numerator/down_std 40 | sortino = sortino*np.sqrt(yearly_return_multiplier) 41 | 42 | return sortino 43 | 44 | def get_max_drawdown(account_vals): 45 | # given account values over a period calculates % max drawdown 46 | max_drawdown = 0 # is positive 47 | for i in range(len(account_vals)-1): 48 | dd = (account_vals[i] - min(account_vals[i+1:]))/account_vals[i] 49 | max_drawdown = max(max_drawdown, dd) 50 | 51 | return max_drawdown 52 | -------------------------------------------------------------------------------- /backtesting.py: -------------------------------------------------------------------------------- 1 | from utils import get_pct_change, get_SR, get_sortino, get_max_drawdown 2 | from datetime import date 3 | from seaborn import lineplot 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pandas as pd 7 | 8 | 9 | class trading_env(): 10 | """Trading environment which has access to daily price data and 11 | can be interacted with to place orders""" 12 | 13 | def __init__(self, backtesting_data, start_value=100000, 14 | commission=0, slippage=0.1, verbose=False): 15 | """ 16 | Parameters 17 | ---------- 18 | backtesting_data : OHLC data for all stocks 19 | start_value : Starting amount in the account 20 | commission : Commission percentage 21 | slippage : Slippage when buying and selling stocks 22 | verbose : If buying and selling orders should be printed 23 | """ 24 | 25 | # Pre-process the OHLC price data. This is written for daily data 26 | backtesting_data = backtesting_data.reset_index(drop=True) 27 | backtesting_data['date'] = backtesting_data['date'].apply( 28 | lambda x: x.date()) 29 | 30 | # Add an index column which is the day_number for each stock 31 | self.date_index = backtesting_data[['date']].drop_duplicates( 32 | ).sort_values('date').reset_index(drop=True) 33 | self.date_index['index'] = self.date_index.index 34 | self.backtesting_data = backtesting_data.merge( 35 | self.date_index, on='date', how='left') 36 | 37 | # Initialize various variables to keep track of different things during backtesting 38 | (self.index, self.curr_buy_number, self.curr_sell_number, self.buy_order_id, 39 | self.invested, self.sold) = 0, 0, 0, 0, 0, 0 40 | self.commission, self.slippage = commission*0.01, slippage*0.01 41 | self.assets, self.orders, self.settled_orders = {}, {}, {} 42 | self.start_value, self.value, self.cash = start_value, start_value, start_value 43 | 44 | # Track daily quantities to compute metrics later 45 | (self.daily_cash, self.daily_values, self.daily_investments, 46 | self.daily_divestments, self.daily_buy_number, 47 | self.daily_sell_number) = [], [], [], [], [], [] 48 | self.dates, self.indices = [], [] 49 | self.ohlc_cols = ['open', 'high', 'close', 'low'] 50 | self.verbose = verbose 51 | 52 | def get_date_given_index(self, input_index): 53 | """Returns index (day_number) given date""" 54 | return self.date_index.loc[input_index, 'date'] 55 | 56 | def get_index_given_date(self, input_date): 57 | """Returns date given index (day_number)""" 58 | return self.date_index.loc[self.date_index['date'].astype('str') == str(input_date), 59 | 'index'].values[0] 60 | 61 | def get_price(self, stock, price_col='open'): 62 | """ Returns price on current day during backtesting. 63 | Current day is determined by self.index""" 64 | return (self.backtesting_data.loc[(self.backtesting_data['stock'] == stock) & 65 | (self.backtesting_data['index'] 66 | == self.index), 67 | price_col]).values[0] 68 | 69 | def buy_stocks(self, buy_dict, buy_price=None): 70 | """Buy stocks in buy_dict. buy_dict has stocks as key and qty as values""" 71 | for stock in buy_dict: 72 | # Get price is not passed. Default behavior is to look up price 73 | if not buy_price: 74 | buy_price = self.get_price(stock)*(1+self.slippage) 75 | 76 | # Compute commission and update self.cash 77 | qty = buy_dict[stock] 78 | total_buy_price = buy_price*qty 79 | commission_amt = self.commission*total_buy_price 80 | self.cash = self.cash - commission_amt 81 | 82 | # Make an entry into self.orders and update self.assets 83 | self.orders[self.buy_order_id] = [ 84 | stock, qty, self.index, buy_price] 85 | if stock in self.assets: 86 | self.assets[stock][0] += qty 87 | self.assets[stock][1] += total_buy_price 88 | 89 | elif qty > 0: 90 | self.assets[stock] = [qty, total_buy_price] 91 | 92 | if qty > 0: 93 | # Increment buy_order_id for next buy order 94 | # Update other variables 95 | self.buy_order_id += 1 96 | self.invested += total_buy_price 97 | self.cash -= total_buy_price 98 | self.curr_buy_number += 1 99 | 100 | # Your trading code ideally shouldn't place orders which will lead to this 101 | if self.cash < 0: 102 | raise ValueError('Cash has become negative!') 103 | 104 | if self.verbose: 105 | print('Bought', stock) 106 | 107 | def sell_stocks(self, order_ids, sell_price=None): 108 | """Sell stocks based on buy_order_id.""" 109 | for order_id in order_ids: 110 | # Get info on stock and qty from buy_order_id 111 | stock = self.orders[order_id][0] 112 | qty = self.orders[order_id][1] 113 | 114 | # Get today's open price 115 | open_price = self.get_price(stock) 116 | if not sell_price: 117 | sell_price = open_price*(1-self.slippage) 118 | 119 | # Update self.assets and self.settled_orders 120 | total_sell_price = sell_price*qty 121 | left_stock_qty = self.assets[stock][0] - qty 122 | 123 | self.settled_orders[order_id] = self.orders[order_id] + \ 124 | [self.index, sell_price] 125 | del self.orders[order_id] 126 | 127 | if left_stock_qty == 0: 128 | del self.assets[stock] 129 | elif left_stock_qty < 0: 130 | raise ValueError( 131 | 'Selling too many stocks than it is possible!') 132 | elif left_stock_qty > 0: 133 | self.assets[stock][0] = left_stock_qty 134 | self.assets[stock][1] = left_stock_qty*open_price 135 | 136 | self.sold += total_sell_price 137 | self.cash += total_sell_price 138 | self.curr_sell_number += 1 139 | 140 | if self.verbose: 141 | print('Sold', stock) 142 | 143 | def get_performance_metrics(self, return_df, row_duration=1): 144 | return_df['value_prev_row'] = return_df['value'].shift() 145 | return_df.loc[0, 'value_prev_row'] = self.start_value 146 | return_df['return_rate'] = get_pct_change( 147 | return_df['value_prev_row'], return_df['value']) 148 | return_rate_mean = return_df['return_rate'].mean() 149 | sharpe_ratio = get_SR( 150 | return_df['return_rate'].to_list(), row_duration=row_duration) 151 | sortino_ratio = get_sortino( 152 | return_df['return_rate'].to_list(), row_duration=row_duration) 153 | max_dd = get_max_drawdown(self.daily_values) 154 | 155 | return return_df, return_rate_mean, sharpe_ratio, sortino_ratio, max_dd 156 | 157 | def get_todays_value(self, stock, column): 158 | # Seems redundant with get_price 159 | cond = (self.backtesting_data['stock'] == stock) & ( 160 | self.backtesting_data['index'] == self.index) 161 | today_value = self.backtesting_data[cond][column].values[0] 162 | 163 | return today_value 164 | 165 | def daily_wrap_up(self): 166 | """This function needs to run after placing all orders for the day during backtesting. 167 | It appends data like today's investments, cash, etc.""" 168 | self.daily_investments.append(self.invested) 169 | self.daily_divestments.append(self.sold) 170 | self.daily_cash.append(self.cash) 171 | self.sold, self.invested = 0, 0 172 | 173 | self.daily_buy_number.append(self.curr_buy_number) 174 | self.daily_sell_number.append(self.curr_sell_number) 175 | self.curr_buy_number, self.curr_sell_number = 0, 0 176 | 177 | invested_value = 0 178 | for stock in self.assets: 179 | close_price = self.backtesting_data.loc[(self.backtesting_data['stock'] == stock) & 180 | (self.backtesting_data['index'] 181 | == self.index), 182 | 'close'].values[0] 183 | self.assets[stock][1] = self.assets[stock][0]*close_price 184 | invested_value += self.assets[stock][0]*close_price 185 | 186 | self.value = invested_value + self.cash 187 | self.daily_values.append(self.value) 188 | 189 | curr_date = self.get_date_given_index(self.index) 190 | self.dates.append(curr_date) 191 | self.indices.append(self.index) 192 | 193 | def compute_test_results(self, verbose_end_results=True): 194 | """This function needs to be run at the end of the backtesting period to compute 195 | all the final metrics""" 196 | if self.settled_orders != {}: 197 | settled_orders_df = pd.DataFrame(self.settled_orders).transpose() 198 | settled_orders_df.columns = [ 199 | 'stock', 'qty', 'index_buy', 'buy_price', 'index_sell', 'sell_price'] 200 | settled_orders_df['return'] = (settled_orders_df['sell_price'] - 201 | settled_orders_df['buy_price'])/settled_orders_df['buy_price'] 202 | settled_orders_df['profit'] = settled_orders_df['qty']*(settled_orders_df['sell_price'] - 203 | settled_orders_df['buy_price']) 204 | return_rate_per_trade = settled_orders_df['return'].mean() 205 | return_amt_per_trade = settled_orders_df['profit'].mean() 206 | else: 207 | return_rate_per_trade = 0 208 | return_amt_per_trade = 0 209 | 210 | daily_return_df = pd.DataFrame() 211 | daily_return_df['value'] = self.daily_values 212 | daily_return_df['date'] = self.dates 213 | daily_return_df['index'] = self.indices 214 | month_duration = 20 215 | monthly_return_df = daily_return_df.loc[(daily_return_df['index'] % month_duration == 0) | 216 | (daily_return_df['index'] == daily_return_df['index'].max())] 217 | 218 | # Compute daily and monthly metrics 219 | daily_return_df, return_rate_daily, \ 220 | sharpe_ratio_daily, sortino_ratio_daily, max_drawdown = self.get_performance_metrics( 221 | daily_return_df, row_duration=1) 222 | monthly_return_df, return_rate_monthly, \ 223 | sharpe_ratio_monthly, sortino_ratio_monthly, max_dd_not_required = self.get_performance_metrics( 224 | monthly_return_df, row_duration=20) 225 | 226 | num_days = len(self.indices) 227 | num_buys = sum(self.daily_buy_number) 228 | num_buy_days = len([val for val in self.daily_buy_number if val != 0]) 229 | neg_profit_days = daily_return_df[daily_return_df['return_rate'] <= 0].shape[0] 230 | pos_profit_days = daily_return_df[daily_return_df['return_rate'] > 0].shape[0] 231 | self.daily_return_df = daily_return_df 232 | self.monthly_return_df = monthly_return_df 233 | 234 | perc_profitable_days = pos_profit_days / \ 235 | (pos_profit_days+neg_profit_days) 236 | perc_trading_days = num_buy_days/num_days 237 | avg_buys_per_tading_day = num_buys/num_buy_days 238 | avg_overall_buys_per_day = num_buys/num_days 239 | full_period_return = get_pct_change( 240 | self.daily_values[0], self.daily_values[-1]) 241 | 242 | # Put all the metrics into a dataframe. 243 | self.test_results = pd.DataFrame({'start_date': self.dates[0], 'end_date': [self.dates[-1]], 244 | # 'prob_buy_thresh': [prob_buy_thresh], 'sell_threshold': [sell_threshold], 245 | # 'stoploss_threshold': [stoploss_threshold], 'sell_window': [window], 246 | 'buy_trades': [num_buys], 'total_days': [num_days], 'neg_profit_days': [neg_profit_days], 247 | 'pos_profit_days': [pos_profit_days], 248 | 'perc_profitable_days': [perc_profitable_days], 249 | 'num_days_buy': [num_buy_days], 250 | 'perc_days_buy': [perc_trading_days], 251 | 'buys_per_tading_day': [avg_buys_per_tading_day], 252 | 'overall_buys_per_day': [avg_overall_buys_per_day], 253 | 'return_per_trade': [return_rate_per_trade], 254 | 'avg_daily_return': [return_rate_daily], 255 | 'avg_monthly_return': [return_rate_monthly], 256 | 'full_period_return': [full_period_return], 257 | 'sharpe_ratio_daily': [sharpe_ratio_daily], 258 | 'sortino_ratio_daily': [sortino_ratio_daily], 259 | 'sharpe_ratio_monthly': [sharpe_ratio_monthly], 260 | 'sortino_ratio_monthly': [sortino_ratio_monthly], 261 | 'max_drawdwon': [max_drawdown] 262 | }) 263 | 264 | # Print all the plots to visualize what the trading strategy did 265 | if verbose_end_results: 266 | print('Avg investments daily', np.mean(self.daily_investments)) 267 | print('Duration in years:', num_days/245) 268 | print('Returns per trade %1.4f' % return_rate_per_trade) 269 | print('Avg monthly return:', return_rate_monthly) # per sell trade 270 | print('Full period return:', full_period_return) 271 | print(month_duration, 'day Sharpe Ratio:', sharpe_ratio_monthly) 272 | print(month_duration, 'day Sortino Ratio:', sortino_ratio_monthly) 273 | print('Max drawdown:', max_drawdown) 274 | 275 | plt.figure() 276 | plt.title('Daily cash') 277 | lineplot(x=range(len(self.daily_cash)), y=self.daily_cash) 278 | plt.show() 279 | 280 | plt.figure() 281 | plt.title('Daily investments') 282 | lineplot(x=range(len(self.daily_investments)), 283 | y=self.daily_investments) 284 | plt.show() 285 | 286 | profits_daily = ( 287 | daily_return_df['value']-daily_return_df['value_prev_row']).to_list() 288 | plt.title('Daily profits') 289 | lineplot(x=range(len(profits_daily)), y=profits_daily) 290 | plt.show() 291 | 292 | plt.figure() 293 | plt.title('Value of portfolio') 294 | lineplot(x=range(len(self.daily_values)), y=self.daily_values) 295 | plt.show() 296 | 297 | plt.figure() 298 | plt.title('Avg. daily returns') 299 | lineplot( 300 | x=range(len(daily_return_df['return_rate'])), y=daily_return_df['return_rate']) 301 | plt.show() 302 | 303 | plt.figure() 304 | plt.title('Avg. monthly returns') 305 | lineplot( 306 | x=range(len(monthly_return_df['return_rate'])), y=monthly_return_df['return_rate']) 307 | plt.show() 308 | 309 | return self.test_results 310 | 311 | 312 | def trade(env, period=None, wait_period=10, sell_thresh=0.1, stop_loss_thresh=0.1, 313 | min_prob_buy=0.25, n_stocks_per_day=5, single_stock_exposure=10000, min_preds_rank=10, 314 | daily_limit=20000, verbose=False): 315 | """Function that interacts with above trading environment 316 | and actually implements the trading strategy""" 317 | if not period: 318 | period = len(env.date_index) 319 | 320 | # Iterate over all the days in backtesting price data 321 | for index in range(period): 322 | env.index = index 323 | 324 | # Check if any of the bought stocks need to be sold 325 | sell_orders = [] 326 | curr_orders = env.orders.copy() 327 | for order in curr_orders: 328 | stock = curr_orders[order][0] 329 | buy_price = curr_orders[order][3] 330 | 331 | # Check if they hit the stop_loss or take profit limits. If yes, then sell 332 | if index > 0: 333 | stock_index_cond = ((env.backtesting_data['index'] == index-1) 334 | & (env.backtesting_data['stock'] == stock)) 335 | min_price = env.backtesting_data[stock_index_cond][[ 336 | 'open', 'close']].min(axis=1).values[0] 337 | 338 | stock_index_cond = ( 339 | env.backtesting_data['index'] == index-1) & (env.backtesting_data['stock'] == stock) 340 | max_price = env.backtesting_data[stock_index_cond][[ 341 | 'open', 'close']].max(axis=1).values[0] 342 | 343 | if min_price < (1-stop_loss_thresh)*buy_price: 344 | env.sell_stocks([order], sell_price=( 345 | 1-stop_loss_thresh)*buy_price) 346 | sell_orders.append(order) 347 | if verbose: 348 | print('Sold at stop loss') 349 | elif max_price >= (1+sell_thresh)*buy_price: 350 | env.sell_stocks([order], sell_price=( 351 | 1+sell_thresh)*buy_price) 352 | sell_orders.append(order) 353 | if verbose: 354 | print('Sold at threshold profit') 355 | 356 | # Check if bought stocks are at the end of hold period. If yes, sell. 357 | if curr_orders[order][2]+wait_period == index and order not in sell_orders: 358 | env.sell_stocks([order]) 359 | if verbose: 360 | print('Sold at end of period') 361 | 362 | # Code to determine today's investment based on strategy 363 | num_last_10_day_invested = wait_period - sum([1 for val in env.daily_investments[-wait_period:] 364 | if val == 0]) 365 | 366 | if num_last_10_day_invested < 4: 367 | num_last_10_day_invested = 4 368 | 369 | todays_investment = (env.value/num_last_10_day_invested)*1.25 370 | 371 | if todays_investment > env.cash: 372 | todays_investment = env.cash 373 | elif todays_investment > daily_limit: 374 | todays_investment = daily_limit 375 | 376 | high_investment_stocks = [ 377 | key for key in env.assets if env.assets[key][1] > single_stock_exposure] 378 | eligible_stock_cond = (env.backtesting_data['index'] == index-1) & (env.backtesting_data['y_pred'] >= min_prob_buy)\ 379 | & (~env.backtesting_data['stock'].isin(high_investment_stocks)) 380 | eligible_stocks = env.backtesting_data[eligible_stock_cond].sort_values( 381 | 'y_pred', ascending=False) 382 | n_stocks_today = min(n_stocks_per_day, len(eligible_stocks)) 383 | stock_bought = 0 384 | stock_rank = 0 385 | 386 | # Buy stocks 387 | for stock in eligible_stocks['stock'].to_list(): 388 | if stock_rank >= min_preds_rank: 389 | break 390 | today_price = env.get_todays_value(stock, 'open') 391 | if stock_bought < n_stocks_today: 392 | investment_per_stock = todays_investment / \ 393 | (n_stocks_today-stock_bought) 394 | else: 395 | investment_per_stock = todays_investment 396 | 397 | actual_today_price = today_price*(1+env.commission+env.slippage) 398 | num_to_buy = int(investment_per_stock/actual_today_price) 399 | if num_to_buy > 0: 400 | env.buy_stocks({stock: num_to_buy}) 401 | 402 | stock_bought += 1 403 | todays_investment -= num_to_buy*actual_today_price 404 | if verbose: 405 | print(stock, actual_today_price, num_to_buy, 406 | todays_investment, env.cash) 407 | 408 | stock_rank += 1 409 | 410 | # buy list: 411 | # 1. top stocks above a threshold 412 | # 2. decide how much to invest today 413 | # 3. go through stocks one by one and invest today_amt/n value 414 | # 4. skip above if threshold amt invested in that stock 415 | # 5. stop when top_amt is exhausted 416 | 417 | # Append metrics for the day in the trading environment 418 | env.daily_wrap_up() 419 | return env 420 | 421 | 422 | def trade_parallelize(inputs): 423 | """Wrapper around trade() for easy parallelization when testing for different sets of parameters""" 424 | # for HPT of trading parameters 425 | wait_period = inputs[0] 426 | sell_thresh = inputs[1] 427 | stop_loss_thresh = inputs[2] 428 | min_prob_buy = inputs[3] 429 | n_stocks_per_day = inputs[4] 430 | single_stock_exposure = inputs[5] 431 | min_preds_rank = inputs[6] 432 | data_bt = inputs[7] 433 | 434 | env = trading_env(data_bt, commission=0, slippage=0.1) 435 | period = len(env.date_index) # len(env.date_index) 436 | 437 | trade(env, period=period, wait_period=wait_period, sell_thresh=sell_thresh, 438 | stop_loss_thresh=stop_loss_thresh, min_prob_buy=min_prob_buy, n_stocks_per_day=n_stocks_per_day, 439 | single_stock_exposure=single_stock_exposure, min_preds_rank=min_preds_rank, verbose=False) # slippage 440 | 441 | results = env.compute_test_results(verbose_end_results=False) 442 | results['wait_period'] = inputs[0] 443 | results['sell_thresh'] = inputs[1] 444 | results['stop_loss_thresh'] = inputs[2] 445 | results['min_prob_buy'] = inputs[3] 446 | results['n_stocks_per_day'] = inputs[4] 447 | results['single_stock_exposure'] = inputs[5] 448 | results['min_preds_rank'] = inputs[6] 449 | 450 | return results 451 | 452 | 453 | def trade_benchmark(env, period=None, wait_period=10, sell_thresh=0.1, stop_loss_thresh=0.1, 454 | min_prob_buy=0.25, n_stocks_per_day=5, single_stock_exposure=10000, min_preds_rank=10, 455 | daily_limit=20000, verbose=False): 456 | """Benchmark trading strategy of buying and holding till end of period""" 457 | if not period: 458 | period = len(env.date_index) 459 | 460 | for index in range(period): 461 | env.index = index 462 | 463 | if index == 0: 464 | todays_data = env.backtesting_data[env.backtesting_data['index'] == index].sort_values( 465 | 'open', ascending=False) 466 | num_stocks_left = len(todays_data) 467 | for stock in todays_data['stock'].to_list(): 468 | cash_left = env.cash 469 | per_stock_amt = (cash_left/num_stocks_left) 470 | open_price = todays_data[todays_data['stock'] 471 | == stock]['open'].values[0] 472 | num_to_buy = int(per_stock_amt/open_price) 473 | 474 | # if num_to_buy>0: 475 | env.buy_stocks({stock: num_to_buy}) 476 | num_stocks_left -= 1 477 | 478 | env.daily_wrap_up() 479 | --------------------------------------------------------------------------------