commit 9ddfbb11aeadf24dc4fbf4154734f09a3335785f Author: moshferatu Date: Thu Sep 14 10:50:52 2023 -0700 Initial commit of IBKR client diff --git a/client.py b/client.py new file mode 100644 index 0000000..144f27c --- /dev/null +++ b/client.py @@ -0,0 +1,52 @@ +import pandas as pd + +from datetime import datetime +from exchange import SMART +from ib_insync import IB, Index, Option +from ib_insync.util import isNan +from market_data_type import LIVE +from typing import Callable + +class Client: + + def __init__(self, host: str, port: int, client_id = 1) -> None: + self.ib = IB() + self.ib.connect(host, port, clientId = client_id) + self.ib.reqMarketDataType(LIVE) + + def get_ticker(self, symbol: str, exchange: str): + underlying = Index(symbol, exchange) + self.ib.qualifyContracts(underlying) + return self.ib.reqTickers(underlying)[0] + + def get_option_chain(self, symbol: str, expiration: datetime, sub_symbol: str = None, + contract_filter: Callable = None) -> pd.DataFrame: + expiration_date = expiration.strftime('%Y%m%d') + contract_details = self.ib.reqContractDetails(Option(symbol, expiration_date, exchange = SMART)) + contracts = [_.contract for _ in contract_details if not sub_symbol or _.contract.tradingClass == sub_symbol] + if contract_filter: + contracts = list(filter(contract_filter, contracts)) + + option_data = [] + for contract in contracts: + option = Option(symbol, expiration_date, contract.strike, contract.right, exchange = SMART, currency = 'USD') + if sub_symbol: + option.tradingClass = sub_symbol + option_data_snapshot = self.ib.reqMktData(option, '', True, False) + option_data.append(option_data_snapshot) + + option_chain = [] + for option in option_data: + print('Processing Option: ', option) + while isNan(option.bid) or isNan(option.ask) or option.modelGreeks is None: + # TODO: Add a timeout? + self.ib.sleep() + option_chain.append({ + 'Strike': option.contract.strike, + 'Type': option.contract.right, # 'C' for Call, 'P' for Put. + 'Bid': option.bid, + 'Ask': option.ask, + 'Delta': option.modelGreeks.delta, + }) + + return pd.DataFrame(option_chain) \ No newline at end of file diff --git a/exchange.py b/exchange.py new file mode 100644 index 0000000..6465923 --- /dev/null +++ b/exchange.py @@ -0,0 +1 @@ +SMART = 'SMART' \ No newline at end of file diff --git a/market_data_type.py b/market_data_type.py new file mode 100644 index 0000000..b57095d --- /dev/null +++ b/market_data_type.py @@ -0,0 +1 @@ +LIVE = 1 \ No newline at end of file diff --git a/option_chain_example.py b/option_chain_example.py new file mode 100644 index 0000000..e30def9 --- /dev/null +++ b/option_chain_example.py @@ -0,0 +1,18 @@ +from client import Client +from datetime import datetime + +ibkr_client = Client(host = '127.0.0.1', port = 7497) + +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) \ No newline at end of file diff --git a/option_chain_example_old.py b/option_chain_example_old.py new file mode 100644 index 0000000..11713d2 --- /dev/null +++ b/option_chain_example_old.py @@ -0,0 +1,98 @@ +######################################################################### +# Not currently using this as the call to reqTickers takes > 10 seconds # +######################################################################### + +import datetime +import pandas as pd + +from exchange import SMART +from ib_insync import * + +ib = IB() +ib.connect('127.0.0.1', 7497, clientId=1) # Assuming TWS is running on the current machine. + +underlying = Index('SPX', 'CBOE') +ib.qualifyContracts(underlying) +underlying_ticker = ib.reqTickers(underlying)[0] + +atm = underlying_ticker.last +print('Last Price:', atm) + +chains = ib.reqSecDefOptParams(underlying.symbol, '', underlying.secType, underlying.conId) + +chain = next(c for c in chains if c.tradingClass == 'SPXW' and c.exchange == SMART) +today = datetime.datetime.now().strftime('%Y%m%d') +expirations = [exp for exp in chain.expirations if exp == today] + +max_strike_distance = 100 +call_strikes = sorted(strike for strike in chain.strikes if strike <= (atm + max_strike_distance) and strike >= atm) +put_strikes = sorted(strike for strike in chain.strikes if strike <= atm and strike >= (atm - max_strike_distance)) + +put_contracts = [Option('SPX', expiration, strike, 'P', SMART) for expiration in expirations for strike in put_strikes] +call_contracts = [Option('SPX', expiration, strike, 'C', SMART) for expiration in expirations for strike in call_strikes] +contracts = put_contracts + call_contracts +qualified_contracts = ib.qualifyContracts(*contracts) + +# Requesting market data (e.g., current bid / ask) for each contract. +# This is what takes a long time. +tickers = ib.reqTickers(*qualified_contracts) +data = [] +for ticker in tickers: + symbol = ticker.contract.localSymbol + strike = ticker.contract.strike + right = 'CALL' if ticker.contract.right == 'C' else 'PUT' + # TODO: Bid and Ask. + price = iv = delta = gamma = vega = theta = underlying_price = None + # TODO: The model greeks are not always available, wait for them. + if ticker.modelGreeks is not None: + price = ticker.modelGreeks.optPrice + iv = ticker.modelGreeks.impliedVol + delta = ticker.modelGreeks.delta + gamma = ticker.modelGreeks.gamma + vega = ticker.modelGreeks.vega + theta = ticker.modelGreeks.theta + underlying_price = ticker.modelGreeks.undPrice + data.append([symbol, strike, right, price, iv, delta, gamma, vega, theta, underlying_price]) + +option_chain = pd.DataFrame(data, columns=['Symbol', 'Strike', 'Type', 'Price', 'IV', 'Delta', 'Gamma', 'Vega', 'Theta', 'Underlying Price']) + +print('Option Chain:') +print(option_chain) + +target_delta = 0.10 + +# Separate calls and puts. +calls = option_chain[option_chain['Type'] == 'CALL'].copy() +puts = option_chain[option_chain['Type'] == 'PUT'].copy() + +# Find the difference between the target delta and actual delta for calls and puts. +calls['Delta Delta'] = abs(calls['Delta'] - target_delta) +puts['Delta Delta'] = abs(puts['Delta'] + target_delta) + +# Find the row where this difference is minimized for calls. +closest_call_strike = calls.loc[calls['Delta Delta'].idxmin()] + +# Find the row where this difference is minimized for puts. +closest_put_strike = puts.loc[puts['Delta Delta'].idxmin()] + +# Determine the target strikes. +target_long_call_strike = closest_call_strike['Strike'] + 50 +target_long_put_strike = closest_put_strike['Strike'] - 50 + +def find_closest_strike(target_strike, option_type, option_chain): + options = option_chain[option_chain['Type'] == option_type].copy() + options['Strike Distance'] = abs(options['Strike'] - target_strike) + nearest_strike = options.loc[options['Strike Distance'].idxmin()] + return nearest_strike + +# Find the closest call and put strikes to the desired targets +closest_long_call_strike = find_closest_strike(target_long_call_strike, 'CALL', option_chain) +closest_long_put_strike = find_closest_strike(target_long_put_strike, 'PUT', option_chain) + +# For entering an iron condor. +print('Short Call Strike:', closest_call_strike['Strike']) +print('Long Call Strike:', closest_long_call_strike['Strike']) +print("Short Put Strike:", closest_put_strike['Strike']) +print('Long Put Strike:', closest_long_put_strike['Strike']) + +ib.disconnect() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..af2ace3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +ib_insync +pandas \ No newline at end of file