├── .gitignore ├── data └── QQQ_close ├── symbol_and_instrument_urls ├── exceptions.py ├── endpoints.py ├── README.md ├── TW_robinhood_scripts.py ├── get_profit_and_loss.py └── Robinhood.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.ipynb_checkpoints 3 | __pycache__ 4 | *.csv 5 | .vscode/ -------------------------------------------------------------------------------- /data/QQQ_close: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trevorwelch/rh-profit-and-loss/HEAD/data/QQQ_close -------------------------------------------------------------------------------- /symbol_and_instrument_urls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trevorwelch/rh-profit-and-loss/HEAD/symbol_and_instrument_urls -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions: custom exceptions for library 3 | """ 4 | 5 | 6 | class RobinhoodException(Exception): 7 | """ 8 | Wrapper for custom Robinhood library exceptions 9 | """ 10 | 11 | pass 12 | 13 | 14 | class LoginFailed(RobinhoodException): 15 | """ 16 | Unable to login to Robinhood 17 | """ 18 | pass 19 | 20 | 21 | class TwoFactorRequired(LoginFailed): 22 | """ 23 | Unable to login because of 2FA failure 24 | """ 25 | 26 | pass 27 | 28 | 29 | class InvalidTickerSymbol(RobinhoodException): 30 | """ 31 | When an invalid ticker (stock symbol) is given 32 | """ 33 | 34 | pass 35 | 36 | 37 | class InvalidInstrumentId(RobinhoodException): 38 | """ 39 | When an invalid instrument id is given 40 | """ 41 | pass 42 | -------------------------------------------------------------------------------- /endpoints.py: -------------------------------------------------------------------------------- 1 | def login(): 2 | return "https://api.robinhood.com/oauth2/token/" 3 | 4 | def logout(): 5 | return "https://api.robinhood.com/api-token-logout/" 6 | 7 | def investment_profile(): 8 | return "https://api.robinhood.com/user/investment_profile/" 9 | 10 | def accounts(): 11 | return "https://api.robinhood.com/accounts/" 12 | 13 | def ach(option): 14 | ''' 15 | Combination of 3 ACH endpoints. Options include: 16 | * iav 17 | * relationships 18 | * transfers 19 | ''' 20 | return "https://api.robinhood.com/ach/iav/auth/" if option == "iav" else "https://api.robinhood.com/ach/{_option}/".format(_option=option) 21 | 22 | def applications(): 23 | return "https://api.robinhood.com/applications/" 24 | 25 | def dividends(): 26 | return "https://api.robinhood.com/dividends/" 27 | 28 | def edocuments(): 29 | return "https://api.robinhood.com/documents/" 30 | 31 | def instruments(instrumentId=None, option=None): 32 | ''' 33 | Return information about a specific instrument by providing its instrument id. 34 | Add extra options for additional information such as "popularity" 35 | ''' 36 | return "https://api.robinhood.com/instruments/" + ("{id}/".format(id=instrumentId) if instrumentId else "") + ("{_option}/".format(_option=option) if option else "") 37 | 38 | def margin_upgrades(): 39 | return "https://api.robinhood.com/margin/upgrades/" 40 | 41 | def markets(): 42 | return "https://api.robinhood.com/markets/" 43 | 44 | def notifications(): 45 | return "https://api.robinhood.com/notifications/" 46 | 47 | def orders(orderId=None): 48 | return "https://api.robinhood.com/orders/" + ("{id}/".format(id=orderId) if orderId else "") 49 | 50 | def options_orders(orderId=None): 51 | return "https://api.robinhood.com/options/orders/" + ("{id}/".format(id=orderId) if orderId else "") 52 | 53 | def password_reset(): 54 | return "https://api.robinhood.com/password_reset/request/" 55 | 56 | def portfolios(): 57 | return "https://api.robinhood.com/portfolios/" 58 | 59 | def positions(): 60 | return "https://api.robinhood.com/positions/" 61 | 62 | def quotes(): 63 | return "https://api.robinhood.com/quotes/" 64 | 65 | def options_base(): 66 | return "https://api.robinhood.com/options/" 67 | 68 | def historicals(): 69 | return "https://api.robinhood.com/quotes/historicals/" 70 | 71 | def document_requests(): 72 | return "https://api.robinhood.com/upload/document_requests/" 73 | 74 | def user(): 75 | return "https://api.robinhood.com/user/" 76 | 77 | def watchlists(): 78 | return "https://api.robinhood.com/watchlists/" 79 | 80 | def news(stock): 81 | return "https://api.robinhood.com/midlands/news/{_stock}/".format(_stock=stock) 82 | 83 | def fundamentals(stock): 84 | return "https://api.robinhood.com/fundamentals/{_stock}/".format(_stock=stock) 85 | 86 | def tags(tag=None): 87 | ''' 88 | Returns endpoint with tag concatenated. 89 | ''' 90 | return "https://api.robinhood.com/midlands/tags/tag/{_tag}/".format(_tag=tag) 91 | 92 | api_url = "https://api.robinhood.com" 93 | 94 | def options_base(): 95 | return api_url + "/options/" 96 | 97 | def chain(instrumentid): 98 | return api_url + "/options/chains/?equity_instrument_ids={_instrumentid}".format(_instrumentid=instrumentid) 99 | 100 | def options(chainid, dates, option_type): 101 | return api_url + "/options/instruments/?chain_id={_chainid}&expiration_dates={_dates}&state=active&tradability=tradable&type={_type}".format(_chainid=chainid, _dates=dates, _type=option_type) 102 | 103 | def market_data(): 104 | return api_url + "/marketdata/" 105 | 106 | def option_market_data(optionid): 107 | return api_url + "/marketdata/options/{_optionid}/".format(_optionid=optionid) 108 | 109 | def convert_token(): 110 | return "https://api.robinhood.com/oauth2/migrate_token/" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RH Profit and Loss 2 | 3 | A Python script to get a look at your trading history from trading options and individual equities on Robinhood: calculate profit/loss, sum dividend payouts and generate buy-and-hold comparison. 4 | 5 | _UPDATE 2020-01-21: This code is so bad I almost want to delete it from my GH, but it works - and I've seen it get some attention so I'm pushing this small update to make it usable (at least until RH changes their API again). Thanks to @nikhilsaraf who forked it and fixed the auth'ing problem._ 6 | 7 | ## Features 8 | 9 | - *Calculate individual equities' trading pnl, dividends received, options trading pnl* 10 | - *Export CSV files with individual trades and other info* 11 | - *Export pickled dataframes* 12 | - *Specify date range to compare over* 13 | - *Print percent pnl* 14 | 15 | ## Download the repo 16 | From the command line: 17 | 18 | ``` 19 | git clone git@github.com:trevorwelch/rh-profit-and-loss.git 20 | cd rh-profit-and-loss 21 | ``` 22 | 23 | ## Run it, run it 24 | 25 | ### First, grab an oauth token from your browser 26 | 27 | * Navigate to Robinhood.com, log out if you're already logged in. 28 | * Right Click > Inspect Element 29 | * Click on Network tab 30 | * In the "Filter" text entry, type "token" 31 | * With the network monitor open, login to Robinhood 32 | * You will see a few JSON files, find the one that has 'access_token' in the return, and copy that whole string 33 | 34 | Run with defaults (this will process your full account history, starting from your first trade): 35 | 36 | `python3 get_profit_and_loss.py --username --password --access_token ` 37 | 38 | For example: 39 | 40 | `python3 get_profit_and_loss.py --username 'timmyturtlehands@gmail.com' --password 'LovePizza!11one' --access_token ` 41 | 42 | You'll see output like: 43 | 44 | ``` 45 | From November 4, 2018 to today, your total PnL is $486.45 46 | You've made $390.1 buying and selling individual equities, received $16.35 in dividends, and made $80.0 on options trades 47 | ``` 48 | 49 | ## Run it and utilize other features 50 | 51 | ### See how your portfolio performed over a specified date range 52 | 53 | #### Specify `--start_date` and `--end_date` args 54 | 55 | For example: 56 | 57 | `python3 get_profit_and_loss.py --username 'timmyturtlehands@gmail.com' --password 'LovePizza!11one' --access_token --start_date 'July 1, 2018' --end_date 'August 1, 2018'` 58 | 59 | ### Export CSV files for further exploration 60 | 61 | #### Use the `--csv` flag 62 | 63 | The script can output a number of CSV files: 64 | - `pnl_df.csv` shows your profit-and-loss per ticker, and any dividends you've been paid out (dividends are not summed into `net_pnl`) 65 | - `divs_raw.csv` is the full data dump of your dividend history (and future dividends) 66 | - `orders.csv` contains all of your individual buy and sell orders (including orders that didn't execute) 67 | - `options_orders_history_df.csv` contains a simplified record of your options activity 68 | 69 | For example: 70 | 71 | `python3 get_profit_and_loss.py --username 'timmyturtlehands@gmail.com' --password 'LovePizza!11one' --access_token --csv` 72 | 73 | ### Export dataframes as pickles for further exploration: 74 | 75 | #### Use the `--pickle` flag 76 | 77 | Similar exports to the CSV section, but as pickled dataframes which can be loaded directly into pandas for further exploration like so: 78 | ``` 79 | import pandas as pd 80 | df_pnl = pd.read_pickle('df_pnl') 81 | 82 | # Find worst ticker and best ticker, dataframe is already sorted by net_pnl 83 | best_ticker = df_pnl.iloc[0].name 84 | best_ticker_pnl = df_pnl.iloc[0].net_pnl 85 | worst_ticker = df_pnl.iloc[-1].name 86 | worst_ticker_pnl = df_pnl.iloc[-1].net_pnl 87 | 88 | print("Your best individual equities trade over this time period was with {}, with ${} in gains".format(best_ticker, best_ticker_pnl)) 89 | print("Your worst individual equities trade over this time period was with {}, with ${} in gains".format(worst_ticker, worst_ticker_pnl)) 90 | ``` 91 | 92 | For example: 93 | 94 | `python3 get_profit_and_loss.py --username 'timmyturtlehands@gmail.com' --password 'LovePizza!11one' --pickle` 95 | 96 | ### Example command with custom options chained together 97 | 98 | `python3 get_profit_and_loss.py --username 'timmyturtlehands@gmail.com' --password 'LovePizzaFhdjeiw!22222' --start_date 'July 1, 2018' --end_date 'November 10, 2018' --starting_allocation '5000' --csv` 99 | 100 | ### Requirements 101 | 102 | ``` 103 | numpy 104 | pandas 105 | requests 106 | six 107 | ``` 108 | 109 | #### other notes and 'bibliography' ;) 110 | 111 | - Includes unrealized gains (so, positions you haven't closed yet / stocks you haven't sold yet) 112 | 113 | - The `symbols_and_instruments_url` is a lookup table that provides RH's internal instrument ids for symbols, and vice versa, which are needed to interact with the API. By saving and updating this pickle, you reduce the amount of requests you make to the RH API. 114 | 115 | - Special thanks to everyone who maintains the unofficial RH Python library, of which a heavily modified, out-of-date version is included in this repo (https://github.com/Jamonek/Robinhood) 116 | 117 | - Some of the order history code is borrowed from (https://github.com/rmccorm4/Robinhood-Scraper/blob/master/Robinhood/robinhood_pl.py) -------------------------------------------------------------------------------- /TW_robinhood_scripts.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import requests 3 | import time 4 | import datetime 5 | import numpy as np 6 | import sys 7 | import pytz 8 | import random 9 | import json 10 | import csv 11 | import pandas as pd 12 | pd.options.mode.chained_assignment = None # default='warn', to silence the errors about copy 13 | import Robinhood 14 | 15 | 16 | ### ORDER HISTORY STUFF ### 17 | 18 | def fetch_json_by_url(my_trader, url): 19 | return my_trader.session.get(url).json() 20 | 21 | def get_symbol_from_instrument_url(url, df): 22 | 23 | try: 24 | symbol = df.loc[url]['symbol'] 25 | 26 | except Exception as e: 27 | response = requests.get(url) 28 | symbol = response.json()['symbol'] 29 | df.at[url, 'symbol'] = symbol 30 | # time.sleep(np.random.randint(low=0, high=2, size=(1))[0]) 31 | 32 | return symbol, df 33 | 34 | def order_item_info(order, my_trader, df): 35 | #side: .side, price: .average_price, shares: .cumulative_quantity, instrument: .instrument, date : .last_transaction_at 36 | symbol, df = get_symbol_from_instrument_url(order['instrument'], df) 37 | 38 | order_info_dict = { 39 | 'side': order['side'], 40 | 'avg_price': order['average_price'], 41 | 'order_price': order['price'], 42 | 'order_quantity': order['quantity'], 43 | 'shares': order['cumulative_quantity'], 44 | 'symbol': symbol, 45 | 'id': order['id'], 46 | 'date': order['last_transaction_at'], 47 | 'state': order['state'], 48 | 'type': order['type'] 49 | } 50 | 51 | return order_info_dict 52 | 53 | def get_all_history_orders(my_trader): 54 | 55 | orders = [] 56 | past_orders = my_trader.order_history() 57 | orders.extend(past_orders['results']) 58 | 59 | while past_orders['next']: 60 | # print("{} order fetched".format(len(orders))) 61 | next_url = past_orders['next'] 62 | past_orders = fetch_json_by_url(my_trader, next_url) 63 | orders.extend(past_orders['results']) 64 | # print("{} order fetched".format(len(orders))) 65 | 66 | return orders 67 | 68 | def mark_pending_orders(row): 69 | if row.state == 'queued' or row.state == 'confirmed': 70 | order_status_is_pending = True 71 | else: 72 | order_status_is_pending = False 73 | return order_status_is_pending 74 | # df_order_history.apply(mark_pending_orders, axis=1) 75 | 76 | def get_order_history(my_trader): 77 | 78 | # Get unfiltered list of order history 79 | past_orders = get_all_history_orders(my_trader) 80 | 81 | # Load in our pickled database of instrument-url lookups 82 | instruments_df = pd.read_pickle('symbol_and_instrument_urls') 83 | 84 | # Create a big dict of order history 85 | orders = [order_item_info(order, my_trader, instruments_df) for order in past_orders] 86 | 87 | # Save our pickled database of instrument-url lookups 88 | instruments_df.to_pickle('symbol_and_instrument_urls') 89 | 90 | df = pd.DataFrame.from_records(orders) 91 | df['ticker'] = df['symbol'] 92 | 93 | columns = ['ticker', 'state', 'order_quantity', 'shares', 'avg_price', 'date', 'id', 'order_price', 'side', 'symbol', 'type'] 94 | df = df[columns] 95 | 96 | df['is_pending'] = df.apply(mark_pending_orders, axis=1) 97 | 98 | return df, instruments_df 99 | 100 | def get_all_history_options_orders(my_trader): 101 | 102 | options_orders = [] 103 | past_options_orders = my_trader.options_order_history() 104 | options_orders.extend(past_options_orders['results']) 105 | 106 | while past_options_orders['next']: 107 | # print("{} order fetched".format(len(orders))) 108 | next_url = past_options_orders['next'] 109 | past_options_orders = fetch_json_by_url(my_trader, next_url) 110 | options_orders.extend(past_options_orders['results']) 111 | # print("{} order fetched".format(len(orders))) 112 | 113 | options_orders_cleaned = np.empty((0, 4)) 114 | 115 | for each in options_orders: 116 | if float(each['processed_premium']) < 1: 117 | continue 118 | else: 119 | # print(each['chain_symbol']) 120 | # print(each['processed_premium']) 121 | # print(each['created_at']) 122 | # print(each['legs'][0]['position_effect']) 123 | # print("~~~") 124 | if each['legs'][0]['position_effect'] == 'open': 125 | value = round(float(each['processed_premium']), 2)*-1 126 | else: 127 | value = round(float(each['processed_premium']), 2) 128 | 129 | one_order = [pd.to_datetime(each['created_at']), each['chain_symbol'], value, each['legs'][0]['position_effect']] 130 | options_orders_cleaned.append(one_order) 131 | 132 | df_options_orders_cleaned = pd.DataFrame(options_orders_cleaned) 133 | df_options_orders_cleaned.columns = ['date', 'ticker', 'value', 'position_effect'] 134 | df_options_orders_cleaned = df_options_orders_cleaned.sort_values('date') 135 | df_options_orders_cleaned = df_options_orders_cleaned.set_index('date') 136 | 137 | return df_options_orders_cleaned 138 | 139 | 140 | ### END ORDER HISTORY GETTING STUFF #### 141 | 142 | def pct_change(new_num, old_num): 143 | change = new_num - old_num 144 | pct_change = change / old_num if old_num else 0 145 | return round(pct_change*100, 2) -------------------------------------------------------------------------------- /get_profit_and_loss.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | import os 4 | import numpy as np 5 | import sys 6 | import json 7 | import pandas as pd 8 | import requests 9 | import operator 10 | import traceback 11 | import sys 12 | 13 | import TW_robinhood_scripts as rh 14 | import Robinhood 15 | 16 | def rh_profit_and_loss(username=None, password=None, access_token=None, starting_allocation=5000, start_date=None, end_date=None, csv_export=1, buy_and_hold=0, pickle=0, options=1): 17 | 18 | # from rmccorm4 Robinhood-Scraper 19 | class Order: 20 | def __init__(self, side, symbol, shares, price, date, state): 21 | self.side = side 22 | self.symbol = symbol 23 | self.shares = float(shares) 24 | self.price = float(price) 25 | self.date = date 26 | self.state = state 27 | 28 | def pl(self): 29 | if self.side == 'buy': 30 | return -1 * int(self.shares) * float(self.price) 31 | else: 32 | return int(self.shares) * float(self.price) 33 | 34 | class Stock: 35 | def __init__(self, symbol): 36 | self.symbol = symbol 37 | self.orders = [] 38 | self.net_shares = 0 39 | self.net_pl = 0 40 | 41 | 42 | def itemize_stocks(): 43 | 44 | # Create list for each stock 45 | stocks = {} 46 | with open('orders.csv', 'r') as csvfile: 47 | lines = csv.reader(csvfile, delimiter=',') 48 | for line in lines: 49 | 50 | ticker = line[1] 51 | 52 | price = line[3] 53 | 54 | # Check for header or invalid entries 55 | if ticker == 'symbol' or price == '': 56 | continue 57 | 58 | # Add stock to dict if not already in there 59 | if ticker not in stocks: 60 | stocks[ticker] = Stock(ticker) 61 | 62 | # Add order to stock 63 | stocks[ticker].orders.append(Order(line[0], line[1], line[2], line[3], line[4], line[5])) 64 | return stocks 65 | 66 | def calculate_itemized_pl(stocks, my_trader): 67 | for stock in stocks.values(): 68 | for order in stock.orders: 69 | if order.side == 'buy': 70 | stock.net_shares += order.shares 71 | else: 72 | stock.net_shares -= order.shares 73 | # order.pl() is positive for selling and negative for buying 74 | stock.net_pl += order.pl() 75 | 76 | # Handle outstanding shares - should be current positions 77 | if stock.net_shares > 0: 78 | 79 | price = my_trader.last_trade_price(stock.symbol)[0][0] 80 | last_price = float(price) 81 | 82 | # Add currently held shares from net_pl as if selling now (unrealized PnL) 83 | stock.net_pl += stock.net_shares * last_price 84 | 85 | # Should handle free gift stocks 86 | elif stock.net_shares < 0: 87 | stock.symbol += ' ' 88 | 89 | def calculate_outstanding_options(my_trader): 90 | owned = my_trader.options_owned() 91 | 92 | pending = None 93 | 94 | if (len(owned) > 0): 95 | df_owned = pd.DataFrame(owned) 96 | 97 | df_owned['quantity'] = pd.to_numeric(df_owned['quantity']) 98 | df_owned['average_price'] = pd.to_numeric(df_owned['average_price']) 99 | df_owned['trade_value_multiplier'] = pd.to_numeric(df_owned['trade_value_multiplier']) 100 | 101 | pending = df_owned[ 102 | df_owned['quantity'] > 0 103 | ] 104 | 105 | pending = pending[['chain_symbol', 'quantity', 'average_price', 'option_id', 'trade_value_multiplier']].reset_index(drop=True) 106 | 107 | pending['avg_option_cost'] = pending['average_price']/pending['trade_value_multiplier'] 108 | 109 | current_option_values = [] 110 | for each in pending['option_id'].values: 111 | try: 112 | current_price = my_trader.get_option_market_data(each)['adjusted_mark_price'] 113 | except Exception: 114 | current_price = np.nan 115 | 116 | current_option_values.append(current_price) 117 | 118 | pending['current_option_values'] = [float(x) for x in current_option_values] 119 | pending['current_value'] = pending['current_option_values']*pending['quantity']*100 120 | 121 | pending['date'] = pd.Timestamp.now() 122 | pending = pending.set_index('date') 123 | 124 | pending.to_csv('pending_options_orders_df.csv') 125 | 126 | pending['position_effect'] = 'pending' 127 | pending['value'] = pending['current_value'] 128 | pending['ticker'] = pending['chain_symbol'] 129 | 130 | pending['original_value'] = pending['quantity'] * pending['average_price'] 131 | 132 | return pending 133 | 134 | 135 | # INSTANTIATE ROBINHOOD my_trader # 136 | my_trader = Robinhood.Robinhood() 137 | logged_in = my_trader.set_oath_access_token(username, password, access_token) 138 | if not logged_in: 139 | logged_in = my_trader.login(username=username, password=password) 140 | my_account = my_trader.get_account()['url'] 141 | 142 | df_order_history, _ = rh.get_order_history(my_trader) 143 | df_orders = df_order_history[['side', 'symbol', 'shares', 'avg_price', 'date', 'state']] 144 | df_orders.columns = ['side', 'symbol', 'shares', 'price', 'date', 'state'] 145 | 146 | # Filter for input dates 147 | df_orders['date'] = pd.to_datetime(df_orders['date']) 148 | df_orders = df_orders.sort_values('date') 149 | df_orders = df_orders.set_index('date') 150 | df_orders = df_orders[start_date:end_date] 151 | df_orders['date'] = df_orders.index 152 | 153 | if pickle == 1: 154 | df_orders.to_pickle('df_orders_history') 155 | 156 | if start_date == 'January 1, 2012': 157 | start_date = df_orders.iloc[0]['date'].strftime('%B %d, %Y') 158 | 159 | df_orders.set_index('side').to_csv('orders.csv', header=None) 160 | stocks = itemize_stocks() 161 | calculate_itemized_pl(stocks, my_trader) 162 | 163 | with open('stockwise_pl.csv', 'w') as outfile: 164 | writer = csv.writer(outfile, delimiter=',') 165 | writer.writerow(['SYMBOL', 'net_pnl', 'n_trades']) 166 | sorted_pl = sorted(stocks.values(), key=operator.attrgetter('net_pl'), reverse=True) 167 | total_pl = 0 168 | total_trades = 0 169 | for stock in sorted_pl: 170 | num_trades = len(stock.orders) 171 | writer.writerow([stock.symbol, '{0:.2f}'.format(stock.net_pl), len(stock.orders)]) 172 | total_pl += stock.net_pl 173 | total_trades += num_trades 174 | writer.writerow(['Totals', total_pl, total_trades]) 175 | # print('Created', outfile.name, 'in this directory.') 176 | 177 | # Read the pnl we generated 178 | df_pnl = pd.read_csv('stockwise_pl.csv') 179 | 180 | # Get dividends from Robinhood 181 | dividends = Robinhood.Robinhood.dividends(my_trader) 182 | 183 | # Put the dividends in a dataframe 184 | list_of_records = [] 185 | for each in dividends['results']: 186 | list_of_records.append(pd.DataFrame(pd.Series(each)).T) 187 | 188 | df_dividends = pd.concat(list_of_records) 189 | df_dividends = df_dividends.set_index('id') 190 | df_dividends['id'] = df_dividends.index 191 | 192 | # Load in our pickled database of instrument-url lookups 193 | instruments_df = pd.read_pickle('symbol_and_instrument_urls') 194 | 195 | df_dividends['ticker'] = np.nan 196 | for each in df_dividends.itertuples(): 197 | symbol, instruments_df = rh.get_symbol_from_instrument_url(each.instrument, instruments_df) 198 | df_dividends.loc[each.id, 'ticker'] = symbol 199 | 200 | if pickle == 1: 201 | df_dividends.to_pickle('df_dividends') 202 | 203 | if csv_export == 1: 204 | df_dividends.to_csv('divs_raw.csv') 205 | # df_pnl.to_csv('pnl.csv') 206 | 207 | # Filter df_dividends 208 | df_dividends['record_date'] = pd.to_datetime(df_dividends['record_date']) 209 | df_dividends = df_dividends.sort_values('record_date') 210 | df_dividends = df_dividends.set_index('record_date') 211 | df_dividends = df_dividends[start_date:end_date] 212 | # convert numbers to actual numbers 213 | df_dividends['amount'] = pd.to_numeric(df_dividends['amount']) 214 | 215 | # Group dividend payouts by ticker and sum 216 | df_divs_summed = df_dividends.groupby('ticker').sum() 217 | 218 | # Set a column to the ticker 219 | df_divs_summed['ticker'] = df_divs_summed.index 220 | 221 | # For the stockwise pnl, set index to the ticker ('SYMBOL') 222 | df_pnl = df_pnl.set_index('SYMBOL') 223 | 224 | try: 225 | df_pnl = df_pnl.drop('Totals') 226 | except KeyError as e: 227 | print("Totals row not found") 228 | 229 | # Set div payouts column 230 | df_pnl['div_payouts'] = np.nan 231 | 232 | # For each in the summed 233 | for each in df_divs_summed.itertuples(): 234 | amount = each.amount 235 | df_pnl.loc[each.ticker, 'div_payouts'] = amount 236 | 237 | if pickle == 1: 238 | df_divs_summed.to_pickle('df_divs_summed') 239 | df_pnl.to_pickle('df_pnl') 240 | 241 | if csv_export == 1: 242 | # df_divs_summed.to_csv('divs_summed_df.csv') 243 | df_pnl.to_csv('pnl_df.csv') 244 | 245 | # Calculate the dividends received (or that are confirmed you will receive in the future) 246 | dividends_paid = float(df_pnl.sum()['div_payouts']) 247 | 248 | # Calculate the total pnl 249 | pnl = float(df_pnl.sum()['net_pnl']) 250 | 251 | # When printing the final output, if no date was provided, print "today" 252 | if end_date == 'January 1, 2030': 253 | end_date_string = 'today' 254 | else: 255 | end_date_string = end_date 256 | 257 | # Retrieve options history 258 | if options == 1: 259 | try: 260 | df_options_orders_history = rh.get_all_history_options_orders(my_trader) 261 | pending_options = calculate_outstanding_options(my_trader) 262 | 263 | options_pnl = 0 264 | 265 | if (pending_options is not None and pending_options.len() > 0): 266 | df_options = pd.concat([ 267 | df_options_orders_history, 268 | pending_options[['ticker', 'value', 'position_effect']], 269 | ]) 270 | 271 | # More hacky shit cause this code is a mess 272 | df_options = df_options.reset_index() 273 | df_options['date'] = pd.to_datetime(df_options['date'], utc=True) 274 | df_options = df_options.set_index(df_options['date']) 275 | 276 | if csv_export == 1: 277 | df_options.to_csv('options_orders_history_df.csv') 278 | if pickle == 1: 279 | df_options.to_pickle('df_options_orders_history') 280 | 281 | df_options = df_options[start_date:end_date] 282 | options_pnl = df_options['value'].sum() 283 | 284 | except Exception as e: 285 | print(traceback.format_exc()) 286 | print(sys.exc_info()[0]) 287 | options_pnl = 0 288 | 289 | total_pnl = round(pnl + dividends_paid + options_pnl, 2) 290 | 291 | 292 | # Print final output 293 | print("~~~") 294 | print("From {} to {}, your total PnL is ${}".format(start_date, end_date_string, total_pnl)) 295 | print("You've made ${} buying and selling individual equities, received ${} in dividends, and ${} on options trades".format(round(pnl,2), round(dividends_paid,2), round(options_pnl,2))) 296 | 297 | # Calculate ROI, if the user input a starting allocation 298 | if roi == 1: 299 | 300 | # Calculate Stocks ROI 301 | long_entries = df_orders[ 302 | (df_orders['side'] == 'buy') 303 | & 304 | (df_orders['state'] == 'filled') 305 | ] 306 | 307 | long_entries['value'] = pd.to_numeric(long_entries['price'])*pd.to_numeric(long_entries['shares']) 308 | 309 | starting_stock_investment = long_entries['value'].sum() 310 | 311 | stocks_roi = rh.pct_change( 312 | pnl+starting_stock_investment, 313 | starting_stock_investment 314 | ) 315 | print("Your return-on-investment (ROI) for stock trades is: %{}".format(stocks_roi)) 316 | 317 | # Calculate Options ROI 318 | # More hacky shit cause this code is a mess 319 | df_options_orders_history = df_options_orders_history.reset_index() 320 | df_options_orders_history['date'] = pd.to_datetime(df_options_orders_history['date'], utc=True) 321 | df_options_orders_history = df_options_orders_history.set_index(df_options_orders_history['date']) 322 | 323 | df_options_orders_history = df_options_orders_history[start_date:end_date] 324 | options_entries = df_options_orders_history[ 325 | df_options_orders_history['value'] < 0 326 | ] 327 | 328 | starting_options_investment = options_entries['value'].sum()*-1 329 | 330 | options_roi = rh.pct_change( 331 | options_pnl+starting_options_investment, 332 | starting_options_investment 333 | ) 334 | print("Your return-on-investment (ROI) for options trades is: %{}".format(options_roi)) 335 | 336 | # print(starting_options_investment) 337 | # print(options_pnl) 338 | # print(pnl) 339 | # print(starting_stock_investment) 340 | 341 | return_on_investment = rh.pct_change( 342 | options_pnl+starting_options_investment+pnl+starting_stock_investment, 343 | starting_options_investment+starting_stock_investment 344 | ) 345 | print("Your return-on-investment (ROI) on overall capital deployed is: %{}".format(return_on_investment)) 346 | 347 | if buy_and_hold == 1: 348 | print("With a starting allocation of ${}, if you had just bought and held QQQ, your PnL would be ${}".format(starting_allocation, round(QQQ_buy_and_hold_gain,2))) 349 | print("~~~") 350 | # Delete the csv we were processing earlier 351 | # os.remove('stockwise_pl.csv') 352 | 353 | if __name__ == '__main__': 354 | 355 | # Parse command line arguments 356 | parser = argparse.ArgumentParser() 357 | parser.add_argument("--username", help="username (required)") 358 | parser.add_argument("--password", help="password (required)") 359 | parser.add_argument("--access_token", help="oath access_token (required)") 360 | parser.add_argument("--start_date", help="begin date for calculations") 361 | parser.add_argument("--end_date", help="begin date for calculations") 362 | parser.add_argument("--csv", help="save csvs along the way", action="store_true") 363 | parser.add_argument("--pickle", help="save pickles along the way", action="store_true") 364 | 365 | args = parser.parse_args() 366 | 367 | if args.username and args.password and args.access_token: 368 | print("Working...") 369 | else: 370 | print("Please enter a username and password and access_token and try again!") 371 | sys.exit() 372 | 373 | # check for flag 374 | if args.csv: 375 | csv_export = 1 376 | else: 377 | csv_export = 0 378 | 379 | # check for flag 380 | if args.pickle: 381 | pickle = 1 382 | else: 383 | pickle = 0 384 | 385 | # check for start date 386 | if args.start_date: 387 | start_date = args.start_date 388 | else: 389 | start_date = 'January 1, 2012' 390 | 391 | # check for end date 392 | if args.end_date: 393 | end_date = args.end_date 394 | else: 395 | end_date = 'January 1, 2030' 396 | 397 | roi = 1 398 | 399 | rh_profit_and_loss(username=args.username, 400 | password=args.password, 401 | access_token=args.access_token, 402 | start_date=start_date, 403 | end_date=end_date, 404 | csv_export=csv_export, 405 | buy_and_hold=0, 406 | options=1, 407 | pickle=pickle) -------------------------------------------------------------------------------- /Robinhood.py: -------------------------------------------------------------------------------- 1 | """Robinhood.py: a collection of utilities for working with Robinhood's Private API """ 2 | 3 | #Standard libraries 4 | import logging 5 | import warnings 6 | 7 | import robin_stocks as r 8 | 9 | from enum import Enum 10 | 11 | #External dependencies 12 | from six.moves.urllib.parse import unquote # pylint: disable=E0401 13 | from six.moves.urllib.request import getproxies # pylint: disable=E0401 14 | from six.moves import input 15 | 16 | import getpass 17 | import requests 18 | import six 19 | import dateutil 20 | import urllib 21 | 22 | #Application-specific imports 23 | try: 24 | from . import exceptions as RH_exception 25 | from . import endpoints 26 | except Exception as e: 27 | import exceptions as RH_exception 28 | import endpoints 29 | 30 | import random 31 | 32 | class Bounds(Enum): 33 | """Enum for bounds in `historicals` endpoint """ 34 | 35 | REGULAR = 'regular' 36 | EXTENDED = 'extended' 37 | 38 | 39 | class Transaction(Enum): 40 | """Enum for buy/sell orders """ 41 | 42 | BUY = 'buy' 43 | SELL = 'sell' 44 | 45 | 46 | class Robinhood: 47 | """Wrapper class for fetching/parsing Robinhood endpoints """ 48 | 49 | session = None 50 | username = None 51 | password = None 52 | headers = None 53 | auth_token = None 54 | oauth_token = None 55 | 56 | logger = logging.getLogger('Robinhood') 57 | logger.addHandler(logging.NullHandler()) 58 | 59 | 60 | ########################################################################### 61 | # Logging in and initializing 62 | ########################################################################### 63 | 64 | def __init__(self): 65 | self.session = requests.session() 66 | self.session.proxies = getproxies() 67 | self.headers = { 68 | "Accept": "*/*", 69 | "Accept-Encoding": "gzip, deflate", 70 | "Accept-Language": "en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5", 71 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 72 | "Connection": "keep-alive", 73 | "User-Agent": "Robinhood/6.28.0 (com.robinhood.release.Robinhood; build:5771; iOS 11.4.1) Alamofire/4.5.1" 74 | } 75 | self.session.headers = self.headers 76 | self.auth_method = self.login_prompt 77 | 78 | def login_required(function): # pylint: disable=E0213 79 | """ Decorator function that prompts user for login if they are not logged in already. Can be applied to any function using the @ notation. """ 80 | def wrapper(self, *args, **kwargs): 81 | if 'Authorization' not in self.headers: 82 | self.auth_method() 83 | return function(self, *args, **kwargs) # pylint: disable=E1102 84 | return wrapper 85 | 86 | def login_prompt(self): # pragma: no cover 87 | """Prompts user for username and password and calls login() """ 88 | 89 | username = input("Username: ") 90 | password = getpass.getpass() 91 | 92 | return self.login(username=username, password=password) 93 | 94 | def set_oath_access_token(self, username, password, access_token): 95 | self.username = username 96 | self.password = password 97 | self.oauth_token = access_token 98 | self.headers['Authorization'] = 'Bearer ' + self.oauth_token 99 | return True 100 | 101 | def login(self, username, password, mfa_code=None): 102 | 103 | self.username = username 104 | self.password = password 105 | 106 | # try getting access_token with new API 107 | l = r.login(username, password) 108 | if 'access_token' in l.keys(): 109 | self.oauth_token = l['access_token'] 110 | self.headers['Authorization'] = 'Bearer ' + self.oauth_token 111 | print(l) 112 | return True 113 | # /try getting access_token with new API 114 | 115 | payload = { 116 | 'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS', 117 | 'expires_in': 86400, 118 | 'grant_type': 'password', 119 | 'password': self.password, 120 | 'scope': 'internal', 121 | 'username': self.username 122 | } 123 | 124 | if mfa_code: 125 | payload['mfa_code'] = mfa_code 126 | 127 | try: 128 | res = self.session.post('https://api.robinhood.com/oauth2/token/', data=payload, timeout=300) 129 | res.raise_for_status() 130 | data = res.json() 131 | except requests.exceptions.HTTPError: 132 | raise RH_exception.LoginFailed() 133 | 134 | if 'mfa_required' in data.keys(): # pragma: no cover 135 | raise RH_exception.TwoFactorRequired() # requires a second call to enable 2FA 136 | 137 | if 'access_token' in data.keys(): 138 | self.oauth_token = data['access_token'] 139 | self.headers['Authorization'] = 'Bearer ' + self.oauth_token 140 | return True 141 | 142 | return False 143 | 144 | 145 | def logout(self): 146 | """Logout from Robinhood 147 | 148 | Returns: 149 | (:obj:`requests.request`) result from logout endpoint 150 | 151 | """ 152 | 153 | try: 154 | req = self.session.post(endpoints.logout(), timeout=300) 155 | req.raise_for_status() 156 | except requests.exceptions.HTTPError as err_msg: 157 | warnings.warn('Failed to log out ' + repr(err_msg)) 158 | 159 | self.headers['Authorization'] = None 160 | self.auth_token = None 161 | 162 | return req 163 | 164 | 165 | ########################################################################### 166 | # GET DATA 167 | ########################################################################### 168 | 169 | def investment_profile(self): 170 | """Fetch investment_profile """ 171 | 172 | res = self.session.get(endpoints.investment_profile(), timeout=300) 173 | res.raise_for_status() # will throw without auth 174 | data = res.json() 175 | 176 | return data 177 | 178 | 179 | def instruments(self, stock): 180 | """Fetch instruments endpoint 181 | 182 | Args: 183 | stock (str): stock ticker 184 | 185 | Returns: 186 | (:obj:`dict`): JSON contents from `instruments` endpoint 187 | """ 188 | 189 | res = self.session.get(endpoints.instruments(), params={'query': stock.upper()}, timeout=300) 190 | res.raise_for_status() 191 | res = res.json() 192 | 193 | # if requesting all, return entire object so may paginate with ['next'] 194 | if (stock == ""): 195 | return res 196 | 197 | return res['results'] 198 | 199 | 200 | def instrument(self, id): 201 | """Fetch instrument info 202 | 203 | Args: 204 | id (str): instrument id 205 | 206 | Returns: 207 | (:obj:`dict`): JSON dict of instrument 208 | """ 209 | url = str(endpoints.instruments()) + str(id) + "/" 210 | 211 | try: 212 | req = requests.get(url, timeout=300) 213 | req.raise_for_status() 214 | data = req.json() 215 | except requests.exceptions.HTTPError: 216 | raise RH_exception.InvalidInstrumentId() 217 | 218 | return data 219 | 220 | 221 | def quote_data(self, stock=''): 222 | """Fetch stock quote 223 | 224 | Args: 225 | stock (str): stock ticker, prompt if blank 226 | 227 | Returns: 228 | (:obj:`dict`): JSON contents from `quotes` endpoint 229 | """ 230 | 231 | url = None 232 | 233 | if stock.find(',') == -1: 234 | url = str(endpoints.quotes()) + str(stock) + "/" 235 | else: 236 | url = str(endpoints.quotes()) + "?symbols=" + str(stock) 237 | 238 | #Check for validity of symbol 239 | try: 240 | req = requests.get(url, headers=self.headers, timeout=300) 241 | req.raise_for_status() 242 | data = req.json() 243 | except requests.exceptions.HTTPError: 244 | raise RH_exception.InvalidTickerSymbol() 245 | 246 | 247 | return data 248 | 249 | 250 | # We will keep for compatibility until next major release 251 | def quotes_data(self, stocks): 252 | """Fetch quote for multiple stocks, in one single Robinhood API call 253 | 254 | Args: 255 | stocks (list): stock tickers 256 | 257 | Returns: 258 | (:obj:`list` of :obj:`dict`): List of JSON contents from `quotes` endpoint, in the 259 | same order of input args. If any ticker is invalid, a None will occur at that position. 260 | """ 261 | 262 | url = str(endpoints.quotes()) + "?symbols=" + ",".join(stocks) 263 | 264 | try: 265 | req = requests.get(url, headers=self.headers, timeout=300) 266 | req.raise_for_status() 267 | data = req.json() 268 | except requests.exceptions.HTTPError: 269 | raise RH_exception.InvalidTickerSymbol() 270 | 271 | 272 | return data["results"] 273 | 274 | 275 | def get_quote_list(self, 276 | stock='', 277 | key=''): 278 | """Returns multiple stock info and keys from quote_data (prompt if blank) 279 | 280 | Args: 281 | stock (str): stock ticker (or tickers separated by a comma) 282 | , prompt if blank 283 | key (str): key attributes that the function should return 284 | 285 | Returns: 286 | (:obj:`list`): Returns values from each stock or empty list 287 | if none of the stocks were valid 288 | 289 | """ 290 | 291 | #Creates a tuple containing the information we want to retrieve 292 | def append_stock(stock): 293 | keys = key.split(',') 294 | myStr = '' 295 | for item in keys: 296 | myStr += stock[item] + "," 297 | 298 | return (myStr.split(',')) 299 | 300 | 301 | #Prompt for stock if not entered 302 | if not stock: # pragma: no cover 303 | stock = input("Symbol: ") 304 | 305 | data = self.quote_data(stock) 306 | res = [] 307 | 308 | # Handles the case of multple tickers 309 | if stock.find(',') != -1: 310 | for stock in data['results']: 311 | if stock is None: 312 | continue 313 | res.append(append_stock(stock)) 314 | 315 | else: 316 | res.append(append_stock(data)) 317 | 318 | return res 319 | 320 | 321 | def get_quote(self, stock=''): 322 | """Wrapper for quote_data """ 323 | 324 | data = self.quote_data(stock) 325 | return data["symbol"] 326 | 327 | def get_historical_quotes(self, stock, interval, span, bounds=Bounds.REGULAR): 328 | """Fetch historical data for stock 329 | 330 | Note: valid interval/span configs 331 | interval = 5minute | 10minute + span = day, week 332 | interval = day + span = year 333 | interval = week 334 | TODO: NEEDS TESTS 335 | 336 | Args: 337 | stock (str): stock ticker 338 | interval (str): resolution of data 339 | span (str): length of data 340 | bounds (:enum:`Bounds`, optional): 'extended' or 'regular' trading hours 341 | 342 | Returns: 343 | (:obj:`dict`) values returned from `historicals` endpoint 344 | """ 345 | if type(stock) is str: 346 | stock = [stock] 347 | 348 | if isinstance(bounds, str): # recast to Enum 349 | bounds = Bounds(bounds) 350 | 351 | params = { 352 | 'symbols': ','.join(stock).upper(), 353 | 'interval': interval, 354 | 'span': span, 355 | 'bounds': bounds.name.lower() 356 | } 357 | 358 | res = self.session.get(endpoints.historicals(), params=params, timeout=300) 359 | return res.json() 360 | 361 | 362 | def get_news(self, stock): 363 | """Fetch news endpoint 364 | Args: 365 | stock (str): stock ticker 366 | 367 | Returns: 368 | (:obj:`dict`) values returned from `news` endpoint 369 | """ 370 | 371 | return self.session.get(endpoints.news(stock.upper()), timeout=300).json() 372 | 373 | 374 | def print_quote(self, stock=''): # pragma: no cover 375 | """Print quote information 376 | Args: 377 | stock (str): ticker to fetch 378 | 379 | Returns: 380 | None 381 | """ 382 | 383 | data = self.get_quote_list(stock, 'symbol,last_trade_price') 384 | for item in data: 385 | quote_str = item[0] + ": $" + item[1] 386 | print(quote_str) 387 | self.logger.info(quote_str) 388 | 389 | 390 | def print_quotes(self, stocks): # pragma: no cover 391 | """Print a collection of stocks 392 | 393 | Args: 394 | stocks (:obj:`list`): list of stocks to pirnt 395 | 396 | Returns: 397 | None 398 | """ 399 | 400 | if stocks is None: 401 | return 402 | 403 | for stock in stocks: 404 | self.print_quote(stock) 405 | 406 | 407 | def ask_price(self, stock=''): 408 | """Get asking price for a stock 409 | 410 | Note: 411 | queries `quote` endpoint, dict wrapper 412 | 413 | Args: 414 | stock (str): stock ticker 415 | 416 | Returns: 417 | (float): ask price 418 | """ 419 | 420 | return self.get_quote_list(stock, 'ask_price') 421 | 422 | 423 | def ask_size(self, stock=''): 424 | """Get ask size for a stock 425 | 426 | Note: 427 | queries `quote` endpoint, dict wrapper 428 | 429 | Args: 430 | stock (str): stock ticker 431 | 432 | Returns: 433 | (int): ask size 434 | """ 435 | 436 | return self.get_quote_list(stock, 'ask_size') 437 | 438 | 439 | def bid_price(self, stock=''): 440 | """Get bid price for a stock 441 | 442 | Note: 443 | queries `quote` endpoint, dict wrapper 444 | 445 | Args: 446 | stock (str): stock ticker 447 | 448 | Returns: 449 | (float): bid price 450 | """ 451 | 452 | return self.get_quote_list(stock, 'bid_price') 453 | 454 | 455 | def bid_size(self, stock=''): 456 | """Get bid size for a stock 457 | 458 | Note: 459 | queries `quote` endpoint, dict wrapper 460 | 461 | Args: 462 | stock (str): stock ticker 463 | 464 | Returns: 465 | (int): bid size 466 | """ 467 | 468 | return self.get_quote_list(stock, 'bid_size') 469 | 470 | 471 | def last_trade_price(self, stock=''): 472 | """Get last trade price for a stock 473 | 474 | Note: 475 | queries `quote` endpoint, dict wrapper 476 | 477 | Args: 478 | stock (str): stock ticker 479 | 480 | Returns: 481 | (float): last trade price 482 | """ 483 | 484 | return self.get_quote_list(stock, 'last_trade_price') 485 | 486 | 487 | def previous_close(self, stock=''): 488 | """Get previous closing price for a stock 489 | 490 | Note: 491 | queries `quote` endpoint, dict wrapper 492 | 493 | Args: 494 | stock (str): stock ticker 495 | 496 | Returns: 497 | (float): previous closing price 498 | """ 499 | 500 | return self.get_quote_list(stock, 'previous_close') 501 | 502 | 503 | def previous_close_date(self, stock=''): 504 | """Get previous closing date for a stock 505 | 506 | Note: 507 | queries `quote` endpoint, dict wrapper 508 | 509 | Args: 510 | stock (str): stock ticker 511 | 512 | Returns: 513 | (str): previous close date 514 | """ 515 | 516 | return self.get_quote_list(stock, 'previous_close_date') 517 | 518 | 519 | def adjusted_previous_close(self, stock=''): 520 | """Get adjusted previous closing price for a stock 521 | 522 | Note: 523 | queries `quote` endpoint, dict wrapper 524 | 525 | Args: 526 | stock (str): stock ticker 527 | 528 | Returns: 529 | (float): adjusted previous closing price 530 | """ 531 | 532 | return self.get_quote_list(stock, 'adjusted_previous_close') 533 | 534 | 535 | def symbol(self, stock=''): 536 | """Get symbol for a stock 537 | 538 | Note: 539 | queries `quote` endpoint, dict wrapper 540 | 541 | Args: 542 | stock (str): stock ticker 543 | 544 | Returns: 545 | (str): stock symbol 546 | """ 547 | 548 | return self.get_quote_list(stock, 'symbol') 549 | 550 | 551 | def last_updated_at(self, stock=''): 552 | """Get last update datetime 553 | 554 | Note: 555 | queries `quote` endpoint, dict wrapper 556 | 557 | Args: 558 | stock (str): stock ticker 559 | 560 | Returns: 561 | (str): last update datetime 562 | """ 563 | 564 | return self.get_quote_list(stock, 'last_updated_at') 565 | 566 | 567 | def last_updated_at_datetime(self, stock=''): 568 | """Get last updated datetime 569 | 570 | Note: 571 | queries `quote` endpoint, dict wrapper 572 | `self.last_updated_at` returns time as `str` in format: 'YYYY-MM-ddTHH:mm:ss:000Z' 573 | 574 | Args: 575 | stock (str): stock ticker 576 | 577 | Returns: 578 | (datetime): last update datetime 579 | 580 | """ 581 | 582 | #Will be in format: 'YYYY-MM-ddTHH:mm:ss:000Z' 583 | datetime_string = self.last_updated_at(stock) 584 | result = dateutil.parser.parse(datetime_string) 585 | 586 | return result 587 | 588 | def get_account(self): 589 | """Fetch account information 590 | 591 | Returns: 592 | (:obj:`dict`): `accounts` endpoint payload 593 | """ 594 | 595 | res = self.session.get(endpoints.accounts(), timeout=300) 596 | res.raise_for_status() # auth required 597 | res = res.json() 598 | 599 | return res['results'][0] 600 | 601 | 602 | def get_url(self, url): 603 | """ 604 | Flat wrapper for fetching URL directly 605 | """ 606 | 607 | return self.session.get(url, timeout=300).json() 608 | 609 | def get_popularity(self, stock=''): 610 | """Get the number of robinhood users who own the given stock 611 | 612 | Args: 613 | stock (str): stock ticker 614 | 615 | Returns: 616 | (int): number of users who own the stock 617 | """ 618 | stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] 619 | return self.get_url(endpoints.instruments(stock_instrument, "popularity"))["num_open_positions"] 620 | 621 | def get_tickers_by_tag(self, tag=None): 622 | """Get a list of instruments belonging to a tag 623 | 624 | Args: tag - Tags may include but are not limited to: 625 | * top-movers 626 | * etf 627 | * 100-most-popular 628 | * mutual-fund 629 | * finance 630 | * cap-weighted 631 | * investment-trust-or-fund 632 | 633 | Returns: 634 | (List): a list of Ticker strings 635 | """ 636 | instrument_list = self.get_url(endpoints.tags(tag))["instruments"] 637 | return [self.get_url(instrument)["symbol"] for instrument in instrument_list] 638 | 639 | ########################################################################### 640 | # GET OPTIONS INFO 641 | ########################################################################### 642 | 643 | def get_options(self, stock, expiration_dates, option_type): 644 | """Get a list (chain) of options contracts belonging to a particular stock 645 | 646 | Args: stock ticker (str), list of expiration dates to filter on (YYYY-MM-DD), and whether or not its a 'put' or a 'call' option type (str). 647 | 648 | Returns: 649 | Options Contracts (List): a list (chain) of contracts for a given underlying equity instrument 650 | """ 651 | instrumentid = self.get_url(self.quote_data(stock)["instrument"])["id"] 652 | if(type(expiration_dates) == list): 653 | _expiration_dates_string = expiration_dates.join(",") 654 | else: 655 | _expiration_dates_string = expiration_dates 656 | chain_id = self.get_url(endpoints.chain(instrumentid))["results"][0]["id"] 657 | return [contract for contract in self.get_url(endpoints.options(chain_id, _expiration_dates_string, option_type))["results"]] 658 | 659 | def get_option_market_data(self, optionid): 660 | """Gets a list of market data for a given optionid. 661 | Args: (str) option id 662 | Returns: dictionary of options market data. 663 | """ 664 | market_data = {} 665 | try: 666 | market_data = self.get_url(endpoints.option_market_data(optionid)) or {} 667 | except requests.exceptions.HTTPError: 668 | raise RH_exception.InvalidOptionId() 669 | return market_data 670 | 671 | def options_owned(self): 672 | options = self.get_url(endpoints.options_base() + "positions/?nonzero=true") 673 | options = options['results'] 674 | return options 675 | 676 | def get_option_marketdata(self, instrument): 677 | info = self.get_url(endpoints.market_data() + "options/?instruments=" + instrument) 678 | return info['results'][0] 679 | 680 | def get_option_chainid(self, symbol): 681 | stock_info = self.get_url(self.endpoints['instruments'] + "?symbol=" + symbol) 682 | stock_id = stock_info['results'][0]['id'] 683 | params = {} 684 | params['equity_instrument_ids'] = stock_id 685 | chains = self.get_url(endpoints.options_base() + "chains/", params = params) 686 | chains = chains['results'] 687 | chain_id = None 688 | 689 | for chain in chains: 690 | if chain['can_open_position'] == True: 691 | chain_id = chain['id'] 692 | 693 | return chain_id 694 | 695 | def get_option_quote(self, arg_dict): 696 | chain_id = self.get_option_chainid(arg_dict.pop('symbol', None)) 697 | arg_dict['chain_id'] = chain_id 698 | option_info = self.get_url(self.endpoints.options_base()+ "instruments/", params = arg_dict) 699 | option_info = option_info['results'] 700 | exp_price_list = [] 701 | 702 | for op in option_info: 703 | mrkt_data = self.get_option_marketdata(op['url']) 704 | op_price = mrkt_data['adjusted_mark_price'] 705 | exp = op['expiration_date'] 706 | exp_price_list.append((exp, op_price)) 707 | 708 | exp_price_list.sort() 709 | 710 | return exp_price_list 711 | 712 | 713 | ########################################################################### 714 | # GET FUNDAMENTALS 715 | ########################################################################### 716 | 717 | def get_fundamentals(self, stock=''): 718 | """Find stock fundamentals data 719 | 720 | Args: 721 | (str): stock ticker 722 | 723 | Returns: 724 | (:obj:`dict`): contents of `fundamentals` endpoint 725 | """ 726 | 727 | #Prompt for stock if not entered 728 | if not stock: # pragma: no cover 729 | stock = input("Symbol: ") 730 | 731 | url = str(endpoints.fundamentals(str(stock.upper()))) 732 | 733 | #Check for validity of symbol 734 | try: 735 | req = requests.get(url, timeout=300) 736 | req.raise_for_status() 737 | data = req.json() 738 | except requests.exceptions.HTTPError: 739 | raise RH_exception.InvalidTickerSymbol() 740 | 741 | 742 | return data 743 | 744 | 745 | def fundamentals(self, stock=''): 746 | """Wrapper for get_fundamentlals function """ 747 | 748 | return self.get_fundamentals(stock) 749 | 750 | 751 | ########################################################################### 752 | # PORTFOLIOS DATA 753 | ########################################################################### 754 | 755 | def portfolios(self): 756 | """Returns the user's portfolio data """ 757 | 758 | req = self.session.get(endpoints.portfolios(), timeout=300) 759 | req.raise_for_status() 760 | 761 | return req.json()['results'][0] 762 | 763 | 764 | def adjusted_equity_previous_close(self): 765 | """Wrapper for portfolios 766 | 767 | Returns: 768 | (float): `adjusted_equity_previous_close` value 769 | 770 | """ 771 | 772 | return float(self.portfolios()['adjusted_equity_previous_close']) 773 | 774 | 775 | def equity(self): 776 | """Wrapper for portfolios 777 | 778 | Returns: 779 | (float): `equity` value 780 | """ 781 | 782 | return float(self.portfolios()['equity']) 783 | 784 | 785 | def equity_previous_close(self): 786 | """Wrapper for portfolios 787 | 788 | Returns: 789 | (float): `equity_previous_close` value 790 | """ 791 | 792 | return float(self.portfolios()['equity_previous_close']) 793 | 794 | 795 | def excess_margin(self): 796 | """Wrapper for portfolios 797 | 798 | Returns: 799 | (float): `excess_margin` value 800 | """ 801 | 802 | return float(self.portfolios()['excess_margin']) 803 | 804 | 805 | def extended_hours_equity(self): 806 | """Wrapper for portfolios 807 | 808 | Returns: 809 | (float): `extended_hours_equity` value 810 | """ 811 | 812 | try: 813 | return float(self.portfolios()['extended_hours_equity']) 814 | except TypeError: 815 | return None 816 | 817 | 818 | def extended_hours_market_value(self): 819 | """Wrapper for portfolios 820 | 821 | Returns: 822 | (float): `extended_hours_market_value` value 823 | """ 824 | 825 | try: 826 | return float(self.portfolios()['extended_hours_market_value']) 827 | except TypeError: 828 | return None 829 | 830 | 831 | def last_core_equity(self): 832 | """Wrapper for portfolios 833 | 834 | Returns: 835 | (float): `last_core_equity` value 836 | """ 837 | 838 | return float(self.portfolios()['last_core_equity']) 839 | 840 | 841 | def last_core_market_value(self): 842 | """Wrapper for portfolios 843 | 844 | Returns: 845 | (float): `last_core_market_value` value 846 | """ 847 | 848 | return float(self.portfolios()['last_core_market_value']) 849 | 850 | 851 | def market_value(self): 852 | """Wrapper for portfolios 853 | 854 | Returns: 855 | (float): `market_value` value 856 | """ 857 | 858 | return float(self.portfolios()['market_value']) 859 | 860 | @login_required 861 | def order_history(self, orderId=None): 862 | """Wrapper for portfolios 863 | Optional Args: add an order ID to retrieve information about a single order. 864 | Returns: 865 | (:obj:`dict`): JSON dict from getting orders 866 | """ 867 | 868 | return self.session.get(endpoints.orders(orderId), timeout=300).json() 869 | @login_required 870 | 871 | def options_order_history(self, orderId=None): 872 | """Wrapper for portfolios 873 | Optional Args: add an order ID to retrieve information about a single order. 874 | Returns: 875 | (:obj:`dict`): JSON dict from getting orders 876 | """ 877 | 878 | return self.session.get(endpoints.options_orders(orderId), timeout=300).json() 879 | 880 | 881 | def dividends(self): 882 | """Wrapper for portfolios 883 | 884 | Returns: 885 | (:obj: `dict`): JSON dict from getting dividends 886 | """ 887 | 888 | return self.session.get(endpoints.dividends(), timeout=300).json() 889 | 890 | 891 | ########################################################################### 892 | # POSITIONS DATA 893 | ########################################################################### 894 | 895 | def positions(self): 896 | """Returns the user's positions data 897 | 898 | Returns: 899 | (:object: `dict`): JSON dict from getting positions 900 | """ 901 | 902 | return self.session.get(endpoints.positions(), timeout=300).json() 903 | 904 | 905 | def securities_owned(self): 906 | """Returns list of securities' symbols that the user has shares in 907 | 908 | Returns: 909 | (:object: `dict`): Non-zero positions 910 | """ 911 | 912 | return self.session.get(endpoints.positions() + '?nonzero=true', timeout=300).json() --------------------------------------------------------------------------------