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
Esempio n. 2
0
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
Esempio n. 3
0
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