diff --git a/iron_condor.py b/iron_condor.py index 8422211..dbcf73b 100644 --- a/iron_condor.py +++ b/iron_condor.py @@ -1,153 +1,89 @@ -from datetime import datetime, timezone +from datetime import datetime from dotenv import load_dotenv -from ibkr import Client -from option_type import CALL, PUT +from ibkr import Client, OptionLeg +from ibkr.option_type import CALL, PUT +from ibkr.order_action import BUY, SELL from os import getenv -from tastytrade import Tastytrade -from tastytrade.order import create_credit_spread, create_stop_limit_order -from tastytrade.symbology import zero_dte_spx_contract as contract -from time import sleep load_dotenv() -ibkr_host = getenv('IBKR_HOST') -ibkr_port = getenv('IBKR_PORT') -ibkr_client = Client(host = ibkr_host, port = ibkr_port) +def enter_iron_condor(): + ibkr_host = getenv('IBKR_HOST') + ibkr_port = getenv('IBKR_PORT') + ibkr_client = Client(host = ibkr_host, port = ibkr_port) -tastytrade_account = getenv('TASTYTRADE_ACCOUNT') -tastytrade_username = getenv('TASTYTRADE_USERNAME') -tastytrade_password = getenv('TASTYTRADE_PASSWORD') -tastytrade_client = Tastytrade(tastytrade_username, tastytrade_password) -tastytrade_client.login() + symbol, sub_symbol = 'SPX', 'SPXW' + expiration = datetime.now() -underlying_ticker = ibkr_client.get_ticker('SPX', 'CBOE') -current_price = underlying_ticker.last + underlying_ticker = ibkr_client.get_ticker(symbol, 'CBOE') + current_price = underlying_ticker.last -# Filtering strikes based on distance from current price speeds up the request. -max_strike_distance = 100 -def contract_filter(contract): - if contract.right == 'C': - 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) + # Filtering strikes based on distance from current price speeds up the request. + max_strike_distance = 100 + def contract_filter(contract): + if contract.right == CALL: + 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('SPX', datetime.now(), sub_symbol = 'SPXW', contract_filter = contract_filter) -print(option_chain) + # 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) + print(option_chain) -target_delta = 0.20 + target_delta = 0.10 -def closest_contract_by_delta(target_delta, option_chain, option_type): - options = option_chain[option_chain['Type'] == option_type].copy() - options['Delta Distance'] = abs(options['Delta'] - target_delta) - return options.loc[options['Delta Distance'].idxmin()] + def closest_contract_by_delta(target_delta, option_chain, option_type): + options = option_chain[option_chain['Type'] == option_type].copy() + options['Delta Distance'] = abs(options['Delta'] - target_delta) + return options.loc[options['Delta Distance'].idxmin()] -# Find the strikes that minimize the distance to the target delta. -short_put_contract = closest_contract_by_delta(-target_delta, option_chain, PUT) -short_call_contract = closest_contract_by_delta(target_delta, option_chain, CALL) + # Find the strikes that minimize the distance to the target delta. + short_put_contract = closest_contract_by_delta(-target_delta, option_chain, PUT) + short_call_contract = closest_contract_by_delta(target_delta, option_chain, CALL) -# When selecting long strikes, minimize the distance to a 50 point spread. -# TODO: Select long strike based on preferred price. -target_long_put_strike = short_put_contract['Strike'] - 50 -target_long_call_strike = short_call_contract['Strike'] + 50 + # When selecting long strikes, minimize the distance to a 50 point spread. + # TODO: Select long strike based on preferred price. + target_long_put_strike = short_put_contract['Strike'] - 50 + target_long_call_strike = short_call_contract['Strike'] + 50 -def closest_contract_by_strike(target_strike, option_chain, option_type): - options = option_chain[option_chain['Type'] == option_type].copy() - options['Strike Distance'] = abs(options['Strike'] - target_strike) - return options.loc[options['Strike Distance'].idxmin()] + def closest_contract_by_strike(target_strike, option_chain, option_type): + options = option_chain[option_chain['Type'] == option_type].copy() + options['Strike Distance'] = abs(options['Strike'] - target_strike) + return options.loc[options['Strike Distance'].idxmin()] -long_put_contract = closest_contract_by_strike(target_long_put_strike, option_chain, PUT) -long_call_contract = closest_contract_by_strike(target_long_call_strike, option_chain, CALL) + long_put_contract = closest_contract_by_strike(target_long_put_strike, option_chain, PUT) + long_call_contract = closest_contract_by_strike(target_long_call_strike, option_chain, CALL) -# Build the iron condor. -short_put_strike = float(short_put_contract['Strike']) -long_put_strike = float(long_put_contract['Strike']) -short_call_strike = float(short_call_contract['Strike']) -long_call_strike = float(long_call_contract['Strike']) + # Build the iron condor. + short_put_strike = float(short_put_contract['Strike']) + long_put_strike = float(long_put_contract['Strike']) + short_call_strike = float(short_call_contract['Strike']) + long_call_strike = float(long_call_contract['Strike']) -put_spread_limit_price = short_put_contract['Bid'] - long_put_contract['Ask'] - 0.05 # Yield to the MMs. -call_spread_limit_price = short_call_contract['Bid'] - long_call_contract['Ask'] - 0.05 + print('Short Put Strike:', short_put_strike) + print('Long Put Strike:', long_put_strike) + print('Short Call Strike:', short_call_strike) + print('Long Call Strike:', long_call_strike) -print("Short Put Strike:", short_put_strike) -print('Long Put Strike:', long_put_strike) -print('Put Spread Limit Price:', put_spread_limit_price) -print('Short Call Strike:', short_call_strike) -print('Long Call Strike:', long_call_strike) -print('Call Spread Limit Price:', call_spread_limit_price) + 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) -put_credit_spread = create_credit_spread( - contract(PUT, short_put_strike), - contract(PUT, long_put_strike), - put_spread_limit_price, 1 -) -call_credit_spread = create_credit_spread( - contract(CALL, short_call_strike), - contract(CALL, long_call_strike), - call_spread_limit_price, 1 -) + call_spread_order = ibkr_client.submit_combo_option_order([short_call_leg, long_call_leg], 1) + while not call_spread_order.isDone(): + ibkr_client.ib.waitOnUpdate() -entry_time = datetime.now(timezone.utc) + 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) -put_spread_result = tastytrade_client.submit_order(tastytrade_account, put_credit_spread) -call_spread_result = tastytrade_client.submit_order(tastytrade_account, call_credit_spread) + 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) -print(put_spread_result) -print(call_spread_result) + put_spread_order = ibkr_client.submit_combo_option_order([short_put_leg, long_put_leg], 1) + while not put_spread_order.isDone(): + ibkr_client.ib.waitOnUpdate() -def spread_fill_price(short_position, long_position): - short_leg_price = float(short_position['average-open-price']) - long_leg_price = float(long_position['average-open-price']) - return short_leg_price - long_leg_price - -def fill_time(position): - return datetime.strptime(position["created-at"], '%Y-%m-%dT%H:%M:%S.%f%z') - -def wait_for_fill(): - while True: - positions = tastytrade_client.get_positions(tastytrade_account) - positions = positions.get('data', {}).get('items', []) # TODO: Client should handle this. - print(positions) - # Consider only positions created after the order was submitted. - new_positions = [position for position in positions if fill_time(position) > entry_time] - - if len(new_positions) == 4: # Assuming no other positions, 4 legs in an iron condor. - short_put = next(p for p in new_positions if str(int(short_put_strike)) in p['symbol']) - long_put = next(p for p in new_positions if str(int(long_put_strike)) in p['symbol']) - short_call = next(p for p in new_positions if str(int(short_call_strike)) in p['symbol']) - long_call = next(p for p in new_positions if str(int(long_call_strike)) in p['symbol']) - - put_spread_fill_price = spread_fill_price(short_put, long_put) - call_spread_fill_price = spread_fill_price(short_call, long_call) - - return put_spread_fill_price, call_spread_fill_price - - # If not all positions are filled, sleep for a few seconds, then retry. - sleep(3) - -put_spread_fill_price, call_spread_fill_price = wait_for_fill() - -print(f'Put Spread Fill Price: {put_spread_fill_price}') -print(f'Call Spread Fill Price: {call_spread_fill_price}') - -put_spread_stop = put_spread_fill_price * 2.0 -put_spread_stop_order = create_stop_limit_order( - contract(PUT, short_put_strike), - contract(PUT, long_put_strike), - stop_trigger = put_spread_stop - 0.25, # Allow for slippage. - limit_price = put_spread_stop, - quantity = 1 -) - -call_spread_stop = call_spread_fill_price * 2.0 -call_spread_stop_order = create_stop_limit_order( - contract(CALL, short_call_strike), - contract(CALL, long_call_strike), - stop_trigger = call_spread_stop - 0.25, # Allow for slippage. - limit_price = call_spread_stop, - quantity = 1 -) - -put_spread_stop_result = tastytrade_client.submit_order(tastytrade_account, put_spread_stop_order) -call_spread_stop_result = tastytrade_client.submit_order(tastytrade_account, call_spread_stop_order) - -print(put_spread_stop_result) -print(call_spread_stop_result) \ No newline at end of file + 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 diff --git a/iron_condor_ibkr.py b/iron_condor_ibkr.py deleted file mode 100644 index 35528e8..0000000 --- a/iron_condor_ibkr.py +++ /dev/null @@ -1,88 +0,0 @@ -from datetime import datetime -from dotenv import load_dotenv -from ibkr import Client, OptionLeg -from ibkr.option_type import CALL, PUT -from ibkr.order_action import BUY, SELL -from os import getenv - -load_dotenv() - -ibkr_host = getenv('IBKR_HOST') -ibkr_port = getenv('IBKR_PORT') -ibkr_client = Client(host = ibkr_host, port = ibkr_port) - -symbol, sub_symbol = 'SPX', 'SPXW' -expiration = datetime.now() - -underlying_ticker = ibkr_client.get_ticker(symbol, 'CBOE') -current_price = underlying_ticker.last - -# Filtering strikes based on distance from current price speeds up the request. -max_strike_distance = 100 -def contract_filter(contract): - if contract.right == CALL: - 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) -print(option_chain) - -target_delta = 0.10 - -def closest_contract_by_delta(target_delta, option_chain, option_type): - options = option_chain[option_chain['Type'] == option_type].copy() - options['Delta Distance'] = abs(options['Delta'] - target_delta) - return options.loc[options['Delta Distance'].idxmin()] - -# Find the strikes that minimize the distance to the target delta. -short_put_contract = closest_contract_by_delta(-target_delta, option_chain, PUT) -short_call_contract = closest_contract_by_delta(target_delta, option_chain, CALL) - -# When selecting long strikes, minimize the distance to a 50 point spread. -# TODO: Select long strike based on preferred price. -target_long_put_strike = short_put_contract['Strike'] - 50 -target_long_call_strike = short_call_contract['Strike'] + 50 - -def closest_contract_by_strike(target_strike, option_chain, option_type): - options = option_chain[option_chain['Type'] == option_type].copy() - options['Strike Distance'] = abs(options['Strike'] - target_strike) - return options.loc[options['Strike Distance'].idxmin()] - -long_put_contract = closest_contract_by_strike(target_long_put_strike, option_chain, PUT) -long_call_contract = closest_contract_by_strike(target_long_call_strike, option_chain, CALL) - -# Build the iron condor. -short_put_strike = float(short_put_contract['Strike']) -long_put_strike = float(long_put_contract['Strike']) -short_call_strike = float(short_call_contract['Strike']) -long_call_strike = float(long_call_contract['Strike']) - -print('Short Put Strike:', short_put_strike) -print('Long Put Strike:', long_put_strike) -print('Short Call Strike:', short_call_strike) -print('Long Call Strike:', long_call_strike) - -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) -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) - -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) -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 diff --git a/iron_condor_tastytrade.py b/iron_condor_tastytrade.py new file mode 100644 index 0000000..8422211 --- /dev/null +++ b/iron_condor_tastytrade.py @@ -0,0 +1,153 @@ +from datetime import datetime, timezone +from dotenv import load_dotenv +from ibkr import Client +from option_type import CALL, PUT +from os import getenv +from tastytrade import Tastytrade +from tastytrade.order import create_credit_spread, create_stop_limit_order +from tastytrade.symbology import zero_dte_spx_contract as contract +from time import sleep + +load_dotenv() + +ibkr_host = getenv('IBKR_HOST') +ibkr_port = getenv('IBKR_PORT') +ibkr_client = Client(host = ibkr_host, port = ibkr_port) + +tastytrade_account = getenv('TASTYTRADE_ACCOUNT') +tastytrade_username = getenv('TASTYTRADE_USERNAME') +tastytrade_password = getenv('TASTYTRADE_PASSWORD') +tastytrade_client = Tastytrade(tastytrade_username, tastytrade_password) +tastytrade_client.login() + +underlying_ticker = ibkr_client.get_ticker('SPX', 'CBOE') +current_price = underlying_ticker.last + +# Filtering strikes based on distance from current price speeds up the request. +max_strike_distance = 100 +def contract_filter(contract): + if contract.right == 'C': + 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('SPX', datetime.now(), sub_symbol = 'SPXW', contract_filter = contract_filter) +print(option_chain) + +target_delta = 0.20 + +def closest_contract_by_delta(target_delta, option_chain, option_type): + options = option_chain[option_chain['Type'] == option_type].copy() + options['Delta Distance'] = abs(options['Delta'] - target_delta) + return options.loc[options['Delta Distance'].idxmin()] + +# Find the strikes that minimize the distance to the target delta. +short_put_contract = closest_contract_by_delta(-target_delta, option_chain, PUT) +short_call_contract = closest_contract_by_delta(target_delta, option_chain, CALL) + +# When selecting long strikes, minimize the distance to a 50 point spread. +# TODO: Select long strike based on preferred price. +target_long_put_strike = short_put_contract['Strike'] - 50 +target_long_call_strike = short_call_contract['Strike'] + 50 + +def closest_contract_by_strike(target_strike, option_chain, option_type): + options = option_chain[option_chain['Type'] == option_type].copy() + options['Strike Distance'] = abs(options['Strike'] - target_strike) + return options.loc[options['Strike Distance'].idxmin()] + +long_put_contract = closest_contract_by_strike(target_long_put_strike, option_chain, PUT) +long_call_contract = closest_contract_by_strike(target_long_call_strike, option_chain, CALL) + +# Build the iron condor. +short_put_strike = float(short_put_contract['Strike']) +long_put_strike = float(long_put_contract['Strike']) +short_call_strike = float(short_call_contract['Strike']) +long_call_strike = float(long_call_contract['Strike']) + +put_spread_limit_price = short_put_contract['Bid'] - long_put_contract['Ask'] - 0.05 # Yield to the MMs. +call_spread_limit_price = short_call_contract['Bid'] - long_call_contract['Ask'] - 0.05 + +print("Short Put Strike:", short_put_strike) +print('Long Put Strike:', long_put_strike) +print('Put Spread Limit Price:', put_spread_limit_price) +print('Short Call Strike:', short_call_strike) +print('Long Call Strike:', long_call_strike) +print('Call Spread Limit Price:', call_spread_limit_price) + +put_credit_spread = create_credit_spread( + contract(PUT, short_put_strike), + contract(PUT, long_put_strike), + put_spread_limit_price, 1 +) +call_credit_spread = create_credit_spread( + contract(CALL, short_call_strike), + contract(CALL, long_call_strike), + call_spread_limit_price, 1 +) + +entry_time = datetime.now(timezone.utc) + +put_spread_result = tastytrade_client.submit_order(tastytrade_account, put_credit_spread) +call_spread_result = tastytrade_client.submit_order(tastytrade_account, call_credit_spread) + +print(put_spread_result) +print(call_spread_result) + +def spread_fill_price(short_position, long_position): + short_leg_price = float(short_position['average-open-price']) + long_leg_price = float(long_position['average-open-price']) + return short_leg_price - long_leg_price + +def fill_time(position): + return datetime.strptime(position["created-at"], '%Y-%m-%dT%H:%M:%S.%f%z') + +def wait_for_fill(): + while True: + positions = tastytrade_client.get_positions(tastytrade_account) + positions = positions.get('data', {}).get('items', []) # TODO: Client should handle this. + print(positions) + # Consider only positions created after the order was submitted. + new_positions = [position for position in positions if fill_time(position) > entry_time] + + if len(new_positions) == 4: # Assuming no other positions, 4 legs in an iron condor. + short_put = next(p for p in new_positions if str(int(short_put_strike)) in p['symbol']) + long_put = next(p for p in new_positions if str(int(long_put_strike)) in p['symbol']) + short_call = next(p for p in new_positions if str(int(short_call_strike)) in p['symbol']) + long_call = next(p for p in new_positions if str(int(long_call_strike)) in p['symbol']) + + put_spread_fill_price = spread_fill_price(short_put, long_put) + call_spread_fill_price = spread_fill_price(short_call, long_call) + + return put_spread_fill_price, call_spread_fill_price + + # If not all positions are filled, sleep for a few seconds, then retry. + sleep(3) + +put_spread_fill_price, call_spread_fill_price = wait_for_fill() + +print(f'Put Spread Fill Price: {put_spread_fill_price}') +print(f'Call Spread Fill Price: {call_spread_fill_price}') + +put_spread_stop = put_spread_fill_price * 2.0 +put_spread_stop_order = create_stop_limit_order( + contract(PUT, short_put_strike), + contract(PUT, long_put_strike), + stop_trigger = put_spread_stop - 0.25, # Allow for slippage. + limit_price = put_spread_stop, + quantity = 1 +) + +call_spread_stop = call_spread_fill_price * 2.0 +call_spread_stop_order = create_stop_limit_order( + contract(CALL, short_call_strike), + contract(CALL, long_call_strike), + stop_trigger = call_spread_stop - 0.25, # Allow for slippage. + limit_price = call_spread_stop, + quantity = 1 +) + +put_spread_stop_result = tastytrade_client.submit_order(tastytrade_account, put_spread_stop_order) +call_spread_stop_result = tastytrade_client.submit_order(tastytrade_account, call_spread_stop_order) + +print(put_spread_stop_result) +print(call_spread_stop_result) \ No newline at end of file