class RobinhoodData: """ Wrapper to download orders and dividends from Robinhood accounts Downloads two dataframes and saves to datafile ---------- Parameters: datafile : location of h5 datafile """ def __init__(self, datafile): self.datafile = datafile def _login(self, username, password): self.client = Robinhood() # try import the module with passwords try: _temp = __import__('auth') self.client.login(_temp.local_user, _temp.local_password) except: self.client.login(username=username, password=password) return self def _get_symbol_from_instrument_url(self, url): return self._fetch_json_by_url(url)['symbol'] # private method for getting all orders def _fetch_json_by_url(self, url): return self.client.session.get(url).json() # deleting sensitive or redundant fields def _delete_sensitive_fields(self, df): for col in ['account', 'url', 'id', 'instrument']: if col in df: del df[col] return df # download orders and fields requiring RB client def _download_orders(self): print("Downloading orders from Robinhood") orders = [] past_orders = self.client.order_history() orders.extend(past_orders['results']) while past_orders['next']: next_url = past_orders['next'] past_orders = self._fetch_json_by_url(next_url) orders.extend(past_orders['results']) df = pd.DataFrame(orders) df['symbol'] = df['instrument'].apply( self._get_symbol_from_instrument_url) df.sort_values(by='created_at', inplace=True) df.reset_index(inplace=True, drop=True) df_ord = self._delete_sensitive_fields(df) return df_ord # download dividends and fields requiring RB client def _download_dividends(self): print("Downloading dividends from Robinhood") dividends = self.client.dividends() dividends = [x for x in dividends['results']] df = pd.DataFrame(dividends) if df.shape[0] > 0: df['symbol'] = df['instrument'].apply( self._get_symbol_from_instrument_url) df.sort_values(by='paid_at', inplace=True) df.reset_index(inplace=True, drop=True) df_div = self._delete_sensitive_fields(df) else: df_div = pd.DataFrame(columns=[ 'symbol', 'amount', 'position', 'rate', 'paid_at', 'payable_date' ]) return df_div # process orders def _process_orders(self, df_ord): # assign to df and reduce the number of fields df = df_ord.copy() fields = [ 'created_at', 'average_price', 'cumulative_quantity', 'fees', 'symbol', 'side' ] df = df[fields] # convert types for field in ['average_price', 'cumulative_quantity', 'fees']: df[field] = pd.to_numeric(df[field]) for field in ['created_at']: df[field] = pd.to_datetime(df[field]) # add days df['date'] = df['created_at'].apply(lambda x: normalize_date(x)) # rename columns for consistency df.rename(columns={'cumulative_quantity': 'current_size'}, inplace=True) # quantity accounting for side of transaction for cumsum later df['signed_size'] = np.where(df.side == 'buy', df['current_size'], -df['current_size']) df['signed_size'] = df['signed_size'].astype(np.int64) return df # process_orders def _process_dividends(self, df_div): df = df_div.copy() # convert types for field in ['amount', 'position', 'rate']: df[field] = pd.to_numeric(df[field]) for field in ['paid_at', 'payable_date']: df[field] = pd.to_datetime(df[field]) # add days df['date'] = df['paid_at'].apply(lambda x: normalize_date(x)) return df def _generate_positions(self, df_ord): """ Process orders dataframe and generate open and closed positions. For all open positions close those which were later sold, so that the cost_basis for open can be calculated correctly. For closed positions calculate the cost_basis based on the closed open positions. Note: the olders open positions are first to be closed. The logic here is to reduce the tax exposure. ----- Parameters: - Pre-processed df_ord Return: - Two dataframes with open and closed positions correspondingly """ # prepare dataframe for open and closed positions df_open = df_ord[df_ord.side == 'buy'].copy() df_closed = df_ord[df_ord.side == 'sell'].copy() # create a new column for today's position size # TODO: may be redundant - review later df_open['final_size'] = df_open['current_size'] df_closed['final_size'] = df_closed['current_size'] # main loop for i_closed, row_closed in df_closed.iterrows(): sell_size = row_closed.final_size sell_cost_basis = 0 for i_open, row_open in df_open[ (df_open.symbol == row_closed.symbol) & (df_open.date < row_closed.date)].iterrows(): new_sell_size = sell_size - df_open.loc[i_open, 'final_size'] new_sell_size = 0 if new_sell_size < 0 else new_sell_size new_open_size = df_open.loc[i_open, 'final_size'] - sell_size new_open_size = new_open_size if new_open_size > 0 else 0 # updating open positions df_open.loc[i_open, 'final_size'] = new_open_size # updating closed positions df_closed.loc[i_closed, 'final_size'] = new_sell_size sold_size = sell_size - new_sell_size sell_cost_basis +=\ df_open.loc[i_open, 'average_price'] * sold_size sell_size = new_sell_size # assign a cost_basis to the closed position df_closed.loc[i_closed, 'current_cost_basis'] = -sell_cost_basis # calculate cost_basis for open positions df_open['current_cost_basis'] =\ df_open['current_size'] * df_open['average_price'] df_open['final_cost_basis'] =\ df_open['final_size'] * df_open['average_price'] # calculate capital gains for closed positions df_closed['realized_gains'] =\ df_closed['current_size'] * df_closed['average_price'] +\ df_closed['current_cost_basis'] df_closed['final_cost_basis'] = 0 return df_open, df_closed def download_robinhood_data(self, user, password): self._login(user, password) df_div = self._process_dividends(self._download_dividends()) df_div.to_hdf(self.datafile, 'dividends') df_ord = self._process_orders(self._download_orders()) df_ord.to_hdf(self.datafile, 'orders') df_open, df_closed = self._generate_positions(df_ord) df_open.to_hdf(self.datafile, 'open') df_closed.to_hdf(self.datafile, 'closed') return df_div, df_ord, df_open, df_closed
class Query: # __init__:Void # param email:String => Email of the Robinhood user. # param password:String => Password for the Robinhood user. def __init__(self, email, password): self.trader = Robinhood() self.trader.login(username=email, password=password) self.email = email self.password = password ## ## # Getters # ## ## # get_fundamentals_by_criteria:[String] # param price_range:(float, float) => High and low prices for the queried fundamentals. # returns List of symbols that fit the given criteria. def get_fundamentals_by_criteria(self, price_range=(0.00, sys.maxsize), tags=None): all_symbols = [] if tags is not None and tags is not []: if isinstance(tags, Enum): try: all_symbols = self.get_by_tag(tag) except Exception as e: pass else: for tag in tags: try: all_symbols += self.get_by_tag(tag) except Exception as e: pass else: all_symbols = [ instrument['symbol'] for instrument in self.trader.instruments_all() ] queried_fundamentals = [] for symbol in all_symbols: try: fundamentals = self.get_fundamentals(symbol) if fundamentals is not None and 'low' in fundamentals and 'high' in fundamentals and float( fundamentals['low'] or -1) >= price_range[0] and float( fundamentals['high'] or sys.maxsize + 1) <= price_range[1]: fundamentals['symbol'] = symbol queried_fundamentals.append(fundamentals) except Exception as e: continue return queried_fundamentals # get_symbols_by_criteria:[String] # param price_range:(float, float) => High and low prices for the queried symbols. # returns List of symbols that fit the given criteria. def get_symbols_by_criteria(self, price_range=(0.00, sys.maxsize), tags=None): queried_fundamentals = self.get_fundamentals_by_criteria( price_range, tags) queried_symbols = [ fundamentals['symbol'] for fundamentals in queried_fundamentals ] return queried_symbols # get_current_price:[String:String] # param symbol:String => String symbol of the instrument to return. # returns Float value of the current price of the stock with the given symbol. def get_current_price(self, symbol): return float(self.trader.quote_data(symbol)['last_trade_price']) # get_quote:[String:String] # param symbol:String => String symbol of the instrument to return. # returns Quote data for the instrument with the given symbol. def get_quote(self, symbol): return self.trader.quote_data(symbol) # get_quotes:[[String:String]] # param symbol:[String] => List of string symbols of the instrument to return. # returns Quote data for the instruments with the given symbols. def get_quotes(self, symbols): return self.trader.quotes_data(symbols) # get_instrument:[String:String] # param symbol:String => String symbol of the instrument. # returns The instrument with the given symbol. def get_instrument(self, symbol): return self.trader.instruments(symbol)[0] or None # stock_from_instrument_url:Dict[String:String] # param url:String => URL of instrument. # returns Stock dictionary from the url of the instrument. def stock_from_instrument_url(self, url): return self.trader.stock_from_instrument_url(url) # get_history:[[String:String]] # param symbol:String => String symbol of the instrument. # param interval:Span => Time in between each value. (default: DAY) # param span:Span => Range for the data to be returned. (default: YEAR) # param bounds:Span => The bounds to be included. (default: REGULAR) # returns Historical quote data for the instruments with the given symbols on a 5-minute, weekly interval. def get_history(self, symbol, interval=Span.DAY, span=Span.YEAR, bounds=Bounds.REGULAR): return self.trader.get_historical_quotes(symbol, interval.value, span.value, bounds.value) # get_news:[[String:String]] # param symbol:String => String symbol of the instrument. # returns News for the instrument with the given symbol. def get_news(self, symbol): return self.trader.get_news(symbol) # get_fundamentals:Dict[String:String] # param symbol:String => String symbol of the instrument. # returns Fundamentals for the instrument with the given symbol. def get_fundamentals(self, symbol): return self.trader.get_fundamentals(symbol) # get_fundamentals:[String:String] # param symbol:String => String symbol of the instrument. # param dates:Date => List of datetime.date objects. # param type:Option => Option.CALL or Option.PUT # returns Options for the given symbol within the listed dates for the given type. def get_options(self, symbol, dates, type): return self.trader.get_options( symbol, list(map(lambda date: date.isoFormat(), dates)), type.value) # get_market_data:[String:String] # param optionId:String => Option ID for the option to return. # returns Options for the given ID. def get_market_data(self, optionId): return self.trader.get_option_market_data(optionId) # get_by_tag:[String:String] # param tag:Tag => Type of tag to return the quotes by. # returns Quotes for the given tag. def get_by_tag(self, tag): return self.trader.get_tickers_by_tag(tag.value) # get_current_bid_price:Float # param symbol:String => String symbol of the quote. # returns The current bid price of the stock, as a float. def get_current_bid_price(self, symbol): return float(self.trader.get_quote(symbol)['bid_price']) or 0.0 ## ## # User Methods # ## ## # user_portfolio:[String:String] # returns Portfolio model for the logged in user. def user_portfolio(self): quotes = [] user_portfolio = self.user_stock_portfolio() for data in user_portfolio: symbol = data['symbol'] count = float(data['quantity']) quotes.append(Quote(symbol, count)) return Portfolio(self, quotes, 'User Portfolio') # user_stock_portfolio:[String:String] # TODO: Better documentation. # returns Stock perfolio for the user. def user_stock_portfolio(self): positions = self.trader.positions()['results'] or [] return list( map( lambda position: Utility.merge_dicts( position, self.trader.session.get(position['instrument'], timeout=15) .json()), positions)) # user_portfolio:[String:String] # returns Positions for the logged in user. def user_positions(self): return self.trader.positions() # user_dividends:[String:String] # returns Dividends for the logged in user. def user_dividends(self): return self.trader.dividends() # user_securities:[String:String] # returns Securities for the logged in user. def user_securities(self): return self.trader.securities_owned() # user_equity:[String:String] # returns Equity for the logged in user. def user_equity(self): return self.trader.equity() # user_equity_prev:[String:String] # returns Equity upon the previous close for the logged in user. def user_equity_prev(self): return self.trader.equity_previous_close() # user_equity_adj_prev:[String:String] # returns Adjusted equity upon the previous close for the logged in user. def user_equity_adj_prev(self): return self.trader.adjusted_equity_previous_close() # user_equity_ext_hours:[String:String] # returns Extended hours equity for the logged in user. def user_equity_ext_hours(self): return self.trader.extended_hours_equity() # user_equity_last_core:[String:String] # returns Last core equity for the logged in user. def user_equity_last_core(self): return self.trader.last_core_equity() # user_excess_margin:[String:String] # returns Excess margin for the logged in user. def user_excess_margin(self): return self.trader.excess_margin() # user_market_value:[String:String] # returns Market value for the logged in user. def user_market_value(self): return self.trader.market_value() # user_market_value_ext_hours:[String:String] # returns Extended hours market value for the logged in user. def user_market_value_ext_hours(self): return self.trader.extended_hours_market_value() # user_market_value_last_core:[String:String] # returns Last core market value for the logged in user. def user_market_value_last_core(self): return self.trader.last_core_market_value() # user_order_history:[String:String] # param orderId:String => The order ID to return the order for. # returns A specified order executed by the logged in user. def user_order(self, orderId): return self.trader.order_history(orderId) # user_orders:[[String:String]] # returns The order history for the logged in user. def user_orders(self): return self.trader.order_history(None) # user_open_orders:[[String:String]] # returns The open orders for the user def user_open_orders(self): orders = self.trader.order_history(None)['results'] open_orders = [] for order in orders: if order['state'] == 'queued': open_orders.append(order) return open_orders # user_account:[[String:String]] # returns The user's account. def user_account(self): return self.trader.get_account() # user_buying_power:float # returns The user's buying power. def user_buying_power(self): return float(self.trader.get_account()['buying_power'] or 0.0) ## ## # Execution Methods # ## ## # exec_buy:[String:String] # param symbol:String => String symbol of the instrument. # param quantity:Number => Number of shares to execute buy for. # param stop:Number? => Sets a stop price on the buy, if not None. # param limit:Number? => Sets a limit price on the buy, if not None. # param time:GoodFor? => Defines the expiration for a limited buy. # returns The order response. def exec_buy(self, symbol, quantity, stop=None, limit=None, time=None): if time is None: time = GoodFor.GOOD_TIL_CANCELED if limit is not None: if stop is not None: return self.trader.place_stop_limit_buy_order( None, symbol, time.value, stop, quantity) return self.trader.place_limit_buy_order(None, symbol, time.value, limit, quantity) elif stop is not None: return self.trader.place_stop_loss_buy_order( None, symbol, time.value, stop, quantity) return self.trader.place_market_buy_order(None, symbol, time.value, quantity) # exec_sell:[String:String] # param symbol:String => String symbol of the instrument. # param quantity:Number => Number of shares to execute sell for. # param stop:Number? => Sets a stop price on the sell, if not None. # param limit:Number? => Sets a limit price on the sell, if not None. # param time:GoodFor? => Defines the expiration for a limited buy. # returns The order response. def exec_sell(self, symbol, quantity, stop=None, limit=None, time=None): if time is None: time = GoodFor.GOOD_TIL_CANCELED if limit is not None: if stop is not None: return self.trader.place_stop_limit_sell_order( None, symbol, time.value, stop, quantity) return self.trader.place_limit_sell_order(None, symbol, time.value, limit, quantity) elif stop is not None: return self.trader.place_stop_loss_sell_order( None, symbol, time.value, stop, quantity) return self.trader.place_market_sell_order(None, symbol, time.value, quantity) # exec_cancel:[String:String] # param order_id:String => ID of the order to cancel. # returns The canceled order response. def exec_cancel(self, order_id): return self.trader.cancel_order(order_id) # exec_cancel_open_orders:[String] # returns A list of string IDs for the cancelled orders. def exec_cancel_open_orders(self): orders = self.trader.order_history(None)['results'] cancelled_order_ids = [] for order in orders: if order['state'] == 'queued': self.trader.cancel_order(order['id']) cancelled_order_ids.append(order['id']) return cancelled_order_ids
def get_all_transfers(trader: Robinhood) -> List[Transfer]: all_transfers: List[Transfer] = [] # These are transfers that were initiated from within RH to an outside acc transfers = trader.get_transfers()["results"] for transfer in transfers: sign = 1 if transfer["direction"] == "deposit" else -1 all_transfers.append( Transfer( amount=float(transfer["amount"]) * sign, date=transfer["created_at"], transfer_type=Transfer.TransferType.internal_transfers)) # This is money from interest sweeps = trader.get_sweeps()["results"] for sweep in sweeps: assert sweep["amount"]["currency_code"] == "USD" amount = get_signed_amount( sweep["amount"]["amount"], key=sweep["direction"], pos="credit", neg="debit") all_transfers.append( Transfer( amount=amount, date=sweep["pay_date"], transfer_type=Transfer.TransferType.interest)) # These are transfers that were initiated from an outside account to RH received_transfers = trader.get_received_transfers()["results"] for transfer in received_transfers: assert transfer["amount"]["currency_code"] == "USD" amount = get_signed_amount( transfer["amount"]["amount"], key=transfer["direction"], pos="credit", neg="debit") all_transfers.append( Transfer( amount=amount, date=transfer["created_at"], transfer_type=Transfer.TransferType.received_transfers)) # These are transfers initiated by the RH Cash debit card settled_transactions = trader.get_settled_transactions()["results"] for transaction in settled_transactions: if transaction["source_type"] != "settled_card_transaction": continue assert transaction["amount"]["currency_code"] == "USD" amount = get_signed_amount(amount=transaction["amount"]["amount"], key=transaction["direction"], pos="credit", neg="debit") all_transfers.append( Transfer( amount=amount, date=transaction["initiated_at"], transfer_type=Transfer.TransferType.settled_transactions)) # Update internal stock purchases and sales order_history = trader.order_history()["results"] for order in order_history: if not order["state"] == "filled" or len(order["executions"]) == 0: logging.info("Ignoring order because it is not marked 'filled'." f" Order {order}") continue symbol = trader.get_url(order["instrument"])["symbol"] amount = 0 if order["dollar_based_amount"] is not None: logging.info("Using dollar based amount") amount = order["dollar_based_amount"]["amount"] else: logging.info("Evaluating amount from executions") for execution in order["executions"]: price = float(execution["price"]) quantity = float(execution["quantity"]) amount += price * quantity amount = get_signed_amount( amount, key=order["side"], pos="sell", neg="buy") memo = f"Robinhood {symbol} {'Purchased' if amount < 0 else 'Sold'}" all_transfers.append( Transfer( amount=amount, date=order["last_transaction_at"], transfer_type=Transfer.TransferType.stock_purchase, memo=memo)) # Update Crypto purchases cryptos = trader.crypto_orders()["results"] for crypto in cryptos: amount = get_signed_amount( amount=crypto["rounded_executed_notional"], key=crypto["side"], pos="sell", neg="buy") memo = f"Robinhood Crypto {'Purchased' if amount < 0 else 'Sold'}" all_transfers.append( Transfer( amount=amount, date=crypto["last_transaction_at"], transfer_type=Transfer.TransferType.stock_purchase, memo=memo)) # Update dividend payouts dividends = trader.dividends()["results"] for dividend in dividends: # Make sure the divident actually went through (could be voided/pending) if dividend["state"] != "paid": continue symbol = trader.get_url(dividend["instrument"])["symbol"] all_transfers.append( Transfer( amount=dividend["amount"], memo=f"Dividend from {symbol}", date=dividend["paid_at"], transfer_type=Transfer.TransferType.dividend)) all_transfers.sort(key=lambda t: t.date) return all_transfers