From eff486a5acd62f05a54135c5c55bae1246c56e89 Mon Sep 17 00:00:00 2001 From: rax Date: Sun, 27 Aug 2023 13:04:05 -0400 Subject: [PATCH 01/10] first stub --- DiscordAlertsTrader/brokerages/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/DiscordAlertsTrader/brokerages/__init__.py b/DiscordAlertsTrader/brokerages/__init__.py index c4dc604..23636fb 100644 --- a/DiscordAlertsTrader/brokerages/__init__.py +++ b/DiscordAlertsTrader/brokerages/__init__.py @@ -81,4 +81,16 @@ def get_brokerage(name=cfg['general']['BROKERAGE']): except Exception as e: print("Got error: \n", e, "\n Trying again...if it fails again, rerun the application.") et.get_session() - return et \ No newline at end of file + return et + # todo : implement stub in ibkr module + elif name.lower() == 'ibkr': + from .ibkr_api import Ibkr + accountId = cfg['ibkr']['accountId'] + accountId = None if len(accountId) == 0 else accountId + ibkr = Ibkr(accountId=accountId) + try: + ibkr.get_session() + except Exception as e: + print("Got error: \n", e, "\n Trying again...if it fails again, rerun the application.") + ibkr.get_session() + return ibkr \ No newline at end of file From 6d65a74aa673505bc7075a93f1c468acb4b0e4b1 Mon Sep 17 00:00:00 2001 From: rax Date: Sun, 27 Aug 2023 21:19:56 -0400 Subject: [PATCH 02/10] add scafolding for IBKR --- DiscordAlertsTrader/brokerages/ibkr_api.py | 429 +++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 DiscordAlertsTrader/brokerages/ibkr_api.py diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py new file mode 100644 index 0000000..be46179 --- /dev/null +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -0,0 +1,429 @@ +import re +import time +from ib_insync import IB, Stock, MarketOrder, util, Option +from datetime import datetime +from DiscordAlertsTrader.configurator import cfg + + +class Ibkr: + def __init__(self, paper_trading: bool = False) -> None: + self._ibkr = IB() + self._loggedin = False + self.name = 'ibkr' + self.option_ids = {} + + def connect(self, port: int = 7496) -> bool: + try: + self._ibkr.connect('127.0.0.1', port, clientId=random.randint(1, 50)) + self.success = True + self._port = port + except: + self.success = False + self._port = None + + def get_session(self, use_workaround: bool = True) -> bool: + # todo : can use singleton pattern + self.success = self.connect(port=7496) + if not self.success: + self.session = None + else: + self.session = self._ibkr + + if self.success: + print(f"Logging into IBKR: Success!") + else: + print(f"Logging into IBKR: Failed!") + return self.success + + def get_account_info(self): + """ + Call portfolio API to retrieve a list of positions held in the specified account + """ + data = self.session.get_account() + + acc_inf ={ + 'securitiesAccount':{ + 'positions':[], + 'accountId' : str(data['secAccountId']), + 'currentBalances':{ + 'liquidationValue': data.get('netLiquidation'), + 'cashBalance': data['accountMembers'][1]['value'], + 'availableFunds': data['accountMembers'][2]['value'], + }, + }} + positions = data['positions'] + for position in positions: + pos = { + "longQuantity" : eval(position['position']), + "symbol": position['ticker']["symbol"], + "marketValue": eval(position['marketValue']), + "assetType": position['assetType'], + "averagePrice": eval(position['costPrice']), + "currentDayProfitLoss": eval(position['unrealizedProfitLoss']), + "currentDayProfitLossPercentage": float(eval(position['unrealizedProfitLoss']))/100, + 'instrument': {'symbol': position['ticker']["symbol"], + 'assetType': position['assetType'], + } + } + acc_inf['securitiesAccount']['positions'].append(pos) + if not len(positions): + acc_inf['securitiesAccount']['positions'] = [] + print("No portfolio") + + # get orders and add them to acc_inf + orders = self.session.get_history_orders(count=10) + orders_inf =[] + if isinstance(orders, dict) and orders.get('success') is False: + raise ValueError("Order entpoint obscolite, go to webull/endpoints.py line 144 and remove '&startTime=1970-0-1'") + + for order in orders: + order_status = order['status'].upper() + if order_status in ['CANCELLED', 'FAILED']: + continue + orders_inf.append(self.format_order(order)) + acc_inf['securitiesAccount']['orderStrategies'] = orders_inf + return acc_inf + + def get_order_info(self, order_id): + """ Get order info from order_id, mimicks the order_info from TDA API""" + orders = self.session.get_history_orders() + for order in orders: + if order['orders'][0]['orderId'] == order_id: + order_status = order['status'].upper() + order_info = self.format_order(order) + return order_status, order_info + return None, None + + def format_order(self, order:dict): + """ output format for order_response. Order, mimicks the order_info from TDA API""" + stopPrice= order['orders'][0].get('stpPrice') + + price = order['orders'][0].get('avgFilledPrice') + if price is None: + price = order['orders'][0].get('lmtPrice') + + timestamp = int(order['orders'][0]['createTime0'])/1000 + enteredTime = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S+00") + timestamp = int(order['orders'][0]['updateTime0'])/1000 + closeTime = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%dT%H:%M:%S+00") + asset = order['orders'][0]['tickerType'].lower() + if asset == 'option': + yer, mnt, day = order['orders'][0]['optionExpireDate'].split("-") + otype = "C" if order['orders'][0]['optionType'][0].upper() =="CALL" else "P" + symbol = f"{order['orders'][0]['symbol']}_{mnt}{day}{yer[2:]}{otype}{order['orders'][0]['optionExercisePrice']}".replace(".00","") + symbol = self.fix_symbol(symbol, "out") + orderStrategyType = order['optionStrategy'].upper() + else: + symbol = order['orders'][0]['symbol'] + orderStrategyType= 'SINGLE' + status = order['status'].upper() + order_info = { + 'status': status, + 'quantity': int(order['orders'][0]['totalQuantity']), + 'filledQuantity': int(order['orders'][0]['filledQuantity']), + 'price': float(price) if price else float(order['auxPrice']), + 'orderStrategyType': orderStrategyType, + "order_id" : order['orders'][0]['orderId'], + "orderId": order['orders'][0]['orderId'], + "stopPrice": stopPrice if stopPrice else None, + 'orderType': order['orders'][0]['orderType'], + 'enteredTime': enteredTime, + "closeTime": closeTime, + 'orderLegCollection':[{ + 'instrument':{'symbol': symbol}, + 'instruction': order['orders'][0]['action'], + 'quantity': int(order['filledQuantity']), + }] + } + return order_info + + def format_option(self, opt_ticker:str)->dict: + """From ticker_monthdayyear[callput]strike to dict {ticker, year-month-day,optionType,strikePrice""" + exp = r"(\w+)_(\d{2})(\d{2})(\d{2})([CP])([\d.]+)" + match = re.search(exp, opt_ticker, re.IGNORECASE) + if match: + symbol, mnt, day, yer, type, strike = match.groups() + type = 'call' if type.lower() == 'c' else 'put' + opt_info = { + 'ticker': symbol, + 'date': f"20{yer}-{mnt}-{day}", + 'direction': type, + 'strike': strike + } + return opt_info + else: + print('No format_option match for', opt_ticker) + + def reformat_option(self, opt_info:dict)->str: + "From dict to standard option format ticker_monthdayyear[callput]strike" + yer, mnt, day = opt_info['date'].split("-") + otype = opt_info['direction'][0].upper() + return f"{opt_info['ticker']}_{mnt}{day}{yer[2:]}{otype}{opt_info['strike']}" + + def get_option_id(self, symb:str): + "Get option id from option symb with standard format" + if self.option_ids.get(symb) is None: + opt_info = self.format_option(symb) + if opt_info: + options_data = self.session.get_options(stock=opt_info['ticker'], + direction=opt_info['direction'], + expireDate=opt_info['date']) + filtered_options_data = [option for option in options_data if option['strikePrice'] == opt_info['strike'] and \ + option[opt_info['direction']]['expireDate'] == opt_info['date']] + option_id = filtered_options_data[0][opt_info['direction']]['tickerId'] + self.option_ids[symb] = str(option_id) + return option_id + else: + return None + else: + return self.option_ids[symb] + + def fix_symbol(self, symbol:str, direction:str): + "Fix symbol for options, direction in or out of webull format" + if direction == 'in': + return symbol.replace("SPXW", "SPX") + elif direction == 'out': + return symbol.replace("SPX", "SPXW") + + def get_quotes(self, symbol:list) -> dict: + resp = {} + for symb in symbol: + if "_" in symb: + symb = self.fix_symbol(symb, "in") + opt_info = self.format_option(symb) + if opt_info: + try: + option_id = self.get_option_id(symb) + quote = self.session.get_option_quote(stock=opt_info['ticker'], optionId=option_id) + + ts = quote['data'][0]['tradeStamp'] + ask = eval(quote['data'][0]['askList'][0]['price']) + bid = eval(quote['data'][0]['bidList'][0]['price']) + ticker = self.fix_symbol(self.reformat_option(opt_info), 'out') + + resp[ticker] = { + 'symbol' : ticker, + 'description': option_id, + 'askPrice': ask, + 'bidPrice': bid, + 'quoteTimeInLong': ts + } + except Exception as e: + sym_out = self.fix_symbol(symb, "out") + print("Error getting quote for", sym_out, e) + resp[sym_out] = {'symbol': sym_out, + 'description':'Symbol not found' + } + else: + quote = self.session.get_quote(symb) + if quote and quote['template']=='stock': + resp[symb] = { + 'symbol' : quote['symbol'], + 'description': quote['disSymbol'], + 'askPrice': eval(quote['askList'][0]['price']), + 'bidPrice': eval(quote['bidList'][0]['price']), + 'quoteTimeInLong': round(time.time()*1000), + } + else: + print(symb, "not found", quote['template']) + resp[symb] = {'symbol' :symb, + 'description':'Symbol not found' + } + return resp + + def send_order(self, new_order:dict): + if new_order['asset'] == 'option': + final_order = {} + for key, val in new_order.items(): + if key in ['optionId', 'lmtPrice', 'stpPrice', 'action', 'orderType', 'enforce', 'quant']: + final_order[key] = val + order_response = self.session.place_order_option(**final_order) + else: + final_order = {} + if new_order.get('lmtPrice'): + new_order['price'] = new_order.pop('lmtPrice') + for key, val in new_order.items(): + if key in ['stock', 'tId', 'price', 'action', 'orderType', 'enforce', 'quant', + 'outsideRegularTradingHour', 'stpPrice', 'trial_value', 'trial_type']: + final_order[key] = val + order_response = self.session.place_order(**final_order) + + if order_response.get('success') is False: + print("Order failed", order_response) + return None, None + # find order id by matching symbol and order type + if order_response.get('data'): #for stocks + order_id = order_response['data']['orderId'] + else: + order_id = order_response['orderId'] + time.sleep(3) # 3 secs to wait for order to show up in history + _, ord_inf = self.get_order_info(order_id) + + order_response.update(ord_inf) + return order_response, order_id + + def cancel_order(self, order_id:int): + resp = self.session.cancel_order(order_id) + return resp + + def get_orders(self): + orders = self.session.get_history_orders() + orders_all = [] + for order in orders: + orders_all.append(self.format_order(order)) + return orders + + def make_BTO_lim_order(self, Symbol:str, Qty:int, price:float, action="BTO", **kwarg): + "Buy with a limit order" + + kwargs = {} + if action == "BTO": + kwargs['action'] = "BUY" + elif action == "STO": + print("STO not available for WeBull") + return + Symbol = self.fix_symbol(Symbol, "in") + if "_" in Symbol: + kwargs['asset'] = 'option' + optionId = self.get_option_id(Symbol) + if optionId is None: + print("No optionId found for", Symbol) + return None + kwargs['optionId'] = optionId + else: + kwargs['asset'] ='stock' + kwargs['outsideRegularTradingHour'] = True + kwargs['stock'] = Symbol + + kwargs['enforce'] ='GTC' + kwargs['quant'] = Qty + kwargs['orderType'] = 'LMT' + kwargs['lmtPrice'] = price + return kwargs + + def make_Lim_SL_order(self, Symbol:str, Qty:int, PT:float, SL:None, action="STC", **kwarg): + """Sell with a limit order and a stop loss order""" + kwargs = {} + if action == "STC": + kwargs['action'] = "SELL" + elif action == "BTC": + print("BTC not available for WeBull") + return + + Symbol = self.fix_symbol(Symbol, "in") + if "_" in Symbol: + kwargs['asset'] = 'option' + optionId = self.get_option_id(Symbol) + if optionId is None: + print("No optionId found for", Symbol) + return None + kwargs['optionId'] = optionId + else: + kwargs['asset'] ='stock' + kwargs['outsideRegularTradingHour'] = True + kwargs['stock'] = Symbol + + kwargs['enforce'] ='GTC' + kwargs['quant'] = Qty + if SL is not None: + print("WARNING: webull api does not support OCO, setting a SL only, do not provide SL to send PT") + kwargs['orderType'] = 'STP' + kwargs['lmtPrice'] = SL + else: + print("WARNING: webull api does not support OCO, sending PT without SL") + kwargs['orderType'] = 'LMT' + kwargs['lmtPrice'] = PT + return kwargs + + def make_STC_lim(self, Symbol:str, Qty:int, price:float, strike=None, action="STC", **kwarg): + """Sell with a limit order and a stop loss order""" + kwargs = {} + if action == "STC": + kwargs['action'] = "SELL" + elif action == "BTC": + print("BTC not available for WeBull") + return + + Symbol = self.fix_symbol(Symbol, "in") + if "_" in Symbol: + kwargs['asset'] = 'option' + optionId = self.get_option_id(Symbol) + if optionId is None: + print("No optionId found for", Symbol) + return None + kwargs['optionId'] = optionId + else: + kwargs['asset'] ='stock' + kwargs['outsideRegularTradingHour'] = True + kwargs['stock'] = Symbol + + kwargs['enforce'] ='GTC' + kwargs['quant'] = Qty + kwargs['orderType'] = 'LMT' + kwargs['lmtPrice'] = price + return kwargs + + def make_STC_SL(self, Symbol:str, Qty:int, SL:float, action="STC", **kwarg): + """Sell with a stop loss order""" + kwargs = {} + if action == "STC": + kwargs['action'] = "SELL" + elif action == "BTC": + print("BTC not available for WeBull") + return + + Symbol = self.fix_symbol(Symbol, "in") + if "_" in Symbol: + kwargs['asset'] = 'option' + optionId = self.get_option_id(Symbol) + if optionId is None: + print("No optionId found for", Symbol) + return None + kwargs['optionId'] = optionId + else: + kwargs['asset'] ='stock' + kwargs['outsideRegularTradingHour'] = False + kwargs['stock'] = Symbol + + kwargs['enforce'] ='GTC' + kwargs['quant'] = Qty + kwargs['orderType'] = 'STP' + kwargs['stpPrice'] = SL + kwargs['lmtPrice'] = SL + return kwargs + + def make_STC_SL_trailstop(self, Symbol:str, Qty:int, trail_stop_const:float, action="STC", **kwarg): + "trail_stop_const" + kwargs = {} + if action == "STC": + kwargs['action'] = "SELL" + elif action == "BTC": + print("BTC not available for WeBull") + return + + Symbol = self.fix_symbol(Symbol, "in") + if "_" in Symbol: + print("WARNING webull does not support trailing stop for options") + return {} + + kwargs['asset'] ='stock' + kwargs['outsideRegularTradingHour'] = True + kwargs['stock'] = Symbol + kwargs['enforce'] ='GTC' + kwargs['quant'] = Qty + kwargs['orderType'] = 'STP TRAIL' + kwargs['trial_value'] = trail_stop_const + kwargs['trial_type'] = 'DOLLAR' + kwargs['outsideRegularTradingHour'] = True + return kwargs + + +if 0: + self = weBull() + self.get_session() + self.get_account_info() + self.get_quotes(["AAuPL_062323C180", "AAPL_062323C190"]) + optid = self.get_option_id("AAPL_062323C180") + order = self.make_BTO_lim_order("NIO_062323P7", 1, 0.01, action="BTO") + order_response, order_id = self.send_order(order) + self.cancel_order(order_id) From 8bb13919013b3401a79d51155fd1e35d6b30a9ee Mon Sep 17 00:00:00 2001 From: rax Date: Mon, 28 Aug 2023 21:07:18 -0400 Subject: [PATCH 03/10] remove dead code --- DiscordAlertsTrader/brokerages/ibkr_api.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index be46179..17bb97e 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -39,6 +39,13 @@ def get_account_info(self): """ Call portfolio API to retrieve a list of positions held in the specified account """ + account_summary = self.session.accountSummary() + res = None + for summary in account_summary: + if summary.tag == ftag: + res = float(summary.value) + break + return res data = self.session.get_account() acc_inf ={ @@ -415,15 +422,4 @@ def make_STC_SL_trailstop(self, Symbol:str, Qty:int, trail_stop_const:float, ac kwargs['trial_value'] = trail_stop_const kwargs['trial_type'] = 'DOLLAR' kwargs['outsideRegularTradingHour'] = True - return kwargs - - -if 0: - self = weBull() - self.get_session() - self.get_account_info() - self.get_quotes(["AAuPL_062323C180", "AAPL_062323C190"]) - optid = self.get_option_id("AAPL_062323C180") - order = self.make_BTO_lim_order("NIO_062323P7", 1, 0.01, action="BTO") - order_response, order_id = self.send_order(order) - self.cancel_order(order_id) + return kwargs \ No newline at end of file From 60c078ce5f6567243be1ab5d2551ee5c85f40af3 Mon Sep 17 00:00:00 2001 From: rax Date: Mon, 28 Aug 2023 21:59:08 -0400 Subject: [PATCH 04/10] add more ibkr stubs --- DiscordAlertsTrader/brokerages/ibkr_api.py | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 17bb97e..10e7ae6 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -3,7 +3,7 @@ from ib_insync import IB, Stock, MarketOrder, util, Option from datetime import datetime from DiscordAlertsTrader.configurator import cfg - +import ib_insync class Ibkr: def __init__(self, paper_trading: bool = False) -> None: @@ -46,7 +46,7 @@ def get_account_info(self): res = float(summary.value) break return res - data = self.session.get_account() + acc_inf ={ 'securitiesAccount':{ @@ -269,14 +269,23 @@ def send_order(self, new_order:dict): order_response.update(ord_inf) return order_response, order_id - def cancel_order(self, order_id:int): - resp = self.session.cancel_order(order_id) - return resp + def cancel_order(self, order_id:int) -> bool: + # todo: handle exception + try: + self.session.cancelOrder(ib_insync.Order(orderId=order_id)) + return True + except: + return False def get_orders(self): - orders = self.session.get_history_orders() + ''' + todo + 1. pass account id to get only that account open orders + 2. is it asking for all orders or just open orders? + ''' + orders_all = [] - for order in orders: + for order in self.session.orders(): orders_all.append(self.format_order(order)) return orders From 374f9ccf89504a72e9a6d60cd3bf12c4f8f982f2 Mon Sep 17 00:00:00 2001 From: rax Date: Thu, 31 Aug 2023 21:44:09 -0400 Subject: [PATCH 05/10] wip on account info --- DiscordAlertsTrader/brokerages/ibkr_api.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 10e7ae6..9667c96 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -35,19 +35,18 @@ def get_session(self, use_workaround: bool = True) -> bool: print(f"Logging into IBKR: Failed!") return self.success - def get_account_info(self): + def get_account_info(self,account:str =""): """ Call portfolio API to retrieve a list of positions held in the specified account """ - account_summary = self.session.accountSummary() + account_summary = self.session.accountSummary(account=account) res = None for summary in account_summary: if summary.tag == ftag: res = float(summary.value) break - return res - - + + # get account metadata information acc_inf ={ 'securitiesAccount':{ 'positions':[], @@ -58,6 +57,8 @@ def get_account_info(self): 'availableFunds': data['accountMembers'][2]['value'], }, }} + + # get positions of the account positions = data['positions'] for position in positions: pos = { @@ -73,6 +74,8 @@ def get_account_info(self): } } acc_inf['securitiesAccount']['positions'].append(pos) + + # checks if account has no open pos if not len(positions): acc_inf['securitiesAccount']['positions'] = [] print("No portfolio") @@ -93,6 +96,7 @@ def get_account_info(self): def get_order_info(self, order_id): """ Get order info from order_id, mimicks the order_info from TDA API""" + order.OrderStatus.g orders = self.session.get_history_orders() for order in orders: if order['orders'][0]['orderId'] == order_id: From 9e518fcfb8c1efe1fbb2fe1502384acfbfe5b077 Mon Sep 17 00:00:00 2001 From: rax Date: Fri, 1 Sep 2023 23:49:06 -0400 Subject: [PATCH 06/10] add get order stub --- DiscordAlertsTrader/brokerages/ibkr_api.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 9667c96..679b2a6 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -95,8 +95,7 @@ def get_account_info(self,account:str =""): return acc_inf def get_order_info(self, order_id): - """ Get order info from order_id, mimicks the order_info from TDA API""" - order.OrderStatus.g + """ Get order info from order_id""" orders = self.session.get_history_orders() for order in orders: if order['orders'][0]['orderId'] == order_id: @@ -281,7 +280,7 @@ def cancel_order(self, order_id:int) -> bool: except: return False - def get_orders(self): + def get_orders(self, open_only=True): ''' todo 1. pass account id to get only that account open orders @@ -289,9 +288,19 @@ def get_orders(self): ''' orders_all = [] - for order in self.session.orders(): - orders_all.append(self.format_order(order)) - return orders + if open_only is True: + for order in self.session.openOrders(): + orders_all.append(self.format_order(order)) + else: + for order in self.session.orders(): + orders_all.append(self.format_order(order)) + + return orders_all + + def get_account_names(self) -> List[str]: + '''Return list of accounts eligible for trading''' + res = self.session.managedAccounts() + return ret def make_BTO_lim_order(self, Symbol:str, Qty:int, price:float, action="BTO", **kwarg): "Buy with a limit order" From aa7e1a584ee41f0747425a848cfeeea3b30d51dc Mon Sep 17 00:00:00 2001 From: adonunes Date: Sat, 2 Sep 2023 22:07:19 -0400 Subject: [PATCH 07/10] remove papertrade --- DiscordAlertsTrader/brokerages/ibkr_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 679b2a6..1d9ba11 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -21,7 +21,7 @@ def connect(self, port: int = 7496) -> bool: self.success = False self._port = None - def get_session(self, use_workaround: bool = True) -> bool: + def get_session(self) -> bool: # todo : can use singleton pattern self.success = self.connect(port=7496) if not self.success: From 1027ca19589cc6439098993b56b1d7b70c994dc7 Mon Sep 17 00:00:00 2001 From: adonunes Date: Sat, 2 Sep 2023 22:39:10 -0400 Subject: [PATCH 08/10] ib examples --- DiscordAlertsTrader/brokerages/ibkr_api.py | 33 ++++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 1d9ba11..e6719c3 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -47,30 +47,35 @@ def get_account_info(self,account:str =""): break # get account metadata information + netLiquidation = [v for v in self.session.accountValues() if + v.tag == 'NetLiquidationByCurrency' and v.currency == 'BASE'] # ib example + cashBalance = self.session.accountValues()['TotalCashValue'] # ib example + availableFunds = self.session.accountValues()['SettledCash'] # ib example acc_inf ={ 'securitiesAccount':{ 'positions':[], 'accountId' : str(data['secAccountId']), 'currentBalances':{ - 'liquidationValue': data.get('netLiquidation'), - 'cashBalance': data['accountMembers'][1]['value'], - 'availableFunds': data['accountMembers'][2]['value'], + 'liquidationValue': netLiquidation, # ib example + 'cashBalance': cashBalance, # ib example + 'availableFunds': availableFunds, # ib example }, }} # get positions of the account - positions = data['positions'] + positions = self.session.positions() # ib example + for position in positions: pos = { - "longQuantity" : eval(position['position']), - "symbol": position['ticker']["symbol"], - "marketValue": eval(position['marketValue']), - "assetType": position['assetType'], - "averagePrice": eval(position['costPrice']), + "longQuantity" : eval(position['position']), # ib example + "symbol": position["symbol"], # ib example + "marketValue": eval(position['position']), + "assetType": position['tradingClass'], # ib example + "averagePrice": eval(position['avgCost']), # ib example "currentDayProfitLoss": eval(position['unrealizedProfitLoss']), "currentDayProfitLossPercentage": float(eval(position['unrealizedProfitLoss']))/100, - 'instrument': {'symbol': position['ticker']["symbol"], - 'assetType': position['assetType'], + 'instrument': {'symbol': position["symbol"], # ib example + 'assetType': position['tradingClass'], # ib example } } acc_inf['securitiesAccount']['positions'].append(pos) @@ -81,11 +86,9 @@ def get_account_info(self,account:str =""): print("No portfolio") # get orders and add them to acc_inf - orders = self.session.get_history_orders(count=10) + orders = self.session.openOrders() # ib example orders_inf =[] - if isinstance(orders, dict) and orders.get('success') is False: - raise ValueError("Order entpoint obscolite, go to webull/endpoints.py line 144 and remove '&startTime=1970-0-1'") - + for order in orders: order_status = order['status'].upper() if order_status in ['CANCELLED', 'FAILED']: From 609d10c5a46aaa2675bdc583571e78b00670386a Mon Sep 17 00:00:00 2001 From: rax Date: Sun, 3 Sep 2023 22:34:55 -0400 Subject: [PATCH 09/10] fixed ibkr dict parsing --- DiscordAlertsTrader/brokerages/ibkr_api.py | 109 ++++++++++----------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index e6719c3..636b64c 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -1,9 +1,13 @@ import re import time +import ib_insync +import random + from ib_insync import IB, Stock, MarketOrder, util, Option from datetime import datetime from DiscordAlertsTrader.configurator import cfg -import ib_insync +from typing import List + class Ibkr: def __init__(self, paper_trading: bool = False) -> None: @@ -11,50 +15,44 @@ def __init__(self, paper_trading: bool = False) -> None: self._loggedin = False self.name = 'ibkr' self.option_ids = {} - - def connect(self, port: int = 7496) -> bool: + self.trapped_errors = [] + def filter_by(self, ftag): + res = [v for v in self.session.accountValues() if + v.tag == f'{ftag}'] # ib example + return res + def connect(self, host = '127.0.0.1', port: int = 7496) -> bool: try: - self._ibkr.connect('127.0.0.1', port, clientId=random.randint(1, 50)) + self._ibkr.connect(host, port, clientId=random.randint(1, 50)) self.success = True self._port = port - except: + self.session = self._ibkr + except Exception as e: self.success = False self._port = None - - def get_session(self) -> bool: - # todo : can use singleton pattern - self.success = self.connect(port=7496) - if not self.success: - self.session = None - else: - self.session = self._ibkr - if self.success: - print(f"Logging into IBKR: Success!") - else: - print(f"Logging into IBKR: Failed!") - return self.success + def get_session(self) -> bool: + return self._ibkr def get_account_info(self,account:str =""): """ Call portfolio API to retrieve a list of positions held in the specified account """ + # print(self.session.accountValues()) account_summary = self.session.accountSummary(account=account) res = None - for summary in account_summary: - if summary.tag == ftag: - res = float(summary.value) - break # get account metadata information - netLiquidation = [v for v in self.session.accountValues() if - v.tag == 'NetLiquidationByCurrency' and v.currency == 'BASE'] # ib example - cashBalance = self.session.accountValues()['TotalCashValue'] # ib example - availableFunds = self.session.accountValues()['SettledCash'] # ib example + netLiquidation_vector = self.filter_by('NetLiquidationByCurrency') + netLiquidation = self.filter_by('NetLiquidationByCurrency')[0].value + + account_id = netLiquidation_vector[0].account + cashBalance = self.filter_by('TotalCashValue')[0].value # ib example + availableFunds = self.filter_by('CashBalance')[0].value # ib example + acc_inf ={ 'securitiesAccount':{ 'positions':[], - 'accountId' : str(data['secAccountId']), + 'accountId' : str(account_id), 'currentBalances':{ 'liquidationValue': netLiquidation, # ib example 'cashBalance': cashBalance, # ib example @@ -65,36 +63,37 @@ def get_account_info(self,account:str =""): # get positions of the account positions = self.session.positions() # ib example - for position in positions: - pos = { - "longQuantity" : eval(position['position']), # ib example - "symbol": position["symbol"], # ib example - "marketValue": eval(position['position']), - "assetType": position['tradingClass'], # ib example - "averagePrice": eval(position['avgCost']), # ib example - "currentDayProfitLoss": eval(position['unrealizedProfitLoss']), - "currentDayProfitLossPercentage": float(eval(position['unrealizedProfitLoss']))/100, - 'instrument': {'symbol': position["symbol"], # ib example - 'assetType': position['tradingClass'], # ib example - } - } - acc_inf['securitiesAccount']['positions'].append(pos) + # for position in positions: + # pos = { + # "longQuantity" : eval(position['position']), # ib example + # "symbol": position["symbol"], # ib example + # "marketValue": eval(position['position']), + # "assetType": position['tradingClass'], # ib example + # "averagePrice": eval(position['avgCost']), # ib example + # "currentDayProfitLoss": eval(position['unrealizedProfitLoss']), + # "currentDayProfitLossPercentage": float(eval(position['unrealizedProfitLoss']))/100, + # 'instrument': {'symbol': position["symbol"], # ib example + # 'assetType': position['tradingClass'], # ib example + # } + # } + # acc_inf['securitiesAccount']['positions'].append(pos) - # checks if account has no open pos - if not len(positions): - acc_inf['securitiesAccount']['positions'] = [] - print("No portfolio") + # # checks if account has no open pos + # if not len(positions): + # acc_inf['securitiesAccount']['positions'] = [] + # print("No portfolio") - # get orders and add them to acc_inf - orders = self.session.openOrders() # ib example - orders_inf =[] + # # get orders and add them to acc_inf + # orders = self.session.openOrders() # ib example + # orders_inf =[] - for order in orders: - order_status = order['status'].upper() - if order_status in ['CANCELLED', 'FAILED']: - continue - orders_inf.append(self.format_order(order)) - acc_inf['securitiesAccount']['orderStrategies'] = orders_inf + # for order in orders: + # order_status = order['status'].upper() + # if order_status in ['CANCELLED', 'FAILED']: + # continue + # orders_inf.append(self.format_order(order)) + # acc_inf['securitiesAccount']['orderStrategies'] = orders_inf + print(acc_inf) return acc_inf def get_order_info(self, order_id): @@ -447,4 +446,4 @@ def make_STC_SL_trailstop(self, Symbol:str, Qty:int, trail_stop_const:float, ac kwargs['trial_value'] = trail_stop_const kwargs['trial_type'] = 'DOLLAR' kwargs['outsideRegularTradingHour'] = True - return kwargs \ No newline at end of file + return kwargs From 066161cd0bec6f2c3edbec2f85811f6848e00dd2 Mon Sep 17 00:00:00 2001 From: rax Date: Sun, 3 Sep 2023 23:34:31 -0400 Subject: [PATCH 10/10] fixed get_account_info stub --- DiscordAlertsTrader/brokerages/ibkr_api.py | 59 +++++++++++----------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/DiscordAlertsTrader/brokerages/ibkr_api.py b/DiscordAlertsTrader/brokerages/ibkr_api.py index 636b64c..9f8a619 100644 --- a/DiscordAlertsTrader/brokerages/ibkr_api.py +++ b/DiscordAlertsTrader/brokerages/ibkr_api.py @@ -61,39 +61,38 @@ def get_account_info(self,account:str =""): }} # get positions of the account + positions = self.session.positions() # ib example - - # for position in positions: - # pos = { - # "longQuantity" : eval(position['position']), # ib example - # "symbol": position["symbol"], # ib example - # "marketValue": eval(position['position']), - # "assetType": position['tradingClass'], # ib example - # "averagePrice": eval(position['avgCost']), # ib example - # "currentDayProfitLoss": eval(position['unrealizedProfitLoss']), - # "currentDayProfitLossPercentage": float(eval(position['unrealizedProfitLoss']))/100, - # 'instrument': {'symbol': position["symbol"], # ib example - # 'assetType': position['tradingClass'], # ib example - # } - # } - # acc_inf['securitiesAccount']['positions'].append(pos) + for position in positions: + pos = { + "longQuantity" : position.position, # ib example + "symbol": position.contract.symbol, # ib example + "marketValue": 'n/a', #todo: fetch in realtime. + "assetType": position.contract.tradingClass, # ib example + "averagePrice":position.avgCost, # ib example + "currentDayProfitLoss": 'n/a', + "currentDayProfitLossPercentage": 'n/a', + 'instrument': {'symbol': position.contract.symbol, # ib example + 'assetType': position.contract.tradingClass, # ib example + } + } + acc_inf['securitiesAccount']['positions'].append(pos) - # # checks if account has no open pos - # if not len(positions): - # acc_inf['securitiesAccount']['positions'] = [] - # print("No portfolio") + # checks if account has no open pos + if not len(positions): + acc_inf['securitiesAccount']['positions'] = [] + print("No portfolio") - # # get orders and add them to acc_inf - # orders = self.session.openOrders() # ib example - # orders_inf =[] - - # for order in orders: - # order_status = order['status'].upper() - # if order_status in ['CANCELLED', 'FAILED']: - # continue - # orders_inf.append(self.format_order(order)) - # acc_inf['securitiesAccount']['orderStrategies'] = orders_inf - print(acc_inf) + # get orders and add them to acc_inf + orders = self.session.openOrders() # ib example + orders_inf =[] + print(orders) + for order in orders: + order_status = order['status'].upper() + if order_status in ['CANCELLED', 'FAILED']: + continue + orders_inf.append(self.format_order(order)) + acc_inf['securitiesAccount']['orderStrategies'] = orders_inf return acc_inf def get_order_info(self, order_id):