├── default.csv ├── README.md ├── ibInterface.py └── OptionSeller.py /default.csv: -------------------------------------------------------------------------------- 1 | ticker,targetBuy,targetSell,divHack,buyStrategy,sellStrategy,priority,minPeriod,maxPeriod,premTarget,weightTarget 2 | NUE,54.50,58.00,yes,aggressive,conservative,4,2w,1m,.01,200 3 | BAC,25.00,27.00,yes,aggressive,conservative,4,2w,1m,.01,300 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Background** 2 | 3 | I would like to start off with a disclaimer: I have not tested this code with real money. I've only run the Options Seller algorithm in Paper Trading. If you plan on using my API or the Options Seller algorithm in your own portfolio, I strongly urge you to do **EXTENSIVE** testing in paper trading before deploying it with real money. 4 | 5 | My motivation for this project was to implement a trading algorithm that integrates cash-secured puts and covered calls. The combination of these two plays is what I gravitated to after a few years of stock and options trading from 2015-2017. For more background on these strategies you can check out their pages on Options Education: 6 | 7 | https://www.optionseducation.org/strategies/all-strategies/cash-secured-put#:~:text=The%20cash%2Dsecured%20put%20involves%20writing%20an%20at%2Dthe%2D,all%20outcomes%20are%20presumably%20acceptable. 8 | 9 | https://www.optionseducation.org/strategies/all-strategies/covered-call-buy-write 10 | 11 | The appeal of cash-secured puts is that you can basically earn income from putting in limit buy orders on a stock you like. Similarly, the appeal of a covered call is that you can earn income from putting in a limit sell order at a price you are comfortable exitting. The combination of these two strategies can turn your portfolio into a cash flow monster. This strategy would usually underperform simple buy and hold in a strong bull market, but it would outperform in a bear, sideways, or weak bull market. 12 | 13 | If you wanted you could execute this strategy on SPY, but it will likely perform better on individual stocks. Since individual stocks are more volatile than the index, you can collect more options premium by selling puts and calls. It also allows for greater diversification for midsize portfolios. Indiviudal stocks are often much cheaper than 1 share of SPY, and cash-secured puts and covered calls require enough cash to purchase the underlying in multiples of 100 due to the nature of options contracts. 14 | 15 | As I mentioned earlier, I never did get a chance to test my algorithm with real money. It was right around the time that I completed the bulk of this project that I also lost interest in stocks and options in general. In late 2017 I began to shift my focus to real estate investment. 16 | 17 | 18 | **Interactive Brokers Interface** 19 | 20 | Interactive Brokers had been my broker of choice for a long time due to their cheap options commissions. I was in luck when I became interested in this project, since they also provide their API for free to account holders. Their API offers a lot of options, but I needed to create a wrapper API around it in order to use it to its fullest potential. Hopefully you can benefit from my wrapper API even if you do not like my Options Selling strategy. The native Interactive Brokers API was somewhat complicated to work with, and I did my best to comment extensviely in my wrapper API. Please don't hesitate to reach out to me with any questions that arise. 21 | 22 | 23 | **Options Seller** 24 | 25 | The Options Seller bot starts off by parsing in a list of stock tickers and their specified parameters from 'default.csv'. The input csv file can be modified by changing the STOCK_CSV constant at the top of OptionSeller.py. Stock parameters are parsed based on the column name, so the order of the columns can be changed as long as their headers remain the same. The 'ticker' column denotes the ticker of the stock for which options will be traded. The algorithm will start trying to sell puts on the ticker when it's price is close to the 'targetBuy' value. If the stock is held and it approaches the 'targetSell', the algorithm will start trying to sell calls to exit the position. Currently, the algorithm is hard coded to begin trying to sell puts when it is within 2% of the targetBuy, and it will begin trying to sell calls when it holds the stock and it is within 1% of targetSell. It will try to accumulate the stock until the value in 'weightTarget' is reached. weightTarget must be a multiple of 100. 26 | 27 | The other headers present in the 'default.csv' file right now are features that I have not yet implemented. 28 | 29 | -------------------------------------------------------------------------------- /ibInterface.py: -------------------------------------------------------------------------------- 1 | # Helper functions for extracting data from TWS/gateway messages 2 | from ib.ext.Contract import Contract 3 | from ib.ext.Order import Order 4 | from ib.opt import ibConnection, message 5 | 6 | import time 7 | import datetime 8 | from threading import Thread 9 | import signal 10 | 11 | # python logging library for monitoring and debugging 12 | import logging 13 | 14 | # Set logging level 15 | logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) 16 | 17 | # Reference codes for tick numbers on messages from TWS/Gateway 18 | # Copied relevant codes ib.ext.TickType, can't get it to import properly for some reason 19 | class TickTypes: 20 | BID = 1 21 | ASK = 2 22 | LAST = 4 23 | VOLUME = 8 24 | CLOSE = 9 25 | BID_OPTION = 10 26 | ASK_OPTION = 11 27 | LAST_OPTION = 12 28 | OPEN = 14 29 | OPEN_INTEREST = 22 30 | OPTION_IMPLIED_VOL = 24 31 | OPTION_CALL_OPEN_INTEREST = 27 32 | OPTION_PUT_OPEN_INTEREST = 28 33 | 34 | # Class to provide a convenient wrapper around the TWS/Gateway message structure 35 | class IbInterface: 36 | def __init__(self): 37 | # Values to be populated by the msg handlers when data received from TWS/Gateway 38 | self.account_value = None 39 | self.ticker = None 40 | self.key = None 41 | self.bid = None 42 | self.ask = None 43 | self.last = None 44 | self.volume = None 45 | self.close = None 46 | self.open_interest = None 47 | self.open = None 48 | self.order_status = None 49 | self.filled_quantity = None 50 | 51 | # list to hold possible options contracts, and ticker associated with contracts 52 | self.contract_list = [] 53 | self.list_ticker = None 54 | 55 | # list to hold open order ids 56 | self.open_id_list = [] 57 | 58 | # list to hold current positions, populated by the get positions method 59 | self.position_list = [] 60 | 61 | # indicator that contract details, valid order id, open order id list, position details are ready 62 | self.detail_ready = False 63 | self.id_ready = False 64 | self.open_order_ready = False 65 | self.positions_ready = False 66 | 67 | # numeric identifier for market data request, contract detail request, placed order, and order for which status requested 68 | self.tick_id = 1 69 | self.detail_id = 1 70 | self.order_id = 0 71 | self.search_id = None 72 | 73 | # number of possible tick_id numbers and detail_id numbers 74 | self.id_max = 1000 75 | 76 | # timeout for quotes, in seconds, and counter for amount of ticks received for a quote 77 | self.quote_timeout = 10 78 | self.stk_tick_max = 5 79 | self.opt_tick_max = 5 80 | self.tick_cnt = 0 81 | 82 | # Connection to TWS/Gateway 83 | self.conn = ibConnection() 84 | 85 | # dict to neatly define function calls from the tick handler 86 | self.tick_callbacks = { 87 | TickTypes.BID : self._set_bid, 88 | TickTypes.ASK : self._set_ask, 89 | TickTypes.LAST : self._set_last, 90 | TickTypes.VOLUME : self._set_volume, 91 | TickTypes.CLOSE : self._set_close, 92 | TickTypes.BID_OPTION : self._set_bid, 93 | TickTypes.ASK_OPTION : self._set_ask, 94 | TickTypes.LAST_OPTION : self._set_last, 95 | TickTypes.OPEN : self._set_open, 96 | TickTypes.OPEN_INTEREST : self._set_open_interest, 97 | TickTypes.OPTION_IMPLIED_VOL : self._set_implied_vol, 98 | TickTypes.OPTION_CALL_OPEN_INTEREST : self._set_open_interest, 99 | TickTypes.OPTION_PUT_OPEN_INTEREST : self._set_open_interest 100 | } 101 | 102 | # Configure message handlers and connect 103 | self.conn.register(self._account_handler, 'UpdateAccountValue') 104 | self.conn.register(self._tick_handler, message.tickSize, message.tickPrice) 105 | self.conn.register(self._detail_handler, 'ContractDetails') 106 | self.conn.register(self._detail_end_handler, 'ContractDetailsEnd') 107 | self.conn.register(self._open_order_handler, 'OpenOrder') 108 | self.conn.register(self._open_order_end_handler, 'OpenOrderEnd') 109 | self.conn.register(self._order_status_handler, 'OrderStatus') 110 | self.conn.register(self._positions_handler, 'Position') 111 | self.conn.register(self._positions_end_handler, 'PositionEnd') 112 | self.conn.registerAll(self._order_id_handler) 113 | self.conn.connect() 114 | 115 | # Order id handler assignment is not working, so need to hack a way to extract valid id messages from All 116 | # Came up with very dirty triple exception hack. Use the fact that valid id messages have only a single key, orderId 117 | # Since other message types also have this key, we can cause exceptions on valid id message keys by trying to reference 118 | # aspects of the msg that don't exist for valid id messages. Absolutely disgusting, but I'm proud of it in a grotesque way. 119 | def _order_id_handler(self, msg): 120 | # Excpetions in this first statement filter out messages with no order id 121 | # The if statement filters out messages that have an order id identitcal to current one 122 | # logging.debug('Message received: ' + str(msg)) 123 | try: 124 | # if this is the first call, order_id will be none, so we need to account for this 125 | if self.order_id is not None: 126 | if msg.orderId == self.order_id: 127 | return 128 | except: 129 | return 130 | 131 | # filter out messages that have a valid status 132 | try: 133 | test = msg.status 134 | except: 135 | # filter out messages that have a valid contract 136 | try: 137 | test = msg.contract 138 | except: 139 | # only fresh valid id msg can be left at this point 140 | self.order_id = msg.orderId 141 | self.id_ready = True 142 | 143 | # reset data after it has been parsed to avoid double-reading 144 | def _reset_account_data(self): 145 | self.account_value = None 146 | 147 | # reset data after it has been parsed to avoid double-reading 148 | def _reset_tick_data(self): 149 | self.ticker = None 150 | self.key = None 151 | self.bid = None 152 | self.ask = None 153 | self.last = None 154 | self.volume = None 155 | self.open = None 156 | self.close = None 157 | self.open_interest = None 158 | 159 | # Handler for account information messages 160 | def _account_handler(self, msg): 161 | if msg.key=='NetLiquidation': 162 | self.account_value = msg.value 163 | 164 | # Handler for option/stock quote messages 165 | def _tick_handler(self, msg): 166 | # only handle messages associated with current tick id and for which we have callbacks 167 | if msg.field in self.tick_callbacks.keys() and msg.tickerId==self.tick_id: 168 | self.tick_callbacks[msg.field](msg) 169 | self.tick_cnt = self.tick_cnt + 1 170 | 171 | # Handler for contract detail messages 172 | def _detail_handler(self, msg): 173 | if msg.reqId==self.detail_id: 174 | self.contract_list.append(msg.contractDetails.m_summary) 175 | 176 | # Handler for the termination of contract details 177 | def _detail_end_handler(self, msg): 178 | self.detail_ready = True 179 | 180 | # Handler for open orders 181 | def _open_order_handler(self, msg): 182 | self.open_id_list.append(msg.orderId) 183 | 184 | # Handler for the end of open order messages 185 | def _open_order_end_handler(self, msg): 186 | self.open_order_ready = True 187 | 188 | # Handler for order status messages 189 | def _order_status_handler(self, msg): 190 | if self.search_id is not None: 191 | if self.search_id == msg.orderId: 192 | self.filled_quantity = msg.filled 193 | self.order_status = msg.status 194 | 195 | # Handler for current position data 196 | def _positions_handler(self, msg): 197 | cont = msg.contract 198 | pos = {} 199 | pos['quantity'] = msg.pos 200 | pos['cost'] = msg.avgCost 201 | pos['ticker'] = cont.m_symbol 202 | pos['type'] = cont.m_secType 203 | if pos['type'] == 'OPT': 204 | pos['right'] = cont.m_right 205 | pos['expiry'] = datetime.datetime.strptime(cont.m_expiry, "%Y%m%d").date() 206 | pos['strike'] = cont.m_strike 207 | self.position_list.append(pos) 208 | 209 | # Handler for the end of position messages 210 | def _positions_end_handler(self, msg): 211 | self.positions_ready = True 212 | 213 | # Called from the tick handler when corresponding message received 214 | # Callbacks assigned in __init__ 215 | def _set_bid(self, msg): 216 | self.bid = msg.price 217 | def _set_ask(self, msg): 218 | self.ask = msg.price 219 | def _set_open(self, msg): 220 | self.open = msg.price 221 | def _set_last(self, msg): 222 | self.last = msg.price 223 | def _set_close(self, msg): 224 | self.close = msg.price 225 | def _set_volume(self, msg): 226 | self.volume = msg.size 227 | def _set_implied_vol(self, msg): 228 | self.implied_vol = msg.size 229 | def _set_open_interest(self, msg): 230 | self.open_interest = msg.size 231 | 232 | # Construct option contract from given data 233 | def _make_option_contract(self, ticker, exp, right, strike): 234 | cont = Contract() 235 | cont.m_symbol = ticker 236 | cont.m_secType = 'OPT' 237 | cont.m_right = right 238 | cont.m_expiry = exp.strftime('%Y%m%d') 239 | cont.m_strike = float(strike) 240 | cont.m_exchange = 'SMART' 241 | cont.m_currency = 'USD' 242 | return cont 243 | 244 | # Construct a partial contract, in order to get available contracts for given ticker from TWS/Gateway 245 | def _make_partial_option_contract(self, ticker): 246 | cont = Contract() 247 | cont.m_symbol = ticker 248 | cont.m_secType = 'OPT' 249 | cont.m_exchange = 'SMART' 250 | cont.m_currency = 'USD' 251 | return cont 252 | 253 | # Construct stock contract from given data 254 | def _make_stock_contract(self, ticker): 255 | cont = Contract() 256 | cont.m_symbol = ticker 257 | cont.m_secType = 'STK' 258 | cont.m_exchange = 'SMART' 259 | cont.m_currency = 'USD' 260 | return cont 261 | 262 | # waits for all fields of a stock quote to be filled 263 | def _wait_for_stock_quote(self): 264 | # set timeout 265 | timeout = time.time() + self.quote_timeout 266 | # not thrilled with this way of waiting, but can't think of an alternative for now 267 | while(self.tick_cnt < self.stk_tick_max): 268 | time.sleep(.1) 269 | if time.time() > timeout: 270 | break 271 | 272 | # waits for all fields of an option quote to be filled 273 | def _wait_for_option_quote(self): 274 | # set timeout 275 | timeout = time.time() + self.quote_timeout 276 | # not thrilled with this way of waiting, but can't think of an alternative for now 277 | while(self.tick_cnt < self.opt_tick_max): 278 | # or self.implied_vol is None or self.open_interest is None): 279 | time.sleep(.1) 280 | if time.time() > timeout: 281 | break 282 | 283 | # Get all contracts available for given ticker 284 | def _get_contract_details(self, ticker): 285 | cont = self._make_partial_option_contract(ticker) 286 | logging.debug('Requesting details on ' + ticker) 287 | self.conn.reqContractDetails(self.detail_id, cont) 288 | logging.debug('Starting timeout timer for contract details') 289 | timeout = time.time() + 90 290 | while not self.detail_ready: 291 | time.sleep(.1) 292 | if time.time() > timeout: 293 | break 294 | logging.debug('Exiting contract details wait') 295 | self.detail_ready = False 296 | self.list_ticker = ticker 297 | self.detail_id = self.detail_id % self.id_max + 1 298 | 299 | # Get the next valid order id 300 | def _set_order_id(self): 301 | # request id and wait for it to be populated 302 | self.conn.reqIds(1) 303 | while not self.id_ready: 304 | time.sleep(.1) 305 | # reset the id_ready flag 306 | self.id_ready = False 307 | 308 | # Make an order to submit to TWS 309 | # For now automatically give everything Time-in-force of the day. No reason to do good-til-cancel from an algo really. 310 | # Also, all orders will be limit orders. Market orders from an algo sounds like the start of a horror story. 311 | def _make_order(self, action, price, quantity): 312 | order = Order() 313 | order.m_action = action 314 | order.m_lmtPrice = price 315 | order.m_totalQuantity = quantity 316 | order.m_orderId = self.order_id 317 | order.m_clientId = 0 318 | order.m_permid = 0 319 | order.m_auxPrice = 0 320 | order.m_tif = 'DAY' 321 | order.m_transmit = True 322 | order.m_orderType = 'LMT' 323 | return order 324 | 325 | # EXPOSED METHODS 326 | # returns account value as a float 327 | def get_account_value(self): 328 | self.conn.reqAccountUpdates(1, '') 329 | while(self.account_value is None): 330 | time.sleep(.1) 331 | acct_val = self.account_value 332 | self._reset_account_data() 333 | return acct_val 334 | 335 | # returns a dict of stock quote data 336 | def get_stock_quote(self, ticker): 337 | # create contract for mkt data request, and send request 338 | cont = self._make_stock_contract(ticker) 339 | self.tick_cnt = 0 340 | self.conn.reqMktData(self.tick_id, cont, '', False) 341 | 342 | # wait for data fields to be populated by msg handlers 343 | self._wait_for_stock_quote() 344 | quote_dict = { 345 | 'bid' : self.bid, 346 | 'ask' : self.ask, 347 | 'last' : self.last, 348 | 'volume' : self.volume, 349 | 'close' : self.close 350 | } 351 | 352 | # Cancel current mkt data request and increment tick id 353 | self.conn.cancelMktData(self.tick_id) 354 | self.tick_id = self.tick_id % self.id_max + 1 355 | 356 | # reset tick data fields to None, and return quote 357 | self._reset_tick_data() 358 | self.tick_cnt = 0 359 | 360 | # if all fields are None, log an error 361 | # for now don't change return value. later possible return None in this case, not sure 362 | if all(value == None for value in quote_dict.values()): 363 | logging.error('No option data found. Could be a problem with data servers.') 364 | 365 | return quote_dict 366 | 367 | # returns a dict of option quote data 368 | def get_option_quote(self, ticker, date, right, strike): 369 | logging.debug('Received quote request with the following data: ' + str(locals())) 370 | # create option contract for data request, and send request 371 | cont = self._make_option_contract(ticker, date, right, strike) 372 | self.tick_cnt = 0 373 | self.conn.reqMktData(self.tick_id, cont, '', False) 374 | 375 | # wait for data fields to be populated by msg handlers 376 | self._wait_for_option_quote() 377 | quote_dict = { 378 | 'bid' : self.bid, 379 | 'ask' : self.ask, 380 | 'last' : self.last, 381 | 'close' : self.close, 382 | 'open' : self.open, 383 | 'volume' : self.volume, 384 | } 385 | 386 | # Cancel current mkt data request and increment tick id 387 | self.conn.cancelMktData(self.tick_id) 388 | self.tick_id = self.tick_id % self.id_max + 1 389 | 390 | # reset tick data fields to None, and return quote 391 | self._reset_tick_data() 392 | self.tick_cnt = 0 393 | 394 | # if all fields are None, log an error 395 | # for now don't change return value. later possible return None in this case, not sure 396 | if all(value == None for value in quote_dict.values()): 397 | logging.error('No option data found. Could be a problem with data servers.') 398 | 399 | return quote_dict 400 | 401 | # Returns possible expiries for given ticker 402 | # Dates will be returned in string format, wasn't certain whether to use date or str 403 | # Decided on date since user-end operations will likely be on date objects, and returning dates improves encapsulation 404 | # (Be careful not to spell get_expires by accident) 405 | def get_expiries(self, ticker): 406 | # If the ticker is not already stored, then we need to get contracts again 407 | # Otherwise the cached contracts apply to this ticker, and we need not get new data 408 | if ticker != self.list_ticker: 409 | self._get_contract_details(ticker) 410 | # Extract unique dates from the contract details list (crazy pythonic method) 411 | return list(set([datetime.datetime.strptime(c.m_expiry, "%Y%m%d").date() for c in self.contract_list])) 412 | 413 | # Return strikes available for given expiry. Expiry input must be date for consistency with get_expiries method 414 | def get_strikes(self, ticker, expiry): 415 | # Convert the date input to a string. Error if wrong type 416 | if type(expiry) is datetime.date: 417 | exp_str = expiry.strftime('%Y%m%d') 418 | else: 419 | logging.error('In get_strikes: Unrecognized expiry type %s, returning None.', str(e_type)) 420 | return None 421 | 422 | # If the ticker is not already stored, then we need to get contracts again 423 | # Otherwise the cached contracts apply to this ticker, and we need not get new data 424 | if ticker != self.list_ticker: 425 | self._get_contract_details(ticker) 426 | # Extract strikes for which the contract expiry matches the given (crazy pythonic method) 427 | return list(set([c.m_strike for c in self.contract_list if c.m_expiry == exp_str])) 428 | 429 | # Place limit order for options contract 430 | # Recommend using keyword argument entry for this method, there are many inputs 431 | # action must be 'BUY' or 'SELL' 432 | # If no order id is supplied, the interface automatically gets the next valid order id to use 433 | # Supplying an order_id manually is not recommended. If you'd like to modify an existing order, you should use the modify_option_order command 434 | def place_option_order(self, action, ticker, expiry, right, strike, price, quantity, order_id=None): 435 | logging.debug('Received order request with the following data: ' + str(locals())) 436 | # Check args 437 | if action != 'BUY' and action != 'SELL': 438 | logging.error('Unrecognized action %s. Action must be BUY or SELL. Returning None', str(action)) 439 | return None 440 | if right != 'P' and right != 'C': 441 | logging.error('Unrecognized right %s. Right must be P or C. Returning None', str(right)) 442 | return None 443 | 444 | # get valid order id 445 | if order_id is None: 446 | self._set_order_id() 447 | order_id = self.order_id 448 | 449 | # Compile arguments into dict for order storage 450 | order_dict = dict(locals()) 451 | del order_dict['self'] 452 | order_dict['order_id'] = order_id 453 | 454 | # first make the contract and the order 455 | order = self._make_order(action, price, quantity) 456 | cont = self._make_option_contract(ticker, expiry, right, strike) 457 | self.conn.placeOrder(order_id, cont, order) 458 | 459 | # return order_id as a handle to this order, and increment current order id 460 | return order_id 461 | 462 | # Get order status of order with id order_id 463 | # Returns a two item list with a string status and int order_quantity 464 | def get_order_status(self, order_id): 465 | # reset order status, and search for order with id order_id 466 | self.order_status = None 467 | self.search_id = order_id 468 | time.sleep(1) 469 | self.conn.reqOpenOrders() 470 | timeout = time.time() + 10 471 | while self.order_status is None: 472 | time.sleep(.1) 473 | if time.time() > timeout: 474 | logging.error('Order status timed out. Order must have been filled or cancelled already') 475 | return None, None 476 | self.search_id = None 477 | return self.order_status, self.filled_quantity 478 | 479 | # Get a list of all current holdings 480 | def get_positions(self): 481 | logging.debug('Requesting positions...') 482 | self.conn.reqPositions() 483 | while not self.positions_ready: 484 | time.sleep(.1) 485 | self.positions_ready = False 486 | ret_list = self.position_list 487 | self.position_list = [] 488 | return ret_list 489 | 490 | # Get quantity of a single stock position 491 | def get_stock_position(self): 492 | pos_list = self.get_positions() 493 | 494 | # Get a list of open order ids 495 | def get_open_order_ids(self): 496 | self.open_id_list = [] 497 | self.open_order_ready = False 498 | self.conn.reqOpenOrders() 499 | timeout = time.time() + 10 500 | while not self.open_order_ready: 501 | if time.time() > timeout: 502 | logging.error('Open order id request timed out. List may be incomplete') 503 | break 504 | time.sleep(.1) 505 | return list(set(self.open_id_list)) 506 | 507 | # Cancel single order with order_id 508 | def cancel_order(self, order_id): 509 | self.conn.cancelOrder(order_id) 510 | timeout = time.time() + 60 511 | logging.debug('starting order cancel check') 512 | while True: 513 | logging.debug('getting order status') 514 | status, filled = self.get_order_status(order_id) 515 | if status is None: 516 | logging.info('Order returned no status. Must already be filled or cancelled.') 517 | return False, None 518 | if status == 'cancelled' or status == 'Cancelled': 519 | logging.info('Order cancelled successfully.') 520 | if filled is not None: 521 | logging.info('Filled quantity was %d', filled) 522 | return True, filled 523 | elif status == 'filled' or status == 'Filled': 524 | logging.info('Order was filled before it could be cancelled.') 525 | if filled is not None: 526 | logging.info('Filled quantity was %d', filled) 527 | if time.time() > timeout: 528 | logging.info('Order cancel timed out. Order has not been confirmed for cancel.') 529 | if filled is not None: 530 | logging.info('Filled quantity was %d', filled) 531 | return False, filled 532 | time.sleep(.1) 533 | 534 | 535 | # Cancel all open orders 536 | def cancel_all_orders(self): 537 | self.conn.reqGlobalCancel(); 538 | 539 | # Shut down the interface 540 | def shut_down(self): 541 | logging.info('Shutting down interface.') 542 | return None 543 | 544 | # test main 545 | def main(): 546 | try: 547 | ibif = IbInterface() 548 | exp = datetime.date(2017, 12, 1) 549 | ibif.place_option_order(action='SELL', ticker='NUE', right='P', strike=56.0, quantity=1, expiry=exp, price=1.0) 550 | time.sleep(5) 551 | id_list = ibif.get_open_order_ids() 552 | print(id_list) 553 | print(str(ibif.get_order_status(125))) 554 | for oid in id_list: 555 | print(str(ibif.get_order_status(oid))) 556 | except: 557 | ibif.shut_down() 558 | 559 | if __name__ == '__main__': 560 | main() -------------------------------------------------------------------------------- /OptionSeller.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | # used for sleep 4 | import time 5 | # date operations 6 | import datetime 7 | # used for exiting program upon error 8 | import sys 9 | # library for interacing with csv files 10 | import csv 11 | # python logging library for monitoring and debugging 12 | import logging 13 | # interface class to IB market data 14 | from ibInterface import IbInterface 15 | from threading import Thread 16 | 17 | # for catching sigint 18 | import signal 19 | 20 | # CSV file for stock data and appropriate parameters 21 | STOCK_CSV = 'default.csv' 22 | # Conf file for global configuration parameters of the OptionSeller 23 | GLOBAL_CONF = 'global.conf' 24 | 25 | # Set logging level 26 | logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) 27 | 28 | # Class for selling put and call options on desired stocks at desired target prices 29 | class OptionSeller: 30 | def __init__(self): 31 | # Parse global parameters 32 | 33 | # extract data from stock csv file into a list of dicts for easy use 34 | self.stock_csv = STOCK_CSV 35 | self.stock_list_of_dicts = [] 36 | self.parse_stocks() 37 | 38 | # Buy and sell thresholds for options selling 39 | self.buy_thresh = .02 40 | self.sell_thresh = .01 41 | 42 | # Amount of loops to wait before modifying order, and amount of modifications before giving up 43 | self.loop_max = 2 44 | self.mod_max = 2 45 | 46 | # Interface to IB api 47 | self.ibif = IbInterface() 48 | 49 | logging.debug('Imported the following stock data: ') 50 | for row in self.stock_list_of_dicts: 51 | logging.debug(row) 52 | 53 | self.quote_list = [] 54 | self.position_list = [] 55 | self.call_order_list = [] 56 | self.put_order_list = [] 57 | self.trade_thread = Thread(target=self.trade_loop) 58 | self.trade = True 59 | self.trade_thread.start() 60 | 61 | # extract data from the stock csv file 62 | def parse_stocks(self): 63 | # declare array to store data from csv 64 | logging.debug("Parsing stock csv") 65 | data_array = [] 66 | with open(self.stock_csv, 'r') as csvfile: 67 | stock_reader = csv.reader(csvfile) 68 | for row in stock_reader: 69 | logging.debug(str(row)) 70 | data_array.append(row) 71 | 72 | # the first row will be keys, and the next rows will be actual stock data 73 | # zip the data into a convenient list of dicts 74 | keys = data_array[0] 75 | logging.debug("zipping stock data") 76 | for i in range (1, len(data_array)): 77 | stock_dict = dict(zip(keys, data_array[i])) 78 | stock_dict['targetBuy'] = float(stock_dict['targetBuy']) 79 | stock_dict['targetSell'] = float(stock_dict['targetSell']) 80 | stock_dict['weightTarget'] = float(stock_dict['weightTarget']) 81 | self.stock_list_of_dicts.append(stock_dict) 82 | 83 | 84 | # Get quotes of the stocks of interest from the ib interface 85 | def get_quotes(self): 86 | logging.debug("Getting quotes...") 87 | self.quote_list = [] 88 | for stock in self.stock_list_of_dicts: 89 | quote_data = self.ibif.get_stock_quote(stock['ticker']) 90 | if quote_data['last'] is None: 91 | price = quote_data['close'] 92 | else: 93 | price = quote_data['last'] 94 | logging.debug('Last price of %s: %f', stock['ticker'], price) 95 | quote_data['ticker'] = stock['ticker'] 96 | self.quote_list.append(quote_data) 97 | 98 | # Get current positions from the ib interface 99 | def get_positions(self): 100 | self.position_list = self.ibif.get_positions() 101 | logging.debug("Current holdings: " + str(self.position_list)) 102 | 103 | # Update current orders, modifying or cancelling ones that require it 104 | def update_orders(self): 105 | # First, remove any orders that are no longer open 106 | open_list = self.ibif.get_open_order_ids() 107 | logging.debug('Open order ids: ' + str(open_list)) 108 | self.put_order_list = [order for order in self.put_order_list if order['id'] in open_list] 109 | self.call_order_list = [order for order in self.call_order_list if order['id'] in open_list] 110 | logging.debug('Current put orders ' + str(self.put_order_list)) 111 | logging.debug('Current call orders ' + str(self.call_order_list)) 112 | 113 | # Then, iterate through open orders and leave, modify, or cancel them 114 | for order in self.put_order_list + self.call_order_list: 115 | status, filledQuant = self.ibif.get_order_status(order['id']) 116 | if status is not None: 117 | logging.debug('Order status is ' + str(status)) 118 | else: 119 | logging.debug('Order did not return a status. Must be closed already.') 120 | return 121 | if filledQuant > 0 and filledQuant < order['quantity']: 122 | self.handle_partial_fill(order) 123 | if order['right'] == 'P': 124 | self.put_order_list = [o for o in self.put_order_list if o['id'] != order['id']] 125 | else: 126 | self.call_order_list = [o for o in self.call_order_list if o['id'] != order['id']] 127 | elif status == 'submitted' or status == 'Submitted': 128 | self.modify_option_sell_order(order) 129 | # This will only happen if order has been filled between now and the ibif order check 130 | elif status == 'filled' or status == 'Filled': 131 | self.put_order_list = [o for o in self.put_order_list if o['id'] != order['id']] 132 | # If order status isn't submitted or filled then we should do nothing at this point 133 | else: 134 | logging.debug('Order not yet submitted. Doing nothing...') 135 | 136 | # Return the stock holdings for the given ticker 137 | def get_stock_holding(self, ticker): 138 | for position in self.position_list: 139 | if position['ticker'] == ticker and position['type'] == 'STK': 140 | return position 141 | return None 142 | 143 | # Return a list of option holdings for the current ticker 144 | def get_option_holdings(self, ticker): 145 | ret_list = [position for position in self.position_list if position['ticker'] == ticker and position['type'] == 'OPT'] 146 | if ret_list: 147 | return ret_list 148 | else: 149 | return None 150 | 151 | # Extract a quote from the current quote list 152 | def get_current_quote(self, ticker): 153 | for quote in self.quote_list: 154 | if quote['ticker'] == ticker: 155 | return quote 156 | 157 | # Looping method to execute the trading strategy 158 | def trade_loop(self): 159 | logging.debug("In trade loop...") 160 | while self.trade: 161 | # Get data from the ib interface 162 | self.get_quotes() 163 | self.get_positions() 164 | # Update current orders 165 | self.update_orders() 166 | for stock in self.stock_list_of_dicts: 167 | ticker = stock['ticker'] 168 | logging.debug("Executing strategy for " + ticker) 169 | # If we have open orders for this ticker, we should do nothing 170 | existing_order = False 171 | for order in self.put_order_list + self.call_order_list: 172 | if order['ticker'] == stock['ticker']: 173 | existing_order = True 174 | break 175 | if existing_order: 176 | logging.info('Order is open for %s. Moving on...', ticker) 177 | continue 178 | 179 | # At this point, no open orders. gather all the data we need to make a decision, and pass it to the decision making method 180 | logging.debug('No open orders for ' + ticker) 181 | # Retrieve position once more to ensure an order was not filled between our last update and now 182 | self.get_positions() 183 | stk_hold = self.get_stock_holding(ticker) 184 | opt_hold = self.get_option_holdings(ticker) 185 | quote = self.get_current_quote(ticker) 186 | self.trade_decision(stock, stk_hold, opt_hold, quote) 187 | time.sleep(10) 188 | 189 | # Make a decision on what to do with the given ticker 190 | def trade_decision(self, stock, stk_hold, opt_hold, quote): 191 | ticker = stock['ticker'] 192 | if quote['last'] is None: 193 | price = quote['close'] 194 | else: 195 | price = quote['last'] 196 | # Determine how far away we are from targets 197 | buy_diff = (price - stock['targetBuy'])/stock['targetBuy'] 198 | sell_diff = (stock['targetSell'] - price)/stock['targetSell'] 199 | 200 | # Determine current call and put holdings 201 | call_hold = None 202 | put_hold = None 203 | put_exposure = 0 204 | call_exposure = 0 205 | if opt_hold: 206 | put_hold = [p for p in opt_hold if p['right'] == 'P'] 207 | call_hold = [c for c in opt_hold if c['right'] == 'C'] 208 | if put_hold: 209 | put_exposure = sum([p['quantity'] for p in put_hold]) 210 | if call_hold: 211 | call_exposure = sum([c['quantity'] for c in call_hold]) 212 | 213 | logging.debug('Current put exposure on %s: %d', ticker, put_exposure) 214 | logging.debug('Current call exposure on %s: %d', ticker, call_exposure) 215 | 216 | # Determine target quantity for positions. Average into and out of positions based on the target weight 217 | if stock['weightTarget'] <= 300: 218 | # If target is 100 or 200, just do 1 contract (100 shares) at a time 219 | target_quantity = 1 220 | else: 221 | # Otherwise, handle the position in thirds, rounding up 222 | target_quantity = round(stock['weightTarget']/300.0 + .5) 223 | 224 | # if stk_hold is None and opt_hold is None: 225 | if stk_hold is None: 226 | if buy_diff < self.buy_thresh: 227 | if put_exposure == 0: 228 | self.sell_puts(stock, price, target_quantity) 229 | else: 230 | logging.debug('We are near price target, but we are already short puts on %s. Doing nothing.', ticker) 231 | return 232 | else: 233 | logging.debug('We are not near the buy target for %s, and we do not have any shares. Doing nothing.', ticker) 234 | return 235 | 236 | # If we currently hold the stock 237 | else: 238 | # If current holdings between 0 and target holdings, sell more puts and sell matching strangle calls 239 | if stk_hold['quantity'] < stock['weightTarget']: 240 | if not call_hold: 241 | # Sell calls on all held shares. Might change this to match the put quantity later, not set on it. 242 | self.sell_strangle_calls(stock, price, stk_hold['quantity']/100, stk_hold) 243 | if not put_hold: 244 | # Put quantity will either be the target quantity calculated earlier or the amount left until weight target hit, whichever is smaller 245 | # TEST 246 | exp1 = datetime.date(2017, 11, 24) 247 | q = self.ibif.get_option_quote('NUE', exp1, 'P', 55.5) 248 | print('After sell_strangle_calls exit', q) 249 | put_quantity = min((stock['weightTarget'] - stk_hold['quantity'])/100, target_quantity) 250 | self.sell_puts(stock, price, put_quantity) 251 | return 252 | 253 | # If current position is greater than or equal to weightTarget, and we are within sell threshold, sell calls 254 | elif call_exposure == 0 and sell_diff < self.sell_thresh: 255 | # Quantity of calls to sell will be the min of the currently held 100s of shares and the target quantity 256 | call_quantity = min(stk_hold['quantity']/100, target_quantity) 257 | self.sell_exit_calls(stock, price, call_quantity, stk_hold) 258 | return 259 | logging.info('Nothing to do for ' + ticker) 260 | 261 | # We run into an odd edge case when an order has partially filled. 262 | # I don't know what happens to a partially filled order when we attempt to modify it, and unfortunately 263 | # this is a very difficult situation for which to develop a test case. 264 | # So it seems best to cancel and re-send with new desired quantity, since we know exactly what will happen that way 265 | def handle_partial_fill(self, order): 266 | cancelled_flag, filled = self.ibif.cancel_order(order['id']) 267 | # check if the order was cancelled property, and get the final amount of contracts filled 268 | if cancelled_flag: 269 | new_quant = order['quantity'] - filled 270 | else: 271 | logging.error('Order was not cancelled properly.') 272 | if filled == order['quantity']: 273 | logging.error('Order was filled completely before cancellation. Returning...') 274 | return 275 | else: 276 | logging.error('Order was not completely filled, but something went wrong in cancellation. Returning...') 277 | return 278 | 279 | order['quantity'] = new_quant 280 | # copy dict for easy sending to ibif 281 | send_order = order.copy() 282 | del send_order['loop_cnt'] 283 | del send_order['mod_cnt'] 284 | del send_order['id'] 285 | order['id'] = self.ibif.place_option_order(**send_order) 286 | order['loop_cnt'] = 0 287 | order['mod_cnt'] = 0 288 | if order['right'] == 'C': 289 | self.call_order_list.append(order) 290 | else: 291 | self.put_order_list.append(order) 292 | 293 | # Reduce asking price if appropriate. Otherwise just increment loop cnt for the order, or cancel it 294 | def modify_option_sell_order(self, order_dict): 295 | loop_cnt = order_dict['loop_cnt'] 296 | mod_cnt = order_dict['mod_cnt'] 297 | # If this order shouldn't be modified yet 298 | if loop_cnt < self.loop_max: 299 | logging.debug('Order with id %d should not be modified yet', order_dict['id']) 300 | order_dict['loop_cnt'] = loop_cnt + 1 301 | # If it should be modified and hasn't hit the max modifications yet 302 | # To modify an order with the IB api, just resubmit with the same order id 303 | elif mod_cnt < self.mod_max: 304 | logging.debug('Modifying order with id %d', order_dict['id']) 305 | # decrement price by .01 306 | order_dict['price'] = order_dict['price'] - .01 307 | # copy dict for easy sending to ibif 308 | send_order = order_dict.copy() 309 | send_order['order_id'] = order_dict['id'] 310 | del send_order['loop_cnt'] 311 | del send_order['mod_cnt'] 312 | del send_order['id'] 313 | self.ibif.place_option_order(**send_order) 314 | order_dict['loop_cnt'] = 0 315 | order_dict['mod_cnt'] = mod_cnt + 1 316 | # If it has hit max mods, should be cancelled 317 | else: 318 | logging.debug('Order with id %d has been modified too many times. Cancelling...', order_dict['id']) 319 | self.ibif.cancel_order(order_dict['id']) 320 | if order_dict['right'] == 'P': 321 | self.put_order_list = [o for o in self.put_order_list if o['id'] != order_dict['id']] 322 | else: 323 | self.call_order_list = [o for o in self.call_order_list if o['id'] != order_dict['id']] 324 | return True 325 | 326 | # If we get here, we need to modify the order list and return that the order has not been cancelled 327 | for order in self.put_order_list + self.call_order_list: 328 | if order['id'] == order_dict['id']: 329 | order['loop_cnt'] = order_dict['loop_cnt'] 330 | order['mod_cnt'] = order_dict['mod_cnt'] 331 | order['price'] = order_dict['price'] 332 | return False 333 | 334 | logging.error('Order not found in the order list. something went wrong...') 335 | return False 336 | 337 | # Sell puts for the given ticker 338 | def sell_puts(self, stock, stk_price, quantity): 339 | logging.info('Selling puts on ' + stock['ticker']) 340 | ticker = stock['ticker'] 341 | # Find the best option strike and expiry 342 | target = self.search_for_option(ticker, stk_price, 'put', stock) 343 | # If we found a target, submit an order 344 | if target is not None: 345 | logging.info('Selling put on %s with strike %f and expiry %s for price %f', ticker, target['strike'], str(target['expiry']), target['price']) 346 | # append target dict for easy sending to ibif 347 | target['ticker'] = ticker 348 | target['quantity'] = int(quantity) 349 | target['right'] = 'P' 350 | target['action'] = 'SELL' 351 | order_id = self.ibif.place_option_order(**target) 352 | # append with bookkeeping attributes for order monitoring 353 | target['id'] = order_id 354 | target['loop_cnt'] = 0 355 | target['mod_cnt'] = 0 356 | self.put_order_list.append(target) 357 | else: 358 | logging.warning('No suitable put found to sell for %s', ticker) 359 | 360 | # Sell calls as part of a strangle. Called when we hold the stock but don't hold the target weight yet 361 | def sell_strangle_calls(self, stock, stk_price, quantity, stk_hold): 362 | ticker = stock['ticker'] 363 | logging.info('Selling calls on ' + ticker + 'as part of a strangle') 364 | # TEST 365 | exp1 = datetime.date(2017, 11, 24) 366 | q = self.ibif.get_option_quote('NUE', exp1, 'P', 55.5) 367 | print('Before search for option', q) 368 | # Get available options expiries and sort them 369 | target = self.search_for_option(ticker, stk_price, 'strangle_call', stock, stk_hold) 370 | # TEST 371 | exp1 = datetime.date(2017, 11, 24) 372 | q = self.ibif.get_option_quote('NUE', exp1, 'P', 55.5) 373 | print('After search for option', q) 374 | # If we found a target, submit an order 375 | if target is not None: 376 | logging.info('Selling call on %s with strike %f and expiry %s for price %f', ticker, target['strike'], str(target['expiry']), target['price']) 377 | # append target dict for easy sending to ibif 378 | target['ticker'] = ticker 379 | target['quantity'] = int(quantity) 380 | target['right'] = 'C' 381 | target['action'] = 'SELL' 382 | order_id = self.ibif.place_option_order(**target) 383 | # append with bookkeeping attributes for order monitoring 384 | target['id'] = order_id 385 | target['loop_cnt'] = 0 386 | target['mod_cnt'] = 0 387 | self.call_order_list.append(target) 388 | # TEST 389 | exp1 = datetime.date(2017, 11, 24) 390 | q = self.ibif.get_option_quote('NUE', exp1, 'P', 55.5) 391 | print('At the end of sell strangle calls', q) 392 | else: 393 | logging.warning('No suitable strangle call found to sell for %s', ticker) 394 | 395 | # Sell calls to begin exiting a stock position. Will be called when target weight is equal to target quantity, and we are near sell target 396 | def sell_exit_calls(self, stock, stk_price, quantity, stk_hold): 397 | ticker = stock['ticker'] 398 | logging.info('Selling calls on ' + ticker + ' to exit the position') 399 | target = self.search_for_option(ticker, stk_price, 'exit_call', stock, stk_hold) 400 | if target is not None: 401 | logging.info('Selling call on %s with strike %f and expiry %s for price %f', ticker, target['strike'], str(target['expiry']), target['price']) 402 | # append target dict for easy sending to ibif 403 | target['ticker'] = ticker 404 | target['quantity'] = int(quantity) 405 | target['right'] = 'C' 406 | target['action'] = 'SELL' 407 | order_id = self.ibif.place_option_order(**target) 408 | # append with bookkeeping attributes for order monitoring 409 | target['id'] = order_id 410 | target['loop_cnt'] = 0 411 | target['mod_cnt'] = 0 412 | self.call_order_list.append(target) 413 | else: 414 | logging.warning('No suitable strangle call found to sell for %s', ticker) 415 | 416 | # Find a suitable option contract for the given situation 417 | def search_for_option(self, ticker, stk_price, strategy, stock, stk_hold=None): 418 | date_list = self.ibif.get_expiries(ticker) 419 | # Only get expiries that are one month or less away 420 | date_list = [date for date in date_list if (date - datetime.datetime.now().date()).days <= 31] 421 | date_list.sort() 422 | logging.debug('Looking for options on the following dates: ' + str(date_list)) 423 | # Set right to use for contracts 424 | if strategy == 'exit_call' or strategy == 'strangle_call': 425 | right = 'C' 426 | else: 427 | right = 'P' 428 | # Initialize target result to None 429 | target = None 430 | for expiry in date_list: 431 | logging.debug('On expiry ' + str(expiry)) 432 | # Get available strikes 433 | strike_list = self.ibif.get_strikes(ticker, expiry) 434 | # find the best strike to use from the list. Criteria changes based on the strategy being implemented 435 | strike = self.find_best_strike(stk_price, strike_list, strategy, stock, stk_hold) 436 | # Quote the selected option 437 | opt_quote = self.ibif.get_option_quote(ticker, expiry, right, strike) 438 | logging.debug('Quote for this option: ' + str(opt_quote)) 439 | # offer = round((opt_quote['bid'] + opt_quote['ask'])/2.0) 440 | if all(value == None for value in opt_quote.values()): 441 | logging.error('Empty quote returned. Possible problem with data connection. Skipping this strike') 442 | continue 443 | if opt_quote['last'] is None: 444 | offer = opt_quote['close'] 445 | else: 446 | offer = opt_quote['last'] 447 | # Check if price is good enough for the expiry 448 | days2exp = (expiry - datetime.datetime.now().date()).days 449 | # Target weeklies and bi-weeklies if the price is good enough 450 | logging.debug('Offer for strike %f: %f', strike, offer) 451 | if days2exp <= 4: 452 | if offer is not None: 453 | if offer >= .005*stk_price: 454 | target = {'expiry': expiry, 'strike': strike, 'price': offer} 455 | else: 456 | logging.debug('Price not good enough for weekly') 457 | elif days2exp <= 11: 458 | if offer is not None: 459 | if offer >= .008*stk_price: 460 | if target is None: 461 | target = {'expiry': expiry, 'strike': strike, 'price': offer} 462 | logging.debug('Bi-weekly fits criteria, and the weekly did not') 463 | break 464 | elif offer > 1.8*target['price']: 465 | target = {'expiry': expiry, 'strike': strike, 'price': offer} 466 | logging.info('The bi-weekly offer trumps the weekly offer') 467 | break 468 | else: 469 | logging.debug('Price not good enough for bi-weekly') 470 | if target is not None: 471 | logging.debug('Executing weekly offer') 472 | break 473 | # if not, get the first available expiry with premium > 1% share price 474 | elif offer is not None: 475 | if offer >= .01*stk_price: 476 | target = {'expiry': expiry, 'strike': strike, 'price': offer} 477 | break 478 | return target 479 | 480 | # Find the most fitting strike from the given list for the given strategy 481 | def find_best_strike(self, stk_price, strike_list, strategy, stock, stk_hold=None): 482 | if strategy == 'strangle_call': 483 | cost = stk_hold['cost'] 484 | if stk_price < cost: 485 | # If we have no unrealized profit, sell at first strike above cost of position 486 | return min(s for s in strike_list if s > cost) 487 | else: 488 | # If we have unrealized profit, sell at first strike above current price 489 | return min(s for s in strike_list if s > stk_price) 490 | elif strategy == 'exit_call': 491 | if stk_price > stock['targetSell']: 492 | # Highest ITM call if we are above target 493 | return max(s for s in strike_list if s < stk_price) 494 | else: 495 | # Lowest OTM call if we are below target 496 | return min(s for s in strike_list if s >= stk_price) 497 | elif strategy == 'put': 498 | if stk_price > stock['targetBuy']: 499 | # Highest OTM put if we are above target 500 | return max(s for s in strike_list if s <= stk_price) 501 | else: 502 | # Lowest ITM put if below target 503 | return min(s for s in strike_list if s > stk_price) 504 | 505 | # Shut down the option seller 506 | def shut_down(self): 507 | self.trade = False 508 | self.trade_thread.join() 509 | self.ibif.shut_down() 510 | 511 | 512 | def main(): 513 | try: 514 | ops = OptionSeller() 515 | while True: 516 | time.sleep(.1) 517 | except KeyboardInterrupt: 518 | logging.warning('Received keyboard interrupt. Exiting gracefully...') 519 | ops.shut_down() 520 | except: 521 | logging.warning('Unexpected error. Shutting it down...') 522 | ops.shut_down() 523 | 524 | 525 | if __name__ == '__main__': 526 | main() --------------------------------------------------------------------------------