class PyRobot(): def __init__(self, client_id: str, redirect_uri: str, paper_trading: bool = True, credentials_path: Optional[str] = None, trading_account: Optional[str] = None) -> None: """Initalizes a new instance of the robot and logs into the API platform specified. Arguments: ---- client_id {str} -- The Consumer ID assigned to you during the App registration. This can be found at the app registration portal. redirect_uri {str} -- This is the redirect URL that you specified when you created your TD Ameritrade Application. Keyword Arguments: ---- credentials_path {str} -- The path to the session state file used to prevent a full OAuth workflow. (default: {None}) trading_account {str} -- Your TD Ameritrade account number. (default: {None}) """ # Set the attirbutes self.trading_account = trading_account self.client_id = client_id self.redirect_uri = redirect_uri self.credentials_path = credentials_path self.session: TDClient = self._create_session() self.trades = {} self.historical_prices = {} self.stock_frame: StockFrame = None self.paper_trading = paper_trading self._bar_size = None self._bar_type = None def _create_session(self) -> TDClient: """Start a new session. Creates a new session with the TD Ameritrade API and logs the user into the new session. Returns: ---- TDClient -- A TDClient object with an authenticated sessions. """ # Create a new instance of the client td_client = TDClient(client_id=self.client_id, redirect_uri=self.redirect_uri, credentials_path=self.credentials_path) # log the client into the new session td_client.login() return td_client @property def pre_market_open(self) -> bool: """Checks if pre-market is open. Uses the datetime module to create US Pre-Market Equity hours in UTC time. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> pre_market_open_flag = trading_robot.pre_market_open >>> pre_market_open_flag True Returns: ---- bool -- True if pre-market is open, False otherwise. """ pre_market_start_time = datetime.utcnow().replace( hour=8, minute=00, second=00).timestamp() market_start_time = datetime.utcnow().replace(hour=13, minute=30, second=00).timestamp() right_now = datetime.utcnow().timestamp() if market_start_time >= right_now >= pre_market_start_time: return True else: return False @property def post_market_open(self): """Checks if post-market is open. Uses the datetime module to create US Post-Market Equity hours in UTC time. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> post_market_open_flag = trading_robot.post_market_open >>> post_market_open_flag True Returns: ---- bool -- True if post-market is open, False otherwise. """ post_market_end_time = datetime.utcnow().replace( hour=00, minute=00, second=00).timestamp() market_end_time = datetime.utcnow().replace(hour=20, minute=00, second=00).timestamp() right_now = datetime.utcnow().timestamp() if post_market_end_time >= right_now >= market_end_time: return True else: return False @property def regular_market_open(self): """Checks if regular market is open. Uses the datetime module to create US Regular Market Equity hours in UTC time. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> market_open_flag = trading_robot.market_open >>> market_open_flag True Returns: ---- bool -- True if post-market is open, False otherwise. """ market_start_time = datetime.utcnow().replace(hour=13, minute=30, second=00).timestamp() market_end_time = datetime.utcnow().replace(hour=20, minute=00, second=00).timestamp() right_now = datetime.utcnow().timestamp() if market_end_time >= right_now >= market_start_time: return True else: return False def create_portfolio(self) -> Portfolio: """Create a new portfolio. Creates a Portfolio Object to help store and organize positions as they are added and removed during trading. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> portfolio = trading_robot.create_portfolio() >>> portfolio <pyrobot.portfolio.Portfolio object at 0x0392BF88> Returns: ---- Portfolio -- A pyrobot.Portfolio object with no positions. """ # Initalize the portfolio. self.portfolio = Portfolio(account_number=self.trading_account) # Assign the Client self.portfolio.td_client = self.session return self.portfolio def create_trade(self, trade_id: str, enter_or_exit: str, long_or_short: str, order_type: str = 'mkt', price: float = 0.0, stop_limit_price=0.0) -> Trade: """Initalizes a new instance of a Trade Object. This helps simplify the process of building an order by using pre-built templates that can be easily modified to incorporate more complex strategies. Arguments: ---- trade_id {str} -- The ID associated with the trade, this can then be used to access the trade during runtime. enter_or_exit {str} -- Defines whether this trade will be used to enter or exit a position. If used to enter, specify `enter`. If used to exit, speicfy `exit`. long_or_short {str} -- Defines whether this trade will be used to go long or short a position. If used to go long, specify `long`. If used to go short, speicfy `short`. Keyword Arguments: ---- order_type {str} -- Defines the type of order to initalize. Possible values are `'mkt', 'lmt', 'stop', 'stop-lmt', 'trailign-stop'` (default: {'mkt'}) price {float} -- The Price to be associate with the order. If the order type is `stop` or `stop-lmt` then it is the stop price, if it is a `lmt` order then it is the limit price, and `mkt` is the market price.(default: {0.0}) stop_limit_price {float} -- Only used if the order is a `stop-lmt` and represents the limit price of the `stop-lmt` order. (default: {0.0}) Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> new_trade = trading_robot_portfolio.create_trade( trade_id='long_1', enter_or_exit='enter', long_or_short='long', order_type='mkt' ) >>> new_trade >>> new_market_trade = trading_robot_portfolio.create_trade( trade_id='long_2', enter_or_exit='enter', long_or_short='long', order_type='mkt', price=12.00 ) >>> new_market_trade >>> new_stop_trade = trading_robot_portfolio.create_trade( trade_id='long_3', enter_or_exit='enter', long_or_short='long', order_type='stop', price=2.00 ) >>> new_stop_trade >>> new_stop_limit_trade = trading_robot_portfolio.create_trade( trade_id='long_4', enter_or_exit='enter', long_or_short='long', order_type='stop-lmt', price=2.00, stop_limit_price=1.90 ) >>> new_stop_limit_trade Returns: ---- Trade -- A pyrobot.Trade object with the specified template. """ # Initalize a new trade object. trade = Trade() # Create a new trade. trade.new_trade(trade_id=trade_id, order_type=order_type, side=long_or_short, enter_or_exit=enter_or_exit, price=price, stop_limit_price=stop_limit_price) self.trades[trade_id] = trade return trade def delete_trade(self, index: int) -> None: """Deletes an exisiting trade from the `trades` collection. Arguments: ---- index {int} -- The index of the order. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> new_trade = trading_robot_portfolio.create_trade( enter_or_exit='enter', long_or_short='long', order_type='mkt' ) >>> trading_robot.delete_trade(index=1) """ if index in self.trades: del self.trades[index] def grab_current_quotes(self) -> dict: """Grabs the current quotes for all positions in the portfolio. Makes a call to the TD Ameritrade Get Quotes endpoint with all the positions in the portfolio. If only one position exist it will return a single dicitionary, otherwise a nested dictionary. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> trading_robot_portfolio.add_position( symbol='MSFT', asset_type='equity' ) >>> current_quote = trading_robot.grab_current_quotes() >>> current_quote { "MSFT": { "assetType": "EQUITY", "assetMainType": "EQUITY", "cusip": "594918104", ... "regularMarketPercentChangeInDouble": 0, "delayed": true } } >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> trading_robot_portfolio.add_position( symbol='MSFT', asset_type='equity' ) >>> trading_robot_portfolio.add_position( symbol='AAPL', asset_type='equity' ) >>> current_quote = trading_robot.grab_current_quotes() >>> current_quote { "MSFT": { "assetType": "EQUITY", "assetMainType": "EQUITY", "cusip": "594918104", ... "regularMarketPercentChangeInDouble": 0, "delayed": False }, "AAPL": { "assetType": "EQUITY", "assetMainType": "EQUITY", "cusip": "037833100", ... "regularMarketPercentChangeInDouble": 0, "delayed": False } } Returns: ---- dict -- A dictionary containing all the quotes for each position. """ # First grab all the symbols. symbols = self.portfolio.positions.keys() # Grab the quotes. quotes = self.session.get_quotes(instruments=list(symbols)) return quotes def grab_historical_prices( self, start: datetime, end: datetime, bar_size: int = 1, bar_type: str = 'minute', symbols: Optional[List[str]] = None) -> List[dict]: """Grabs the historical prices for all the postions in a portfolio. Overview: ---- Any of the historical price data returned will include extended hours price data by default. Arguments: ---- start {datetime} -- Defines the start date for the historical prices. end {datetime} -- Defines the end date for the historical prices. Keyword Arguments: ---- bar_size {int} -- Defines the size of each bar. (default: {1}) bar_type {str} -- Defines the bar type, can be one of the following: `['minute', 'week', 'month', 'year']` (default: {'minute'}) symbols {List[str]} -- A list of ticker symbols to pull. (default: None) Returns: ---- {List[Dict]} -- The historical price candles. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> start_date = datetime.today() >>> end_date = start_date - timedelta(days=30) >>> historical_prices = trading_robot.grab_historical_prices( start=end_date, end=start_date, bar_size=1, bar_type='minute' ) """ self._bar_size = bar_size self._bar_type = bar_type start = str(milliseconds_since_epoch(dt_object=start)) end = str(milliseconds_since_epoch(dt_object=end)) new_prices = [] if not symbols: symbols = self.portfolio.positions for symbol in symbols: historical_prices_response = self.session.get_price_history( symbol=symbol, period_type='day', start_date=start, end_date=end, frequency_type=bar_type, frequency=bar_size, extended_hours=True) self.historical_prices[symbol] = {} self.historical_prices[symbol][ 'candles'] = historical_prices_response['candles'] for candle in historical_prices_response['candles']: new_price_mini_dict = {} new_price_mini_dict['symbol'] = symbol new_price_mini_dict['open'] = candle['open'] new_price_mini_dict['close'] = candle['close'] new_price_mini_dict['high'] = candle['high'] new_price_mini_dict['low'] = candle['low'] new_price_mini_dict['volume'] = candle['volume'] new_price_mini_dict['datetime'] = candle['datetime'] new_prices.append(new_price_mini_dict) self.historical_prices['aggregated'] = new_prices return self.historical_prices def get_latest_bar(self) -> List[dict]: """Returns the latest bar for each symbol in the portfolio. Returns: --- {List[dict]} -- A simplified quote list. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> latest_bars = trading_robot.get_latest_bar() >>> latest_bars """ # Grab the info from the last quest. bar_size = self._bar_size bar_type = self._bar_type # Define the start and end date. start_date = datetime.today() end_date = start_date - timedelta(minutes=bar_size * 15) start = str(milliseconds_since_epoch(dt_object=start_date)) end = str(milliseconds_since_epoch(dt_object=end_date)) latest_prices = [] # Loop through each symbol. for symbol in self.portfolio.positions: # Grab the request. historical_prices_response = self.session.get_price_history( symbol=symbol, period_type='day', start_date=start, end_date=end, frequency_type=bar_type, frequency=bar_size, extended_hours=True) if 'error' in historical_prices_response: time_true.sleep(2) # Grab the request. historical_prices_response = self.session.get_price_history( symbol=symbol, period_type='day', start_date=start, end_date=end, frequency_type=bar_type, frequency=bar_size, extended_hours=True) # parse the candles. for candle in historical_prices_response['candles'][-1:]: new_price_mini_dict = {} new_price_mini_dict['symbol'] = symbol new_price_mini_dict['open'] = candle['open'] new_price_mini_dict['close'] = candle['close'] new_price_mini_dict['high'] = candle['high'] new_price_mini_dict['low'] = candle['low'] new_price_mini_dict['volume'] = candle['volume'] new_price_mini_dict['datetime'] = candle['datetime'] latest_prices.append(new_price_mini_dict) return latest_prices def wait_till_next_bar(self, last_bar_timestamp: pd.DatetimeIndex) -> None: """Waits the number of seconds till the next bar is released. Arguments: ---- last_bar_timestamp {pd.DatetimeIndex} -- The last bar's timestamp. """ last_bar_time = last_bar_timestamp.to_pydatetime()[0].replace( tzinfo=timezone.utc) next_bar_time = last_bar_time + timedelta(seconds=60) curr_bar_time = datetime.now(tz=timezone.utc) last_bar_timestamp = int(last_bar_time.timestamp()) next_bar_timestamp = int(next_bar_time.timestamp()) curr_bar_timestamp = int(curr_bar_time.timestamp()) _time_to_wait_bar = next_bar_timestamp - last_bar_timestamp time_to_wait_now = next_bar_timestamp - curr_bar_timestamp if time_to_wait_now < 0: time_to_wait_now = 0 print("=" * 80) print("Pausing for the next bar") print("-" * 80) print("Curr Time: {time_curr}".format( time_curr=curr_bar_time.strftime("%Y-%m-%d %H:%M:%S"))) print("Next Time: {time_next}".format( time_next=next_bar_time.strftime("%Y-%m-%d %H:%M:%S"))) print("Sleep Time: {seconds}".format(seconds=time_to_wait_now)) print("-" * 80) print('') time_true.sleep(time_to_wait_now) def create_stock_frame(self, data: List[dict]) -> StockFrame: """Generates a new StockFrame Object. Arguments: ---- data {List[dict]} -- The data to add to the StockFrame object. Returns: ---- StockFrame -- A multi-index pandas data frame built for trading. """ # Create the Frame. self.stock_frame = StockFrame(data=data) return self.stock_frame def execute_signals(self, signals: List[pd.Series], trades_to_execute: dict) -> List[dict]: """Executes the specified trades for each signal. Arguments: ---- signals {list} -- A pandas.Series object representing the buy signals and sell signals. Will check if series is empty before making any trades. Trades: ---- trades_to_execute {dict} -- the trades you want to execute if signals are found. Returns: ---- {List[dict]} -- Returns all order responses. Usage: ---- >>> trades_dict = { 'MSFT': { 'trade_func': trading_robot.trades['long_msft'], 'trade_id': trading_robot.trades['long_msft'].trade_id } } >>> signals = indicator_client.check_signals() >>> trading_robot.execute_signals( signals=signals, trades_to_execute=trades_dict ) """ buys: pd.Series = signals[0][1] sells: pd.Series = signals[1][1] order_responses = [] # If we have buys or sells continue. if not buys.empty: # Grab the buy Symbols. symbols_list = buys.index.get_level_values(0).to_list() # Loop through each symbol. for symbol in symbols_list: # Check to see if there is a Trade object. if symbol in trades_to_execute: if self.portfolio.in_portfolio(symbol=symbol): self.portfolio.set_ownership_status(symbol=symbol, ownership=True) # Set the Execution Flag. trades_to_execute[symbol]['has_executed'] = True trade_obj: Trade = trades_to_execute[symbol]['trade_func'] if not self.paper_trading: # Execute the order. order_response = self.execute_orders( trade_obj=trade_obj) order_response = { 'order_id': order_response['order_id'], 'request_body': order_response['request_body'], 'timestamp': datetime.now().isoformat() } order_responses.append(order_response) else: order_response = { 'order_id': trade_obj._generate_order_id(), 'request_body': trade_obj.order, 'timestamp': datetime.now().isoformat() } order_responses.append(order_response) elif not sells.empty: # Grab the buy Symbols. symbols_list = sells.index.get_level_values(0).to_list() # Loop through each symbol. for symbol in symbols_list: # Check to see if there is a Trade object. if symbol in trades_to_execute: # Set the Execution Flag. trades_to_execute[symbol]['has_executed'] = True if self.portfolio.in_portfolio(symbol=symbol): self.portfolio.set_ownership_status(symbol=symbol, ownership=False) trade_obj: Trade = trades_to_execute[symbol]['trade_func'] if not self.paper_trading: # Execute the order. order_response = self.execute_orders( trade_obj=trade_obj) order_response = { 'order_id': order_response['order_id'], 'request_body': order_response['request_body'], 'timestamp': datetime.now().isoformat() } order_responses.append(order_response) else: order_response = { 'order_id': trade_obj._generate_order_id(), 'request_body': trade_obj.order, 'timestamp': datetime.now().isoformat() } order_responses.append(order_response) # Save the response. self.save_orders(order_response_dict=order_responses) return order_responses def execute_orders(self, trade_obj: Trade) -> dict: """Executes a Trade Object. Overview: ---- The `execute_orders` method will execute trades as they're signaled. When executed, the `Trade` object will have the order response saved to it, and the order response will be saved to a JSON file for further analysis. Arguments: ---- trade_obj {Trade} -- A trade object with the `order` property filled out. Returns: ---- {dict} -- An order response dicitonary. """ # Execute the order. order_dict = self.session.place_order(account=self.trading_account, order=trade_obj.order) return order_dict def save_orders(self, order_response_dict: dict) -> bool: """Saves the order to a JSON file for further review. Arguments: ---- order_response {dict} -- A single order response. Returns: ---- {bool} -- `True` if the orders were successfully saved. """ # Define the folder. folder: pathlib.PurePath = pathlib.Path(__file__).parents[1].joinpath( "data") # See if it exist, if not create it. if not folder.exists(): folder.mkdir() # Define the file path. file_path = folder.joinpath('orders.json') # First check if the file alread exists. if file_path.exists(): with open('data/orders.json', 'r') as order_json: orders_list = json.load(order_json) else: orders_list = [] # Combine both lists. orders_list = orders_list + order_response_dict # Write the new data back. with open(file='data/orders.json', mode='w+') as order_json: json.dump(obj=orders_list, fp=order_json, indent=4) return True def get_accounts(self, account_number: str = None, all_accounts: bool = False) -> dict: """Returns all the account balances for a specified account. Keyword Arguments: ---- account_number {str} -- The account number you want to query. (default: {None}) all_accounts {bool} -- Specifies whether you want to grab all accounts `True` or not `False`. (default: {False}) Returns: ---- Dict -- A dictionary containing all the information in your account. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> trading_robot_accounts = trading_robot.session.get_accounts( account_number="<YOUR ACCOUNT NUMBER>" ) >>> trading_robot_accounts [ { 'account_number': 'ACCOUNT_ID', 'account_type': 'CASH', 'available_funds': 0.0, 'buying_power': 0.0, 'cash_available_for_trading': 0.0, 'cash_available_for_withdrawl': 0.0, 'cash_balance': 0.0, 'day_trading_buying_power': 0.0, 'long_market_value': 0.0, 'maintenance_call': 0.0, 'maintenance_requirement': 0.0, 'short_balance': 0.0, 'short_margin_value': 0.0, 'short_market_value': 0.0 } ] """ # Depending on how the client was initalized, either use the state account # or the one passed through the function. if all_accounts: account = 'all' elif self.trading_account: account = self.trading_account else: account = account_number # Grab the accounts. accounts = self.session.get_accounts(account=account) # Parse the account info. accounts_parsed = self._parse_account_balances( accounts_response=accounts) return accounts_parsed def _parse_account_balances( self, accounts_response: Union[Dict, List]) -> List[Dict]: """Parses an Account response into a more simplified dictionary. Arguments: ---- accounts_response {Union[Dict, List]} -- A response from the `get_accounts` call. Returns: ---- List[Dict] -- A list of simplified account dictionaries. """ account_lists = [] if isinstance(accounts_response, dict): account_dict = {} for account_type_key in accounts_response: account_info = accounts_response[account_type_key] account_id = account_info['accountId'] account_type = account_info['type'] account_current_balances = account_info['currentBalances'] # account_inital_balances = account_info['initialBalances'] account_dict['account_number'] = account_id account_dict['account_type'] = account_type account_dict['cash_balance'] = account_current_balances[ 'cashBalance'] account_dict['long_market_value'] = account_current_balances[ 'longMarketValue'] account_dict[ 'cash_available_for_trading'] = account_current_balances.get( 'cashAvailableForTrading', 0.0) account_dict[ 'cash_available_for_withdrawl'] = account_current_balances.get( 'cashAvailableForWithDrawal', 0.0) account_dict['available_funds'] = account_current_balances.get( 'availableFunds', 0.0) account_dict['buying_power'] = account_current_balances.get( 'buyingPower', 0.0) account_dict[ 'day_trading_buying_power'] = account_current_balances.get( 'dayTradingBuyingPower', 0.0) account_dict[ 'maintenance_call'] = account_current_balances.get( 'maintenanceCall', 0.0) account_dict[ 'maintenance_requirement'] = account_current_balances.get( 'maintenanceRequirement', 0.0) account_dict['short_balance'] = account_current_balances.get( 'shortBalance', 0.0) account_dict[ 'short_market_value'] = account_current_balances.get( 'shortMarketValue', 0.0) account_dict[ 'short_margin_value'] = account_current_balances.get( 'shortMarginValue', 0.0) account_lists.append(account_dict) elif isinstance(accounts_response, list): for account in accounts_response: account_dict = {} for account_type_key in account: account_info = account[account_type_key] account_id = account_info['accountId'] account_type = account_info['type'] account_current_balances = account_info['currentBalances'] # account_inital_balances = account_info['initialBalances'] account_dict['account_number'] = account_id account_dict['account_type'] = account_type account_dict['cash_balance'] = account_current_balances[ 'cashBalance'] account_dict[ 'long_market_value'] = account_current_balances[ 'longMarketValue'] account_dict[ 'cash_available_for_trading'] = account_current_balances.get( 'cashAvailableForTrading', 0.0) account_dict[ 'cash_available_for_withdrawl'] = account_current_balances.get( 'cashAvailableForWithDrawal', 0.0) account_dict[ 'available_funds'] = account_current_balances.get( 'availableFunds', 0.0) account_dict[ 'buying_power'] = account_current_balances.get( 'buyingPower', 0.0) account_dict[ 'day_trading_buying_power'] = account_current_balances.get( 'dayTradingBuyingPower', 0.0) account_dict[ 'maintenance_call'] = account_current_balances.get( 'maintenanceCall', 0.0) account_dict[ 'maintenance_requirement'] = account_current_balances.get( 'maintenanceRequirement', 0.0) account_dict[ 'short_balance'] = account_current_balances.get( 'shortBalance', 0.0) account_dict[ 'short_market_value'] = account_current_balances.get( 'shortMarketValue', 0.0) account_dict[ 'short_margin_value'] = account_current_balances.get( 'shortMarginValue', 0.0) account_lists.append(account_dict) return account_lists def get_positions(self, account_number: str = None, all_accounts: bool = False) -> List[Dict]: """Gets all the positions for a specified account number. Arguments: ---- account_number (str, optional): The account number of the account you want to pull positions for. Defaults to None. all_accounts (bool, optional): If you want to return all the positions for every account then set to `True`. Defaults to False. Returns: ---- List[Dict]: A list of Position objects. Usage: ---- >>> trading_robot = PyRobot( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) >>> trading_robot_positions = trading_robot.session.get_positions( account_number="<YOUR ACCOUNT NUMBER>" ) >>> trading_robot_positions [ { 'account_number': '111111111', 'asset_type': 'EQUITY', 'average_price': 0.00, 'current_day_profit_loss': -0.96, 'current_day_profit_loss_percentage': -5.64, 'cusip': '565849106', 'description': '', 'long_quantity': 3.0, 'market_value': 16.05, 'settled_long_quantity': 3.0, 'settled_short_quantity': 0.0, 'short_quantity': 0.0, 'sub_asset_type': '', 'symbol': 'MRO', 'type': '' }, { 'account_number': '111111111', 'asset_type': 'EQUITY', 'average_price': 5.60667, 'current_day_profit_loss': -0.96, 'current_day_profit_loss_percentage': -5.64, 'cusip': '565849106', 'description': '', 'long_quantity': 3.0, 'market_value': 16.05, 'settled_long_quantity': 3.0, 'settled_short_quantity': 0.0, 'short_quantity': 0.0, 'sub_asset_type': '', 'symbol': 'MRO', 'type': '' } ] """ if all_accounts: account = 'all' elif self.trading_account and account_number is None: account = self.trading_account else: account = account_number # Grab the positions. positions = self.session.get_accounts(account=account, fields=['positions']) # Parse the positions. positions_parsed = self._parse_account_positions( positions_response=positions) return positions_parsed def _parse_account_positions( self, positions_response: Union[List, Dict]) -> List[Dict]: """Parses the response from the `get_positions` into a more simplified list. Arguments: ---- positions_response {Union[List, Dict]} -- Either a list or a dictionary that represents a position. Returns: ---- List[Dict] -- A more simplified list of positions. """ positions_lists = [] if isinstance(positions_response, dict): for account_type_key in positions_response: account_info = positions_response[account_type_key] account_id = account_info['accountId'] positions = account_info['positions'] for position in positions: position_dict = {} position_dict['account_number'] = account_id position_dict['average_price'] = position['averagePrice'] position_dict['market_value'] = position['marketValue'] position_dict[ 'current_day_profit_loss_percentage'] = position[ 'currentDayProfitLossPercentage'] position_dict['current_day_profit_loss'] = position[ 'currentDayProfitLoss'] position_dict['long_quantity'] = position['longQuantity'] position_dict['short_quantity'] = position['shortQuantity'] position_dict['settled_long_quantity'] = position[ 'settledLongQuantity'] position_dict['settled_short_quantity'] = position[ 'settledShortQuantity'] position_dict['symbol'] = position['instrument']['symbol'] position_dict['cusip'] = position['instrument']['cusip'] position_dict['asset_type'] = position['instrument'][ 'assetType'] position_dict['sub_asset_type'] = position[ 'instrument'].get('subAssetType', "") position_dict['description'] = position['instrument'].get( 'description', "") position_dict['type'] = position['instrument'].get( 'type', "") positions_lists.append(position_dict) elif isinstance(positions_response, list): for account in positions_response: for account_type_key in account: account_info = account[account_type_key] account_id = account_info['accountId'] positions = account_info['positions'] for position in positions: position_dict = {} position_dict['account_number'] = account_id position_dict['average_price'] = position[ 'averagePrice'] position_dict['market_value'] = position['marketValue'] position_dict[ 'current_day_profit_loss_percentage'] = position[ 'currentDayProfitLossPercentage'] position_dict['current_day_profit_loss'] = position[ 'currentDayProfitLoss'] position_dict['long_quantity'] = position[ 'longQuantity'] position_dict['short_quantity'] = position[ 'shortQuantity'] position_dict['settled_long_quantity'] = position[ 'settledLongQuantity'] position_dict['settled_short_quantity'] = position[ 'settledShortQuantity'] position_dict['symbol'] = position['instrument'][ 'symbol'] position_dict['cusip'] = position['instrument'][ 'cusip'] position_dict['asset_type'] = position['instrument'][ 'assetType'] position_dict['sub_asset_type'] = position[ 'instrument'].get('subAssetType', "") position_dict['description'] = position[ 'instrument'].get('description', "") position_dict['type'] = position['instrument'].get( 'type', "") positions_lists.append(position_dict) return positions_lists
class PyRobotPortfolioTest(TestCase): """Will perform a unit test for the Portfolio object.""" def setUp(self) -> None: """Set up the Portfolio.""" self.portfolio = Portfolio() self.maxDiff = None # Grab configuration values. config = ConfigParser() config.read('configs/config.ini') CLIENT_ID = config.get('main', 'CLIENT_ID') REDIRECT_URI = config.get('main', 'REDIRECT_URI') CREDENTIALS_PATH = config.get('main', 'JSON_PATH') self.ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') self.td_client = TDClient( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, credentials_path=CREDENTIALS_PATH ) self.td_client.login() def test_create_portofolio(self): """Make sure it's a Portfolio.""" self.assertIsInstance(self.portfolio, Portfolio) def test_td_client_property(self): """Test the TD Client property.""" # Should be None if wasn't initalized from the PyRobot. self.assertIsNone(self.portfolio.td_client) def test_stock_frame_property(self): """Test the Stock Frame property.""" # Should be None if wasn't initalized from the PyRobot. self.assertIsNone(self.portfolio.stock_frame) def test_historical_prices_property(self): """Test the Historical Prices property.""" # Should be False if wasn't initalized from the PyRobot. self.assertFalse(self.portfolio.historical_prices) def test_add_position(self): """Test adding a single position to the portfolio.""" new_position = self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) correct_position = { 'symbol': 'MSFT', 'asset_type': 'equity', 'ownership_status': True, 'quantity': 10, 'purchase_price': 3.00, 'purchase_date': '2020-01-31' } self.assertDictEqual(new_position, correct_position) def test_add_position_default_arguments(self): """Test adding a single position to the portfolio, no date.""" new_position = self.portfolio.add_position( symbol='MSFT', asset_type='equity' ) correct_position = { 'symbol': 'MSFT', 'asset_type': 'equity', 'ownership_status': False, 'quantity': 0, 'purchase_price': 0.00, 'purchase_date': None } self.assertDictEqual(new_position, correct_position) def test_delete_existing_position(self): """Test deleting an exisiting position.""" self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) delete_status = self.portfolio.remove_position(symbol='MSFT') correct_status = (True, 'MSFT was successfully removed.') self.assertTupleEqual(delete_status, correct_status) def test_delete_non_existing_position(self): """Test deleting a non-exisiting position.""" delete_status = self.portfolio.remove_position(symbol='AAPL') correct_status = (False, 'AAPL did not exist in the porfolio.') self.assertTupleEqual(delete_status, correct_status) def test_in_portfolio_exisitng(self): """Checks to see if an exisiting position exists.""" self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) in_portfolio_flag = self.portfolio.in_portfolio(symbol='MSFT') self.assertTrue(in_portfolio_flag) def test_in_portfolio_non_exisitng(self): """Checks to see if a non exisiting position exists.""" in_portfolio_flag = self.portfolio.in_portfolio(symbol='AAPL') self.assertFalse(in_portfolio_flag) def test_is_profitable(self): """Checks to see if a position is profitable.""" # Add a position. self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) # Test for being Profitable. is_profitable = self.portfolio.is_profitable( symbol='MSFT', current_price=5.00 ) # Test for not being profitable. is_not_profitable = self.portfolio.is_profitable( symbol='MSFT', current_price=1.00 ) self.assertTrue(is_profitable) self.assertFalse(is_not_profitable) def test_projected_market_value(self): """Tests the generation of a market value summary, for all of the positions.""" # Add a position. self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) correct_dict = { 'MSFT': { 'current_price': 5.0, 'is_profitable': True, 'purchase_price': 3.0, 'quantity': 10, 'total_invested_capital': 30.0, 'total_loss_or_gain_$': 20.0, 'total_loss_or_gain_%': 0.6667, 'total_market_value': 50.0 }, 'total': { 'number_of_breakeven_positions': 0, 'number_of_non_profitable_positions': 0, 'number_of_profitable_positions': 1, 'total_invested_capital': 30.0, 'total_market_value': 50.0, 'total_positions': 1, 'total_profit_or_loss': 20.0 } } portfolio_summary = self.portfolio.projected_market_value(current_prices={'MSFT':{'lastPrice':5.0}}) self.assertDictEqual(correct_dict, portfolio_summary) def test_grab_historical_prices(self): pass def test_portfolio_summary(self): """Tests the generation of a portfolio summary, for all of the positions.""" # Add a position. self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) self.portfolio.td_client = self.td_client correct_dict = [ 'projected_market_value', 'portfolio_weights', 'portfolio_risk' ] correct_dict = set(correct_dict) summary_dict = self.portfolio.portfolio_summary() self.assertTrue(correct_dict.issubset(summary_dict)) def test_ownership_status(self): """Tests getting and setting the ownership status.""" # Add a position. self.portfolio.add_position( symbol='MSFT', asset_type='equity', quantity=10, purchase_price=3.00, purchase_date='2020-01-31' ) # Should be True, since `purchase_date` was set. self.assertTrue(self.portfolio.get_ownership_status(symbol='MSFT')) # Reassign it. self.portfolio.set_ownership_status(symbol='MSFT', ownership=False) # Should be False. self.assertFalse(self.portfolio.get_ownership_status(symbol='MSFT')) def tearDown(self) -> None: """Teardown the Portfolio object.""" self.portfolio = None