diff --git a/iron_condor.py b/iron_condor.py index 2a37501..7fea067 100644 --- a/iron_condor.py +++ b/iron_condor.py @@ -1,6 +1,8 @@ import logging +import nest_asyncio import traceback +from dataclasses import replace from datetime import datetime from dotenv import load_dotenv from ibkr import Client, OptionLeg @@ -10,8 +12,73 @@ from os import getenv load_dotenv() +# Allows for starting an event loop even if there's already one running in the current thread. +# Necessary for monitoring spread prices asynchronously while interacting with the IBKR client. +nest_asyncio.apply() + +quantity = 1 + +def monitor_spread_price(short_leg: OptionLeg, long_leg: OptionLeg, stop_price: float, client: Client): + """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + Stop loss orders will not execute if trying to sell back a contract with no bid while paper trading. + Therefore, the spread price must be monitored and the spread manually exited if the stop price is reached. + If there is no bid for the long leg, only the short leg will be exited. + """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + stopped_out = False + market_data = {} # Stores real-time market data for each leg. + + def on_market_data_update(update_event): + # Prevent the trade from being exited multiple times if there are other updates queued. + # This will prevent unintentionally entering new trades. + nonlocal stopped_out + if stopped_out: + return + + # Ensure there is market data for both legs before proceeding. + if short_leg in market_data and long_leg in market_data: + short_contract = market_data[short_leg] + long_contract = market_data[long_leg] + + # If a contract has no bid -1.0 is returned, set it to 0 to avoid negative mid prices. + mid_price_short = (max(short_contract.bid, 0) + short_contract.ask) / 2 + mid_price_long = (max(long_contract.bid, 0) + long_contract.ask) / 2 + current_spread_price = mid_price_short - mid_price_long + + logging.info(f'Short Contract: {short_leg.strike} {short_leg.option_type}') + logging.info(f'Long Contract: {long_leg.strike} {long_leg.option_type}') + logging.info(f'Current Spread Price: {current_spread_price}') + logging.info(f'Stop Price: {stop_price}') + + if current_spread_price >= stop_price: + stopped_out = True + logging.info('Stop price reached or exceeded. Exiting trade.') + + short_leg_exit = replace(short_leg, action = BUY if short_leg.action == SELL else SELL) + long_leg_exit = replace(long_leg, action = BUY if long_leg.action == SELL else SELL) + if long_contract.bid > 0: + client.submit_combo_option_order([short_leg_exit, long_leg_exit], quantity) + logging.info('Whole spread exited.') + else: + client.submit_option_order(short_leg_exit, quantity) + logging.info('Short leg only exited.') + + # Unsubscribe from market data updates once the trade has exited. + for leg in [short_leg, long_leg]: + market_data[leg].updateEvent -= on_market_data_update + + for leg in [short_leg, long_leg]: + option_contract = client.get_option_contract(leg) + leg_market_data = client.get_market_data(option_contract, streaming=True) + market_data[leg] = leg_market_data + leg_market_data.updateEvent += on_market_data_update + def enter_iron_condor(): - logging.basicConfig(filename=f'iron_condor_{datetime.now().strftime("%H%M")}.log', level=logging.INFO) + logging.basicConfig( + filename=f'iron_condor_{datetime.now().strftime("%H%M")}.log', + level=logging.INFO, + format='%(asctime)s : %(levelname)s : %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) try: _enter_iron_condor() except Exception as e: @@ -22,6 +89,7 @@ def _enter_iron_condor(): ibkr_port = getenv('IBKR_PORT') ibkr_client = Client(host = ibkr_host, port = ibkr_port) + # The weekly symbol for SPX (SPXW) is required in order to distinguish from monthly options. symbol, sub_symbol = 'SPX', 'SPXW' expiration = datetime.now() @@ -35,7 +103,6 @@ def _enter_iron_condor(): return contract.strike <= (current_price + max_strike_distance) and contract.strike >= current_price return contract.strike <= current_price and contract.strike >= (current_price - max_strike_distance) - # The weekly symbol for SPX (SPXW) is required in order to distinguish from monthly options. option_chain = ibkr_client.get_option_chain(symbol, expiration, sub_symbol = sub_symbol, contract_filter = contract_filter) logging.info(option_chain) @@ -77,23 +144,26 @@ def _enter_iron_condor(): short_call_leg = OptionLeg(symbol, expiration, short_call_strike, CALL, SELL, sub_symbol) long_call_leg = OptionLeg(symbol, expiration, long_call_strike, CALL, BUY, sub_symbol) - call_spread_order = ibkr_client.submit_combo_option_order([short_call_leg, long_call_leg], 1) + call_spread_order = ibkr_client.submit_combo_option_order([short_call_leg, long_call_leg], quantity) while not call_spread_order.isDone(): ibkr_client.ib.waitOnUpdate() if call_spread_order.orderStatus.status == 'Filled': - fill_price = call_spread_order.orderStatus.avgFillPrice - print('Call Spread Fill Price: ', fill_price) - ibkr_client.submit_stop_loss_order(call_spread_order, fill_price * 2) + fill_price = abs(call_spread_order.orderStatus.avgFillPrice) + logging.info(f'Call Spread Fill Price: {fill_price}') + monitor_spread_price(short_call_leg, long_call_leg, fill_price * 2, ibkr_client) short_put_leg = OptionLeg(symbol, expiration, short_put_strike, PUT, SELL, sub_symbol) long_put_leg = OptionLeg(symbol, expiration, long_put_strike, PUT, BUY, sub_symbol) - put_spread_order = ibkr_client.submit_combo_option_order([short_put_leg, long_put_leg], 1) + put_spread_order = ibkr_client.submit_combo_option_order([short_put_leg, long_put_leg], quantity) while not put_spread_order.isDone(): ibkr_client.ib.waitOnUpdate() if put_spread_order.orderStatus.status == 'Filled': - fill_price = put_spread_order.orderStatus.avgFillPrice - print('Put Spread Fill Price: ', fill_price) - ibkr_client.submit_stop_loss_order(put_spread_order, fill_price * 2) \ No newline at end of file + fill_price = abs(put_spread_order.orderStatus.avgFillPrice) + logging.info(f'Put Spread Fill Price: {fill_price}') + monitor_spread_price(short_put_leg, long_put_leg, fill_price * 2, ibkr_client) + + # TODO: Add a shutdown hook. + ibkr_client.run_event_loop() \ No newline at end of file