├── .gitignore ├── LICENSE ├── README.md ├── Robinhood ├── Robinhood.py ├── __init__.py ├── endpoints.py └── exceptions.py ├── gf-export.py ├── requirements.txt ├── run.bat └── troubleshooting.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.db 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | #Vim stuff 54 | *.swp 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | #.DS_Store 66 | .DS_Store 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Brian Searls 5 | Copyright (c) 2015 Josh Fraser 6 | Copyright (c) 2015 Rohan Pai 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robinhood for Google Finance 2 | 3 | https://youtu.be/T17CHHhvsmc 4 | 5 | Here is a pretty good tutorial I found on youtube! It should be sufficient to help most run this program without much experience. 6 | 7 | ### Usage instructions 8 | 9 | #### Windows 10 | All you need to do on windows is download and install the latest version of python (2.7.X or 3.X, probably 3.X as you may run in to fewer issues) [here](https://www.python.org/downloads/). Make sure that when you're installing, select the option to include python in your windows path (it will be a check box before you start the installation). After that, double click the .bat file. This will run both of the commands listed below. It's important to read .bat files in particular, as they have potential to be extremely malicious. Try editing the run.bat file in your favorite text editor and confirm it is only running those two commands. I've also included some useful comments, in case you need help understanding what's going on! 11 | 12 | If you run in to issues during this process, please check out the troubleshooting readme called "troubleshooting.md". I've tried to include as many obvious fixes as possible, but if I missed something please contact me and I'll add it to the list. 13 | 14 | #### Linux/maybe osx 15 | Someone actually wrote a nice article showing how to run this [here](http://ask.xmodulo.com/export-robinhood-transaction-data.html). 16 | 17 | ### Description 18 | 19 | A Python script to export your [Robinhood](https://www.robinhood.com) trades to a .csv file (In a nice, Google Finance friendly format). Based on the [Robinhood library by Rohan Pai](https://github.com/Jamonek/Robinhood) and [Robinhood to CSV by Josh Fraser](https://github.com/joshfraser). 20 | Works on Python 2.7+ and 3.5+ 21 | 22 | ### Install: 23 | pip install -r requests 24 | 25 | ### Run: 26 | python gf-export.py 27 | 28 | I've added the orginal script by josh which is useful in the way of getting more information than normally possible. This will eventually be changed to be more helpful by correcting a few of the original issues, but for now is a good start to get some useful debug information. If you have any issues please run this script as well, and if possible send the results of gf-export.py and debug.py to me with a small description on the issue at hand. 29 | 30 | ### Debug: 31 | python debug.py 32 | -------------------------------------------------------------------------------- /Robinhood/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 | from enum import Enum 8 | 9 | #External dependencies 10 | from six.moves.urllib.parse import unquote # pylint: disable=E0401 11 | from six.moves.urllib.request import getproxies # pylint: disable=E0401 12 | from six.moves import input 13 | 14 | import getpass 15 | import requests 16 | import six 17 | import dateutil 18 | 19 | #Application-specific imports 20 | from . import exceptions as RH_exception 21 | from . import endpoints 22 | 23 | 24 | 25 | class Bounds(Enum): 26 | """Enum for bounds in `historicals` endpoint """ 27 | 28 | REGULAR = 'regular' 29 | EXTENDED = 'extended' 30 | 31 | 32 | class Transaction(Enum): 33 | """Enum for buy/sell orders """ 34 | 35 | BUY = 'buy' 36 | SELL = 'sell' 37 | 38 | 39 | class Robinhood: 40 | """Wrapper class for fetching/parsing Robinhood endpoints """ 41 | 42 | session = None 43 | username = None 44 | password = None 45 | headers = None 46 | auth_token = None 47 | oauth_token = None 48 | 49 | logger = logging.getLogger('Robinhood') 50 | logger.addHandler(logging.NullHandler()) 51 | 52 | 53 | ########################################################################### 54 | # Logging in and initializing 55 | ########################################################################### 56 | 57 | def __init__(self): 58 | self.session = requests.session() 59 | self.session.proxies = getproxies() 60 | self.headers = { 61 | "Accept": "*/*", 62 | "Accept-Encoding": "gzip, deflate", 63 | "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", 64 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 65 | "X-Robinhood-API-Version": "1.0.0", 66 | "Connection": "keep-alive", 67 | "User-Agent": "Robinhood/823 (iPhone; iOS 7.1.2; Scale/2.00)" 68 | } 69 | self.session.headers = self.headers 70 | self.auth_method = self.login_prompt 71 | 72 | def login_required(function): # pylint: disable=E0213 73 | """ Decorator function that prompts user for login if they are not logged in already. Can be applied to any function using the @ notation. """ 74 | def wrapper(self, *args, **kwargs): 75 | if 'Authorization' not in self.headers: 76 | self.auth_method() 77 | return function(self, *args, **kwargs) # pylint: disable=E1102 78 | return wrapper 79 | 80 | def login_prompt(self): # pragma: no cover 81 | """Prompts user for username and password and calls login() """ 82 | 83 | username = input("Username: ") 84 | password = getpass.getpass() 85 | 86 | return self.login(username=username, password=password) 87 | 88 | 89 | def login(self, 90 | username, 91 | password): 92 | """Save and test login info for Robinhood accounts 93 | 94 | Args: 95 | username (str): username 96 | password (str): password 97 | 98 | Returns: 99 | (bool): received valid auth token 100 | 101 | """ 102 | 103 | fields = {'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS', 104 | 'expires_in': 86400, 105 | 'grant_type': 'password', 106 | 'password': password, 107 | 'scope': 'internal', 108 | 'username': username, 109 | } 110 | data = fields 111 | 112 | res = self.session.post(endpoints.login(), data=data) 113 | res = res.json() 114 | 115 | if 'mfa_required' in res: 116 | try: 117 | self.mfa_code = raw_input("MFA code: ").strip() 118 | except: 119 | self.mfa_code = input("MFA code: ").strip() 120 | 121 | fields['mfa_code'] = self.mfa_code 122 | data = fields 123 | 124 | res = self.session.post(endpoints.login(), data=data) 125 | res = res.json() 126 | 127 | try: 128 | self.oauth_token = res['access_token'] 129 | except KeyError: 130 | return res 131 | 132 | self.headers['Authorization'] = 'Bearer ' + self.oauth_token 133 | return True 134 | 135 | 136 | def logout(self): 137 | """Logout from Robinhood 138 | 139 | Returns: 140 | (:obj:`requests.request`) result from logout endpoint 141 | 142 | """ 143 | 144 | try: 145 | req = self.session.post(endpoints.logout(), timeout=15) 146 | req.raise_for_status() 147 | except requests.exceptions.HTTPError as err_msg: 148 | warnings.warn('Failed to log out ' + repr(err_msg)) 149 | 150 | self.headers['Authorization'] = None 151 | self.auth_token = None 152 | 153 | return req 154 | 155 | 156 | ########################################################################### 157 | # GET DATA 158 | ########################################################################### 159 | 160 | def investment_profile(self): 161 | """Fetch investment_profile """ 162 | 163 | res = self.session.get(endpoints.investment_profile(), timeout=15) 164 | res.raise_for_status() # will throw without auth 165 | data = res.json() 166 | 167 | return data 168 | 169 | 170 | def instruments(self, stock): 171 | """Fetch instruments endpoint 172 | 173 | Args: 174 | stock (str): stock ticker 175 | 176 | Returns: 177 | (:obj:`dict`): JSON contents from `instruments` endpoint 178 | """ 179 | 180 | res = self.session.get(endpoints.instruments(), params={'query': stock.upper()}, timeout=15) 181 | res.raise_for_status() 182 | res = res.json() 183 | 184 | # if requesting all, return entire object so may paginate with ['next'] 185 | if (stock == ""): 186 | return res 187 | 188 | return res['results'] 189 | 190 | 191 | def instrument(self, id): 192 | """Fetch instrument info 193 | 194 | Args: 195 | id (str): instrument id 196 | 197 | Returns: 198 | (:obj:`dict`): JSON dict of instrument 199 | """ 200 | url = str(endpoints.instruments()) + str(id) + "/" 201 | 202 | try: 203 | req = requests.get(url, timeout=15) 204 | req.raise_for_status() 205 | data = req.json() 206 | except requests.exceptions.HTTPError: 207 | raise RH_exception.InvalidInstrumentId() 208 | 209 | return data 210 | 211 | 212 | def quote_data(self, stock=''): 213 | """Fetch stock quote 214 | 215 | Args: 216 | stock (str): stock ticker, prompt if blank 217 | 218 | Returns: 219 | (:obj:`dict`): JSON contents from `quotes` endpoint 220 | """ 221 | 222 | url = None 223 | 224 | if stock.find(',') == -1: 225 | url = str(endpoints.quotes()) + str(stock) + "/" 226 | else: 227 | url = str(endpoints.quotes()) + "?symbols=" + str(stock) 228 | 229 | #Check for validity of symbol 230 | try: 231 | req = requests.get(url, timeout=15) 232 | req.raise_for_status() 233 | data = req.json() 234 | except requests.exceptions.HTTPError: 235 | raise RH_exception.InvalidTickerSymbol() 236 | 237 | 238 | return data 239 | 240 | 241 | # We will keep for compatibility until next major release 242 | def quotes_data(self, stocks): 243 | """Fetch quote for multiple stocks, in one single Robinhood API call 244 | 245 | Args: 246 | stocks (list): stock tickers 247 | 248 | Returns: 249 | (:obj:`list` of :obj:`dict`): List of JSON contents from `quotes` endpoint, in the 250 | same order of input args. If any ticker is invalid, a None will occur at that position. 251 | """ 252 | 253 | url = str(endpoints.quotes()) + "?symbols=" + ",".join(stocks) 254 | 255 | try: 256 | req = requests.get(url, timeout=15) 257 | req.raise_for_status() 258 | data = req.json() 259 | except requests.exceptions.HTTPError: 260 | raise RH_exception.InvalidTickerSymbol() 261 | 262 | 263 | return data["results"] 264 | 265 | 266 | def get_quote_list(self, 267 | stock='', 268 | key=''): 269 | """Returns multiple stock info and keys from quote_data (prompt if blank) 270 | 271 | Args: 272 | stock (str): stock ticker (or tickers separated by a comma) 273 | , prompt if blank 274 | key (str): key attributes that the function should return 275 | 276 | Returns: 277 | (:obj:`list`): Returns values from each stock or empty list 278 | if none of the stocks were valid 279 | 280 | """ 281 | 282 | #Creates a tuple containing the information we want to retrieve 283 | def append_stock(stock): 284 | keys = key.split(',') 285 | myStr = '' 286 | for item in keys: 287 | myStr += stock[item] + "," 288 | 289 | return (myStr.split(',')) 290 | 291 | 292 | #Prompt for stock if not entered 293 | if not stock: # pragma: no cover 294 | stock = input("Symbol: ") 295 | 296 | data = self.quote_data(stock) 297 | res = [] 298 | 299 | # Handles the case of multple tickers 300 | if stock.find(',') != -1: 301 | for stock in data['results']: 302 | if stock is None: 303 | continue 304 | res.append(append_stock(stock)) 305 | 306 | else: 307 | res.append(append_stock(data)) 308 | 309 | return res 310 | 311 | 312 | def get_quote(self, stock=''): 313 | """Wrapper for quote_data """ 314 | 315 | data = self.quote_data(stock) 316 | return data["symbol"] 317 | 318 | def get_historical_quotes(self, stock, interval, span, bounds=Bounds.REGULAR): 319 | """Fetch historical data for stock 320 | 321 | Note: valid interval/span configs 322 | interval = 5minute | 10minute + span = day, week 323 | interval = day + span = year 324 | interval = week 325 | TODO: NEEDS TESTS 326 | 327 | Args: 328 | stock (str): stock ticker 329 | interval (str): resolution of data 330 | span (str): length of data 331 | bounds (:enum:`Bounds`, optional): 'extended' or 'regular' trading hours 332 | 333 | Returns: 334 | (:obj:`dict`) values returned from `historicals` endpoint 335 | """ 336 | if type(stock) is str: 337 | stock = [stock] 338 | 339 | if isinstance(bounds, str): # recast to Enum 340 | bounds = Bounds(bounds) 341 | 342 | params = { 343 | 'symbols': ','.join(stock).upper(), 344 | 'interval': interval, 345 | 'span': span, 346 | 'bounds': bounds.name.lower() 347 | } 348 | 349 | res = self.session.get(endpoints.historicals(), params=params, timeout=15) 350 | return res.json() 351 | 352 | 353 | def get_news(self, stock): 354 | """Fetch news endpoint 355 | Args: 356 | stock (str): stock ticker 357 | 358 | Returns: 359 | (:obj:`dict`) values returned from `news` endpoint 360 | """ 361 | 362 | return self.session.get(endpoints.news(stock.upper()), timeout=15).json() 363 | 364 | 365 | def print_quote(self, stock=''): # pragma: no cover 366 | """Print quote information 367 | Args: 368 | stock (str): ticker to fetch 369 | 370 | Returns: 371 | None 372 | """ 373 | 374 | data = self.get_quote_list(stock, 'symbol,last_trade_price') 375 | for item in data: 376 | quote_str = item[0] + ": $" + item[1] 377 | print(quote_str) 378 | self.logger.info(quote_str) 379 | 380 | 381 | def print_quotes(self, stocks): # pragma: no cover 382 | """Print a collection of stocks 383 | 384 | Args: 385 | stocks (:obj:`list`): list of stocks to pirnt 386 | 387 | Returns: 388 | None 389 | """ 390 | 391 | if stocks is None: 392 | return 393 | 394 | for stock in stocks: 395 | self.print_quote(stock) 396 | 397 | 398 | def ask_price(self, stock=''): 399 | """Get asking price for a stock 400 | 401 | Note: 402 | queries `quote` endpoint, dict wrapper 403 | 404 | Args: 405 | stock (str): stock ticker 406 | 407 | Returns: 408 | (float): ask price 409 | """ 410 | 411 | return self.get_quote_list(stock, 'ask_price') 412 | 413 | 414 | def ask_size(self, stock=''): 415 | """Get ask size for a stock 416 | 417 | Note: 418 | queries `quote` endpoint, dict wrapper 419 | 420 | Args: 421 | stock (str): stock ticker 422 | 423 | Returns: 424 | (int): ask size 425 | """ 426 | 427 | return self.get_quote_list(stock, 'ask_size') 428 | 429 | 430 | def bid_price(self, stock=''): 431 | """Get bid price for a stock 432 | 433 | Note: 434 | queries `quote` endpoint, dict wrapper 435 | 436 | Args: 437 | stock (str): stock ticker 438 | 439 | Returns: 440 | (float): bid price 441 | """ 442 | 443 | return self.get_quote_list(stock, 'bid_price') 444 | 445 | 446 | def bid_size(self, stock=''): 447 | """Get bid size for a stock 448 | 449 | Note: 450 | queries `quote` endpoint, dict wrapper 451 | 452 | Args: 453 | stock (str): stock ticker 454 | 455 | Returns: 456 | (int): bid size 457 | """ 458 | 459 | return self.get_quote_list(stock, 'bid_size') 460 | 461 | 462 | def last_trade_price(self, stock=''): 463 | """Get last trade price for a stock 464 | 465 | Note: 466 | queries `quote` endpoint, dict wrapper 467 | 468 | Args: 469 | stock (str): stock ticker 470 | 471 | Returns: 472 | (float): last trade price 473 | """ 474 | 475 | return self.get_quote_list(stock, 'last_trade_price') 476 | 477 | 478 | def previous_close(self, stock=''): 479 | """Get previous closing price for a stock 480 | 481 | Note: 482 | queries `quote` endpoint, dict wrapper 483 | 484 | Args: 485 | stock (str): stock ticker 486 | 487 | Returns: 488 | (float): previous closing price 489 | """ 490 | 491 | return self.get_quote_list(stock, 'previous_close') 492 | 493 | 494 | def previous_close_date(self, stock=''): 495 | """Get previous closing date for a stock 496 | 497 | Note: 498 | queries `quote` endpoint, dict wrapper 499 | 500 | Args: 501 | stock (str): stock ticker 502 | 503 | Returns: 504 | (str): previous close date 505 | """ 506 | 507 | return self.get_quote_list(stock, 'previous_close_date') 508 | 509 | 510 | def adjusted_previous_close(self, stock=''): 511 | """Get adjusted previous closing price for a stock 512 | 513 | Note: 514 | queries `quote` endpoint, dict wrapper 515 | 516 | Args: 517 | stock (str): stock ticker 518 | 519 | Returns: 520 | (float): adjusted previous closing price 521 | """ 522 | 523 | return self.get_quote_list(stock, 'adjusted_previous_close') 524 | 525 | 526 | def symbol(self, stock=''): 527 | """Get symbol for a stock 528 | 529 | Note: 530 | queries `quote` endpoint, dict wrapper 531 | 532 | Args: 533 | stock (str): stock ticker 534 | 535 | Returns: 536 | (str): stock symbol 537 | """ 538 | 539 | return self.get_quote_list(stock, 'symbol') 540 | 541 | 542 | def last_updated_at(self, stock=''): 543 | """Get last update datetime 544 | 545 | Note: 546 | queries `quote` endpoint, dict wrapper 547 | 548 | Args: 549 | stock (str): stock ticker 550 | 551 | Returns: 552 | (str): last update datetime 553 | """ 554 | 555 | return self.get_quote_list(stock, 'last_updated_at') 556 | 557 | 558 | def last_updated_at_datetime(self, stock=''): 559 | """Get last updated datetime 560 | 561 | Note: 562 | queries `quote` endpoint, dict wrapper 563 | `self.last_updated_at` returns time as `str` in format: 'YYYY-MM-ddTHH:mm:ss:000Z' 564 | 565 | Args: 566 | stock (str): stock ticker 567 | 568 | Returns: 569 | (datetime): last update datetime 570 | 571 | """ 572 | 573 | #Will be in format: 'YYYY-MM-ddTHH:mm:ss:000Z' 574 | datetime_string = self.last_updated_at(stock) 575 | result = dateutil.parser.parse(datetime_string) 576 | 577 | return result 578 | 579 | def get_account(self): 580 | """Fetch account information 581 | 582 | Returns: 583 | (:obj:`dict`): `accounts` endpoint payload 584 | """ 585 | 586 | res = self.session.get(endpoints.accounts(), timeout=15) 587 | res.raise_for_status() # auth required 588 | res = res.json() 589 | 590 | return res['results'][0] 591 | 592 | 593 | def get_url(self, url): 594 | """ 595 | Flat wrapper for fetching URL directly 596 | """ 597 | 598 | return self.session.get(url, timeout=15).json() 599 | 600 | def get_popularity(self, stock=''): 601 | """Get the number of robinhood users who own the given stock 602 | 603 | Args: 604 | stock (str): stock ticker 605 | 606 | Returns: 607 | (int): number of users who own the stock 608 | """ 609 | stock_instrument = self.get_url(self.quote_data(stock)["instrument"])["id"] 610 | return self.get_url(endpoints.instruments(stock_instrument, "popularity"))["num_open_positions"] 611 | 612 | def get_tickers_by_tag(self, tag=None): 613 | """Get a list of instruments belonging to a tag 614 | 615 | Args: tag - Tags may include but are not limited to: 616 | * top-movers 617 | * etf 618 | * 100-most-popular 619 | * mutual-fund 620 | * finance 621 | * cap-weighted 622 | * investment-trust-or-fund 623 | 624 | Returns: 625 | (List): a list of Ticker strings 626 | """ 627 | instrument_list = self.get_url(endpoints.tags(tag))["instruments"] 628 | return [self.get_url(instrument)["symbol"] for instrument in instrument_list] 629 | 630 | ########################################################################### 631 | # GET OPTIONS INFO 632 | ########################################################################### 633 | 634 | def get_options(self, stock, expiration_dates, option_type): 635 | """Get a list (chain) of options contracts belonging to a particular stock 636 | 637 | 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). 638 | 639 | Returns: 640 | Options Contracts (List): a list (chain) of contracts for a given underlying equity instrument 641 | """ 642 | instrumentid = self.get_url(self.quote_data(stock)["instrument"])["id"] 643 | if(type(expiration_dates) == list): 644 | _expiration_dates_string = expiration_dates.join(",") 645 | else: 646 | _expiration_dates_string = expiration_dates 647 | chain_id = self.get_url(endpoints.chain(instrumentid))["results"][0]["id"] 648 | return [contract for contract in self.get_url(endpoints.options(chain_id, _expiration_dates_string, option_type))["results"]] 649 | 650 | @login_required 651 | def get_option_market_data(self, optionid): 652 | """Gets a list of market data for a given optionid. 653 | 654 | Args: (str) option id 655 | 656 | Returns: dictionary of options market data. 657 | """ 658 | if not self.oauth_token: 659 | res = self.session.post(endpoints.convert_token(), timeout=15) 660 | res.raise_for_status() 661 | res = res.json() 662 | self.oauth_token = res["access_token"] 663 | self.headers['Authorization'] = 'Bearer ' + self.oauth_token 664 | return self.get_url(endpoints.market_data(optionid)) 665 | 666 | 667 | ########################################################################### 668 | # GET FUNDAMENTALS 669 | ########################################################################### 670 | 671 | def get_fundamentals(self, stock=''): 672 | """Find stock fundamentals data 673 | 674 | Args: 675 | (str): stock ticker 676 | 677 | Returns: 678 | (:obj:`dict`): contents of `fundamentals` endpoint 679 | """ 680 | 681 | #Prompt for stock if not entered 682 | if not stock: # pragma: no cover 683 | stock = input("Symbol: ") 684 | 685 | url = str(endpoints.fundamentals(str(stock.upper()))) 686 | 687 | #Check for validity of symbol 688 | try: 689 | req = requests.get(url, timeout=15) 690 | req.raise_for_status() 691 | data = req.json() 692 | except requests.exceptions.HTTPError: 693 | raise RH_exception.InvalidTickerSymbol() 694 | 695 | 696 | return data 697 | 698 | 699 | def fundamentals(self, stock=''): 700 | """Wrapper for get_fundamentlals function """ 701 | 702 | return self.get_fundamentals(stock) 703 | 704 | 705 | ########################################################################### 706 | # PORTFOLIOS DATA 707 | ########################################################################### 708 | 709 | def portfolios(self): 710 | """Returns the user's portfolio data """ 711 | 712 | req = self.session.get(endpoints.portfolios(), timeout=15) 713 | req.raise_for_status() 714 | 715 | return req.json()['results'][0] 716 | 717 | 718 | def adjusted_equity_previous_close(self): 719 | """Wrapper for portfolios 720 | 721 | Returns: 722 | (float): `adjusted_equity_previous_close` value 723 | 724 | """ 725 | 726 | return float(self.portfolios()['adjusted_equity_previous_close']) 727 | 728 | 729 | def equity(self): 730 | """Wrapper for portfolios 731 | 732 | Returns: 733 | (float): `equity` value 734 | """ 735 | 736 | return float(self.portfolios()['equity']) 737 | 738 | 739 | def equity_previous_close(self): 740 | """Wrapper for portfolios 741 | 742 | Returns: 743 | (float): `equity_previous_close` value 744 | """ 745 | 746 | return float(self.portfolios()['equity_previous_close']) 747 | 748 | 749 | def excess_margin(self): 750 | """Wrapper for portfolios 751 | 752 | Returns: 753 | (float): `excess_margin` value 754 | """ 755 | 756 | return float(self.portfolios()['excess_margin']) 757 | 758 | 759 | def extended_hours_equity(self): 760 | """Wrapper for portfolios 761 | 762 | Returns: 763 | (float): `extended_hours_equity` value 764 | """ 765 | 766 | try: 767 | return float(self.portfolios()['extended_hours_equity']) 768 | except TypeError: 769 | return None 770 | 771 | 772 | def extended_hours_market_value(self): 773 | """Wrapper for portfolios 774 | 775 | Returns: 776 | (float): `extended_hours_market_value` value 777 | """ 778 | 779 | try: 780 | return float(self.portfolios()['extended_hours_market_value']) 781 | except TypeError: 782 | return None 783 | 784 | 785 | def last_core_equity(self): 786 | """Wrapper for portfolios 787 | 788 | Returns: 789 | (float): `last_core_equity` value 790 | """ 791 | 792 | return float(self.portfolios()['last_core_equity']) 793 | 794 | 795 | def last_core_market_value(self): 796 | """Wrapper for portfolios 797 | 798 | Returns: 799 | (float): `last_core_market_value` value 800 | """ 801 | 802 | return float(self.portfolios()['last_core_market_value']) 803 | 804 | 805 | def market_value(self): 806 | """Wrapper for portfolios 807 | 808 | Returns: 809 | (float): `market_value` value 810 | """ 811 | 812 | return float(self.portfolios()['market_value']) 813 | 814 | @login_required 815 | def order_history(self, orderId=None): 816 | """Wrapper for portfolios 817 | Optional Args: add an order ID to retrieve information about a single order. 818 | Returns: 819 | (:obj:`dict`): JSON dict from getting orders 820 | """ 821 | 822 | return self.session.get(endpoints.orders(orderId), timeout=15).json() 823 | 824 | 825 | def dividends(self): 826 | """Wrapper for portfolios 827 | 828 | Returns: 829 | (:obj: `dict`): JSON dict from getting dividends 830 | """ 831 | 832 | return self.session.get(endpoints.dividends(), timeout=15).json() 833 | 834 | 835 | ########################################################################### 836 | # POSITIONS DATA 837 | ########################################################################### 838 | 839 | def positions(self): 840 | """Returns the user's positions data 841 | 842 | Returns: 843 | (:object: `dict`): JSON dict from getting positions 844 | """ 845 | 846 | return self.session.get(endpoints.positions(), timeout=15).json() 847 | 848 | 849 | def securities_owned(self): 850 | """Returns list of securities' symbols that the user has shares in 851 | 852 | Returns: 853 | (:object: `dict`): Non-zero positions 854 | """ 855 | 856 | return self.session.get(endpoints.positions() + '?nonzero=true', timeout=15).json() 857 | 858 | 859 | ########################################################################### 860 | # PLACE ORDER 861 | ########################################################################### 862 | 863 | def place_order(self, 864 | instrument, 865 | quantity=1, 866 | bid_price=0.0, 867 | transaction=None, 868 | trigger='immediate', 869 | order='market', 870 | time_in_force='gfd'): 871 | """Place an order with Robinhood 872 | 873 | Notes: 874 | OMFG TEST THIS PLEASE! 875 | 876 | Just realized this won't work since if type is LIMIT you need to use "price" and if 877 | a STOP you need to use "stop_price". Oops. 878 | Reference: https://github.com/sanko/Robinhood/blob/master/Order.md#place-an-order 879 | 880 | Args: 881 | instrument (dict): the RH URL and symbol in dict for the instrument to be traded 882 | quantity (int): quantity of stocks in order 883 | bid_price (float): price for order 884 | transaction (:enum:`Transaction`): BUY or SELL enum 885 | trigger (:enum:`Trigger`): IMMEDIATE or STOP enum 886 | order (:enum:`Order`): MARKET or LIMIT 887 | time_in_force (:enum:`TIME_IN_FORCE`): GFD or GTC (day or until cancelled) 888 | 889 | Returns: 890 | (:obj:`requests.request`): result from `orders` put command 891 | """ 892 | 893 | if isinstance(transaction, str): 894 | transaction = Transaction(transaction) 895 | 896 | if not bid_price: 897 | bid_price = self.quote_data(instrument['symbol'])['bid_price'] 898 | 899 | payload = { 900 | 'account': self.get_account()['url'], 901 | 'instrument': unquote(instrument['url']), 902 | 'price': float(bid_price), 903 | 'quantity': quantity, 904 | 'side': transaction.name.lower(), 905 | 'symbol': instrument['symbol'], 906 | 'time_in_force': time_in_force.lower(), 907 | 'trigger': trigger, 908 | 'type': order.lower() 909 | } 910 | 911 | #data = 'account=%s&instrument=%s&price=%f&quantity=%d&side=%s&symbol=%s#&time_in_force=gfd&trigger=immediate&type=market' % ( 912 | # self.get_account()['url'], 913 | # urllib.parse.unquote(instrument['url']), 914 | # float(bid_price), 915 | # quantity, 916 | # transaction, 917 | # instrument['symbol'] 918 | #) 919 | 920 | res = self.session.post(endpoints.orders(), data=payload, timeout=15) 921 | res.raise_for_status() 922 | 923 | return res 924 | 925 | 926 | def place_buy_order(self, 927 | instrument, 928 | quantity, 929 | bid_price=0.0): 930 | """Wrapper for placing buy orders 931 | 932 | Args: 933 | instrument (dict): the RH URL and symbol in dict for the instrument to be traded 934 | quantity (int): quantity of stocks in order 935 | bid_price (float): price for order 936 | 937 | Returns: 938 | (:obj:`requests.request`): result from `orders` put command 939 | 940 | """ 941 | 942 | transaction = Transaction.BUY 943 | 944 | return self.place_order(instrument, quantity, bid_price, transaction) 945 | 946 | 947 | def place_sell_order(self, 948 | instrument, 949 | quantity, 950 | bid_price=0.0): 951 | """Wrapper for placing sell orders 952 | 953 | Args: 954 | instrument (dict): the RH URL and symbol in dict for the instrument to be traded 955 | quantity (int): quantity of stocks in order 956 | bid_price (float): price for order 957 | 958 | Returns: 959 | (:obj:`requests.request`): result from `orders` put command 960 | """ 961 | 962 | transaction = Transaction.SELL 963 | 964 | return self.place_order(instrument, quantity, bid_price, transaction) 965 | 966 | # Methods below here are a complete rewrite for buying and selling 967 | # These are new. Use at your own risk! 968 | 969 | def place_market_buy_order(self, 970 | instrument_URL=None, 971 | symbol=None, 972 | time_in_force=None, 973 | quantity=None): 974 | """Wrapper for placing market buy orders 975 | 976 | Notes: 977 | If only one of the instrument_URL or symbol are passed as 978 | arguments the other will be looked up automatically. 979 | 980 | Args: 981 | instrument_URL (str): The RH URL of the instrument 982 | symbol (str): The ticker symbol of the instrument 983 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 984 | quantity (int): Number of shares to buy 985 | 986 | Returns: 987 | (:obj:`requests.request`): result from `orders` put command 988 | """ 989 | return(self.submit_order(order_type='market', 990 | trigger='immediate', 991 | side='buy', 992 | instrument_URL=instrument_URL, 993 | symbol=symbol, 994 | time_in_force=time_in_force, 995 | quantity=quantity)) 996 | 997 | def place_limit_buy_order(self, 998 | instrument_URL=None, 999 | symbol=None, 1000 | time_in_force=None, 1001 | price=None, 1002 | quantity=None): 1003 | """Wrapper for placing limit buy orders 1004 | 1005 | Notes: 1006 | If only one of the instrument_URL or symbol are passed as 1007 | arguments the other will be looked up automatically. 1008 | 1009 | Args: 1010 | instrument_URL (str): The RH URL of the instrument 1011 | symbol (str): The ticker symbol of the instrument 1012 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1013 | price (float): The max price you're willing to pay per share 1014 | quantity (int): Number of shares to buy 1015 | 1016 | Returns: 1017 | (:obj:`requests.request`): result from `orders` put command 1018 | """ 1019 | return(self.submit_order(order_type='limit', 1020 | trigger='immediate', 1021 | side='buy', 1022 | instrument_URL=instrument_URL, 1023 | symbol=symbol, 1024 | time_in_force=time_in_force, 1025 | price=price, 1026 | quantity=quantity)) 1027 | 1028 | def place_stop_loss_buy_order(self, 1029 | instrument_URL=None, 1030 | symbol=None, 1031 | time_in_force=None, 1032 | stop_price=None, 1033 | quantity=None): 1034 | """Wrapper for placing stop loss buy orders 1035 | 1036 | Notes: 1037 | If only one of the instrument_URL or symbol are passed as 1038 | arguments the other will be looked up automatically. 1039 | 1040 | Args: 1041 | instrument_URL (str): The RH URL of the instrument 1042 | symbol (str): The ticker symbol of the instrument 1043 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1044 | stop_price (float): The price at which this becomes a market order 1045 | quantity (int): Number of shares to buy 1046 | 1047 | Returns: 1048 | (:obj:`requests.request`): result from `orders` put command 1049 | """ 1050 | return(self.submit_order(order_type='market', 1051 | trigger='stop', 1052 | side='buy', 1053 | instrument_URL=instrument_URL, 1054 | symbol=symbol, 1055 | time_in_force=time_in_force, 1056 | stop_price=stop_price, 1057 | quantity=quantity)) 1058 | 1059 | def place_stop_limit_buy_order(self, 1060 | instrument_URL=None, 1061 | symbol=None, 1062 | time_in_force=None, 1063 | stop_price=None, 1064 | price=None, 1065 | quantity=None): 1066 | """Wrapper for placing stop limit buy orders 1067 | 1068 | Notes: 1069 | If only one of the instrument_URL or symbol are passed as 1070 | arguments the other will be looked up automatically. 1071 | 1072 | Args: 1073 | instrument_URL (str): The RH URL of the instrument 1074 | symbol (str): The ticker symbol of the instrument 1075 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1076 | stop_price (float): The price at which this becomes a limit order 1077 | price (float): The max price you're willing to pay per share 1078 | quantity (int): Number of shares to buy 1079 | 1080 | Returns: 1081 | (:obj:`requests.request`): result from `orders` put command 1082 | """ 1083 | return(self.submit_order(order_type='limit', 1084 | trigger='stop', 1085 | side='buy', 1086 | instrument_URL=instrument_URL, 1087 | symbol=symbol, 1088 | time_in_force=time_in_force, 1089 | stop_price=stop_price, 1090 | price=price, 1091 | quantity=quantity)) 1092 | 1093 | def place_market_sell_order(self, 1094 | instrument_URL=None, 1095 | symbol=None, 1096 | time_in_force=None, 1097 | quantity=None): 1098 | """Wrapper for placing market sell orders 1099 | 1100 | Notes: 1101 | If only one of the instrument_URL or symbol are passed as 1102 | arguments the other will be looked up automatically. 1103 | 1104 | Args: 1105 | instrument_URL (str): The RH URL of the instrument 1106 | symbol (str): The ticker symbol of the instrument 1107 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1108 | quantity (int): Number of shares to sell 1109 | 1110 | Returns: 1111 | (:obj:`requests.request`): result from `orders` put command 1112 | """ 1113 | return(self.submit_order(order_type='market', 1114 | trigger='immediate', 1115 | side='sell', 1116 | instrument_URL=instrument_URL, 1117 | symbol=symbol, 1118 | time_in_force=time_in_force, 1119 | quantity=quantity)) 1120 | 1121 | def place_limit_sell_order(self, 1122 | instrument_URL=None, 1123 | symbol=None, 1124 | time_in_force=None, 1125 | price=None, 1126 | quantity=None): 1127 | """Wrapper for placing limit sell orders 1128 | 1129 | Notes: 1130 | If only one of the instrument_URL or symbol are passed as 1131 | arguments the other will be looked up automatically. 1132 | 1133 | Args: 1134 | instrument_URL (str): The RH URL of the instrument 1135 | symbol (str): The ticker symbol of the instrument 1136 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1137 | price (float): The minimum price you're willing to get per share 1138 | quantity (int): Number of shares to sell 1139 | 1140 | Returns: 1141 | (:obj:`requests.request`): result from `orders` put command 1142 | """ 1143 | return(self.submit_order(order_type='limit', 1144 | trigger='immediate', 1145 | side='sell', 1146 | instrument_URL=instrument_URL, 1147 | symbol=symbol, 1148 | time_in_force=time_in_force, 1149 | price=price, 1150 | quantity=quantity)) 1151 | 1152 | def place_stop_loss_sell_order(self, 1153 | instrument_URL=None, 1154 | symbol=None, 1155 | time_in_force=None, 1156 | stop_price=None, 1157 | quantity=None): 1158 | """Wrapper for placing stop loss sell orders 1159 | 1160 | Notes: 1161 | If only one of the instrument_URL or symbol are passed as 1162 | arguments the other will be looked up automatically. 1163 | 1164 | Args: 1165 | instrument_URL (str): The RH URL of the instrument 1166 | symbol (str): The ticker symbol of the instrument 1167 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1168 | stop_price (float): The price at which this becomes a market order 1169 | quantity (int): Number of shares to sell 1170 | 1171 | Returns: 1172 | (:obj:`requests.request`): result from `orders` put command 1173 | """ 1174 | return(self.submit_order(order_type='market', 1175 | trigger='stop', 1176 | side='sell', 1177 | instrument_URL=instrument_URL, 1178 | symbol=symbol, 1179 | time_in_force=time_in_force, 1180 | stop_price=stop_price, 1181 | quantity=quantity)) 1182 | 1183 | def place_stop_limit_sell_order(self, 1184 | instrument_URL=None, 1185 | symbol=None, 1186 | time_in_force=None, 1187 | price=None, 1188 | stop_price=None, 1189 | quantity=None): 1190 | """Wrapper for placing stop limit sell orders 1191 | 1192 | Notes: 1193 | If only one of the instrument_URL or symbol are passed as 1194 | arguments the other will be looked up automatically. 1195 | 1196 | Args: 1197 | instrument_URL (str): The RH URL of the instrument 1198 | symbol (str): The ticker symbol of the instrument 1199 | time_in_force (str): 'GFD' or 'GTC' (day or until cancelled) 1200 | stop_price (float): The price at which this becomes a limit order 1201 | price (float): The max price you're willing to get per share 1202 | quantity (int): Number of shares to sell 1203 | 1204 | Returns: 1205 | (:obj:`requests.request`): result from `orders` put command 1206 | """ 1207 | return(self.submit_order(order_type='limit', 1208 | trigger='stop', 1209 | side='sell', 1210 | instrument_URL=instrument_URL, 1211 | symbol=symbol, 1212 | time_in_force=time_in_force, 1213 | stop_price=stop_price, 1214 | price=price, 1215 | quantity=quantity)) 1216 | 1217 | def submit_order(self, 1218 | instrument_URL=None, 1219 | symbol=None, 1220 | order_type=None, 1221 | time_in_force=None, 1222 | trigger=None, 1223 | price=None, 1224 | stop_price=None, 1225 | quantity=None, 1226 | side=None): 1227 | """Submits order to Robinhood 1228 | 1229 | Notes: 1230 | This is normally not called directly. Most programs should use 1231 | one of the following instead: 1232 | 1233 | place_market_buy_order() 1234 | place_limit_buy_order() 1235 | place_stop_loss_buy_order() 1236 | place_stop_limit_buy_order() 1237 | place_market_sell_order() 1238 | place_limit_sell_order() 1239 | place_stop_loss_sell_order() 1240 | place_stop_limit_sell_order() 1241 | 1242 | Args: 1243 | instrument_URL (str): the RH URL for the instrument 1244 | symbol (str): the ticker symbol for the instrument 1245 | order_type (str): 'MARKET' or 'LIMIT' 1246 | time_in_force (:enum:`TIME_IN_FORCE`): GFD or GTC (day or 1247 | until cancelled) 1248 | trigger (str): IMMEDIATE or STOP enum 1249 | price (float): The share price you'll accept 1250 | stop_price (float): The price at which the order becomes a 1251 | market or limit order 1252 | quantity (int): The number of shares to buy/sell 1253 | side (str): BUY or sell 1254 | 1255 | Returns: 1256 | (:obj:`requests.request`): result from `orders` put command 1257 | """ 1258 | 1259 | # Start with some parameter checks. I'm paranoid about $. 1260 | if(instrument_URL is None): 1261 | if(symbol is None): 1262 | raise(ValueError('Neither instrument_URL nor symbol were passed to submit_order')) 1263 | instrument_URL = self.instruments(symbol)[0]['url'] 1264 | 1265 | if(symbol is None): 1266 | symbol = self.session.get(instrument_URL, timeout=15).json()['symbol'] 1267 | 1268 | if(side is None): 1269 | raise(ValueError('Order is neither buy nor sell in call to submit_order')) 1270 | 1271 | if(order_type is None): 1272 | if(price is None): 1273 | if(stop_price is None): 1274 | order_type = 'market' 1275 | else: 1276 | order_type = 'limit' 1277 | 1278 | symbol = str(symbol).upper() 1279 | order_type = str(order_type).lower() 1280 | time_in_force = str(time_in_force).lower() 1281 | trigger = str(trigger).lower() 1282 | side = str(side).lower() 1283 | 1284 | if(order_type != 'market') and (order_type != 'limit'): 1285 | raise(ValueError('Invalid order_type in call to submit_order')) 1286 | 1287 | if(order_type == 'limit'): 1288 | if(price is None): 1289 | raise(ValueError('Limit order has no price in call to submit_order')) 1290 | if(price <= 0): 1291 | raise(ValueError('Price must be positive number in call to submit_order')) 1292 | 1293 | if(trigger == 'stop'): 1294 | if(stop_price is None): 1295 | raise(ValueError('Stop order has no stop_price in call to submit_order')) 1296 | if(price <= 0): 1297 | raise(ValueError('Stop_price must be positive number in call to submit_order')) 1298 | 1299 | if(stop_price is not None): 1300 | if(trigger != 'stop'): 1301 | raise(ValueError('Stop price set for non-stop order in call to submit_order')) 1302 | 1303 | if(price is None): 1304 | if(order_type == 'limit'): 1305 | raise(ValueError('Limit order has no price in call to submit_order')) 1306 | 1307 | if(price is not None): 1308 | if(order_type.lower() == 'market'): 1309 | raise(ValueError('Market order has price limit in call to submit_order')) 1310 | 1311 | price = float(price) 1312 | 1313 | if(quantity is None): 1314 | raise(ValueError('No quantity specified in call to submit_order')) 1315 | 1316 | quantity = int(quantity) 1317 | 1318 | if(quantity <= 0): 1319 | raise(ValueError('Quantity must be positive number in call to submit_order')) 1320 | 1321 | payload = {} 1322 | 1323 | for field, value in [ 1324 | ('account', self.get_account()['url']), 1325 | ('instrument', instrument_URL), 1326 | ('symbol', symbol), 1327 | ('type', order_type), 1328 | ('time_in_force', time_in_force), 1329 | ('trigger', trigger), 1330 | ('price', price), 1331 | ('stop_price', stop_price), 1332 | ('quantity', quantity), 1333 | ('side', side) 1334 | ]: 1335 | if(value is not None): 1336 | payload[field] = value 1337 | 1338 | res = self.session.post(endpoints.orders(), data=payload, timeout=15) 1339 | res.raise_for_status() 1340 | 1341 | return res 1342 | 1343 | ############################## 1344 | # CANCEL ORDER 1345 | ############################## 1346 | 1347 | def cancel_order( 1348 | self, 1349 | order_id 1350 | ): 1351 | """ 1352 | Cancels specified order and returns the response (results from `orders` command). 1353 | If order cannot be cancelled, `None` is returned. 1354 | 1355 | Args: 1356 | order_id (str): Order ID that is to be cancelled or order dict returned from 1357 | order get. 1358 | Returns: 1359 | (:obj:`requests.request`): result from `orders` put command 1360 | """ 1361 | if order_id is str: 1362 | try: 1363 | order = self.session.get(self.endpoints['orders'] + order_id, timeout=15).json() 1364 | except (requests.exceptions.HTTPError) as err_msg: 1365 | raise ValueError('Failed to get Order for ID: ' + order_id 1366 | + '\n Error message: '+ repr(err_msg)) 1367 | else: 1368 | raise ValueError('Cancelling orders requires a valid order_id string') 1369 | 1370 | if order.get('cancel') is not None: 1371 | try: 1372 | res = self.session.post(order['cancel'], timeout=15) 1373 | res.raise_for_status() 1374 | except (requests.exceptions.HTTPError) as err_msg: 1375 | raise ValueError('Failed to cancel order ID: ' + order_id 1376 | + '\n Error message: '+ repr(err_msg)) 1377 | return None 1378 | 1379 | # Order type cannot be cancelled without a valid cancel link 1380 | else: 1381 | raise ValueError('Unable to cancel order ID: ' + order_id) 1382 | 1383 | return res 1384 | -------------------------------------------------------------------------------- /Robinhood/__init__.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | if six.PY3: 4 | from Robinhood.Robinhood import Robinhood 5 | else: 6 | from Robinhood import Robinhood 7 | import exceptions as RH_exception 8 | -------------------------------------------------------------------------------- /Robinhood/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 password_reset(): 51 | return "https://api.robinhood.com/password_reset/request/" 52 | 53 | def portfolios(): 54 | return "https://api.robinhood.com/portfolios/" 55 | 56 | def positions(): 57 | return "https://api.robinhood.com/positions/" 58 | 59 | def quotes(): 60 | return "https://api.robinhood.com/quotes/" 61 | 62 | def historicals(): 63 | return "https://api.robinhood.com/quotes/historicals/" 64 | 65 | def document_requests(): 66 | return "https://api.robinhood.com/upload/document_requests/" 67 | 68 | def user(): 69 | return "https://api.robinhood.com/user/" 70 | 71 | def watchlists(): 72 | return "https://api.robinhood.com/watchlists/" 73 | 74 | def news(stock): 75 | return "https://api.robinhood.com/midlands/news/{_stock}/".format(_stock=stock) 76 | 77 | def fundamentals(stock): 78 | return "https://api.robinhood.com/fundamentals/{_stock}/".format(_stock=stock) 79 | 80 | def tags(tag=None): 81 | ''' 82 | Returns endpoint with tag concatenated. 83 | ''' 84 | return "https://api.robinhood.com/midlands/tags/tag/{_tag}/".format(_tag=tag) 85 | 86 | def chain(instrumentid): 87 | return "https://api.robinhood.com/options/chains/?equity_instrument_ids={_instrumentid}".format(_instrumentid=instrumentid) 88 | 89 | def options(chainid, dates, option_type): 90 | return "https://api.robinhood.com/options/instruments/?chain_id={_chainid}&expiration_dates={_dates}&state=active&tradability=tradable&type={_type}".format(_chainid=chainid, _dates=dates, _type=option_type) 91 | 92 | def market_data(optionid): 93 | return "https://api.robinhood.com/marketdata/options/{_optionid}/".format(_optionid=optionid) 94 | 95 | def convert_token(): 96 | return "https://api.robinhood.com/oauth2/migrate_token/" 97 | -------------------------------------------------------------------------------- /Robinhood/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 | -------------------------------------------------------------------------------- /gf-export.py: -------------------------------------------------------------------------------- 1 | from Robinhood import Robinhood 2 | import shelve 3 | import json 4 | import csv 5 | 6 | logged_in = False 7 | 8 | def get_symbol_from_instrument_url(rb_client, url, db): 9 | instrument = {} 10 | if type(url) != str: 11 | url = url.encode('utf8') 12 | if url in db: 13 | instrument = db[url] 14 | else: 15 | db[url] = fetch_json_by_url(rb_client, url) 16 | instrument = db[url] 17 | return instrument['symbol'] 18 | 19 | 20 | def fetch_json_by_url(rb_client, url): 21 | return rb_client.session.get(url).json() 22 | 23 | 24 | def order_item_info(order, rb_client, db): 25 | #side: .side, price: .average_price, shares: .cumulative_quantity, instrument: .instrument, date : .last_transaction_at 26 | symbol = get_symbol_from_instrument_url(rb_client, order['instrument'], db) 27 | return { 28 | 'Transaction Type': order['side'], 29 | 'Purchase price per share': order['average_price'], 30 | 'Shares': order['cumulative_quantity'], 31 | 'Symbol': symbol, 32 | 'Date Purchased': order['last_transaction_at'], 33 | 'Commission': order['fees'] 34 | } 35 | 36 | 37 | def get_all_history_orders(rb_client): 38 | orders = [] 39 | past_orders = rb_client.order_history() 40 | orders.extend(past_orders['results']) 41 | while past_orders['next']: 42 | print("{} order fetched".format(len(orders))) 43 | next_url = past_orders['next'] 44 | past_orders = fetch_json_by_url(rb_client, next_url) 45 | orders.extend(past_orders['results']) 46 | print("{} order fetched".format(len(orders))) 47 | return orders 48 | 49 | robinhood = Robinhood() 50 | 51 | # fetch order history and related metadata from the Robinhood API 52 | past_orders = get_all_history_orders(robinhood) 53 | 54 | instruments_db = shelve.open('instruments.db') 55 | orders = [order_item_info(order, robinhood, instruments_db) for order in past_orders] 56 | keys = ['Purchase price per share', 'Date Purchased', 'Commission', 'Shares', 'Symbol', 'Transaction Type'] 57 | with open('orders.csv', 'w') as output_file: 58 | dict_writer = csv.DictWriter(output_file, keys) 59 | dict_writer.writeheader() 60 | dict_writer.writerows(orders) 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | six 3 | requests 4 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | ::This first line runs pip, which is a package manager available for python It downloads and installs necessary python libraries, and in our case we're installing a library named "requests". 2 | pip install requests 3 | pip install six 4 | pip install python-dateutils 5 | 6 | ::This line takes whichever version of python you have installed, and compiles the script gf-export.py. It will then execute it, and then you'll enter your info. 7 | python gf-export.py 8 | -------------------------------------------------------------------------------- /troubleshooting.md: -------------------------------------------------------------------------------- 1 | I've tried to organize this in order of most frequent issues. I'm assuming you've installed python 3.x, and if not just install it now so we're all on the same page. It's alright to have both python 2.x and 3.x installed simultaneously. 2 | 3 | 1) I double click on the bat file but it dissapears instantly! 4 | - This means that the first command had some error and could not execute, so the batch job ended instantly. This is most likely a cause of both python and pip not being added to the path (make sure you check the box during installation). To test, type cmd into your windows search bar, and open command prompt. Type "python" without the quotes, it should bring you to a >>. If it says it is not a recognized internal or external blah blah blah, that means it is certainly not added to your path. Try doing the same for "pip" without quotes. 5 | These are programs you're trying to run from command line, and the only way windows will know the location is if you add them to some list, which I've been calling the path. Try reinstalling python 3 with the box ticked, or if you're brave you can try to add python and pip to your path (try google, it's a very common problem!). After reinstalling, make sure you close and open cmd again so your cmd instance has the latest path from windows and try typing them in again. If that works, try to run the .bat file. 6 | 7 | 2) There are a ton of errors! HELP! 8 | - It's hard to know what kind of errors you got without reading them, but I have a hunch it may say "Access Denied" in some of them. If it does, try running your .bat file in administrator mode. 9 | If they don't, please send them to me and I'll take a look and add them as a clause here. There's not much that can go wrong, so you shouldn't have many issues actually running the script, getting there is usually the hard part. 10 | 11 | Please feel free to contribute to this list by either emailing me, or if you're brave enough fork this to your own repo, edit and remerge. I'll either approve or leave some feedback. 12 | --------------------------------------------------------------------------------