Monitor the price of each spread and perform stop outs when necessary

This commit is contained in:
moshferatu 2023-12-15 14:48:47 -08:00
parent 1cc0c7867f
commit 21bf4c92f5

View File

@ -1,6 +1,8 @@
import logging import logging
import nest_asyncio
import traceback import traceback
from dataclasses import replace
from datetime import datetime from datetime import datetime
from dotenv import load_dotenv from dotenv import load_dotenv
from ibkr import Client, OptionLeg from ibkr import Client, OptionLeg
@ -10,8 +12,73 @@ from os import getenv
load_dotenv() 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(): 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: try:
_enter_iron_condor() _enter_iron_condor()
except Exception as e: except Exception as e:
@ -22,6 +89,7 @@ def _enter_iron_condor():
ibkr_port = getenv('IBKR_PORT') ibkr_port = getenv('IBKR_PORT')
ibkr_client = Client(host = ibkr_host, port = 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' symbol, sub_symbol = 'SPX', 'SPXW'
expiration = datetime.now() 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 + max_strike_distance) and contract.strike >= current_price
return contract.strike <= current_price and contract.strike >= (current_price - max_strike_distance) 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) option_chain = ibkr_client.get_option_chain(symbol, expiration, sub_symbol = sub_symbol, contract_filter = contract_filter)
logging.info(option_chain) 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) 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) 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(): while not call_spread_order.isDone():
ibkr_client.ib.waitOnUpdate() ibkr_client.ib.waitOnUpdate()
if call_spread_order.orderStatus.status == 'Filled': if call_spread_order.orderStatus.status == 'Filled':
fill_price = call_spread_order.orderStatus.avgFillPrice fill_price = abs(call_spread_order.orderStatus.avgFillPrice)
print('Call Spread Fill Price: ', fill_price) logging.info(f'Call Spread Fill Price: {fill_price}')
ibkr_client.submit_stop_loss_order(call_spread_order, fill_price * 2) 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) 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) 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(): while not put_spread_order.isDone():
ibkr_client.ib.waitOnUpdate() ibkr_client.ib.waitOnUpdate()
if put_spread_order.orderStatus.status == 'Filled': if put_spread_order.orderStatus.status == 'Filled':
fill_price = put_spread_order.orderStatus.avgFillPrice fill_price = abs(put_spread_order.orderStatus.avgFillPrice)
print('Put Spread Fill Price: ', fill_price) logging.info(f'Put Spread Fill Price: {fill_price}')
ibkr_client.submit_stop_loss_order(put_spread_order, fill_price * 2) monitor_spread_price(short_put_leg, long_put_leg, fill_price * 2, ibkr_client)
# TODO: Add a shutdown hook.
ibkr_client.run_event_loop()