Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion webull/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}'

Expand Down Expand Up @@ -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}'
Expand Down
85 changes: 59 additions & 26 deletions webull/webull.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -319,30 +319,51 @@ 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'])
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 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):
'''
Expand Down Expand Up @@ -497,16 +518,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
Expand All @@ -517,15 +548,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()

Expand Down Expand Up @@ -1471,7 +1504,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
Expand Down