├── .gitignore ├── .pylintrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __init__.py ├── cancel.py ├── cancel_crypto.py ├── cancel_options.py ├── disable_mfa.py ├── download_documents.py ├── download_history.py ├── download_portfolio.py ├── enable_mfa.py ├── login.py ├── logout.py ├── order.py ├── order_crypto.py ├── order_options.py ├── prepare_sentiment.py ├── requirements.txt ├── robinhood ├── RobinhoodCachedClient.py ├── RobinhoodClient.py ├── RobinhoodPortfolio.py ├── __init__.py ├── certs │ ├── _.apexclearing.com.pem │ ├── _.s3.amazonaws.com │ ├── all.pem │ ├── analytics.robinhood.com.pem │ ├── api.robinhood.com.pem │ └── nummus.robinhood.com.pem ├── exceptions.py └── util.py ├── setup.py ├── show_crypto_quote.py ├── show_interesting_stocks.py ├── show_options_discoveries.py ├── show_options_quote.py ├── show_pending_options_orders.py ├── show_pending_orders.py ├── show_potentials.py └── show_quote.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.robinhood 2 | /*.csv 3 | /*.pdf 4 | /*.json 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Scrapy stuff: 59 | .scrapy 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # pyenv 68 | .python-version 69 | 70 | # dotenv 71 | .env 72 | 73 | # virtualenv 74 | .venv 75 | venv/ 76 | ENV/ 77 | 78 | # mkdocs documentation 79 | /site 80 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [pylint] 2 | indent-string=' ' 3 | disable=fixme 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you would like to contribute anything: 2 | 3 | 1. Above anything else, look at the code around where you're contributing and follow the existing patterns. 4 | 2. If you would like to improve the existing patterns, do it as a discreet commit and make the changes everywhere. 5 | 3. Feel free to contribute to both the base client code and the high level scripts, especially if you want to use them. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # robinhood-python 🗠 2 | Robinhood module with convenience scripts for automating common activites (downloading entire history, trading, etc.) 3 | 4 | Some current caveats: 5 | * Development has been in python 3, I'm not taking much care to keep python 2 support at the moment. 6 | * The scripts currently default to extreme caching policies, use --live to guarantee most recent data. 7 | * Much of the code will assert (or not) in scenarioes where states and paging is involved where they haven't been handled correctly yet 8 | 9 | ## Security 10 | 11 | * Certificate pinning is used to block against MITM attacks 12 | 13 | ## Module 14 | 15 | * [RobinhoodClient](robinhood/RobinhoodClient.py) 16 | * Client that handles getting data from the Robinhood APIs 17 | * [RobinhoodCachedClient](robinhood/RobinhoodCachedClient.py) 18 | * Client that handles caching on top of the normal client 19 | * [RobinhoodPortfolio](robinhood/RobinhoodPortfolio.py) 20 | * Utility to help process an entire portfolio in a consistent manner 21 | 22 | ## Scripts 23 | 24 | ### Account 25 | 26 | * [login.py](login.py) 27 | * Forces a new login and caches the token. Note most scripts will do this 28 | automatically so you usually don't need to call this. 29 | * [logout.py](logout.py) 30 | * Invalidates the current auth token and deletes the cached token 31 | * [enable_mfa.py](enable_mfa.py) [app|sms] 32 | * Enables MFA for login 33 | * [disable_mfa.py](disable_mfa.py) 34 | * Disables MFA for login 35 | 36 | ### Portfolio management 37 | 38 | * [download_portfolio.py](download_portfolio.py) [--live] 39 | * Current positions with various stats 40 | * [download_history.py](download_history.py) [--live] 41 | * Downloads all account history (orders, dividends, transfers, rewards, margin, etc.) 42 | * [download_documents.py](download_documents.py) [--live] 43 | * Documents (including PDFs) that you've received 44 | 45 | ### Stocks 46 | 47 | #### Purchasing stocks 48 | 49 | * [show_quote.py](show_quote.py) AMZN [--live] 50 | * Displays the latest stock quote for the given symbol along with auxilary info 51 | * [order.py](order.py) [market|limit] [buy|sell] SYMBOL QUANTITY PRICE [--no-cancel] 52 | * Prints the quote for the given symbol, confirms, and places an order 53 | * [show_pending_orders.py](show_pending_orders.py) 54 | * Displays any outstanding stock orders along with position information 55 | * [cancel.py](cancel.py) ORDER_ID... 56 | * Cancels one or more order ids given, or all pending orders if none given 57 | * [show_potentials.py](show_potentials.py) 58 | * Show stocks and some stats to help decide on positions to push forward on. 59 | * Note this is basically only useful to myself ATM. 60 | * [show_interesting_stocks.py](show_interesting_stocks.py) 61 | * Show stocks that are on various lists 62 | * 10 popular S&P 500 stocks with Robinhood users 63 | * S&P 500 top movers up and down 64 | * Top 10 and 100 popular sticks with Robinhood users 65 | * This script is kind of a mess and mostly just a raw dump. 66 | 67 | ### Options 68 | 69 | #### Purchasing options 70 | 71 | * [show_options_discoveries.py](show_options_quote.py) AMZN [--live] 72 | * Displays robinhood's options suggestions for the given symbol (pretty raw for now) 73 | * [show_options_quote.py](show_options_quote.py) AMZN [--type=call|put] [--date 2018-05-21] [--strike=55] [--live] 74 | * Displays the quote for the given options contract (pretty raw for now) 75 | * Can do things like get all $55 puts, get all puts on 2 dates, etc. 76 | * [order_options.py](order_options.py) [market|limit] [buy|sell] SYMBOL DATE STRIKE [call|put] QUANTITY PRICE 77 | * Places an options order 78 | * [show_pending_options_orders.py](show_pending_options_orders.py) 79 | * Displays any outstanding options orders 80 | * [cancel_options.py](cancel_options.py) ORDER_ID... 81 | * Cancels one or more options order ids given, or all pending options orders if none given 82 | 83 | ### Cryptocurrency 84 | 85 | #### Purchasing cryptocurrency 86 | 87 | * [show_crypto_quote.py](show_crypto_quote.py) [-s BTCUSD] [--live] 88 | * Displays a quote for the given cryptocurrencies or all crypto currencies when none given. 89 | * [order_crypto.py](order_crypto.py) [market|limit] [buy|sell] SYMBOL QUANTITY PRICE 90 | * Places some cryptocurrency 91 | * [show_pending_crypto_orders.py](show_pending_crypto_orders.py) 92 | * Displays any outstanding crypto orders 93 | * [cancel_crypto.py](cancel_crypto.py) ORDER_ID... 94 | * Cancels one or more crypto order ids given, or all pending crypto orders if none given 95 | 96 | ## Legal 97 | 98 | * This library may have bugs which could result in financial consequences, you are responsible for anything you execute. Inspect the underlying code if you want to be sure it's doing what you think it should be doing. 99 | * I am not affiliated with Robinhood and this library uses an undocumented API. If you have any questions about them, contact Robinhood directly: https://robinhood.com/ 100 | * By using this library, you understand that you are not to charge or make any money through advertisements or fees. Until Robinhood releases an official API with official guidance, this is only to be used for non-profit activites. I am not responsible if Robinhood cancels your account because of misuse of this library. 101 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mstrum/robinhood-python/ef02ee04f2527d6568b48fae444bc22740694a3d/__init__.py -------------------------------------------------------------------------------- /cancel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | 8 | # Set up the client 9 | client = RobinhoodCachedClient() 10 | client.login() 11 | 12 | def cancel_orders(order_ids): 13 | 14 | print('') 15 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 16 | confirm = input('Cancel {} order{}? [N/y] '.format(len(order_ids), 's' if len(order_ids) > 1 else '')) 17 | if confirm not in ['y', 'yes']: 18 | print('Bailed out!') 19 | exit() 20 | 21 | for order_id in order_ids: 22 | cancelled_order = client.cancel_order(order_id) 23 | print('Cancelled order {}'.format(order_id)) 24 | 25 | 26 | if __name__ == '__main__': 27 | parser = argparse.ArgumentParser(description='Cancel orders') 28 | parser.add_argument('order_ids', nargs='*', help='The order ids to cancel') 29 | args = parser.parse_args() 30 | 31 | order_ids = args.order_ids 32 | if not order_ids: 33 | orders = client.get_orders(cache_mode=FORCE_LIVE) 34 | order_ids = [order['id'] for order in orders if order['state'] in ['queued', 'confirmed']] 35 | 36 | if not order_ids: 37 | print('Nothing to see here... Move along.') 38 | exit() 39 | 40 | cancel_orders(order_ids) 41 | -------------------------------------------------------------------------------- /cancel_crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | 8 | # Set up the client 9 | client = RobinhoodCachedClient() 10 | client.login() 11 | 12 | def cancel_crypto_orders(order_ids): 13 | print('') 14 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 15 | confirm = input('Cancel {} order{}? [N/y] '.format(len(order_ids), 's' if len(order_ids) > 1 else '')) 16 | if confirm not in ['y', 'yes']: 17 | print('Bailed out!') 18 | exit() 19 | 20 | for order_id in order_ids: 21 | cancelled_order = client.cancel_crypto_order(order_id) 22 | print('Cancelled order {}'.format(order_id)) 23 | 24 | 25 | if __name__ == '__main__': 26 | parser = argparse.ArgumentParser(description='Cancel crypto orders') 27 | parser.add_argument('order_ids', nargs='*', help='The crypto order ids to cancel') 28 | args = parser.parse_args() 29 | 30 | order_ids = args.order_ids 31 | if not order_ids: 32 | orders = client.get_crypto_orders() 33 | order_ids = [order['id'] for order in orders if order['state'] in ['queued', 'confirmed']] 34 | 35 | if not order_ids: 36 | print('Nothing to see here... Move along.') 37 | exit() 38 | 39 | cancel_crypto_orders(order_ids) 40 | -------------------------------------------------------------------------------- /cancel_options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | 8 | # Set up the client 9 | client = RobinhoodCachedClient() 10 | client.login() 11 | 12 | def cancel_options_orders(order_ids): 13 | 14 | print('') 15 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 16 | confirm = input('Cancel {} order{}? [N/y] '.format(len(order_ids), 's' if len(order_ids) > 1 else '')) 17 | if confirm not in ['y', 'yes']: 18 | print('Bailed out!') 19 | exit() 20 | 21 | for order_id in order_ids: 22 | cancelled_order = client.cancel_options_order(order_id) 23 | print('Cancelled order {}'.format(order_id)) 24 | 25 | 26 | if __name__ == '__main__': 27 | parser = argparse.ArgumentParser(description='Cancel options orders') 28 | parser.add_argument('order_ids', nargs='*', help='The options order ids to cancel') 29 | args = parser.parse_args() 30 | 31 | order_ids = args.order_ids 32 | if not order_ids: 33 | orders = client.get_options_orders() 34 | order_ids = [order['id'] for order in orders if order['state'] in ['queued', 'confirmed']] 35 | 36 | if not order_ids: 37 | print('Nothing to see here... Move along.') 38 | exit() 39 | 40 | cancel_options_orders(order_ids) 41 | -------------------------------------------------------------------------------- /disable_mfa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | 8 | # Set up the client 9 | client = RobinhoodCachedClient() 10 | client.login() 11 | 12 | mfa_details = client.get_mfa() 13 | if not mfa_details['enabled']: 14 | print('It looks like MFA is already disabled, nothing to do here.') 15 | exit() 16 | else: 17 | confirm = input('Are you sure you want to disable MFA? [N/y] ') 18 | if confirm not in ['y', 'yes']: 19 | print('Bailed out!') 20 | exit() 21 | 22 | client.remove_mfa() 23 | print('Disabled MFA :( Try me again soon.') 24 | -------------------------------------------------------------------------------- /download_documents.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import csv 5 | import time 6 | 7 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 8 | 9 | # Set up the client 10 | client = RobinhoodCachedClient() 11 | client.login() 12 | 13 | 14 | def download_documents(cache_mode): 15 | with open('documents.csv', 'w', newline='') as csv_file: 16 | fieldnames = ['document_id', 'date', 'type', 'path'] 17 | csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) 18 | csv_writer.writeheader() 19 | 20 | for document in client.get_documents(cache_mode=cache_mode): 21 | document_id = document['id'] 22 | document_type = document['type'] 23 | document_date = document['date'] 24 | 25 | # Try to not get throttled, there isn't a batch API 26 | time.sleep(1) 27 | contents = client.download_document_by_id(document_id) 28 | pdf_path = 'document_{}.pdf'.format(document_id) 29 | with open(pdf_path, 'wb') as document_pdf_file: 30 | document_pdf_file.write(contents) 31 | 32 | csv_writer.writerow({ 33 | 'document_id': document_id, 34 | 'date': document_date, 35 | 'type': document_type, 36 | 'path': pdf_path, 37 | }) 38 | 39 | 40 | if __name__ == '__main__': 41 | parser = argparse.ArgumentParser(description='Download a list of your documents') 42 | parser.add_argument( 43 | '--live', 44 | action='store_true', 45 | help='Force to not use cache for APIs where values change' 46 | ) 47 | args = parser.parse_args() 48 | download_documents(FORCE_LIVE if args.live else CACHE_FIRST) 49 | -------------------------------------------------------------------------------- /download_history.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | import argparse 5 | import csv 6 | 7 | #import logging 8 | #logging.basicConfig(level=logging.DEBUG) 9 | 10 | from dateutil.parser import parse 11 | import pytz 12 | 13 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 14 | from robinhood.util import get_last_id_from_url 15 | 16 | # Set up the client 17 | client = RobinhoodCachedClient() 18 | client.login() 19 | 20 | def add_margin(csv_writer, cache_mode): 21 | account = client.get_account(cache_mode=cache_mode) 22 | unallocated_margin_cash = Decimal(account['margin_balances']['unallocated_margin_cash']) 23 | margin_limit = Decimal(account['margin_balances']['margin_limit']) 24 | updated_at = parse(account['margin_balances']['updated_at']).astimezone(pytz.timezone('US/Pacific')).date() 25 | if margin_limit == 0: 26 | return 27 | 28 | used_margin = margin_limit - unallocated_margin_cash 29 | 30 | csv_writer.writerow({ 31 | 'symbol': '', 32 | 'name': 'Robinhood Gold', 33 | 'type': 'margin', 34 | 'side': 'used', 35 | 'quantity': 1, 36 | 'price': used_margin, 37 | 'amount': used_margin, 38 | 'date': updated_at, 39 | 'fees': 0, 40 | }) 41 | csv_writer.writerow({ 42 | 'symbol': '', 43 | 'name': 'Robinhood Gold', 44 | 'type': 'margin', 45 | 'side': 'available', 46 | 'quantity': 1, 47 | 'price': '{:.2f}'.format(unallocated_margin_cash), 48 | 'amount': '{:.2f}'.format(unallocated_margin_cash), 49 | 'date': updated_at, 50 | 'fees': 0, 51 | }) 52 | 53 | def add_subscription_fees(csv_writer, cache_mode): 54 | for subscription_fee in client.get_subscription_fees(cache_mode=cache_mode): 55 | amount = Decimal(subscription_fee['amount']) 56 | created_at = parse(subscription_fee['created_at']).astimezone(pytz.timezone('US/Pacific')).date() 57 | assert not subscription_fee['refunds'] 58 | assert float(subscription_fee['credit']) == 0.0 59 | assert float(subscription_fee['carry_forward_credit']) == 0.0 60 | 61 | csv_writer.writerow({ 62 | 'symbol': '', 63 | 'name': 'Robinhood Gold', 64 | 'type': 'subscription_fee', 65 | 'side': 'paid', 66 | 'quantity': 1, 67 | 'price': amount, 68 | 'amount': amount, 69 | 'date': created_at, 70 | 'fees': 0, 71 | }) 72 | 73 | def add_transfers(csv_writer, cache_mode): 74 | for transfer in client.get_ach_transfers(cache_mode=cache_mode): 75 | transfer_amount = Decimal(transfer['amount']) 76 | early_access_amount = Decimal(transfer['early_access_amount']) 77 | updated_at = parse(transfer['updated_at']).astimezone(pytz.timezone('US/Pacific')).date() 78 | amount = early_access_amount or transfer_amount 79 | direction = 'deposit_early_access' if early_access_amount else transfer['direction'] 80 | 81 | relationship = client.get_ach_relationship_by_id(get_last_id_from_url(transfer['ach_relationship']), cache_mode=cache_mode) 82 | 83 | csv_writer.writerow({ 84 | 'symbol': '', 85 | 'name': relationship['bank_account_nickname'], 86 | 'type': 'transfer', 87 | 'side': direction, 88 | 'quantity': 1, 89 | 'price': amount, 90 | 'amount': amount, 91 | 'date': updated_at, 92 | 'fees': 0, 93 | }) 94 | 95 | 96 | def add_rewards(csv_writer, cache_mode): 97 | referrals = client.get_referrals(cache_mode=cache_mode) 98 | 99 | instrument_ids = [ 100 | get_last_id_from_url(r['reward']['stocks'][0]['instrument_url']) 101 | for r in referrals if r['reward']['stocks'] 102 | ] 103 | instruments = client.get_instruments(instrument_ids) 104 | instrument_by_id = {i['id']: i for i in instruments} 105 | 106 | for referral in referrals: 107 | direction = referral['direction'] 108 | if direction != 'from': 109 | continue 110 | if not referral['reward']['stocks']: 111 | continue 112 | assert len(referral['reward']['stocks']) == 1 113 | if referral['reward']['stocks'][0]['state'] != 'granted': 114 | continue 115 | 116 | cost_basis = Decimal(referral['reward']['stocks'][0]['cost_basis']) 117 | quantity = int(referral['reward']['stocks'][0]['quantity']) 118 | updated_at = parse(referral['updated_at']).astimezone(pytz.timezone('US/Pacific')).date() 119 | 120 | instrument_id = get_last_id_from_url(referral['reward']['stocks'][0]['instrument_url']) 121 | instrument = instrument_by_id[instrument_id] 122 | name = instrument['simple_name'] or instrument['name'] 123 | symbol = instrument['symbol'] 124 | 125 | csv_writer.writerow({ 126 | 'symbol': symbol, 127 | 'name': name, 128 | 'type': 'reward', 129 | 'side': 'receive', 130 | 'quantity': quantity, 131 | 'price': '{:.2f}'.format(cost_basis), 132 | 'amount': '{:.2f}'.format(quantity * cost_basis), 133 | 'date': updated_at.isoformat(), 134 | 'fees': 0, 135 | }) 136 | 137 | 138 | def add_orders(csv_writer, cache_mode): 139 | orders = client.get_orders(cache_mode=cache_mode) 140 | 141 | instrument_ids = [get_last_id_from_url(o['instrument']) for o in orders] 142 | instruments = client.get_instruments(instrument_ids) 143 | instrument_by_id = {i['id']: i for i in instruments} 144 | 145 | for order in orders: 146 | order_id = order['id'] 147 | state = order['state'] 148 | 149 | if state != 'filled': 150 | if state not in ['queued', 'confirmed', 'cancelled']: 151 | print('Skipping order {} with state {} that may need to be handled...'.format(order_id, state)) 152 | continue 153 | 154 | fees = Decimal(order['fees']) 155 | side = order['side'] 156 | 157 | instrument_id = get_last_id_from_url(order['instrument']) 158 | instrument = instrument_by_id[instrument_id] 159 | name = instrument['simple_name'] or instrument['name'] 160 | symbol = instrument['symbol'] 161 | 162 | for execution in order['executions']: 163 | price = Decimal(execution['price']) 164 | quantity = int(float(execution['quantity'])) 165 | amount = quantity * price 166 | transaction_on = parse(execution['timestamp']).astimezone(pytz.timezone('US/Pacific')).date() 167 | 168 | csv_writer.writerow({ 169 | 'symbol': symbol, 170 | 'name': name, 171 | 'type': 'order', 172 | 'side': side, 173 | 'quantity': quantity, 174 | 'price': '{:.2f}'.format(price), 175 | 'amount': '{:.2f}'.format(amount), 176 | 'date': transaction_on.isoformat(), 177 | 'fees': fees, 178 | }) 179 | # Don't duplicate fees in multiple executions 180 | if fees: 181 | fees = Decimal(0) 182 | 183 | 184 | def add_dividends(csv_writer, cache_mode): 185 | dividends = client.get_dividends(cache_mode=cache_mode) 186 | 187 | instrument_ids = [get_last_id_from_url(d['instrument']) for d in dividends] 188 | instruments = client.get_instruments(instrument_ids) 189 | instrument_by_id = {i['id']: i for i in instruments} 190 | 191 | for dividend in dividends: 192 | paid_at = dividend['paid_at'] 193 | if not paid_at: 194 | continue 195 | paid_at = parse(paid_at) 196 | rate = Decimal(dividend['rate']) 197 | amount = Decimal(dividend['amount']) 198 | quantity = int(float(dividend['position'])) 199 | 200 | instrument_id = get_last_id_from_url(dividend['instrument']) 201 | instrument = instrument_by_id[instrument_id] 202 | name = instrument['simple_name'] or instrument['name'] 203 | symbol = instrument['symbol'] 204 | 205 | csv_writer.writerow({ 206 | 'symbol': symbol, 207 | 'name': name, 208 | 'type': 'dividend', 209 | 'side': 'receive', 210 | 'quantity': quantity, 211 | 'price': '{:.2f}'.format(rate), 212 | 'amount': '{:.2f}'.format(amount), 213 | 'date': paid_at.date(), 214 | 'fees': 0, 215 | }) 216 | 217 | 218 | def download_history(cache_mode): 219 | with open('history.csv', 'w', newline='') as csv_file: 220 | fieldnames = [ 221 | 'symbol', 222 | 'name', 223 | 'side', 224 | 'type', 225 | 'quantity', 226 | 'price', 227 | 'amount', 228 | 'date', 229 | 'fees', 230 | ] 231 | csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) 232 | csv_writer.writeheader() 233 | add_orders(csv_writer, cache_mode) 234 | add_dividends(csv_writer, cache_mode) 235 | add_rewards(csv_writer, cache_mode) 236 | add_transfers(csv_writer, cache_mode) 237 | add_subscription_fees(csv_writer, cache_mode) 238 | add_margin(csv_writer, cache_mode) 239 | 240 | 241 | if __name__ == '__main__': 242 | parser = argparse.ArgumentParser(description='Download a list of all financial history') 243 | parser.add_argument( 244 | '--live', 245 | action='store_true', 246 | help='Force to not use cache for APIs where values change' 247 | ) 248 | args = parser.parse_args() 249 | download_history(FORCE_LIVE if args.live else CACHE_FIRST) 250 | -------------------------------------------------------------------------------- /download_portfolio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import csv 5 | 6 | #import logging 7 | #logging.basicConfig(level=logging.DEBUG) 8 | 9 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 10 | from robinhood.RobinhoodPortfolio import RobinhoodPortfolio 11 | 12 | # Set up the client 13 | client = RobinhoodCachedClient() 14 | client.login() 15 | 16 | 17 | def download_portfolio(cache_mode): 18 | with open('portfolio.csv', 'w', newline='') as csv_file: 19 | fieldnames = [ 20 | 'symbol', 21 | 'name', 22 | 'quantity', 23 | 'average_buy_price', 24 | 'equity_cost', 25 | 'last_price', 26 | 'day_price_change', 27 | 'day_percentage_change', 28 | 'total_price_change', 29 | 'total_percentage_change', 30 | 'equity_worth', 31 | 'equity_percentage', 32 | 'equity_idx', 33 | 'robinhood_holders', 34 | 'buy_rating', 35 | 'sell_rating', 36 | ] 37 | csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) 38 | csv_writer.writeheader() 39 | 40 | portfolio = RobinhoodPortfolio(client, {'cache_mode': cache_mode}) 41 | 42 | for idx, position in enumerate(portfolio.positions_by_equity_worth): 43 | csv_writer.writerow({ 44 | 'symbol': position['symbol'], 45 | 'name': position['shortest_name'], 46 | 'quantity': position['quantity'], 47 | 'average_buy_price': round(position['average_buy_price'], 2), 48 | 'equity_cost': round(position['equity_cost'], 2), 49 | 'last_price': round(position['last_price'], 2), 50 | 'day_price_change': round(position['day_price_change'], 2), 51 | 'day_percentage_change': round(position['day_percentage_change'], 2), 52 | 'total_price_change': round(position['total_price_change'], 2), 53 | 'total_percentage_change': round(position['total_percentage_change'], 2), 54 | 'equity_worth': round(position['equity_worth'], 2), 55 | 'equity_percentage': round(position['equity_percentage'], 2), 56 | 'equity_idx': idx + 1, 57 | 'buy_rating': position['buy_rating'], 58 | 'sell_rating': position['sell_rating'], 59 | 'robinhood_holders': position['robinhood_holders'], 60 | }) 61 | 62 | 63 | if __name__ == '__main__': 64 | parser = argparse.ArgumentParser(description='Download a snapshot of your portfolio') 65 | parser.add_argument( 66 | '--live', 67 | action='store_true', 68 | help='Force to not use cache for APIs where values change' 69 | ) 70 | args = parser.parse_args() 71 | download_portfolio(FORCE_LIVE if args.live else CACHE_FIRST) 72 | -------------------------------------------------------------------------------- /enable_mfa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | 8 | # Set up the client 9 | client = RobinhoodCachedClient() 10 | client.login() 11 | 12 | SMS = 'sms' 13 | APP = 'app' 14 | 15 | def enable_mfa(mfa_source): 16 | if mfa_source == APP: 17 | request_mfa_details = client.request_app_mfa() 18 | totp_token = request_mfa_details['totp_token'] 19 | print('Your time-based one-time password token is: {}'.format(totp_token)) 20 | otp = input("After entering into your app, please enter a one time code: ") 21 | verify_details = client.verify_app_mfa(otp) 22 | 23 | elif mfa_source == SMS: 24 | phone_number = input("What phone number would you like to receive for MFA codes? ") 25 | sms_details = client.request_sms_mfa(phone_number) 26 | print('You should be getting a code sent to {}'.format(sms_details['phone_number'])) 27 | otp = input("Please enter the code you are sent: ") 28 | verify_details = client.verify_sms_mfa(otp) 29 | 30 | if not verify_details['enabled']: 31 | print('It looks like something went wrong, unable to setup MFA.') 32 | exit() 33 | 34 | print("It looks you're good to go!") 35 | backup = client.get_mfa_backup() 36 | print("If you lose access to your MFA device, your backup code is: {}".format(backup['backup_code'])) 37 | print('Keep it secret, keep it safe.') 38 | 39 | 40 | if __name__ == '__main__': 41 | mfa_details = client.get_mfa() 42 | if mfa_details['enabled']: 43 | challenge_type = mfa_details['challenge_type'] 44 | print('It looks like MFA is already enabled (using {}), nothing to do here.'.format(challenge_type)) 45 | exit() 46 | 47 | parser = argparse.ArgumentParser(description='Enable MFA on your Robinhood account') 48 | parser.add_argument('mfa_source', choices=[SMS, APP]) 49 | args = parser.parse_args() 50 | enable_mfa(args.mfa_source) 51 | -------------------------------------------------------------------------------- /login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient 4 | 5 | 6 | client = RobinhoodCachedClient() 7 | client.login() 8 | print('Logged in!') 9 | -------------------------------------------------------------------------------- /logout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient 4 | 5 | 6 | client = RobinhoodCachedClient() 7 | client.logout() 8 | print('Logged out!') 9 | -------------------------------------------------------------------------------- /order.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | 6 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 7 | from robinhood.util import ORDER_TYPES, ORDER_SIDES 8 | 9 | # Set up the client 10 | client = RobinhoodCachedClient() 11 | client.login() 12 | 13 | from show_quote import display_quote 14 | 15 | 16 | def place_order(order_type, order_side, symbol, quantity, price, no_cancel): 17 | account_url = client.get_account()['url'] 18 | instrument = client.get_instrument_by_symbol(symbol) 19 | instrument_id = instrument['id'] 20 | 21 | display_quote(client, symbol, True) 22 | 23 | # We could be more smart and allow canceling individual orders, 24 | # but for now just do complete cancelling 25 | orders = client.get_orders(instrument_id=instrument_id, cache_mode=FORCE_LIVE) 26 | pending_same_side_orders = [ 27 | order for order in orders 28 | if order['state'] in ['queued', 'confirmed'] and order['side'] == order_side 29 | ] 30 | if pending_same_side_orders and no_cancel is not True: 31 | print('') 32 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 33 | confirm = input('There appear to be pending orders, continuing will cancel them prior to placing the new order. Continue? [N/y] ') 34 | if confirm not in ['y', 'yes']: 35 | print('Bailed out!') 36 | exit() 37 | 38 | print('') 39 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 40 | confirm = input('Are you sure that you want to {} {} {} shares of {} ({}) for ${:.2f} each? [N/y]? '.format( 41 | order_type, order_side, quantity, symbol, instrument['simple_name'] or instrument['name'], price)).lower() 42 | if confirm not in ['y', 'yes']: 43 | print('Bailed out!') 44 | exit() 45 | 46 | if no_cancel is not True and pending_same_side_orders: 47 | for pending_order in pending_same_side_orders: 48 | cancelled_order = client.cancel_order(pending_order['id']) 49 | print('Cancelled order {}'.format(pending_order['id'])) 50 | 51 | order = client.order( 52 | instrument_id, 53 | order_type, 54 | order_side, 55 | symbol, 56 | quantity, 57 | price, 58 | use_account_url=account_url 59 | ) 60 | print(json.dumps(order, indent=4)) 61 | 62 | 63 | if __name__ == '__main__': 64 | parser = argparse.ArgumentParser(description='Place an order') 65 | parser.add_argument('order_type', choices=ORDER_TYPES) 66 | parser.add_argument('order_side', choices=ORDER_SIDES) 67 | parser.add_argument('symbol', type=str.upper, help='The stock ticker') 68 | parser.add_argument('quantity', type=int) 69 | parser.add_argument('price', type=float) 70 | parser.add_argument('--no-cancel', action='store_true', help='Dont cancel any pending orders') 71 | args = parser.parse_args() 72 | place_order( 73 | args.order_type, 74 | args.order_side, 75 | args.symbol, 76 | args.quantity, 77 | args.price, 78 | args.no_cancel, 79 | ) 80 | 81 | -------------------------------------------------------------------------------- /order_crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | import argparse 5 | import json 6 | 7 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 8 | from robinhood.util import ORDER_TYPES, ORDER_SIDES 9 | 10 | # Set up the client 11 | client = RobinhoodCachedClient() 12 | client.login() 13 | 14 | 15 | def place_order(order_type, order_side, symbol, quantity, price): 16 | quote = client.get_crypto_quote(symbol) 17 | currency_pair_id = quote['id'] 18 | currency_pair = client.get_crypto_currency_pair(currency_pair_id) 19 | name = currency_pair['name'] 20 | 21 | is_tradable = currency_pair['tradability'] == 'tradable' 22 | if not is_tradable: 23 | print("Sorry, {} ({}) isn't tradable".format(symbol, name)) 24 | exit() 25 | 26 | min_order_size = Decimal(currency_pair['min_order_size']) 27 | max_order_size = Decimal(currency_pair['max_order_size']) 28 | crypto_increment = Decimal(currency_pair['asset_currency']['increment']) 29 | price_increment = Decimal(currency_pair['quote_currency']['increment']) 30 | 31 | if price % price_increment != 0: 32 | print("Sorry, price must be a multiple of {}".format(price_increment)) 33 | exit() 34 | if quantity < min_order_size or quantity > max_order_size: 35 | print("Sorry, order quantity must be between {} and {}".format(min_order_size, max_order_size)) 36 | exit() 37 | if quantity % crypto_increment != 0: 38 | print("Sorry, quantity must be a multiple of {}".format(crypto_increment)) 39 | exit() 40 | 41 | print('') 42 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 43 | confirm = input('Are you sure that you want to {} {} {} {} ({}) for ${:.2f}? [N/y]? '.format( 44 | order_type, 45 | order_side, 46 | quantity, 47 | symbol, 48 | name, 49 | price)).lower() 50 | if confirm not in ['y', 'yes']: 51 | print('Bailed out!') 52 | exit() 53 | 54 | order = client.order_crypto( 55 | currency_pair_id, order_type, order_side, quantity, price) 56 | print(json.dumps(order, indent=4)) 57 | 58 | 59 | if __name__ == '__main__': 60 | parser = argparse.ArgumentParser(description='Place an order') 61 | parser.add_argument('order_type', choices=ORDER_TYPES) 62 | parser.add_argument('order_side', choices=ORDER_SIDES) 63 | parser.add_argument('symbol', type=str.upper, help='The cryptocurrency + currency ticker') 64 | parser.add_argument('quantity', type=Decimal) 65 | parser.add_argument('price', type=Decimal) 66 | args = parser.parse_args() 67 | place_order( 68 | args.order_type, 69 | args.order_side, 70 | args.symbol, 71 | args.quantity, 72 | args.price 73 | ) 74 | 75 | -------------------------------------------------------------------------------- /order_options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | import argparse 5 | import json 6 | 7 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 8 | from robinhood.util import ORDER_TYPES, ORDER_SIDES, OPTIONS_TYPES, ORDER_SIDE_TO_DIRECTION 9 | 10 | # Set up the client 11 | client = RobinhoodCachedClient() 12 | client.login() 13 | 14 | 15 | def place_order(order_type, order_side, symbol, date, strike, options_type, quantity, price): 16 | try: 17 | instrument = client.get_instrument_by_symbol(symbol) 18 | except NotFound: 19 | print('symbol {} was not found'.format(symbol)) 20 | exit() 21 | else: 22 | instrument_id = instrument['id'] 23 | name = instrument['simple_name'] or instrument['name'] 24 | account_url = client.get_account()['url'] 25 | 26 | options_chains = [c for c in client.get_options_chains(instrument_ids=[instrument_id]) if c['can_open_position']] 27 | if len(options_chains) != 1: 28 | raise Exception('Expected exactly one options chains listing, but got: {}'.format(json.dumps(options_chains, indent=4))) 29 | options_chain = options_chains[0] 30 | 31 | if len(options_chain['underlying_instruments']) != 1: 32 | raise Exception('Expected exactly one underlying instrument, but got: {}'.format(json.dumps(options_chain, indent=4))) 33 | if not options_chain['can_open_position']: 34 | raise Exception("Can't open position: {}".format(json.dumps(options_chain, indent=4))) 35 | chain_id = options_chain['id'] 36 | options_instrument_id = options_chain['underlying_instruments'][0]['id'] 37 | multiplier = Decimal(options_chain['trade_value_multiplier']) 38 | 39 | options_instruments = client.get_options_instruments( 40 | chain_id=chain_id, options_type=options_type, tradability='tradable', state='active', expiration_dates=[date]) 41 | if not options_instruments: 42 | raise Exception('No options found on that date') 43 | 44 | options_instrument = None 45 | for potential_options_instrument in options_instruments: 46 | potential_strike = float(potential_options_instrument['strike_price']) 47 | if potential_strike == strike: 48 | options_instrument = potential_options_instrument 49 | break 50 | if not options_instrument: 51 | raise Exception('No options found at that strike price') 52 | 53 | options_quote = client.get_options_marketdata(options_instrument['id']) 54 | 55 | print('') 56 | print('!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!') 57 | confirm = input('Are you sure that you want to {} {} {} {} ${:.2f} {} options of {} ({}) for ${:.2f} each? [N/y]? '.format( 58 | order_type, 59 | order_side, 60 | quantity, 61 | date, 62 | strike, 63 | options_type, 64 | symbol, 65 | instrument['simple_name'] or instrument['name'], 66 | price)).lower() 67 | if confirm not in ['y', 'yes']: 68 | print('Bailed out!') 69 | exit() 70 | 71 | order = client.order_options( 72 | options_instrument['id'], order_type, ORDER_SIDE_TO_DIRECTION[order_side], quantity, price, use_account_url=account_url) 73 | print(json.dumps(order, indent=4)) 74 | 75 | 76 | if __name__ == '__main__': 77 | parser = argparse.ArgumentParser(description='Place an order') 78 | parser.add_argument('order_type', choices=ORDER_TYPES) 79 | parser.add_argument('order_side', choices=ORDER_SIDES) 80 | parser.add_argument('symbol', type=str.upper, help='The stock ticker') 81 | parser.add_argument('date', type=str, help='Date for the options to expire') 82 | parser.add_argument('strike', type=float, help='Amount the strike price should be') 83 | parser.add_argument('options_type', choices=OPTIONS_TYPES) 84 | parser.add_argument('quantity', type=int) 85 | parser.add_argument('price', type=float) 86 | args = parser.parse_args() 87 | place_order( 88 | args.order_type, 89 | args.order_side, 90 | args.symbol, 91 | args.date, 92 | args.strike, 93 | args.options_type, 94 | args.quantity, 95 | args.price 96 | ) 97 | 98 | -------------------------------------------------------------------------------- /prepare_sentiment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | import os 6 | import shutil 7 | 8 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 9 | from robinhood.RobinhoodPortfolio import RobinhoodPortfolio 10 | 11 | # Set up the client 12 | client = RobinhoodCachedClient() 13 | client.login() 14 | 15 | 16 | def show_potentials(decay_priority, cache_mode): 17 | portfolio = RobinhoodPortfolio(client, {'cache_mode': cache_mode}) 18 | 19 | # Load sentiment if available. 20 | if os.path.exists('sentiment.json'): 21 | with open('sentiment.json', 'r') as sentiment_file: 22 | sentiment_json = json.load(sentiment_file) 23 | shutil.copyfile('sentiment.json', 'sentiment.old.json') 24 | else: 25 | sentiment_json = {} 26 | 27 | # Delete any equities that were sold off. 28 | sentiment_symbols = sentiment_json.keys() 29 | for symbol in sentiment_symbols: 30 | if symbol not in portfolio.symbols: 31 | del sentiment_json[symbol] 32 | 33 | # Add any new ones. 34 | for symbol in portfolio.symbols: 35 | if symbol not in sentiment_json: 36 | sentiment_json[symbol] = {} 37 | 38 | # Bump up priority if there are any p0 39 | if decay_priority: 40 | highest_priority = min(p.get('priority', 9999) for p in sentiment_json.values()) 41 | if highest_priority == 0: 42 | for position in sentiment_json.values(): 43 | position_pority = position.get('priority') 44 | if position_pority is not None: 45 | position['priority'] += 1 46 | 47 | with open('sentiment.json', 'w') as sentiment_file: 48 | json.dump(sentiment_json, sentiment_file, indent=4) 49 | 50 | 51 | if __name__ == '__main__': 52 | parser = argparse.ArgumentParser(description='Show various position potentials to buy into') 53 | parser.add_argument( 54 | '--live', 55 | action='store_true', 56 | help='Force to not use cache for APIs where values change' 57 | ) 58 | parser.add_argument( 59 | '--decay-priority', 60 | action='store_true', 61 | help='Lower the priority of everything' 62 | ) 63 | args = parser.parse_args() 64 | show_potentials( 65 | args.decay_priority, 66 | FORCE_LIVE if args.live else CACHE_FIRST 67 | ) 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # robinhood/ 2 | requests >= 2.18.4 3 | 4 | # ./ 5 | python-dateutil >= 2.6.1 6 | pytz >= 2018.3 7 | 8 | # dev 9 | flake8 >= 3.5.0 10 | -------------------------------------------------------------------------------- /robinhood/RobinhoodCachedClient.py: -------------------------------------------------------------------------------- 1 | """ 2 | A wrapper of RobinhoodClient that introduces a caching layer. 3 | """ 4 | from datetime import datetime 5 | import getpass 6 | import json 7 | import logging 8 | import os 9 | 10 | from .exceptions import MfaRequired 11 | from .RobinhoodClient import RobinhoodClient 12 | from .util import get_last_id_from_url 13 | 14 | cache_root_path = '.robinhood' 15 | if not os.path.exists(cache_root_path): 16 | os.makedirs(cache_root_path) 17 | 18 | # Cache modes 19 | CACHE_FIRST = 'CACHE_FIRST' 20 | FORCE_LIVE = 'FORCE_LIVE' 21 | FORCE_CACHE = 'FORCE_CACHE' 22 | 23 | class RobinhoodCachedClient(RobinhoodClient): 24 | def __init__(self): 25 | super(RobinhoodCachedClient, self).__init__() 26 | self.confirm_disclosures_if_needed() 27 | 28 | def confirm_disclosures_if_needed(self): 29 | cache_path = os.path.join(cache_root_path, 'disclosures_acknowledged') 30 | if os.path.exists(cache_path): 31 | return 32 | 33 | disclosures = self.get_home_screen_disclosures() 34 | print('') 35 | print('vvvvvvvvvvvvvvvvvvvvvvv Robinhood disclosures vvvvvvvvvvvvvvvvvvvvvvv') 36 | print('') 37 | print(disclosures['disclosure']) 38 | print('') 39 | print('^^^^^^^^^^^^^^^^^^^^^^^ Robinhood disclosures ^^^^^^^^^^^^^^^^^^^^^^^') 40 | print('') 41 | confirm = input("Do you acknowledge that you understand Robinhood's disclosures? [N/y] ") 42 | if confirm not in ['y', 'yes']: 43 | print("Sorry, you need to acknowledge Robinhood's disclosures to continue.") 44 | exit() 45 | 46 | print(""" 47 | vvvvvvvvvvvvvvvvvvvvvvv robinhood-python disclosures vvvvvvvvvvvvvvvvvvvvvvv 48 | 49 | * This library may have bugs which could result in financial consequences, 50 | you are responsible for anything you execute. Inspect the underlying code 51 | if you want to be sure it's doing what you think it should be doing. 52 | 53 | * This library is not affiliated with Robinhood and this library uses an 54 | undocumented API. If you have any questions about them, contact Robinhood 55 | directly: https://robinhood.com/ 56 | 57 | * By using this library, you understand that you are not to charge or 58 | make any money through advertisements or fees. Until Robinhood releases 59 | an official API with official guidance, this is only to be used for 60 | non-profit activites. Only you are responsible if Robinhood cancels your 61 | account because of misuse of this library. 62 | 63 | ^^^^^^^^^^^^^^^^^^^^^^^ robinhood-python disclosures ^^^^^^^^^^^^^^^^^^^^^^^ 64 | """) 65 | confirm = input("Do you acknowledge that you understand robinhood-python's disclosures? [N/y] ") 66 | if confirm not in ['y', 'yes']: 67 | print("Sorry, you need to acknowledge robinhood-python's disclosures to continue.") 68 | exit() 69 | 70 | with open(cache_path, 'w') as cache_file: 71 | cache_file.write(datetime.now().isoformat()) 72 | 73 | def login(self, force_login=False): 74 | cache_path = os.path.join(cache_root_path, 'auth_data') 75 | if os.path.exists(cache_path) and not force_login: 76 | with open(cache_path, 'r') as cache_file: 77 | auth_data = json.load(cache_file) 78 | self.set_oauth2_token( 79 | auth_data['token_type'], 80 | auth_data['access_token'], 81 | datetime.strptime(auth_data['expires_at'], "%Y-%m-%d %H:%M:%S.%f"), 82 | auth_data['refresh_token'] 83 | ) 84 | else: 85 | # Get a new auth token 86 | username = input('Username: ') 87 | password = getpass.getpass() 88 | 89 | try: 90 | self.set_auth_token_with_credentials(username, password) 91 | except MfaRequired: 92 | mfa = input('MFA: ') 93 | self.set_auth_token_with_credentials(username, password, mfa) 94 | 95 | with open(cache_path, 'w') as cache_file: 96 | auth_header = self._authorization_headers['Authorization'] 97 | auth_data = { 98 | 'token_type': auth_header.split(' ')[0], 99 | 'access_token': auth_header.split(' ')[1], 100 | 'expires_at': self._oauth2_expires_at, 101 | 'refresh_token': self._oauth2_refresh_token 102 | } 103 | json.dump(auth_data, cache_file, default=str) 104 | 105 | def logout(self): 106 | cache_path = os.path.join(cache_root_path, 'auth_data') 107 | if os.path.exists(cache_path): 108 | os.remove(cache_path) 109 | if self._oauth2_refresh_token: 110 | self.clear_auth_token() 111 | 112 | def _simple_call(self, cache_name, method, cache_mode, args=[], kwargs={}, binary=False): 113 | cache_path = os.path.join(cache_root_path, cache_name) 114 | binary_flag = 'b' if binary else '' 115 | if os.path.exists(cache_path) and cache_mode != FORCE_LIVE: 116 | logging.debug('Getting {} from cache'.format(cache_name)) 117 | with open(cache_path, 'r' + binary_flag) as cache_file: 118 | if binary: 119 | return cache_file.read() 120 | else: 121 | return json.load(cache_file) 122 | elif cache_mode == FORCE_CACHE: 123 | return None 124 | else: 125 | live_content = method(*args, **kwargs) 126 | with open(cache_path, 'w' + binary_flag) as cache_file: 127 | if binary: 128 | cache_file.write(live_content) 129 | else: 130 | json.dump(live_content, cache_file) 131 | return live_content 132 | 133 | def get_user(self, cache_mode=CACHE_FIRST): 134 | return self._simple_call( 135 | 'user', 136 | super(RobinhoodCachedClient, self).get_user, 137 | cache_mode 138 | ) 139 | 140 | def get_user_basic_info(self, cache_mode=CACHE_FIRST): 141 | return self._simple_call( 142 | 'user_basic_info', 143 | super(RobinhoodCachedClient, self).get_user_basic_info, 144 | cache_mode 145 | ) 146 | 147 | def get_user_additional_info(self, cache_mode=CACHE_FIRST): 148 | return self._simple_call( 149 | 'user_additional_info', 150 | super(RobinhoodCachedClient, self).get_user_additional_info, 151 | cache_mode 152 | ) 153 | 154 | def get_experiments(self, cache_mode=CACHE_FIRST): 155 | return self._simple_call( 156 | 'experiments', 157 | super(RobinhoodCachedClient, self).get_experiments, 158 | cache_mode 159 | ) 160 | 161 | def get_referral_code(self, cache_mode=CACHE_FIRST): 162 | return self._simple_call( 163 | 'referral_code', 164 | super(RobinhoodCachedClient, self).get_referral_code, 165 | cache_mode 166 | ) 167 | 168 | def get_subscription_fees(self, cache_mode=CACHE_FIRST): 169 | return self._simple_call( 170 | 'subscription_fees', 171 | super(RobinhoodCachedClient, self).get_subscription_fees, 172 | cache_mode 173 | ) 174 | 175 | def get_subscriptions(self, cache_mode=CACHE_FIRST): 176 | return self._simple_call( 177 | 'subscriptions', 178 | super(RobinhoodCachedClient, self).get_subscriptions, 179 | cache_mode 180 | ) 181 | 182 | def get_markets(self, cache_mode=CACHE_FIRST): 183 | return self._simple_call( 184 | 'markets', 185 | super(RobinhoodCachedClient, self).get_markets, 186 | cache_mode 187 | ) 188 | 189 | def download_document_by_id(self, document_id, cache_mode=CACHE_FIRST): 190 | return self._simple_call( 191 | 'document_pdf_{}'.format(document_id), 192 | super(RobinhoodCachedClient, self).download_document_by_id, 193 | cache_mode, 194 | args=[document_id], 195 | binary=True 196 | ) 197 | 198 | def get_document_by_id(self, document_id): 199 | return self._simple_call( 200 | 'document_{}'.format(document_id), 201 | super(RobinhoodCachedClient, self).get_document_by_id, 202 | # Documents don't change, never force live 203 | CACHE_FIRST, 204 | args=[document_id] 205 | ) 206 | 207 | def get_watchlists(self, cache_mode=CACHE_FIRST): 208 | return self._simple_call( 209 | 'watchlists', 210 | super(RobinhoodCachedClient, self).get_watchlists, 211 | cache_mode 212 | ) 213 | 214 | def get_watchlist_instruments(self, watchlist_name, cache_mode=CACHE_FIRST): 215 | return self._simple_call( 216 | 'watchlist_instruments_{}'.format(watchlist_name), 217 | super(RobinhoodCachedClient, self).get_watchlist_instruments, 218 | cache_mode, 219 | args=[watchlist_name] 220 | ) 221 | 222 | def get_notification_settings(self, cache_mode=CACHE_FIRST): 223 | return self._simple_call( 224 | 'notification_settings', 225 | super(RobinhoodCachedClient, self).get_notification_settings, 226 | cache_mode 227 | ) 228 | 229 | def get_notification_devices(self, cache_mode=CACHE_FIRST): 230 | return self._simple_call( 231 | 'notification_devices', 232 | super(RobinhoodCachedClient, self).get_notification_devices, 233 | cache_mode 234 | ) 235 | 236 | def get_account(self, cache_mode=CACHE_FIRST): 237 | return self._simple_call( 238 | 'account', 239 | super(RobinhoodCachedClient, self).get_account, 240 | cache_mode 241 | ) 242 | 243 | def get_investment_profile(self, cache_mode=CACHE_FIRST): 244 | return self._simple_call( 245 | 'investment_profile', 246 | super(RobinhoodCachedClient, self).get_investment_profile, 247 | cache_mode 248 | ) 249 | 250 | def get_similar_to(self, instrument_id, cache_mode=CACHE_FIRST): 251 | return self._simple_call( 252 | 'similar_to_{}'.format(instrument_id), 253 | super(RobinhoodCachedClient, self).get_similar_to, 254 | cache_mode, 255 | args=[instrument_id] 256 | ) 257 | 258 | def get_popularity(self, instrument_id, cache_mode=CACHE_FIRST): 259 | return self._simple_call( 260 | 'popularity_{}'.format(instrument_id), 261 | super(RobinhoodCachedClient, self).get_popularity, 262 | cache_mode, 263 | args=[instrument_id] 264 | ) 265 | 266 | def get_rating(self, instrument_id, cache_mode=CACHE_FIRST): 267 | return self._simple_call( 268 | 'rating_{}'.format(instrument_id), 269 | super(RobinhoodCachedClient, self).get_rating, 270 | cache_mode, 271 | args=[instrument_id] 272 | ) 273 | 274 | def get_instrument_reasons_for_personal_tag(self, tag, cache_mode=CACHE_FIRST): 275 | return self._simple_call( 276 | 'instrument_reasons_for_personal_tag_{}'.format(tag), 277 | super(RobinhoodCachedClient, self).get_instrument_reasons_for_personal_tag, 278 | cache_mode, 279 | args=[tag] 280 | ) 281 | 282 | def get_instrument_ids_for_tag(self, tag, cache_mode=CACHE_FIRST): 283 | return self._simple_call( 284 | 'tag_{}'.format(tag), 285 | super(RobinhoodCachedClient, self).get_instrument_ids_for_tag, 286 | cache_mode, 287 | args=[tag] 288 | ) 289 | 290 | def get_earnings(self, instrument_id, cache_mode=CACHE_FIRST): 291 | return self._simple_call( 292 | 'earnings_{}'.format(instrument_id), 293 | super(RobinhoodCachedClient, self).get_earnings, 294 | cache_mode, 295 | args=[instrument_id] 296 | ) 297 | 298 | def get_instrument_by_id(self, instrument_id, cache_mode=CACHE_FIRST): 299 | return self._simple_call( 300 | 'instrument_{}'.format(instrument_id), 301 | super(RobinhoodCachedClient, self).get_instrument_by_id, 302 | cache_mode, 303 | args=[instrument_id] 304 | ) 305 | 306 | def get_instrument_by_symbol(self, symbol, cache_mode=CACHE_FIRST): 307 | symbol_instrument_id_cache_path = os.path.join(cache_root_path, 'symbol_instrument_id_{}'.format(symbol)) 308 | if os.path.exists(symbol_instrument_id_cache_path): 309 | with open(symbol_instrument_id_cache_path, 'r') as symbol_instrument_id_cache_file: 310 | instrument_id = symbol_instrument_id_cache_file.read() 311 | return self.get_instrument_by_id(instrument_id, cache_mode=cache_mode) 312 | 313 | instrument = self._simple_call( 314 | 'instrument_{}'.format(symbol), 315 | super(RobinhoodCachedClient, self).get_instrument_by_symbol, 316 | cache_mode, 317 | args=[symbol] 318 | ) 319 | instrument_id = instrument['id'] 320 | 321 | os.rename( 322 | os.path.join(cache_root_path, 'instrument_{}'.format(symbol)), 323 | os.path.join(cache_root_path, 'instrument_{}'.format(instrument_id)) 324 | ) 325 | with open(symbol_instrument_id_cache_path, 'w') as symbol_instrument_id_cache_file: 326 | symbol_instrument_id_cache_file.write(instrument_id) 327 | 328 | return instrument 329 | 330 | def get_instrument_split_history(self, instrument_id, cache_mode=CACHE_FIRST): 331 | return self._simple_call( 332 | 'instrument_split_history_{}'.format(instrument_id), 333 | super(RobinhoodCachedClient, self).get_instrument_split_history, 334 | cache_mode, 335 | args=[instrument_id] 336 | ) 337 | 338 | def get_historical_quote(self, symbol, interval, span=None, bounds=None, cache_mode=CACHE_FIRST): 339 | return self._simple_call( 340 | 'historical_quote_{}_{}_{}_{}'.format(symbol, interval, span, bounds), 341 | super(RobinhoodCachedClient, self).get_historical_quote, 342 | cache_mode, 343 | args=[symbol, interval], 344 | kwargs={ 345 | 'span': span, 346 | 'bounds': bounds, 347 | } 348 | ) 349 | 350 | def get_quote(self, instrument_id, cache_mode=CACHE_FIRST): 351 | return self._simple_call( 352 | 'quote_{}'.format(instrument_id), 353 | super(RobinhoodCachedClient, self).get_quote, 354 | cache_mode, 355 | args=[instrument_id] 356 | ) 357 | 358 | def get_dividend_by_id(self, dividend_id, cache_mode=CACHE_FIRST): 359 | return self._simple_call( 360 | 'dividend_{}'.format(dividend_id), 361 | super(RobinhoodCachedClient, self).get_dividend_by_id, 362 | cache_mode, 363 | args=[dividend_id] 364 | ) 365 | 366 | def get_ach_relationship_by_id(self, relationship_id, cache_mode=CACHE_FIRST): 367 | return self._simple_call( 368 | 'ach_relationship_{}'.format(relationship_id), 369 | super(RobinhoodCachedClient, self).get_ach_relationship_by_id, 370 | cache_mode, 371 | args=[relationship_id] 372 | ) 373 | 374 | def get_ach_transfer_by_id(self, transfer_id, cache_mode=CACHE_FIRST): 375 | return self._simple_call( 376 | 'ach_transfer_{}'.format(transfer_id), 377 | super(RobinhoodCachedClient, self).get_ach_transfer_by_id, 378 | cache_mode, 379 | args=[transfer_id] 380 | ) 381 | 382 | def get_popular_stocks(self, cache_mode=CACHE_FIRST): 383 | return self._simple_call( 384 | 'popular_stocks', 385 | super(RobinhoodCachedClient, self).get_popular_stocks, 386 | cache_mode 387 | ) 388 | 389 | def get_sp500_movers(self, direction, cache_mode=CACHE_FIRST): 390 | return self._simple_call( 391 | 'sp500_movers_{}'.format(direction), 392 | super(RobinhoodCachedClient, self).get_sp500_movers, 393 | cache_mode, 394 | args=[direction] 395 | ) 396 | 397 | def get_portfolio(self, cache_mode=CACHE_FIRST): 398 | return self._simple_call( 399 | 'portfolio', 400 | super(RobinhoodCachedClient, self).get_portfolio, 401 | cache_mode 402 | ) 403 | 404 | def get_portfolio_history(self, interval, span=None, use_account_number=None, cache_mode=CACHE_FIRST): 405 | account_number = use_account_number or self.get_account()['account_number'] 406 | return self._simple_call( 407 | 'portfolio_history_{}_{}'.format(interval, span), 408 | super(RobinhoodCachedClient, self).get_portfolio_history, 409 | cache_mode, 410 | args=[interval], 411 | kwargs={ 412 | 'span': span, 413 | 'use_account_number': account_number, 414 | } 415 | ) 416 | 417 | def get_position_by_instrument_id(self, instrument_id, use_account_number=None, cache_mode=CACHE_FIRST): 418 | account_number = use_account_number or self.get_account()['account_number'] 419 | return self._simple_call( 420 | 'position_{}'.format(instrument_id), 421 | super(RobinhoodCachedClient, self).get_position_by_instrument_id, 422 | cache_mode, 423 | args=[instrument_id], 424 | kwargs={'use_account_number': account_number} 425 | ) 426 | 427 | def get_news(self, symbol, cache_mode=CACHE_FIRST): 428 | return self._simple_call( 429 | 'news_{}'.format(symbol), 430 | super(RobinhoodCachedClient, self).get_news, 431 | cache_mode, 432 | args=[symbol] 433 | ) 434 | 435 | def get_tags(self, instrument_id, cache_mode=CACHE_FIRST): 436 | return self._simple_call( 437 | 'tags_{}'.format(instrument_id), 438 | super(RobinhoodCachedClient, self).get_tags, 439 | cache_mode, 440 | args=[instrument_id] 441 | ) 442 | 443 | def get_fundamental(self, instrument_id, cache_mode=CACHE_FIRST): 444 | return self._simple_call( 445 | 'fundamental_{}'.format(instrument_id), 446 | super(RobinhoodCachedClient, self).get_fundamental, 447 | cache_mode, 448 | args=[instrument_id] 449 | ) 450 | 451 | def get_referrals(self, cache_mode=CACHE_FIRST): 452 | return self._simple_call( 453 | 'referrals', 454 | super(RobinhoodCachedClient, self).get_referrals, 455 | cache_mode 456 | ) 457 | 458 | def get_order_by_id(self, order_id, cache_mode=CACHE_FIRST): 459 | return self._simple_call( 460 | 'order_{}'.format(order_id), 461 | super(RobinhoodCachedClient, self).get_order_by_id, 462 | cache_mode, 463 | args=[order_id] 464 | ) 465 | 466 | def _search_and_cache_call( 467 | self, 468 | search_method, 469 | item_to_id_method, 470 | item_cache_name_template, 471 | search_args=[], 472 | search_kwargs={}): 473 | results = [] 474 | live_search_content = search_method(*search_args, **search_kwargs) 475 | for live_item in live_search_content: 476 | results.append(live_item) 477 | item_id = item_to_id_method(live_item) 478 | item_cache_path = os.path.join(cache_root_path, item_cache_name_template.format(item_id)) 479 | with open(item_cache_path, 'w') as item_cache_file: 480 | json.dump(live_item, item_cache_file) 481 | return results 482 | 483 | def _list_call( 484 | self, 485 | list_cache_name, 486 | list_method, 487 | item_method, 488 | item_to_id_method, 489 | item_cache_name_template, 490 | cache_mode, 491 | list_args=[], 492 | list_kwargs={}, 493 | item_extra_args=[], 494 | item_kwargs={}): 495 | results = [] 496 | list_cache_path = os.path.join(cache_root_path, list_cache_name) 497 | if os.path.exists(list_cache_path) and cache_mode != FORCE_LIVE: 498 | logging.debug('Loading {} from cache'.format(list_cache_name)) 499 | with open(list_cache_path, 'r') as list_cache_file: 500 | list_json = json.load(list_cache_file) 501 | for item_id in list_json: 502 | results.append(item_method(item_id, *item_extra_args, **item_kwargs, cache_mode=cache_mode)) 503 | else: 504 | live_list_content = self._search_and_cache_call( 505 | list_method, 506 | item_to_id_method, 507 | item_cache_name_template, 508 | search_args=list_args, 509 | search_kwargs=list_kwargs) 510 | live_list_ids = [] 511 | for live_item in live_list_content: 512 | item_id = item_to_id_method(live_item) 513 | live_list_ids.append(item_id) 514 | results.append(live_item) 515 | with open(list_cache_path, 'w') as list_cache_file: 516 | json.dump(live_list_ids, list_cache_file) 517 | return results 518 | 519 | def get_documents(self, cache_mode=CACHE_FIRST): 520 | return self._list_call( 521 | 'documents', 522 | super(RobinhoodCachedClient, self).get_documents, 523 | self.get_document_by_id, 524 | lambda document: document['id'], 525 | 'document_{}', 526 | cache_mode 527 | ) 528 | 529 | def get_ach_relationships(self, cache_mode=CACHE_FIRST): 530 | return self._list_call( 531 | 'ach_relationships', 532 | super(RobinhoodCachedClient, self).get_ach_relationships, 533 | self.get_ach_relationship_by_id, 534 | lambda ach_relationship: ach_relationship['id'], 535 | 'ach_relationship_{}', 536 | cache_mode 537 | ) 538 | 539 | def get_ach_transfers(self, cache_mode=CACHE_FIRST): 540 | return self._list_call( 541 | 'ach_transfers', 542 | super(RobinhoodCachedClient, self).get_ach_transfers, 543 | self.get_ach_transfer_by_id, 544 | lambda ach_transfer: ach_transfer['id'], 545 | 'ach_transfer_{}', 546 | cache_mode 547 | ) 548 | 549 | def get_dividends(self, cache_mode=CACHE_FIRST): 550 | return self._list_call( 551 | 'dividends', 552 | super(RobinhoodCachedClient, self).get_dividends, 553 | self.get_dividend_by_id, 554 | lambda dividend: dividend['id'], 555 | 'dividend_{}', 556 | cache_mode 557 | ) 558 | 559 | def get_positions(self, include_old=False, use_account_number=None, cache_mode=CACHE_FIRST): 560 | account_number = use_account_number or self.get_account()['account_number'] 561 | return self._list_call( 562 | 'positions_' + ('all' if include_old else 'current'), 563 | super(RobinhoodCachedClient, self).get_positions, 564 | self.get_position_by_instrument_id, 565 | lambda position: get_last_id_from_url(position['instrument']), 566 | 'position_{}', 567 | cache_mode, 568 | list_kwargs={'include_old': include_old}, 569 | item_kwargs={'use_account_number': account_number} 570 | ) 571 | 572 | def get_orders(self, instrument_id=False, cache_mode=CACHE_FIRST): 573 | return self._list_call( 574 | 'instrument_orders_{}'.format(instrument_id) if instrument_id else 'orders', 575 | super(RobinhoodCachedClient, self).get_orders, 576 | self.get_order_by_id, 577 | lambda order: order['id'], 578 | 'order_{}', 579 | cache_mode, 580 | list_kwargs={'instrument_id': instrument_id} 581 | ) 582 | 583 | def get_historical_quotes(self, symbols, interval, span=None, bounds=None, cache_mode=CACHE_FIRST): 584 | combined_sorted_symbols = ''.join(sorted(symbols)) 585 | return self._list_call( 586 | 'historical_quotes_{}_{}_{}_{}'.format(combined_sorted_symbols, interval, span, bounds), 587 | super(RobinhoodCachedClient, self).get_historical_quotes, 588 | self.get_historical_quote, 589 | lambda historical_quote: historical_quote['symbol'], 590 | 'historical_quote_{{}}_{}_{}_{}'.format(interval, span, bounds), 591 | cache_mode, 592 | list_args=[symbols, interval], 593 | item_extra_args=[interval], 594 | list_kwargs={ 595 | 'span': span, 596 | 'bounds': bounds, 597 | }, 598 | item_kwargs={ 599 | 'span': span, 600 | 'bounds': bounds, 601 | } 602 | ) 603 | 604 | def _search_call( 605 | self, 606 | item_ids, 607 | search_method, 608 | item_to_id_method, 609 | item_cache_name_template, 610 | item_method, 611 | cache_mode): 612 | items = [] 613 | unfound_item_ids = item_ids[:] 614 | if cache_mode != FORCE_LIVE: 615 | for item_id in item_ids: 616 | item = item_method(item_id, cache_mode=FORCE_CACHE) 617 | if item: 618 | unfound_item_ids.remove(item_id) 619 | items.append(item) 620 | 621 | if unfound_item_ids: 622 | live_search_content = self._search_and_cache_call( 623 | search_method, 624 | item_to_id_method, 625 | item_cache_name_template, 626 | search_args=[unfound_item_ids]) 627 | items.extend(live_search_content) 628 | 629 | return items 630 | 631 | def get_instruments(self, instrument_ids, cache_mode=CACHE_FIRST): 632 | return self._search_call( 633 | instrument_ids, 634 | super(RobinhoodCachedClient, self).get_instruments, 635 | lambda instrument: instrument['id'], 636 | 'instrument_{}', 637 | self.get_instrument_by_id, 638 | cache_mode) 639 | 640 | def get_fundamentals(self, instrument_ids, cache_mode=CACHE_FIRST): 641 | return self._search_call( 642 | instrument_ids, 643 | super(RobinhoodCachedClient, self).get_fundamentals, 644 | lambda fundamental: get_last_id_from_url(fundamental['instrument']), 645 | 'fundamental_{}', 646 | self.get_fundamental, 647 | cache_mode) 648 | 649 | def get_popularities(self, instrument_ids, cache_mode=CACHE_FIRST): 650 | return self._search_call( 651 | instrument_ids, 652 | super(RobinhoodCachedClient, self).get_popularities, 653 | lambda popularity: get_last_id_from_url(popularity['instrument']), 654 | 'popularity_{}', 655 | self.get_popularity, 656 | cache_mode) 657 | 658 | def get_ratings(self, instrument_ids, cache_mode=CACHE_FIRST): 659 | return self._search_call( 660 | instrument_ids, 661 | super(RobinhoodCachedClient, self).get_ratings, 662 | lambda rating: rating['instrument_id'], 663 | 'rating_{}', 664 | self.get_rating, 665 | cache_mode) 666 | 667 | def get_quotes(self, instrument_ids, cache_mode=CACHE_FIRST): 668 | return self._search_call( 669 | instrument_ids, 670 | super(RobinhoodCachedClient, self).get_quotes, 671 | lambda quote: get_last_id_from_url(quote['instrument']), 672 | 'quote_{}', 673 | self.get_quote, 674 | cache_mode) 675 | 676 | # TODO: get_prices 677 | # TODO: crypto 678 | # TODO: options 679 | # TODO: oauth2 680 | -------------------------------------------------------------------------------- /robinhood/RobinhoodPortfolio.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from robinhood.util import get_last_id_from_url 4 | 5 | 6 | class RobinhoodPortfolio: 7 | """Class to help with working with a portfolio as a whole.""" 8 | def __init__(self, client, client_kwargs): 9 | """Gather info together to represent the portfolio.""" 10 | # Start off by getting all of the current positions. 11 | positions = client.get_positions(**client_kwargs) 12 | position_by_instrument_id = {} 13 | for position in positions: 14 | quantity = int(float(position['quantity'])) 15 | average_buy_price = Decimal(position['average_buy_price']) 16 | instrument_id = get_last_id_from_url(position['instrument']) 17 | 18 | position_by_instrument_id[instrument_id] = { 19 | 'quantity': quantity, 20 | 'average_buy_price': average_buy_price, 21 | 'equity_cost': quantity * average_buy_price, 22 | } 23 | 24 | # Augment with instruments. 25 | instrument_ids = list(position_by_instrument_id.keys()) 26 | instruments = client.get_instruments(instrument_ids) 27 | for instrument in instruments: 28 | instrument_id = instrument['id'] 29 | position_by_instrument_id[instrument_id]['symbol'] = instrument['symbol'] 30 | position_by_instrument_id[instrument_id]['simple_name'] = instrument['simple_name'] 31 | position_by_instrument_id[instrument_id]['full_name'] = instrument['name'] 32 | position_by_instrument_id[instrument_id]['shortest_name'] = instrument['simple_name'] or instrument['name'] 33 | 34 | # Augment with popularities. 35 | popularities = client.get_popularities(instrument_ids, **client_kwargs) 36 | for popularity in popularities: 37 | instrument_id = get_last_id_from_url(popularity['instrument']) 38 | position_by_instrument_id[instrument_id]['robinhood_holders'] = popularity['num_open_positions'] 39 | 40 | # Augment with ratings. 41 | ratings = client.get_ratings(instrument_ids, **client_kwargs) 42 | for rating in ratings: 43 | instrument_id = rating['instrument_id'] 44 | num_ratings = sum(v for _, v in rating['summary'].items()) if rating['summary'] else None 45 | if num_ratings: 46 | percent_buy = rating['summary']['num_buy_ratings'] * 100 / num_ratings 47 | percent_sell = rating['summary']['num_sell_ratings'] * 100 / num_ratings 48 | position_by_instrument_id[instrument_id]['buy_rating'] = 'N/A' if not num_ratings else '{:.2f}'.format(percent_buy) 49 | position_by_instrument_id[instrument_id]['sell_rating'] = 'N/A' if not num_ratings else '{:.2f}'.format(percent_sell) 50 | 51 | # Augment with quotes. 52 | position_quotes = client.get_quotes(instrument_ids, **client_kwargs) 53 | for quote in position_quotes: 54 | instrument_id = get_last_id_from_url(quote['instrument']) 55 | position = position_by_instrument_id[instrument_id] 56 | 57 | position['last_price'] = Decimal(quote['last_trade_price']) 58 | position['equity_worth'] = position['quantity'] * position['last_price'] 59 | position['previous_close'] = Decimal(quote['previous_close']) 60 | 61 | # Store some common calculations 62 | total_equity = sum(position['equity_worth'] for position in position_by_instrument_id.values()) 63 | for position in position_by_instrument_id.values(): 64 | position['equity_percentage'] = position['equity_worth'] * 100 / total_equity 65 | position['total_price_change'] = position['last_price'] - position['average_buy_price'] 66 | position['day_price_change'] = position['last_price'] - position['previous_close'] 67 | position['day_percentage_change'] = position['day_price_change'] * 100 / position['previous_close'] 68 | position['total_percentage_change'] = position['total_price_change'] * 100 / position['average_buy_price'] if position['average_buy_price'] else 100 69 | 70 | positions_by_equity_worth = sorted( 71 | position_by_instrument_id.values(), 72 | key=lambda p: p['equity_worth'], 73 | reverse=True 74 | ) 75 | 76 | self.total_equity = total_equity 77 | self.positions_by_equity_worth = positions_by_equity_worth 78 | self.position_by_instrument_id = position_by_instrument_id 79 | self.symbol_to_instrument_id = {position['symbol']: instrument_id for instrument_id, position in position_by_instrument_id.items()} 80 | 81 | @property 82 | def symbols(self): 83 | return self.symbol_to_instrument_id.keys() 84 | 85 | @property 86 | def instrument_ids(self): 87 | return self.position_by_instrument_id.keys() 88 | 89 | @property 90 | def positions(self): 91 | return self.positions_by_equity_worth 92 | 93 | def get_position_for_symbol(self, symbol): 94 | instrument_id = self.symbol_to_instrument_id[symbol] 95 | position = self.position_by_instrument_id[instrument_id] 96 | return position 97 | -------------------------------------------------------------------------------- /robinhood/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mstrum/robinhood-python/ef02ee04f2527d6568b48fae444bc22740694a3d/robinhood/__init__.py -------------------------------------------------------------------------------- /robinhood/certs/_.apexclearing.com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFOzCCBCOgAwIBAgIIZcAw/MHZf7owDQYJKoZIhvcNAQELBQAwgbQxCzAJBgNV 3 | BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRow 4 | GAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UECxMkaHR0cDovL2NlcnRz 5 | LmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQDEypHbyBEYWRkeSBTZWN1 6 | cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwHhcNMTcwNTA5MTgzOTAwWhcN 7 | MTkwNzE1MTIyMDM4WjBAMSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0 8 | ZWQxGzAZBgNVBAMMEiouYXBleGNsZWFyaW5nLmNvbTCCASIwDQYJKoZIhvcNAQEB 9 | BQADggEPADCCAQoCggEBALPUwZ79aaxWs7M4Unnn+Kq4R8BoKtoURvzZ4Q77ZduE 10 | Q0Q845Bp6b9F8YwoEzZ+hqCI5uBT33v96EmYYiGCLYSfYntFPq60MdgIuyZlGJcc 11 | xCQg5eiaIUmRZyJ763Ts+xI+iK0WXwu2WIWnmGRlbv0kJ4xdDXtwXXwZ6MkOwGSS 12 | 54W/aLmNKxOGcOCxbp/jVTcFHHxVDWt1JZiqUNI51+zfr+vmRkR0TAf2VUart3bq 13 | 64mE6Q7lulS2ZVfDFRe7CWMJ4DObL3ZsGk6uiGv+UQE5TiyU4v+VIUnLdMg1wE0K 14 | 3pwNJZhCkENBe8NyOlSEOBa3BL2X4wJBWc3EYPHKoDMCAwEAAaOCAcIwggG+MAwG 15 | A1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1Ud 16 | DwEB/wQEAwIFoDA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY3JsLmdvZGFkZHku 17 | Y29tL2dkaWcyczEtNTA3LmNybDBdBgNVHSAEVjBUMEgGC2CGSAGG/W0BBxcBMDkw 18 | NwYIKwYBBQUHAgEWK2h0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVw 19 | b3NpdG9yeS8wCAYGZ4EMAQIBMHYGCCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYY 20 | aHR0cDovL29jc3AuZ29kYWRkeS5jb20vMEAGCCsGAQUFBzAChjRodHRwOi8vY2Vy 21 | dGlmaWNhdGVzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvZ2RpZzIuY3J0MB8GA1Ud 22 | IwQYMBaAFEDCvSeOzDSDMKIz1/tss/C0LIDOMC8GA1UdEQQoMCaCEiouYXBleGNs 23 | ZWFyaW5nLmNvbYIQYXBleGNsZWFyaW5nLmNvbTAdBgNVHQ4EFgQUJUtyU0zsrnNJ 24 | N65Vn5rXxrI8NfcwDQYJKoZIhvcNAQELBQADggEBAH9YUtmkUKdQ6eQ0gEI+l89N 25 | hk1nkQUeLZDE6sHTB+ZwCBnOQcoIKkGPPYV1FUPepAAavEXfDo6i0PSVO/IG8Yor 26 | Sbm81url4wGIATjT9gZIt4gcoNlnmT48tnDQQRPSifHCCkRq+wBysnIw8azhli7u 27 | ts3gX9yc9a6QUgz6cDBhaCVfba/L0zb92FZzXkxaM1Zf4K9+J60qb2mllQuiXdyx 28 | Xa/NanTHQNVOtHOmMvlrOQamvNYmfJzuvjAET4u/EL4B24Y4i3tcCIahLKTgNxn9 29 | OYsC5P1Ou4+pkGCmzcjD2r2nZlsjQSWz1tKd5ZyHf6JjAeYW4U9se7NUy0OGHSk= 30 | -----END CERTIFICATE----- 31 | -----BEGIN CERTIFICATE----- 32 | MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 33 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 34 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 35 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3 36 | MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 37 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE 38 | CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD 39 | EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi 40 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD 41 | BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv 42 | K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e 43 | cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY 44 | pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n 45 | eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB 46 | AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV 47 | HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv 48 | 9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v 49 | b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n 50 | b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG 51 | CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv 52 | MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz 53 | 91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2 54 | RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi 55 | DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11 56 | GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x 57 | LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB 58 | -----END CERTIFICATE----- 59 | -----BEGIN CERTIFICATE----- 60 | MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 61 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 62 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 63 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz 64 | NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 65 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE 66 | AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw 67 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD 68 | E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH 69 | /PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy 70 | DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh 71 | GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR 72 | tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA 73 | AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE 74 | FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX 75 | WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu 76 | 9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr 77 | gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 78 | 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO 79 | LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 80 | 4uJEvlz36hz1 81 | -----END CERTIFICATE----- 82 | -------------------------------------------------------------------------------- /robinhood/certs/_.s3.amazonaws.com: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFWjCCBEKgAwIBAgIQBVG1kvpTzyBSuLcPJ1zBWTANBgkqhkiG9w0BAQsFADBk 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSMwIQYDVQQDExpEaWdpQ2VydCBCYWx0aW1vcmUgQ0Et 5 | MiBHMjAeFw0xNzA5MjIwMDAwMDBaFw0xOTAxMDMxMjAwMDBaMGsxCzAJBgNVBAYT 6 | AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYD 7 | VQQKEw9BbWF6b24uY29tIEluYy4xGzAZBgNVBAMMEiouczMuYW1hem9uYXdzLmNv 8 | bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKK5it42LH/8WFaEE7O9 9 | 4XJMg48TengCM25C9O0dUaPGOnZ1+oGf7DNvdo/MGVoAW5hfPX9WqjY30KyuKiFv 10 | 8uAbyX+9Bzq8KMLjdvGxzYMqbSxeAPSBf1yMdCyDEc8gW+vh2uXO+x9QePgiOAoO 11 | qujLzyS/p7pSWPUKUH8kpMgP99RPoD/lRNbPAFr3QeJr7D/x3xNTKBHCbpE4SxBH 12 | QM02GCu2DSYQtgm3hfZmD79BB1Ej4U1vM0hgiOu9eFLwmycbnrnU8sa4DkvmUJlv 13 | xiIP5PvNweawm/QeoH6Qk/zDF30nr+5Av9IUhE4uAhl1H2OMqPp79SfJ2wxtvmNt 14 | 3z0CAwEAAaOCAf8wggH7MB8GA1UdIwQYMBaAFMASsih0aEZn6XAldBoARVsGfVxE 15 | MB0GA1UdDgQWBBSQFZo7H1VA7uODvdTP1I6idMLqyDAvBgNVHREEKDAmghIqLnMz 16 | LmFtYXpvbmF3cy5jb22CEHMzLmFtYXpvbmF3cy5jb20wDgYDVR0PAQH/BAQDAgWg 17 | MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBgQYDVR0fBHoweDA6oDig 18 | NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QmFsdGltb3JlQ0Et 19 | MkcyLmNybDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 20 | QmFsdGltb3JlQ0EtMkcyLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgG 21 | CCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEC 22 | AjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj 23 | ZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t 24 | L0RpZ2lDZXJ0QmFsdGltb3JlQ0EtMkcyLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqG 25 | SIb3DQEBCwUAA4IBAQBydwnbmrwTTwhtCckZ9lDGoE+tdDFchjFG59dyzX1I4bHV 26 | thVUwjEoc3VxngvmKb3cqLcHzXHNdoEdffRtw0IiEerPRsk7Nxh2p1HWR3cu/Hyp 27 | kqGlObJADiw1DX4UDob735hevqcGN9M0rn9P/AbPK0HCn5uokLDmbeqe6u1YgdJL 28 | m4skA/WsUsMCfsZY27RiZ+B162+laDD0oRiovOx5zKrzG/3yAFVutz2Bfud3QenU 29 | 3zt14ttnzFCcMlglW9oqffHMwQ1Smx/30ccDonoa1E02hnNmxeLyBwomPqd/ZYy6 30 | zSJW0aSi1DadkZsifpr65AwgSuN5uGEhQas0glsN 31 | -----END CERTIFICATE----- 32 | -----BEGIN CERTIFICATE----- 33 | MIIEYzCCA0ugAwIBAgIQAYL4CY6i5ia5GjsnhB+5rzANBgkqhkiG9w0BAQsFADBa 34 | MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl 35 | clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTE1 36 | MTIwODEyMDUwN1oXDTI1MDUxMDEyMDAwMFowZDELMAkGA1UEBhMCVVMxFTATBgNV 37 | BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEjMCEG 38 | A1UEAxMaRGlnaUNlcnQgQmFsdGltb3JlIENBLTIgRzIwggEiMA0GCSqGSIb3DQEB 39 | AQUAA4IBDwAwggEKAoIBAQC75wD+AAFz75uI8FwIdfBccHMf/7V6H40II/3HwRM/ 40 | sSEGvU3M2y24hxkx3tprDcFd0lHVsF5y1PBm1ITykRhBtQkmsgOWBGmVU/oHTz6+ 41 | hjpDK7JZtavRuvRZQHJaZ7bN5lX8CSukmLK/zKkf1L+Hj4Il/UWAqeydjPl0kM8c 42 | +GVQr834RavIL42ONh3e6onNslLZ5QnNNnEr2sbQm8b2pFtbObYfAB8ZpPvTvgzm 43 | +4/dDoDmpOdaxMAvcu6R84Nnyc3KzkqwIIH95HKvCRjnT0LsTSdCTQeg3dUNdfc2 44 | YMwmVJihiDfwg/etKVkgz7sl4dWe5vOuwQHrtQaJ4gqPAgMBAAGjggEZMIIBFTAd 45 | BgNVHQ4EFgQUwBKyKHRoRmfpcCV0GgBFWwZ9XEQwHwYDVR0jBBgwFoAU5Z1ZMIJH 46 | WMys+ghUNoZ7OrUETfAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC 47 | AYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 48 | Y2VydC5jb20wOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQu 49 | Y29tL09tbmlyb290MjAyNS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYB 50 | BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDQYJKoZIhvcNAQEL 51 | BQADggEBAC/iN2bDGs+RVe4pFPpQEL6ZjeIo8XQWB2k7RDA99blJ9Wg2/rcwjang 52 | B0lCY0ZStWnGm0nyGg9Xxva3vqt1jQ2iqzPkYoVDVKtjlAyjU6DqHeSmpqyVDmV4 53 | 7DOMvpQ+2HCr6sfheM4zlbv7LFjgikCmbUHY2Nmz+S8CxRtwa+I6hXsdGLDRS5rB 54 | bxcQKegOw+FUllSlkZUIII1pLJ4vP1C0LuVXH6+kc9KhJLsNkP5FEx2noSnYZgvD 55 | 0WyzT7QrhExHkOyL4kGJE7YHRndC/bseF/r/JUuOUFfrjsxOFT+xJd1BDKCcYm1v 56 | upcHi9nzBhDFKdT3uhaQqNBU4UtJx5g= 57 | -----END CERTIFICATE----- 58 | -----BEGIN CERTIFICATE----- 59 | MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ 60 | RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD 61 | VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX 62 | DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y 63 | ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy 64 | VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr 65 | mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr 66 | IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK 67 | mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu 68 | XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy 69 | dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye 70 | jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 71 | BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 72 | DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 73 | 9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx 74 | jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 75 | Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz 76 | ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS 77 | R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp 78 | -----END CERTIFICATE----- 79 | -------------------------------------------------------------------------------- /robinhood/certs/all.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIG0jCCBbqgAwIBAgIQBVYJ0dqGKw21y4y7pMLUQDANBgkqhkiG9w0BAQsFADBN 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E 4 | aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwMzA3MDAwMDAwWhcN 5 | MjAwNDA5MTIwMDAwWjCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju 6 | aWExEjAQBgNVBAcTCVBhbG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtl 7 | dHMsIEluYy4xHTAbBgNVBAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMRgwFgYDVQQD 8 | DA8qLnJvYmluaG9vZC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 9 | AQC+4gjah4hdy28DewuFuODeKpthjkcZerE3Po7KzdoVoEMUqtoEVlYHGANpmrgs 10 | A4YulURtn/LZHm+PS+pmx3mFH5X0zbE7Uu5+yijWDqTOUvhqrRifDRZvF12CQPY3 11 | GKS2cmSiBHVtJlnxIdNzLdV9tPoecopCRoNsM1vEEz37T2+DqioX8OHVmCKjZob9 12 | Q293kvIFNOifO3SObRrIzvhJBNLr3jKv+OdQsrDk2T2sD9JNV9KNzndPFhI25dmt 13 | /vt9aL/QjXxo5I05IMRr0O7MuDj7aIPFoPYo1kQgrTDrqTF8AyPQQyjm4ry2yRl7 14 | 6XCNFf5bRlj8jdDwxyS0bB+BAgMBAAGjggNnMIIDYzAfBgNVHSMEGDAWgBQPgGEc 15 | gjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUH7z8cF/mmvT1x+SAW61wlH2U+rMw 16 | KQYDVR0RBCIwIIIPKi5yb2Jpbmhvb2QuY29tgg1yb2Jpbmhvb2QuY29tMA4GA1Ud 17 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwawYDVR0f 18 | BGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1n 19 | Ni5jcmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTIt 20 | ZzYuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0 21 | dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHwGCCsGAQUFBwEB 22 | BHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEYGCCsG 23 | AQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEy 24 | U2VjdXJlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF+BgorBgEEAdZ5AgQC 25 | BIIBbgSCAWoBaAB3AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB 26 | Yf4WR7UAAAQDAEgwRgIhAMMgoMqEO/kBsw1YB1fufETVFvO+JxjcFf/PTqPxuxU5 27 | AiEAwK8h1dt09fw28ltb8LJAzt7OYMY0FOyQiPbjHUawqkEAdQBvU3asMfAxGdiZ 28 | AKRRFf93FRwR2QLBACkGjbIImjfZEwAAAWH+Fkk2AAAEAwBGMEQCIBSNsoPd328k 29 | n1B7oYuMe2AVchFQFZA8Pl3B82MDLbJlAiB26D7WO5aCzZhGIH1ucyu3gK8ltMYC 30 | HMNrmrAA661S4gB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAAB 31 | Yf4WR/8AAAQDAEcwRQIhALK5DbUlSKJAQN5vTLN+J5Dal2fYmV0ZcucGFC45kbWX 32 | AiB1lzy4Z4teF99KmL/0xVIJkffbOxDaPQQlbAJY23M1/TANBgkqhkiG9w0BAQsF 33 | AAOCAQEAb5CzPvc3wDYEmn52aj2aaTWw0D2okL5w9GaS4agbKE1j/+3CavS7tTdg 34 | vfHVLOvpy1r0vwalOg116G4B7IMMV2QXD+VOAaxKlE0cHi7e5VsOuO/ZDIiymV0+ 35 | 0QDjkL8fClyrrSrZMKkf8LBhAnnqQDXJpwqxoNkZ2nmanUd1UBlNbrAus9O2LZpK 36 | R3VNYqnhot/+jjXlYMPVtEFCmVPeHlLnaPmlAx4tlOa8eIfKOU1ve3N9nZBtxbmX 37 | Oduc22eSQFCEbuOSpu/k8rAad036K08Fo8/bT9uVKjri/1XOos+y+MTRRv1/VcwG 38 | q3/bRutCLoUp1NCSufA8e6J1/jzrlQ== 39 | -----END CERTIFICATE----- 40 | -----BEGIN CERTIFICATE----- 41 | MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh 42 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 43 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 44 | QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT 45 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg 46 | U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 47 | ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 48 | nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd 49 | KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f 50 | /ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX 51 | kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 52 | /RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C 53 | AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY 54 | aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 55 | Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 56 | oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD 57 | QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v 58 | d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh 59 | xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB 60 | CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl 61 | 5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA 62 | 8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC 63 | 2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit 64 | c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 65 | j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz 66 | -----END CERTIFICATE----- 67 | -----BEGIN CERTIFICATE----- 68 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 69 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 70 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 71 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 72 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 73 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 74 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 75 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 76 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 77 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 78 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 79 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 80 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 81 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 82 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 83 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 84 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 85 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 86 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 87 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 88 | -----END CERTIFICATE----- 89 | -----BEGIN CERTIFICATE----- 90 | MIIFOzCCBCOgAwIBAgIIZcAw/MHZf7owDQYJKoZIhvcNAQELBQAwgbQxCzAJBgNV 91 | BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRow 92 | GAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UECxMkaHR0cDovL2NlcnRz 93 | LmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQDEypHbyBEYWRkeSBTZWN1 94 | cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwHhcNMTcwNTA5MTgzOTAwWhcN 95 | MTkwNzE1MTIyMDM4WjBAMSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0 96 | ZWQxGzAZBgNVBAMMEiouYXBleGNsZWFyaW5nLmNvbTCCASIwDQYJKoZIhvcNAQEB 97 | BQADggEPADCCAQoCggEBALPUwZ79aaxWs7M4Unnn+Kq4R8BoKtoURvzZ4Q77ZduE 98 | Q0Q845Bp6b9F8YwoEzZ+hqCI5uBT33v96EmYYiGCLYSfYntFPq60MdgIuyZlGJcc 99 | xCQg5eiaIUmRZyJ763Ts+xI+iK0WXwu2WIWnmGRlbv0kJ4xdDXtwXXwZ6MkOwGSS 100 | 54W/aLmNKxOGcOCxbp/jVTcFHHxVDWt1JZiqUNI51+zfr+vmRkR0TAf2VUart3bq 101 | 64mE6Q7lulS2ZVfDFRe7CWMJ4DObL3ZsGk6uiGv+UQE5TiyU4v+VIUnLdMg1wE0K 102 | 3pwNJZhCkENBe8NyOlSEOBa3BL2X4wJBWc3EYPHKoDMCAwEAAaOCAcIwggG+MAwG 103 | A1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA4GA1Ud 104 | DwEB/wQEAwIFoDA3BgNVHR8EMDAuMCygKqAohiZodHRwOi8vY3JsLmdvZGFkZHku 105 | Y29tL2dkaWcyczEtNTA3LmNybDBdBgNVHSAEVjBUMEgGC2CGSAGG/W0BBxcBMDkw 106 | NwYIKwYBBQUHAgEWK2h0dHA6Ly9jZXJ0aWZpY2F0ZXMuZ29kYWRkeS5jb20vcmVw 107 | b3NpdG9yeS8wCAYGZ4EMAQIBMHYGCCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYY 108 | aHR0cDovL29jc3AuZ29kYWRkeS5jb20vMEAGCCsGAQUFBzAChjRodHRwOi8vY2Vy 109 | dGlmaWNhdGVzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvZ2RpZzIuY3J0MB8GA1Ud 110 | IwQYMBaAFEDCvSeOzDSDMKIz1/tss/C0LIDOMC8GA1UdEQQoMCaCEiouYXBleGNs 111 | ZWFyaW5nLmNvbYIQYXBleGNsZWFyaW5nLmNvbTAdBgNVHQ4EFgQUJUtyU0zsrnNJ 112 | N65Vn5rXxrI8NfcwDQYJKoZIhvcNAQELBQADggEBAH9YUtmkUKdQ6eQ0gEI+l89N 113 | hk1nkQUeLZDE6sHTB+ZwCBnOQcoIKkGPPYV1FUPepAAavEXfDo6i0PSVO/IG8Yor 114 | Sbm81url4wGIATjT9gZIt4gcoNlnmT48tnDQQRPSifHCCkRq+wBysnIw8azhli7u 115 | ts3gX9yc9a6QUgz6cDBhaCVfba/L0zb92FZzXkxaM1Zf4K9+J60qb2mllQuiXdyx 116 | Xa/NanTHQNVOtHOmMvlrOQamvNYmfJzuvjAET4u/EL4B24Y4i3tcCIahLKTgNxn9 117 | OYsC5P1Ou4+pkGCmzcjD2r2nZlsjQSWz1tKd5ZyHf6JjAeYW4U9se7NUy0OGHSk= 118 | -----END CERTIFICATE----- 119 | -----BEGIN CERTIFICATE----- 120 | MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 121 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 122 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 123 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3 124 | MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 125 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE 126 | CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD 127 | EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi 128 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD 129 | BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv 130 | K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e 131 | cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY 132 | pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n 133 | eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB 134 | AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV 135 | HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv 136 | 9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v 137 | b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n 138 | b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG 139 | CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv 140 | MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz 141 | 91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2 142 | RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi 143 | DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11 144 | GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x 145 | LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB 146 | -----END CERTIFICATE----- 147 | -----BEGIN CERTIFICATE----- 148 | MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx 149 | EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT 150 | EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp 151 | ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz 152 | NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH 153 | EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE 154 | AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw 155 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD 156 | E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH 157 | /PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy 158 | DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh 159 | GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR 160 | tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA 161 | AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE 162 | FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX 163 | WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu 164 | 9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr 165 | gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo 166 | 2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO 167 | LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 168 | 4uJEvlz36hz1 169 | -----END CERTIFICATE----- 170 | -----BEGIN CERTIFICATE----- 171 | MIIIBjCCBu6gAwIBAgIQDZ3f2h6gf9FpLadbtMXL6zANBgkqhkiG9w0BAQsFADB1 172 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 173 | d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk 174 | IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE3MDIyODAwMDAwMFoXDTE5MDQxNjEy 175 | MDAwMFowggEbMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjETMBEGCysG 176 | AQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhEZWxhd2FyZTEQMA4GA1UE 177 | BRMHNTQxNzY0ODEVMBMGA1UECRMMMzIwMCBBc2ggU3QuMQ4wDAYDVQQREwU5NDMw 178 | NjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBh 179 | bG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtldHMsIEluYy4xHTAbBgNV 180 | BAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMRowGAYDVQQDExFhcGkucm9iaW5ob29k 181 | LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALg2Jc3fMhakhBaO 182 | C3fKDq+b/Zi/5bi0jRjsJD5L6jwMihTJIlbHUt9ZK7Nnvs+fAWOC5hJNKckxJmpk 183 | jdjpknbGKQbdgyBqjVsl2VRoD5BHyx46z6i/eOpFYFH6rm0v7NUlyIfKhxjGQlhf 184 | xKIVTo96kfz3JfL0VMLkuHO+I26lN5+FLBA1RYyin9NjCmqPyAkxawEfYxLfm7ZN 185 | vZWJWuh3NfMXLKvuaCMq1u3deF7Nnz4LDUERu+bRTGdweghrP6sX8vH07FCOTTvu 186 | p7mzEf9W6qIWZxqVUL3KSN8iiJJU8hE9SKlkB4cW8liE3OLRg34qlqZ9H+t1Wkak 187 | JDSx+qMCAwEAAaOCA+gwggPkMB8GA1UdIwQYMBaAFD3TUKXWoK3u80pgCmXTIdT4 188 | +NYPMB0GA1UdDgQWBBRl9n6MFxKHd0szJCar/46F8epCQDAcBgNVHREEFTATghFh 189 | cGkucm9iaW5ob29kLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB 190 | BQUHAwEGCCsGAQUFBwMCMHUGA1UdHwRuMGwwNKAyoDCGLmh0dHA6Ly9jcmwzLmRp 191 | Z2ljZXJ0LmNvbS9zaGEyLWV2LXNlcnZlci1nMS5jcmwwNKAyoDCGLmh0dHA6Ly9j 192 | cmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWV2LXNlcnZlci1nMS5jcmwwSwYDVR0gBEQw 193 | QjA3BglghkgBhv1sAgEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl 194 | cnQuY29tL0NQUzAHBgVngQwBATCBiAYIKwYBBQUHAQEEfDB6MCQGCCsGAQUFBzAB 195 | hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wUgYIKwYBBQUHMAKGRmh0dHA6Ly9j 196 | YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJFeHRlbmRlZFZhbGlkYXRp 197 | b25TZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADCCAfYGCisGAQQB1nkCBAIEggHm 198 | BIIB4gHgAHUApLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BAAAAFahoFW 199 | CwAABAMARjBEAiBRG6qZzOlyg0wUsElsit/j/OI6bBQYaUCTOAw4aiBadgIgVNu5 200 | Yrh78qJV+RdWwTz/br4j7rA+q1KcwiBPzmjlXJAAdwBWFAaaL9fC7NP14b1Esj7H 201 | Rna5vJkRXMDvlJhV1onQ3QAAAVqGgVdPAAAEAwBIMEYCIQDve6ZL6lf/qk646/3T 202 | 4owkwyIAxdDo0MIHGBEeFiXEBAIhAJOdbDKvbVj4Hk2BeCYSMWBtmruUUOwfkM+N 203 | xEA83yXwAHYA7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/csAAAFahoFZ 204 | CAAABAMARzBFAiAgDkhRkVgB1H3lT5iaAe0QvkM8ssFg2Sbb+p26JnGiJAIhAIuC 205 | ZrqUePJJHCpRyyispClLTkchPBlfnV4/vVJpjo0rAHYAu9nfvB+KcbWTlCOXqpJ7 206 | RzhXlQqrUugakJZkNo4e0YUAAAFahoFWzQAABAMARzBFAiEAjDuYT2R8WPuS1U97 207 | kR7c5oDb4819G/lrhQPJzgR8BaMCIH4Om5CruEopTKI13YM3NXN666PcKQr2+AxC 208 | dVq5Gce5MA0GCSqGSIb3DQEBCwUAA4IBAQC8iPu/LTVZg9B87NfH8LiqAtN7kw5i 209 | B8IfyFgU3yQmKEx7aR+Qra0Ft+mgsE+I78/QL1mLS9A5RZIoz4MlyLXqR7u5CGVt 210 | RzxMxmiT9gF4KhEeBoS12xEibpJl/S5cVL/mDdZ9pyYSIkQ1QskD7o30bPj+wLR3 211 | WxCVVic/s9IlzUn28XdOJVAAgN/AEyhTU3Cu9PM/SEIk/s2d0qDUP4GfcyvuL+xP 212 | SV+6ZIU/os3ClxvbNUC47QS3Y5aQHMFn0Rs9qRHYEuw/ums6mFx8jQ6CgLHvS8Rh 213 | i/cubG31RKgBXhglKz81KxYPJ1oTWI6pHU5/oYrPjRsP5tLK6Dtvkqjf 214 | -----END CERTIFICATE----- 215 | -----BEGIN CERTIFICATE----- 216 | MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs 217 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 218 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 219 | ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL 220 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 221 | LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW 222 | YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 223 | ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY 224 | uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/ 225 | LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy 226 | /Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh 227 | cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k 228 | 8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB 229 | Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF 230 | BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp 231 | Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy 232 | dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2 233 | MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j 234 | b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW 235 | gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh 236 | hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg 237 | 4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa 238 | 2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs 239 | 1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1 240 | oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn 241 | 8TUoE6smftX3eg== 242 | -----END CERTIFICATE----- 243 | -----BEGIN CERTIFICATE----- 244 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs 245 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 246 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 247 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL 248 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 249 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug 250 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm 251 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW 252 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM 253 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB 254 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 255 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg 256 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF 257 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA 258 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec 259 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z 260 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF 261 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 262 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe 263 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep 264 | +OkuE6N36B9K 265 | -----END CERTIFICATE----- 266 | -----BEGIN CERTIFICATE----- 267 | MIIFSzCCBDOgAwIBAgIQAoYu7nX51E3T38wzXv7rEDANBgkqhkiG9w0BAQsFADBN 268 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E 269 | aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcxMjE5MDAwMDAwWhcN 270 | MTkxMjIzMTIwMDAwWjCBljELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju 271 | aWExEjAQBgNVBAcTCVBhbG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtl 272 | dHMsIEluYy4xHTAbBgNVBAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMR0wGwYDVQQD 273 | ExRudW1tdXMucm9iaW5ob29kLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 274 | AQoCggEBAJ4c4/mYdnJkxZpW3uJ3uT0yu1m/6ERJEC8tIryx7GaI+d9DE5tk5h2/ 275 | n053SLQFhFozYrpSOKr4taweu5Pci+RTpvs6dFydO/D3jH8oR6UIOE8WDCoJaafz 276 | zLd8JRnM6RHPFlF9pchnOW1T595WreMDTCpZ6Ft9bwZ1ohOOqcAa7yZDo76lvGw8 277 | xFYZbNN8+tMPQQcp6+E8AaCXlI4MXoWabV/WFA0Xltrmk8v0AdGWM/Q2k/H+GdJr 278 | hUxxc/nUYYj70lWBZUvPO8IivWdLg6wZVbedDg+IHbZT6aOhYnQknZRp/ExCvDAn 279 | fWuQbiZtCnk10OInM05v/qhisI3h6MsCAwEAAaOCAdswggHXMB8GA1UdIwQYMBaA 280 | FA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBRFaYQ3/DVM4dv3oXNPtpha 281 | nOxp8zAfBgNVHREEGDAWghRudW1tdXMucm9iaW5ob29kLmNvbTAOBgNVHQ8BAf8E 282 | BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGsGA1UdHwRkMGIw 283 | L6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTItZzYuY3Js 284 | MC+gLaArhilodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNy 285 | bDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUFBwIBFhxodHRwczov 286 | L3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjB8BggrBgEFBQcBAQRwMG4w 287 | JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBGBggrBgEFBQcw 288 | AoY6aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMlNlY3Vy 289 | ZVNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCj 290 | mEzMK80pDktzLrfe/rkSvLazCWl816esqmDq52dcEEF3vvKStMrOpK6yoqzca0lm 291 | IGCey/R+1qTo3JWoQUY5P2WEWVVSOHgX0XW0oadTxHlC7jDd2CrwB5VKJQcipw/G 292 | iB9RkxrfHq61u4uo+ve7aXgd7jPuxsoqdyhVn0+5bejVtC51dUciJxFF4J5gJ6l6 293 | q0CEQqR66BXVF5tMHyvmMd12wLvF294DYAI9RJrfTSq/MVCkXzQLU/RMHRL+qzpg 294 | 3tUwtEx2sBClkQVZjMuI+bFqS2sJ08C5/BhQ+AK6DM7TkNEeQzz7ztNPQrjTYQJQ 295 | JA+cit0/cz03OL3g+1zq 296 | -----END CERTIFICATE----- 297 | -----BEGIN CERTIFICATE----- 298 | MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh 299 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 300 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 301 | QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT 302 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg 303 | U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 304 | ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 305 | nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd 306 | KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f 307 | /ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX 308 | kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 309 | /RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C 310 | AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY 311 | aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 312 | Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 313 | oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD 314 | QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v 315 | d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh 316 | xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB 317 | CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl 318 | 5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA 319 | 8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC 320 | 2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit 321 | c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 322 | j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz 323 | -----END CERTIFICATE----- 324 | -----BEGIN CERTIFICATE----- 325 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 326 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 327 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 328 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 329 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 330 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 331 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 332 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 333 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 334 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 335 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 336 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 337 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 338 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 339 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 340 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 341 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 342 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 343 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 344 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 345 | -----END CERTIFICATE----- 346 | -----BEGIN CERTIFICATE----- 347 | MIIFWjCCBEKgAwIBAgIQBVG1kvpTzyBSuLcPJ1zBWTANBgkqhkiG9w0BAQsFADBk 348 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 349 | d3cuZGlnaWNlcnQuY29tMSMwIQYDVQQDExpEaWdpQ2VydCBCYWx0aW1vcmUgQ0Et 350 | MiBHMjAeFw0xNzA5MjIwMDAwMDBaFw0xOTAxMDMxMjAwMDBaMGsxCzAJBgNVBAYT 351 | AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYD 352 | VQQKEw9BbWF6b24uY29tIEluYy4xGzAZBgNVBAMMEiouczMuYW1hem9uYXdzLmNv 353 | bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKK5it42LH/8WFaEE7O9 354 | 4XJMg48TengCM25C9O0dUaPGOnZ1+oGf7DNvdo/MGVoAW5hfPX9WqjY30KyuKiFv 355 | 8uAbyX+9Bzq8KMLjdvGxzYMqbSxeAPSBf1yMdCyDEc8gW+vh2uXO+x9QePgiOAoO 356 | qujLzyS/p7pSWPUKUH8kpMgP99RPoD/lRNbPAFr3QeJr7D/x3xNTKBHCbpE4SxBH 357 | QM02GCu2DSYQtgm3hfZmD79BB1Ej4U1vM0hgiOu9eFLwmycbnrnU8sa4DkvmUJlv 358 | xiIP5PvNweawm/QeoH6Qk/zDF30nr+5Av9IUhE4uAhl1H2OMqPp79SfJ2wxtvmNt 359 | 3z0CAwEAAaOCAf8wggH7MB8GA1UdIwQYMBaAFMASsih0aEZn6XAldBoARVsGfVxE 360 | MB0GA1UdDgQWBBSQFZo7H1VA7uODvdTP1I6idMLqyDAvBgNVHREEKDAmghIqLnMz 361 | LmFtYXpvbmF3cy5jb22CEHMzLmFtYXpvbmF3cy5jb20wDgYDVR0PAQH/BAQDAgWg 362 | MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjCBgQYDVR0fBHoweDA6oDig 363 | NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QmFsdGltb3JlQ0Et 364 | MkcyLmNybDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 365 | QmFsdGltb3JlQ0EtMkcyLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgG 366 | CCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEC 367 | AjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj 368 | ZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t 369 | L0RpZ2lDZXJ0QmFsdGltb3JlQ0EtMkcyLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqG 370 | SIb3DQEBCwUAA4IBAQBydwnbmrwTTwhtCckZ9lDGoE+tdDFchjFG59dyzX1I4bHV 371 | thVUwjEoc3VxngvmKb3cqLcHzXHNdoEdffRtw0IiEerPRsk7Nxh2p1HWR3cu/Hyp 372 | kqGlObJADiw1DX4UDob735hevqcGN9M0rn9P/AbPK0HCn5uokLDmbeqe6u1YgdJL 373 | m4skA/WsUsMCfsZY27RiZ+B162+laDD0oRiovOx5zKrzG/3yAFVutz2Bfud3QenU 374 | 3zt14ttnzFCcMlglW9oqffHMwQ1Smx/30ccDonoa1E02hnNmxeLyBwomPqd/ZYy6 375 | zSJW0aSi1DadkZsifpr65AwgSuN5uGEhQas0glsN 376 | -----END CERTIFICATE----- 377 | -----BEGIN CERTIFICATE----- 378 | MIIEYzCCA0ugAwIBAgIQAYL4CY6i5ia5GjsnhB+5rzANBgkqhkiG9w0BAQsFADBa 379 | MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl 380 | clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTE1 381 | MTIwODEyMDUwN1oXDTI1MDUxMDEyMDAwMFowZDELMAkGA1UEBhMCVVMxFTATBgNV 382 | BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEjMCEG 383 | A1UEAxMaRGlnaUNlcnQgQmFsdGltb3JlIENBLTIgRzIwggEiMA0GCSqGSIb3DQEB 384 | AQUAA4IBDwAwggEKAoIBAQC75wD+AAFz75uI8FwIdfBccHMf/7V6H40II/3HwRM/ 385 | sSEGvU3M2y24hxkx3tprDcFd0lHVsF5y1PBm1ITykRhBtQkmsgOWBGmVU/oHTz6+ 386 | hjpDK7JZtavRuvRZQHJaZ7bN5lX8CSukmLK/zKkf1L+Hj4Il/UWAqeydjPl0kM8c 387 | +GVQr834RavIL42ONh3e6onNslLZ5QnNNnEr2sbQm8b2pFtbObYfAB8ZpPvTvgzm 388 | +4/dDoDmpOdaxMAvcu6R84Nnyc3KzkqwIIH95HKvCRjnT0LsTSdCTQeg3dUNdfc2 389 | YMwmVJihiDfwg/etKVkgz7sl4dWe5vOuwQHrtQaJ4gqPAgMBAAGjggEZMIIBFTAd 390 | BgNVHQ4EFgQUwBKyKHRoRmfpcCV0GgBFWwZ9XEQwHwYDVR0jBBgwFoAU5Z1ZMIJH 391 | WMys+ghUNoZ7OrUETfAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC 392 | AYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 393 | Y2VydC5jb20wOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQu 394 | Y29tL09tbmlyb290MjAyNS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYB 395 | BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDQYJKoZIhvcNAQEL 396 | BQADggEBAC/iN2bDGs+RVe4pFPpQEL6ZjeIo8XQWB2k7RDA99blJ9Wg2/rcwjang 397 | B0lCY0ZStWnGm0nyGg9Xxva3vqt1jQ2iqzPkYoVDVKtjlAyjU6DqHeSmpqyVDmV4 398 | 7DOMvpQ+2HCr6sfheM4zlbv7LFjgikCmbUHY2Nmz+S8CxRtwa+I6hXsdGLDRS5rB 399 | bxcQKegOw+FUllSlkZUIII1pLJ4vP1C0LuVXH6+kc9KhJLsNkP5FEx2noSnYZgvD 400 | 0WyzT7QrhExHkOyL4kGJE7YHRndC/bseF/r/JUuOUFfrjsxOFT+xJd1BDKCcYm1v 401 | upcHi9nzBhDFKdT3uhaQqNBU4UtJx5g= 402 | -----END CERTIFICATE----- 403 | -----BEGIN CERTIFICATE----- 404 | MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ 405 | RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD 406 | VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX 407 | DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y 408 | ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy 409 | VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr 410 | mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr 411 | IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK 412 | mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu 413 | XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy 414 | dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye 415 | jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 416 | BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 417 | DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 418 | 9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx 419 | jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 420 | Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz 421 | ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS 422 | R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp 423 | -----END CERTIFICATE----- 424 | -------------------------------------------------------------------------------- /robinhood/certs/analytics.robinhood.com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIG0jCCBbqgAwIBAgIQBVYJ0dqGKw21y4y7pMLUQDANBgkqhkiG9w0BAQsFADBN 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E 4 | aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwMzA3MDAwMDAwWhcN 5 | MjAwNDA5MTIwMDAwWjCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju 6 | aWExEjAQBgNVBAcTCVBhbG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtl 7 | dHMsIEluYy4xHTAbBgNVBAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMRgwFgYDVQQD 8 | DA8qLnJvYmluaG9vZC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 9 | AQC+4gjah4hdy28DewuFuODeKpthjkcZerE3Po7KzdoVoEMUqtoEVlYHGANpmrgs 10 | A4YulURtn/LZHm+PS+pmx3mFH5X0zbE7Uu5+yijWDqTOUvhqrRifDRZvF12CQPY3 11 | GKS2cmSiBHVtJlnxIdNzLdV9tPoecopCRoNsM1vEEz37T2+DqioX8OHVmCKjZob9 12 | Q293kvIFNOifO3SObRrIzvhJBNLr3jKv+OdQsrDk2T2sD9JNV9KNzndPFhI25dmt 13 | /vt9aL/QjXxo5I05IMRr0O7MuDj7aIPFoPYo1kQgrTDrqTF8AyPQQyjm4ry2yRl7 14 | 6XCNFf5bRlj8jdDwxyS0bB+BAgMBAAGjggNnMIIDYzAfBgNVHSMEGDAWgBQPgGEc 15 | gjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUH7z8cF/mmvT1x+SAW61wlH2U+rMw 16 | KQYDVR0RBCIwIIIPKi5yb2Jpbmhvb2QuY29tgg1yb2Jpbmhvb2QuY29tMA4GA1Ud 17 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwawYDVR0f 18 | BGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1n 19 | Ni5jcmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTIt 20 | ZzYuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0 21 | dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHwGCCsGAQUFBwEB 22 | BHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEYGCCsG 23 | AQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEy 24 | U2VjdXJlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF+BgorBgEEAdZ5AgQC 25 | BIIBbgSCAWoBaAB3AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7IDdwQAAAB 26 | Yf4WR7UAAAQDAEgwRgIhAMMgoMqEO/kBsw1YB1fufETVFvO+JxjcFf/PTqPxuxU5 27 | AiEAwK8h1dt09fw28ltb8LJAzt7OYMY0FOyQiPbjHUawqkEAdQBvU3asMfAxGdiZ 28 | AKRRFf93FRwR2QLBACkGjbIImjfZEwAAAWH+Fkk2AAAEAwBGMEQCIBSNsoPd328k 29 | n1B7oYuMe2AVchFQFZA8Pl3B82MDLbJlAiB26D7WO5aCzZhGIH1ucyu3gK8ltMYC 30 | HMNrmrAA661S4gB2AO5Lvbd1zmC64UJpH6vhnmajD35fsHLYgwDEe4l6qP3LAAAB 31 | Yf4WR/8AAAQDAEcwRQIhALK5DbUlSKJAQN5vTLN+J5Dal2fYmV0ZcucGFC45kbWX 32 | AiB1lzy4Z4teF99KmL/0xVIJkffbOxDaPQQlbAJY23M1/TANBgkqhkiG9w0BAQsF 33 | AAOCAQEAb5CzPvc3wDYEmn52aj2aaTWw0D2okL5w9GaS4agbKE1j/+3CavS7tTdg 34 | vfHVLOvpy1r0vwalOg116G4B7IMMV2QXD+VOAaxKlE0cHi7e5VsOuO/ZDIiymV0+ 35 | 0QDjkL8fClyrrSrZMKkf8LBhAnnqQDXJpwqxoNkZ2nmanUd1UBlNbrAus9O2LZpK 36 | R3VNYqnhot/+jjXlYMPVtEFCmVPeHlLnaPmlAx4tlOa8eIfKOU1ve3N9nZBtxbmX 37 | Oduc22eSQFCEbuOSpu/k8rAad036K08Fo8/bT9uVKjri/1XOos+y+MTRRv1/VcwG 38 | q3/bRutCLoUp1NCSufA8e6J1/jzrlQ== 39 | -----END CERTIFICATE----- 40 | -----BEGIN CERTIFICATE----- 41 | MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh 42 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 43 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 44 | QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT 45 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg 46 | U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 47 | ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 48 | nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd 49 | KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f 50 | /ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX 51 | kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 52 | /RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C 53 | AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY 54 | aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 55 | Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 56 | oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD 57 | QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v 58 | d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh 59 | xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB 60 | CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl 61 | 5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA 62 | 8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC 63 | 2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit 64 | c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 65 | j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz 66 | -----END CERTIFICATE----- 67 | -----BEGIN CERTIFICATE----- 68 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 69 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 70 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 71 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 72 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 73 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 74 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 75 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 76 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 77 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 78 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 79 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 80 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 81 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 82 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 83 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 84 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 85 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 86 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 87 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 88 | -----END CERTIFICATE----- 89 | -------------------------------------------------------------------------------- /robinhood/certs/api.robinhood.com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIIBjCCBu6gAwIBAgIQDZ3f2h6gf9FpLadbtMXL6zANBgkqhkiG9w0BAQsFADB1 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk 5 | IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE3MDIyODAwMDAwMFoXDTE5MDQxNjEy 6 | MDAwMFowggEbMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjETMBEGCysG 7 | AQQBgjc8AgEDEwJVUzEZMBcGCysGAQQBgjc8AgECEwhEZWxhd2FyZTEQMA4GA1UE 8 | BRMHNTQxNzY0ODEVMBMGA1UECRMMMzIwMCBBc2ggU3QuMQ4wDAYDVQQREwU5NDMw 9 | NjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBh 10 | bG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtldHMsIEluYy4xHTAbBgNV 11 | BAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMRowGAYDVQQDExFhcGkucm9iaW5ob29k 12 | LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALg2Jc3fMhakhBaO 13 | C3fKDq+b/Zi/5bi0jRjsJD5L6jwMihTJIlbHUt9ZK7Nnvs+fAWOC5hJNKckxJmpk 14 | jdjpknbGKQbdgyBqjVsl2VRoD5BHyx46z6i/eOpFYFH6rm0v7NUlyIfKhxjGQlhf 15 | xKIVTo96kfz3JfL0VMLkuHO+I26lN5+FLBA1RYyin9NjCmqPyAkxawEfYxLfm7ZN 16 | vZWJWuh3NfMXLKvuaCMq1u3deF7Nnz4LDUERu+bRTGdweghrP6sX8vH07FCOTTvu 17 | p7mzEf9W6qIWZxqVUL3KSN8iiJJU8hE9SKlkB4cW8liE3OLRg34qlqZ9H+t1Wkak 18 | JDSx+qMCAwEAAaOCA+gwggPkMB8GA1UdIwQYMBaAFD3TUKXWoK3u80pgCmXTIdT4 19 | +NYPMB0GA1UdDgQWBBRl9n6MFxKHd0szJCar/46F8epCQDAcBgNVHREEFTATghFh 20 | cGkucm9iaW5ob29kLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB 21 | BQUHAwEGCCsGAQUFBwMCMHUGA1UdHwRuMGwwNKAyoDCGLmh0dHA6Ly9jcmwzLmRp 22 | Z2ljZXJ0LmNvbS9zaGEyLWV2LXNlcnZlci1nMS5jcmwwNKAyoDCGLmh0dHA6Ly9j 23 | cmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWV2LXNlcnZlci1nMS5jcmwwSwYDVR0gBEQw 24 | QjA3BglghkgBhv1sAgEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl 25 | cnQuY29tL0NQUzAHBgVngQwBATCBiAYIKwYBBQUHAQEEfDB6MCQGCCsGAQUFBzAB 26 | hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wUgYIKwYBBQUHMAKGRmh0dHA6Ly9j 27 | YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJFeHRlbmRlZFZhbGlkYXRp 28 | b25TZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADCCAfYGCisGAQQB1nkCBAIEggHm 29 | BIIB4gHgAHUApLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BAAAAFahoFW 30 | CwAABAMARjBEAiBRG6qZzOlyg0wUsElsit/j/OI6bBQYaUCTOAw4aiBadgIgVNu5 31 | Yrh78qJV+RdWwTz/br4j7rA+q1KcwiBPzmjlXJAAdwBWFAaaL9fC7NP14b1Esj7H 32 | Rna5vJkRXMDvlJhV1onQ3QAAAVqGgVdPAAAEAwBIMEYCIQDve6ZL6lf/qk646/3T 33 | 4owkwyIAxdDo0MIHGBEeFiXEBAIhAJOdbDKvbVj4Hk2BeCYSMWBtmruUUOwfkM+N 34 | xEA83yXwAHYA7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/csAAAFahoFZ 35 | CAAABAMARzBFAiAgDkhRkVgB1H3lT5iaAe0QvkM8ssFg2Sbb+p26JnGiJAIhAIuC 36 | ZrqUePJJHCpRyyispClLTkchPBlfnV4/vVJpjo0rAHYAu9nfvB+KcbWTlCOXqpJ7 37 | RzhXlQqrUugakJZkNo4e0YUAAAFahoFWzQAABAMARzBFAiEAjDuYT2R8WPuS1U97 38 | kR7c5oDb4819G/lrhQPJzgR8BaMCIH4Om5CruEopTKI13YM3NXN666PcKQr2+AxC 39 | dVq5Gce5MA0GCSqGSIb3DQEBCwUAA4IBAQC8iPu/LTVZg9B87NfH8LiqAtN7kw5i 40 | B8IfyFgU3yQmKEx7aR+Qra0Ft+mgsE+I78/QL1mLS9A5RZIoz4MlyLXqR7u5CGVt 41 | RzxMxmiT9gF4KhEeBoS12xEibpJl/S5cVL/mDdZ9pyYSIkQ1QskD7o30bPj+wLR3 42 | WxCVVic/s9IlzUn28XdOJVAAgN/AEyhTU3Cu9PM/SEIk/s2d0qDUP4GfcyvuL+xP 43 | SV+6ZIU/os3ClxvbNUC47QS3Y5aQHMFn0Rs9qRHYEuw/ums6mFx8jQ6CgLHvS8Rh 44 | i/cubG31RKgBXhglKz81KxYPJ1oTWI6pHU5/oYrPjRsP5tLK6Dtvkqjf 45 | -----END CERTIFICATE----- 46 | -----BEGIN CERTIFICATE----- 47 | MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs 48 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 49 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 50 | ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL 51 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 52 | LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW 53 | YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 54 | ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY 55 | uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/ 56 | LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy 57 | /Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh 58 | cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k 59 | 8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB 60 | Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF 61 | BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp 62 | Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy 63 | dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2 64 | MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j 65 | b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW 66 | gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh 67 | hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg 68 | 4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa 69 | 2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs 70 | 1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1 71 | oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn 72 | 8TUoE6smftX3eg== 73 | -----END CERTIFICATE----- 74 | -----BEGIN CERTIFICATE----- 75 | MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs 76 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 77 | d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j 78 | ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL 79 | MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 80 | LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug 81 | RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm 82 | +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW 83 | PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM 84 | xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB 85 | Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 86 | hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg 87 | EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF 88 | MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA 89 | FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec 90 | nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z 91 | eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF 92 | hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 93 | Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe 94 | vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep 95 | +OkuE6N36B9K 96 | -----END CERTIFICATE----- 97 | -------------------------------------------------------------------------------- /robinhood/certs/nummus.robinhood.com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFSzCCBDOgAwIBAgIQAoYu7nX51E3T38wzXv7rEDANBgkqhkiG9w0BAQsFADBN 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E 4 | aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcxMjE5MDAwMDAwWhcN 5 | MTkxMjIzMTIwMDAwWjCBljELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju 6 | aWExEjAQBgNVBAcTCVBhbG8gQWx0bzEgMB4GA1UEChMXUm9iaW5ob29kIE1hcmtl 7 | dHMsIEluYy4xHTAbBgNVBAsTFFRlY2huaWNhbCBPcGVyYXRpb25zMR0wGwYDVQQD 8 | ExRudW1tdXMucm9iaW5ob29kLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 9 | AQoCggEBAJ4c4/mYdnJkxZpW3uJ3uT0yu1m/6ERJEC8tIryx7GaI+d9DE5tk5h2/ 10 | n053SLQFhFozYrpSOKr4taweu5Pci+RTpvs6dFydO/D3jH8oR6UIOE8WDCoJaafz 11 | zLd8JRnM6RHPFlF9pchnOW1T595WreMDTCpZ6Ft9bwZ1ohOOqcAa7yZDo76lvGw8 12 | xFYZbNN8+tMPQQcp6+E8AaCXlI4MXoWabV/WFA0Xltrmk8v0AdGWM/Q2k/H+GdJr 13 | hUxxc/nUYYj70lWBZUvPO8IivWdLg6wZVbedDg+IHbZT6aOhYnQknZRp/ExCvDAn 14 | fWuQbiZtCnk10OInM05v/qhisI3h6MsCAwEAAaOCAdswggHXMB8GA1UdIwQYMBaA 15 | FA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBRFaYQ3/DVM4dv3oXNPtpha 16 | nOxp8zAfBgNVHREEGDAWghRudW1tdXMucm9iaW5ob29kLmNvbTAOBgNVHQ8BAf8E 17 | BAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGsGA1UdHwRkMGIw 18 | L6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTItZzYuY3Js 19 | MC+gLaArhilodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc3NjYS1zaGEyLWc2LmNy 20 | bDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUFBwIBFhxodHRwczov 21 | L3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjB8BggrBgEFBQcBAQRwMG4w 22 | JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBGBggrBgEFBQcw 23 | AoY6aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMlNlY3Vy 24 | ZVNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCj 25 | mEzMK80pDktzLrfe/rkSvLazCWl816esqmDq52dcEEF3vvKStMrOpK6yoqzca0lm 26 | IGCey/R+1qTo3JWoQUY5P2WEWVVSOHgX0XW0oadTxHlC7jDd2CrwB5VKJQcipw/G 27 | iB9RkxrfHq61u4uo+ve7aXgd7jPuxsoqdyhVn0+5bejVtC51dUciJxFF4J5gJ6l6 28 | q0CEQqR66BXVF5tMHyvmMd12wLvF294DYAI9RJrfTSq/MVCkXzQLU/RMHRL+qzpg 29 | 3tUwtEx2sBClkQVZjMuI+bFqS2sJ08C5/BhQ+AK6DM7TkNEeQzz7ztNPQrjTYQJQ 30 | JA+cit0/cz03OL3g+1zq 31 | -----END CERTIFICATE----- 32 | -----BEGIN CERTIFICATE----- 33 | MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh 34 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 35 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 36 | QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT 37 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg 38 | U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 39 | ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 40 | nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd 41 | KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f 42 | /ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX 43 | kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 44 | /RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C 45 | AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY 46 | aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 47 | Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 48 | oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD 49 | QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v 50 | d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh 51 | xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB 52 | CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl 53 | 5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA 54 | 8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC 55 | 2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit 56 | c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 57 | j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz 58 | -----END CERTIFICATE----- 59 | -----BEGIN CERTIFICATE----- 60 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 61 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 62 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 63 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 64 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 65 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 66 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 67 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 68 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 69 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 70 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 71 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 72 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 73 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 74 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 75 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 76 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 77 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 78 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 79 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 80 | -----END CERTIFICATE----- 81 | -------------------------------------------------------------------------------- /robinhood/exceptions.py: -------------------------------------------------------------------------------- 1 | class NotFound(Exception): 2 | pass 3 | 4 | class NotLoggedIn(Exception): 5 | pass 6 | 7 | class MfaRequired(Exception): 8 | pass 9 | 10 | class BadRequest(Exception): 11 | pass 12 | 13 | class TooManyRequests(Exception): 14 | pass 15 | 16 | class Forbidden(Exception): 17 | pass 18 | -------------------------------------------------------------------------------- /robinhood/util.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import parse_qs, urlparse 2 | import os 3 | 4 | CURRENT_DIRECTORY = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 5 | API_HOST = 'https://api.robinhood.com/' 6 | CERT_BUNDLE_PATH = os.path.join(CURRENT_DIRECTORY, 'certs', 'all.pem') 7 | ANALYTICS_HOST = 'https://analytics.robinhood.com/' 8 | NUMMUS_HOST = 'https://nummus.robinhood.com/' 9 | NUMMUS = 'NUMMUS' 10 | API = 'API' 11 | ANALYTICS = 'ANALYTICS' 12 | 13 | 14 | ORDER_TYPES = [ 15 | 'market', 16 | 'limit', 17 | ] 18 | ORDER_SIDES = [ 19 | 'buy', 20 | 'sell', 21 | ] 22 | TIME_IN_FORCES = [ 23 | 'gfd', 24 | 'gtc', 25 | 'ioc', 26 | 'opg', 27 | ] 28 | TRIGGERS = [ 29 | 'immediate', 30 | 'stop', 31 | ] 32 | ORDER_STATES = [ 33 | 'queued', 34 | 'unconfirmed', 35 | 'confirmed', 36 | 'partially_filled', 37 | 'filled', 38 | 'rejected', 39 | 'canceled', 40 | 'failed', 41 | ] 42 | TRADABILITY = [ 43 | 'tradable', 44 | 'untradable', 45 | ] 46 | INTERVALS = [ 47 | '15second', 48 | '5minute', 49 | '10minute', 50 | 'hour', 51 | 'day', 52 | 'week', 53 | ] 54 | SPANS = [ 55 | 'day', 56 | 'hour', 57 | 'week', 58 | 'year', 59 | '5year', 60 | 'all', 61 | ] 62 | BOUNDS = [ 63 | '24_7', 64 | 'regular', 65 | 'extended', 66 | 'trading', 67 | ] 68 | OPTIONS_TYPES = [ 69 | 'put', 70 | 'call', 71 | ] 72 | OPTIONS_STATES = [ 73 | 'active', 74 | 'inactive', 75 | 'expired', 76 | ] 77 | DIRECTIONS = [ 78 | 'debit', 79 | 'credit', 80 | ] 81 | DIRECTION_TO_ORDER_SIDE = { 82 | 'debit': 'buy', 83 | 'credit': 'sell', 84 | } 85 | ORDER_SIDE_TO_DIRECTION = { 86 | 'buy': 'debit', 87 | 'sell': 'credit', 88 | } 89 | 90 | 91 | def get_last_id_from_url(url): 92 | item_id = urlparse(url).path.split('/')[-2] 93 | # Make sure this is in the expected format 94 | assert len(item_id) == 36 # UUID v4 length 95 | assert item_id[14] == '4' # UUID v4 marker 96 | return item_id 97 | 98 | 99 | def get_cursor_from_url(url): 100 | parsed_query = parse_qs(urlparse(url).query) 101 | assert 'cursor' in parsed_query 102 | assert len(parsed_query.get('cursor', [])) == 1 103 | cursor = parsed_query['cursor'][0] 104 | assert cursor 105 | return cursor 106 | 107 | 108 | def get_document_download_url_from_id(document_id): 109 | return '{}documents/{}/download/'.format(API_HOST, document_id) 110 | 111 | 112 | def instrument_id_to_url(instrument_id): 113 | return '{}instruments/{}/'.format(API_HOST, instrument_id) 114 | 115 | def options_instrument_id_to_url(instrument_id): 116 | return '{}options/instruments/{}/'.format(API_HOST, instrument_id) 117 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """setup.py for core library""" 2 | 3 | from setuptools import setup 4 | 5 | __version__ = '1.0.0' 6 | 7 | setup( 8 | name='robinhood-python', 9 | author='Matt Strum', 10 | url='https://github.com/mstrum/robinhood-python', 11 | version=__version__, 12 | license='Apache', 13 | keywords='Trade API for Robinhood', 14 | packages=['robinhood'], 15 | package_data={ 16 | 'robinhood': ['certs/*'] 17 | }, 18 | install_requires=[ 19 | 'requests >= 2.18.4', 20 | 'python-dateutil >= 2.6.1', 21 | 'pytz >= 2018.3' 22 | ], 23 | extras_require={ 24 | 'dev': [ 25 | 'flake8 >= 3.5.0' 26 | ] 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /show_crypto_quote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from datetime import datetime 5 | from decimal import Decimal 6 | from math import ceil 7 | 8 | from dateutil.parser import parse 9 | import pytz 10 | 11 | from robinhood.exceptions import NotFound 12 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 13 | 14 | 15 | def display_crypto_quote(client, symbols, cache_mode): 16 | currency_pairs = client.get_crypto_currency_pairs() 17 | currency_pair_by_id = { 18 | currency_pair['id']: currency_pair for currency_pair in currency_pairs 19 | } 20 | 21 | if symbols: 22 | quotes = client.get_crypto_quotes(symbols=symbols) 23 | else: 24 | quotes = client.get_crypto_quotes(currency_pair_ids=list(currency_pair_by_id.keys())) 25 | 26 | for quote in quotes: 27 | currency_pair = currency_pair_by_id[quote['id']] 28 | ask = Decimal(quote['ask_price']) 29 | bid = Decimal(quote['bid_price']) 30 | low = Decimal(quote['low_price']) 31 | high = Decimal(quote['high_price']) 32 | mark = Decimal(quote['mark_price']) 33 | 34 | print('') 35 | print('Crypto:\t{} ({})'.format(quote['symbol'], currency_pair['name'])) 36 | print('L/H:\t${:.2f} <-> ${:.2f}'.format(low, high)) 37 | if bid != ask: 38 | print('Spread:\t${:.2f} <-> ${:.2f}'.format(bid, ask)) 39 | print('Mark:\t${:.2f}'.format(mark)) 40 | print('Vol:\t{}'.format(int(round(float(quote['volume']))))) 41 | 42 | if __name__ == '__main__': 43 | parser = argparse.ArgumentParser(description='Get a quote for a cryptocurrency') 44 | parser.add_argument('-s', '--symbol', nargs='*', type=str.upper, dest='symbols', default=[], help='A symbol to get a quote on') 45 | parser.add_argument( 46 | '--live', 47 | action='store_true', 48 | help='Force to not use cache for APIs where values change' 49 | ) 50 | args = parser.parse_args() 51 | 52 | client = RobinhoodCachedClient() 53 | client.login() 54 | display_crypto_quote(client, args.symbols, FORCE_LIVE if args.live else CACHE_FIRST) 55 | -------------------------------------------------------------------------------- /show_interesting_stocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | 5 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 6 | from robinhood.util import get_last_id_from_url 7 | 8 | client = RobinhoodCachedClient() 9 | client.login() 10 | 11 | 12 | def display_popular_stocks(): 13 | popular_stocks = client.get_popular_stocks()['data'] 14 | 15 | print('') 16 | print('----------------- Popular S&P 500 Stocks With Robinhood Traders -----------------') 17 | for popular_stock in popular_stocks: 18 | print('{}\t({})'.format(popular_stock['symbol'], popular_stock['subtitle'])) 19 | 20 | 21 | def display_sp500_movers(): 22 | print('') 23 | print('----------------- Top S&P 500 Movers -----------------') 24 | for direction in ['up', 'down']: 25 | extra_percentage_symbol = '+' if direction == 'up' else '' 26 | movers = client.get_sp500_movers(direction, cache_mode=FORCE_LIVE) 27 | if direction == 'down': 28 | movers.reverse() 29 | 30 | 31 | instruments = client.get_instruments([get_last_id_from_url(mover['instrument_url']) for mover in movers]) 32 | instruments_by_id = {i['id']: i for i in instruments} 33 | 34 | for mover in movers: 35 | last_price = Decimal(mover['price_movement']['market_hours_last_price']) 36 | movement_pct = Decimal(mover['price_movement']['market_hours_last_movement_pct']) 37 | instrument = instruments_by_id[get_last_id_from_url(mover['instrument_url'])] 38 | 39 | print('${:.2f}\t({}{}%)\t{}\t({})'.format( 40 | last_price, extra_percentage_symbol, movement_pct, instrument['symbol'], instrument['simple_name'] or instrument['name'])) 41 | 42 | 43 | def display_for_you(): 44 | print('') 45 | print('----------------- Stocks For You -----------------') 46 | reasons = client.get_instrument_reasons_for_personal_tag('for-you') 47 | for reason in reasons: 48 | print('{}\t{}'.format(reason['symbol'], reason['reason'])) 49 | 50 | def display_top_movers(): 51 | print('') 52 | print('----------------- Top Movers -----------------') 53 | instrument_ids = client.get_instrument_ids_for_tag('top-movers', cache_mode=FORCE_LIVE) 54 | instruments = client.get_instruments(instrument_ids) 55 | for instrument in instruments: 56 | print('{}\t{}'.format(instrument['symbol'], instrument['simple_name'] or instrument['name'])) 57 | 58 | def display_100_most_popular(): 59 | print('') 60 | print('----------------- 100 Most Popular Stocks With Robinhood Users -----------------') 61 | instrument_ids = client.get_instrument_ids_for_tag('100-most-popular', cache_mode=FORCE_LIVE) 62 | instruments = client.get_instruments(instrument_ids) 63 | for instrument in instruments: 64 | print('{}\t{}'.format(instrument['symbol'], instrument['simple_name'] or instrument['name'])) 65 | 66 | 67 | if __name__ == '__main__': 68 | display_popular_stocks() 69 | display_sp500_movers() 70 | display_top_movers() 71 | display_for_you() 72 | display_100_most_popular() 73 | print('') 74 | -------------------------------------------------------------------------------- /show_options_discoveries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | import argparse 5 | import json 6 | 7 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 8 | from robinhood.util import get_last_id_from_url 9 | 10 | client = RobinhoodCachedClient() 11 | client.login() 12 | 13 | 14 | def display_options_discoveries(symbol, cache_mode): 15 | try: 16 | instrument = client.get_instrument_by_symbol(symbol) 17 | except NotFound: 18 | print('symbol {} was not found'.format(symbol)) 19 | exit() 20 | else: 21 | instrument_id = instrument['id'] 22 | name = instrument['simple_name'] or instrument['name'] 23 | 24 | options_chains = [c for c in client.get_options_chains(instrument_ids=[instrument_id]) if c['can_open_position']] 25 | if len(options_chains) != 1: 26 | raise Exception('Expected exactly one options chains listing, but got: {}'.format(json.dumps(options_chains, indent=4))) 27 | options_chain = options_chains[0] 28 | 29 | if len(options_chain['underlying_instruments']) != 1: 30 | raise Exception('Expected exactly one underlying instrument, but got: {}'.format(json.dumps(options_chain, indent=4))) 31 | if not options_chain['can_open_position']: 32 | raise Exception("Can't open position: {}".format(json.dumps(options_chain, indent=4))) 33 | chain_id = options_chain['id'] 34 | options_instrument_id = options_chain['underlying_instruments'][0]['id'] 35 | multiplier = Decimal(options_chain['trade_value_multiplier']) 36 | 37 | discoveries = client.get_options_discoveries(chain_id) 38 | option_instrument_ids = [discovery['legs'][0]['option_id'] for discovery in discoveries] 39 | options_instruments = client.get_options_instruments(options_instrument_ids=option_instrument_ids) 40 | options_instruments_by_id = { 41 | options_instrument['id']: options_instrument for options_instrument in options_instruments 42 | } 43 | 44 | options_quotes = client.get_options_marketdatas(option_instrument_ids) 45 | options_quote_by_id = { 46 | get_last_id_from_url(options_quote['instrument']): options_quote for options_quote in options_quotes 47 | } 48 | 49 | for discovery in discoveries: 50 | print('') 51 | print('----------------------------------------------') 52 | print(discovery['description']) 53 | print(discovery['strategy_type']) 54 | print(discovery['strategy_category']) 55 | print(', '.join(discovery['tags'])) 56 | print(discovery['legs'][0]['side']) 57 | option_instrument_id = discovery['legs'][0]['option_id'] 58 | option_instrument = options_instruments_by_id[option_instrument_id] 59 | print('Option') 60 | print('\tType\t{}'.format(option_instrument['type'])) 61 | print('\tExpires\t{}'.format(option_instrument['expiration_date'])) 62 | print('\tStrike\t{}'.format(Decimal(option_instrument['strike_price']))) 63 | options_quote = options_quote_by_id[option_instrument_id] 64 | adjusted_mark_price = Decimal(options_quote['adjusted_mark_price']) 65 | print('\tPrice\t${:.2f}'.format(adjusted_mark_price)) 66 | print('\tCost\t${:.2f}'.format(adjusted_mark_price * 100)) 67 | 68 | 69 | if __name__ == '__main__': 70 | parser = argparse.ArgumentParser(description='Discover options for a symbol') 71 | parser.add_argument('symbol', type=str.upper, help='A symbol to discover options for') 72 | parser.add_argument( 73 | '--live', 74 | action='store_true', 75 | help='Force to not use cache for APIs where values change' 76 | ) 77 | args = parser.parse_args() 78 | display_options_discoveries( 79 | args.symbol, 80 | FORCE_LIVE if args.live else CACHE_FIRST 81 | ) 82 | -------------------------------------------------------------------------------- /show_options_quote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | from decimal import Decimal 5 | from math import ceil 6 | import argparse 7 | import json 8 | 9 | from dateutil.parser import parse 10 | import pytz 11 | 12 | from robinhood.exceptions import NotFound 13 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 14 | from robinhood.util import get_last_id_from_url, OPTIONS_TYPES 15 | 16 | 17 | def display_options_quote(client, options_type, symbol, dates, strike, cache_mode): 18 | try: 19 | instrument = client.get_instrument_by_symbol(symbol) 20 | except NotFound: 21 | print('symbol {} was not found'.format(symbol)) 22 | exit() 23 | else: 24 | instrument_id = instrument['id'] 25 | name = instrument['simple_name'] or instrument['name'] 26 | 27 | options_chains = [c for c in client.get_options_chains(instrument_ids=[instrument_id]) if c['can_open_position']] 28 | if len(options_chains) != 1: 29 | raise Exception('Expected exactly one options chains listing, but got: {}'.format(json.dumps(options_chains, indent=4))) 30 | options_chain = options_chains[0] 31 | 32 | if len(options_chain['underlying_instruments']) != 1: 33 | raise Exception('Expected exactly one underlying instrument, but got: {}'.format(json.dumps(options_chain, indent=4))) 34 | if not options_chain['can_open_position']: 35 | raise Exception("Can't open position: {}".format(json.dumps(options_chain, indent=4))) 36 | chain_id = options_chain['id'] 37 | multiplier = Decimal(options_chain['trade_value_multiplier']) 38 | 39 | kwargs = {} 40 | if dates: 41 | kwargs['expiration_dates'] = dates 42 | if options_type: 43 | kwargs['options_type'] = options_type 44 | potential_options_instruments = client.get_options_instruments(chain_id=chain_id, tradability='tradable', state='active', **kwargs) 45 | if not potential_options_instruments: 46 | raise Exception('No options found') 47 | 48 | options_instruments = [] 49 | for potential_options_instrument in potential_options_instruments: 50 | if strike is None or strike == float(potential_options_instrument['strike_price']): 51 | options_instruments.append(potential_options_instrument) 52 | if not options_instruments: 53 | raise Exception('No options found') 54 | 55 | options_instrument_by_id = { 56 | options_instrument['id']: options_instrument for options_instrument in options_instruments 57 | } 58 | 59 | options_quotes = client.get_options_marketdatas([options_instrument['id'] for options_instrument in options_instruments]) 60 | 61 | for options_quote in options_quotes: 62 | break_even_price = Decimal(options_quote['break_even_price']) 63 | ask_size = options_quote['ask_size'] 64 | ask_price = Decimal(options_quote['ask_price']) 65 | bid_size = options_quote['bid_size'] 66 | bid_price = Decimal(options_quote['bid_price']) 67 | adjusted_mark_price = Decimal(options_quote['adjusted_mark_price']) 68 | mark_price = Decimal(options_quote['mark_price']) 69 | max_loss = multiplier * adjusted_mark_price 70 | bid_spread = ask_price - bid_price 71 | implied_volatility = Decimal(options_quote['implied_volatility'] or 1) * 100 72 | high_price = Decimal(options_quote['high_price'] or 0) 73 | low_price = Decimal(options_quote['low_price'] or 0) 74 | hl_spread = high_price - low_price 75 | last_trade_size = options_quote['last_trade_size'] 76 | last_trade_price = Decimal(options_quote['last_trade_price'] or 0) 77 | open_interest = options_quote['open_interest'] 78 | volume = options_quote['volume'] 79 | options_instrument_id = get_last_id_from_url(options_quote['instrument']) 80 | options_instrument = options_instrument_by_id[options_instrument_id] 81 | expiration_date = options_instrument['expiration_date'] 82 | option_strike = Decimal(options_instrument['strike_price']) 83 | 84 | print('') 85 | print('${:.2f} {} ({}) {}'.format(option_strike, symbol, name, options_type[0].upper() + options_type[1:])) 86 | print('Break even\t ${:.2f}'.format(break_even_price)) 87 | print('Expires\t\t {}'.format(expiration_date)) 88 | print('Spread\t\t ${:.2f} ({}) <-> ${:.2f} ({})'.format(bid_price, bid_size, ask_price, ask_size)) 89 | print('\t\t\t${:.2f} ({:.2f}%)'.format(bid_spread, bid_spread * 100 / adjusted_mark_price)) 90 | print('Low/High\t ${:.2f} <-> ${:.2f}'.format(low_price, high_price)) 91 | print('\t\t\t${:.2f} ({:.2f}%)'.format(hl_spread, hl_spread * 100 / adjusted_mark_price)) 92 | print('Max loss\t ${:.2f}'.format(max_loss)) 93 | print('Impl Volatil\t {:.2f}%'.format(implied_volatility)) 94 | print('Last\t\t {} @ ${:.2f}'.format(last_trade_size, last_trade_price)) 95 | print('Open Int\t {}'.format(open_interest)) 96 | print('Volume\t\t {}'.format(volume)) 97 | 98 | 99 | if __name__ == '__main__': 100 | parser = argparse.ArgumentParser(description='Get a quote for a symbol') 101 | parser.add_argument('symbol', type=str.upper, help='A symbol to get an options quote on') 102 | parser.add_argument('-t', '--type', choices=OPTIONS_TYPES) 103 | parser.add_argument('-d', '--date', nargs='+', type=str, dest='dates', default=[], help='Date for the options to expire') 104 | parser.add_argument('-s', '--strike', type=float, help='Amount the strike price should be') 105 | parser.add_argument( 106 | '--live', 107 | action='store_true', 108 | help='Force to not use cache for APIs where values change' 109 | ) 110 | args = parser.parse_args() 111 | 112 | if not args.dates and not args.strike: 113 | raise Exception('You need to pass in --date and/or --strike') 114 | 115 | client = RobinhoodCachedClient() 116 | client.login() 117 | display_options_quote( 118 | client, 119 | args.type, 120 | args.symbol, 121 | args.dates, 122 | args.strike, 123 | FORCE_LIVE if args.live else CACHE_FIRST 124 | ) 125 | -------------------------------------------------------------------------------- /show_pending_options_orders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from collections import defaultdict 5 | from datetime import datetime 6 | from decimal import Decimal 7 | from math import ceil 8 | 9 | from dateutil.parser import parse 10 | import pytz 11 | 12 | from robinhood.exceptions import NotFound 13 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 14 | from robinhood.util import get_last_id_from_url, DIRECTION_TO_ORDER_SIDE 15 | 16 | 17 | client = RobinhoodCachedClient() 18 | client.login() 19 | 20 | 21 | def display_pending_options_orders(): 22 | orders = client.get_options_orders() 23 | pending_orders = [order for order in orders if order['state'] in ['queued', 'confirmed']] 24 | if len(pending_orders) == 0: 25 | print('\tNo pending orders') 26 | exit() 27 | 28 | chain_ids = [order['chain_id'] for order in pending_orders] 29 | chains = client.get_options_chains(chain_ids=chain_ids) 30 | chain_by_id = { 31 | chain['id']: chain for chain in chains 32 | } 33 | 34 | instrument_id_by_chain_id = { 35 | chain['id']: get_last_id_from_url(chain['underlying_instruments'][0]['instrument']) for chain in chains 36 | } 37 | instruments = client.get_instruments(list(instrument_id_by_chain_id.values())) 38 | instrument_by_id = {i['id']: i for i in instruments} 39 | 40 | options_instrument_id_by_order_id = { 41 | order['id']: get_last_id_from_url(order['legs'][0]['option']) for order in pending_orders 42 | } 43 | options_instruments = client.get_options_instruments(options_instrument_ids=list(set(options_instrument_id_by_order_id.values()))) 44 | option_instruments_by_id = { 45 | options_instrument['id']: options_instrument for options_instrument in options_instruments 46 | } 47 | 48 | instrument_id_to_orders = defaultdict(list) 49 | for order in pending_orders: 50 | instrument_id_to_orders[instrument_id_by_chain_id[order['chain_id']]].append(order) 51 | 52 | 53 | for instrument_id, instrument_orders in instrument_id_to_orders.items(): 54 | instrument = instrument_by_id[instrument_id] 55 | 56 | print('{} ({})'.format(instrument['symbol'], instrument['simple_name'] or instrument['name'])) 57 | 58 | for order in instrument_orders: 59 | order_id = order['id'] 60 | order_state = order['state'] 61 | order_type = order['type'] 62 | order_side = DIRECTION_TO_ORDER_SIDE[order['direction']] 63 | order_quantity = int(float(order['quantity'])) 64 | order_price = Decimal(order['price']) 65 | order_premium = Decimal(order['premium']) 66 | order_options_instrument = option_instruments_by_id[options_instrument_id_by_order_id[order_id]] 67 | order_option_type = order_options_instrument['type'] 68 | order_option_expires = order_options_instrument['expiration_date'] 69 | order_option_strike = Decimal(order_options_instrument['strike_price']) 70 | 71 | print('\t{}\t{} {}\t{} @ ${:.2f} (${:.2f} per share)\t({})'.format( 72 | order_state, order_type, order_side, order_quantity, order_premium, order_price, order_id)) 73 | print('\t\t${:.2f} {} expiring {}'.format(order_option_strike, order_option_type, order_option_expires)) 74 | 75 | 76 | if __name__ == '__main__': 77 | display_pending_options_orders() 78 | -------------------------------------------------------------------------------- /show_pending_orders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from collections import defaultdict 4 | from datetime import datetime 5 | from decimal import Decimal 6 | from math import ceil 7 | 8 | from robinhood.exceptions import NotFound 9 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, FORCE_LIVE 10 | from robinhood.util import get_last_id_from_url 11 | 12 | 13 | client = RobinhoodCachedClient() 14 | client.login() 15 | 16 | 17 | def display_pending_orders(): 18 | orders = client.get_orders(cache_mode=FORCE_LIVE) 19 | pending_orders = [order for order in orders if order['state'] in ['queued', 'confirmed']] 20 | if len(pending_orders) == 0: 21 | print('\tNo pending orders') 22 | exit() 23 | 24 | instrument_ids = [get_last_id_from_url(o['instrument']) for o in pending_orders] 25 | instruments = client.get_instruments(instrument_ids) 26 | instrument_by_id = { i['id']: i for i in instruments } 27 | 28 | quotes = client.get_quotes(instrument_ids, cache_mode=FORCE_LIVE) 29 | quote_by_id = { get_last_id_from_url(quote['instrument']): quote for quote in quotes } 30 | 31 | pending_orders_by_instrument = defaultdict(list) 32 | for order in pending_orders: 33 | instrument_id = get_last_id_from_url(order['instrument']) 34 | pending_orders_by_instrument[instrument_id].append(order) 35 | 36 | order_amounts = Decimal(0) 37 | for instrument_id, instrument_orders in pending_orders_by_instrument.items(): 38 | instrument = instrument_by_id[instrument_id] 39 | 40 | print('{} ({})'.format(instrument['symbol'], instrument['simple_name'] or instrument['name'])) 41 | 42 | # Show position 43 | try: 44 | position = client.get_position_by_instrument_id(instrument['id']) 45 | except NotFound: 46 | print('\tNo current position (or ever)') 47 | else: 48 | position_quantity = int(float(position['quantity'])) 49 | if not position_quantity: 50 | print('\tNo current position (sold off)') 51 | else: 52 | position_average_buy_price = Decimal(position['average_buy_price']) 53 | position_equity_cost = position_quantity * position_average_buy_price 54 | print('\tcurrent position\t\t{} @ ${:.2f}'.format( 55 | position_quantity, position_average_buy_price, position_equity_cost)) 56 | 57 | # Show quote 58 | quote = quote_by_id[instrument_id] 59 | last_trade_price = Decimal(quote['last_trade_price']) 60 | bid_price = Decimal(quote['bid_price']) 61 | bid_size = int(quote['bid_size']) 62 | ask_price = Decimal(quote['ask_price']) 63 | ask_size = int(quote['ask_size']) 64 | print('\tspread:\t${:.2f} ({}) <-> ${:.2f} ({})'.format(bid_price, bid_size, ask_price, ask_size)) 65 | print('\tlast:\t${:.2f}'.format(last_trade_price)) 66 | 67 | # Show orders 68 | for order in instrument_orders: 69 | order_id = order['id'] 70 | order_state = order['state'] 71 | order_type = order['type'] 72 | order_side = order['side'] 73 | order_quantity = int(float(order['quantity'])) 74 | order_price = Decimal(order['price']) 75 | multiplier = 1 76 | order_total = order_quantity * order_price 77 | if order_side == 'sell': 78 | order_amounts -= order_total 79 | else: 80 | order_amounts += order_total 81 | 82 | print('\t{}\t{} {}\t{} @ ${:.2f}\t({})'.format( 83 | order_state, order_type, order_side, order_quantity, order_price, order_id)) 84 | 85 | print() 86 | print('Combined order totals (negative means added account value) = ${:.2f}'.format(order_amounts)) 87 | 88 | if __name__ == '__main__': 89 | display_pending_orders() 90 | -------------------------------------------------------------------------------- /show_potentials.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from decimal import Decimal 4 | import argparse 5 | import json 6 | 7 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 8 | from robinhood.RobinhoodPortfolio import RobinhoodPortfolio 9 | 10 | # Set up the client 11 | client = RobinhoodCachedClient() 12 | client.login() 13 | 14 | 15 | def show_potentials(cache_mode): 16 | # Load the portfolio 17 | portfolio = RobinhoodPortfolio(client, {'cache_mode': cache_mode}) 18 | 19 | # Load the sentiment 20 | with open('sentiment.json', 'r') as sentiment_file: 21 | sentiment_json = json.load(sentiment_file) 22 | 23 | # Augment the portfolio with sentiment 24 | for symbol, symbol_sentiment in sentiment_json.items(): 25 | position = portfolio.get_position_for_symbol(symbol) 26 | assert 'sentiment' not in position 27 | position['sentiment'] = symbol_sentiment 28 | 29 | position['category'] = symbol_sentiment.get('category', 'N/A') 30 | position['priority'] = symbol_sentiment.get('priority', 99) 31 | if 'equity_target' in symbol_sentiment: 32 | equity_target = symbol_sentiment['equity_target'] 33 | position['equity_target'] = equity_target 34 | equity_left = equity_target - position['equity_worth'] 35 | if equity_left <= 0: 36 | position['equity_left'] = 0 37 | position['shares_needed'] = 0 38 | else: 39 | position['equity_left'] = equity_left 40 | position['shares_needed'] = equity_left / position['last_price'] 41 | position['paused'] = symbol_sentiment.get('paused', False) 42 | 43 | positions_by_priority = sorted(portfolio.positions, key=lambda p: p.get('priority', 999)) 44 | print('pri\tsym\teq_tot\tlast\twrth\ttrgt\tlft\tsh\tcat') 45 | for position in positions_by_priority: 46 | if position.get('paused'): 47 | continue 48 | print('{}\t{}\t{:.2f}\t{:.2f}\t{:.2f}\t{}\t{:.2f}\t{:.2f}\t{}'.format( 49 | position.get('priority', 'N/A'), 50 | position['symbol'], 51 | position['equity_cost'], 52 | position['last_price'], 53 | position['equity_worth'], 54 | position.get('equity_target', 'N/A'), 55 | position.get('equity_left', 0), 56 | position.get('shares_needed', 0), 57 | position.get('category') 58 | )) 59 | 60 | # TODO: Discounted (below buy in price) 61 | 62 | 63 | if __name__ == '__main__': 64 | parser = argparse.ArgumentParser(description='Show various position potentials to buy into') 65 | parser.add_argument( 66 | '--live', 67 | action='store_true', 68 | help='Force to not use cache for APIs where values change' 69 | ) 70 | args = parser.parse_args() 71 | show_potentials(FORCE_LIVE if args.live else CACHE_FIRST) 72 | -------------------------------------------------------------------------------- /show_quote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from datetime import datetime 5 | from decimal import Decimal 6 | from math import ceil 7 | 8 | from dateutil.parser import parse 9 | import pytz 10 | 11 | from robinhood.exceptions import NotFound 12 | from robinhood.RobinhoodCachedClient import RobinhoodCachedClient, CACHE_FIRST, FORCE_LIVE 13 | 14 | def display_quote(client, symbol, cache_mode): 15 | now = datetime.now(pytz.UTC) 16 | # Get instrument parts 17 | try: 18 | instrument = client.get_instrument_by_symbol(symbol) 19 | except NotFound: 20 | print('symbol {} was not found'.format(symbol)) 21 | exit() 22 | else: 23 | instrument_id = instrument['id'] 24 | listed_since = parse(instrument['list_date']).date() 25 | tradable = instrument['tradeable'] 26 | simple_name = instrument['simple_name'] 27 | 28 | # Get fundamentals 29 | fundamental = client.get_fundamental(instrument_id, cache_mode=cache_mode) 30 | founded_year = fundamental['year_founded'] 31 | high_52 = Decimal(fundamental['high_52_weeks']) if fundamental['high_52_weeks'] else None 32 | low_52 = Decimal(fundamental['low_52_weeks']) if fundamental['low_52_weeks'] else None 33 | last_open = Decimal(fundamental['open']) 34 | last_high = Decimal(fundamental['high']) 35 | last_low = Decimal(fundamental['low']) 36 | pe_ratio = Decimal(fundamental['pe_ratio']) if fundamental['pe_ratio'] else 0.0 37 | dividend_yield = Decimal(fundamental['dividend_yield']) if fundamental['dividend_yield'] else 0.0 38 | 39 | num_open_positions = client.get_popularity(instrument_id, cache_mode=cache_mode)['num_open_positions'] 40 | ratings = client.get_rating(instrument_id, cache_mode=cache_mode)['summary'] 41 | num_ratings = sum(v for _, v in ratings.items()) if ratings else None 42 | if num_ratings: 43 | percent_buy = ratings['num_buy_ratings'] * 100 / num_ratings 44 | percent_sell = ratings['num_sell_ratings'] * 100 / num_ratings 45 | 46 | print('==================== {} ({}) ===================='.format(symbol, simple_name)) 47 | if not tradable: 48 | print('!!!!!!!!!!!!! NOT TRADEABLE !!!!!!!!!!!!!') 49 | print('founded {} ... listed {}'.format(founded_year, listed_since.year)) 50 | 51 | print('---------------- sentiment ----------------') 52 | print('# ratings:\t{}'.format(num_ratings)) 53 | if num_ratings: 54 | print('Buy:\t{:.2f}%'.format(percent_buy)) 55 | print('Sell:\t{:.2f}%'.format(percent_sell)) 56 | print('RH holders:\t{}'.format(num_open_positions)) 57 | 58 | print('') 59 | print('---------------- recents ----------------') 60 | print('dy:\t{:.2f}%'.format(dividend_yield)) 61 | print('pe:\t{:.2f}x'.format(pe_ratio)) 62 | if high_52: 63 | year_spread = high_52 - low_52 64 | print('52w:\t${:.2f} <-> ${:.2f} (spread is ${:.2f} / {:.2f}%)'.format(low_52, high_52, year_spread, (year_spread) * 100 / high_52)) 65 | day_spread = last_high - last_low 66 | print('1d:\topen ${:.2f} ... ${:.2f} <-> ${:.2f} (spread is ${:.2f} / ${:.2f}%) '.format(last_open, last_low, last_high, day_spread, (day_spread) * 100 / last_high)) 67 | 68 | print('') 69 | print('---------------- position ----------------') 70 | 71 | # Get position 72 | try: 73 | position = client.get_position_by_instrument_id(instrument_id, cache_mode=cache_mode) 74 | except NotFound: 75 | position_average_buy_price = 0 76 | position_quantity = 0 77 | position_equity_cost = 0 78 | print('None') 79 | else: 80 | position_quantity = int(float(position['quantity'])) 81 | if not position_quantity: 82 | print('None anymore') 83 | else: 84 | position_average_buy_price = Decimal(position['average_buy_price']) 85 | position_equity_cost = position_quantity * position_average_buy_price 86 | print('{} @ ${:.2f} = ${:.2f}'.format(position_quantity, position_average_buy_price, position_equity_cost)) 87 | 88 | # Get order history, put as a subdisplay of position 89 | print('') 90 | print('\t-------------- orders --------------') 91 | orders = client.get_orders(instrument_id=instrument_id, cache_mode=cache_mode) 92 | if len(orders) == 0: 93 | print('\tNone') 94 | else: 95 | for order in orders: 96 | order_state = order['state'] 97 | order_type = order['type'] 98 | order_side = order['side'] 99 | order_quantity = int(float(order['quantity'])) 100 | order_price = Decimal(order['average_price']) if order['average_price'] else Decimal(order['price']) 101 | order_last_executed_at = parse(order['last_transaction_at']).date() 102 | print('\t{:%m/%d/%Y}\t{}\t{} {}\t{} @ ${:.2f}'.format(order_last_executed_at, order_state, order_type, order_side, order_quantity, order_price)) 103 | 104 | # Get quote 105 | quote = client.get_quote(instrument_id, cache_mode=cache_mode) 106 | updated_at = parse(quote['updated_at']) 107 | updated_minutes_ago = ceil((now - updated_at).total_seconds() / 60) 108 | has_traded = quote['has_traded'] 109 | trading_halted = quote['trading_halted'] 110 | last_close_price = Decimal(quote['previous_close']) 111 | last_close_date = parse(quote['previous_close_date']).date() 112 | last_extended_hours_trade_price = Decimal(quote['last_extended_hours_trade_price']) if quote['last_extended_hours_trade_price'] else last_close_price 113 | last_trade_price = Decimal(quote['last_trade_price']) 114 | bid_price = Decimal(quote['bid_price']) 115 | bid_size = int(quote['bid_size']) 116 | ask_price = Decimal(quote['ask_price']) 117 | ask_size = int(quote['ask_size']) 118 | 119 | print('') 120 | print('---------------- quote ----------------') 121 | if trading_halted: 122 | print('!!!!!!!!!!!!! HALTED !!!!!!!!!!!!!') 123 | if not has_traded: 124 | print('!!!!!!!!!!!!! HAS NOT TRADED !!!!!!!!!!!!!') 125 | print('close\t${:.2f} on {:%b %d}'.format(last_close_price, last_close_date)) 126 | extended_hours_spread = last_extended_hours_trade_price - last_close_price 127 | print('extnd:\t${:.2f} (${:.2f} / {:.2f}% spread)'.format(last_extended_hours_trade_price, extended_hours_spread, (last_extended_hours_trade_price - last_close_price) * 100 / last_close_price)) 128 | print('spread:\t${:.2f} ({}) <-> ${:.2f} ({})'.format(bid_price, bid_size, ask_price, ask_size)) 129 | bid_spread = ask_price - bid_price 130 | print('\t${:.2f} ({:.2f}%)'.format(bid_spread, bid_spread * 100 / last_trade_price)) 131 | print('last:\t${:.2f} ({:.2f}% within spread)'.format(last_trade_price, (bid_spread - (ask_price - last_trade_price)) * 100 / bid_spread)) 132 | print('age:\t{}m ago @ {:%I:%M%p}'.format(updated_minutes_ago, updated_at.astimezone(pytz.timezone('US/Pacific')))) 133 | 134 | 135 | if __name__ == '__main__': 136 | parser = argparse.ArgumentParser(description='Get a quote for a symbol') 137 | parser.add_argument('symbol', type=str.upper, help='A symbol to get a quote on') 138 | parser.add_argument( 139 | '--live', 140 | action='store_true', 141 | help='Force to not use cache for APIs where values change' 142 | ) 143 | args = parser.parse_args() 144 | 145 | client = RobinhoodCachedClient() 146 | client.login() 147 | display_quote(client, args.symbol, FORCE_LIVE if args.live else CACHE_FIRST) 148 | --------------------------------------------------------------------------------