├── README.md ├── _config.yml ├── modular blank.py └── modular_blank_TSMM.py /README.md: -------------------------------------------------------------------------------- 1 | # Modular_IB 2 | Modular IB_Insync strategy -- 3 | This is mean't to be used as a base framework to automate trading of multiple instruments in Python with Interactive Brokers. 4 | Simply fill in entry logic, add indicators or filters to the DF's, and select an interval -- and hit run. 5 | 6 | Issue -- If logging does not work in spyder output, comment out the if statement in log function. 7 | 8 | Best of luck! 9 | 10 | Zach 11 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /modular blank.py: -------------------------------------------------------------------------------- 1 | """ 2 | Live Trading with IB Modular Design 3 | Version 1.0.0 4 | Platform: IB API and TWS 5 | By: Zach Oakes 6 | 7 | 8 | Revision Notes: 9 | 1.0.0 (07/30/2019) - Initial 10 | 11 | Useful References: 12 | 13 | To do... 14 | trailing stop logic 15 | position sizing function 16 | why does df.tail print 2x on bar update? line 137 17 | 18 | """ 19 | ############################################################################### 20 | # Import required libraries 21 | import calendar 22 | import datetime 23 | from ib_insync import * 24 | from ibapi import * 25 | import logging 26 | import numpy as np 27 | import pandas as pd 28 | import pytz 29 | import sys 30 | import time 31 | 32 | 33 | #import asyncio 34 | #import dateutil.tz 35 | #import json 36 | #import requests 37 | #import threading 38 | #import traceback 39 | 40 | ############################################################################### 41 | # Required variables for the algo 42 | # Set to True when using Spyder 43 | USING_NOTEBOOK = True 44 | 45 | # Set timezone for your TWS setup 46 | TWS_TIMEZONE = pytz.timezone('US/Central') 47 | 48 | 49 | ############################################################################### 50 | class IBAlgoStrategy(object): 51 | """ 52 | Algorithmic Strategy for Interactive Brokers. 53 | """ 54 | def __init__(self): 55 | """Initialize algo""" 56 | # Setup logger 57 | # https://docs.python.org/3.6/library/logging.html# 58 | # logging levels: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET 59 | self.logger = logging.getLogger(__name__) 60 | self.logger.setLevel(logging.INFO) 61 | self.handler = logging.FileHandler('IBAlgoStrategy.log') 62 | self.handler.setLevel(logging.INFO) 63 | self.formatter = logging.Formatter( 64 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 65 | self.handler.setFormatter(self.formatter) 66 | self.logger.addHandler(self.handler) 67 | self.logger.info('Starting log at {}'.format(datetime.datetime.now())) 68 | 69 | # Create IB connection 70 | self.ib = self.connect() 71 | 72 | # Create empty list of instruments, bars, and indicators to track 73 | self.instruments = [] 74 | self.bars = [] 75 | self.bars_minutes = [] # keep list of bar minutes as ints 76 | self.indicators = [] 77 | 78 | # Create empty dictionary of DataFrames for instrument bars 79 | self.dfs = {} 80 | 81 | 82 | ############################################################################### 83 | def run(self): 84 | """Run logic for live trading""" 85 | # Get today's trading hours 86 | self.get_trading_hours() 87 | 88 | # Wait until the markets open 89 | open_hr = int(self.exchange_open[:2]) 90 | open_min = int(self.exchange_open[-2:]) 91 | self.log('Waiting until the market opens ({}:{} ET)...'.format( 92 | open_hr, open_min)) 93 | while True: 94 | now = datetime.datetime.now(tz=pytz.timezone('US/Eastern')) 95 | if ((now.minute >= open_min) and (now.hour >= open_hr)) or \ 96 | (now.hour >= open_hr+1): 97 | break 98 | 99 | # Sleep 5 seconds 100 | self.ib.sleep(5) 101 | 102 | # Exchange is now open 103 | self.log() 104 | self.log('The market is now open {}'.format(now)) 105 | self.start_time = time.time() 106 | 107 | # Set time reference 9:30AM ET for today / 13:30 UTC time 108 | # Used for determining when new bars are available 109 | self.time_ref = calendar.timegm(time.strptime( 110 | now.strftime('%Y-%m-%d') + ' 13:30:00', '%Y-%m-%d %H:%M:%S')) 111 | 112 | # Run loop during exhange hours 113 | while True: 114 | 115 | # Check for new intraday bars 116 | if len(self.bars_minutes)>0: 117 | for minutes in self.bars_minutes: 118 | time_since_open = time.time() - self.time_ref 119 | if 60*minutes-(time_since_open%(60*minutes)) <= 5: 120 | # Time to update 'minutes' bar for all instruments 121 | if minutes == 1: 122 | bar = '1 min' 123 | elif minutes < 60: 124 | bar = str(minutes) + ' mins' 125 | elif minutes == 60: 126 | bar = '1 hour' 127 | else: 128 | hours = minutes/60 129 | bar = str(hours) + ' hours' 130 | # Loop through all instruments and update bar dfs 131 | for instrument in self.instruments: 132 | # Get current df for instrument/bar 133 | df = self.dfs[instrument][bar] 134 | # Update instrument/bar df 135 | df = self.update_bar(df, instrument, bar) 136 | self.dfs[instrument][bar] = df 137 | self.log("Updated {}'s {} df".format( 138 | instrument.symbol, bar)) 139 | # Process signals (new bar) 140 | self.on_data() 141 | 142 | # To do 143 | # Need to do some trailing stop work here... 144 | 145 | # Sleep 146 | #self.ib.sleep(5) #Speed up the runtime 147 | 148 | self.log('Algo no longer running for the day.') 149 | 150 | 151 | ############################################################################### 152 | def on_data(self): 153 | """Process signals for new bar""" 154 | self.log('on_data()') 155 | # Loop through instruments 156 | for instrument in self.instruments: 157 | # Check for any open orders 158 | #open_orders = self.get_open_orders(instrument) 159 | 160 | # Get current qty 161 | qty = self.get_quantity(instrument) 162 | self.log('Current qty for {}: {}'.format(instrument.symbol, qty)) 163 | 164 | # Process current long position 165 | if qty > 0: 166 | # Check for short entry signal 167 | if self.long_entry_signal(instrument): 168 | # Reverse position and go short 169 | self.go_short(instrument) 170 | 171 | # Check for long exit signal 172 | elif self.long_exit_signal(instrument): 173 | # Go flat 174 | self.go_flat(instrument) 175 | 176 | # Process current short position 177 | elif qty < 0: 178 | # Check for long entry signal 179 | if self.long_entry_signal(instrument): 180 | # Reverse position and go long 181 | self.go_long(instrument) 182 | 183 | # Check for short exit signal 184 | elif self.short_exit_signal(instrument): 185 | # Go flat 186 | self.go_flat(instrument) 187 | 188 | # Check for entry signal 189 | else: 190 | # Check for long entry signal 191 | if self.long_entry_signal(instrument): 192 | # Go long 193 | self.go_long(instrument) 194 | # Check for short entry signal 195 | elif self.short_entry_signal(instrument): 196 | # Go short 197 | self.go_short(instrument) 198 | 199 | 200 | 201 | ############################################################################### 202 | def long_entry_signal(self, instrument): 203 | """ 204 | Check for LONG entry signal for the instrument. 205 | Returns True or False. 206 | """ 207 | # Always use the first bar for this signal 208 | bar = self.bars[0] 209 | 210 | # Get intrument/bar df 211 | df = self.dfs[instrument][bar] 212 | 213 | # Get last close, indicators 214 | 215 | 216 | # Entry logic 217 | 218 | 219 | 220 | ############################################################################### 221 | def long_exit_signal(self, instrument): 222 | """ 223 | Check for LONG exit signal for the instrument. 224 | Returns True or False. 225 | """ 226 | # Always use the first bar for this signal 227 | bar = self.bars[0] 228 | 229 | # Get intrument/bar df 230 | df = self.dfs[instrument][bar] 231 | 232 | # Get last close, indicators 233 | price = df['close'].iloc[-1] 234 | 235 | 236 | # Exit logic 237 | if (condition): 238 | self.log('{} LONG EXIT SIGNAL: price={}, PSAR={}, RSI={}'.format( 239 | instrument.symbol, price, PSAR, RSI)) 240 | return True 241 | 242 | 243 | 244 | ############################################################################### 245 | def short_entry_signal(self, instrument): 246 | """ 247 | Check for SHORT entry signal for the instrument. 248 | Returns True or False. 249 | """ 250 | # Always use the first bar for this signal 251 | bar = self.bars[0] 252 | 253 | # Get intrument/bar df 254 | df = self.dfs[instrument][bar] 255 | 256 | # Get last close, indicators 257 | price = df['close'].iloc[-1] 258 | 259 | 260 | # Entry logic 261 | if condition: 262 | self.log('{} SHORT ENTRY SIGNAL: price={}, PSAR={}, RSI={}'.format( 263 | instrument.symbol, price, PSAR, RSI)) 264 | return True 265 | return False 266 | 267 | 268 | ############################################################################### 269 | def short_exit_signal(self, instrument): 270 | """ 271 | Check for SHORT exit signal for the instrument. 272 | Returns True or False. 273 | """ 274 | # Always use the first bar for this signal 275 | bar = self.bars[0] 276 | 277 | # Get intrument/bar df 278 | df = self.dfs[instrument][bar] 279 | 280 | # Get last close, PSAR, and RSI 281 | price = df['close'].iloc[-1] 282 | 283 | 284 | # Exit logic 285 | if condition: 286 | self.log('{} SHORT EXIT SIGNAL: price={}, PSAR={}, RSI={}'.format( 287 | instrument.symbol, price, PSAR, RSI)) 288 | return True 289 | return False 290 | 291 | 292 | ############################################################################### 293 | def go_long(self, instrument): 294 | """Go LONG instrument""" 295 | # Get current qty 296 | qty = self.get_quantity(instrument) 297 | # Get desired qty - no function for this currently 298 | desired_qty = 10 299 | # Get order quantity 300 | order_qty = desired_qty-qty 301 | # Place market order to go long 302 | self.market_order(instrument, 'BUY', abs(order_qty)) 303 | 304 | 305 | ############################################################################### 306 | def go_short(self, instrument): 307 | """Go SHORT instrument""" 308 | # Get current qty 309 | qty = self.get_quantity(instrument) 310 | # Get desired qty - no function for this currently 311 | desired_qty = -10 312 | # Get order quantity 313 | order_qty = desired_qty-qty 314 | # Place market order to go short 315 | self.market_order(instrument, 'SELL', abs(order_qty)) 316 | 317 | 318 | ############################################################################### 319 | def go_flat(self, instrument): 320 | """Go FLAT instrument""" 321 | # Get current qty 322 | qty = self.get_quantity(instrument) 323 | # Place market order to go flat 324 | if qty > 0: 325 | action = 'SELL' 326 | elif qty < 0: 327 | action = 'BUY' 328 | else: 329 | self.log('{} already FLAT.'.format(instrument.symbol)) 330 | return 331 | # Place market order to go flat 332 | self.market_order(instrument, action, abs(qty)) 333 | 334 | 335 | ############################################################################### 336 | def get_open_orders(self, instrument): 337 | """ 338 | Checks for any open orders for instrument. 339 | Returns True or False. 340 | """ 341 | # Verify open orders match open trades 342 | for i in range(10): 343 | open_trades = list(self.ib.openTrades()) 344 | trade_ids = set([t.order.orderId for t in open_trades]) 345 | open_orders = list(self.ib.reqOpenOrders()) 346 | order_ids = set([o.orderId for o in open_orders]) 347 | missing = order_ids.difference(trade_ids) 348 | if len(missing) == 0 and len(open_trades) > 0: 349 | break 350 | 351 | # Return True if any open trade is for instrument, otherwise False 352 | for trade in open_trades: 353 | if instrument.symbol == trade.contract.localSymbol: 354 | return True 355 | return False 356 | 357 | 358 | ############################################################################### 359 | def get_quantity(self, instrument): 360 | """Returns the current quantity held for instrument""" 361 | # Get instrument type 362 | if str(type(instrument))[-7:-2] == 'Stock': 363 | instrument_type = 'Stock' 364 | elif str(type(instrument))[-7:-2] == 'Option': 365 | instrument_type = 'Option' 366 | elif str(type(instrument))[-7:-2] == 'Future': 367 | instrument_type = 'Future' 368 | else: 369 | raise ValueError('Invalid instrument type ({}) for ' 370 | 'current_quantity'.format(type(instrument))) 371 | 372 | # Loop through all current positions 373 | for position in self.ib.positions(): 374 | # Verify position is for instrument 375 | contract = position.contract 376 | if instrument_type == 'Stock': 377 | try: 378 | if contract.secType == 'STK' and \ 379 | contract.localSymbol == instrument.symbol: 380 | return abs(int(position.position)) 381 | except: 382 | continue 383 | elif instrument_type == 'Option': 384 | pass 385 | elif instrument_type == 'Future': 386 | pass 387 | 388 | return 0 389 | 390 | 391 | ############################################################################### 392 | def market_order(self, instrument, action, qty): 393 | """Place market order for instrument""" 394 | # Verify action 395 | if action != 'BUY' and action != 'SELL': 396 | raise ValueError("Invalid action () for market order. Must be " 397 | "'BUY' or 'SELL'.".format(action)) 398 | 399 | market_order = MarketOrder( 400 | action=action, 401 | totalQuantity=float(qty) 402 | ) 403 | self.log('{}ING {} units of {} at MARKET'.format( 404 | action, qty, instrument.symbol)) 405 | self.ib.placeOrder(instrument, market_order) 406 | 407 | 408 | ############################################################################### 409 | def limit_order(self, instrument, action, qty, limit_price): 410 | """Place limit order for instrument""" 411 | # Verify action 412 | if action != 'BUY' and action != 'SELL': 413 | raise ValueError("Invalid action () for market order. Must be " 414 | "'BUY' or 'SELL'.".format(action)) 415 | 416 | limit_order = LimitOrder( 417 | action=action, 418 | totalQuantity=float(qty), 419 | lmtPrice=float(limit_price) 420 | ) 421 | self.log('{}ING {} units of {} at {} LIMIT'.format( 422 | action, qty, instrument.symbol, limit_price)) 423 | self.ib.placeOrder(instrument, limit_order) 424 | 425 | 426 | ############################################################################### 427 | def connect(self): 428 | """Connect to Interactive Brokers TWS""" 429 | self.log('Connecting to Interactive Brokers TWS...') 430 | try: 431 | if USING_NOTEBOOK: 432 | util.startLoop() 433 | ib = IB() 434 | ib.connect('127.0.0.1', 7497, clientId=1) 435 | self.log('Connected') 436 | self.log() 437 | return ib 438 | except: 439 | self.log('Error in connecting to TWS!! Exiting...') 440 | self.log(sys.exc_info()[0]) 441 | exit(-1) 442 | 443 | ############################################################################### 444 | def log(self, msg=""): 445 | """Add log to output file""" 446 | self.logger.info(msg) 447 | if USING_NOTEBOOK: 448 | print(msg) 449 | 450 | 451 | ############################################################################### 452 | def get_trading_hours(self): 453 | """Get today's trading hours for algo's instruments""" 454 | open_time = None 455 | close_time = None 456 | 457 | # Get todays date in YYYYMMDD format 458 | today = datetime.datetime.today().strftime('%Y%m%d') 459 | 460 | # Loop through instruments 461 | for instrument in self.instruments: 462 | # Get contract details 463 | contract_details = self.ib.reqContractDetails(instrument)[0] 464 | # Get regular trading hours for today 465 | trading_hours_list = contract_details.liquidHours.split(';') 466 | for item in trading_hours_list: 467 | if item[:8] == today: 468 | # Update open time 469 | if open_time is None: 470 | open_time = item.split('-')[0].split(':')[1] 471 | else: 472 | if item.split('-')[0].split(':')[1] < open_time: 473 | open_time = item.split('-')[0].split(':')[1] 474 | # Update close time 475 | if close_time is None: 476 | close_time = item.split('-')[1].split(':')[1] 477 | else: 478 | if item.split('-')[1].split(':')[1] > close_time: 479 | close_time = item.split('-')[1].split(':')[1] 480 | break 481 | 482 | # Save earliest start time 483 | self.exchange_open = open_time 484 | # Save latest end time 485 | self.exchange_close = close_time 486 | self.log() 487 | self.log("Today's exchange hours are {}-{}".format( 488 | self.exchange_open, self.exchange_close)) 489 | 490 | 491 | ############################################################################### 492 | def add_instrument(self, instrument_type, ticker, last_trade_date=None, 493 | strike=None, option_type=None, routing='SMART', 494 | currency='USD', primary_exchange='NASDAQ', 495 | future_exchange='GLOBEX'): 496 | """ 497 | Add instrument to trade to algo. 498 | https://github.com/erdewit/ib_insync/blob/master/ib_insync/contract.py 499 | """ 500 | if instrument_type == 'Stock': 501 | instrument = Stock(ticker, routing, currency, 502 | primaryExchange=primary_exchange) 503 | elif instrument_type == 'Option': 504 | # Verify option type 505 | valid_option_types = ['C', 'P', 'CALL', 'PUT'] 506 | if option_type not in valid_option_types: 507 | raise ValueError( 508 | 'Invalid option_type: {}. Must be in {}'.format( 509 | option_type, valid_option_types)) 510 | instrument = Option(ticker, last_trade_date, float(strike), 511 | option_type, routing) 512 | elif instrument_type == 'Future': 513 | instrument = Future(ticker, last_trade_date, future_exchange) 514 | else: 515 | raise ValueError( 516 | "Invalid instrument type: {}".format(instrument_type)) 517 | 518 | # Append instrument to algo list 519 | self.ib.qualifyContracts(instrument) 520 | self.instruments.append(instrument) 521 | 522 | # Create dictionary for instrument bars 523 | self.dfs[instrument] = {} 524 | 525 | 526 | ############################################################################### 527 | def add_bar(self, bar): 528 | """Add bar to algo""" 529 | # https://interactivebrokers.github.io/tws-api/historical_bars.html 530 | valid_bars = ['1 min','2 mins','3 mins','10 mins','20 mins','30 mins', 531 | '1 hour','2 hours','3 hours','4 hours','8 hours', 532 | '1 day','1 week','1 month'] 533 | # Verify bar size 534 | if bar not in valid_bars: 535 | raise ValueError('Invalid bar: {}. Must be in {}'.format 536 | (bar, valid_bars)) 537 | # Append bar to algo list 538 | self.bars.append(bar) 539 | 540 | # Get bar minutes (when applicable) 541 | if bar[-3:] == 'min' or bar[-4:] == 'mins': 542 | # min bar 543 | bar_minutes = int(bar.split(' ')[0]) 544 | self.bars_minutes.append(bar_minutes) 545 | elif bar[-4:] == 'hour' or bar[-5:] == 'hours': 546 | # hourly bar 547 | bar_minutes = int(60int(*bar.split(' ')[0])) 548 | self.bars_minutes.append(bar_minutes) 549 | 550 | # Initialize dfs for all instruments 551 | for instrument in self.instruments: 552 | # Get ohlc pandas DataFrame 553 | df = self.get_historical_data(instrument, bar) 554 | # Add indicators to df and save to algo 555 | self.dfs[instrument][bar] = self.add_indicators(df) 556 | 557 | 558 | ############################################################################### 559 | def get_historical_data(self, instrument, bar, end_date="", use_RTH=True): 560 | """ 561 | Get historical bars for instrument. 562 | https://interactivebrokers.github.io/tws-api/historical_bars.html 563 | """ 564 | # Get max duration for a given bar size 565 | if bar in ['1 day','1 week','1 month']: 566 | duration = '1 Y' # one year 567 | elif bar in ['30 mins','1 hour','2 hours','3 hours','4 hours','8 hours']: 568 | duration = '1 M' # one month 569 | elif bar in ['3 mins','10 mins','20 mins']: 570 | duration = '1 W' # one week 571 | elif bar == '2 mins': 572 | duration = '2 D' # two days 573 | elif bar == '1 min': 574 | duration = '1 D' # one day 575 | else: 576 | raise ValueError( 577 | 'Invalid bar: {} for get_historical_data()'.format(bar)) 578 | 579 | # Get historical bars 580 | bars = self.ib.reqHistoricalData( 581 | instrument, 582 | endDateTime=end_date, 583 | durationStr=duration, 584 | barSizeSetting=bar, 585 | whatToShow='TRADES', 586 | useRTH=use_RTH) # use regular trading hours 587 | 588 | # Convert bars to a pandas dataframe 589 | hist = util.df(bars) 590 | 591 | # Make the 'date' column the index 592 | hist.set_index('date', inplace=True) 593 | 594 | # Remove the 'volume', 'barCount', and 'average' columns 595 | hist.drop(columns=['volume', 'barCount', 'average'], inplace=True) 596 | 597 | # Add TWS time zone to datetimes 598 | hist = hist.tz_localize(TWS_TIMEZONE) 599 | 600 | return hist 601 | 602 | 603 | ############################################################################### 604 | def update_bar(self, df, instrument, bar, end_date="", use_RTH=True): 605 | """ 606 | Update historical bars for instrument df. 607 | """ 608 | # Set duration for 1 day 609 | duration = '1 D' # one day 610 | 611 | # Get historical bars 612 | bars = self.ib.reqHistoricalData( 613 | instrument, 614 | endDateTime=end_date, 615 | durationStr=duration, 616 | barSizeSetting=bar, 617 | whatToShow='TRADES', 618 | useRTH=use_RTH) # use regular trading hours 619 | 620 | # Convert bars to a pandas dataframe 621 | hist = util.df(bars) 622 | 623 | # Make the 'date' column the index 624 | hist.set_index('date', inplace=True) 625 | 626 | # Remove the 'volume', 'barCount', and 'average' columns 627 | hist.drop(columns=['volume', 'barCount', 'average'], inplace=True) 628 | 629 | # Add TWS time zone to datetimes 630 | hist = hist.tz_localize(TWS_TIMEZONE) 631 | 632 | # Check if hist last date != df last date 633 | if hist.index[-1] != df.index[-1]: 634 | # Append new bar to df 635 | try: 636 | df = df.append(hist, sort=True) 637 | except: 638 | df = df.append(hist) 639 | # Add indicators to df 640 | df = self.add_indicators(df) 641 | 642 | return df 643 | 644 | 645 | ############################################################################### 646 | def add_indicators(self, df): 647 | """Add technical indicators to pandas DataFrame""" 648 | # Check for RSI in indicators 649 | if 'RSI' in self.indicators: 650 | # Add RSI to df 651 | df = self.get_RSI(df) 652 | # Check for Parabolic SAR in indicators 653 | if 'SAR' in self.indicators: 654 | # Add SAR to df 655 | df = self.get_SAR(df) 656 | # Check for ATR in indicators 657 | if 'ATR' in self.indicators: 658 | df = self.get_ATR(df) 659 | 660 | return df 661 | 662 | 663 | ############################################################################### 664 | def add_RSI(self, length, alpha): 665 | """Add the RSI indicator to the list of indicators.""" 666 | # Verify correct alpha input 667 | valid_alpha_list = ['Wilders', 'Standard'] 668 | if alpha not in valid_alpha_list: 669 | raise ValueError('Invalid RSI alpha input ({}). Must be {}'.format( 670 | alpha, alpha_list)) 671 | # Save RSI input parameters to algo 672 | self.RSI_length = length 673 | self.RSI_alpha = alpha 674 | # Add RSI to list of indicators 675 | self.indicators.append('RSI') 676 | 677 | 678 | ############################################################################### 679 | def get_RSI(self, df): 680 | """Add the RSI calculations to pandas DataFrame""" 681 | # Using Wilder's EMA: alpha=1/n instead of standard alpha = 2/(n+1) 682 | df['delta'] = df['close'].diff() 683 | df['gain'], df['loss'] = df['delta'].copy(), df['delta'].copy() 684 | df['gain'][df['gain']<0] = 0 685 | df['loss'][df['loss']>0] = 0 686 | 687 | # Get alpha for RSI EMA 688 | if self.RSI_alpha == 'Wilders': # alpha=1/n 689 | alpha = float(1.0/self.RSI_length) 690 | elif self.RSI_alpha == 'Standard': 691 | alpha = float(2.0/(self.RSI_length+1)) 692 | 693 | # Calculate the avg gain and loss 694 | df['avg_gain'] = df['gain'].ewm(alpha=alpha).mean() 695 | df['avg_loss'] = df['loss'].ewm(alpha=alpha).mean() 696 | 697 | # Calculate RS and RSI 698 | df['RS'] = abs(df['avg_gain']/df['avg_loss']) 699 | df['RSI'] = 100.0-(100.0/(1+df['RS'])) 700 | return df 701 | 702 | 703 | ############################################################################### 704 | def add_SAR(self, af, af_max): 705 | """Add the Parabolic SAR indicator to the list of indicators.""" 706 | # Save SAR input parameters to algo 707 | self.SAR_af = af 708 | self.SAR_af_max = af_max 709 | # Add SAR to list of indicators 710 | self.indicators.append('SAR') 711 | 712 | 713 | ############################################################################### 714 | def get_SAR(self, df): 715 | """ 716 | Add the Parabolic SAR indicator to pandas DataFrame 717 | ref: https://school.stockcharts.com/doku.php?id=technical_indicators:parabolic_sar 718 | ref: https://stackoverflow.com/questions/54918485/parabolic-sar-in-python-psar-keep-growing-instead-of-reversing 719 | """ 720 | # Reset index 721 | df.reset_index(inplace=True) 722 | 723 | # Initialize df columns first values 724 | df.loc[0,'AF'] = self.SAR_af 725 | df.loc[0,'PSAR'] = df.loc[0,'low'] 726 | df.loc[0,'EP'] = df.loc[0,'high'] 727 | df.loc[0,'PSAR_dir'] = "bull" 728 | 729 | # Loop through rows of df 730 | for a in range(1, len(df)): 731 | # Previous uptrend 732 | if df.loc[a-1,'PSAR_dir'] == 'bull': 733 | # Calculate the current PSAR (rising) 734 | # = previous PSAR + previous AF(previous EP-previous PSAR) 735 | df.loc[a,'PSAR'] = df.loc[a-1,'PSAR'] + \ 736 | df.loc[a-1,'AF']*(df.loc[a-1,'EP']-df.loc[a-1,'PSAR']) 737 | 738 | # Check for change in trend 739 | if df.loc[a,'low'] < df.loc[a-1,'PSAR']: 740 | # now downtrend 741 | df.loc[a,'PSAR_dir'] = "bear" 742 | df.loc[a,'PSAR'] = df.loc[a-1,'EP'] 743 | df.loc[a,'EP'] = df.loc[a-1,'low'] 744 | df.loc[a,'AF'] = self.SAR_af 745 | 746 | # Update uptrend SAR 747 | else: 748 | df.loc[a,'PSAR_dir'] = "bull" 749 | # Check for extreme point (high) 750 | if df.loc[a,'high'] > df.loc[a-1,'EP']: 751 | # Update extreme point 752 | df.loc[a,'EP'] = df.loc[a,'high'] 753 | # Increase AF if below max 754 | if df.loc[a-1,'AF'] <= self.SAR_af_max-self.SAR_af: 755 | df.loc[a,'AF'] = df.loc[a-1,'AF'] + self.SAR_af 756 | else: 757 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 758 | # No new extreme point 759 | elif df.loc[a,'high'] <= df.loc[a-1, 'EP']: 760 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 761 | df.loc[a,'EP'] = df.loc[a-1,'EP'] 762 | 763 | # Previous downtrend 764 | elif df.loc[a-1,'PSAR_dir'] == 'bear': 765 | # Calculate the current PSAR (falling) 766 | # = previous PSAR - previous AF(previous PSAR-previous EP) 767 | df.loc[a,'PSAR'] = df.loc[a-1,'PSAR'] - \ 768 | (df.loc[a-1,'AF']*(df.loc[a-1,'PSAR']-df.loc[a-1,'EP'])) 769 | 770 | # Check for change in trend 771 | if df.loc[a,'high'] > df.loc[a-1,'PSAR']: 772 | # now uptrend 773 | df.loc[a,'PSAR_dir'] = "bull" 774 | df.loc[a,'PSAR'] = df.loc[a-1,'EP'] 775 | df.loc[a,'EP'] = df.loc[a-1,'high'] 776 | df.loc[a,'AF'] = self.SAR_af 777 | 778 | # Update downtrend SAR 779 | else: 780 | df.loc[a,'PSAR_dir'] = "bear" 781 | # Check for new extreme point (low) 782 | if df.loc[a,'low'] < df.loc[a-1, 'EP']: 783 | # Update extreme point 784 | df.loc[a,'EP'] = df.loc[a, 'low'] 785 | # Increase AF if below max 786 | if df.loc[a-1,'AF'] <= self.SAR_af_max-self.SAR_af: 787 | df.loc[a,'AF'] = df.loc[a-1,'AF'] + self.SAR_af 788 | else: 789 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 790 | # No new extreme point 791 | elif df.loc[a,'low'] >= df.loc[a-1,'EP']: 792 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 793 | df.loc[a,'EP'] = df.loc[a-1,'EP'] 794 | 795 | # Reset date to be the index 796 | df.set_index('date', inplace=True) 797 | 798 | return df 799 | 800 | 801 | ############################################################################### 802 | def add_ATR(self, length): 803 | """Add the ATR indicator to the list of indicators.""" 804 | # Save ATR input parameters to algo 805 | self.ATR_length = length 806 | # Add ATR to list of indicators 807 | self.indicators.append('ATR') 808 | 809 | 810 | ############################################################################### 811 | def get_ATR(self, df): 812 | """Add the ATR calculations to pandas DataFrame""" 813 | df['tr1'] = df['high'] - df['low'] # H-L 814 | df['tr2'] = abs(df['high'] - df['close'].shift()) # abs(H-prev_close) 815 | df['tr3'] = abs(df['low'] - df['close'].shift()) # abs(L-prev_close) 816 | df['tr'] = df[['tr1', 'tr2', 'tr3']].apply(max,axis=1) #axis=1 for row max, axis=0 for column max 817 | df['atr'] = df['tr'].rolling(self.ATR_length).mean() 818 | return df 819 | 820 | 821 | ############################################################################### 822 | if __name__ == '__main__': 823 | # Create algo object 824 | algo = IBAlgoStrategy() 825 | 826 | # Add instruments to trade 827 | algo.add_instrument('Stock', 'SPY') 828 | #algo.add_instrument('Option', 'SPY', '20190920', 300, 'C') 829 | #algo.add_instrument('Future', 'ES', '20190920') 830 | 831 | # Add RSI to indicators 832 | algo.add_RSI(length=14, alpha='Wilders') 833 | 834 | # Add ATR to indicators 835 | algo.add_ATR(length=14) 836 | 837 | # Add SAR to indicators 838 | algo.add_SAR(af=0.02, af_max=0.2) 839 | 840 | # Add intraday bars 841 | #valid_bars = ['1 min','2 mins','3 mins','10 mins','20 mins','30 mins','1 hour','2 hours','3 hours','4 hours','8 hours','1 day','1 week','1 month'] 842 | #algo.add_bar('1 hour') 843 | algo.add_bar('3 mins') 844 | 845 | # Run algo for the day 846 | algo.run() 847 | 848 | 849 | 850 | 851 | """ 852 | # get first instrument (i), first bar, and df for i, bar 853 | i = algo.instruments[0] 854 | bar = algo.bars[0] 855 | df = algo.dfs[i][bar] 856 | 857 | # place market order for i 858 | algo.market_order(i, action='BUY', qty=5) 859 | 860 | # place limit order for i 861 | algo.limit_order(i, action='SELL', qty=5, limit_price=3000) 862 | 863 | # wait 5 seconds for order(s) to go through 864 | algo.ib.sleep(5) 865 | 866 | # check for open orders 867 | open_orders = algo.get_open_orders(i) 868 | print('open orders: {}'.format(open_orders)) 869 | 870 | # check for position 871 | qty = algo.get_quantity(i) 872 | print('qty for {}: {}'.format(i.symbol, qty)) 873 | 874 | 875 | 876 | """ 877 | 878 | 879 | 880 | 881 | 882 | 883 | -------------------------------------------------------------------------------- /modular_blank_TSMM.py: -------------------------------------------------------------------------------- 1 | """ 2 | Live Trading with IB Modular Design 3 | Version 1.0.1 4 | Platform: IB API and TWS 5 | By: Zach Oakes 6 | 7 | 8 | Revision Notes: 9 | 1.0.0 (07/30/2019) - Initial 10 | 1.0.1 (08/06/2019) - Add fixed stop and trailing stop logic 11 | 1.0.2 (11/4/2019) - Added EOD exit, reset time for TS/CS check to constant 12 | 1.0.3 (11.4.19) - Added MM -- FR 13 | 1.0.4 (11.6.19) - Added Fxd Qty (AND DYNAMIC FXD) 14 | """ 15 | ############################################################################### 16 | # Import required libraries 17 | import calendar 18 | import datetime 19 | from ib_insync import * 20 | from ibapi import * 21 | import logging 22 | import numpy as np 23 | import pandas as pd 24 | import pytz 25 | import sys 26 | import time 27 | 28 | ############################################################################### 29 | # Required variables for the algo 30 | # Set to True when using Spyder 31 | USING_NOTEBOOK = True 32 | 33 | # Set timezone for your TWS setup 34 | TWS_TIMEZONE = pytz.timezone('US/Central') 35 | 36 | # Set fixed position size amount 37 | QTY = 100 38 | # Set value for stop loss 39 | SL = 100.00 40 | # Set value for profit required before trailing exit is turned on 41 | PT = 100.00 42 | # Set value for percent trailing stop once turned on 43 | PCT = 0.15 # percent as decimal, 0.20 = 20% 44 | 45 | 46 | '''MM Globals ''' 47 | POS_SIZE_USD = 10000 48 | CLOSE_DICT = { 49 | 'SPY': 300, 50 | 'ROKU':150, 51 | 'TSLA':300, 52 | 'AMD':37, 53 | 'INTC':57, 54 | 'UAL':93 55 | } 56 | 57 | QTY_DICT = {} 58 | 59 | PNL = {} 60 | DELTA = 1000 61 | 62 | 63 | ############################################################################### 64 | class IBAlgoStrategy(object): 65 | """ 66 | Algorithmic Strategy for Interactive Brokers. 67 | """ 68 | def __init__(self): 69 | """Initialize algo""" 70 | # Setup logger 71 | # https://docs.python.org/3.6/library/logging.html# 72 | # logging levels: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET 73 | self.logger = logging.getLogger(__name__) 74 | self.logger.setLevel(logging.INFO) 75 | self.handler = logging.FileHandler('IBAlgoStrategy.log') 76 | self.handler.setLevel(logging.INFO) 77 | self.formatter = logging.Formatter( 78 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 79 | self.handler.setFormatter(self.formatter) 80 | self.logger.addHandler(self.handler) 81 | self.logger.info('Starting log at {}'.format(datetime.datetime.now())) 82 | 83 | # Create IB connection 84 | self.ib = self.connect() 85 | 86 | # Create empty list of instruments, bars, and indicators to track 87 | self.instruments = [] 88 | self.bars = [] 89 | self.bars_minutes = [] # keep list of bar minutes as ints 90 | self.indicators = [] 91 | 92 | # Create empty dictionary of DataFrames for instrument bars 93 | self.dfs = {} 94 | 95 | # Create empty dictionary for trailing stop's enabled (for instruments) 96 | self.trailing_stop_enabled = {} 97 | # Create empty dictionary for best profit achived (for instruments) 98 | self.trade_profit_high = {} 99 | 100 | 101 | ############################################################################### 102 | def run(self): 103 | """Run logic for live trading""" 104 | # Get today's trading hours 105 | self.get_trading_hours() 106 | 107 | # Wait until the markets open 108 | open_hr = int(self.exchange_open[:2]) 109 | open_min = int(self.exchange_open[-2:]) 110 | self.log('Waiting until the market opens ({}:{} ET)...'.format( 111 | open_hr, open_min)) 112 | while True: 113 | now = datetime.datetime.now(tz=pytz.timezone('US/Eastern')) 114 | if ((now.minute >= open_min) and (now.hour >= open_hr)) or \ 115 | (now.hour >= open_hr+1): 116 | break 117 | 118 | # Sleep 5 seconds 119 | self.ib.sleep(5) 120 | 121 | # Exchange is now open 122 | self.log() 123 | self.log('The market is now open {}'.format(now)) 124 | self.start_time = time.time() 125 | 126 | # Set time reference 9:30AM ET for today / 13:30 UTC time 127 | # Used for determining when new bars are available 128 | self.time_ref = calendar.timegm(time.strptime( 129 | now.strftime('%Y-%m-%d') + ' 13:30:00', '%Y-%m-%d %H:%M:%S')) 130 | 131 | # Run loop during exhange hours 132 | while True: 133 | # Get the time delta from the time reference 134 | time_since_open = time.time() - self.time_ref 135 | 136 | # Check for new intraday bars 137 | if len(self.bars_minutes)>0: 138 | for minutes in self.bars_minutes: 139 | if 60*minutes-(time_since_open%(60*minutes)) <= 5: 140 | # Time to update 'minutes' bar for all instruments 141 | if minutes == 1: 142 | bar = '1 min' 143 | elif minutes < 60: 144 | bar = str(minutes) + ' mins' 145 | elif minutes == 60: 146 | bar = '1 hour' 147 | else: 148 | hours = minutes/60 149 | bar = str(hours) + ' hours' 150 | # Loop through all instruments and update bar dfs 151 | for instrument in self.instruments: 152 | # Get current df for instrument/bar 153 | df = self.dfs[instrument][bar] 154 | # Update instrument/bar df 155 | df = self.update_bar(df, instrument, bar) 156 | self.dfs[instrument][bar] = df 157 | self.log("Updated {}'s {} df".format( 158 | instrument.symbol, bar)) 159 | # Process signals (new bar) 160 | self.on_data() 161 | 162 | # Perform other checks once per minute (60s) 163 | if True: #5-(time_since_open%5) <= 5: #CONFIRM THIS IS CORRECT 164 | # Loop through instruments #if 60-(time_since_open%60) <= 5: (check every minute, 5s margin of error) 165 | for instrument in self.instruments: #Replaced with Constant check. 166 | # Get current qty 167 | qty = self.get_quantity(instrument) 168 | # Check for trailing exit signal 169 | if qty != 0: 170 | if self.trailing_exit_signal(instrument, qty): 171 | # Go flat 172 | self.go_flat(instrument) 173 | 174 | # Get current ET time 175 | now = datetime.datetime.now(tz=pytz.timezone('US/Eastern')) 176 | 177 | # Get number of minutes until the market close 178 | close_str = now.strftime('%Y-%m-%d') + "T" \ 179 | + self.exchange_close[:2] \ 180 | + ":" + self.exchange_close[-2:] + ":00-04:00" 181 | close_time = datetime.datetime.strptime( 182 | ''.join(close_str.rsplit(':', 1)), '%Y-%m-%dT%H:%M:%S%z') 183 | min_to_close = int((close_time-now).seconds/60) 184 | 185 | #Exit for EOD! #ADDED EOD EXIT 186 | if min_to_close <= 30: 187 | if qty != 0: 188 | self.go_flat(instrument) 189 | 190 | 191 | # Check for exchange closing time 192 | if min_to_close <= 0: 193 | log('The market is now closed: {}'.format(now)) 194 | break 195 | 196 | # Sleep 197 | self.ib.sleep(5) 198 | 199 | self.log('Algo no longer running for the day.') 200 | 201 | 202 | ############################################################################### 203 | def on_data(self): 204 | """Process signals for new bar""" 205 | #self.log('on_data()') 206 | # Loop through instruments 207 | for instrument in self.instruments: 208 | # Check for any open orders 209 | #open_orders = self.get_open_orders(instrument) 210 | 211 | # Get current qty 212 | qty = self.get_quantity(instrument) 213 | self.log('Current qty for {}: {}'.format(instrument.symbol, qty)) 214 | 215 | # Process current long position 216 | if qty > 0: 217 | # Check for short entry signal 218 | if self.short_entry_signal(instrument): 219 | # Reverse position and go short 220 | self.go_short(instrument) 221 | 222 | # Check for long exit signal 223 | elif self.long_exit_signal(instrument): 224 | # Go flat 225 | self.go_flat(instrument) 226 | 227 | # Process current short position 228 | elif qty < 0: 229 | # Check for long entry signal 230 | if self.long_entry_signal(instrument): 231 | # Reverse position and go long 232 | self.go_long(instrument) 233 | 234 | # Check for short exit signal 235 | elif self.short_exit_signal(instrument): 236 | # Go flat 237 | self.go_flat(instrument) 238 | 239 | # Check for entry signal 240 | else: 241 | # Check for long entry signal 242 | if self.long_entry_signal(instrument): 243 | # Go long 244 | self.go_long(instrument) 245 | # Check for short entry signal 246 | elif self.short_entry_signal(instrument): 247 | # Go short 248 | self.go_short(instrument) 249 | 250 | 251 | 252 | ############################################################################### --Modified for BBs 253 | def long_entry_signal(self, instrument): 254 | """ 255 | Check for LONG entry signal for the instrument. 256 | Returns True or False. 257 | """ 258 | # Always use the first bar for this signal 259 | bar = self.bars[0] 260 | 261 | # Get intrument/bar df 262 | df = self.dfs[instrument][bar] 263 | 264 | # Get last close, PSAR, and RSI 265 | price = df['close'].iloc[-1] 266 | RSI = df['RSI'].iloc[-1] 267 | HH = df['HH'].iloc[-1] 268 | 269 | # Entry logic 270 | if condition: 271 | self.log('{} LONG ENTRY SIGNAL: price={}, HH={}'.format( 272 | instrument.symbol, price, HH)) 273 | return True 274 | return False 275 | 276 | 277 | ############################################################################### --Modified for BBs 278 | def long_exit_signal(self, instrument): 279 | """ 280 | Check for LONG exit signal for the instrument. 281 | Returns True or False. 282 | """ 283 | # Always use the first bar for this signal 284 | bar = self.bars[0] 285 | 286 | # Get intrument/bar df 287 | df = self.dfs[instrument][bar] 288 | 289 | # Get last close, PSAR, and RSI 290 | price = df['close'].iloc[-1] 291 | PSAR = df['PSAR'].iloc[-1] 292 | 293 | 294 | 295 | # Exit logic 296 | if condition: 297 | self.log('{} LONG BB EXIT SIGNAL: price={}, PSAR={}'.format( 298 | instrument.symbol, price, PSAR)) 299 | return True 300 | return False 301 | 302 | 303 | ############################################################################### -- Modified for BBS 304 | def short_entry_signal(self, instrument): 305 | """ 306 | Check for SHORT entry signal for the instrument. 307 | Returns True or False. 308 | """ 309 | # Always use the first bar for this signal 310 | bar = self.bars[0] 311 | 312 | # Get intrument/bar df 313 | df = self.dfs[instrument][bar] 314 | 315 | # Get last close, PSAR, and RSI 316 | price = df['close'].iloc[-1] 317 | RSI = df['RSI'].iloc[-1] 318 | 319 | 320 | 321 | 322 | 323 | # Entry logic 324 | if condition: 325 | self.log('{} SHORT ENTRY SIGNAL: price={},LL={}'.format( 326 | instrument.symbol, price, LL)) 327 | return True 328 | return False 329 | 330 | 331 | ############################################################################### -- Modified for BBS 332 | def short_exit_signal(self, instrument): 333 | """ 334 | Check for SHORT exit signal for the instrument. 335 | Returns True or False. 336 | """ 337 | # Always use the first bar for this signal 338 | bar = self.bars[0] 339 | 340 | # Get intrument/bar df 341 | df = self.dfs[instrument][bar] 342 | 343 | # Get last close, PSAR, and RSI 344 | price = df['close'].iloc[-1] 345 | PSAR = df['PSAR'].iloc[-1] 346 | 347 | 348 | 349 | # Exit logic 350 | if condition: 351 | self.log('{} SHORT BB EXIT SIGNAL: price={}, PSAR={}'.format( 352 | instrument.symbol, price, PSAR)) 353 | return True 354 | return False 355 | 356 | 357 | ############################################################################### 358 | def trailing_exit_signal(self, instrument, qty): 359 | """ 360 | Check for trailing exit signal for the instrument. 361 | Also checks for the initial stop loss exit signal for the instrument. 362 | Returns True or False. 363 | """ 364 | self.log('Checking for stop loss or trailing stop exit for {}'.format( 365 | instrument.symbol)) 366 | 367 | # Get cost basis for instrument 368 | cost_basis = self.get_cost_basis(instrument) 369 | dollar_cost = cost_basis*qty 370 | 371 | # Get latest prices for instrument 372 | bid, ask, mid = self.get_price(instrument) 373 | # Verify valid prices 374 | if mid is None: 375 | return False 376 | 377 | # Calculate current dollar profit (Per Position basis, not contract) 378 | if qty > 0: 379 | # Long position 380 | profit = (mid-cost_basis)*qty 381 | elif qty < 0: 382 | # Short position 383 | profit = (cost_basis-mid)*qty 384 | 385 | # Check if the stop loss is triggered 386 | if profit <= -abs(SL): 387 | self.log('Stop loss exit triggered for {}, profit={}'.format( 388 | instrument.symbol, profit)) 389 | return True 390 | 391 | # Check if trailing stop should be enabled (PT reached) 392 | if not self.trailing_stop_enabled[instrument]: 393 | if profit >= PT: 394 | # Set trailing stop to be enabled and save high profit 395 | self.trailing_stop_enabled[instrument] = True 396 | self.trade_profit_high[instrument] = 0 397 | PNL[instrument] += profit #Added PNL Increment 398 | self.log('Trailing stop for {} enabled, profit={}'.format( 399 | instrument.symbol, profit)) 400 | # Always return False for exit signal if not previously turned on 401 | return False 402 | 403 | # Trailing stop previously enabled, so check for exit signal 404 | # Check for new profit high 405 | if profit > self.trade_profit_high[instrument]: 406 | # Update trade profit and return False for signal 407 | self.trade_profit_high[instrument] = profit 408 | return False 409 | 410 | # Check for exit signal 411 | exit_trigger = (1.0-PCT)*self.trade_profit_high[instrument] 412 | if profit < exit_trigger: 413 | PNL[instrument] += profit #Added PNL Increment 414 | self.log("Trailing exit triggered for {}: profit={}, " 415 | "high profit={}".format( 416 | instrument.symbol, profit, 417 | self.trade_profit_high[instrument])) 418 | return True 419 | return False 420 | 421 | 422 | ############################################################################### 423 | def get_price(self, instrument): 424 | """Get the current bid, ask, and mid price for an instrument""" 425 | # Create IB object to request price info 426 | ticker = self.ib.reqMktData(instrument, "", False, False) 427 | # Wait 2 seconds 428 | self.ib.sleep(2.0) 429 | # Loop until getting bid and ask or 20s max 430 | bid = None 431 | ask = None 432 | for i in range(100): 433 | self.ib.sleep(0.2) 434 | if ticker.bid is not None and ticker.ask is not None: 435 | bid = float(ticker.bid) 436 | ask = float(ticker.ask) 437 | break 438 | 439 | # Check for valid bid and ask 440 | try: 441 | mid = round((bid+ask)/2,2) 442 | except: 443 | self.log('Error getting current bid/ask prices for {}'.format( 444 | instrument)) 445 | return None, None, None 446 | 447 | #self.log('{} current bid={}, ask={}, mid={}'.format( 448 | # instrument.symbol, bid, ask, mid)) 449 | return bid, ask, mid 450 | 451 | 452 | ############################################################################### 453 | def go_long(self, instrument): 454 | """Go LONG instrument""" 455 | # Get current qty 456 | qty = self.get_quantity(instrument) 457 | # Get desired qty - no function for this currently 458 | #desired_qty = QTY 459 | #desired_qty = self.get_FR_qty(instrument) 460 | 461 | desired_qty = self.get_fxd_qty(instrument) 462 | # Get order quantity 463 | order_qty = desired_qty-qty 464 | # Place market order to go long 465 | self.market_order(instrument, 'BUY', abs(order_qty)) 466 | # Set trailing stop enabled to False for instrument 467 | self.trailing_stop_enabled[instrument] = False 468 | # Set trade profit high to date to be zero 469 | self.trade_profit_high[instrument] = 0 470 | 471 | 472 | ############################################################################### 473 | def go_short(self, instrument): 474 | """Go SHORT instrument""" 475 | # Get current qty 476 | qty = self.get_quantity(instrument) 477 | # Get desired qty - no function for this currently 478 | #desired_qty = -QTY 479 | #desired_qty = -(self.get_FR_qty(instrument)) 480 | desired_qty = -(self.get_fxd_qty(instrument)) 481 | # Get order quantity 482 | order_qty = desired_qty-qty 483 | # Place market order to go short 484 | self.market_order(instrument, 'SELL', abs(order_qty)) 485 | # Set trailing stop enabled to False for instrument 486 | self.trailing_stop_enabled[instrument] = False 487 | # Set trade profit high to date to be zero 488 | self.trade_profit_high[instrument] = 0 489 | 490 | 491 | ############################################################################### 492 | def go_flat(self, instrument): 493 | """Go FLAT instrument""" 494 | # Get current qty 495 | qty = self.get_quantity(instrument) 496 | # Place market order to go flat 497 | if qty > 0: 498 | action = 'SELL' 499 | elif qty < 0: 500 | action = 'BUY' 501 | else: 502 | self.log('{} already FLAT.'.format(instrument.symbol)) 503 | return 504 | # Place market order to go flat 505 | self.market_order(instrument, action, abs(qty)) 506 | 507 | 508 | ############################################################################### 509 | def get_open_orders(self, instrument): 510 | """ 511 | Checks for any open orders for instrument. 512 | Returns True or False. 513 | """ 514 | # Verify open orders match open trades 515 | for i in range(10): 516 | open_trades = list(self.ib.openTrades()) 517 | trade_ids = set([t.order.orderId for t in open_trades]) 518 | open_orders = list(self.ib.reqOpenOrders()) 519 | order_ids = set([o.orderId for o in open_orders]) 520 | missing = order_ids.difference(trade_ids) 521 | if len(missing) == 0 and len(open_trades) > 0: 522 | break 523 | 524 | # Return True if any open trade is for instrument, otherwise False 525 | for trade in open_trades: 526 | if instrument.symbol == trade.contract.localSymbol: 527 | return True 528 | return False 529 | 530 | 531 | ############################################################################### 532 | def get_quantity(self, instrument): 533 | """Returns the current quantity held for instrument""" 534 | # Get instrument type 535 | if str(type(instrument))[-7:-2] == 'Stock': 536 | instrument_type = 'Stock' 537 | elif str(type(instrument))[-7:-2] == 'Option': 538 | instrument_type = 'Option' 539 | elif str(type(instrument))[-7:-2] == 'Future': 540 | instrument_type = 'Future' 541 | else: 542 | raise ValueError('Invalid instrument type ({}) for ' 543 | 'current_quantity'.format(type(instrument))) 544 | 545 | # Loop through all current positions 546 | for position in self.ib.positions(): 547 | # Verify position is for instrument 548 | contract = position.contract 549 | if instrument_type == 'Stock': 550 | try: 551 | if contract.secType == 'STK' and \ 552 | contract.localSymbol == instrument.symbol: 553 | return int(position.position) 554 | except: 555 | continue 556 | elif instrument_type == 'Option': 557 | pass 558 | elif instrument_type == 'Future': 559 | pass 560 | 561 | return 0 562 | 563 | 564 | ############################################################################### 565 | def get_cost_basis(self, instrument): 566 | """Returns the current cost basis for an instrument's position""" 567 | # Get instrument type 568 | if str(type(instrument))[-7:-2] == 'Stock': 569 | instrument_type = 'Stock' 570 | elif str(type(instrument))[-7:-2] == 'Option': 571 | instrument_type = 'Option' 572 | elif str(type(instrument))[-7:-2] == 'Future': 573 | instrument_type = 'Future' 574 | else: 575 | raise ValueError('Invalid instrument type ({}) for ' 576 | 'current_quantity'.format(type(instrument))) 577 | 578 | # Loop through all current positions 579 | for position in self.ib.positions(): 580 | # Verify position is for instrument 581 | contract = position.contract 582 | if instrument_type == 'Stock': 583 | try: 584 | if contract.secType == 'STK' and \ 585 | contract.localSymbol == instrument.symbol: 586 | return float(position.avgCost) 587 | except: 588 | continue 589 | elif instrument_type == 'Option': 590 | pass 591 | elif instrument_type == 'Future': 592 | pass 593 | 594 | return 0 595 | 596 | 597 | ############################################################################### 598 | def market_order(self, instrument, action, qty): 599 | """Place market order for instrument""" 600 | # Verify action 601 | if action != 'BUY' and action != 'SELL': 602 | raise ValueError("Invalid action () for market order. Must be " 603 | "'BUY' or 'SELL'.".format(action)) 604 | 605 | market_order = MarketOrder( 606 | action=action, 607 | totalQuantity=float(qty) 608 | ) 609 | self.log('{}ING {} units of {} at MARKET'.format( 610 | action, qty, instrument.symbol)) 611 | self.ib.placeOrder(instrument, market_order) 612 | 613 | 614 | ############################################################################### 615 | def limit_order(self, instrument, action, qty, limit_price): 616 | """Place limit order for instrument""" 617 | # Verify action 618 | if action != 'BUY' and action != 'SELL': 619 | raise ValueError("Invalid action () for market order. Must be " 620 | "'BUY' or 'SELL'.".format(action)) 621 | 622 | limit_order = LimitOrder( 623 | action=action, 624 | totalQuantity=float(qty), 625 | lmtPrice=float(limit_price) 626 | ) 627 | self.log('{}ING {} units of {} at {} LIMIT'.format( 628 | action, qty, instrument.symbol, limit_price)) 629 | self.ib.placeOrder(instrument, limit_order) 630 | 631 | 632 | ############################################################################### 633 | def connect(self): 634 | """Connect to Interactive Brokers TWS""" 635 | self.log('Connecting to Interactive Brokers TWS...') 636 | try: 637 | if USING_NOTEBOOK: 638 | util.startLoop() 639 | ib = IB() 640 | ib.connect('127.0.0.1', 7497, clientId=1) 641 | self.log('Connected') 642 | self.log() 643 | return ib 644 | except: 645 | self.log('Error in connecting to TWS!! Exiting...') 646 | self.log(sys.exc_info()[0]) 647 | exit(-1) 648 | 649 | ############################################################################### 650 | def log(self, msg=""): 651 | """Add log to output file""" 652 | self.logger.info(msg) 653 | if not USING_NOTEBOOK: 654 | print(msg) 655 | 656 | 657 | ############################################################################### 658 | def get_trading_hours(self): 659 | """Get today's trading hours for algo's instruments""" 660 | open_time = None 661 | close_time = None 662 | 663 | # Get todays date in YYYYMMDD format 664 | today = datetime.datetime.today().strftime('%Y%m%d') 665 | 666 | # Loop through instruments 667 | for instrument in self.instruments: 668 | # Get contract details 669 | contract_details = self.ib.reqContractDetails(instrument)[0] 670 | # Get regular trading hours for today 671 | trading_hours_list = contract_details.liquidHours.split(';') 672 | for item in trading_hours_list: 673 | if item[:8] == today: 674 | # Update open time 675 | if open_time is None: 676 | open_time = item.split('-')[0].split(':')[1] 677 | else: 678 | if item.split('-')[0].split(':')[1] < open_time: 679 | open_time = item.split('-')[0].split(':')[1] 680 | # Update close time 681 | if close_time is None: 682 | close_time = item.split('-')[1].split(':')[1] 683 | else: 684 | if item.split('-')[1].split(':')[1] > close_time: 685 | close_time = item.split('-')[1].split(':')[1] 686 | break 687 | 688 | # Save earliest start time 689 | self.exchange_open = open_time 690 | # Save latest end time 691 | self.exchange_close = close_time 692 | self.log() 693 | self.log("Today's exchange hours are {}-{}".format( 694 | self.exchange_open, self.exchange_close)) 695 | 696 | 697 | ############################################################################### 698 | def add_instrument(self, instrument_type, ticker, last_trade_date=None, 699 | strike=None, option_type=None, routing='SMART', 700 | currency='USD', primary_exchange='NASDAQ', 701 | future_exchange='GLOBEX'): 702 | """ 703 | Add instrument to trade to algo. 704 | https://github.com/erdewit/ib_insync/blob/master/ib_insync/contract.py 705 | """ 706 | if instrument_type == 'Stock': 707 | instrument = Stock(ticker, routing, currency, 708 | primaryExchange=primary_exchange) 709 | elif instrument_type == 'Option': 710 | # Verify option type 711 | valid_option_types = ['C', 'P', 'CALL', 'PUT'] 712 | if option_type not in valid_option_types: 713 | raise ValueError( 714 | 'Invalid option_type: {}. Must be in {}'.format( 715 | option_type, valid_option_types)) 716 | instrument = Option(ticker, last_trade_date, float(strike), 717 | option_type, routing) 718 | elif instrument_type == 'Future': 719 | instrument = Future(ticker, last_trade_date, future_exchange) 720 | else: 721 | raise ValueError( 722 | "Invalid instrument type: {}".format(instrument_type)) 723 | 724 | # Append instrument to algo list 725 | self.ib.qualifyContracts(instrument) 726 | self.instruments.append(instrument) 727 | 728 | # Create dictionary for instrument bars 729 | self.dfs[instrument] = {} 730 | 731 | 732 | ############################################################################### 733 | def add_bar(self, bar): 734 | """Add bar to algo""" 735 | # https://interactivebrokers.github.io/tws-api/historical_bars.html 736 | valid_bars = ['1 min','2 mins','3 mins','10 mins','20 mins','30 mins', 737 | '1 hour','2 hours','3 hours','4 hours','8 hours', 738 | '1 day','1 week','1 month'] 739 | # Verify bar size 740 | if bar not in valid_bars: 741 | raise ValueError('Invalid bar: {}. Must be in {}'.format 742 | (bar, valid_bars)) 743 | # Append bar to algo list 744 | self.bars.append(bar) 745 | 746 | # Get bar minutes (when applicable) 747 | if bar[-3:] == 'min' or bar[-4:] == 'mins': 748 | # min bar 749 | bar_minutes = int(bar.split(' ')[0]) 750 | self.bars_minutes.append(bar_minutes) 751 | elif bar[-4:] == 'hour' or bar[-5:] == 'hours': 752 | # hourly bar 753 | bar_minutes = int(60*bar.split(' ')[0]) 754 | self.bars_minutes.append(bar_minutes) 755 | 756 | # Initialize dfs for all instruments 757 | for instrument in self.instruments: 758 | # Get ohlc pandas DataFrame 759 | df = self.get_historical_data(instrument, bar) 760 | # Add indicators to df and save to algo 761 | self.dfs[instrument][bar] = self.add_indicators(df) 762 | 763 | 764 | ############################################################################### 765 | def get_historical_data(self, instrument, bar, end_date="", use_RTH=True): 766 | """ 767 | Get historical bars for instrument. 768 | https://interactivebrokers.github.io/tws-api/historical_bars.html 769 | """ 770 | # Get max duration for a given bar size 771 | if bar in ['1 day','1 week','1 month']: 772 | duration = '1 Y' # one year 773 | elif bar in ['30 mins','1 hour','2 hours','3 hours','4 hours','8 hours']: 774 | duration = '1 M' # one month 775 | elif bar in ['3 mins','10 mins','20 mins']: 776 | duration = '1 W' # one week 777 | elif bar == '2 mins': 778 | duration = '2 D' # two days 779 | elif bar == '1 min': 780 | duration = '1 D' # one day 781 | else: 782 | raise ValueError( 783 | 'Invalid bar: {} for get_historical_data()'.format(bar)) 784 | 785 | # Get historical bars 786 | bars = self.ib.reqHistoricalData( 787 | instrument, 788 | endDateTime=end_date, 789 | durationStr=duration, 790 | barSizeSetting=bar, 791 | whatToShow='TRADES', 792 | useRTH=use_RTH) # use regular trading hours 793 | 794 | # Convert bars to a pandas dataframe 795 | hist = util.df(bars) 796 | 797 | # Make the 'date' column the index 798 | hist.set_index('date', inplace=True) 799 | 800 | # Remove the 'volume', 'barCount', and 'average' columns 801 | hist.drop(columns=['volume', 'barCount', 'average'], inplace=True) 802 | 803 | # Add TWS time zone to datetimes 804 | hist = hist.tz_localize(TWS_TIMEZONE) 805 | 806 | return hist 807 | 808 | 809 | ############################################################################### 810 | def update_bar(self, df, instrument, bar, end_date="", use_RTH=True): 811 | """ 812 | Update historical bars for instrument df. 813 | """ 814 | # Set duration for 1 day 815 | duration = '1 D' # one day 816 | 817 | # Get historical bars 818 | bars = self.ib.reqHistoricalData( 819 | instrument, 820 | endDateTime=end_date, 821 | durationStr=duration, 822 | barSizeSetting=bar, 823 | whatToShow='TRADES', 824 | useRTH=use_RTH) # use regular trading hours 825 | 826 | # Convert bars to a pandas dataframe 827 | hist = util.df(bars) 828 | 829 | # Make the 'date' column the index 830 | hist.set_index('date', inplace=True) 831 | 832 | # Remove the 'volume', 'barCount', and 'average' columns 833 | hist.drop(columns=['volume', 'barCount', 'average'], inplace=True) 834 | 835 | # Add TWS time zone to datetimes 836 | hist = hist.tz_localize(TWS_TIMEZONE) 837 | 838 | # Check if hist last date != df last date 839 | if hist.index[-1] != df.index[-1]: 840 | # Append new bar to df 841 | try: 842 | df = df.append(hist, sort=True) 843 | except: 844 | df = df.append(hist) 845 | # Add indicators to df 846 | df = self.add_indicators(df) 847 | 848 | return df 849 | 850 | 851 | ############################################################################### 852 | def add_indicators(self, df): 853 | """Add technical indicators to pandas DataFrame""" 854 | # Check for RSI in indicators 855 | if 'RSI' in self.indicators: 856 | # Add RSI to df 857 | df = self.get_RSI(df) 858 | # Check for Parabolic SAR in indicators 859 | if 'SAR' in self.indicators: 860 | # Add SAR to df 861 | df = self.get_SAR(df) 862 | # Check for ATR in indicators 863 | if 'ATR' in self.indicators: 864 | df = self.get_ATR(df) 865 | if 'HL' in self.indicators: 866 | df = self.get_HL(df) 867 | 868 | return df 869 | 870 | 871 | ############################################################################### 872 | def add_RSI(self, length, alpha): 873 | """Add the RSI indicator to the list of indicators.""" 874 | # Verify correct alpha input 875 | valid_alpha_list = ['Wilders', 'Standard'] 876 | if alpha not in valid_alpha_list: 877 | raise ValueError('Invalid RSI alpha input ({}). Must be {}'.format( 878 | alpha, alpha_list)) 879 | # Save RSI input parameters to algo 880 | self.RSI_length = length 881 | self.RSI_alpha = alpha 882 | # Add RSI to list of indicators 883 | self.indicators.append('RSI') 884 | 885 | 886 | ############################################################################### 887 | def get_RSI(self, df): 888 | """Add the RSI calculations to pandas DataFrame""" 889 | # Using Wilder's EMA: alpha=1/n instead of standard alpha = 2/(n+1) 890 | df['delta'] = df['close'].diff() 891 | df['gain'], df['loss'] = df['delta'].copy(), df['delta'].copy() 892 | df['gain'][df['gain']<0] = 0 893 | df['loss'][df['loss']>0] = 0 894 | 895 | # Get alpha for RSI EMA 896 | if self.RSI_alpha == 'Wilders': # alpha=1/n 897 | alpha = float(1.0/self.RSI_length) 898 | elif self.RSI_alpha == 'Standard': 899 | alpha = float(2.0/(self.RSI_length+1)) 900 | 901 | # Calculate the avg gain and loss 902 | df['avg_gain'] = df['gain'].ewm(alpha=alpha).mean() 903 | df['avg_loss'] = df['loss'].ewm(alpha=alpha).mean() 904 | 905 | # Calculate RS and RSI 906 | df['RS'] = abs(df['avg_gain']/df['avg_loss']) 907 | df['RSI'] = 100.0-(100.0/(1+df['RS'])) 908 | return df 909 | 910 | ############################################################################### ---Addit 911 | 912 | def add_HL(self,length): 913 | '''Adds HH LL to list of indicators to init''' 914 | if length < 2 or length > 10: 915 | raise ValueError('Invalid Length {}; Length must be between 2 and 10'.format(length)) 916 | self.HL_Len = length 917 | self.indicators.append('HL') 918 | 919 | ############################################################################### ---Addit 920 | 921 | def get_HL(self,df): 922 | '''Adds HH LL (both) to pd DF''' 923 | c = df['close'] 924 | #High 925 | for i in range(self.HL_Len - 1): 926 | if c.iloc[-i] < c.iloc[-i -1]: 927 | df['HH'] = False 928 | df['HH'] = True 929 | for i in range(self.HL_Len - 1): 930 | if c.iloc[-i] > c.iloc[-i -1]: 931 | df['LL'] = False 932 | df['LL'] = True 933 | return df 934 | 935 | 936 | 937 | 938 | ############################################################################### 939 | def add_SAR(self, af, af_max): 940 | """Add the Parabolic SAR indicator to the list of indicators.""" 941 | # Save SAR input parameters to algo 942 | self.SAR_af = af 943 | self.SAR_af_max = af_max 944 | # Add SAR to list of indicators 945 | self.indicators.append('SAR') 946 | 947 | 948 | ############################################################################### 949 | def get_SAR(self, df): 950 | """ 951 | Add the Parabolic SAR indicator to pandas DataFrame 952 | ref: https://school.stockcharts.com/doku.php?id=technical_indicators:parabolic_sar 953 | ref: https://stackoverflow.com/questions/54918485/parabolic-sar-in-python-psar-keep-growing-instead-of-reversing 954 | """ 955 | # Reset index 956 | df.reset_index(inplace=True) 957 | 958 | # Initialize df columns first values 959 | df.loc[0,'AF'] = self.SAR_af 960 | df.loc[0,'PSAR'] = df.loc[0,'low'] 961 | df.loc[0,'EP'] = df.loc[0,'high'] 962 | df.loc[0,'PSAR_dir'] = "bull" 963 | 964 | # Loop through rows of df 965 | for a in range(1, len(df)): 966 | # Previous uptrend 967 | if df.loc[a-1,'PSAR_dir'] == 'bull': 968 | # Calculate the current PSAR (rising) 969 | # = previous PSAR + previous AF(previous EP-previous PSAR) 970 | df.loc[a,'PSAR'] = df.loc[a-1,'PSAR'] + \ 971 | df.loc[a-1,'AF']*(df.loc[a-1,'EP']-df.loc[a-1,'PSAR']) 972 | 973 | # Check for change in trend 974 | if df.loc[a,'low'] < df.loc[a-1,'PSAR']: 975 | # now downtrend 976 | df.loc[a,'PSAR_dir'] = "bear" 977 | df.loc[a,'PSAR'] = df.loc[a-1,'EP'] 978 | df.loc[a,'EP'] = df.loc[a-1,'low'] 979 | df.loc[a,'AF'] = self.SAR_af 980 | 981 | # Update uptrend SAR 982 | else: 983 | df.loc[a,'PSAR_dir'] = "bull" 984 | # Check for extreme point (high) 985 | if df.loc[a,'high'] > df.loc[a-1,'EP']: 986 | # Update extreme point 987 | df.loc[a,'EP'] = df.loc[a,'high'] 988 | # Increase AF if below max 989 | if df.loc[a-1,'AF'] <= self.SAR_af_max-self.SAR_af: 990 | df.loc[a,'AF'] = df.loc[a-1,'AF'] + self.SAR_af 991 | else: 992 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 993 | # No new extreme point 994 | elif df.loc[a,'high'] <= df.loc[a-1, 'EP']: 995 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 996 | df.loc[a,'EP'] = df.loc[a-1,'EP'] 997 | 998 | # Previous downtrend 999 | elif df.loc[a-1,'PSAR_dir'] == 'bear': 1000 | # Calculate the current PSAR (falling) 1001 | # = previous PSAR - previous AF(previous PSAR-previous EP) 1002 | df.loc[a,'PSAR'] = df.loc[a-1,'PSAR'] - \ 1003 | (df.loc[a-1,'AF']*(df.loc[a-1,'PSAR']-df.loc[a-1,'EP'])) 1004 | 1005 | # Check for change in trend 1006 | if df.loc[a,'high'] > df.loc[a-1,'PSAR']: 1007 | # now uptrend 1008 | df.loc[a,'PSAR_dir'] = "bull" 1009 | df.loc[a,'PSAR'] = df.loc[a-1,'EP'] 1010 | df.loc[a,'EP'] = df.loc[a-1,'high'] 1011 | df.loc[a,'AF'] = self.SAR_af 1012 | 1013 | # Update downtrend SAR 1014 | else: 1015 | df.loc[a,'PSAR_dir'] = "bear" 1016 | # Check for new extreme point (low) 1017 | if df.loc[a,'low'] < df.loc[a-1, 'EP']: 1018 | # Update extreme point 1019 | df.loc[a,'EP'] = df.loc[a, 'low'] 1020 | # Increase AF if below max 1021 | if df.loc[a-1,'AF'] <= self.SAR_af_max-self.SAR_af: 1022 | df.loc[a,'AF'] = df.loc[a-1,'AF'] + self.SAR_af 1023 | else: 1024 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 1025 | # No new extreme point 1026 | elif df.loc[a,'low'] >= df.loc[a-1,'EP']: 1027 | df.loc[a,'AF'] = df.loc[a-1,'AF'] 1028 | df.loc[a,'EP'] = df.loc[a-1,'EP'] 1029 | 1030 | # Reset date to be the index 1031 | df.set_index('date', inplace=True) 1032 | 1033 | return df 1034 | 1035 | 1036 | ############################################################################### 1037 | def add_ATR(self, length): 1038 | """Add the ATR indicator to the list of indicators.""" 1039 | # Save ATR input parameters to algo 1040 | self.ATR_length = length 1041 | # Add ATR to list of indicators 1042 | self.indicators.append('ATR') 1043 | 1044 | 1045 | ############################################################################### 1046 | def get_ATR(self, df): 1047 | """Add the ATR calculations to pandas DataFrame""" 1048 | df['tr1'] = df['high'] - df['low'] # H-L 1049 | df['tr2'] = abs(df['high'] - df['close'].shift()) # abs(H-prev_close) 1050 | df['tr3'] = abs(df['low'] - df['close'].shift()) # abs(L-prev_close) 1051 | df['tr'] = df[['tr1', 'tr2', 'tr3']].apply(max,axis=1) #axis=1 for row max, axis=0 for column max 1052 | df['atr'] = df['tr'].rolling(self.ATR_length).mean() 1053 | return df 1054 | 1055 | 1056 | ############################################################################### #ADDED 1057 | '''DYNAMIC VERSION OF FIXED QTY IN USD -- CALCULATES EACH TIME''' 1058 | 1059 | def get_dyn_fxd(self,instrument): 1060 | '''Dynamic version of fxd qty''' 1061 | # Always use the first bar for this signal 1062 | bar = self.bars[0] 1063 | # Get intrument/bar df 1064 | df = self.dfs[instrument][bar] 1065 | 1066 | c = df['close'].iloc[-1] 1067 | QTY_DICT[instrument] = round(POS_SIZE_USD / c,0) 1068 | return int(QTY_DICT[instrument]) 1069 | 1070 | 1071 | 1072 | def create_fxd_dict(self,instrument_list): 1073 | for sec in instrument_list: 1074 | QTY_DICT[sec] = 0 1075 | return QTY_DICT 1076 | 1077 | 1078 | 1079 | ############################################################################### #ADDED 1080 | 1081 | def get_fxd_qty(self,instrument): 1082 | '''Get Fixed Qty Per instrument -- BASIC''' 1083 | #qty = POS_SIZE_USD / df[instrument]['close'] 1084 | qty = round(POS_SIZE_USD / CLOSE_DICT[instrument],0) 1085 | return qty 1086 | 1087 | ############################################################################### #ADDED 1088 | 1089 | def create_PNL(self,instrument_list): 1090 | '''Create Dict of PNL[sec] = sec_pnl (maybe later DF) ''' 1091 | for sec in instrument_list: 1092 | PNL[sec] = 0 1093 | #pnl[sec] = [] #To track all wins/losses, + pnl[sec].append(profit) 1094 | return PNL 1095 | 1096 | ############################################################################### #ADDED 1097 | 1098 | def get_FR_qty(self,instrument): 1099 | '''Accesses PNL Dict and Returns desired Qty''' 1100 | sec_pnl = PNL[instrument] 1101 | q = .5 * (np.sqrt(1 + 8*(sec_pnl/DELTA)) + 1) 1102 | return q 1103 | 1104 | 1105 | ############################################################################### 1106 | if __name__ == '__main__': 1107 | # Create algo object 1108 | algo = IBAlgoStrategy() 1109 | 1110 | # Add instruments to trade 1111 | algo.add_instrument('Stock', 'SPY') 1112 | #algo.add_instrument('Option', 'SPY', '20190920', 300, 'C') 1113 | #algo.add_instrument('Future', 'ES', '20190920') 1114 | PNL = algo.create_PNL(['SPY']) 1115 | 1116 | algo.create_fxd_dict(['SPY']) 1117 | 1118 | 1119 | # Add RSI to indicators 1120 | algo.add_RSI(length=14, alpha='Wilders') 1121 | 1122 | # Add ATR to indicators 1123 | algo.add_ATR(length=14) 1124 | 1125 | 1126 | 1127 | # Add intraday bars 1128 | #valid_bars = ['1 min','2 mins','3 mins','10 mins','20 mins','30 mins','1 hour','2 hours','3 hours','4 hours','8 hours','1 day','1 week','1 month'] 1129 | #algo.add_bar('1 hour') 1130 | algo.add_bar('1 hour') 1131 | 1132 | # Run algo for the day 1133 | algo.run() 1134 | --------------------------------------------------------------------------------