From e97f0524ba2258cf6f5a2669678ca92233625259 Mon Sep 17 00:00:00 2001 From: Cfomodz Date: Wed, 8 May 2024 15:04:32 -0600 Subject: [PATCH 1/3] Update webull.py --- webull/webull.py | 75 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/webull/webull.py b/webull/webull.py index ec61e89..19b06db 100644 --- a/webull/webull.py +++ b/webull/webull.py @@ -19,7 +19,7 @@ class webull : - def __init__(self, region_code=None) : + def __init__(self, did, region_code=None) : self._session = requests.session() self._headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0', @@ -39,7 +39,7 @@ def __init__(self, region_code=None) : 'locale': 'eng', # 'reqid': req_id, 'device-type': 'Web', - 'did': self._get_did() + 'did': did } #endpoints @@ -81,7 +81,7 @@ def _get_did(self, path=''): pickle.dump(did, open(filename, 'wb')) return did - def _set_did(self, did, path=''): + def set_did(self, did, path=''): ''' If your starting to use this package after webull's new image verification for login, you'll need to login from a browser to get your did file in order to login through this api. You can @@ -319,30 +319,45 @@ def get_account_id(self, id=0): ''' headers = self.build_req_headers() - response = requests.get(self._urls.account_id(), headers=headers, timeout=self.timeout) + response = requests.get(self._urls.account_list(), headers=headers, timeout=self.timeout) result = response.json() - if result['success'] and len(result['data']) > 0 : - self.zone_var = str(result['data'][int(id)]['rzone']) - self._account_id = str(result['data'][int(id)]['secAccountId']) + if result.get('accountList') and id < len(result['accountList']) and result['accountList'][int(id)]['status'] == 'active': + self.zone_var = str(result['accountList'][int(id)]['rzone']) + self._account_id = str(result['accountList'][int(id)]['secAccountId']) return self._account_id else: return None - def get_account(self): + def set_account_id(self, account_id): + ''' + set account id + ''' + self._account_id = account_id + + def get_account(self, v2=False): ''' get important details of account, positions, portfolio stance...etc ''' - headers = self.build_req_headers() - response = requests.get(self._urls.account(self._account_id), headers=headers, timeout=self.timeout) + headers = self.build_req_headers(include_trade_token=True) + url = self._urls.account(self._account_id) + if v2: + url = self._urls.account_summary(self._account_id) + response = requests.get(url, headers=headers, timeout=self.timeout) result = response.json() return result - def get_positions(self): + def get_positions(self, v2=False): ''' output standing positions of stocks ''' - data = self.get_account() - return data['positions'] + data = self.get_account(v2) + if v2: + if data.get('assetSummaryVO', {}).get('positions') is not None: + return data['assetSummaryVO']['positions'] + else: + if data.get('positions') is not None: + return data['positions'] + return None def get_portfolio(self): ''' @@ -497,16 +512,26 @@ def place_order(self, stock=None, tId=None, price=0, action='BUY', orderType='LM pass elif not stock is None: tId = self.get_ticker(stock) + print(tId) else: raise ValueError('Must provide a stock symbol or a stock id') + # Webull only supports fractionals < 1 full share + if quant < 1: + quant = float(quant) + elif quant % 1 == 0: # Check for 1.0, 2.0 float etc + quant = int(quant) + elif isinstance(quant, float): + raise ValueError('Fractional shares must be less than 1') + print(quant) + headers = self.build_req_headers(include_trade_token=True, include_time=True) data = { 'action': action, 'comboType': 'NORMAL', 'orderType': orderType, 'outsideRegularTradingHour': outsideRegularTradingHour, - 'quantity': int(quant), + 'quantity': quant, 'serialId': str(uuid.uuid4()), 'tickerId': tId, 'timeInForce': enforce @@ -517,15 +542,17 @@ def place_order(self, stock=None, tId=None, price=0, action='BUY', orderType='LM data['outsideRegularTradingHour'] = False elif orderType == 'LMT': data['lmtPrice'] = float(price) - elif orderType == 'STP' : - data['auxPrice'] = float(stpPrice) - elif orderType == 'STP LMT' : - data['lmtPrice'] = float(price) - data['auxPrice'] = float(stpPrice) - elif orderType == 'STP TRAIL' : - data['trailingStopStep'] = float(trial_value) - data['trailingType'] = str(trial_type) - + print(data) + + # Check if account can place order (PDT check, etc) + check_response = requests.post(self._urls.check_stock_order(self._account_id), json=data, headers=headers, timeout=self.timeout) + check_result = check_response.json() + print(check_result) + if not check_result['forward']: + warning = check_result['checkResultList'][0] + raise Exception(f"{warning['code']}: {warning['msg']}") + time.sleep(2.5) + # Place order response = requests.post(self._urls.place_orders(self._account_id), json=data, headers=headers, timeout=self.timeout) return response.json() @@ -1471,7 +1498,7 @@ def place_order(self, stock=None, tId=None, price=0, action='BUY', orderType='LM 'lmtPrice': float(price), 'orderType': orderType, # 'LMT','MKT' 'outsideRegularTradingHour': outsideRegularTradingHour, - 'quantity': int(quant), + 'quantity': float(quant) if orderType == 'MKT' else int(quant), 'serialId': str(uuid.uuid4()), 'tickerId': tId, 'timeInForce': enforce # GTC or DAY From e8018910d5d3198791fc5b77f8cba4bf892497cf Mon Sep 17 00:00:00 2001 From: Cfomodz Date: Wed, 8 May 2024 15:08:02 -0600 Subject: [PATCH 2/3] Update endpoints.py --- webull/endpoints.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/webull/endpoints.py b/webull/endpoints.py index 5c52bae..2989bfa 100644 --- a/webull/endpoints.py +++ b/webull/endpoints.py @@ -20,12 +20,18 @@ def __init__(self) : def account(self, account_id): return f'{self.base_trade_url}/v3/home/{account_id}' + def account_summary(self, account_id): + return f'{self.base_ustrade_url}/trading/v1/webull/account/accountAssetSummary/v2?secAccountId={account_id}' + def account_id(self): return f'{self.base_trade_url}/account/getSecAccountList/v5' def account_activities(self, account_id): return f'{self.base_ustrade_url}/trade/v2/funds/{account_id}/activities' + def account_list(self): + return f'{self.base_new_trade_url}/trading/v1/global/tradetab/display?supportOmniIra=true' + def active_gainers_losers(self, direction, region_code, rank_type, num) : if direction == 'gainer' : url = 'topGainers' @@ -74,6 +80,9 @@ def cancel_otoco_orders(self, account_id, combo_id): def check_otoco_orders(self, account_id): return f'{self.base_ustrade_url}/trade/v2/corder/stock/check/{account_id}' + def check_stock_order(self, account_id): + return f'{self.base_ustrade_url}/trading/v1/webull/order/stockOrderCheck?secAccountId={account_id}' + def place_otoco_orders(self, account_id): return f'{self.base_ustrade_url}/trade/v2/corder/stock/place/{account_id}' @@ -168,7 +177,7 @@ def place_option_orders(self, account_id): return f'{self.base_ustrade_url}/trade/v2/option/placeOrder/{account_id}' def place_orders(self, account_id): - return f'{self.base_ustrade_url}/trade/order/{account_id}/placeStockOrder' + return f'{self.base_ustrade_url}/trading/v1/webull/order/stockOrderPlace?secAccountId={account_id}' def modify_order(self, account_id, order_id): return f'{self.base_ustrade_url}/trading/v1/webull/order/stockOrderModify?secAccountId={account_id}' From f22754c67bb830e3614adfcce1d46335b8bfc961 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 8 May 2024 18:00:25 -0400 Subject: [PATCH 3/3] account based on broker ID (the one shown in UI) --- webull/webull.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/webull/webull.py b/webull/webull.py index 19b06db..820d048 100644 --- a/webull/webull.py +++ b/webull/webull.py @@ -321,12 +321,18 @@ def get_account_id(self, id=0): response = requests.get(self._urls.account_list(), headers=headers, timeout=self.timeout) result = response.json() - if result.get('accountList') and id < len(result['accountList']) and result['accountList'][int(id)]['status'] == 'active': - self.zone_var = str(result['accountList'][int(id)]['rzone']) - self._account_id = str(result['accountList'][int(id)]['secAccountId']) - return self._account_id - else: + print(result) + account_list = result.get('accountList') + if not account_list: + return None + target_account = account_list[id] if isinstance(id, int) else [account for account in account_list if account['accountNumber'] == id][0] + if not target_account: + return None + if target_account.get('status') != 'active': return None + self.zone_var = str(target_account['rzone']) + self._account_id = str(target_account['secAccountId']) + return self._account_id def set_account_id(self, account_id): '''