tradezero_api.main

  1from __future__ import annotations
  2
  3import time
  4import os
  5import warnings
  6from collections import namedtuple
  7
  8from selenium.webdriver.chrome.service import Service as ChromeService
  9from webdriver_manager.chrome import ChromeDriverManager
 10from selenium import webdriver
 11from selenium.webdriver.common.by import By
 12from selenium.webdriver.common.keys import Keys
 13from selenium.webdriver.support.ui import Select
 14from selenium.common.exceptions import NoSuchElementException, WebDriverException, StaleElementReferenceException
 15from termcolor import colored
 16
 17from .time_helpers import Time, Timer, time_it
 18from .watchlist import Watchlist
 19from .portfolio import Portfolio
 20from .notification import Notification
 21from .account import Account
 22from .enums import Order, TIF
 23
 24os.system('color')
 25
 26TZ_HOME_URL = 'https://standard.tradezeroweb.us/'
 27
 28
 29class TradeZero(Time):
 30    def __init__(self, user_name: str, password: str, headless: bool = False,
 31                 hide_attributes: bool = False):
 32        """
 33        :param user_name: TradeZero user_name
 34        :param password: TradeZero password
 35        :param headless: default: False, True will run the browser in headless mode, which means it won't be visible
 36        :param hide_attributes: bool, if True: Hide account attributes (acc username, equity, total exposure...)
 37        """
 38        super().__init__()
 39        self.user_name = user_name
 40        self.password = password
 41        self.hide_attributes = hide_attributes
 42
 43        service = ChromeService(ChromeDriverManager().install())
 44        options = webdriver.ChromeOptions()
 45        options.add_experimental_option('excludeSwitches', ['enable-logging'])
 46        if headless is True:
 47            options.headless = headless
 48
 49        self.driver = webdriver.Chrome(service=service, options=options)
 50        self.driver.get(TZ_HOME_URL)
 51
 52        self.Watchlist = Watchlist(self.driver)
 53        self.Portfolio = Portfolio(self.driver)
 54        self.Notification = Notification(self.driver)
 55        self.Account = Account(self.driver)
 56
 57        # to instantiate the time, pytz, and datetime modules:
 58        Timer()
 59        self.time_between(time1=(9, 30), time2=(10, 30))
 60
 61    def _dom_fully_loaded(self, iter_amount: int = 1):
 62        """
 63        check that webpage elements are fully loaded/visible.
 64        there is no need to call this method, but instead call tz_conn() and that will take care of all the rest.
 65
 66        :param iter_amount: int, default: 1, number of times it will iterate.
 67        :return: if the elements are fully loaded: return True, else: return False.
 68        """
 69        container_xpath = "//*[contains(@id,'portfolio-container')]//div//div//h2"
 70        for i in range(iter_amount):
 71            elements = self.driver.find_elements(By.XPATH, container_xpath)
 72            text_elements = [x.text for x in elements]
 73            if 'Portfolio' in text_elements:
 74                return True
 75            time.sleep(0.5)
 76        return False
 77
 78    @time_it
 79    def login(self, log_time_elapsed: bool = False):
 80        """
 81        log-in TradeZero's website
 82
 83        :param log_time_elapsed: bool, if True it will print time elapsed for login
 84        """
 85        login_form = self.driver.find_element(By.ID, "login")
 86        login_form.send_keys(self.user_name)
 87
 88        password_form = self.driver.find_element(By.ID, "password")
 89        password_form.send_keys(self.password, Keys.RETURN)
 90
 91        self._dom_fully_loaded(150)
 92        if self.hide_attributes:
 93            self.Account.hide_attributes()
 94
 95        Select(self.driver.find_element(By.ID, "trading-order-select-type")).select_by_index(1)
 96
 97    def conn(self, log_tz_conn: bool = False):
 98        """
 99        make sure that the website stays connected and is fully loaded.
100        TradeZero will ask for a Login twice a day, and sometimes it will require the page to be reloaded,
101        so this will make sure that its fully loaded, by reloading or doing the login.
102
103        :param log_tz_conn: bool, default: False. if True it will print if it reconnects through the login or refresh.
104        :return: True if connected
105        :raises Exception: if it fails to reconnect after a while
106        """
107        if self._dom_fully_loaded(1):
108            return True
109
110        try:
111            self.driver.find_element(By.ID, "login")
112            self.login()
113
114            self.Watchlist.restore()
115
116            if log_tz_conn is True:
117                print(colored('tz_conn(): Login worked', 'cyan'))
118            return True
119
120        except NoSuchElementException:
121            self.driver.get("https://standard.tradezeroweb.us/")
122            if self._dom_fully_loaded(150):
123
124                if self.hide_attributes:
125                    self.Account.hide_attributes()
126
127                self.Watchlist.restore()
128
129                if log_tz_conn is True:
130                    print(colored('tz_conn(): Refresh worked', 'cyan'))
131                return True
132
133        raise Exception('@ tz_conn(): Error: not able to reconnect, max retries exceeded')
134
135    def exit(self):
136        """close Selenium window and driver"""
137        try:
138            self.driver.close()
139        except WebDriverException:
140            pass
141
142        self.driver.quit()
143
144    def load_symbol(self, symbol: str):
145        """
146        make sure the data for the symbol is fully loaded and that the symbol itself is valid
147
148        :param symbol: str
149        :return: True if symbol data loaded, False if prices == 0.00 (mkt closed), Error if symbol not found
150        :raises Exception: if symbol not found
151        """
152        if symbol.upper() == self.current_symbol():
153            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
154            if price.isdigit() and float(price) > 0:
155                return True
156
157        input_symbol = self.driver.find_element(By.ID, "trading-order-input-symbol")
158        input_symbol.send_keys(symbol.lower(), Keys.RETURN)
159        time.sleep(0.04)
160
161        for i in range(300):
162            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
163            if price == '':
164                time.sleep(0.01)
165
166            elif price.isdigit() and float(price) == 0:
167                warnings.warn(f"Market Closed, ask/bid = {price}")
168                return False
169
170            elif price.isdigit():
171                return True
172
173            elif i == 15 or i == 299:
174                last_notif = self.Notification.get_last_notification_message()
175                message = f'Symbol not found: {symbol.upper()}'
176                if message == last_notif:
177                    raise Exception(f"ERROR: {symbol=} Not found")
178
179    def current_symbol(self):
180        """get current symbol"""
181        return self.driver.find_element(By.ID, 'trading-order-symbol').text.replace('(USD)', '')
182
183    @property
184    def bid(self):
185        """get bid price"""
186        return float(self.driver.find_element(By.ID, 'trading-order-bid').text.replace(',', ''))
187
188    @property
189    def ask(self):
190        """get ask price"""
191        return float(self.driver.find_element(By.ID, 'trading-order-ask').text.replace(',', ''))
192
193    @property
194    def last(self):
195        """get last price"""
196        return float(self.driver.find_element(By.ID, 'trading-order-p').text.replace(',', ''))
197
198    def data(self, symbol: str):
199        """
200        return a namedtuple with data for the given symbol, the properties are:
201        'open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'.
202
203        :param symbol: str: ex: 'aapl', 'amd', 'NVDA', 'GM'
204        :return: namedtuple = (open, high, low, close, volume, last, ask, bid)
205        """
206        Data = namedtuple('Data', ['open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'])
207
208        if self.load_symbol(symbol) is False:
209            return Data(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
210
211        element_ids = [
212            'trading-order-open',
213            'trading-order-high',
214            'trading-order-low',
215            'trading-order-close',
216            'trading-order-vol',
217            'trading-order-p',
218            'trading-order-ask',
219            'trading-order-bid',
220        ]
221        lst = []
222        for id_ in element_ids:
223            val = self.driver.find_element(By.ID, id_).text
224            val = float(val.replace(',', ''))  # replace comma for volume, and when prices > 999
225            lst.append(val)
226
227        return Data._make(lst)
228
229    def calculate_order_quantity(self, symbol: str, buying_power: float, float_option: bool = False):
230        """
231        returns the amount of shares you can buy with the given buying_power as int(), but if float_option is True,
232        it will return the amount as a float.
233
234        :param symbol: str
235        :param buying_power: float,
236        :param float_option: bool, default: False, if True returns the original number as float
237        :return: int or float
238        """
239        if self.load_symbol(symbol) is False:
240            return
241        quantity = (buying_power / self.last)
242
243        if float_option is True:
244            return quantity
245        return int(quantity)
246
247    def locate_stock(self, symbol: str, share_amount: int, max_price: float = 0, debug_info: bool = False):
248        """
249        Locate a stock, requires: stock symbol, and share_amount. optional: max_price.
250        if the locate_price is less than max_price: it will accept, else: decline.
251
252        :param symbol: str, symbol to locate.
253        :param share_amount: int, must be a multiple of 100 (100, 200, 300...)
254        :param max_price: float, default: 0, total price you are willing to pay for locates
255        :param debug_info: bool, if True it will print info about the locates in the console
256        :return: named tuple with the following attributes: 'price_per_share' and 'total'
257        :raises Exception: if share_amount is not divisible by 100
258        """
259        Data = namedtuple('Data', ['price_per_share', 'total'])
260
261        if share_amount is not None and share_amount % 100 != 0:
262            raise Exception(f'ERROR: share_amount is not divisible by 100 ({share_amount=})')
263
264        if not self.load_symbol(symbol):
265            return
266
267        if self.last <= 1.00:
268            print(f'Error: Cannot locate stocks priced under $1.00 ({symbol=}, price={self.last})')
269
270        self.driver.find_element(By.ID, "locate-tab-1").click()
271        input_symbol = self.driver.find_element(By.ID, "short-list-input-symbol")
272        input_symbol.clear()
273        input_symbol.send_keys(symbol, Keys.RETURN)
274
275        input_shares = self.driver.find_element(By.ID, "short-list-input-shares")
276        input_shares.clear()
277        input_shares.send_keys(share_amount)
278
279        while self.driver.find_element(By.ID, "short-list-locate-status").text == '':
280            time.sleep(0.1)
281
282        if self.driver.find_element(By.ID, "short-list-locate-status").text == 'Easy to borrow':
283            locate_pps = 0.00
284            locate_total = 0.00
285            if debug_info:
286                print(colored(f'Stock ({symbol}) is "Easy to borrow"', 'green'))
287            return Data(locate_pps, locate_total)
288
289        self.driver.find_element(By.ID, "short-list-button-locate").click()
290
291        for i in range(300):
292            try:
293                locate_pps = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-2").text)
294                locate_total = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-6").text)
295                break
296
297            except (ValueError, NoSuchElementException, StaleElementReferenceException):
298                time.sleep(0.15)
299                if i == 15 or i == 299:
300                    insufficient_bp = 'Insufficient BP to short a position with requested quantity.'
301                    last_notif = self.Notification.get_last_notification_message()
302                    if insufficient_bp in last_notif:
303                        warnings.warn(f"ERROR! {insufficient_bp}")
304                        return
305        else:
306            raise Exception(f'Error: not able to locate symbol element ({symbol=})')
307
308        if locate_total <= max_price:
309            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[1]').click()
310            if debug_info:
311                print(colored(f'HTB Locate accepted ({symbol}, $ {locate_total})', 'cyan'))
312        else:
313            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[2]').click()
314
315        return Data(locate_pps, locate_total)
316        # TODO create a function to get the pps and another one that just locates the shares
317
318    def credit_locates(self, symbol: str, quantity=None):
319        """
320        sell/ credit stock locates, if no value is given in 'quantity', it will credit all the shares
321        available of the given symbol.
322
323        :param symbol: str
324        :param quantity: amount of shares to sell, must be a multiple of 100, ie: 100, 200, 300
325        :return:
326        :raises Exception: if given symbol in not already located
327        :raises ValueError: if quantity is not divisible by 100 or quantity > located shares
328        """
329        located_symbols = self.driver.find_elements(By.XPATH, '//*[@id="locate-inventory-table"]/tbody/tr/td[1]')
330        located_symbols = [x.text for x in located_symbols]
331
332        if symbol.upper() not in located_symbols:
333            raise Exception(f"ERROR! cannot find {symbol} in located symbols")
334
335        if quantity is not None:
336            if quantity % 100 != 0:
337                raise ValueError(f"ERROR! quantity is not divisible by 100 ({quantity=})")
338
339            located_shares = float(self.driver.find_element(By.ID, f"inv-{symbol.upper()}-cell-1").text)
340            if quantity > located_shares:
341                raise ValueError(f"ERROR! you cannot credit more shares than u already have "
342                                 f"({quantity} vs {located_shares}")
343
344            input_quantity = self.driver.find_element(By.ID, f"inv-{symbol.upper()}-sell-qty")
345            input_quantity.clear()
346            input_quantity.send_keys(quantity)
347
348        self.driver.find_element(By.XPATH, f'//*[@id="inv-{symbol.upper()}-sell"]/button').click()
349        return
350
351    @time_it
352    def limit_order(self, order_direction: Order, symbol: str, share_amount: int, limit_price: float,
353                    time_in_force: TIF = TIF.DAY, log_info: bool = False):
354        """
355        Place a Limit Order, the following params are required: order_direction, symbol, share_amount, and limit_price.
356
357        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
358        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
359        :param limit_price: float
360        :param share_amount: int
361        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
362        :param log_info: bool, if True it will print information about the order
363        :return: True if operation succeeded
364        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
365        """
366        symbol = symbol.lower()
367        order_direction = order_direction.value
368        time_in_force = time_in_force.value
369
370        if time_in_force not in ['DAY', 'GTC', 'GTX']:
371            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
372
373        self.load_symbol(symbol)
374
375        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
376        order_menu.select_by_index(1)
377
378        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
379        tif_menu.select_by_visible_text(time_in_force)
380
381        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
382        input_quantity.clear()
383        input_quantity.send_keys(share_amount)
384
385        price_input = self.driver.find_element(By.ID, "trading-order-input-price")
386        price_input.clear()
387        price_input.send_keys(limit_price)
388
389        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
390
391        if log_info is True:
392            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
393                  f"Limit Price: {limit_price}, Shares amount: {share_amount}")
394
395    @time_it
396    def market_order(self, order_direction: Order, symbol: str, share_amount: int,
397                     time_in_force: TIF = TIF.DAY, log_info: bool = False):
398        """
399        Place a Market Order, The following params are required: order_direction, symbol, and share_amount
400
401        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
402        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
403        :param share_amount: int
404        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
405        :param log_info: bool, if True it will print information about the order
406        :return:
407        :raises Exception: if time not during market hours (9:30 - 16:00)
408        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
409        """
410        symbol = symbol.lower()
411        order_direction = order_direction.value
412        time_in_force = time_in_force.value
413
414        if not self.time_between((9, 30), (16, 0)):
415            raise Exception(f'Error: Market orders are not allowed at this time ({self.time})')
416
417        if time_in_force not in ['DAY', 'GTC', 'GTX']:
418            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
419
420        self.load_symbol(symbol)
421
422        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
423        order_menu.select_by_index(0)
424
425        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
426        tif_menu.select_by_visible_text(time_in_force)
427
428        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
429        input_quantity.clear()
430        input_quantity.send_keys(share_amount)
431
432        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
433
434        if log_info is True:
435            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
436                  f"Price: {self.last}, Shares amount: {share_amount}")
437
438    @time_it
439    def stop_market_order(self, order_direction: Order, symbol: str, share_amount: int, stop_price: float,
440                          time_in_force: TIF = TIF.DAY, log_info: bool = False):
441        """
442        Place a Stop Market Order, the following params are required: order_direction, symbol,
443        share_amount, and stop_price.
444        note that a Stop Market Order can only be placed during market-hours (09:30:00 - 16:00:00), therefore if a
445        Stop Market Order is placed outside market hours it will raise an error.
446
447        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
448        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
449        :param stop_price: float
450        :param share_amount: int
451        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
452        :param log_info: bool, if True it will print information about the order
453        :return: True if operation succeeded
454        :raises Exception: if time not during market hours (9:30 - 16:00)
455        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
456        """
457        symbol = symbol.lower()
458        order_direction = order_direction.value
459        time_in_force = time_in_force.value
460
461        if not self.time_between((9, 30), (16, 0)):
462            raise Exception(f'Error: Stop Market orders are not allowed at this time ({self.time})')
463
464        if time_in_force not in ['DAY', 'GTC', 'GTX']:
465            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
466
467        self.load_symbol(symbol)
468
469        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
470        order_menu.select_by_index(2)
471
472        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
473        tif_menu.select_by_visible_text(time_in_force)
474
475        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
476        input_quantity.clear()
477        input_quantity.send_keys(share_amount)
478
479        price_input = self.driver.find_element(By.ID, "trading-order-input-sprice")
480        price_input.clear()
481        price_input.send_keys(stop_price)
482
483        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
484
485        if log_info is True:
486            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
487                  f"Stop Price: {stop_price}, Shares amount: {share_amount}")
TZ_HOME_URL = 'https://standard.tradezeroweb.us/'
class TradeZero(tradezero_api.time_helpers.Time):
 30class TradeZero(Time):
 31    def __init__(self, user_name: str, password: str, headless: bool = False,
 32                 hide_attributes: bool = False):
 33        """
 34        :param user_name: TradeZero user_name
 35        :param password: TradeZero password
 36        :param headless: default: False, True will run the browser in headless mode, which means it won't be visible
 37        :param hide_attributes: bool, if True: Hide account attributes (acc username, equity, total exposure...)
 38        """
 39        super().__init__()
 40        self.user_name = user_name
 41        self.password = password
 42        self.hide_attributes = hide_attributes
 43
 44        service = ChromeService(ChromeDriverManager().install())
 45        options = webdriver.ChromeOptions()
 46        options.add_experimental_option('excludeSwitches', ['enable-logging'])
 47        if headless is True:
 48            options.headless = headless
 49
 50        self.driver = webdriver.Chrome(service=service, options=options)
 51        self.driver.get(TZ_HOME_URL)
 52
 53        self.Watchlist = Watchlist(self.driver)
 54        self.Portfolio = Portfolio(self.driver)
 55        self.Notification = Notification(self.driver)
 56        self.Account = Account(self.driver)
 57
 58        # to instantiate the time, pytz, and datetime modules:
 59        Timer()
 60        self.time_between(time1=(9, 30), time2=(10, 30))
 61
 62    def _dom_fully_loaded(self, iter_amount: int = 1):
 63        """
 64        check that webpage elements are fully loaded/visible.
 65        there is no need to call this method, but instead call tz_conn() and that will take care of all the rest.
 66
 67        :param iter_amount: int, default: 1, number of times it will iterate.
 68        :return: if the elements are fully loaded: return True, else: return False.
 69        """
 70        container_xpath = "//*[contains(@id,'portfolio-container')]//div//div//h2"
 71        for i in range(iter_amount):
 72            elements = self.driver.find_elements(By.XPATH, container_xpath)
 73            text_elements = [x.text for x in elements]
 74            if 'Portfolio' in text_elements:
 75                return True
 76            time.sleep(0.5)
 77        return False
 78
 79    @time_it
 80    def login(self, log_time_elapsed: bool = False):
 81        """
 82        log-in TradeZero's website
 83
 84        :param log_time_elapsed: bool, if True it will print time elapsed for login
 85        """
 86        login_form = self.driver.find_element(By.ID, "login")
 87        login_form.send_keys(self.user_name)
 88
 89        password_form = self.driver.find_element(By.ID, "password")
 90        password_form.send_keys(self.password, Keys.RETURN)
 91
 92        self._dom_fully_loaded(150)
 93        if self.hide_attributes:
 94            self.Account.hide_attributes()
 95
 96        Select(self.driver.find_element(By.ID, "trading-order-select-type")).select_by_index(1)
 97
 98    def conn(self, log_tz_conn: bool = False):
 99        """
100        make sure that the website stays connected and is fully loaded.
101        TradeZero will ask for a Login twice a day, and sometimes it will require the page to be reloaded,
102        so this will make sure that its fully loaded, by reloading or doing the login.
103
104        :param log_tz_conn: bool, default: False. if True it will print if it reconnects through the login or refresh.
105        :return: True if connected
106        :raises Exception: if it fails to reconnect after a while
107        """
108        if self._dom_fully_loaded(1):
109            return True
110
111        try:
112            self.driver.find_element(By.ID, "login")
113            self.login()
114
115            self.Watchlist.restore()
116
117            if log_tz_conn is True:
118                print(colored('tz_conn(): Login worked', 'cyan'))
119            return True
120
121        except NoSuchElementException:
122            self.driver.get("https://standard.tradezeroweb.us/")
123            if self._dom_fully_loaded(150):
124
125                if self.hide_attributes:
126                    self.Account.hide_attributes()
127
128                self.Watchlist.restore()
129
130                if log_tz_conn is True:
131                    print(colored('tz_conn(): Refresh worked', 'cyan'))
132                return True
133
134        raise Exception('@ tz_conn(): Error: not able to reconnect, max retries exceeded')
135
136    def exit(self):
137        """close Selenium window and driver"""
138        try:
139            self.driver.close()
140        except WebDriverException:
141            pass
142
143        self.driver.quit()
144
145    def load_symbol(self, symbol: str):
146        """
147        make sure the data for the symbol is fully loaded and that the symbol itself is valid
148
149        :param symbol: str
150        :return: True if symbol data loaded, False if prices == 0.00 (mkt closed), Error if symbol not found
151        :raises Exception: if symbol not found
152        """
153        if symbol.upper() == self.current_symbol():
154            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
155            if price.isdigit() and float(price) > 0:
156                return True
157
158        input_symbol = self.driver.find_element(By.ID, "trading-order-input-symbol")
159        input_symbol.send_keys(symbol.lower(), Keys.RETURN)
160        time.sleep(0.04)
161
162        for i in range(300):
163            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
164            if price == '':
165                time.sleep(0.01)
166
167            elif price.isdigit() and float(price) == 0:
168                warnings.warn(f"Market Closed, ask/bid = {price}")
169                return False
170
171            elif price.isdigit():
172                return True
173
174            elif i == 15 or i == 299:
175                last_notif = self.Notification.get_last_notification_message()
176                message = f'Symbol not found: {symbol.upper()}'
177                if message == last_notif:
178                    raise Exception(f"ERROR: {symbol=} Not found")
179
180    def current_symbol(self):
181        """get current symbol"""
182        return self.driver.find_element(By.ID, 'trading-order-symbol').text.replace('(USD)', '')
183
184    @property
185    def bid(self):
186        """get bid price"""
187        return float(self.driver.find_element(By.ID, 'trading-order-bid').text.replace(',', ''))
188
189    @property
190    def ask(self):
191        """get ask price"""
192        return float(self.driver.find_element(By.ID, 'trading-order-ask').text.replace(',', ''))
193
194    @property
195    def last(self):
196        """get last price"""
197        return float(self.driver.find_element(By.ID, 'trading-order-p').text.replace(',', ''))
198
199    def data(self, symbol: str):
200        """
201        return a namedtuple with data for the given symbol, the properties are:
202        'open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'.
203
204        :param symbol: str: ex: 'aapl', 'amd', 'NVDA', 'GM'
205        :return: namedtuple = (open, high, low, close, volume, last, ask, bid)
206        """
207        Data = namedtuple('Data', ['open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'])
208
209        if self.load_symbol(symbol) is False:
210            return Data(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
211
212        element_ids = [
213            'trading-order-open',
214            'trading-order-high',
215            'trading-order-low',
216            'trading-order-close',
217            'trading-order-vol',
218            'trading-order-p',
219            'trading-order-ask',
220            'trading-order-bid',
221        ]
222        lst = []
223        for id_ in element_ids:
224            val = self.driver.find_element(By.ID, id_).text
225            val = float(val.replace(',', ''))  # replace comma for volume, and when prices > 999
226            lst.append(val)
227
228        return Data._make(lst)
229
230    def calculate_order_quantity(self, symbol: str, buying_power: float, float_option: bool = False):
231        """
232        returns the amount of shares you can buy with the given buying_power as int(), but if float_option is True,
233        it will return the amount as a float.
234
235        :param symbol: str
236        :param buying_power: float,
237        :param float_option: bool, default: False, if True returns the original number as float
238        :return: int or float
239        """
240        if self.load_symbol(symbol) is False:
241            return
242        quantity = (buying_power / self.last)
243
244        if float_option is True:
245            return quantity
246        return int(quantity)
247
248    def locate_stock(self, symbol: str, share_amount: int, max_price: float = 0, debug_info: bool = False):
249        """
250        Locate a stock, requires: stock symbol, and share_amount. optional: max_price.
251        if the locate_price is less than max_price: it will accept, else: decline.
252
253        :param symbol: str, symbol to locate.
254        :param share_amount: int, must be a multiple of 100 (100, 200, 300...)
255        :param max_price: float, default: 0, total price you are willing to pay for locates
256        :param debug_info: bool, if True it will print info about the locates in the console
257        :return: named tuple with the following attributes: 'price_per_share' and 'total'
258        :raises Exception: if share_amount is not divisible by 100
259        """
260        Data = namedtuple('Data', ['price_per_share', 'total'])
261
262        if share_amount is not None and share_amount % 100 != 0:
263            raise Exception(f'ERROR: share_amount is not divisible by 100 ({share_amount=})')
264
265        if not self.load_symbol(symbol):
266            return
267
268        if self.last <= 1.00:
269            print(f'Error: Cannot locate stocks priced under $1.00 ({symbol=}, price={self.last})')
270
271        self.driver.find_element(By.ID, "locate-tab-1").click()
272        input_symbol = self.driver.find_element(By.ID, "short-list-input-symbol")
273        input_symbol.clear()
274        input_symbol.send_keys(symbol, Keys.RETURN)
275
276        input_shares = self.driver.find_element(By.ID, "short-list-input-shares")
277        input_shares.clear()
278        input_shares.send_keys(share_amount)
279
280        while self.driver.find_element(By.ID, "short-list-locate-status").text == '':
281            time.sleep(0.1)
282
283        if self.driver.find_element(By.ID, "short-list-locate-status").text == 'Easy to borrow':
284            locate_pps = 0.00
285            locate_total = 0.00
286            if debug_info:
287                print(colored(f'Stock ({symbol}) is "Easy to borrow"', 'green'))
288            return Data(locate_pps, locate_total)
289
290        self.driver.find_element(By.ID, "short-list-button-locate").click()
291
292        for i in range(300):
293            try:
294                locate_pps = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-2").text)
295                locate_total = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-6").text)
296                break
297
298            except (ValueError, NoSuchElementException, StaleElementReferenceException):
299                time.sleep(0.15)
300                if i == 15 or i == 299:
301                    insufficient_bp = 'Insufficient BP to short a position with requested quantity.'
302                    last_notif = self.Notification.get_last_notification_message()
303                    if insufficient_bp in last_notif:
304                        warnings.warn(f"ERROR! {insufficient_bp}")
305                        return
306        else:
307            raise Exception(f'Error: not able to locate symbol element ({symbol=})')
308
309        if locate_total <= max_price:
310            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[1]').click()
311            if debug_info:
312                print(colored(f'HTB Locate accepted ({symbol}, $ {locate_total})', 'cyan'))
313        else:
314            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[2]').click()
315
316        return Data(locate_pps, locate_total)
317        # TODO create a function to get the pps and another one that just locates the shares
318
319    def credit_locates(self, symbol: str, quantity=None):
320        """
321        sell/ credit stock locates, if no value is given in 'quantity', it will credit all the shares
322        available of the given symbol.
323
324        :param symbol: str
325        :param quantity: amount of shares to sell, must be a multiple of 100, ie: 100, 200, 300
326        :return:
327        :raises Exception: if given symbol in not already located
328        :raises ValueError: if quantity is not divisible by 100 or quantity > located shares
329        """
330        located_symbols = self.driver.find_elements(By.XPATH, '//*[@id="locate-inventory-table"]/tbody/tr/td[1]')
331        located_symbols = [x.text for x in located_symbols]
332
333        if symbol.upper() not in located_symbols:
334            raise Exception(f"ERROR! cannot find {symbol} in located symbols")
335
336        if quantity is not None:
337            if quantity % 100 != 0:
338                raise ValueError(f"ERROR! quantity is not divisible by 100 ({quantity=})")
339
340            located_shares = float(self.driver.find_element(By.ID, f"inv-{symbol.upper()}-cell-1").text)
341            if quantity > located_shares:
342                raise ValueError(f"ERROR! you cannot credit more shares than u already have "
343                                 f"({quantity} vs {located_shares}")
344
345            input_quantity = self.driver.find_element(By.ID, f"inv-{symbol.upper()}-sell-qty")
346            input_quantity.clear()
347            input_quantity.send_keys(quantity)
348
349        self.driver.find_element(By.XPATH, f'//*[@id="inv-{symbol.upper()}-sell"]/button').click()
350        return
351
352    @time_it
353    def limit_order(self, order_direction: Order, symbol: str, share_amount: int, limit_price: float,
354                    time_in_force: TIF = TIF.DAY, log_info: bool = False):
355        """
356        Place a Limit Order, the following params are required: order_direction, symbol, share_amount, and limit_price.
357
358        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
359        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
360        :param limit_price: float
361        :param share_amount: int
362        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
363        :param log_info: bool, if True it will print information about the order
364        :return: True if operation succeeded
365        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
366        """
367        symbol = symbol.lower()
368        order_direction = order_direction.value
369        time_in_force = time_in_force.value
370
371        if time_in_force not in ['DAY', 'GTC', 'GTX']:
372            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
373
374        self.load_symbol(symbol)
375
376        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
377        order_menu.select_by_index(1)
378
379        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
380        tif_menu.select_by_visible_text(time_in_force)
381
382        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
383        input_quantity.clear()
384        input_quantity.send_keys(share_amount)
385
386        price_input = self.driver.find_element(By.ID, "trading-order-input-price")
387        price_input.clear()
388        price_input.send_keys(limit_price)
389
390        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
391
392        if log_info is True:
393            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
394                  f"Limit Price: {limit_price}, Shares amount: {share_amount}")
395
396    @time_it
397    def market_order(self, order_direction: Order, symbol: str, share_amount: int,
398                     time_in_force: TIF = TIF.DAY, log_info: bool = False):
399        """
400        Place a Market Order, The following params are required: order_direction, symbol, and share_amount
401
402        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
403        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
404        :param share_amount: int
405        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
406        :param log_info: bool, if True it will print information about the order
407        :return:
408        :raises Exception: if time not during market hours (9:30 - 16:00)
409        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
410        """
411        symbol = symbol.lower()
412        order_direction = order_direction.value
413        time_in_force = time_in_force.value
414
415        if not self.time_between((9, 30), (16, 0)):
416            raise Exception(f'Error: Market orders are not allowed at this time ({self.time})')
417
418        if time_in_force not in ['DAY', 'GTC', 'GTX']:
419            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
420
421        self.load_symbol(symbol)
422
423        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
424        order_menu.select_by_index(0)
425
426        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
427        tif_menu.select_by_visible_text(time_in_force)
428
429        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
430        input_quantity.clear()
431        input_quantity.send_keys(share_amount)
432
433        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
434
435        if log_info is True:
436            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
437                  f"Price: {self.last}, Shares amount: {share_amount}")
438
439    @time_it
440    def stop_market_order(self, order_direction: Order, symbol: str, share_amount: int, stop_price: float,
441                          time_in_force: TIF = TIF.DAY, log_info: bool = False):
442        """
443        Place a Stop Market Order, the following params are required: order_direction, symbol,
444        share_amount, and stop_price.
445        note that a Stop Market Order can only be placed during market-hours (09:30:00 - 16:00:00), therefore if a
446        Stop Market Order is placed outside market hours it will raise an error.
447
448        :param order_direction: str: 'buy', 'sell', 'short', 'cover'
449        :param symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
450        :param stop_price: float
451        :param share_amount: int
452        :param time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
453        :param log_info: bool, if True it will print information about the order
454        :return: True if operation succeeded
455        :raises Exception: if time not during market hours (9:30 - 16:00)
456        :raises AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
457        """
458        symbol = symbol.lower()
459        order_direction = order_direction.value
460        time_in_force = time_in_force.value
461
462        if not self.time_between((9, 30), (16, 0)):
463            raise Exception(f'Error: Stop Market orders are not allowed at this time ({self.time})')
464
465        if time_in_force not in ['DAY', 'GTC', 'GTX']:
466            raise AttributeError(f"Error: time_in_force argument must be one of the following: 'DAY', 'GTC', 'GTX'")
467
468        self.load_symbol(symbol)
469
470        order_menu = Select(self.driver.find_element(By.ID, "trading-order-select-type"))
471        order_menu.select_by_index(2)
472
473        tif_menu = Select(self.driver.find_element(By.ID, "trading-order-select-time"))
474        tif_menu.select_by_visible_text(time_in_force)
475
476        input_quantity = self.driver.find_element(By.ID, "trading-order-input-quantity")
477        input_quantity.clear()
478        input_quantity.send_keys(share_amount)
479
480        price_input = self.driver.find_element(By.ID, "trading-order-input-sprice")
481        price_input.clear()
482        price_input.send_keys(stop_price)
483
484        self.driver.find_element(By.ID, f"trading-order-button-{order_direction}").click()
485
486        if log_info is True:
487            print(f"Time: {self.time}, Order direction: {order_direction}, Symbol: {symbol}, "
488                  f"Stop Price: {stop_price}, Shares amount: {share_amount}")
TradeZero( user_name: str, password: str, headless: bool = False, hide_attributes: bool = False)
31    def __init__(self, user_name: str, password: str, headless: bool = False,
32                 hide_attributes: bool = False):
33        """
34        :param user_name: TradeZero user_name
35        :param password: TradeZero password
36        :param headless: default: False, True will run the browser in headless mode, which means it won't be visible
37        :param hide_attributes: bool, if True: Hide account attributes (acc username, equity, total exposure...)
38        """
39        super().__init__()
40        self.user_name = user_name
41        self.password = password
42        self.hide_attributes = hide_attributes
43
44        service = ChromeService(ChromeDriverManager().install())
45        options = webdriver.ChromeOptions()
46        options.add_experimental_option('excludeSwitches', ['enable-logging'])
47        if headless is True:
48            options.headless = headless
49
50        self.driver = webdriver.Chrome(service=service, options=options)
51        self.driver.get(TZ_HOME_URL)
52
53        self.Watchlist = Watchlist(self.driver)
54        self.Portfolio = Portfolio(self.driver)
55        self.Notification = Notification(self.driver)
56        self.Account = Account(self.driver)
57
58        # to instantiate the time, pytz, and datetime modules:
59        Timer()
60        self.time_between(time1=(9, 30), time2=(10, 30))
Parameters
  • user_name: TradeZero user_name
  • password: TradeZero password
  • headless: default: False, True will run the browser in headless mode, which means it won't be visible
  • hide_attributes: bool, if True: Hide account attributes (acc username, equity, total exposure...)
user_name
password
hide_attributes
driver
Watchlist
Portfolio
Notification
Account
def login(*args, **kwargs):
47    def wrapper(*args, **kwargs):
48        timer = Timer()
49        rv = func(*args, **kwargs)
50
51        if kwargs.get('log_time_elapsed') or kwargs.get('log_info'):
52            print(f'Time elapsed: {timer.time_elapsed:.2f} seconds')
53
54        return rv

log-in TradeZero's website

Parameters
  • log_time_elapsed: bool, if True it will print time elapsed for login
def conn(self, log_tz_conn: bool = False):
 98    def conn(self, log_tz_conn: bool = False):
 99        """
100        make sure that the website stays connected and is fully loaded.
101        TradeZero will ask for a Login twice a day, and sometimes it will require the page to be reloaded,
102        so this will make sure that its fully loaded, by reloading or doing the login.
103
104        :param log_tz_conn: bool, default: False. if True it will print if it reconnects through the login or refresh.
105        :return: True if connected
106        :raises Exception: if it fails to reconnect after a while
107        """
108        if self._dom_fully_loaded(1):
109            return True
110
111        try:
112            self.driver.find_element(By.ID, "login")
113            self.login()
114
115            self.Watchlist.restore()
116
117            if log_tz_conn is True:
118                print(colored('tz_conn(): Login worked', 'cyan'))
119            return True
120
121        except NoSuchElementException:
122            self.driver.get("https://standard.tradezeroweb.us/")
123            if self._dom_fully_loaded(150):
124
125                if self.hide_attributes:
126                    self.Account.hide_attributes()
127
128                self.Watchlist.restore()
129
130                if log_tz_conn is True:
131                    print(colored('tz_conn(): Refresh worked', 'cyan'))
132                return True
133
134        raise Exception('@ tz_conn(): Error: not able to reconnect, max retries exceeded')

make sure that the website stays connected and is fully loaded. TradeZero will ask for a Login twice a day, and sometimes it will require the page to be reloaded, so this will make sure that its fully loaded, by reloading or doing the login.

Parameters
  • log_tz_conn: bool, default: False. if True it will print if it reconnects through the login or refresh.
Returns

True if connected

Raises
  • Exception: if it fails to reconnect after a while
def exit(self):
136    def exit(self):
137        """close Selenium window and driver"""
138        try:
139            self.driver.close()
140        except WebDriverException:
141            pass
142
143        self.driver.quit()

close Selenium window and driver

def load_symbol(self, symbol: str):
145    def load_symbol(self, symbol: str):
146        """
147        make sure the data for the symbol is fully loaded and that the symbol itself is valid
148
149        :param symbol: str
150        :return: True if symbol data loaded, False if prices == 0.00 (mkt closed), Error if symbol not found
151        :raises Exception: if symbol not found
152        """
153        if symbol.upper() == self.current_symbol():
154            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
155            if price.isdigit() and float(price) > 0:
156                return True
157
158        input_symbol = self.driver.find_element(By.ID, "trading-order-input-symbol")
159        input_symbol.send_keys(symbol.lower(), Keys.RETURN)
160        time.sleep(0.04)
161
162        for i in range(300):
163            price = self.driver.find_element(By.ID, "trading-order-ask").text.replace('.', '').replace(',', '')
164            if price == '':
165                time.sleep(0.01)
166
167            elif price.isdigit() and float(price) == 0:
168                warnings.warn(f"Market Closed, ask/bid = {price}")
169                return False
170
171            elif price.isdigit():
172                return True
173
174            elif i == 15 or i == 299:
175                last_notif = self.Notification.get_last_notification_message()
176                message = f'Symbol not found: {symbol.upper()}'
177                if message == last_notif:
178                    raise Exception(f"ERROR: {symbol=} Not found")

make sure the data for the symbol is fully loaded and that the symbol itself is valid

Parameters
  • symbol: str
Returns

True if symbol data loaded, False if prices == 0.00 (mkt closed), Error if symbol not found

Raises
  • Exception: if symbol not found
def current_symbol(self):
180    def current_symbol(self):
181        """get current symbol"""
182        return self.driver.find_element(By.ID, 'trading-order-symbol').text.replace('(USD)', '')

get current symbol

bid

get bid price

ask

get ask price

last

get last price

def data(self, symbol: str):
199    def data(self, symbol: str):
200        """
201        return a namedtuple with data for the given symbol, the properties are:
202        'open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'.
203
204        :param symbol: str: ex: 'aapl', 'amd', 'NVDA', 'GM'
205        :return: namedtuple = (open, high, low, close, volume, last, ask, bid)
206        """
207        Data = namedtuple('Data', ['open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'])
208
209        if self.load_symbol(symbol) is False:
210            return Data(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
211
212        element_ids = [
213            'trading-order-open',
214            'trading-order-high',
215            'trading-order-low',
216            'trading-order-close',
217            'trading-order-vol',
218            'trading-order-p',
219            'trading-order-ask',
220            'trading-order-bid',
221        ]
222        lst = []
223        for id_ in element_ids:
224            val = self.driver.find_element(By.ID, id_).text
225            val = float(val.replace(',', ''))  # replace comma for volume, and when prices > 999
226            lst.append(val)
227
228        return Data._make(lst)

return a namedtuple with data for the given symbol, the properties are: 'open', 'high', 'low', 'close', 'volume', 'last', 'ask', 'bid'.

Parameters
  • symbol: str: ex: 'aapl', 'amd', 'NVDA', 'GM'
Returns

namedtuple = (open, high, low, close, volume, last, ask, bid)

def calculate_order_quantity(self, symbol: str, buying_power: float, float_option: bool = False):
230    def calculate_order_quantity(self, symbol: str, buying_power: float, float_option: bool = False):
231        """
232        returns the amount of shares you can buy with the given buying_power as int(), but if float_option is True,
233        it will return the amount as a float.
234
235        :param symbol: str
236        :param buying_power: float,
237        :param float_option: bool, default: False, if True returns the original number as float
238        :return: int or float
239        """
240        if self.load_symbol(symbol) is False:
241            return
242        quantity = (buying_power / self.last)
243
244        if float_option is True:
245            return quantity
246        return int(quantity)

returns the amount of shares you can buy with the given buying_power as int(), but if float_option is True, it will return the amount as a float.

Parameters
  • symbol: str
  • buying_power: float,
  • float_option: bool, default: False, if True returns the original number as float
Returns

int or float

def locate_stock( self, symbol: str, share_amount: int, max_price: float = 0, debug_info: bool = False):
248    def locate_stock(self, symbol: str, share_amount: int, max_price: float = 0, debug_info: bool = False):
249        """
250        Locate a stock, requires: stock symbol, and share_amount. optional: max_price.
251        if the locate_price is less than max_price: it will accept, else: decline.
252
253        :param symbol: str, symbol to locate.
254        :param share_amount: int, must be a multiple of 100 (100, 200, 300...)
255        :param max_price: float, default: 0, total price you are willing to pay for locates
256        :param debug_info: bool, if True it will print info about the locates in the console
257        :return: named tuple with the following attributes: 'price_per_share' and 'total'
258        :raises Exception: if share_amount is not divisible by 100
259        """
260        Data = namedtuple('Data', ['price_per_share', 'total'])
261
262        if share_amount is not None and share_amount % 100 != 0:
263            raise Exception(f'ERROR: share_amount is not divisible by 100 ({share_amount=})')
264
265        if not self.load_symbol(symbol):
266            return
267
268        if self.last <= 1.00:
269            print(f'Error: Cannot locate stocks priced under $1.00 ({symbol=}, price={self.last})')
270
271        self.driver.find_element(By.ID, "locate-tab-1").click()
272        input_symbol = self.driver.find_element(By.ID, "short-list-input-symbol")
273        input_symbol.clear()
274        input_symbol.send_keys(symbol, Keys.RETURN)
275
276        input_shares = self.driver.find_element(By.ID, "short-list-input-shares")
277        input_shares.clear()
278        input_shares.send_keys(share_amount)
279
280        while self.driver.find_element(By.ID, "short-list-locate-status").text == '':
281            time.sleep(0.1)
282
283        if self.driver.find_element(By.ID, "short-list-locate-status").text == 'Easy to borrow':
284            locate_pps = 0.00
285            locate_total = 0.00
286            if debug_info:
287                print(colored(f'Stock ({symbol}) is "Easy to borrow"', 'green'))
288            return Data(locate_pps, locate_total)
289
290        self.driver.find_element(By.ID, "short-list-button-locate").click()
291
292        for i in range(300):
293            try:
294                locate_pps = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-2").text)
295                locate_total = float(self.driver.find_element(By.ID, f"oitem-l-{symbol.upper()}-cell-6").text)
296                break
297
298            except (ValueError, NoSuchElementException, StaleElementReferenceException):
299                time.sleep(0.15)
300                if i == 15 or i == 299:
301                    insufficient_bp = 'Insufficient BP to short a position with requested quantity.'
302                    last_notif = self.Notification.get_last_notification_message()
303                    if insufficient_bp in last_notif:
304                        warnings.warn(f"ERROR! {insufficient_bp}")
305                        return
306        else:
307            raise Exception(f'Error: not able to locate symbol element ({symbol=})')
308
309        if locate_total <= max_price:
310            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[1]').click()
311            if debug_info:
312                print(colored(f'HTB Locate accepted ({symbol}, $ {locate_total})', 'cyan'))
313        else:
314            self.driver.find_element(By.XPATH, f'//*[@id="oitem-l-{symbol.upper()}-cell-8"]/span[2]').click()
315
316        return Data(locate_pps, locate_total)
317        # TODO create a function to get the pps and another one that just locates the shares

Locate a stock, requires: stock symbol, and share_amount. optional: max_price. if the locate_price is less than max_price: it will accept, else: decline.

Parameters
  • symbol: str, symbol to locate.
  • share_amount: int, must be a multiple of 100 (100, 200, 300...)
  • max_price: float, default: 0, total price you are willing to pay for locates
  • debug_info: bool, if True it will print info about the locates in the console
Returns

named tuple with the following attributes: 'price_per_share' and 'total'

Raises
  • Exception: if share_amount is not divisible by 100
def credit_locates(self, symbol: str, quantity=None):
319    def credit_locates(self, symbol: str, quantity=None):
320        """
321        sell/ credit stock locates, if no value is given in 'quantity', it will credit all the shares
322        available of the given symbol.
323
324        :param symbol: str
325        :param quantity: amount of shares to sell, must be a multiple of 100, ie: 100, 200, 300
326        :return:
327        :raises Exception: if given symbol in not already located
328        :raises ValueError: if quantity is not divisible by 100 or quantity > located shares
329        """
330        located_symbols = self.driver.find_elements(By.XPATH, '//*[@id="locate-inventory-table"]/tbody/tr/td[1]')
331        located_symbols = [x.text for x in located_symbols]
332
333        if symbol.upper() not in located_symbols:
334            raise Exception(f"ERROR! cannot find {symbol} in located symbols")
335
336        if quantity is not None:
337            if quantity % 100 != 0:
338                raise ValueError(f"ERROR! quantity is not divisible by 100 ({quantity=})")
339
340            located_shares = float(self.driver.find_element(By.ID, f"inv-{symbol.upper()}-cell-1").text)
341            if quantity > located_shares:
342                raise ValueError(f"ERROR! you cannot credit more shares than u already have "
343                                 f"({quantity} vs {located_shares}")
344
345            input_quantity = self.driver.find_element(By.ID, f"inv-{symbol.upper()}-sell-qty")
346            input_quantity.clear()
347            input_quantity.send_keys(quantity)
348
349        self.driver.find_element(By.XPATH, f'//*[@id="inv-{symbol.upper()}-sell"]/button').click()
350        return

sell/ credit stock locates, if no value is given in 'quantity', it will credit all the shares available of the given symbol.

Parameters
  • symbol: str
  • quantity: amount of shares to sell, must be a multiple of 100, ie: 100, 200, 300
Returns

Raises
  • Exception: if given symbol in not already located
  • ValueError: if quantity is not divisible by 100 or quantity > located shares
def limit_order(*args, **kwargs):
47    def wrapper(*args, **kwargs):
48        timer = Timer()
49        rv = func(*args, **kwargs)
50
51        if kwargs.get('log_time_elapsed') or kwargs.get('log_info'):
52            print(f'Time elapsed: {timer.time_elapsed:.2f} seconds')
53
54        return rv

Place a Limit Order, the following params are required: order_direction, symbol, share_amount, and limit_price.

Parameters
  • order_direction: str: 'buy', 'sell', 'short', 'cover'
  • symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
  • limit_price: float
  • share_amount: int
  • time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
  • log_info: bool, if True it will print information about the order
Returns

True if operation succeeded

Raises
  • AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
def market_order(*args, **kwargs):
47    def wrapper(*args, **kwargs):
48        timer = Timer()
49        rv = func(*args, **kwargs)
50
51        if kwargs.get('log_time_elapsed') or kwargs.get('log_info'):
52            print(f'Time elapsed: {timer.time_elapsed:.2f} seconds')
53
54        return rv

Place a Market Order, The following params are required: order_direction, symbol, and share_amount

Parameters
  • order_direction: str: 'buy', 'sell', 'short', 'cover'
  • symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
  • share_amount: int
  • time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
  • log_info: bool, if True it will print information about the order
Returns

Raises
  • Exception: if time not during market hours (9:30 - 16: 00)
  • AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'
def stop_market_order(*args, **kwargs):
47    def wrapper(*args, **kwargs):
48        timer = Timer()
49        rv = func(*args, **kwargs)
50
51        if kwargs.get('log_time_elapsed') or kwargs.get('log_info'):
52            print(f'Time elapsed: {timer.time_elapsed:.2f} seconds')
53
54        return rv

Place a Stop Market Order, the following params are required: order_direction, symbol, share_amount, and stop_price. note that a Stop Market Order can only be placed during market-hours (09:30:00 - 16:00:00), therefore if a Stop Market Order is placed outside market hours it will raise an error.

Parameters
  • order_direction: str: 'buy', 'sell', 'short', 'cover'
  • symbol: str: e.g: 'aapl', 'amd', 'NVDA', 'GM'
  • stop_price: float
  • share_amount: int
  • time_in_force: str, default: 'DAY', must be one of the following: 'DAY', 'GTC', or 'GTX'
  • log_info: bool, if True it will print information about the order
Returns

True if operation succeeded

Raises
  • Exception: if time not during market hours (9:30 - 16: 00)
  • AttributeError: if time_in_force argument not one of the following: 'DAY', 'GTC', 'GTX'