├── Data ├── signed.pdf ├── Fees.csv ├── document.pdf ├── passport-td3.png ├── MasterOrder.csv ├── Goal Probability Table.csv ├── MasterOrder_Filled.csv ├── Dividends.csv ├── Account_Allocations.csv ├── Risk Questions.csv ├── Risk Answers.csv └── Risk Mapping Lookup.csv ├── LICENSE ├── README.md ├── Chapter10.ipynb ├── Chapter06.ipynb ├── Chapter11.ipynb ├── Chapter07.ipynb ├── Chapter14.ipynb ├── Chapter15.ipynb └── Chapter12.ipynb /Data/signed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Robo-Advisor-with-Python/HEAD/Data/signed.pdf -------------------------------------------------------------------------------- /Data/Fees.csv: -------------------------------------------------------------------------------- 1 | ,Account,FeeAmount 2 | 0,123456789,0.0005706570443678678 3 | 1,987654321,0.001931956557080065 4 | -------------------------------------------------------------------------------- /Data/document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Robo-Advisor-with-Python/HEAD/Data/document.pdf -------------------------------------------------------------------------------- /Data/passport-td3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Robo-Advisor-with-Python/HEAD/Data/passport-td3.png -------------------------------------------------------------------------------- /Data/MasterOrder.csv: -------------------------------------------------------------------------------- 1 | ,Symbol,Type,DollarAmount 2 | 0,GLD,BUY,6.8606 3 | 1,IEI,BUY,15.9037 4 | 2,TLT,BUY,0.6466 5 | 3,VTI,BUY,6.589 6 | -------------------------------------------------------------------------------- /Data/Goal Probability Table.csv: -------------------------------------------------------------------------------- 1 | Realize,Avoid,MinP,MaxP 2 | Dreams,Concerns,0.5,0.64 3 | Wishes,Worries,0.65,0.79 4 | Wants,Fears,0.8,0.89 5 | Needs,Nightmares,0.9,0.99 -------------------------------------------------------------------------------- /Data/MasterOrder_Filled.csv: -------------------------------------------------------------------------------- 1 | OrderID,Symbol,Type,Units,Price 2 | 0,GLD,BUY,0.04121002328,163.22 3 | 1,IEI,BUY,0.1308265395,115.3 4 | 2,TLT,BUY,0.009743440233,102.9 5 | 3,VTI,BUY,0.03565942245,201.54 -------------------------------------------------------------------------------- /Data/Dividends.csv: -------------------------------------------------------------------------------- 1 | Account,Symbol,Amount,Units 2 | 123456789,TLT,0,0.0004871720117 3 | 123456789,IEI,0,0.0003595316567 4 | 123456789,VTI,0.1699714236,0 5 | 987654321,IEI,0,0.002256999133 6 | 987654321,VTI,0.1937368557,0 -------------------------------------------------------------------------------- /Data/Account_Allocations.csv: -------------------------------------------------------------------------------- 1 | ,Account,Symbol,Type,Units 2 | 0,123456789,GLD,BUY,0.02213974051911262 3 | 1,123456789,IEI,BUY,0.02171626612838836 4 | 2,123456789,VTI,BUY,0.0163863407640173 5 | 3,123456789,TLT,BUY,0.009743440233 6 | 4,987654321,GLD,BUY,0.019070282760887385 7 | 5,987654321,IEI,BUY,0.10911027337161164 8 | 6,987654321,VTI,BUY,0.019273081685982695 9 | -------------------------------------------------------------------------------- /Data/Risk Questions.csv: -------------------------------------------------------------------------------- 1 | QuestionID,QuestionType,QuestionText,QuestionWeight 2 | 1,Tolerance,"In general, how would your best friend describe you as a risk taker?",2 3 | 2,Tolerance,You are on a TV game show and can choose one of the following. Which would you take?,1 4 | 3,Tolerance,When you think of the word risk which of the following words comes to mind first?,1 5 | 4,Capacity,You are able to save money regularly.,1 6 | 5,Capacity,You can pay all your monthly bills on time -- including any credit card or other debt.,1 7 | 6,Capacity,"If you lose money investing today, your current lifestyle would not be impacted.",1 8 | 7,Capacity,You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.,1 -------------------------------------------------------------------------------- /Data/Risk Answers.csv: -------------------------------------------------------------------------------- 1 | AnswerID,AnswerText,AnswerValue,QuestionID 2 | 1,A real gambler,4,1 3 | 2,Willing to take risks after completing adequate research,3,1 4 | 3,Cautious,2,1 5 | 4,A real risk avoider,1,1 6 | 5,"$1,000 in cash",1,2 7 | 6,"A 50% chance at winning $5,000",2,2 8 | 7,"A 25% chance at winning $10,000",3,2 9 | 8,"A 5% chance at winning $100,000",4,2 10 | 9,Loss,1,3 11 | 10,Uncertainty,2,3 12 | 11,Opportunity,3,3 13 | 12,Thrill,4,3 14 | 13,Completely false,1,4 15 | 14,Somewhat true,2,4 16 | 15,Completely true,3,4 17 | 16,Completely false,1,5 18 | 17,Somewhat true,2,5 19 | 18,Completely true,3,5 20 | 19,Completely false,1,6 21 | 20,Somewhat true,2,6 22 | 21,Completely true,3,6 23 | 22,Completely false,1,7 24 | 23,Somewhat true,2,7 25 | 24,Completely true,3,7 -------------------------------------------------------------------------------- /Data/Risk Mapping Lookup.csv: -------------------------------------------------------------------------------- 1 | Portfolio,Capacity_min,Capacity_max,Tolerance_min,Tolerance_max 2 | 1,4,4,4,6 3 | 1,4,4,7,9 4 | 1,4,4,10,12 5 | 1,4,4,13,15 6 | 1,4,4,16,16 7 | 2,5,5,4,6 8 | 2,5,5,7,9 9 | 2,5,5,10,12 10 | 2,5,5,13,15 11 | 2,5,5,16,16 12 | 2,6,6,4,6 13 | 2,6,6,7,9 14 | 2,6,6,10,12 15 | 2,6,6,13,15 16 | 2,6,6,16,16 17 | 2,7,7,4,6 18 | 2,7,7,7,9 19 | 2,7,7,10,12 20 | 2,7,7,13,15 21 | 3,7,7,16,16 22 | 2,8,8,4,6 23 | 2,8,8,7,9 24 | 2,8,8,10,12 25 | 3,8,8,13,15 26 | 3,8,8,16,16 27 | 2,9,9,4,6 28 | 2,9,9,7,9 29 | 3,9,9,10,12 30 | 3,9,9,13,15 31 | 3,9,9,16,16 32 | 3,10,10,4,6 33 | 3,10,10,7,9 34 | 3,10,10,10,12 35 | 4,10,10,13,15 36 | 4,10,10,16,16 37 | 3,11,11,4,6 38 | 3,11,11,7,9 39 | 4,11,11,10,12 40 | 4,11,11,13,15 41 | 5,11,11,16,16 42 | 3,12,12,4,6 43 | 4,12,12,7,9 44 | 4,12,12,10,12 45 | 5,12,12,13,15 46 | 5,12,12,16,16 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Robo-Advisor with Python 4 | 5 | Robo-Advisor with Python 6 | 7 | This is the code repository for [Robo-Advisor with Python](https://www.amazon.in/Robo-Advisor-Python-Hands-Operate-Robo-advisor-ebook/dp/B0BNNTXR9P/ref=sr_1_1?crid=ZS2Z48YPUPH2&keywords=robo+advisor+with+python&qid=1677481036&sprefix=robo+advisor+with+python%2Caps%2C217&sr=8-1), published by Packt. 8 | 9 | **A hands-on guide to building and operating your own Robo-advisor** 10 | 11 | ## What is this book about? 12 | Robo-advisors are becoming table stakes for the wealth management industry across all segments, from retail to high-net-worth investors. Robo-advisors enable you to manage your own portfolios and financial institutions to create automated platforms for effective digital wealth management. This book is your hands-on guide to understanding how Robo-advisors work, and how to build one efficiently. The chapters are designed in a way to help you get a comprehensive grasp of what Robo-advisors do and how they are structured with an end-to-end workflow. 13 | You’ll begin by learning about the key decisions that influence the building of a Robo-advisor, along with considerations on building and licensing a platform. As you advance, you’ll find out how to build all the core capabilities of a Robo-advisor using Python, including goals, risk questionnaires, portfolios, and projections. The book also shows you how to create orders, as well as open accounts and perform KYC verification for transacting. Finally, you’ll be able to implement capabilities such as performance reporting and rebalancing for operating a Robo-advisor with ease. 14 | By the end of this book, you’ll have gained a solid understanding of how Robo-advisors work and be well on your way to building one for yourself or your business. 15 | 16 | This book covers the following exciting features: 17 | * Explore what Robo-advisors do and why they exist 18 | * Create a workflow to design and build a Robo-advisor from the bottom up 19 | * Build and license Robo-advisors using different approaches 20 | * Open and fund accounts, complete KYC verification, and manage orders 21 | * Build Robo-advisor features for goals, projections, portfolios, and more 22 | * Operate a Robo-advisor with P&L, rebalancing, and fee management 23 | 24 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1801819696) today! 25 | 26 | https://www.packtpub.com/ 27 | 28 | ## Instructions and Navigations 29 | All of the code is organized into folders. 30 | 31 | The code will look like the following: 32 | ``` 33 | stocks = Allocation("SPY", 0.6) 34 | bonds = Allocation("TLT", 0.4) 35 | myPortfolio = Portfolio("Growth", 4) 36 | myPortfolio.allocations.append(stocks) 37 | myPortfolio.allocations.append(bonds) 38 | df = myPortfolio.getDailyPrices("20y") 39 | ``` 40 | 41 | **Following is what you need for this book:** 42 | If you are a finance professional or a data professional working in wealth management and are curious about how robo-advisors work, this book is for you. It will be helpful to have a basic understanding of Python and investing concepts. This is a great handbook for developers interested in building their own robo-advisor to manage personal investments or build a platform for their business to operate, as well as for product managers and business leaders in financial services looking to lease, buy, or build a robo-advisor. 43 | With the following software and hardware list you can run all code files present in the book (Chapter 1-16). 44 | 45 | ### Software and Hardware List 46 | 47 | | Chapter | Software required | OS required | 48 | | -------- | -------------------------------------------------------------------------------------| -----------------------------------| 49 | | 1-16 | Google Colab | Windows, Mac OS X, and Linux (Any) | 50 | | 1-16 | Python | Windows, Mac OS X, and Linux (Any) | 51 | 52 | ### Related products 53 | * Python for Finance Cookbook - Second Edition [[Packt]](https://www.packtpub.com/product/python-for-finance-cookbook-second-edition/9781803243191) [[Amazon]](https://www.amazon.com/dp/1803243198) 54 | 55 | * Developing High-Frequency Trading Systems [[Packt]](https://www.packtpub.com/product/developing-high-frequency-trading-systems/9781803242811?_ga=2.158598970.1649601379.1677480968-1347501151.1654864057) [[Amazon]](https://www.amazon.com/dp/1803242817) 56 | 57 | ## Get to Know the Author 58 | **Aki Ranin** is the founder of two A.I. startups in Singapore: Bambu in Fintech, and Healthzilla in Healthtech. He serves as Adjunct Lecturer at Singapore Management University on the topic of Machine Learning. His company Bambu has been a pioneer of Robo-advisor platforms since 2016, having built the first Robo-advisor in Singapore and worked with 20+ Enterprise clients across the world to design, build, and launch Robo-advisors 59 | Aki lives in Singapore and holds a Masters Degree in Computer Science from Aalto University in Finland and writes occasionally about startups, philosophy, and technology 60 | -------------------------------------------------------------------------------- /Chapter10.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "id": "34nKAAjoeaxS" 8 | }, 9 | "source": [ 10 | "# Chapter 10: Account Opening and KYC" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": { 17 | "colab": { 18 | "base_uri": "https://localhost:8080/" 19 | }, 20 | "id": "tOSpyE4EegxU", 21 | "outputId": "56edd710-03b4-46f5-c610-e9a7bbb6f9db" 22 | }, 23 | "outputs": [], 24 | "source": [ 25 | "import re\n", 26 | "user_name = input(\"Please enter your Family Name: \")\n", 27 | "if not re.match(\"^[\\w' -]*$\", user_name):\n", 28 | " print (\"Error! Make sure you only use alphaanumerics and the characters dash, space, and apostrophe in your name.\")" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": { 35 | "id": "bABpmIL6P8n6" 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "user_name = input(\"Please enter your Family Name: \")\n", 40 | "if not re.match(\"^[a-zA-Z' -]{1,64}$\", user_name):\n", 41 | " print (\"Error! Make sure you only use letters and the characters dash, space, and apostrophe in your name.\")" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": { 48 | "colab": { 49 | "base_uri": "https://localhost:8080/" 50 | }, 51 | "id": "beUwcokwQ6Ku", 52 | "outputId": "c03134d1-8a37-4986-c457-e7b9419932b9" 53 | }, 54 | "outputs": [], 55 | "source": [ 56 | "phone_no = input(\"Please enter your Phone Number: \")\n", 57 | "if not re.match(\"\\d\\d\\d-\\d\\d\\d-\\d\\d\\d\\d\", phone_no):\n", 58 | " print (\"Error! Please ensure your phone number follows the format 123-456-7890.\")" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": { 65 | "colab": { 66 | "base_uri": "https://localhost:8080/" 67 | }, 68 | "id": "6qxvk3-vTRdz", 69 | "outputId": "c432a977-cfd4-49ce-9585-a769e56bd02c" 70 | }, 71 | "outputs": [], 72 | "source": [ 73 | "ssn_no = input(\"Please enter your SSN: \")\n", 74 | "if not re.match(\"^(?!(000|666|9))\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}$\", ssn_no):\n", 75 | " print (\"Error! Please check your SSN.\")" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": { 82 | "colab": { 83 | "base_uri": "https://localhost:8080/" 84 | }, 85 | "id": "uVtPW0C4WJVb", 86 | "outputId": "e414690d-8a7d-4dc6-bebf-51c46e9d8450" 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "pip install smartystreets_python_sdk" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": { 97 | "colab": { 98 | "base_uri": "https://localhost:8080/", 99 | "height": 550 100 | }, 101 | "id": "PwKwh1LdWLql", 102 | "outputId": "af152c71-f271-4ee2-bed6-dfb0a3fbec8e" 103 | }, 104 | "outputs": [], 105 | "source": [ 106 | "from smartystreets_python_sdk import StaticCredentials, ClientBuilder \n", 107 | "from smartystreets_python_sdk.us_street import Lookup \n", 108 | " \n", 109 | "auth_id = \"your_id\" \n", 110 | "auth_token = \"your_token\" \n", 111 | "credentials = StaticCredentials(auth_id, auth_token) \n", 112 | " \n", 113 | "print(\"Step 0. Wire up the client with your keypair.\") \n", 114 | "client = ClientBuilder(credentials).build_us_street_api_client() \n", 115 | " \n", 116 | "print(\"Step 1. Make a lookup. (BTW, you can also send entire batches of lookups...)\") \n", 117 | "lookup = Lookup() \n", 118 | "lookup.street = \"1 Rosedale\" \n", 119 | "lookup.lastline = \"Baltimore MD\" \n", 120 | "lookup.candidates = 10 \n", 121 | " \n", 122 | "print(\"Step 2. Send the lookup.\") \n", 123 | "client.send_lookup(lookup) \n", 124 | " \n", 125 | "print(\"Step 3. Show the resulting candidate addresses:\") \n", 126 | "for c, candidate in enumerate(lookup.result): \n", 127 | " print(\"- {}: {}, {}\".format(c, candidate.delivery_line_1, candidate.last_line))" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 2, 133 | "metadata": { 134 | "colab": { 135 | "base_uri": "https://localhost:8080/" 136 | }, 137 | "id": "-eZ2XnhXYGkl", 138 | "outputId": "d5b0120e-8c89-43fb-8649-46e4534ccb8e" 139 | }, 140 | "outputs": [ 141 | { 142 | "name": "stdout", 143 | "output_type": "stream", 144 | "text": [ 145 | "Requirement already satisfied: pyHanko in /Users/akiranin/miniforge3/lib/python3.10/site-packages (0.16.0)\n", 146 | "Requirement already satisfied: asn1crypto>=1.5.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (1.5.1)\n", 147 | "Requirement already satisfied: tzlocal>=2.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (4.2)\n", 148 | "Requirement already satisfied: requests>=2.24.0 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (2.28.2)\n", 149 | "Requirement already satisfied: qrcode>=6.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (7.3.1)\n", 150 | "Requirement already satisfied: pytz>=2020.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (2022.7.1)\n", 151 | "Requirement already satisfied: cryptography>=3.3.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (39.0.0)\n", 152 | "Requirement already satisfied: click>=7.1.2 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (8.1.3)\n", 153 | "Requirement already satisfied: pyyaml>=5.3.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (6.0)\n", 154 | "Requirement already satisfied: pyhanko-certvalidator~=0.19.8 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyHanko) (0.19.8)\n", 155 | "Requirement already satisfied: cffi>=1.12 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from cryptography>=3.3.1->pyHanko) (1.15.1)\n", 156 | "Requirement already satisfied: uritools>=3.0.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyhanko-certvalidator~=0.19.8->pyHanko) (4.0.1)\n", 157 | "Requirement already satisfied: oscrypto>=1.1.0 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pyhanko-certvalidator~=0.19.8->pyHanko) (1.3.0)\n", 158 | "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from requests>=2.24.0->pyHanko) (1.26.14)\n", 159 | "Requirement already satisfied: idna<4,>=2.5 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from requests>=2.24.0->pyHanko) (3.4)\n", 160 | "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from requests>=2.24.0->pyHanko) (2.0.12)\n", 161 | "Requirement already satisfied: certifi>=2017.4.17 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from requests>=2.24.0->pyHanko) (2022.12.7)\n", 162 | "Requirement already satisfied: pytz-deprecation-shim in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from tzlocal>=2.1->pyHanko) (0.1.0.post0)\n", 163 | "Requirement already satisfied: pycparser in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from cffi>=1.12->cryptography>=3.3.1->pyHanko) (2.21)\n", 164 | "Requirement already satisfied: tzdata in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pytz-deprecation-shim->tzlocal>=2.1->pyHanko) (2022.7)\n", 165 | "Requirement already satisfied: nest_asyncio in /Users/akiranin/miniforge3/lib/python3.10/site-packages (1.5.6)\n" 166 | ] 167 | } 168 | ], 169 | "source": [ 170 | "!pip install pyHanko\n", 171 | "!pip install nest_asyncio" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 2, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "# Run in terminal:\n", 181 | "#!openssl req -x509 -newkey rsa:4096 -keyout ./Data/key.pem -out ./Data/cert.pem -sha256 -days 365" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 4, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "from pyhanko.sign import signers\n", 191 | "from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter\n", 192 | "import nest_asyncio\n", 193 | "nest_asyncio.apply()\n", 194 | "\n", 195 | "cms_signer = signers.SimpleSigner.load(\n", 196 | " './Data/key.pem', './Data/cert.pem',\n", 197 | " key_passphrase=b'password'\n", 198 | ")\n", 199 | "\n", 200 | "with open('./Data/document.pdf', 'r+b') as doc:\n", 201 | " w = IncrementalPdfFileWriter(doc)\n", 202 | " out = signers.sign_pdf(\n", 203 | " w, signers.PdfSignatureMetadata(field_name='Signature1'),\n", 204 | " signer=cms_signer,\n", 205 | " in_place=False\n", 206 | " )\n", 207 | " \n", 208 | "with open(\"./Data/signed.pdf\", \"wb\") as f:\n", 209 | " f.write(out.getbuffer())" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "metadata": { 216 | "colab": { 217 | "base_uri": "https://localhost:8080/" 218 | }, 219 | "id": "fh3fMAsGeAN_", 220 | "outputId": "316a1374-3174-402d-fb99-93df9134dd18" 221 | }, 222 | "outputs": [], 223 | "source": [ 224 | "pip install PassportEye" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": { 231 | "colab": { 232 | "base_uri": "https://localhost:8080/" 233 | }, 234 | "id": "3nSjouNMjgF7", 235 | "outputId": "ea00efff-6a89-486f-e5ce-fa2d2e266de9" 236 | }, 237 | "outputs": [], 238 | "source": [ 239 | "! apt install tesseract-ocr" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "metadata": { 246 | "colab": { 247 | "base_uri": "https://localhost:8080/" 248 | }, 249 | "id": "mvLR3ligh8oK", 250 | "outputId": "f9456509-e34e-4ddf-c7e5-9f262a9c4d98" 251 | }, 252 | "outputs": [], 253 | "source": [ 254 | "pip install pytesseract" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": { 261 | "id": "BRjlnoq0ixd_" 262 | }, 263 | "outputs": [], 264 | "source": [ 265 | "import pytesseract\n", 266 | "pytesseract.pytesseract.tesseract_cmd = r'/usr/bin/tesseract'" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": null, 272 | "metadata": { 273 | "id": "yiFtGT6cg9AS" 274 | }, 275 | "outputs": [], 276 | "source": [ 277 | "from passporteye import read_mrz\n", 278 | "mrz = read_mrz('./Data/passport-td3.png')\n", 279 | "#mrz = read_mrz('./Data/passport-td3.png', extra_cmdline_params='--oem 0')" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": { 286 | "colab": { 287 | "base_uri": "https://localhost:8080/" 288 | }, 289 | "id": "LkRUFgIvjuaR", 290 | "outputId": "d5663fbf-29e8-4fd6-e111-c6f33f3a21ff" 291 | }, 292 | "outputs": [], 293 | "source": [ 294 | "print(mrz.number)" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "metadata": { 301 | "id": "jAiPRQTUp9MS" 302 | }, 303 | "outputs": [], 304 | "source": [ 305 | "import requests\n", 306 | "response = requests.get(\"https://www.treasury.gov/ofac/downloads/sdnlist.txt\")\n", 307 | "data = response.text" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": null, 313 | "metadata": { 314 | "colab": { 315 | "base_uri": "https://localhost:8080/" 316 | }, 317 | "id": "qSKjT0g4qD6a", 318 | "outputId": "f7aa1f37-0557-447a-d3d5-4385b49f9b2f" 319 | }, 320 | "outputs": [], 321 | "source": [ 322 | "data.find(\"Ranin\")" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": null, 328 | "metadata": { 329 | "colab": { 330 | "base_uri": "https://localhost:8080/" 331 | }, 332 | "id": "npYLNj5NqxlZ", 333 | "outputId": "50cd85ff-bb9f-4fdb-9f37-cfce584e5028" 334 | }, 335 | "outputs": [], 336 | "source": [ 337 | "data.find(\"Zakharova\")" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": null, 343 | "metadata": { 344 | "colab": { 345 | "base_uri": "https://localhost:8080/" 346 | }, 347 | "id": "PFTAG_fsrGHF", 348 | "outputId": "b0ddb411-f4f0-4864-d531-faf8d7cfe7cc" 349 | }, 350 | "outputs": [], 351 | "source": [ 352 | "for item in data.split(\"\\n\"):\n", 353 | " if \"Zakharova\" in item:\n", 354 | " print (item.strip())" 355 | ] 356 | } 357 | ], 358 | "metadata": { 359 | "colab": { 360 | "provenance": [], 361 | "toc_visible": true 362 | }, 363 | "kernelspec": { 364 | "display_name": "Python 3", 365 | "language": "python", 366 | "name": "python3" 367 | }, 368 | "language_info": { 369 | "codemirror_mode": { 370 | "name": "ipython", 371 | "version": 3 372 | }, 373 | "file_extension": ".py", 374 | "mimetype": "text/x-python", 375 | "name": "python", 376 | "nbconvert_exporter": "python", 377 | "pygments_lexer": "ipython3", 378 | "version": "3.10.8" 379 | }, 380 | "vscode": { 381 | "interpreter": { 382 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 383 | } 384 | } 385 | }, 386 | "nbformat": 4, 387 | "nbformat_minor": 0 388 | } 389 | -------------------------------------------------------------------------------- /Chapter06.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "FHVzSolan7k_" 7 | }, 8 | "source": [ 9 | "# Chapter 6: Goal-based Investing" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": { 16 | "id": "ES_V8TqoluVJ" 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "goalName = \"Retirement\"\n", 21 | "goalTarget = 3000000" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 2, 27 | "metadata": { 28 | "id": "okc6jcWFpTjD" 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "class Goal: \n", 33 | " def __init__(self, name, target): \n", 34 | " self.name = name \n", 35 | " self.target= target " 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 3, 41 | "metadata": { 42 | "id": "RXHU4xwYpHmH" 43 | }, 44 | "outputs": [], 45 | "source": [ 46 | "myGoal = Goal(\"Retirement\", 3000000)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 4, 52 | "metadata": { 53 | "colab": { 54 | "base_uri": "https://localhost:8080/" 55 | }, 56 | "id": "G2swfuMcr0Gj", 57 | "outputId": "b7ff3be9-5f8d-4af8-d6c9-ebe63690eeda" 58 | }, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "Retirement\n", 65 | "3000000\n" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "print(myGoal.name)\n", 71 | "print(myGoal.target)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 5, 77 | "metadata": { 78 | "colab": { 79 | "base_uri": "https://localhost:8080/" 80 | }, 81 | "id": "KJsT_UDwr7Um", 82 | "outputId": "a9f6e060-fea0-4d33-e959-69b73d720ef1" 83 | }, 84 | "outputs": [ 85 | { 86 | "name": "stdout", 87 | "output_type": "stream", 88 | "text": [ 89 | "1000000\n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "myGoal.target = 1000000\n", 95 | "print(myGoal.target)" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 6, 101 | "metadata": { 102 | "id": "4fYnX-K8EbWn" 103 | }, 104 | "outputs": [], 105 | "source": [ 106 | "class Goal:\n", 107 | " def __init__(self, name, targetYear, targetValue, initialContribution=0, monthlyContribution=0):\n", 108 | " self.name = name\n", 109 | " self.targetYear = targetYear\n", 110 | " self.targetValue = targetValue\n", 111 | " self.initialContribution = initialContribution\n", 112 | " self.monthlyContribution = monthlyContribution" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 7, 118 | "metadata": { 119 | "id": "U0h16DmpxRZc" 120 | }, 121 | "outputs": [], 122 | "source": [ 123 | "myGoal = Goal(\"Retirement\", 2060, 3000000)\n", 124 | "myGoal.initialContribution = 10000" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 8, 130 | "metadata": { 131 | "colab": { 132 | "base_uri": "https://localhost:8080/" 133 | }, 134 | "id": "Y3PgcOrjy4D0", 135 | "outputId": "7e2c851f-e0a8-4c3b-acc3-2bc281590e2c" 136 | }, 137 | "outputs": [ 138 | { 139 | "name": "stdout", 140 | "output_type": "stream", 141 | "text": [ 142 | "2047\n" 143 | ] 144 | } 145 | ], 146 | "source": [ 147 | "from datetime import date\n", 148 | "\n", 149 | "retirementAge = 65\n", 150 | "startingAge = 41\n", 151 | "targetYear = date.today().year + (retirementAge-startingAge)\n", 152 | "\n", 153 | "myGoal.targetYear = targetYear\n", 154 | "print(myGoal.targetYear)" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 9, 160 | "metadata": { 161 | "id": "LGtYzshGFfy2" 162 | }, 163 | "outputs": [], 164 | "source": [ 165 | "class RetirementGoal(Goal):\n", 166 | " def __init__(self, name, targetValue, startingAge, retirementAge):\n", 167 | " targetYear = date.today().year + (retirementAge-startingAge)\n", 168 | " super().__init__(name, targetYear, targetValue)\n", 169 | " self.retirementAge = retirementAge" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 10, 175 | "metadata": { 176 | "colab": { 177 | "base_uri": "https://localhost:8080/" 178 | }, 179 | "id": "RwdSIu2dHYmo", 180 | "outputId": "65cf2c4c-bfeb-43ae-9d2f-7d2e0a40d8c5" 181 | }, 182 | "outputs": [ 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | "2042\n" 188 | ] 189 | } 190 | ], 191 | "source": [ 192 | "myRetirement = RetirementGoal(\"Honolulu\", 3000000, 41, 60)\n", 193 | "print(myRetirement.targetYear)" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 11, 199 | "metadata": { 200 | "id": "xg50MUpeJaMb" 201 | }, 202 | "outputs": [], 203 | "source": [ 204 | "class GrowWealthGoal(Goal):\n", 205 | " def __init__(self, initialContribution, monthlyContribution):\n", 206 | " targetYear = date.today().year + 10\n", 207 | " targetAmount = 1000000\n", 208 | " super().__init__(\"Grow My Wealth\", targetYear, targetAmount, initialContribution, monthlyContribution)\n", 209 | "\n", 210 | "class EducationGoal(Goal):\n", 211 | " def __init__(self, name, startYear, degreeLengthYears, annualTuitionFees, degreeType, schoolName):\n", 212 | " targetValue = degreeLengthYears * annualTuitionFees\n", 213 | " super().__init__(name, startYear, targetValue)\n", 214 | " self.degreeType = degreeType\n", 215 | " self.schoolName = schoolName\n", 216 | "\n", 217 | "class RealEstateGoal(Goal):\n", 218 | " def __init__(self, name, targetYear, homeValue, downPayment, mortgagePayment, interestRate):\n", 219 | " targetValue = downPayment\n", 220 | " super().__init__(name, targetYear, targetValue)\n", 221 | " self.homeValue = homeValue\n", 222 | " self.downPayment = downPayment\n", 223 | " self.mortgagePayment = mortgagePayment\n", 224 | " self.interestRate = interestRate\n", 225 | "\n", 226 | "class StartupGoal(Goal):\n", 227 | " def __init__(self, companyName, startYear, seedFunding):\n", 228 | " super().__init__(companyName, startYear, seedFunding)" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 12, 234 | "metadata": { 235 | "id": "Et_yDfmtJTNF" 236 | }, 237 | "outputs": [], 238 | "source": [ 239 | "class SavingsGoal:\n", 240 | " def __init__(self, name, targetDate, targetValue, initialContribution, monthlyContribution):\n", 241 | " from dateutil import parser\n", 242 | " targetDateTime = parser.parse(targetDate)\n", 243 | " from dateutil.relativedelta import relativedelta\n", 244 | " delta = relativedelta(targetDateTime, date.today())\n", 245 | " difference_in_months = delta.months + delta.years * 12\n", 246 | " value = initialContribution + (monthlyContribution * difference_in_months)\n", 247 | " print(value)\n", 248 | " if not (value >= targetValue):\n", 249 | " raise ValueError('Target value too high to be achieved.')\n", 250 | " self.name = name\n", 251 | " self.targetDate = targetDateTime\n", 252 | " self.targetValue = targetValue\n", 253 | " self.initialContribution = initialContribution\n", 254 | " self.monthlyContribution = monthlyContribution" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 23, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "from dateutil.relativedelta import relativedelta\n", 264 | "new_date = date.today() + relativedelta(years=1)\n", 265 | "new_date = new_date.strftime(\"%B %d, %Y\")" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 24, 271 | "metadata": { 272 | "colab": { 273 | "base_uri": "https://localhost:8080/", 274 | "height": 312 275 | }, 276 | "id": "C7x3m8Aiw6wL", 277 | "outputId": "4252089b-e7ea-49f1-df55-e541b65ac9b2" 278 | }, 279 | "outputs": [ 280 | { 281 | "name": "stdout", 282 | "output_type": "stream", 283 | "text": [ 284 | "2200\n" 285 | ] 286 | }, 287 | { 288 | "ename": "ValueError", 289 | "evalue": "Target value too high to be achieved.", 290 | "output_type": "error", 291 | "traceback": [ 292 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 293 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 294 | "Cell \u001b[0;32mIn[24], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m saver \u001b[39m=\u001b[39m SavingsGoal(\u001b[39m\"\u001b[39;49m\u001b[39mRainy Day\u001b[39;49m\u001b[39m\"\u001b[39;49m, new_date, \u001b[39m10000\u001b[39;49m, \u001b[39m1000\u001b[39;49m, \u001b[39m100\u001b[39;49m)\n", 295 | "Cell \u001b[0;32mIn[12], line 11\u001b[0m, in \u001b[0;36mSavingsGoal.__init__\u001b[0;34m(self, name, targetDate, targetValue, initialContribution, monthlyContribution)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[39mprint\u001b[39m(value)\n\u001b[1;32m 10\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mnot\u001b[39;00m (value \u001b[39m>\u001b[39m\u001b[39m=\u001b[39m targetValue):\n\u001b[0;32m---> 11\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mValueError\u001b[39;00m(\u001b[39m'\u001b[39m\u001b[39mTarget value too high to be achieved.\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m 12\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mname \u001b[39m=\u001b[39m name\n\u001b[1;32m 13\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mtargetDate \u001b[39m=\u001b[39m targetDateTime\n", 296 | "\u001b[0;31mValueError\u001b[0m: Target value too high to be achieved." 297 | ] 298 | } 299 | ], 300 | "source": [ 301 | "saver = SavingsGoal(\"Rainy Day\", new_date, 10000, 1000, 100)" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 25, 307 | "metadata": { 308 | "colab": { 309 | "base_uri": "https://localhost:8080/" 310 | }, 311 | "id": "S8s4mi4XyRj4", 312 | "outputId": "8c86efe5-c318-4b2e-f6ee-593a66af50fa" 313 | }, 314 | "outputs": [ 315 | { 316 | "name": "stdout", 317 | "output_type": "stream", 318 | "text": [ 319 | "13000\n" 320 | ] 321 | } 322 | ], 323 | "source": [ 324 | "saver = SavingsGoal(\"Rainy Day\", new_date, 10000, 1000, 1000)" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 27, 330 | "metadata": { 331 | "id": "rMGt_E2zw6AI" 332 | }, 333 | "outputs": [], 334 | "source": [ 335 | "class WeddingGoal(SavingsGoal):\n", 336 | " def __init__(self, name, weddingDate, budget, initialContribution, monthlyContribution):\n", 337 | " super().__init__(name, weddingDate, budget, initialContribution, monthlyContribution)\n", 338 | " self.weddingDate = weddingDate\n", 339 | "\n", 340 | "class TravelGoal(SavingsGoal):\n", 341 | " def __init__(self, destination, tripDate, tripDuration, budget, initialContribution, monthlyContribution):\n", 342 | " super().__init__(destination, tripDate, budget, initialContribution, monthlyContribution)\n", 343 | " self.tripDuration = tripDuration\n", 344 | "\n", 345 | "class SplurgeGoal(SavingsGoal):\n", 346 | " def __init__(self, itemName, storeName, targetPurchaseDate, budget, initialContribution, monthlyContribution):\n", 347 | " super().__init__(itemName + \" @ \" + storeName, targetPurchaseDate, budget, initialContribution, monthlyContribution)" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 28, 353 | "metadata": { 354 | "colab": { 355 | "base_uri": "https://localhost:8080/", 356 | "height": 53 357 | }, 358 | "id": "xPuH33sqKPrm", 359 | "outputId": "f210c1f1-f8d3-44ab-934e-d0729995ddd0" 360 | }, 361 | "outputs": [ 362 | { 363 | "name": "stdout", 364 | "output_type": "stream", 365 | "text": [ 366 | "2000\n" 367 | ] 368 | }, 369 | { 370 | "data": { 371 | "text/plain": [ 372 | "'MacBook @ Apple'" 373 | ] 374 | }, 375 | "execution_count": 28, 376 | "metadata": {}, 377 | "output_type": "execute_result" 378 | } 379 | ], 380 | "source": [ 381 | "new_date = date.today() + relativedelta(months=3)\n", 382 | "new_date = new_date.strftime(\"%B %d, %Y\")\n", 383 | "\n", 384 | "goal = SplurgeGoal(\"MacBook\", \"Apple\", new_date, 1500, 500, 500)\n", 385 | "goal.name" 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": 29, 391 | "metadata": { 392 | "id": "IE2IK8Fep5Mg" 393 | }, 394 | "outputs": [], 395 | "source": [ 396 | "class IncomeGoal:\n", 397 | " def __init__(self, durationYears, startingValue, monthlyDividend):\n", 398 | " self.durationYears = durationYears\n", 399 | " self.startingValue = startingValue\n", 400 | " self.monthlyDividend = monthlyDividend\n", 401 | "\n", 402 | "class RetirementIncome(IncomeGoal):\n", 403 | " def __init__(self, retirementSavings, currentAge, retirementAge, retirementIncome):\n", 404 | " lifeExpectancy = 79\n", 405 | " durationYears = lifeExpectancy - retirementAge\n", 406 | " super().__init__(durationYears, retirementSavings, retirementIncome)\n", 407 | " self.retirementYear = date.today().year + (retirementAge-currentAge)" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 30, 413 | "metadata": { 414 | "id": "XyyVSb6tpr6F" 415 | }, 416 | "outputs": [], 417 | "source": [ 418 | "class Goal:\n", 419 | " def __init__(self, name, targetYear, targetValue, initialContribution=0, monthlyContribution=0, priority=\"\"):\n", 420 | " self.name = name\n", 421 | " self.targetYear = targetYear\n", 422 | " self.targetValue = targetValue\n", 423 | " self.initialContribution = initialContribution\n", 424 | " self.monthlyContribution = monthlyContribution\n", 425 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 426 | " raise ValueError('Wrong value set for Priority.')\n", 427 | " self.priority = priority" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": 31, 433 | "metadata": { 434 | "colab": { 435 | "base_uri": "https://localhost:8080/", 436 | "height": 35 437 | }, 438 | "id": "EOc8OWnmxZtz", 439 | "outputId": "c5a48bad-7f09-4a3e-d6b7-71f5ec469173" 440 | }, 441 | "outputs": [ 442 | { 443 | "data": { 444 | "text/plain": [ 445 | "'Dreams'" 446 | ] 447 | }, 448 | "execution_count": 31, 449 | "metadata": {}, 450 | "output_type": "execute_result" 451 | } 452 | ], 453 | "source": [ 454 | "myGoal = Goal(\"Vacation\", 2027, 10000, priority=\"Dreams\")\n", 455 | "myGoal.priority" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 32, 461 | "metadata": { 462 | "id": "NpOS1yBjsmSO" 463 | }, 464 | "outputs": [], 465 | "source": [ 466 | "def getGoalProbabilities(priority):\n", 467 | " import pandas as pd\n", 468 | " lookupTable=pd.read_csv('./Data/Goal Probability Table.csv')\n", 469 | " match = (lookupTable['Realize'] == priority)\n", 470 | " minProb = lookupTable['MinP'][(match)]\n", 471 | " maxProb = lookupTable['MaxP'][(match)]\n", 472 | " return minProb.values[0], maxProb.values[0]" 473 | ] 474 | }, 475 | { 476 | "cell_type": "code", 477 | "execution_count": 33, 478 | "metadata": { 479 | "colab": { 480 | "base_uri": "https://localhost:8080/" 481 | }, 482 | "id": "pE9LaUOttrD9", 483 | "outputId": "464114ac-d481-48cf-b6f8-4f5cc7cf6164" 484 | }, 485 | "outputs": [ 486 | { 487 | "name": "stdout", 488 | "output_type": "stream", 489 | "text": [ 490 | "0.5\n", 491 | "0.64\n" 492 | ] 493 | } 494 | ], 495 | "source": [ 496 | "minP, maxP = getGoalProbabilities(myGoal.priority)\n", 497 | "print(minP)\n", 498 | "print(maxP)" 499 | ] 500 | }, 501 | { 502 | "cell_type": "code", 503 | "execution_count": 34, 504 | "metadata": { 505 | "id": "V80sY1610HYl" 506 | }, 507 | "outputs": [], 508 | "source": [ 509 | "class Goal:\n", 510 | " def __init__(self, name, targetYear, targetValue, initialContribution=0, monthlyContribution=0, priority=\"\"):\n", 511 | " self.name = name\n", 512 | " self.targetYear = targetYear\n", 513 | " self.targetValue = targetValue\n", 514 | " self.initialContribution = initialContribution\n", 515 | " self.monthlyContribution = monthlyContribution\n", 516 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 517 | " raise ValueError('Wrong value set for Priority.')\n", 518 | " self.priority = priority\n", 519 | "\n", 520 | " def getGoalProbabilities(self):\n", 521 | " if (self.priority == \"\"):\n", 522 | " raise ValueError('No value set for Priority.')\n", 523 | " import pandas as pd\n", 524 | " lookupTable=pd.read_csv('./Data/Goal Probability Table.csv')\n", 525 | " match = (lookupTable['Realize'] == self.priority)\n", 526 | " minProb = lookupTable['MinP'][(match)]\n", 527 | " maxProb = lookupTable['MaxP'][(match)]\n", 528 | " return minProb.values[0], maxProb.values[0]" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": 35, 534 | "metadata": { 535 | "colab": { 536 | "base_uri": "https://localhost:8080/" 537 | }, 538 | "id": "WlSEihXY0Mei", 539 | "outputId": "424489c3-079a-4b82-c584-a900fa345fee" 540 | }, 541 | "outputs": [ 542 | { 543 | "name": "stdout", 544 | "output_type": "stream", 545 | "text": [ 546 | "0.5\n", 547 | "0.64\n" 548 | ] 549 | } 550 | ], 551 | "source": [ 552 | "myGoal = Goal(\"Vacation\", 2027, 10000, priority=\"Dreams\")\n", 553 | "minP, maxP = myGoal.getGoalProbabilities()\n", 554 | "print(minP)\n", 555 | "print(maxP)" 556 | ] 557 | } 558 | ], 559 | "metadata": { 560 | "colab": { 561 | "provenance": [], 562 | "toc_visible": true 563 | }, 564 | "kernelspec": { 565 | "display_name": "Python 3", 566 | "language": "python", 567 | "name": "python3" 568 | }, 569 | "language_info": { 570 | "codemirror_mode": { 571 | "name": "ipython", 572 | "version": 3 573 | }, 574 | "file_extension": ".py", 575 | "mimetype": "text/x-python", 576 | "name": "python", 577 | "nbconvert_exporter": "python", 578 | "pygments_lexer": "ipython3", 579 | "version": "3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]" 580 | }, 581 | "vscode": { 582 | "interpreter": { 583 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 584 | } 585 | } 586 | }, 587 | "nbformat": 4, 588 | "nbformat_minor": 0 589 | } 590 | -------------------------------------------------------------------------------- /Chapter11.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "id": "UyfHQbNh9MyZ" 8 | }, 9 | "source": [ 10 | "# Chapter 11: Funding your Account" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": { 17 | "id": "nFi6gzxS9S9Y" 18 | }, 19 | "outputs": [], 20 | "source": [ 21 | "# When sending a domestic bank wire, you will need to provide the recipient’s \n", 22 | "# name, address, bank account number, and ABA number (routing number).\n", 23 | "\n", 24 | "# When sending an international bank wire, you will need to provide the recipient’s \n", 25 | "# name, address, banks SWIFT BIC, and bank account number, \n", 26 | "# plus the International Payments System Routing Code, for certain countries (you will be prompted for this)." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": { 33 | "colab": { 34 | "base_uri": "https://localhost:8080/" 35 | }, 36 | "id": "h5yOVsNKSkMD", 37 | "outputId": "7daebeba-ab22-43bd-c16e-762a58d8ab2c" 38 | }, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", 45 | "Collecting plaid-python\n", 46 | " Downloading plaid-python-11.2.0.tar.gz (518 kB)\n", 47 | "\u001b[K |████████████████████████████████| 518 kB 4.4 MB/s \n", 48 | "\u001b[?25hCollecting urllib3>=1.25.3\n", 49 | " Downloading urllib3-1.26.12-py2.py3-none-any.whl (140 kB)\n", 50 | "\u001b[K |████████████████████████████████| 140 kB 41.7 MB/s \n", 51 | "\u001b[?25hRequirement already satisfied: python-dateutil in /usr/local/lib/python3.7/dist-packages (from plaid-python) (2.8.2)\n", 52 | "Collecting nulltype\n", 53 | " Downloading nulltype-2.3.1-py2.py3-none-any.whl (11 kB)\n", 54 | "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil->plaid-python) (1.15.0)\n", 55 | "Building wheels for collected packages: plaid-python\n", 56 | " Building wheel for plaid-python (setup.py) ... \u001b[?25l\u001b[?25hdone\n", 57 | " Created wheel for plaid-python: filename=plaid_python-11.2.0-py3-none-any.whl size=2728437 sha256=7f70816a0db7a2a851fc70f5535b1edcb6cb521af52e78d00d284a3a947335ef\n", 58 | " Stored in directory: /root/.cache/pip/wheels/b7/62/6c/7d2b98598a3099f1943d4a01baf2a398d8972454c39d5798f1\n", 59 | "Successfully built plaid-python\n", 60 | "Installing collected packages: urllib3, nulltype, plaid-python\n", 61 | " Attempting uninstall: urllib3\n", 62 | " Found existing installation: urllib3 1.24.3\n", 63 | " Uninstalling urllib3-1.24.3:\n", 64 | " Successfully uninstalled urllib3-1.24.3\n", 65 | "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", 66 | "requests 2.23.0 requires urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1, but you have urllib3 1.26.12 which is incompatible.\u001b[0m\n", 67 | "Successfully installed nulltype-2.3.1 plaid-python-11.2.0 urllib3-1.26.12\n" 68 | ] 69 | } 70 | ], 71 | "source": [ 72 | "pip install plaid-python" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "metadata": { 79 | "id": "eWH5Zgx5ClJS" 80 | }, 81 | "outputs": [], 82 | "source": [ 83 | "import plaid\n", 84 | "from plaid.api import plaid_api\n", 85 | "\n", 86 | "# Available environments are\n", 87 | "# 'Production'\n", 88 | "# 'Development'\n", 89 | "# 'Sandbox'\n", 90 | "configuration = plaid.Configuration(\n", 91 | " host=plaid.Environment.Sandbox,\n", 92 | " api_key={\n", 93 | " 'clientId': \"6379ff4660579b0013a77c14\",\n", 94 | " 'secret': \"95a8f8027645a1228015b1f6c701d7\",\n", 95 | " }\n", 96 | ")\n", 97 | "\n", 98 | "api_client = plaid.ApiClient(configuration)\n", 99 | "client = plaid_api.PlaidApi(api_client)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": { 106 | "id": "T6hQ6CjFi9b8" 107 | }, 108 | "outputs": [], 109 | "source": [ 110 | "from plaid.model.country_code import CountryCode\n", 111 | "from plaid.model.link_token_create_request import LinkTokenCreateRequest\n", 112 | "from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser\n", 113 | "from plaid.model.products import Products\n", 114 | "import time\n", 115 | "\n", 116 | "CLIENT_NAME = 'Plaid Test'\n", 117 | "\n", 118 | "request = LinkTokenCreateRequest(\n", 119 | " products=[Products('auth'), Products('transactions')],\n", 120 | " client_name=CLIENT_NAME,\n", 121 | " country_codes=[CountryCode('GB')],\n", 122 | " language='en',\n", 123 | " user=LinkTokenCreateRequestUser(\n", 124 | " client_user_id=str(time.time())\n", 125 | " )\n", 126 | " )\n", 127 | "# create link token\n", 128 | "response = client.link_token_create(request)\n", 129 | "\n", 130 | "# assert on response\n", 131 | "link_token = response['link_token']" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": { 138 | "id": "8DEspIVXfLCo" 139 | }, 140 | "outputs": [], 141 | "source": [ 142 | "from plaid.model.sandbox_public_token_create_request import SandboxPublicTokenCreateRequest\n", 143 | "from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest\n", 144 | "from plaid.model.products import Products\n", 145 | "\n", 146 | "SANDBOX_INSTITUTION = 'ins_109508'\n", 147 | "\n", 148 | "pt_request = SandboxPublicTokenCreateRequest(\n", 149 | " institution_id=SANDBOX_INSTITUTION,\n", 150 | " initial_products=[Products('auth')]\n", 151 | " )\n", 152 | "\n", 153 | "pt_response = client.sandbox_public_token_create(pt_request)\n", 154 | "\n", 155 | "exchange_request = ItemPublicTokenExchangeRequest(\n", 156 | " public_token=pt_response['public_token']\n", 157 | ")" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": { 164 | "id": "_W7FTnqzEMeS" 165 | }, 166 | "outputs": [], 167 | "source": [ 168 | "import plaid\n", 169 | "from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest\n", 170 | "\n", 171 | "# the public token is received from Plaid Link\n", 172 | "exchange_request = ItemPublicTokenExchangeRequest(\n", 173 | " public_token=pt_response['public_token']\n", 174 | ")\n", 175 | "exchange_response = client.item_public_token_exchange(exchange_request)\n", 176 | "access_token = exchange_response['access_token']" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "metadata": { 183 | "id": "Ak00kpzGep1t" 184 | }, 185 | "outputs": [], 186 | "source": [ 187 | "from plaid.model.account_type import AccountType\n", 188 | "from plaid.model.accounts_get_request import AccountsGetRequest\n", 189 | "\n", 190 | "accounts_request = AccountsGetRequest(\n", 191 | " access_token=access_token\n", 192 | ")\n", 193 | "accounts_response = client.accounts_get(accounts_request)\n", 194 | "\n", 195 | "account = next(\n", 196 | " acct for acct in accounts_response['accounts'] if acct['type'] == AccountType('depository'))\n", 197 | "\n", 198 | "account_id = account['account_id']" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "metadata": { 205 | "colab": { 206 | "base_uri": "https://localhost:8080/", 207 | "height": 35 208 | }, 209 | "id": "de14aY1KmzjM", 210 | "outputId": "d28fa6cb-4f2d-41d0-955f-27b61d9032ae" 211 | }, 212 | "outputs": [ 213 | { 214 | "data": { 215 | "application/vnd.google.colaboratory.intrinsic+json": { 216 | "type": "string" 217 | }, 218 | "text/plain": [ 219 | "'aqyaKraXmeCLvGbgaZg4IW4nEqAMjytP5pA5l'" 220 | ] 221 | }, 222 | "execution_count": 20, 223 | "metadata": {}, 224 | "output_type": "execute_result" 225 | } 226 | ], 227 | "source": [ 228 | "account_id" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": { 235 | "colab": { 236 | "base_uri": "https://localhost:8080/", 237 | "height": 586 238 | }, 239 | "id": "rr-VxuVXeLLT", 240 | "outputId": "aca31a31-fdb8-4209-9159-776ab2bb6683" 241 | }, 242 | "outputs": [ 243 | { 244 | "ename": "ApiException", 245 | "evalue": "ignored", 246 | "output_type": "error", 247 | "traceback": [ 248 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 249 | "\u001b[0;31mApiException\u001b[0m Traceback (most recent call last)", 250 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 21\u001b[0m )\n\u001b[1;32m 22\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 23\u001b[0;31m \u001b[0mbt_response\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mclient\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbank_transfer_create\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbt_request\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 251 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 767\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 768\u001b[0m \"\"\"\n\u001b[0;32m--> 769\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 770\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 771\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mcall_with_http_info\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 252 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api/plaid_api.py\u001b[0m in \u001b[0;36m__bank_transfer_create\u001b[0;34m(self, bank_transfer_create_request, **kwargs)\u001b[0m\n\u001b[1;32m 2297\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'bank_transfer_create_request'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2298\u001b[0m \u001b[0mbank_transfer_create_request\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2299\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcall_with_http_info\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2300\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2301\u001b[0m self.bank_transfer_create = _Endpoint(\n", 253 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36mcall_with_http_info\u001b[0;34m(self, **kwargs)\u001b[0m\n\u001b[1;32m 845\u001b[0m \u001b[0m_request_timeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'_request_timeout'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 846\u001b[0m \u001b[0m_host\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_host\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 847\u001b[0;31m collection_formats=params['collection_format'])\n\u001b[0m", 254 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36mcall_api\u001b[0;34m(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, async_req, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host, _check_type)\u001b[0m\n\u001b[1;32m 410\u001b[0m \u001b[0m_return_http_data_only\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcollection_formats\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 411\u001b[0m \u001b[0m_preload_content\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_request_timeout\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_host\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 412\u001b[0;31m _check_type)\n\u001b[0m\u001b[1;32m 413\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 414\u001b[0m return self.pool.apply_async(self.__call_api, (resource_path,\n", 255 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36m__call_api\u001b[0;34m(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host, _check_type)\u001b[0m\n\u001b[1;32m 198\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mApiException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 199\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbody\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'utf-8'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 200\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 201\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlast_response\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mresponse_data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 256 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36m__call_api\u001b[0;34m(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host, _check_type)\u001b[0m\n\u001b[1;32m 195\u001b[0m \u001b[0mpost_params\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mpost_params\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbody\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 196\u001b[0m \u001b[0m_preload_content\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_preload_content\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 197\u001b[0;31m _request_timeout=_request_timeout)\n\u001b[0m\u001b[1;32m 198\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mApiException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 199\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbody\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbody\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'utf-8'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 257 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/api_client.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, method, url, query_params, headers, post_params, body, _preload_content, _request_timeout)\u001b[0m\n\u001b[1;32m 456\u001b[0m \u001b[0m_preload_content\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_preload_content\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 457\u001b[0m \u001b[0m_request_timeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_request_timeout\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 458\u001b[0;31m body=body)\n\u001b[0m\u001b[1;32m 459\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mmethod\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"PUT\"\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 460\u001b[0m return self.rest_client.PUT(url,\n", 258 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/rest.py\u001b[0m in \u001b[0;36mPOST\u001b[0;34m(self, url, headers, query_params, post_params, body, _preload_content, _request_timeout)\u001b[0m\n\u001b[1;32m 268\u001b[0m \u001b[0m_preload_content\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_preload_content\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 269\u001b[0m \u001b[0m_request_timeout\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0m_request_timeout\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 270\u001b[0;31m body=body)\n\u001b[0m\u001b[1;32m 271\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 272\u001b[0m def PUT(self, url, headers=None, query_params=None, post_params=None,\n", 259 | "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/plaid/rest.py\u001b[0m in \u001b[0;36mrequest\u001b[0;34m(self, method, url, query_params, headers, body, post_params, _preload_content, _request_timeout)\u001b[0m\n\u001b[1;32m 221\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mServiceException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhttp_resp\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 222\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 223\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mApiException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhttp_resp\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 224\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 225\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mr\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 260 | "\u001b[0;31mApiException\u001b[0m: (400)\nReason: Bad Request\nHTTP response headers: HTTPHeaderDict({'Server': 'nginx', 'Date': 'Sun, 20 Nov 2022 10:29:34 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '342', 'Connection': 'keep-alive', 'plaid-version': '2020-09-14'})\nHTTP response body: {\n \"display_message\": null,\n \"documentation_url\": \"https://plaid.com/docs/?ref=error#invalid-input-errors\",\n \"error_code\": \"INVALID_PRODUCT\",\n \"error_message\": \"client is not authorized to access the following products: [\\\"bank_transfer\\\"]\",\n \"error_type\": \"INVALID_INPUT\",\n \"request_id\": \"fSbWQAB5AZ1ULrN\",\n \"suggested_action\": null\n}\n" 261 | ] 262 | } 263 | ], 264 | "source": [ 265 | "from plaid.model.bank_transfer_create_request import BankTransferCreateRequest\n", 266 | "from plaid.model.bank_transfer_network import BankTransferNetwork\n", 267 | "from plaid.model.bank_transfer_idempotency_key import BankTransferIdempotencyKey\n", 268 | "from plaid.model.bank_transfer_type import BankTransferType\n", 269 | "from plaid.model.bank_transfer_user import BankTransferUser\n", 270 | "from plaid.model.ach_class import ACHClass\n", 271 | "from random import random\n", 272 | "\n", 273 | "bt_request = BankTransferCreateRequest(\n", 274 | " idempotency_key=BankTransferIdempotencyKey(str(random())),\n", 275 | " access_token=access_token,\n", 276 | " account_id=account_id,\n", 277 | " type=BankTransferType('credit'),\n", 278 | " network=BankTransferNetwork('ach'),\n", 279 | " amount='1.00',\n", 280 | " iso_currency_code='USD',\n", 281 | " description='test',\n", 282 | " user=BankTransferUser(legal_name='Firstname Lastname'),\n", 283 | " ach_class=ACHClass('ppd'),\n", 284 | " custom_tag='',\n", 285 | " )\n", 286 | "\n", 287 | "bt_response = client.bank_transfer_create(bt_request)" 288 | ] 289 | } 290 | ], 291 | "metadata": { 292 | "colab": { 293 | "provenance": [], 294 | "toc_visible": true 295 | }, 296 | "kernelspec": { 297 | "display_name": "Python 3", 298 | "language": "python", 299 | "name": "python3" 300 | }, 301 | "language_info": { 302 | "codemirror_mode": { 303 | "name": "ipython", 304 | "version": 3 305 | }, 306 | "file_extension": ".py", 307 | "mimetype": "text/x-python", 308 | "name": "python", 309 | "nbconvert_exporter": "python", 310 | "pygments_lexer": "ipython3", 311 | "version": "3.11.0 (main, Nov 15 2022, 05:43:36) [Clang 14.0.0 (clang-1400.0.29.202)]" 312 | }, 313 | "vscode": { 314 | "interpreter": { 315 | "hash": "1a1af0ee75eeea9e2e1ee996c87e7a2b11a0bebd85af04bb136d915cefc0abce" 316 | } 317 | } 318 | }, 319 | "nbformat": 4, 320 | "nbformat_minor": 0 321 | } 322 | -------------------------------------------------------------------------------- /Chapter07.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "8gOHbgRVoLwx" 7 | }, 8 | "source": [ 9 | "# Chapter 7: Risk Profiling" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": { 16 | "id": "BTGk_kQ9pryP" 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "#HEADING 2: Creating a Risk Questionnaire\n", 21 | "class RiskQuestion:\n", 22 | " def __init__(self, questionText, weight=1):\n", 23 | " self.questionText = questionText\n", 24 | " self.weight = weight\n", 25 | " self.answers = []\n", 26 | "\n", 27 | "class RiskQuestionAnswer:\n", 28 | " def __init__(self, answerText, score, selected=False):\n", 29 | " self.answerText = answerText\n", 30 | " self.score = score\n", 31 | " self.selected = selected\n", 32 | "\n", 33 | "class RiskQuestionnaire:\n", 34 | " def __init__(self):\n", 35 | " self.questions = []" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 2, 41 | "metadata": { 42 | "id": "SBDhK8HCv_SG" 43 | }, 44 | "outputs": [], 45 | "source": [ 46 | "# Initialize a Risk Tolerance Questionnaire\n", 47 | "# Using https://pfp.missouri.edu/research/investment-risk-tolerance-assessment/\n", 48 | "# Risk Tolerance Quiz Source: Grable, J. E., & Lytton, R. H. (1999). Financial risk tolerance revisited: The development of a risk assessment instrument. Financial Services Review, 8, 163-181.\n", 49 | "toleranceQuestionnaire = RiskQuestionnaire()\n", 50 | "\n", 51 | "question1 = RiskQuestion(\"In general, how would your best friend describe you as a risk taker?\", 2)\n", 52 | "question1.answers.append(RiskQuestionAnswer(\"A real gambler\",4))\n", 53 | "question1.answers.append(RiskQuestionAnswer(\"Willing to take risks after completing adequate research\",3))\n", 54 | "question1.answers.append(RiskQuestionAnswer(\"Cautious\",2))\n", 55 | "question1.answers.append(RiskQuestionAnswer(\"A real risk avoider\",1))\n", 56 | "\n", 57 | "question2 = RiskQuestion(\"You are on a TV game show and can choose one of the following. Which would you take?\")\n", 58 | "question2.answers.append(RiskQuestionAnswer(\"$1,000 in cash\",1))\n", 59 | "question2.answers.append(RiskQuestionAnswer(\"A 50% chance at winning $5,000\",2))\n", 60 | "question2.answers.append(RiskQuestionAnswer(\"A 25% chance at winning $10,000\",3))\n", 61 | "question2.answers.append(RiskQuestionAnswer(\"A 5% chance at winning $100,000\",4))\n", 62 | "\n", 63 | "question3 = RiskQuestion(\"When you think of the word risk which of the following words comes to mind first?\")\n", 64 | "question3.answers.append(RiskQuestionAnswer(\"Loss\",1))\n", 65 | "question3.answers.append(RiskQuestionAnswer(\"Uncertainty\",2))\n", 66 | "question3.answers.append(RiskQuestionAnswer(\"Opportunity\",3))\n", 67 | "question3.answers.append(RiskQuestionAnswer(\"Thrill\",4))\n", 68 | "\n", 69 | "toleranceQuestionnaire.questions.append(question1)\n", 70 | "toleranceQuestionnaire.questions.append(question2)\n", 71 | "toleranceQuestionnaire.questions.append(question3)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 3, 77 | "metadata": { 78 | "id": "rg5HJmpD6B78" 79 | }, 80 | "outputs": [], 81 | "source": [ 82 | "\n", 83 | "# Initialize a Risk Capacity Questionnaire\n", 84 | "# https://www.rbcgam.com/en/ca/learn-plan/investment-basics/whats-your-risk-capacity/detail\n", 85 | "capacityQuestionnaire = RiskQuestionnaire()\n", 86 | "\n", 87 | "question4 = RiskQuestion(\"You are able to save money regularly.\")\n", 88 | "question4.answers.append(RiskQuestionAnswer(\"Completely false\",1))\n", 89 | "question4.answers.append(RiskQuestionAnswer(\"Somewhat true\",2))\n", 90 | "question4.answers.append(RiskQuestionAnswer(\"Completely true\",3))\n", 91 | "\n", 92 | "question5 = RiskQuestion(\"You can pay all your monthly bills on time -- including any credit card or other debt.\")\n", 93 | "question5.answers.append(RiskQuestionAnswer(\"Completely false\",1))\n", 94 | "question5.answers.append(RiskQuestionAnswer(\"Somewhat true\",2))\n", 95 | "question5.answers.append(RiskQuestionAnswer(\"Completely true\",3))\n", 96 | "\n", 97 | "question6 = RiskQuestion(\"If you lose money investing today, your current lifestyle would not be impacted.\")\n", 98 | "question6.answers.append(RiskQuestionAnswer(\"Completely false\",1))\n", 99 | "question6.answers.append(RiskQuestionAnswer(\"Somewhat true\",2))\n", 100 | "question6.answers.append(RiskQuestionAnswer(\"Completely true\",3))\n", 101 | "\n", 102 | "question7 = RiskQuestion(\"You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.\")\n", 103 | "question7.answers.append(RiskQuestionAnswer(\"Completely false\",1))\n", 104 | "question7.answers.append(RiskQuestionAnswer(\"Somewhat true\",2))\n", 105 | "question7.answers.append(RiskQuestionAnswer(\"Completely true\",3))\n", 106 | "\n", 107 | "capacityQuestionnaire.questions.append(question4)\n", 108 | "capacityQuestionnaire.questions.append(question5)\n", 109 | "capacityQuestionnaire.questions.append(question6)\n", 110 | "capacityQuestionnaire.questions.append(question7)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 4, 116 | "metadata": { 117 | "colab": { 118 | "base_uri": "https://localhost:8080/" 119 | }, 120 | "id": "R9C33x2Ay84Z", 121 | "outputId": "65e72d86-9e30-4e34-ad1e-34962dfc5585" 122 | }, 123 | "outputs": [ 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "Risk Tolerance: \n", 129 | "\n", 130 | "In general, how would your best friend describe you as a risk taker?\n", 131 | " -A real gambler\n", 132 | " -Willing to take risks after completing adequate research\n", 133 | " -Cautious\n", 134 | " -A real risk avoider\n", 135 | "\n", 136 | "\n", 137 | "You are on a TV game show and can choose one of the following. Which would you take?\n", 138 | " -$1,000 in cash\n", 139 | " -A 50% chance at winning $5,000\n", 140 | " -A 25% chance at winning $10,000\n", 141 | " -A 5% chance at winning $100,000\n", 142 | "\n", 143 | "\n", 144 | "When you think of the word risk which of the following words comes to mind first?\n", 145 | " -Loss\n", 146 | " -Uncertainty\n", 147 | " -Opportunity\n", 148 | " -Thrill\n", 149 | "\n", 150 | "\n", 151 | "Risk Capacity: \n", 152 | "\n", 153 | "You are able to save money regularly.\n", 154 | " -Completely false\n", 155 | " -Somewhat true\n", 156 | " -Completely true\n", 157 | "\n", 158 | "\n", 159 | "You can pay all your monthly bills on time -- including any credit card or other debt.\n", 160 | " -Completely false\n", 161 | " -Somewhat true\n", 162 | " -Completely true\n", 163 | "\n", 164 | "\n", 165 | "If you lose money investing today, your current lifestyle would not be impacted.\n", 166 | " -Completely false\n", 167 | " -Somewhat true\n", 168 | " -Completely true\n", 169 | "\n", 170 | "\n", 171 | "You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.\n", 172 | " -Completely false\n", 173 | " -Somewhat true\n", 174 | " -Completely true\n", 175 | "\n", 176 | "\n" 177 | ] 178 | } 179 | ], 180 | "source": [ 181 | "# Answer Risk Questionnaires\n", 182 | "print(\"Risk Tolerance: \\n\")\n", 183 | "for question in toleranceQuestionnaire.questions:\n", 184 | " print(question.questionText)\n", 185 | " for answer in question.answers:\n", 186 | " print(\" -\" + answer.answerText)\n", 187 | " print(\"\\n\")\n", 188 | "\n", 189 | "print(\"Risk Capacity: \\n\")\n", 190 | "for question in capacityQuestionnaire.questions:\n", 191 | " print(question.questionText)\n", 192 | " for answer in question.answers:\n", 193 | " print(\" -\" + answer.answerText)\n", 194 | " print(\"\\n\")" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 5, 200 | "metadata": { 201 | "id": "6ofQeYWFD7U5" 202 | }, 203 | "outputs": [], 204 | "source": [ 205 | "class RiskQuestionnaire:\n", 206 | " def __init__(self):\n", 207 | " self.questions = []\n", 208 | "\n", 209 | " def loadQuestionnaire(self, riskQuestionsFileName, riskAnswersFileName, type):\n", 210 | "\n", 211 | " if not (type in [\"Tolerance\", \"Capacity\"]):\n", 212 | " raise ValueError('Type must be Tolerance or Capacity.')\n", 213 | "\n", 214 | " import pandas as pd\n", 215 | " riskQuestions = pd.read_csv(riskQuestionsFileName).reset_index()\n", 216 | " riskAnswers = pd.read_csv(riskAnswersFileName).reset_index()\n", 217 | "\n", 218 | " if (type == \"Tolerance\"):\n", 219 | " toleranceQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Tolerance')].reset_index()\n", 220 | " for index, row in toleranceQuestions.iterrows():\n", 221 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 222 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 223 | " for indexA, rowA in answers.iterrows():\n", 224 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 225 | " else:\n", 226 | " capacityQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Capacity')].reset_index()\n", 227 | " for index, row in capacityQuestions.iterrows():\n", 228 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 229 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 230 | " for indexA, rowA in answers.iterrows():\n", 231 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 232 | " " 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": 6, 238 | "metadata": { 239 | "id": "NYO-g9M2Q1tF" 240 | }, 241 | "outputs": [], 242 | "source": [ 243 | "questionsFileName = './Data/Risk Questions.csv'\n", 244 | "answersFileName = './Data/Risk Answers.csv'\n", 245 | "\n", 246 | "toleranceQuestionnaire = RiskQuestionnaire()\n", 247 | "toleranceQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Tolerance\")\n", 248 | "\n", 249 | "capacityQuestionnaire = RiskQuestionnaire()\n", 250 | "capacityQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Capacity\")" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 7, 256 | "metadata": { 257 | "colab": { 258 | "base_uri": "https://localhost:8080/" 259 | }, 260 | "id": "BqpgbuEXIdcU", 261 | "outputId": "c93b0056-c996-4b64-e160-dc23b1d719b7" 262 | }, 263 | "outputs": [ 264 | { 265 | "name": "stdout", 266 | "output_type": "stream", 267 | "text": [ 268 | "Risk Tolerance: \n", 269 | "\n", 270 | "In general, how would your best friend describe you as a risk taker?\n", 271 | " -A real gambler\n", 272 | " -Willing to take risks after completing adequate research\n", 273 | " -Cautious\n", 274 | " -A real risk avoider\n", 275 | "\n", 276 | "\n", 277 | "You are on a TV game show and can choose one of the following. Which would you take?\n", 278 | " -$1,000 in cash\n", 279 | " -A 50% chance at winning $5,000\n", 280 | " -A 25% chance at winning $10,000\n", 281 | " -A 5% chance at winning $100,000\n", 282 | "\n", 283 | "\n", 284 | "When you think of the word risk which of the following words comes to mind first?\n", 285 | " -Loss\n", 286 | " -Uncertainty\n", 287 | " -Opportunity\n", 288 | " -Thrill\n", 289 | "\n", 290 | "\n", 291 | "Risk Capacity: \n", 292 | "\n", 293 | "You are able to save money regularly.\n", 294 | " -Completely false\n", 295 | " -Somewhat true\n", 296 | " -Completely true\n", 297 | "\n", 298 | "\n", 299 | "You can pay all your monthly bills on time -- including any credit card or other debt.\n", 300 | " -Completely false\n", 301 | " -Somewhat true\n", 302 | " -Completely true\n", 303 | "\n", 304 | "\n", 305 | "If you lose money investing today, your current lifestyle would not be impacted.\n", 306 | " -Completely false\n", 307 | " -Somewhat true\n", 308 | " -Completely true\n", 309 | "\n", 310 | "\n", 311 | "You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.\n", 312 | " -Completely false\n", 313 | " -Somewhat true\n", 314 | " -Completely true\n", 315 | "\n", 316 | "\n" 317 | ] 318 | } 319 | ], 320 | "source": [ 321 | "# Print again to confirm\n", 322 | "print(\"Risk Tolerance: \\n\")\n", 323 | "for question in toleranceQuestionnaire.questions:\n", 324 | " print(question.questionText)\n", 325 | " for answer in question.answers:\n", 326 | " print(\" -\" + answer.answerText)\n", 327 | " print(\"\\n\")\n", 328 | "\n", 329 | "print(\"Risk Capacity: \\n\")\n", 330 | "for question in capacityQuestionnaire.questions:\n", 331 | " print(question.questionText)\n", 332 | " for answer in question.answers:\n", 333 | " print(\" -\" + answer.answerText)\n", 334 | " print(\"\\n\")" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 8, 340 | "metadata": { 341 | "id": "pC6Lmlzd1SEI" 342 | }, 343 | "outputs": [], 344 | "source": [ 345 | "# Set selected for each question\n", 346 | "toleranceQuestionnaire.questions[0].answers[1].selected = True\n", 347 | "toleranceQuestionnaire.questions[1].answers[0].selected = True\n", 348 | "toleranceQuestionnaire.questions[2].answers[2].selected = True\n", 349 | "\n", 350 | "capacityQuestionnaire.questions[0].answers[1].selected = True\n", 351 | "capacityQuestionnaire.questions[1].answers[1].selected = True\n", 352 | "capacityQuestionnaire.questions[2].answers[2].selected = True\n", 353 | "capacityQuestionnaire.questions[3].answers[1].selected = True" 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": 9, 359 | "metadata": { 360 | "id": "0JoR-SiW9SMI" 361 | }, 362 | "outputs": [], 363 | "source": [ 364 | "# Interactive questionnaire\n", 365 | "\n", 366 | "class RiskQuestionnaire:\n", 367 | " def __init__(self):\n", 368 | " self.questions = []\n", 369 | "\n", 370 | " def loadQuestionnaire(self, riskQuestionsFileName, riskAnswersFileName, type):\n", 371 | "\n", 372 | " if not (type in [\"Tolerance\", \"Capacity\"]):\n", 373 | " raise ValueError('Type must be Tolerance or Capacity.')\n", 374 | "\n", 375 | " import pandas as pd\n", 376 | " riskQuestions = pd.read_csv(riskQuestionsFileName).reset_index()\n", 377 | " riskAnswers = pd.read_csv(riskAnswersFileName).reset_index()\n", 378 | "\n", 379 | " if (type == \"Tolerance\"):\n", 380 | " toleranceQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Tolerance')].reset_index()\n", 381 | " for index, row in toleranceQuestions.iterrows():\n", 382 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 383 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 384 | " for indexA, rowA in answers.iterrows():\n", 385 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 386 | " else:\n", 387 | " capacityQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Capacity')].reset_index()\n", 388 | " for index, row in capacityQuestions.iterrows():\n", 389 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 390 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 391 | " for indexA, rowA in answers.iterrows():\n", 392 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 393 | " \n", 394 | "\n", 395 | " def answerQuestionnaire(self):\n", 396 | " for i in range(len(self.questions)):\n", 397 | " question = self.questions[i]\n", 398 | " print(question.questionText)\n", 399 | " for n in range(len(question.answers)):\n", 400 | " answer = question.answers[n]\n", 401 | " print(str(n) + \": \" + answer.answerText)\n", 402 | " nChosen = int(input(\"Choose your answer between 0 and \" + str(len(question.answers)-1) + \": \"))\n", 403 | " self.questions[i].answers[nChosen].selected = True\n", 404 | " print(\"\\n\")" 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": 10, 410 | "metadata": { 411 | "colab": { 412 | "base_uri": "https://localhost:8080/" 413 | }, 414 | "id": "zzqT54s0V0w3", 415 | "outputId": "fa7e50f9-c66d-4517-9c92-bc52ba117092" 416 | }, 417 | "outputs": [ 418 | { 419 | "name": "stdout", 420 | "output_type": "stream", 421 | "text": [ 422 | "In general, how would your best friend describe you as a risk taker?\n", 423 | "0: A real gambler\n", 424 | "1: Willing to take risks after completing adequate research\n", 425 | "2: Cautious\n", 426 | "3: A real risk avoider\n", 427 | "\n", 428 | "\n", 429 | "You are on a TV game show and can choose one of the following. Which would you take?\n", 430 | "0: $1,000 in cash\n", 431 | "1: A 50% chance at winning $5,000\n", 432 | "2: A 25% chance at winning $10,000\n", 433 | "3: A 5% chance at winning $100,000\n", 434 | "\n", 435 | "\n", 436 | "When you think of the word risk which of the following words comes to mind first?\n", 437 | "0: Loss\n", 438 | "1: Uncertainty\n", 439 | "2: Opportunity\n", 440 | "3: Thrill\n", 441 | "\n", 442 | "\n", 443 | "You are able to save money regularly.\n", 444 | "0: Completely false\n", 445 | "1: Somewhat true\n", 446 | "2: Completely true\n", 447 | "\n", 448 | "\n", 449 | "You can pay all your monthly bills on time -- including any credit card or other debt.\n", 450 | "0: Completely false\n", 451 | "1: Somewhat true\n", 452 | "2: Completely true\n", 453 | "\n", 454 | "\n", 455 | "If you lose money investing today, your current lifestyle would not be impacted.\n", 456 | "0: Completely false\n", 457 | "1: Somewhat true\n", 458 | "2: Completely true\n", 459 | "\n", 460 | "\n", 461 | "You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.\n", 462 | "0: Completely false\n", 463 | "1: Somewhat true\n", 464 | "2: Completely true\n", 465 | "\n", 466 | "\n" 467 | ] 468 | } 469 | ], 470 | "source": [ 471 | "questionsFileName = './Data/Risk Questions.csv'\n", 472 | "answersFileName = './Data/Risk Answers.csv'\n", 473 | "\n", 474 | "toleranceQuestionnaire = RiskQuestionnaire()\n", 475 | "toleranceQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Tolerance\")\n", 476 | "\n", 477 | "capacityQuestionnaire = RiskQuestionnaire()\n", 478 | "capacityQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Capacity\")\n", 479 | "\n", 480 | "toleranceQuestionnaire.answerQuestionnaire()\n", 481 | "capacityQuestionnaire.answerQuestionnaire()" 482 | ] 483 | }, 484 | { 485 | "cell_type": "code", 486 | "execution_count": 11, 487 | "metadata": { 488 | "colab": { 489 | "base_uri": "https://localhost:8080/" 490 | }, 491 | "id": "VEb5eotjDp1j", 492 | "outputId": "15fb38e8-56ea-4fbf-f8a2-aa7a86a57ca6" 493 | }, 494 | "outputs": [ 495 | { 496 | "name": "stdout", 497 | "output_type": "stream", 498 | "text": [ 499 | "False\n", 500 | "False\n" 501 | ] 502 | } 503 | ], 504 | "source": [ 505 | "print(toleranceQuestionnaire.questions[0].answers[1].selected)\n", 506 | "print(capacityQuestionnaire.questions[2].answers[2].selected)" 507 | ] 508 | }, 509 | { 510 | "cell_type": "code", 511 | "execution_count": 12, 512 | "metadata": { 513 | "id": "46Ka-bxNrQga" 514 | }, 515 | "outputs": [], 516 | "source": [ 517 | "# Add goal-based risk tolerance: can you afford a delay or change in amount? Goal priority: essential/important/aspirational?\n", 518 | "tolerranceQuestionnaireGoals = RiskQuestionnaire()\n", 519 | "\n", 520 | "question1 = RiskQuestion(\"How flexible are you in the target amount and timeline for your Goal?\", 2)\n", 521 | "question1.answers.append(RiskQuestionAnswer(\"Above 10% flexible on amount and time\",4))\n", 522 | "question1.answers.append(RiskQuestionAnswer(\"Above 10% flexible on one dimension\",3))\n", 523 | "question1.answers.append(RiskQuestionAnswer(\"Below 10% flexible on one dimension\",2))\n", 524 | "question1.answers.append(RiskQuestionAnswer(\"Below 10% flexible on amount and time\",1))\n", 525 | "\n", 526 | "question2 = RiskQuestion(\"What negative emotions will achieving this Goal help you avoid?\")\n", 527 | "question2.answers.append(RiskQuestionAnswer(\"Nightmares\",1))\n", 528 | "question2.answers.append(RiskQuestionAnswer(\"Fears\",2))\n", 529 | "question2.answers.append(RiskQuestionAnswer(\"Worries\",3))\n", 530 | "question2.answers.append(RiskQuestionAnswer(\"Concerns\",4))\n", 531 | "\n", 532 | "question3 = RiskQuestion(\"What are you hoping to realize through this Goal?\")\n", 533 | "question3.answers.append(RiskQuestionAnswer(\"Needs\",1))\n", 534 | "question3.answers.append(RiskQuestionAnswer(\"Wants\",2))\n", 535 | "question3.answers.append(RiskQuestionAnswer(\"Wishes\",3))\n", 536 | "question3.answers.append(RiskQuestionAnswer(\"Dreams\",4))\n", 537 | "\n", 538 | "tolerranceQuestionnaireGoals.questions.append(question1)\n", 539 | "tolerranceQuestionnaireGoals.questions.append(question2)\n", 540 | "tolerranceQuestionnaireGoals.questions.append(question3)\n" 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": 13, 546 | "metadata": { 547 | "id": "MsbeWo3fpr39" 548 | }, 549 | "outputs": [], 550 | "source": [ 551 | "#HEADING 3: Calculating a Risk Score\n", 552 | "\n", 553 | "# Interactive questionnaire\n", 554 | "\n", 555 | "class RiskQuestionnaire:\n", 556 | " def __init__(self):\n", 557 | " self.questions = []\n", 558 | " self.score = 0\n", 559 | "\n", 560 | " def loadQuestionnaire(self, riskQuestionsFileName, riskAnswersFileName, type):\n", 561 | "\n", 562 | " if not (type in [\"Tolerance\", \"Capacity\"]):\n", 563 | " raise ValueError('Type must be Tolerance or Capacity.')\n", 564 | "\n", 565 | " import pandas as pd\n", 566 | " riskQuestions = pd.read_csv(riskQuestionsFileName).reset_index()\n", 567 | " riskAnswers = pd.read_csv(riskAnswersFileName).reset_index()\n", 568 | "\n", 569 | " if (type == \"Tolerance\"):\n", 570 | " toleranceQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Tolerance')].reset_index()\n", 571 | " for index, row in toleranceQuestions.iterrows():\n", 572 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 573 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 574 | " for indexA, rowA in answers.iterrows():\n", 575 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 576 | " else:\n", 577 | " capacityQuestions = riskQuestions[(riskQuestions['QuestionType'] == 'Capacity')].reset_index()\n", 578 | " for index, row in capacityQuestions.iterrows():\n", 579 | " self.questions.append(RiskQuestion(row['QuestionText'], row['QuestionWeight']))\n", 580 | " answers = riskAnswers[(riskAnswers['QuestionID'] == row['QuestionID'])]\n", 581 | " for indexA, rowA in answers.iterrows():\n", 582 | " self.questions[index].answers.append(RiskQuestionAnswer(rowA['AnswerText'],rowA['AnswerValue']))\n", 583 | " \n", 584 | "\n", 585 | " def answerQuestionnaire(self):\n", 586 | " for i in range(len(self.questions)):\n", 587 | " question = self.questions[i]\n", 588 | " print(question.questionText)\n", 589 | " for n in range(len(question.answers)):\n", 590 | " answer = question.answers[n]\n", 591 | " print(str(n) + \": \" + answer.answerText)\n", 592 | " nChosen = int(input(\"Choose your answer between 0 and \" + str(len(question.answers)-1) + \": \"))\n", 593 | " self.questions[i].answers[nChosen].selected = True\n", 594 | " print(\"\\n\")\n", 595 | "\n", 596 | " def calculateScore(self):\n", 597 | " print(\"Risk Score:\")\n", 598 | " myTotalScore = 0\n", 599 | " for question in self.questions:\n", 600 | " for answer in question.answers:\n", 601 | " if (answer.selected == True):\n", 602 | " myTotalScore = myTotalScore + (answer.score * question.weight)\n", 603 | " print(answer.answerText + \": \" + str(answer.score * question.weight))\n", 604 | " print(\"Total Risk Score: \" + str(myTotalScore) + \"\\n\")\n", 605 | " self.score = myTotalScore" 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": 14, 611 | "metadata": { 612 | "colab": { 613 | "base_uri": "https://localhost:8080/" 614 | }, 615 | "id": "4HCErUXmiiiS", 616 | "outputId": "b910e3a7-f4b3-449e-eaeb-a1b586dfcf92" 617 | }, 618 | "outputs": [ 619 | { 620 | "name": "stdout", 621 | "output_type": "stream", 622 | "text": [ 623 | "In general, how would your best friend describe you as a risk taker?\n", 624 | "0: A real gambler\n", 625 | "1: Willing to take risks after completing adequate research\n", 626 | "2: Cautious\n", 627 | "3: A real risk avoider\n", 628 | "\n", 629 | "\n", 630 | "You are on a TV game show and can choose one of the following. Which would you take?\n", 631 | "0: $1,000 in cash\n", 632 | "1: A 50% chance at winning $5,000\n", 633 | "2: A 25% chance at winning $10,000\n", 634 | "3: A 5% chance at winning $100,000\n", 635 | "\n", 636 | "\n", 637 | "When you think of the word risk which of the following words comes to mind first?\n", 638 | "0: Loss\n", 639 | "1: Uncertainty\n", 640 | "2: Opportunity\n", 641 | "3: Thrill\n", 642 | "\n", 643 | "\n", 644 | "You are able to save money regularly.\n", 645 | "0: Completely false\n", 646 | "1: Somewhat true\n", 647 | "2: Completely true\n", 648 | "\n", 649 | "\n", 650 | "You can pay all your monthly bills on time -- including any credit card or other debt.\n", 651 | "0: Completely false\n", 652 | "1: Somewhat true\n", 653 | "2: Completely true\n", 654 | "\n", 655 | "\n", 656 | "If you lose money investing today, your current lifestyle would not be impacted.\n", 657 | "0: Completely false\n", 658 | "1: Somewhat true\n", 659 | "2: Completely true\n", 660 | "\n", 661 | "\n", 662 | "You do not need to draw down more than 5% of your investment portfolio for any major financial goal in the next five years.\n", 663 | "0: Completely false\n", 664 | "1: Somewhat true\n", 665 | "2: Completely true\n", 666 | "\n", 667 | "\n" 668 | ] 669 | } 670 | ], 671 | "source": [ 672 | "questionsFileName = './Data/Risk Questions.csv'\n", 673 | "answersFileName = './Data/Risk Answers.csv'\n", 674 | "\n", 675 | "toleranceQuestionnaire = RiskQuestionnaire()\n", 676 | "toleranceQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Tolerance\")\n", 677 | "\n", 678 | "capacityQuestionnaire = RiskQuestionnaire()\n", 679 | "capacityQuestionnaire.loadQuestionnaire(questionsFileName, answersFileName, \"Capacity\")\n", 680 | "\n", 681 | "toleranceQuestionnaire.answerQuestionnaire()\n", 682 | "capacityQuestionnaire.answerQuestionnaire()" 683 | ] 684 | }, 685 | { 686 | "cell_type": "code", 687 | "execution_count": 15, 688 | "metadata": { 689 | "colab": { 690 | "base_uri": "https://localhost:8080/" 691 | }, 692 | "id": "KDyP_vE6ipTV", 693 | "outputId": "22bf1eec-002b-40a2-d69b-673cbb552711" 694 | }, 695 | "outputs": [ 696 | { 697 | "name": "stdout", 698 | "output_type": "stream", 699 | "text": [ 700 | "Risk Score:\n", 701 | "A real gambler: 8\n", 702 | "$1,000 in cash: 1\n", 703 | "Loss: 1\n", 704 | "Total Risk Score: 10\n", 705 | "\n", 706 | "Risk Score:\n", 707 | "Completely false: 1\n", 708 | "Completely false: 1\n", 709 | "Completely false: 1\n", 710 | "Completely false: 1\n", 711 | "Total Risk Score: 4\n", 712 | "\n" 713 | ] 714 | } 715 | ], 716 | "source": [ 717 | "toleranceQuestionnaire.calculateScore()\n", 718 | "capacityQuestionnaire.calculateScore()" 719 | ] 720 | } 721 | ], 722 | "metadata": { 723 | "colab": { 724 | "provenance": [], 725 | "toc_visible": true 726 | }, 727 | "kernelspec": { 728 | "display_name": "Python 3", 729 | "language": "python", 730 | "name": "python3" 731 | }, 732 | "language_info": { 733 | "codemirror_mode": { 734 | "name": "ipython", 735 | "version": 3 736 | }, 737 | "file_extension": ".py", 738 | "mimetype": "text/x-python", 739 | "name": "python", 740 | "nbconvert_exporter": "python", 741 | "pygments_lexer": "ipython3", 742 | "version": "3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]" 743 | }, 744 | "vscode": { 745 | "interpreter": { 746 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 747 | } 748 | } 749 | }, 750 | "nbformat": 4, 751 | "nbformat_minor": 0 752 | } 753 | -------------------------------------------------------------------------------- /Chapter14.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "id": "DNdSh44KF-UH" 8 | }, 9 | "source": [ 10 | "# Chapter 14: Rebalancing and Tax Loss Harvesting" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 5, 16 | "metadata": { 17 | "id": "QTkVOzVRGExY" 18 | }, 19 | "outputs": [], 20 | "source": [ 21 | "# Learn how to calculate Portfolio Drift " 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 6, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import yfinance as yf\n", 31 | "import pandas as pd" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 7, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "class Allocation:\n", 41 | " def __init__(self, ticker, percentage):\n", 42 | " self.ticker = ticker\n", 43 | " self.percentage = percentage\n", 44 | " self.units = 0.0\n", 45 | "\n", 46 | "class Portfolio:\n", 47 | "\n", 48 | " def __init__(self, tickerString: str, expectedReturn: float, portfolioName: str, riskBucket: int):\n", 49 | "\n", 50 | " self.name = portfolioName\n", 51 | " self.riskBucket = riskBucket\n", 52 | " self.expectedReturn = expectedReturn\n", 53 | " self.allocations = []\n", 54 | "\n", 55 | " from pypfopt.efficient_frontier import EfficientFrontier\n", 56 | " from pypfopt import risk_models\n", 57 | " from pypfopt import expected_returns\n", 58 | "\n", 59 | " df = self.__getDailyPrices(tickerString, \"20y\")\n", 60 | "\n", 61 | " mu = expected_returns.mean_historical_return(df)\n", 62 | " S = risk_models.sample_cov(df)\n", 63 | "\n", 64 | " ef = EfficientFrontier(mu, S)\n", 65 | "\n", 66 | " ef.efficient_return(expectedReturn)\n", 67 | " self.expectedRisk = ef.portfolio_performance()[1]\n", 68 | " portfolioWeights = ef.clean_weights()\n", 69 | "\n", 70 | " for key, value in portfolioWeights.items():\n", 71 | " newAllocation = Allocation(key, value)\n", 72 | " self.allocations.append(newAllocation)\n", 73 | "\n", 74 | " def __getDailyPrices(self, tickerStringList, period):\n", 75 | " data = yf.download(tickerStringList, group_by=\"Ticker\", period=period)\n", 76 | " data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 77 | " data = data.dropna()\n", 78 | " data.columns = data.columns.droplevel(1)\n", 79 | " return data\n", 80 | "\n", 81 | " def printPortfolio(self):\n", 82 | " print(\"Portfolio Name: \" + self.name)\n", 83 | " print(\"Risk Bucket: \" + str(self.riskBucket))\n", 84 | " print(\"Expected Return: \" + str(self.expectedReturn))\n", 85 | " print(\"Expected Risk: \" + str(self.expectedRisk))\n", 86 | " print(\"Allocations: \")\n", 87 | " for allocation in self.allocations:\n", 88 | " print(\"Ticker: \" + allocation.ticker + \", Percentage: \" + str(allocation.percentage))\n", 89 | "\n", 90 | " @staticmethod\n", 91 | " def getPortfolioMapping(riskToleranceScore, riskCapacityScore):\n", 92 | " allocationLookupTable=pd.read_csv('./Data/Risk Mapping Lookup.csv')\n", 93 | " matchTol = (allocationLookupTable['Tolerance_min'] <= riskToleranceScore) & (allocationLookupTable['Tolerance_max'] >= riskToleranceScore)\n", 94 | " matchCap = (allocationLookupTable['Capacity_min'] <= riskCapacityScore) & (allocationLookupTable['Capacity_max'] >= riskCapacityScore)\n", 95 | " portfolioID = allocationLookupTable['Portfolio'][(matchTol & matchCap)]\n", 96 | " return portfolioID.values[0]\n", 97 | "\n", 98 | "class Goal:\n", 99 | " def __init__(self, name: str, targetYear: int, targetValue: float, portfolio: Portfolio=None, initialContribution: float=0, monthlyContribution: float=0, priority: str=\"\"):\n", 100 | " self.name = name\n", 101 | " self.targetYear = targetYear\n", 102 | " self.targetValue = targetValue\n", 103 | " self.initialContribution = initialContribution\n", 104 | " self.monthlyContribution = monthlyContribution\n", 105 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 106 | " raise ValueError(\"Wrong value set for Priority.\")\n", 107 | " self.priority = priority\n", 108 | " self.portfolio = portfolio\n", 109 | "\n", 110 | " def getGoalProbabilities(self):\n", 111 | " if (self.priority == \"\"):\n", 112 | " raise ValueError(\"No value set for Priority.\")\n", 113 | " lookupTable=pd.read_csv(\"./Data/Goal Probability Table.csv\")\n", 114 | " match = (lookupTable[\"Realize\"] == self.priority)\n", 115 | " minProb = lookupTable[\"MinP\"][(match)]\n", 116 | " maxProb = lookupTable[\"MaxP\"][(match)]\n", 117 | " return minProb.values[0], maxProb.values[0]\n", 118 | "\n", 119 | "class AccountType():\n", 120 | " def __init__(self, value: str):\n", 121 | " if not value in(\"Taxable\", \"Roth IRA\", \"Traditional IRA\"):\n", 122 | " raise ValueError(\"Allowed types: Taxable, Roth IRA, Traditional IRA\")\n", 123 | " self.value = value\n", 124 | " def __eq__(self, other):\n", 125 | " return self.value == other.value\n", 126 | "\n", 127 | "class AccountStatus():\n", 128 | " def __init__(self, value: str):\n", 129 | " if not value in(\"PENDING\", \"IN_REVIEW\", \"APPROVED\", \"REJECTED\", \"SUSPENDED\"):\n", 130 | " raise ValueError(\"Allowed statuses: PENDING, IN_REVIEW, APPROVED, REJECTED, SUSPENDED\")\n", 131 | " self.value = value\n", 132 | " def __eq__(self, other):\n", 133 | " return self.value == other.value\n", 134 | "\n", 135 | "class Account():\n", 136 | " def __init__(self, number: str, accountType: AccountType, accountStatus: AccountStatus, cashBalance: float=0.0):\n", 137 | " self.goals = []\n", 138 | " self.number = number\n", 139 | " self.cashBalance = cashBalance\n", 140 | " self.accountType = accountType\n", 141 | " self.accountStatus = accountStatus\n", 142 | "\n", 143 | "class TransactionType():\n", 144 | " def __init__(self, value: str):\n", 145 | " if not value in(\"BUY\", \"SELL\"):\n", 146 | " raise ValueError(\"Allowed types: BUY, SELL.\")\n", 147 | " self.value = value\n", 148 | " def __eq__(self, other):\n", 149 | " return self.value == other.value\n", 150 | "\n", 151 | "class OrderStatus():\n", 152 | " def __init__(self, value: str):\n", 153 | " if not value in(\"NEW\", \"PENDING\", \"FILLED\", \"REJECTED\"):\n", 154 | " raise ValueError(\"Allowed statuses: NEW, PENDING, FILLED, REJECTED.\")\n", 155 | " self.value = value\n", 156 | " def __eq__(self, other):\n", 157 | " return self.value == other.value\n", 158 | "\n", 159 | "class Order:\n", 160 | " def __init__(self, account: Account, goal: Goal, transactionType: TransactionType, status: OrderStatus=OrderStatus(\"NEW\"), dollarAmount: float=0.0):\n", 161 | " \n", 162 | " self.account = account\n", 163 | " self.transactionType = transactionType\n", 164 | " self.dollarAmount = dollarAmount\n", 165 | " self.goal = goal\n", 166 | " self.status = status\n", 167 | "\n", 168 | " def checkAccountStatus(self) -> bool:\n", 169 | " if self.account.accountStatus == AccountStatus(\"APPROVED\"):\n", 170 | " return True\n", 171 | " else:\n", 172 | " return False\n", 173 | "\n", 174 | " def checkOrderSize(self) -> bool:\n", 175 | " if self.dollarAmount > 1.00:\n", 176 | " return True\n", 177 | " else:\n", 178 | " return False\n", 179 | "\n", 180 | " def checkBalances(self) -> bool:\n", 181 | " if self.transactionType == TransactionType(\"BUY\") and self.account.cashBalance >= self.dollarAmount:\n", 182 | " return True\n", 183 | " elif self.transactionType == TransactionType(\"SELL\"):\n", 184 | " goalValue = 0.0\n", 185 | " for allocation in self.goal.portfolio.allocations:\n", 186 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 187 | " goalValue += allocation.units * price\n", 188 | " if self.dollarAmount <= goalValue:\n", 189 | " return True\n", 190 | " else:\n", 191 | " return False\n", 192 | " else:\n", 193 | " return False\n", 194 | "\n", 195 | " def checkOrderViability(self) -> bool:\n", 196 | " if self.checkAccountStatus() and self.checkOrderSize() and self.checkBalances() and isMarketOpen():\n", 197 | " return True\n", 198 | " else:\n", 199 | " return False\n", 200 | "\n", 201 | " def split(self) -> list:\n", 202 | " splits = []\n", 203 | " for allocation in self.goal.portfolio.allocations:\n", 204 | " if (allocation.percentage > 0):\n", 205 | " splits.append(SplitOrder(originalOrder=self, ticker=allocation.ticker, dollarAmount=allocation.percentage * self.dollarAmount))\n", 206 | " return splits" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 8, 212 | "metadata": {}, 213 | "outputs": [ 214 | { 215 | "name": "stdout", 216 | "output_type": "stream", 217 | "text": [ 218 | "[*********************100%***********************] 5 of 5 completed\n" 219 | ] 220 | } 221 | ], 222 | "source": [ 223 | "myPortfolio = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.05, portfolioName = \"Moderate\", riskBucket = 3)\n", 224 | "myGoal = Goal(name=\"Vacation\", targetYear=2027, targetValue=10000, priority=\"Dreams\", portfolio=myPortfolio)\n", 225 | "myAccount=Account(number=\"123456789\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=11.0)\n", 226 | "myAccount.goals.append(myGoal)\n", 227 | "\n", 228 | "# Manual update using Chapter 12 outputs:\n", 229 | "myPortfolio.allocations[0].units = 0.0\n", 230 | "myPortfolio.allocations[1].units = 0.02213974051911262\n", 231 | "myPortfolio.allocations[2].units = 0.02171626612838836\n", 232 | "myPortfolio.allocations[3].units = 0.0163863407640173\n", 233 | "myPortfolio.allocations[4].units = 0.009743440233" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 9, 239 | "metadata": {}, 240 | "outputs": [ 241 | { 242 | "name": "stdout", 243 | "output_type": "stream", 244 | "text": [ 245 | "[*********************100%***********************] 5 of 5 completed\n" 246 | ] 247 | } 248 | ], 249 | "source": [ 250 | "myPortfolio2 = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.03, portfolioName = \"Conservative\", riskBucket = 2)\n", 251 | "myGoal2 = Goal(name=\"Car\", targetYear=2025, targetValue=5000, priority=\"Dreams\", portfolio=myPortfolio2)\n", 252 | "myAccount2=Account(number=\"987654321\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=21.0)\n", 253 | "myAccount2.goals.append(myGoal2)\n", 254 | "\n", 255 | "# Manual update using Chapter 12 outputs:\n", 256 | "myPortfolio2.allocations[0].units = 0.019070282760887385\n", 257 | "myPortfolio2.allocations[1].units = 0.0\n", 258 | "myPortfolio2.allocations[2].units = 0.10911027337161164\n", 259 | "myPortfolio2.allocations[3].units = 0.019273081685982695\n", 260 | "myPortfolio2.allocations[4].units = 0.0" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 10, 266 | "metadata": { 267 | "colab": { 268 | "base_uri": "https://localhost:8080/" 269 | }, 270 | "id": "b1M77yHivqZc", 271 | "outputId": "9c820f88-27e1-4915-e94b-cef5631e2490" 272 | }, 273 | "outputs": [ 274 | { 275 | "data": { 276 | "text/plain": [ 277 | "[0.0, 0.15874, 0.66319, 0.0, 0.17806]" 278 | ] 279 | }, 280 | "execution_count": 10, 281 | "metadata": {}, 282 | "output_type": "execute_result" 283 | } 284 | ], 285 | "source": [ 286 | "allocations = [obj.percentage for obj in myPortfolio2.allocations]\n", 287 | "allocations" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": 11, 293 | "metadata": { 294 | "colab": { 295 | "base_uri": "https://localhost:8080/" 296 | }, 297 | "id": "NYAfItmwwxMj", 298 | "outputId": "ad8af41a-5779-4b4d-f58c-a3e2404a50a3" 299 | }, 300 | "outputs": [ 301 | { 302 | "data": { 303 | "text/plain": [ 304 | "[0.019070282760887385, 0.0, 0.10911027337161164, 0.019273081685982695, 0.0]" 305 | ] 306 | }, 307 | "execution_count": 11, 308 | "metadata": {}, 309 | "output_type": "execute_result" 310 | } 311 | ], 312 | "source": [ 313 | "holdings = [obj.units for obj in myPortfolio2.allocations]\n", 314 | "holdings" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": 13, 320 | "metadata": { 321 | "colab": { 322 | "base_uri": "https://localhost:8080/" 323 | }, 324 | "id": "3KlTViC6xHPI", 325 | "outputId": "c6560ff2-48d6-46d4-a47a-e25c46f50160" 326 | }, 327 | "outputs": [ 328 | { 329 | "data": { 330 | "text/plain": [ 331 | "[180.27999877929688,\n", 332 | " 117.37000274658203,\n", 333 | " 200.9499969482422,\n", 334 | " 25.010000228881836,\n", 335 | " 107.22000122070312]" 336 | ] 337 | }, 338 | "execution_count": 13, 339 | "metadata": {}, 340 | "output_type": "execute_result" 341 | } 342 | ], 343 | "source": [ 344 | "# Get Portfolio data\n", 345 | "market_values = []\n", 346 | "for allocation in myPortfolio.allocations:\n", 347 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 348 | " market_values.append(price)\n", 349 | "market_values" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 15, 355 | "metadata": { 356 | "id": "SLPq9fDM0Fit" 357 | }, 358 | "outputs": [], 359 | "source": [ 360 | "# Define the model portfolio allocations\n", 361 | "#allocations = [0.3, 0.2, 0.4, 0.1]\n", 362 | "allocations = [obj.percentage for obj in myPortfolio.allocations]\n", 363 | "\n", 364 | "# Define the current holdings and market values of the assets in the portfolio\n", 365 | "#holdings = [100, 200, 150, 50]\n", 366 | "holdings = [obj.units for obj in myPortfolio.allocations]\n", 367 | "\n", 368 | "#market_values = [10, 20, 30, 40]\n", 369 | "market_values = []\n", 370 | "for allocation in myPortfolio.allocations:\n", 371 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 372 | " market_values.append(price)" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 16, 378 | "metadata": { 379 | "colab": { 380 | "base_uri": "https://localhost:8080/" 381 | }, 382 | "id": "Vqvy1O39GWdH", 383 | "outputId": "e9b6f68c-3f38-4171-ec27-164c6a05c0ea" 384 | }, 385 | "outputs": [ 386 | { 387 | "name": "stdout", 388 | "output_type": "stream", 389 | "text": [ 390 | "[0.0, 0.3087276005622959, 0.5184644408071606, 0.048690192773816796, 0.12411776585672678]\n", 391 | "Portfolio Drift: 36.86%\n" 392 | ] 393 | } 394 | ], 395 | "source": [ 396 | "# Calculate the current allocation of assets in the portfolio\n", 397 | "import numpy as np\n", 398 | "current_allocation = []\n", 399 | "for i in range(len(holdings)):\n", 400 | " current_allocation.append(holdings[i] * market_values[i])\n", 401 | "current_allocation = [x / sum(current_allocation) for x in current_allocation]\n", 402 | "print(current_allocation)\n", 403 | "\n", 404 | "# Determine the difference between the model portfolio allocations and the current allocation of assets\n", 405 | "diff = [x1 - x2 for (x1, x2) in zip(allocations, current_allocation)]\n", 406 | "\n", 407 | "print(\"Portfolio Drift: \" + '{0:.2f}'.format((np.abs(diff).sum()/2)*100) + \"%\")" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 45, 413 | "metadata": { 414 | "id": "KFm7qJd68SnY" 415 | }, 416 | "outputs": [], 417 | "source": [ 418 | "class Portfolio:\n", 419 | "\n", 420 | " def __init__(self, tickerString: str, expectedReturn: float, portfolioName: str, riskBucket: int):\n", 421 | "\n", 422 | " self.name = portfolioName\n", 423 | " self.riskBucket = riskBucket\n", 424 | " self.expectedReturn = expectedReturn\n", 425 | " self.allocations = []\n", 426 | " self.needRebalancing = False\n", 427 | "\n", 428 | " from pypfopt.efficient_frontier import EfficientFrontier\n", 429 | " from pypfopt import risk_models\n", 430 | " from pypfopt import expected_returns\n", 431 | "\n", 432 | " df = self.__getDailyPrices(tickerString, \"20y\")\n", 433 | "\n", 434 | " mu = expected_returns.mean_historical_return(df)\n", 435 | " S = risk_models.sample_cov(df)\n", 436 | "\n", 437 | " ef = EfficientFrontier(mu, S)\n", 438 | "\n", 439 | " ef.efficient_return(expectedReturn)\n", 440 | " self.expectedRisk = ef.portfolio_performance()[1]\n", 441 | " portfolioWeights = ef.clean_weights()\n", 442 | "\n", 443 | " for key, value in portfolioWeights.items():\n", 444 | " newAllocation = Allocation(key, value)\n", 445 | " self.allocations.append(newAllocation)\n", 446 | "\n", 447 | " def __getDailyPrices(self, tickerStringList, period):\n", 448 | " data = yf.download(tickerStringList, group_by=\"Ticker\", period=period)\n", 449 | " data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 450 | " data = data.dropna()\n", 451 | " data.columns = data.columns.droplevel(1)\n", 452 | " return data\n", 453 | "\n", 454 | " def printPortfolio(self):\n", 455 | " print(\"Portfolio Name: \" + self.name)\n", 456 | " print(\"Risk Bucket: \" + str(self.riskBucket))\n", 457 | " print(\"Expected Return: \" + str(self.expectedReturn))\n", 458 | " print(\"Expected Risk: \" + str(self.expectedRisk))\n", 459 | " print(\"Allocations: \")\n", 460 | " for allocation in self.allocations:\n", 461 | " print(\"Ticker: \" + allocation.ticker + \", Percentage: \" + str(allocation.percentage))\n", 462 | "\n", 463 | " @staticmethod\n", 464 | " def getPortfolioMapping(riskToleranceScore, riskCapacityScore):\n", 465 | " allocationLookupTable=pd.read_csv('./Data/Risk Mapping Lookup.csv')\n", 466 | " matchTol = (allocationLookupTable['Tolerance_min'] <= riskToleranceScore) & (allocationLookupTable['Tolerance_max'] >= riskToleranceScore)\n", 467 | " matchCap = (allocationLookupTable['Capacity_min'] <= riskCapacityScore) & (allocationLookupTable['Capacity_max'] >= riskCapacityScore)\n", 468 | " portfolioID = allocationLookupTable['Portfolio'][(matchTol & matchCap)]\n", 469 | " return portfolioID.values[0]\n", 470 | "\n", 471 | " def calculateDiffsToModel(self) -> list:\n", 472 | " allocations = [obj.percentage for obj in self.allocations]\n", 473 | " holdings = [obj.units for obj in self.allocations]\n", 474 | " if sum(holdings) == 0.0:\n", 475 | " return []\n", 476 | " market_values = []\n", 477 | " for allocation in self.allocations:\n", 478 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 479 | " market_values.append(price)\n", 480 | " \n", 481 | " current_allocation = []\n", 482 | " for i in range(len(holdings)):\n", 483 | " current_allocation.append(holdings[i] * market_values[i])\n", 484 | " current_allocation = [x / sum(current_allocation) for x in current_allocation]\n", 485 | " \n", 486 | " diff = [x1 - x2 for (x1, x2) in zip(allocations, current_allocation)]\n", 487 | " return diff\n", 488 | "\n", 489 | " def checkNeedRebalancing(self, thres: float, diff: list=[]):\n", 490 | " if diff == []:\n", 491 | " diff = self.calculateDiffsToModel()\n", 492 | " drift = self.calculateDrift(diff)\n", 493 | " \n", 494 | " if drift >= thres:\n", 495 | " self.needRebalancing = True\n", 496 | " else:\n", 497 | " self.needRebalancing = False\n", 498 | "\n", 499 | " def calculateDrift(self, diff: list=[]) -> float:\n", 500 | " if diff == []:\n", 501 | " diff = self.calculateDiffsToModel()\n", 502 | " return(np.abs(diff).sum()/2)\n", 503 | "\n", 504 | " def rebalance(self, diff: list=[]) -> list:\n", 505 | " if diff == []:\n", 506 | " diff = self.calculateDiffsToModel()\n", 507 | "\n", 508 | " if not self.needRebalancing:\n", 509 | " return []\n", 510 | "\n", 511 | " splitOrders = []\n", 512 | " for i in range(len(diff)):\n", 513 | " if diff[i] > 0:\n", 514 | " diffValue = diff[i] * holdings[i] * market_values[i]\n", 515 | " newOrder = Order(account = myAccount, \n", 516 | " goal = myGoal, \n", 517 | " transactionType = TransactionType('BUY'), \n", 518 | " dollarAmount = diffValue)\n", 519 | " splitOrders.append(SplitOrder(originalOrder = newOrder,\n", 520 | " ticker = myPortfolio.allocations[i].ticker, \n", 521 | " dollarAmount = diffValue))\n", 522 | " elif diff[i] < 0:\n", 523 | " diffValue = abs(diff[i]) * holdings[i] * market_values[i]\n", 524 | " newOrder = Order(account = myAccount, \n", 525 | " goal = myGoal, \n", 526 | " transactionType = TransactionType('SELL'), \n", 527 | " dollarAmount = diffValue)\n", 528 | " splitOrders.append(SplitOrder(originalOrder = newOrder,\n", 529 | " ticker = myPortfolio.allocations[i].ticker, \n", 530 | " dollarAmount = diffValue))\n", 531 | " return splitOrders\n", 532 | "\n", 533 | "class SplitOrder:\n", 534 | " def __init__(self, originalOrder: Order, ticker: str, dollarAmount: float):\n", 535 | " \n", 536 | " self.originalOrder = originalOrder\n", 537 | " self.ticker = ticker\n", 538 | " self.dollarAmount = dollarAmount\n", 539 | " self.units = 0\n", 540 | " " 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": 56, 546 | "metadata": {}, 547 | "outputs": [ 548 | { 549 | "name": "stdout", 550 | "output_type": "stream", 551 | "text": [ 552 | "[*********************100%***********************] 5 of 5 completed\n" 553 | ] 554 | } 555 | ], 556 | "source": [ 557 | "myPortfolio = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.05, portfolioName = \"Moderate\", riskBucket = 3)\n", 558 | "myGoal = Goal(name=\"Vacation\", targetYear=2027, targetValue=10000, priority=\"Dreams\", portfolio=myPortfolio)\n", 559 | "myAccount=Account(number=\"123456789\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=11.0)\n", 560 | "myAccount.goals.append(myGoal)\n", 561 | "\n", 562 | "# Manual update using Chapter 12 outputs:\n", 563 | "myPortfolio.allocations[0].units = 0.0\n", 564 | "myPortfolio.allocations[1].units = 0.02213974051911262\n", 565 | "myPortfolio.allocations[2].units = 0.02171626612838836\n", 566 | "myPortfolio.allocations[3].units = 0.0163863407640173\n", 567 | "myPortfolio.allocations[4].units = 0.009743440233" 568 | ] 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": 57, 573 | "metadata": { 574 | "id": "2LmsIBDI81kQ" 575 | }, 576 | "outputs": [ 577 | { 578 | "name": "stdout", 579 | "output_type": "stream", 580 | "text": [ 581 | "[*********************100%***********************] 5 of 5 completed\n" 582 | ] 583 | } 584 | ], 585 | "source": [ 586 | "myPortfolio2 = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.03, portfolioName = \"Conservative\", riskBucket = 2)\n", 587 | "myGoal2 = Goal(name=\"Car\", targetYear=2025, targetValue=5000, priority=\"Dreams\", portfolio=myPortfolio2)\n", 588 | "myAccount2=Account(number=\"987654321\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=21.0)\n", 589 | "myAccount2.goals.append(myGoal2)\n", 590 | "\n", 591 | "# Manual update using Chapter 12 outputs:\n", 592 | "myPortfolio2.allocations[0].units = 0.019070282760887385\n", 593 | "myPortfolio2.allocations[1].units = 0.0\n", 594 | "myPortfolio2.allocations[2].units = 0.10911027337161164\n", 595 | "myPortfolio2.allocations[3].units = 0.019273081685982695\n", 596 | "myPortfolio2.allocations[4].units = 0.0" 597 | ] 598 | }, 599 | { 600 | "cell_type": "code", 601 | "execution_count": 58, 602 | "metadata": { 603 | "colab": { 604 | "base_uri": "https://localhost:8080/" 605 | }, 606 | "id": "F9v0PKiQ8_Xe", 607 | "outputId": "822b8014-6861-4798-8bd7-fe8709c18af8" 608 | }, 609 | "outputs": [ 610 | { 611 | "data": { 612 | "text/plain": [ 613 | "0.03138460411794584" 614 | ] 615 | }, 616 | "execution_count": 58, 617 | "metadata": {}, 618 | "output_type": "execute_result" 619 | } 620 | ], 621 | "source": [ 622 | "myPortfolio.calculateDrift()" 623 | ] 624 | }, 625 | { 626 | "cell_type": "code", 627 | "execution_count": 59, 628 | "metadata": {}, 629 | "outputs": [ 630 | { 631 | "data": { 632 | "text/plain": [ 633 | "[0.0,\n", 634 | " 0.0016507772916259378,\n", 635 | " 0.029672622624120265,\n", 636 | " 6.620420219966006e-05,\n", 637 | " -0.03137960411794581]" 638 | ] 639 | }, 640 | "execution_count": 59, 641 | "metadata": {}, 642 | "output_type": "execute_result" 643 | } 644 | ], 645 | "source": [ 646 | "myPortfolio.calculateDiffsToModel()" 647 | ] 648 | }, 649 | { 650 | "cell_type": "code", 651 | "execution_count": 60, 652 | "metadata": { 653 | "colab": { 654 | "base_uri": "https://localhost:8080/" 655 | }, 656 | "id": "Jngp7ltlGvvt", 657 | "outputId": "af719375-2a08-4d3b-ac57-4c7ddae26d3b" 658 | }, 659 | "outputs": [ 660 | { 661 | "data": { 662 | "text/plain": [ 663 | "[0.36858,\n", 664 | " -0.04473760056229592,\n", 665 | " -0.2156844408071606,\n", 666 | " -0.048690192773816796,\n", 667 | " -0.05945776585672678]" 668 | ] 669 | }, 670 | "execution_count": 60, 671 | "metadata": {}, 672 | "output_type": "execute_result" 673 | } 674 | ], 675 | "source": [ 676 | "diff" 677 | ] 678 | }, 679 | { 680 | "cell_type": "code", 681 | "execution_count": 61, 682 | "metadata": { 683 | "id": "jdkygG6RGMGR" 684 | }, 685 | "outputs": [], 686 | "source": [ 687 | "# Learn how to implement Time-based Rebalancing " 688 | ] 689 | }, 690 | { 691 | "cell_type": "code", 692 | "execution_count": 62, 693 | "metadata": { 694 | "id": "WMjORD832XOx" 695 | }, 696 | "outputs": [], 697 | "source": [ 698 | "splitOrders = []\n", 699 | "for i in range(len(diff)):\n", 700 | " if diff[i] > 0:\n", 701 | " diffValue = diff[i] * holdings[i] * market_values[i]\n", 702 | " newOrder = Order(account = myAccount, \n", 703 | " goal = myGoal, \n", 704 | " transactionType = TransactionType('BUY'), \n", 705 | " dollarAmount = diffValue)\n", 706 | " splitOrders.append(SplitOrder(originalOrder = newOrder,\n", 707 | " ticker = myPortfolio.allocations[i].ticker, \n", 708 | " dollarAmount = diffValue))\n", 709 | " elif diff[i] < 0:\n", 710 | " diffValue = abs(diff[i]) * holdings[i] * market_values[i]\n", 711 | " newOrder = Order(account = myAccount, \n", 712 | " goal = myGoal, \n", 713 | " transactionType = TransactionType('SELL'), \n", 714 | " dollarAmount = diffValue)\n", 715 | " splitOrders.append(SplitOrder(originalOrder = newOrder,\n", 716 | " ticker = myPortfolio.allocations[i].ticker, \n", 717 | " dollarAmount = diffValue))" 718 | ] 719 | }, 720 | { 721 | "cell_type": "code", 722 | "execution_count": 63, 723 | "metadata": { 724 | "id": "lteF2870yl9v" 725 | }, 726 | "outputs": [], 727 | "source": [ 728 | "splits = myPortfolio.rebalance()" 729 | ] 730 | }, 731 | { 732 | "cell_type": "code", 733 | "execution_count": 64, 734 | "metadata": { 735 | "id": "Cm0eenHyE07Q" 736 | }, 737 | "outputs": [], 738 | "source": [ 739 | "for split in splits:\n", 740 | " print(split.ticker + \": \" + split.originalOrder.transactionType.value + \" \" + '${0:.2f}'.format(split.dollarAmount))" 741 | ] 742 | }, 743 | { 744 | "cell_type": "code", 745 | "execution_count": 65, 746 | "metadata": { 747 | "id": "tZQxxsuWGTXH" 748 | }, 749 | "outputs": [], 750 | "source": [ 751 | "# Learn how to implement Threshold-based Rebalancing " 752 | ] 753 | }, 754 | { 755 | "cell_type": "code", 756 | "execution_count": 66, 757 | "metadata": { 758 | "id": "ZoB6Kop6KpgC" 759 | }, 760 | "outputs": [], 761 | "source": [ 762 | "threshold = 0.003\n", 763 | "accounts = [myAccount, myAccount2]\n", 764 | "for account in accounts:\n", 765 | " for goal in account.goals:\n", 766 | " diffs = goal.portfolio.calculateDiffsToModel()\n", 767 | " goal.portfolio.checkNeedRebalancing(diff=diffs, thres=threshold)" 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": 67, 773 | "metadata": { 774 | "colab": { 775 | "base_uri": "https://localhost:8080/" 776 | }, 777 | "id": "IdCt21qVNVSw", 778 | "outputId": "b5af9abc-0bc3-4b0b-bb53-f1374d8f3dbb" 779 | }, 780 | "outputs": [ 781 | { 782 | "name": "stdout", 783 | "output_type": "stream", 784 | "text": [ 785 | "True\n", 786 | "0.03138460411794584\n", 787 | "True\n", 788 | "0.15874500000000002\n" 789 | ] 790 | } 791 | ], 792 | "source": [ 793 | "print(myAccount.goals[0].portfolio.needRebalancing)\n", 794 | "print(myAccount.goals[0].portfolio.calculateDrift())\n", 795 | "\n", 796 | "print(myAccount2.goals[0].portfolio.needRebalancing)\n", 797 | "print(myAccount2.goals[0].portfolio.calculateDrift())" 798 | ] 799 | } 800 | ], 801 | "metadata": { 802 | "colab": { 803 | "provenance": [], 804 | "toc_visible": true 805 | }, 806 | "kernelspec": { 807 | "display_name": "Python 3", 808 | "language": "python", 809 | "name": "python3" 810 | }, 811 | "language_info": { 812 | "codemirror_mode": { 813 | "name": "ipython", 814 | "version": 3 815 | }, 816 | "file_extension": ".py", 817 | "mimetype": "text/x-python", 818 | "name": "python", 819 | "nbconvert_exporter": "python", 820 | "pygments_lexer": "ipython3", 821 | "version": "3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]" 822 | }, 823 | "vscode": { 824 | "interpreter": { 825 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 826 | } 827 | } 828 | }, 829 | "nbformat": 4, 830 | "nbformat_minor": 0 831 | } 832 | -------------------------------------------------------------------------------- /Chapter15.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "NClsv6anAqTF" 7 | }, 8 | "source": [ 9 | "# Chapter 15: Dividends and Fee Management" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 4, 15 | "metadata": { 16 | "colab": { 17 | "base_uri": "https://localhost:8080/" 18 | }, 19 | "id": "G4nK9Z1rDMET", 20 | "outputId": "6b142e7c-4116-4915-d8e2-6eaecbf84520" 21 | }, 22 | "outputs": [ 23 | { 24 | "name": "stdout", 25 | "output_type": "stream", 26 | "text": [ 27 | " Account Symbol Amount Units\n", 28 | "0 123456789 TLT 0.000000 0.000487\n", 29 | "1 123456789 IEI 0.000000 0.000360\n", 30 | "2 123456789 VTI 0.169971 0.000000\n", 31 | "3 987654321 IEI 0.000000 0.002257\n", 32 | "4 987654321 VTI 0.193737 0.000000\n" 33 | ] 34 | } 35 | ], 36 | "source": [ 37 | "# Get dividend from custodian as CSV\n", 38 | "import yfinance as yf\n", 39 | "import pandas as pd\n", 40 | "\n", 41 | "newDividends = pd.read_csv('./Data/Dividends.csv')\n", 42 | "print(newDividends)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 5, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "class Allocation:\n", 52 | " def __init__(self, ticker, percentage):\n", 53 | " self.ticker = ticker\n", 54 | " self.percentage = percentage\n", 55 | " self.units = 0.0\n", 56 | "\n", 57 | "class Portfolio:\n", 58 | "\n", 59 | " def __init__(self, tickerString: str, expectedReturn: float, portfolioName: str, riskBucket: int):\n", 60 | "\n", 61 | " self.name = portfolioName\n", 62 | " self.riskBucket = riskBucket\n", 63 | " self.expectedReturn = expectedReturn\n", 64 | " self.allocations = []\n", 65 | "\n", 66 | " from pypfopt.efficient_frontier import EfficientFrontier\n", 67 | " from pypfopt import risk_models\n", 68 | " from pypfopt import expected_returns\n", 69 | "\n", 70 | " df = self.__getDailyPrices(tickerString, \"20y\")\n", 71 | "\n", 72 | " mu = expected_returns.mean_historical_return(df)\n", 73 | " S = risk_models.sample_cov(df)\n", 74 | "\n", 75 | " ef = EfficientFrontier(mu, S)\n", 76 | "\n", 77 | " ef.efficient_return(expectedReturn)\n", 78 | " self.expectedRisk = ef.portfolio_performance()[1]\n", 79 | " portfolioWeights = ef.clean_weights()\n", 80 | "\n", 81 | " for key, value in portfolioWeights.items():\n", 82 | " newAllocation = Allocation(key, value)\n", 83 | " self.allocations.append(newAllocation)\n", 84 | "\n", 85 | " def __getDailyPrices(self, tickerStringList, period):\n", 86 | " data = yf.download(tickerStringList, group_by=\"Ticker\", period=period)\n", 87 | " data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 88 | " data = data.dropna()\n", 89 | " data.columns = data.columns.droplevel(1)\n", 90 | " return data\n", 91 | "\n", 92 | " def printPortfolio(self):\n", 93 | " print(\"Portfolio Name: \" + self.name)\n", 94 | " print(\"Risk Bucket: \" + str(self.riskBucket))\n", 95 | " print(\"Expected Return: \" + str(self.expectedReturn))\n", 96 | " print(\"Expected Risk: \" + str(self.expectedRisk))\n", 97 | " print(\"Allocations: \")\n", 98 | " for allocation in self.allocations:\n", 99 | " print(\"Ticker: \" + allocation.ticker + \", Percentage: \" + str(allocation.percentage))\n", 100 | "\n", 101 | " @staticmethod\n", 102 | " def getPortfolioMapping(riskToleranceScore, riskCapacityScore):\n", 103 | " allocationLookupTable=pd.read_csv('./Data/Risk Mapping Lookup.csv')\n", 104 | " matchTol = (allocationLookupTable['Tolerance_min'] <= riskToleranceScore) & (allocationLookupTable['Tolerance_max'] >= riskToleranceScore)\n", 105 | " matchCap = (allocationLookupTable['Capacity_min'] <= riskCapacityScore) & (allocationLookupTable['Capacity_max'] >= riskCapacityScore)\n", 106 | " portfolioID = allocationLookupTable['Portfolio'][(matchTol & matchCap)]\n", 107 | " return portfolioID.values[0]\n", 108 | "\n", 109 | "class Goal:\n", 110 | " def __init__(self, name: str, targetYear: int, targetValue: float, portfolio: Portfolio=None, initialContribution: float=0, monthlyContribution: float=0, priority: str=\"\"):\n", 111 | " self.name = name\n", 112 | " self.targetYear = targetYear\n", 113 | " self.targetValue = targetValue\n", 114 | " self.initialContribution = initialContribution\n", 115 | " self.monthlyContribution = monthlyContribution\n", 116 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 117 | " raise ValueError(\"Wrong value set for Priority.\")\n", 118 | " self.priority = priority\n", 119 | " self.portfolio = portfolio\n", 120 | "\n", 121 | " def getGoalProbabilities(self):\n", 122 | " if (self.priority == \"\"):\n", 123 | " raise ValueError(\"No value set for Priority.\")\n", 124 | " lookupTable=pd.read_csv(\"./Data/Goal Probability Table.csv\")\n", 125 | " match = (lookupTable[\"Realize\"] == self.priority)\n", 126 | " minProb = lookupTable[\"MinP\"][(match)]\n", 127 | " maxProb = lookupTable[\"MaxP\"][(match)]\n", 128 | " return minProb.values[0], maxProb.values[0]\n", 129 | "\n", 130 | "class AccountType():\n", 131 | " def __init__(self, value: str):\n", 132 | " if not value in(\"Taxable\", \"Roth IRA\", \"Traditional IRA\"):\n", 133 | " raise ValueError(\"Allowed types: Taxable, Roth IRA, Traditional IRA\")\n", 134 | " self.value = value\n", 135 | " def __eq__(self, other):\n", 136 | " return self.value == other.value\n", 137 | "\n", 138 | "class AccountStatus():\n", 139 | " def __init__(self, value: str):\n", 140 | " if not value in(\"PENDING\", \"IN_REVIEW\", \"APPROVED\", \"REJECTED\", \"SUSPENDED\"):\n", 141 | " raise ValueError(\"Allowed statuses: PENDING, IN_REVIEW, APPROVED, REJECTED, SUSPENDED\")\n", 142 | " self.value = value\n", 143 | " def __eq__(self, other):\n", 144 | " return self.value == other.value\n", 145 | "\n", 146 | "class Account():\n", 147 | " def __init__(self, number: str, accountType: AccountType, accountStatus: AccountStatus, cashBalance: float=0.0):\n", 148 | " self.goals = []\n", 149 | " self.number = number\n", 150 | " self.cashBalance = cashBalance\n", 151 | " self.accountType = accountType\n", 152 | " self.accountStatus = accountStatus" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 12, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "name": "stdout", 162 | "output_type": "stream", 163 | "text": [ 164 | "[*********************100%***********************] 5 of 5 completed\n" 165 | ] 166 | } 167 | ], 168 | "source": [ 169 | "myPortfolio = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.05, portfolioName = \"Moderate\", riskBucket = 3)\n", 170 | "myGoal = Goal(name=\"Vacation\", targetYear=2027, targetValue=10000, priority=\"Dreams\", portfolio=myPortfolio)\n", 171 | "myAccount=Account(number=\"123456789\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=11.0)\n", 172 | "myAccount.goals.append(myGoal)\n", 173 | "\n", 174 | "# Manual update using Chapter 12 outputs:\n", 175 | "myPortfolio.allocations[0].units = 0.0\n", 176 | "myPortfolio.allocations[1].units = 0.02213974051911262\n", 177 | "myPortfolio.allocations[2].units = 0.02171626612838836\n", 178 | "myPortfolio.allocations[3].units = 0.0163863407640173\n", 179 | "myPortfolio.allocations[4].units = 0.009743440233" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 13, 185 | "metadata": {}, 186 | "outputs": [ 187 | { 188 | "name": "stdout", 189 | "output_type": "stream", 190 | "text": [ 191 | "[*********************100%***********************] 5 of 5 completed\n" 192 | ] 193 | } 194 | ], 195 | "source": [ 196 | "myPortfolio2 = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.03, portfolioName = \"Conservative\", riskBucket = 2)\n", 197 | "myGoal2 = Goal(name=\"Car\", targetYear=2025, targetValue=5000, priority=\"Dreams\", portfolio=myPortfolio2)\n", 198 | "myAccount2=Account(number=\"987654321\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=21.0)\n", 199 | "myAccount2.goals.append(myGoal2)\n", 200 | "\n", 201 | "# Manual update using Chapter 12 outputs:\n", 202 | "myPortfolio2.allocations[0].units = 0.019070282760887385\n", 203 | "myPortfolio2.allocations[1].units = 0.0\n", 204 | "myPortfolio2.allocations[2].units = 0.10911027337161164\n", 205 | "myPortfolio2.allocations[3].units = 0.019273081685982695\n", 206 | "myPortfolio2.allocations[4].units = 0.0" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 14, 212 | "metadata": { 213 | "id": "Q8p9vWaLqFvD" 214 | }, 215 | "outputs": [], 216 | "source": [ 217 | "accounts = [myAccount, myAccount2]" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 15, 223 | "metadata": { 224 | "colab": { 225 | "base_uri": "https://localhost:8080/" 226 | }, 227 | "id": "ARjzjyIbqvVN", 228 | "outputId": "41b306e2-59ae-480b-efd9-b4787e2e2cbb" 229 | }, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "Account: 123456789\n", 236 | "Cash: 11.0\n", 237 | "Portfolio: Moderate\n", 238 | "GLD, units: 0.0\n", 239 | "DBC, units: 0.02213974051911262\n", 240 | "TLT, units: 0.02171626612838836\n", 241 | "IEI, units: 0.0163863407640173\n", 242 | "VTI, units: 0.009743440233\n", 243 | "\n", 244 | "\n", 245 | "Account: 987654321\n", 246 | "Cash: 21.0\n", 247 | "Portfolio: Conservative\n", 248 | "TLT, units: 0.019070282760887385\n", 249 | "DBC, units: 0.0\n", 250 | "GLD, units: 0.10911027337161164\n", 251 | "IEI, units: 0.019273081685982695\n", 252 | "VTI, units: 0.0\n", 253 | "\n", 254 | "\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "for account in accounts:\n", 260 | " print(\"Account: \" + str(account.number))\n", 261 | " print(\"Cash: \" + str(account.cashBalance))\n", 262 | " for goal in account.goals:\n", 263 | " print(\"Portfolio: \" + goal.portfolio.name)\n", 264 | " for allocation in goal.portfolio.allocations:\n", 265 | " print(allocation.ticker + \", units: \" + str(allocation.units))\n", 266 | " print(\"\\n\")" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 16, 272 | "metadata": { 273 | "colab": { 274 | "base_uri": "https://localhost:8080/" 275 | }, 276 | "id": "4VCuFlDYu_G6", 277 | "outputId": "6487b9fb-e957-46e6-f086-3c460aaddb1f" 278 | }, 279 | "outputs": [ 280 | { 281 | "data": { 282 | "text/plain": [ 283 | "[<__main__.Account at 0x16a132bf0>, <__main__.Account at 0x16a2fd450>]" 284 | ] 285 | }, 286 | "execution_count": 16, 287 | "metadata": {}, 288 | "output_type": "execute_result" 289 | } 290 | ], 291 | "source": [ 292 | "accounts" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 17, 298 | "metadata": { 299 | "id": "Evdru55MUDnP" 300 | }, 301 | "outputs": [], 302 | "source": [ 303 | "# Split/allocate back to account.goal(s).portfolio\n", 304 | "for index, row in newDividends.iterrows():\n", 305 | " accountNo = row['Account']\n", 306 | " \n", 307 | " account = next((account for account in accounts if str(account.number) == str(accountNo)), None)\n", 308 | "\n", 309 | " if account == None:\n", 310 | " print(\"No account\")\n", 311 | " break\n", 312 | " \n", 313 | " if row['Amount'] > 0:\n", 314 | " account.cashBalance += row['Amount']\n", 315 | " elif row['Units'] > 0:\n", 316 | " unitsToAllocate = row['Units']\n", 317 | " unitsAcrossPortfolios = 0.0\n", 318 | "\n", 319 | " for goal in account.goals:\n", 320 | " for allocation in goal.portfolio.allocations:\n", 321 | " if allocation.ticker == row['Symbol']:\n", 322 | " unitsAcrossPortfolios += allocation.units\n", 323 | "\n", 324 | " for goal in account.goals:\n", 325 | " for allocation in goal.portfolio.allocations:\n", 326 | " if allocation.ticker == row['Symbol']:\n", 327 | " allocation.units += unitsToAllocate * (allocation.units / unitsAcrossPortfolios) " 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 18, 333 | "metadata": { 334 | "colab": { 335 | "base_uri": "https://localhost:8080/" 336 | }, 337 | "id": "0f7dpn64vR3N", 338 | "outputId": "38d6c331-3bb8-4021-cd20-fc4245a6e080" 339 | }, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "Account: 123456789\n", 346 | "Cash: 11.1699714236\n", 347 | "Portfolio: Moderate\n", 348 | "GLD, units: 0.0\n", 349 | "DBC, units: 0.02213974051911262\n", 350 | "TLT, units: 0.02220343814008836\n", 351 | "IEI, units: 0.0167458724207173\n", 352 | "VTI, units: 0.009743440233\n", 353 | "\n", 354 | "\n", 355 | "Account: 987654321\n", 356 | "Cash: 21.1937368557\n", 357 | "Portfolio: Conservative\n", 358 | "TLT, units: 0.019070282760887385\n", 359 | "DBC, units: 0.0\n", 360 | "GLD, units: 0.10911027337161164\n", 361 | "IEI, units: 0.021530080818982694\n", 362 | "VTI, units: 0.0\n", 363 | "\n", 364 | "\n" 365 | ] 366 | } 367 | ], 368 | "source": [ 369 | "for account in accounts:\n", 370 | " print(\"Account: \" + str(account.number))\n", 371 | " print(\"Cash: \" + str(account.cashBalance))\n", 372 | " for goal in account.goals:\n", 373 | " print(\"Portfolio: \" + goal.portfolio.name)\n", 374 | " for allocation in goal.portfolio.allocations:\n", 375 | " print(allocation.ticker + \", units: \" + str(allocation.units))\n", 376 | " print(\"\\n\")" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": 19, 382 | "metadata": { 383 | "id": "o3CyuKa17A_D" 384 | }, 385 | "outputs": [], 386 | "source": [ 387 | "# Create a function, standalone or account class\n", 388 | "import pandas as pd\n", 389 | "def allocateDividends(dividendFile: pd.DataFrame, accounts: list):\n", 390 | "\n", 391 | " for index, row in dividendFile.iterrows():\n", 392 | " accountNo = row['Account']\n", 393 | " \n", 394 | " account = next((account for account in accounts if str(account.number) == str(accountNo)), None)\n", 395 | "\n", 396 | " if account == None:\n", 397 | " print(\"No account\")\n", 398 | " break\n", 399 | " \n", 400 | " if row['Amount'] > 0:\n", 401 | " account.cashBalance += row['Amount']\n", 402 | " elif row['Units'] > 0:\n", 403 | " unitsToAllocate = row['Units']\n", 404 | " unitsAcrossPortfolios = 0.0\n", 405 | "\n", 406 | " for goal in account.goals:\n", 407 | " for allocation in goal.portfolio.allocations:\n", 408 | " if allocation.ticker == row['Symbol']:\n", 409 | " unitsAcrossPortfolios += allocation.units\n", 410 | "\n", 411 | " for goal in account.goals:\n", 412 | " for allocation in goal.portfolio.allocations:\n", 413 | " if allocation.ticker == row['Symbol']:\n", 414 | " allocation.units += unitsToAllocate * (allocation.units / unitsAcrossPortfolios) \n" 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": 20, 420 | "metadata": { 421 | "colab": { 422 | "base_uri": "https://localhost:8080/" 423 | }, 424 | "id": "hODUICWJAxyI", 425 | "outputId": "f1006d73-bb6d-40ff-c3f8-d9734a1f1511" 426 | }, 427 | "outputs": [ 428 | { 429 | "data": { 430 | "text/plain": [ 431 | "Date\n", 432 | "1993-03-19 00:00:00-05:00 0.213\n", 433 | "1993-06-18 00:00:00-04:00 0.318\n", 434 | "1993-09-17 00:00:00-04:00 0.286\n", 435 | "1993-12-17 00:00:00-05:00 0.317\n", 436 | "1994-03-18 00:00:00-05:00 0.271\n", 437 | " ... \n", 438 | "2021-12-17 00:00:00-05:00 1.633\n", 439 | "2022-03-18 00:00:00-04:00 1.366\n", 440 | "2022-06-17 00:00:00-04:00 1.577\n", 441 | "2022-09-16 00:00:00-04:00 1.596\n", 442 | "2022-12-16 00:00:00-05:00 1.781\n", 443 | "Name: Dividends, Length: 121, dtype: float64" 444 | ] 445 | }, 446 | "execution_count": 20, 447 | "metadata": {}, 448 | "output_type": "execute_result" 449 | } 450 | ], 451 | "source": [ 452 | "# Show (print) an account level summary of new dividends with date included\n", 453 | "spy = yf.Ticker(\"SPY\")\n", 454 | "spy.dividends" 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": 22, 460 | "metadata": { 461 | "colab": { 462 | "base_uri": "https://localhost:8080/" 463 | }, 464 | "id": "V9lUxKYu0OTv", 465 | "outputId": "33b2d655-3f3c-471a-e1d0-d2385d5b32e6" 466 | }, 467 | "outputs": [], 468 | "source": [ 469 | "#spy.info['yield']" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 23, 475 | "metadata": { 476 | "id": "C3gd5n3ZA3iX" 477 | }, 478 | "outputs": [], 479 | "source": [ 480 | "#Learn how to calculate Fees" 481 | ] 482 | }, 483 | { 484 | "cell_type": "code", 485 | "execution_count": 24, 486 | "metadata": { 487 | "id": "6vQQ-AVkN7AT" 488 | }, 489 | "outputs": [], 490 | "source": [ 491 | "accounts = [myAccount, myAccount2]" 492 | ] 493 | }, 494 | { 495 | "cell_type": "code", 496 | "execution_count": 26, 497 | "metadata": { 498 | "colab": { 499 | "base_uri": "https://localhost:8080/" 500 | }, 501 | "id": "AwmohkNBOVVt", 502 | "outputId": "9023e899-52e6-4be4-dded-0673bb5f2213" 503 | }, 504 | "outputs": [ 505 | { 506 | "name": "stdout", 507 | "output_type": "stream", 508 | "text": [ 509 | "Account: 123456789\n", 510 | "Cash: 11.1699714236\n", 511 | "Portfolio: Moderate\n", 512 | "AUM: $6.86\n", 513 | "\n", 514 | "\n", 515 | "Account: 987654321\n", 516 | "Cash: 21.1937368557\n", 517 | "Portfolio: Conservative\n", 518 | "AUM: $24.24\n", 519 | "\n", 520 | "\n" 521 | ] 522 | } 523 | ], 524 | "source": [ 525 | "# Calculate AUM today\n", 526 | "for account in accounts:\n", 527 | " print(\"Account: \" + str(account.number))\n", 528 | " print(\"Cash: \" + str(account.cashBalance))\n", 529 | " aum = 0.0\n", 530 | " for goal in account.goals:\n", 531 | " print(\"Portfolio: \" + goal.portfolio.name)\n", 532 | " for allocation in goal.portfolio.allocations:\n", 533 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 534 | " aum += price * allocation.units\n", 535 | " print(\"AUM: \" + '${0:.2f}'.format(aum))\n", 536 | " print(\"\\n\")" 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": 27, 542 | "metadata": { 543 | "colab": { 544 | "base_uri": "https://localhost:8080/" 545 | }, 546 | "id": "4d2CMBl0S2w-", 547 | "outputId": "32455f32-f66a-4b2f-907b-07c7754cf6a1" 548 | }, 549 | "outputs": [ 550 | { 551 | "name": "stdout", 552 | "output_type": "stream", 553 | "text": [ 554 | "GLD DBC TLT IEI VTI \n" 555 | ] 556 | } 557 | ], 558 | "source": [ 559 | "tickerString = \"\"\n", 560 | "for account in accounts:\n", 561 | " for goal in account.goals:\n", 562 | " for allocation in goal.portfolio.allocations:\n", 563 | " if not allocation.ticker in tickerString:\n", 564 | " tickerString += allocation.ticker + \" \"\n", 565 | "print(tickerString)" 566 | ] 567 | }, 568 | { 569 | "cell_type": "code", 570 | "execution_count": 29, 571 | "metadata": { 572 | "colab": { 573 | "base_uri": "https://localhost:8080/" 574 | }, 575 | "id": "yXtTIl-US45_", 576 | "outputId": "788a9b6b-690f-4239-c4e5-3ddd0b84df13" 577 | }, 578 | "outputs": [ 579 | { 580 | "name": "stdout", 581 | "output_type": "stream", 582 | "text": [ 583 | "First date of previous month: 2022-12-01\n", 584 | "Last date of previous month: 2022-12-31\n" 585 | ] 586 | } 587 | ], 588 | "source": [ 589 | "# Import the datetime module\n", 590 | "from datetime import datetime, timedelta\n", 591 | "from calendar import monthrange\n", 592 | "\n", 593 | "# Get the current date\n", 594 | "today = datetime.today()\n", 595 | "\n", 596 | "# Get the first day of the current month\n", 597 | "first_day_of_month = today.replace(day=1)\n", 598 | "\n", 599 | "# Use the monthrange() method to get the number of days in the previous month\n", 600 | "month = first_day_of_month.month-1\n", 601 | "if month == 0:\n", 602 | " month = 12\n", 603 | "_, num_days_in_prev_month = monthrange(first_day_of_month.year, month)\n", 604 | "\n", 605 | "# Subtract the number of days in the previous month from the first day of the current month to get the first day of the previous month\n", 606 | "first_day_of_prev_month = first_day_of_month - timedelta(days=num_days_in_prev_month)\n", 607 | "\n", 608 | "# Get the last day of the previous month by adding one day to the first day of the current month, and then subtracting one day\n", 609 | "last_day_of_prev_month = first_day_of_month - timedelta(days=1)\n", 610 | "\n", 611 | "# Print the first and last dates of the previous month\n", 612 | "print(f\"First date of previous month: {first_day_of_prev_month:%Y-%m-%d}\")\n", 613 | "print(f\"Last date of previous month: {last_day_of_prev_month:%Y-%m-%d}\")\n" 614 | ] 615 | }, 616 | { 617 | "cell_type": "code", 618 | "execution_count": 30, 619 | "metadata": { 620 | "colab": { 621 | "base_uri": "https://localhost:8080/" 622 | }, 623 | "id": "GFGxuBTGPY6M", 624 | "outputId": "86711716-84c4-40ff-b599-e63a2848250a" 625 | }, 626 | "outputs": [ 627 | { 628 | "name": "stdout", 629 | "output_type": "stream", 630 | "text": [ 631 | "[*********************100%***********************] 5 of 5 completed\n", 632 | " GLD VTI DBC IEI \\\n", 633 | "Date \n", 634 | "2022-12-01 00:00:00-05:00 167.839996 204.179993 25.360001 116.430000 \n", 635 | "2022-12-02 00:00:00-05:00 167.259995 203.990005 25.090000 116.529999 \n", 636 | "2022-12-05 00:00:00-05:00 164.389999 199.979996 24.440001 115.720001 \n", 637 | "2022-12-06 00:00:00-05:00 164.839996 196.979996 24.049999 116.059998 \n", 638 | "2022-12-07 00:00:00-05:00 166.330002 196.660004 23.930000 116.720001 \n", 639 | "2022-12-08 00:00:00-05:00 166.470001 198.360001 23.889999 116.290001 \n", 640 | "2022-12-09 00:00:00-05:00 167.059998 196.699997 23.920000 115.940002 \n", 641 | "2022-12-12 00:00:00-05:00 165.679993 199.529999 24.180000 115.860001 \n", 642 | "2022-12-13 00:00:00-05:00 168.509995 201.020004 24.580000 116.540001 \n", 643 | "2022-12-14 00:00:00-05:00 168.100006 199.899994 24.770000 116.760002 \n", 644 | "2022-12-15 00:00:00-05:00 165.350006 194.919998 24.469999 116.589996 \n", 645 | "2022-12-16 00:00:00-05:00 166.789993 192.690002 24.160000 116.570000 \n", 646 | "2022-12-19 00:00:00-05:00 166.320007 190.750000 23.969999 116.120003 \n", 647 | "2022-12-20 00:00:00-05:00 169.080002 191.149994 24.080000 115.750000 \n", 648 | "2022-12-21 00:00:00-05:00 168.800003 194.039993 24.389999 115.849998 \n", 649 | "2022-12-22 00:00:00-05:00 166.759995 190.369995 24.080000 115.769997 \n", 650 | "2022-12-23 00:00:00-05:00 167.259995 191.419998 24.520000 115.559998 \n", 651 | "2022-12-27 00:00:00-05:00 168.669998 190.619995 24.740000 115.019997 \n", 652 | "2022-12-28 00:00:00-05:00 167.910004 188.220001 24.459999 114.940002 \n", 653 | "2022-12-29 00:00:00-05:00 168.850006 191.679993 24.430000 115.150002 \n", 654 | "2022-12-30 00:00:00-05:00 169.639999 191.190002 24.650000 114.889999 \n", 655 | "\n", 656 | " TLT \n", 657 | "Date \n", 658 | "2022-12-01 00:00:00-05:00 105.760002 \n", 659 | "2022-12-02 00:00:00-05:00 107.089996 \n", 660 | "2022-12-05 00:00:00-05:00 105.589996 \n", 661 | "2022-12-06 00:00:00-05:00 106.949997 \n", 662 | "2022-12-07 00:00:00-05:00 109.470001 \n", 663 | "2022-12-08 00:00:00-05:00 109.169998 \n", 664 | "2022-12-09 00:00:00-05:00 106.330002 \n", 665 | "2022-12-12 00:00:00-05:00 106.669998 \n", 666 | "2022-12-13 00:00:00-05:00 107.699997 \n", 667 | "2022-12-14 00:00:00-05:00 108.160004 \n", 668 | "2022-12-15 00:00:00-05:00 108.320000 \n", 669 | "2022-12-16 00:00:00-05:00 107.110001 \n", 670 | "2022-12-19 00:00:00-05:00 105.309998 \n", 671 | "2022-12-20 00:00:00-05:00 103.440002 \n", 672 | "2022-12-21 00:00:00-05:00 103.699997 \n", 673 | "2022-12-22 00:00:00-05:00 103.680000 \n", 674 | "2022-12-23 00:00:00-05:00 102.160004 \n", 675 | "2022-12-27 00:00:00-05:00 100.139999 \n", 676 | "2022-12-28 00:00:00-05:00 99.550003 \n", 677 | "2022-12-29 00:00:00-05:00 100.680000 \n", 678 | "2022-12-30 00:00:00-05:00 99.559998 \n" 679 | ] 680 | } 681 | ], 682 | "source": [ 683 | "# Get price history over period for all accounts and tickers (one month)\n", 684 | "data = yf.download(tickerString, group_by=\"Ticker\", start=first_day_of_prev_month, end=last_day_of_prev_month)\n", 685 | "data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 686 | "data = data.dropna()\n", 687 | "data.columns = data.columns.droplevel(1)\n", 688 | "print(data)" 689 | ] 690 | }, 691 | { 692 | "cell_type": "code", 693 | "execution_count": 31, 694 | "metadata": { 695 | "colab": { 696 | "base_uri": "https://localhost:8080/" 697 | }, 698 | "id": "u6DBb28-Pegr", 699 | "outputId": "c3b4659e-63b0-4c2b-cbaf-49051bcffa4f" 700 | }, 701 | "outputs": [ 702 | { 703 | "name": "stdout", 704 | "output_type": "stream", 705 | "text": [ 706 | "Account: 123456789\n", 707 | "Cash: 11.1699714236\n", 708 | "Portfolio: Moderate\n", 709 | "Average AUM: $6.72\n", 710 | "\n", 711 | "\n", 712 | "Account: 987654321\n", 713 | "Cash: 21.1937368557\n", 714 | "Portfolio: Conservative\n", 715 | "Average AUM: $22.75\n", 716 | "\n", 717 | "\n" 718 | ] 719 | } 720 | ], 721 | "source": [ 722 | "# Calculate average AUM over period\n", 723 | "for account in accounts:\n", 724 | " print(\"Account: \" + str(account.number))\n", 725 | " print(\"Cash: \" + str(account.cashBalance))\n", 726 | " aum = 0.0\n", 727 | " for goal in account.goals:\n", 728 | " print(\"Portfolio: \" + goal.portfolio.name)\n", 729 | " for allocation in goal.portfolio.allocations:\n", 730 | " for index, row in data.iterrows():\n", 731 | " price = row[allocation.ticker]\n", 732 | " aum += price * allocation.units\n", 733 | " aum = aum / len(data)\n", 734 | " print(\"Average AUM: \" + '${0:.2f}'.format(aum))\n", 735 | " print(\"\\n\")" 736 | ] 737 | }, 738 | { 739 | "cell_type": "code", 740 | "execution_count": 32, 741 | "metadata": { 742 | "colab": { 743 | "base_uri": "https://localhost:8080/" 744 | }, 745 | "id": "VMLnM7qkWtBp", 746 | "outputId": "ceb6c410-8f63-46c5-f6a8-85775f42f205" 747 | }, 748 | "outputs": [ 749 | { 750 | "name": "stdout", 751 | "output_type": "stream", 752 | "text": [ 753 | "Account: 123456789\n", 754 | "Cash: 11.1699714236\n", 755 | "Portfolio: Moderate\n", 756 | "Average AUM: $6.72\n", 757 | "Fee: $0.0005706570443678678\n", 758 | "\n", 759 | "\n", 760 | "Account: 987654321\n", 761 | "Cash: 21.1937368557\n", 762 | "Portfolio: Conservative\n", 763 | "Average AUM: $22.75\n", 764 | "Fee: $0.001931956557080065\n", 765 | "\n", 766 | "\n" 767 | ] 768 | } 769 | ], 770 | "source": [ 771 | "# Calculate fee\n", 772 | "feeRate = 0.001 # 10bps\n", 773 | "\n", 774 | "for account in accounts:\n", 775 | " print(\"Account: \" + str(account.number))\n", 776 | " print(\"Cash: \" + str(account.cashBalance))\n", 777 | " aum = 0.0\n", 778 | " for goal in account.goals:\n", 779 | " print(\"Portfolio: \" + goal.portfolio.name)\n", 780 | " for allocation in goal.portfolio.allocations:\n", 781 | " for index, row in data.iterrows():\n", 782 | " price = row[allocation.ticker]\n", 783 | " aum += price * allocation.units\n", 784 | " aum = aum / len(data)\n", 785 | " fee = aum * feeRate * (num_days_in_prev_month / 365)\n", 786 | " print(\"Average AUM: \" + '${0:.2f}'.format(aum))\n", 787 | " print(\"Fee: $\" + str(fee))\n", 788 | " print(\"\\n\")" 789 | ] 790 | }, 791 | { 792 | "cell_type": "code", 793 | "execution_count": 33, 794 | "metadata": { 795 | "id": "UHMZ_oTCPs6G" 796 | }, 797 | "outputs": [], 798 | "source": [ 799 | "# May need to create sell order(s) if cash < AUM" 800 | ] 801 | }, 802 | { 803 | "cell_type": "code", 804 | "execution_count": 34, 805 | "metadata": { 806 | "colab": { 807 | "base_uri": "https://localhost:8080/" 808 | }, 809 | "id": "hkz9azlLP2cC", 810 | "outputId": "3f310e5d-c8d7-4e2b-bacc-3694227f48da" 811 | }, 812 | "outputs": [ 813 | { 814 | "name": "stdout", 815 | "output_type": "stream", 816 | "text": [ 817 | " Account FeeAmount\n", 818 | "0 123456789 0.000571\n", 819 | "1 987654321 0.001932\n" 820 | ] 821 | } 822 | ], 823 | "source": [ 824 | "# Generate broker instructions for fees (CSV?)\n", 825 | "feeRate = 0.001 # 10bps\n", 826 | "feeTable = pd.DataFrame(columns=['Account','FeeAmount'])\n", 827 | "\n", 828 | "for account in accounts:\n", 829 | " aum = 0.0\n", 830 | " for goal in account.goals:\n", 831 | " for allocation in goal.portfolio.allocations:\n", 832 | " for index, row in data.iterrows():\n", 833 | " price = row[allocation.ticker]\n", 834 | " aum += price * allocation.units\n", 835 | " aum = aum / len(data)\n", 836 | " fee = aum * feeRate * (num_days_in_prev_month / 365)\n", 837 | " new_row = {'Account':account.number, 'FeeAmount':fee}\n", 838 | " feeTable = feeTable.append(new_row, ignore_index=True)\n", 839 | "\n", 840 | "print(feeTable)" 841 | ] 842 | }, 843 | { 844 | "cell_type": "code", 845 | "execution_count": 35, 846 | "metadata": { 847 | "id": "xMg8iuiLZOgG" 848 | }, 849 | "outputs": [], 850 | "source": [ 851 | "pd.set_option(\"display.precision\", 8)" 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "execution_count": 36, 857 | "metadata": { 858 | "colab": { 859 | "base_uri": "https://localhost:8080/" 860 | }, 861 | "id": "8LlKczkmZTeg", 862 | "outputId": "c9e08ef2-58ac-4128-9822-97c0e445ac5a" 863 | }, 864 | "outputs": [ 865 | { 866 | "name": "stdout", 867 | "output_type": "stream", 868 | "text": [ 869 | " Account FeeAmount\n", 870 | "0 123456789 0.00057066\n", 871 | "1 987654321 0.00193196\n" 872 | ] 873 | } 874 | ], 875 | "source": [ 876 | "# Generate broker instructions for fees (CSV?)\n", 877 | "feeRate = 0.001 # 10bps\n", 878 | "feeTable = pd.DataFrame(columns=['Account','FeeAmount'])\n", 879 | "\n", 880 | "for account in accounts:\n", 881 | " aum = 0.0\n", 882 | " for goal in account.goals:\n", 883 | " for allocation in goal.portfolio.allocations:\n", 884 | " for index, row in data.iterrows():\n", 885 | " price = row[allocation.ticker]\n", 886 | " aum += price * allocation.units\n", 887 | " aum = aum / len(data)\n", 888 | " fee = aum * feeRate * (num_days_in_prev_month / 365)\n", 889 | " new_row = {'Account':account.number, 'FeeAmount':fee}\n", 890 | " feeTable = feeTable.append(new_row, ignore_index=True)\n", 891 | "\n", 892 | "print(feeTable)" 893 | ] 894 | }, 895 | { 896 | "cell_type": "code", 897 | "execution_count": 39, 898 | "metadata": { 899 | "id": "4NMtrYWhPjrT" 900 | }, 901 | "outputs": [], 902 | "source": [ 903 | "# Create function\n", 904 | "def generateFeeInstructions(accounts: list) -> pd.DataFrame:\n", 905 | " tickerString = \"\"\n", 906 | " for account in accounts:\n", 907 | " for goal in account.goals:\n", 908 | " for allocation in goal.portfolio.allocations:\n", 909 | " if not allocation.ticker in tickerString:\n", 910 | " tickerString += allocation.ticker + \" \"\n", 911 | " \n", 912 | " from datetime import datetime, timedelta\n", 913 | " from calendar import monthrange\n", 914 | " today = datetime.today()\n", 915 | " first_day_of_month = today.replace(day=1)\n", 916 | " month = first_day_of_month.month-1\n", 917 | " if month == 0:\n", 918 | " month = 12\n", 919 | " _, num_days_in_prev_month = monthrange(first_day_of_month.year, month)\n", 920 | " first_day_of_prev_month = first_day_of_month - timedelta(days=num_days_in_prev_month)\n", 921 | " last_day_of_prev_month = first_day_of_month - timedelta(days=1)\n", 922 | "\n", 923 | " data = yf.download(tickerString, group_by=\"Ticker\", start=first_day_of_prev_month, end=last_day_of_prev_month)\n", 924 | " data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 925 | " data = data.dropna()\n", 926 | " data.columns = data.columns.droplevel(1)\n", 927 | "\n", 928 | " feeRate = 0.001 # 10bps\n", 929 | " feeTable = pd.DataFrame(columns=['Account','FeeAmount'])\n", 930 | "\n", 931 | " for account in accounts:\n", 932 | " aum = 0.0\n", 933 | " for goal in account.goals:\n", 934 | " for allocation in goal.portfolio.allocations:\n", 935 | " for index, row in data.iterrows():\n", 936 | " price = row[allocation.ticker]\n", 937 | " aum += price * allocation.units\n", 938 | " aum = aum / len(data)\n", 939 | " fee = aum * feeRate * (num_days_in_prev_month / 365)\n", 940 | " new_row = {'Account':account.number, 'FeeAmount':fee}\n", 941 | " feeTable = feeTable.append(new_row, ignore_index=True)\n", 942 | "\n", 943 | " return feeTable" 944 | ] 945 | }, 946 | { 947 | "cell_type": "code", 948 | "execution_count": 40, 949 | "metadata": { 950 | "colab": { 951 | "base_uri": "https://localhost:8080/" 952 | }, 953 | "id": "kIpYr_ppaC-g", 954 | "outputId": "d7b3c1a6-a652-4d81-d3eb-0dd8cb9e7577" 955 | }, 956 | "outputs": [ 957 | { 958 | "name": "stdout", 959 | "output_type": "stream", 960 | "text": [ 961 | "[*********************100%***********************] 5 of 5 completed\n" 962 | ] 963 | } 964 | ], 965 | "source": [ 966 | "# Save to CSV\n", 967 | "feeFile = generateFeeInstructions(accounts)" 968 | ] 969 | }, 970 | { 971 | "cell_type": "code", 972 | "execution_count": 41, 973 | "metadata": { 974 | "id": "Ljs5OQcYalHx" 975 | }, 976 | "outputs": [], 977 | "source": [ 978 | "feeFile.to_csv('./Data/Fees.csv')" 979 | ] 980 | } 981 | ], 982 | "metadata": { 983 | "colab": { 984 | "provenance": [], 985 | "toc_visible": true 986 | }, 987 | "kernelspec": { 988 | "display_name": "Python 3", 989 | "language": "python", 990 | "name": "python3" 991 | }, 992 | "language_info": { 993 | "codemirror_mode": { 994 | "name": "ipython", 995 | "version": 3 996 | }, 997 | "file_extension": ".py", 998 | "mimetype": "text/x-python", 999 | "name": "python", 1000 | "nbconvert_exporter": "python", 1001 | "pygments_lexer": "ipython3", 1002 | "version": "3.10.8" 1003 | }, 1004 | "vscode": { 1005 | "interpreter": { 1006 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 1007 | } 1008 | } 1009 | }, 1010 | "nbformat": 4, 1011 | "nbformat_minor": 0 1012 | } 1013 | -------------------------------------------------------------------------------- /Chapter12.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "id": "CB2igcSLonpW" 8 | }, 9 | "source": [ 10 | "# Chapter 12: Order Management and Execution" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import yfinance as yf\n", 20 | "import pandas as pd" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": { 27 | "id": "gDVjTN_tamDz" 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "class Allocation:\n", 32 | " def __init__(self, ticker, percentage):\n", 33 | " self.ticker = ticker\n", 34 | " self.percentage = percentage\n", 35 | " self.units = 0.0\n", 36 | "\n", 37 | "class Portfolio:\n", 38 | "\n", 39 | " def __init__(self, tickerString: str, expectedReturn: float, portfolioName: str, riskBucket: int):\n", 40 | "\n", 41 | " self.name = portfolioName\n", 42 | " self.riskBucket = riskBucket\n", 43 | " self.expectedReturn = expectedReturn\n", 44 | " self.allocations = []\n", 45 | "\n", 46 | " from pypfopt.efficient_frontier import EfficientFrontier\n", 47 | " from pypfopt import risk_models\n", 48 | " from pypfopt import expected_returns\n", 49 | "\n", 50 | " df = self.__getDailyPrices(tickerString, \"20y\")\n", 51 | "\n", 52 | " mu = expected_returns.mean_historical_return(df)\n", 53 | " S = risk_models.sample_cov(df)\n", 54 | "\n", 55 | " ef = EfficientFrontier(mu, S)\n", 56 | "\n", 57 | " ef.efficient_return(expectedReturn)\n", 58 | " self.expectedRisk = ef.portfolio_performance()[1]\n", 59 | " portfolioWeights = ef.clean_weights()\n", 60 | "\n", 61 | " for key, value in portfolioWeights.items():\n", 62 | " newAllocation = Allocation(key, value)\n", 63 | " self.allocations.append(newAllocation)\n", 64 | "\n", 65 | " def __getDailyPrices(self, tickerStringList, period):\n", 66 | " data = yf.download(tickerStringList, group_by=\"Ticker\", period=period)\n", 67 | " data = data.iloc[:, data.columns.get_level_values(1)==\"Close\"]\n", 68 | " data = data.dropna()\n", 69 | " data.columns = data.columns.droplevel(1)\n", 70 | " return data\n", 71 | "\n", 72 | " def printPortfolio(self):\n", 73 | " print(\"Portfolio Name: \" + self.name)\n", 74 | " print(\"Risk Bucket: \" + str(self.riskBucket))\n", 75 | " print(\"Expected Return: \" + str(self.expectedReturn))\n", 76 | " print(\"Expected Risk: \" + str(self.expectedRisk))\n", 77 | " print(\"Allocations: \")\n", 78 | " for allocation in self.allocations:\n", 79 | " print(\"Ticker: \" + allocation.ticker + \", Percentage: \" + str(allocation.percentage))\n", 80 | "\n", 81 | " @staticmethod\n", 82 | " def getPortfolioMapping(riskToleranceScore, riskCapacityScore):\n", 83 | " import pandas as pd\n", 84 | " allocationLookupTable=pd.read_csv('./Data/Risk Mapping Lookup.csv')\n", 85 | " matchTol = (allocationLookupTable['Tolerance_min'] <= riskToleranceScore) & (allocationLookupTable['Tolerance_max'] >= riskToleranceScore)\n", 86 | " matchCap = (allocationLookupTable['Capacity_min'] <= riskCapacityScore) & (allocationLookupTable['Capacity_max'] >= riskCapacityScore)\n", 87 | " portfolioID = allocationLookupTable['Portfolio'][(matchTol & matchCap)]\n", 88 | " return portfolioID.values[0]\n", 89 | "\n", 90 | "class Goal:\n", 91 | " def __init__(self, name, targetYear, targetValue, initialContribution=0, monthlyContribution=0, priority=\"\"):\n", 92 | " self.name = name\n", 93 | " self.targetYear = targetYear\n", 94 | " self.targetValue = targetValue\n", 95 | " self.initialContribution = initialContribution\n", 96 | " self.monthlyContribution = monthlyContribution\n", 97 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 98 | " raise ValueError('Wrong value set for Priority.')\n", 99 | " self.priority = priority\n", 100 | "\n", 101 | " def getGoalProbabilities(self):\n", 102 | " if (self.priority == \"\"):\n", 103 | " raise ValueError('No value set for Priority.')\n", 104 | " lookupTable=pd.read_csv('./Data/Goal Probability Table.csv')\n", 105 | " match = (lookupTable['Realize'] == self.priority)\n", 106 | " minProb = lookupTable['MinP'][(match)]\n", 107 | " maxProb = lookupTable['MaxP'][(match)]\n", 108 | " return minProb.values[0], maxProb.values[0]" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 3, 114 | "metadata": { 115 | "id": "GpPFh25AC3Uw" 116 | }, 117 | "outputs": [], 118 | "source": [ 119 | "class AccountType():\n", 120 | " def __init__(self, value: str):\n", 121 | " if not value in(\"Taxable\", \"Roth IRA\", \"Traditional IRA\"):\n", 122 | " raise ValueError(\"Allowed types: Taxable, Roth IRA, Traditional IRA\")\n", 123 | " self.value = value\n", 124 | " def __eq__(self, other):\n", 125 | " return self.value == other.value\n", 126 | "\n", 127 | "class AccountStatus():\n", 128 | " def __init__(self, value: str):\n", 129 | " if not value in(\"PENDING\", \"IN_REVIEW\", \"APPROVED\", \"REJECTED\", \"SUSPENDED\"):\n", 130 | " raise ValueError(\"Allowed statuses: PENDING, IN_REVIEW, APPROVED, REJECTED, SUSPENDED\")\n", 131 | " self.value = value\n", 132 | " def __eq__(self, other):\n", 133 | " return self.value == other.value\n", 134 | "\n", 135 | "class Account():\n", 136 | " def __init__(self, number: str, accountType: AccountType, accountStatus: AccountStatus, cashBalance: float=0.0):\n", 137 | " self.goals = []\n", 138 | " self.number = number\n", 139 | " self.cashBalance = cashBalance\n", 140 | " self.accountType = accountType\n", 141 | " self.accountStatus = accountStatus" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 4, 147 | "metadata": { 148 | "id": "ts4n53XrFoWX" 149 | }, 150 | "outputs": [], 151 | "source": [ 152 | "class Goal:\n", 153 | " def __init__(self, name: str, targetYear: int, targetValue: float, portfolio: Portfolio=None, initialContribution: float=0, monthlyContribution: float=0, priority: str=\"\"):\n", 154 | " self.name = name\n", 155 | " self.targetYear = targetYear\n", 156 | " self.targetValue = targetValue\n", 157 | " self.initialContribution = initialContribution\n", 158 | " self.monthlyContribution = monthlyContribution\n", 159 | " if not (priority == \"\") and not (priority in [\"Dreams\", \"Wishes\", \"Wants\", \"Needs\"]):\n", 160 | " raise ValueError(\"Wrong value set for Priority.\")\n", 161 | " self.priority = priority\n", 162 | " self.portfolio = portfolio\n", 163 | "\n", 164 | " def getGoalProbabilities(self):\n", 165 | " if (self.priority == \"\"):\n", 166 | " raise ValueError(\"No value set for Priority.\")\n", 167 | " lookupTable=pd.read_csv(\"./Data/Goal Probability Table.csv\")\n", 168 | " match = (lookupTable[\"Realize\"] == self.priority)\n", 169 | " minProb = lookupTable[\"MinP\"][(match)]\n", 170 | " maxProb = lookupTable[\"MaxP\"][(match)]\n", 171 | " return minProb.values[0], maxProb.values[0]" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 5, 177 | "metadata": { 178 | "id": "FYgpvO2E0ZHq" 179 | }, 180 | "outputs": [], 181 | "source": [ 182 | "class TransactionType():\n", 183 | " def __init__(self, value: str):\n", 184 | " if not value in(\"BUY\", \"SELL\"):\n", 185 | " raise ValueError(\"Allowed types: BUY, SELL.\")\n", 186 | " self.value = value\n", 187 | " def __eq__(self, other):\n", 188 | " return self.value == other.value\n", 189 | "\n", 190 | "class OrderStatus():\n", 191 | " def __init__(self, value: str):\n", 192 | " if not value in(\"NEW\", \"PENDING\", \"FILLED\", \"REJECTED\"):\n", 193 | " raise ValueError(\"Allowed statuses: NEW, PENDING, FILLED, REJECTED.\")\n", 194 | " self.value = value\n", 195 | " def __eq__(self, other):\n", 196 | " return self.value == other.value" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": { 203 | "id": "mcrGFlTGPi4n" 204 | }, 205 | "outputs": [], 206 | "source": [ 207 | "class Order:\n", 208 | " def __init__(self, account: Account, goal: Goal, transactionType: TransactionType, status: OrderStatus=OrderStatus(\"NEW\"), dollarAmount: float=0.0):\n", 209 | " \n", 210 | " self.account = account\n", 211 | " self.transactionType = transactionType\n", 212 | " self.dollarAmount = dollarAmount\n", 213 | " self.goal = goal\n", 214 | " self.status = status" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 7, 220 | "metadata": { 221 | "colab": { 222 | "base_uri": "https://localhost:8080/" 223 | }, 224 | "id": "lg2EPtePhVE-", 225 | "outputId": "878be89f-44f0-4687-a5fd-681ed8ce464e" 226 | }, 227 | "outputs": [ 228 | { 229 | "name": "stdout", 230 | "output_type": "stream", 231 | "text": [ 232 | "Requirement already satisfied: pandas_market_calendars in /Users/akiranin/miniforge3/lib/python3.10/site-packages (4.1.3)\n", 233 | "Requirement already satisfied: python-dateutil in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pandas_market_calendars) (2.8.2)\n", 234 | "Requirement already satisfied: exchange-calendars>=3.3 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pandas_market_calendars) (4.2.4)\n", 235 | "Requirement already satisfied: pandas>=1.1 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pandas_market_calendars) (1.3.5)\n", 236 | "Requirement already satisfied: pytz in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from pandas_market_calendars) (2022.7.1)\n", 237 | "Requirement already satisfied: numpy in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from exchange-calendars>=3.3->pandas_market_calendars) (1.24.1)\n", 238 | "Requirement already satisfied: pyluach in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from exchange-calendars>=3.3->pandas_market_calendars) (2.0.2)\n", 239 | "Requirement already satisfied: toolz in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from exchange-calendars>=3.3->pandas_market_calendars) (0.12.0)\n", 240 | "Requirement already satisfied: korean-lunar-calendar in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from exchange-calendars>=3.3->pandas_market_calendars) (0.3.1)\n", 241 | "Requirement already satisfied: six>=1.5 in /Users/akiranin/miniforge3/lib/python3.10/site-packages (from python-dateutil->pandas_market_calendars) (1.16.0)\n", 242 | "Note: you may need to restart the kernel to use updated packages.\n" 243 | ] 244 | } 245 | ], 246 | "source": [ 247 | "pip install pandas_market_calendars" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": 8, 253 | "metadata": { 254 | "id": "RuBDd-gghOFJ" 255 | }, 256 | "outputs": [], 257 | "source": [ 258 | "def isMarketOpen():\n", 259 | " from datetime import datetime, timedelta\n", 260 | " import pandas_market_calendars as mcal\n", 261 | " \n", 262 | " previousday = datetime.now() - timedelta(5)\n", 263 | " nextday = datetime.now() + timedelta(5)\n", 264 | " nyse = mcal.get_calendar('NYSE')\n", 265 | " sched = nyse.schedule(start_date=previousday, end_date=nextday)\n", 266 | " \n", 267 | " return nyse.is_open_now(sched)" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 9, 273 | "metadata": { 274 | "colab": { 275 | "base_uri": "https://localhost:8080/" 276 | }, 277 | "id": "ubUwyMJ6knYf", 278 | "outputId": "6420875c-4b31-4044-cad6-0cc3f4cf8e9e" 279 | }, 280 | "outputs": [ 281 | { 282 | "data": { 283 | "text/plain": [ 284 | "False" 285 | ] 286 | }, 287 | "execution_count": 9, 288 | "metadata": {}, 289 | "output_type": "execute_result" 290 | } 291 | ], 292 | "source": [ 293 | "isMarketOpen()" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 10, 299 | "metadata": { 300 | "id": "jvWs0QDZuIhu" 301 | }, 302 | "outputs": [], 303 | "source": [ 304 | "class Order:\n", 305 | " def __init__(self, account: Account, goal: Goal, transactionType: TransactionType, status: OrderStatus=OrderStatus(\"NEW\"), dollarAmount: float=0.0):\n", 306 | " \n", 307 | " self.account = account\n", 308 | " self.transactionType = transactionType\n", 309 | " self.dollarAmount = dollarAmount\n", 310 | " self.goal = goal\n", 311 | " self.status = status\n", 312 | "\n", 313 | " def checkAccountStatus(self) -> bool:\n", 314 | " if self.account.accountStatus == AccountStatus(\"APPROVED\"):\n", 315 | " return True\n", 316 | " else:\n", 317 | " return False\n", 318 | "\n", 319 | " def checkOrderSize(self) -> bool:\n", 320 | " if self.dollarAmount > 1.00:\n", 321 | " return True\n", 322 | " else:\n", 323 | " return False\n", 324 | "\n", 325 | " def checkBuyPower(self) -> bool:\n", 326 | " if self.transactionType == TransactionType(\"BUY\") and self.account.cashBalance >= self.dollarAmount:\n", 327 | " return True\n", 328 | " elif self.transactionType == TransactionType(\"SELL\"):\n", 329 | " return True\n", 330 | " else:\n", 331 | " return False\n", 332 | "\n", 333 | " def checkOrderViability(self) -> bool:\n", 334 | " if self.checkAccountStatus() and self.checkOrderSize() and self.checkBuyPower() and isMarketOpen():\n", 335 | " return True\n", 336 | " else:\n", 337 | " return False" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": 11, 343 | "metadata": { 344 | "colab": { 345 | "base_uri": "https://localhost:8080/" 346 | }, 347 | "id": "CaKDZPEhFf5o", 348 | "outputId": "d2295f03-5af5-49df-8493-f6373d8d4953" 349 | }, 350 | "outputs": [ 351 | { 352 | "name": "stdout", 353 | "output_type": "stream", 354 | "text": [ 355 | "Python 3.10.8\n" 356 | ] 357 | } 358 | ], 359 | "source": [ 360 | "!python --version" 361 | ] 362 | }, 363 | { 364 | "cell_type": "code", 365 | "execution_count": 12, 366 | "metadata": { 367 | "id": "MSiE9MctGUBR" 368 | }, 369 | "outputs": [], 370 | "source": [ 371 | "#pip install fastapi" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 13, 377 | "metadata": { 378 | "colab": { 379 | "base_uri": "https://localhost:8080/", 380 | "height": 35 381 | }, 382 | "id": "4oCA6KOsG-d3", 383 | "outputId": "3a61d972-21e0-4d40-926b-384948d0421e" 384 | }, 385 | "outputs": [ 386 | { 387 | "data": { 388 | "text/plain": [ 389 | "'from fastapi import FastAPI\\napp = FastAPI()\\n\\n@app.get(\"/my-first-api\")\\ndef hello():\\n return {\"Hello world!\"}'" 390 | ] 391 | }, 392 | "execution_count": 13, 393 | "metadata": {}, 394 | "output_type": "execute_result" 395 | } 396 | ], 397 | "source": [ 398 | "'''from fastapi import FastAPI\n", 399 | "app = FastAPI()\n", 400 | "\n", 401 | "@app.get(\"/my-first-api\")\n", 402 | "def hello():\n", 403 | " return {\"Hello world!\"}'''" 404 | ] 405 | }, 406 | { 407 | "cell_type": "code", 408 | "execution_count": 14, 409 | "metadata": { 410 | "colab": { 411 | "base_uri": "https://localhost:8080/" 412 | }, 413 | "id": "F3nW-SFRuoUg", 414 | "outputId": "1cbefd4d-4a01-4419-9c01-21ae019820aa" 415 | }, 416 | "outputs": [ 417 | { 418 | "name": "stdout", 419 | "output_type": "stream", 420 | "text": [ 421 | "(CVXPY) Jan 26 12:16:29 PM: Encountered unexpected exception importing solver SCS:\n", 422 | "ImportError(\"dlopen(/Users/akiranin/miniforge3/lib/python3.10/site-packages/_scs_direct.cpython-310-darwin.so, 0x0002): tried: '/Users/akiranin/miniforge3/lib/python3.10/site-packages/_scs_direct.cpython-310-darwin.so' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/Users/akiranin/miniforge3/lib/python3.10/site-packages/_scs_direct.cpython-310-darwin.so' (no such file), '/Users/akiranin/miniforge3/lib/python3.10/site-packages/_scs_direct.cpython-310-darwin.so' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))\")\n", 423 | "(CVXPY) Jan 26 12:16:29 PM: Encountered unexpected exception importing solver OSQP:\n", 424 | "ImportError(\"dlopen(/Users/akiranin/miniforge3/lib/python3.10/site-packages/qdldl.cpython-310-darwin.so, 0x0002): tried: '/Users/akiranin/miniforge3/lib/python3.10/site-packages/qdldl.cpython-310-darwin.so' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/Users/akiranin/miniforge3/lib/python3.10/site-packages/qdldl.cpython-310-darwin.so' (no such file), '/Users/akiranin/miniforge3/lib/python3.10/site-packages/qdldl.cpython-310-darwin.so' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))\")\n", 425 | "[*********************100%***********************] 5 of 5 completed\n", 426 | "False\n" 427 | ] 428 | } 429 | ], 430 | "source": [ 431 | "myPortfolio = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.05, portfolioName = \"Moderate\", riskBucket = 3)\n", 432 | "myGoal = Goal(name=\"Vacation\", targetYear=2027, targetValue=10000, priority=\"Dreams\", portfolio=myPortfolio)\n", 433 | "myAccount=Account(number=\"123456789\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=11.0)\n", 434 | "myAccount.goals.append(myGoal)\n", 435 | "\n", 436 | "newOrder = Order(account=myAccount, goal=myGoal, transactionType=TransactionType(\"BUY\"), dollarAmount=10.0)\n", 437 | "print(newOrder.checkOrderViability())" 438 | ] 439 | }, 440 | { 441 | "cell_type": "code", 442 | "execution_count": 15, 443 | "metadata": { 444 | "colab": { 445 | "base_uri": "https://localhost:8080/" 446 | }, 447 | "id": "kIfhYa-sIieR", 448 | "outputId": "5ce17a42-e820-476a-ad2e-c9a5815daa75" 449 | }, 450 | "outputs": [ 451 | { 452 | "name": "stdout", 453 | "output_type": "stream", 454 | "text": [ 455 | "Portfolio Name: Moderate\n", 456 | "Risk Bucket: 3\n", 457 | "Expected Return: 0.05\n", 458 | "Expected Risk: 0.09288271673415222\n", 459 | "Allocations: \n", 460 | "Ticker: DBC, Percentage: 0.0\n", 461 | "Ticker: GLD, Percentage: 0.36858\n", 462 | "Ticker: IEI, Percentage: 0.26399\n", 463 | "Ticker: VTI, Percentage: 0.30278\n", 464 | "Ticker: TLT, Percentage: 0.06466\n" 465 | ] 466 | } 467 | ], 468 | "source": [ 469 | "newOrder.goal.portfolio.printPortfolio()" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 16, 475 | "metadata": { 476 | "colab": { 477 | "base_uri": "https://localhost:8080/" 478 | }, 479 | "id": "0htJOym6Makt", 480 | "outputId": "161b1b8b-0ccf-46aa-f38d-41ff8966bb9b" 481 | }, 482 | "outputs": [ 483 | { 484 | "name": "stdout", 485 | "output_type": "stream", 486 | "text": [ 487 | "BUY: DBC, $0.0\n", 488 | "BUY: GLD, $3.6858000000000004\n", 489 | "BUY: IEI, $2.6399\n", 490 | "BUY: VTI, $3.0278\n", 491 | "BUY: TLT, $0.6466\n" 492 | ] 493 | } 494 | ], 495 | "source": [ 496 | "for allocation in newOrder.goal.portfolio.allocations:\n", 497 | " print(newOrder.transactionType.value + \": \" + allocation.ticker + \", $\" + str(allocation.percentage * newOrder.dollarAmount))" 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": 17, 503 | "metadata": { 504 | "id": "L2zdCLQiNcog" 505 | }, 506 | "outputs": [], 507 | "source": [ 508 | "class SplitOrder:\n", 509 | " def __init__(self, originalOrder: Order, ticker: str, dollarAmount: float):\n", 510 | " \n", 511 | " self.originalOrder = originalOrder\n", 512 | " self.ticker = ticker\n", 513 | " self.dollarAmount = dollarAmount\n", 514 | " self.units = 0" 515 | ] 516 | }, 517 | { 518 | "cell_type": "code", 519 | "execution_count": 18, 520 | "metadata": { 521 | "id": "F6twSJg6P6rA" 522 | }, 523 | "outputs": [], 524 | "source": [ 525 | "class Order:\n", 526 | " def __init__(self, account: Account, goal: Goal, transactionType: TransactionType, status: OrderStatus=OrderStatus(\"NEW\"), dollarAmount: float=0.0):\n", 527 | " \n", 528 | " self.account = account\n", 529 | " self.transactionType = transactionType\n", 530 | " self.dollarAmount = dollarAmount\n", 531 | " self.goal = goal\n", 532 | " self.status = status\n", 533 | "\n", 534 | " def checkAccountStatus(self) -> bool:\n", 535 | " if self.account.accountStatus == AccountStatus(\"APPROVED\"):\n", 536 | " return True\n", 537 | " else:\n", 538 | " return False\n", 539 | "\n", 540 | " def checkOrderSize(self) -> bool:\n", 541 | " if self.dollarAmount > 1.00:\n", 542 | " return True\n", 543 | " else:\n", 544 | " return False\n", 545 | "\n", 546 | " def checkBalances(self) -> bool:\n", 547 | " if self.transactionType == TransactionType(\"BUY\") and self.account.cashBalance >= self.dollarAmount:\n", 548 | " return True\n", 549 | " elif self.transactionType == TransactionType(\"SELL\"):\n", 550 | " goalValue = 0.0\n", 551 | " for allocation in self.goal.portfolio.allocations:\n", 552 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 553 | " goalValue += allocation.units * price\n", 554 | " if self.dollarAmount <= goalValue:\n", 555 | " return True\n", 556 | " else:\n", 557 | " return False\n", 558 | " else:\n", 559 | " return False\n", 560 | "\n", 561 | " def checkOrderViability(self) -> bool:\n", 562 | " if self.checkAccountStatus() and self.checkOrderSize() and self.checkBalances() and isMarketOpen():\n", 563 | " return True\n", 564 | " else:\n", 565 | " return False\n", 566 | "\n", 567 | " def split(self) -> list:\n", 568 | " splits = []\n", 569 | " for allocation in self.goal.portfolio.allocations:\n", 570 | " if (allocation.percentage > 0):\n", 571 | " splits.append(SplitOrder(originalOrder=self, ticker=allocation.ticker, dollarAmount=allocation.percentage * self.dollarAmount))\n", 572 | " return splits" 573 | ] 574 | }, 575 | { 576 | "cell_type": "code", 577 | "execution_count": 21, 578 | "metadata": { 579 | "colab": { 580 | "base_uri": "https://localhost:8080/" 581 | }, 582 | "id": "aoFY5CY8r-BY", 583 | "outputId": "39d5891f-3df9-4138-b122-375069592f4f" 584 | }, 585 | "outputs": [ 586 | { 587 | "name": "stdout", 588 | "output_type": "stream", 589 | "text": [ 590 | "0.0\n" 591 | ] 592 | } 593 | ], 594 | "source": [ 595 | "goalValue = 0.0\n", 596 | "for allocation in myGoal.portfolio.allocations:\n", 597 | " price = float(yf.Ticker(allocation.ticker).basic_info[\"previous_close\"])\n", 598 | " goalValue += allocation.units * price\n", 599 | "print(goalValue)" 600 | ] 601 | }, 602 | { 603 | "cell_type": "code", 604 | "execution_count": 22, 605 | "metadata": { 606 | "id": "W2lK_1LURHKD" 607 | }, 608 | "outputs": [], 609 | "source": [ 610 | "newOrder = Order(account=myAccount, goal=myGoal, transactionType=TransactionType(\"BUY\"), dollarAmount=10.0)\n", 611 | "splitOrders = newOrder.split()" 612 | ] 613 | }, 614 | { 615 | "cell_type": "code", 616 | "execution_count": 23, 617 | "metadata": { 618 | "colab": { 619 | "base_uri": "https://localhost:8080/" 620 | }, 621 | "id": "WedIAu-fQ4CK", 622 | "outputId": "0f68dd78-46ae-49be-9c03-85f4c83c3160" 623 | }, 624 | "outputs": [ 625 | { 626 | "name": "stdout", 627 | "output_type": "stream", 628 | "text": [ 629 | "GLD, $3.6858000000000004\n", 630 | "IEI, $2.6399\n", 631 | "VTI, $3.0278\n", 632 | "TLT, $0.6466\n" 633 | ] 634 | } 635 | ], 636 | "source": [ 637 | "for split in splitOrders:\n", 638 | " print(split.ticker + \", $\" + str(split.dollarAmount))" 639 | ] 640 | }, 641 | { 642 | "cell_type": "code", 643 | "execution_count": 24, 644 | "metadata": { 645 | "id": "L1PenkpVStsW" 646 | }, 647 | "outputs": [], 648 | "source": [ 649 | "# Master Order class that maintains ref. to split to original.\n", 650 | "# Some ID to track back to splitOrders?\n", 651 | "# Update status of originalOrder(s)\n", 652 | "class MasterOrder:\n", 653 | " def __init__(self, status: OrderStatus=OrderStatus(\"NEW\")):\n", 654 | " \n", 655 | " self.splitOrders = []\n", 656 | " self.masterTable = pd.DataFrame(columns=['Account','Symbol','Type','DollarAmount'])\n", 657 | " self.status = status\n", 658 | "\n", 659 | " def addSplitOrder(self, splitOrder: SplitOrder):\n", 660 | " self.splitOrders.append(splitOrder)\n", 661 | "\n", 662 | " def aggregate(self) -> pd.DataFrame:\n", 663 | " for split in self.splitOrders:\n", 664 | " new_row = {'Account':split.originalOrder.account.number,'Symbol':split.ticker,'Type':split.originalOrder.transactionType.value,'DollarAmount':split.dollarAmount}\n", 665 | " self.masterTable = self.masterTable.append(new_row, ignore_index=True)\n", 666 | " \n", 667 | " return self.masterTable.groupby(['Symbol','Type']).sum().reset_index()\n", 668 | " \n", 669 | " def orderSent(self):\n", 670 | " newStatus = OrderStatus(\"PENDING\")\n", 671 | " self.status = newStatus\n", 672 | " for split in self.splitOrders:\n", 673 | " split.originalOrder.status = newStatus\n", 674 | "\n", 675 | " def allocateAccounts(self, filledMasterOrderFile: pd.DataFrame) -> pd.DataFrame:\n", 676 | " accountTable = pd.DataFrame(columns=['Account','Symbol','Type','Units'], index=self.masterTable.index)\n", 677 | " for index, row in filledMasterOrderFile.iterrows():\n", 678 | " ordersToAllocate = self.masterTable[(self.masterTable['Symbol'] == row['Symbol']) & (self.masterTable['Type'] == row['Type'])]\n", 679 | " totalValue = float(ordersToAllocate.groupby(['Symbol','Type'])['DollarAmount'].sum()[0])\n", 680 | " for index2, row2 in ordersToAllocate.iterrows():\n", 681 | " unitsAllocated = (row2['DollarAmount'] / totalValue) * row['Units']\n", 682 | " new_row = {'Account':row2['Account'],'Symbol':row2['Symbol'],'Type':row2['Type'],'Units':unitsAllocated}\n", 683 | " accountTable.iloc[index2] = new_row\n", 684 | " self.splitOrders[index2].units = unitsAllocated\n", 685 | " self.splitOrders[index2].originalOrder.account.cashBalance -= unitsAllocated * row['Price']\n", 686 | " #print(self.splitOrders[index2].originalOrder.account.number)\n", 687 | " return accountTable\n", 688 | "\n", 689 | " def allocateGoals(self):\n", 690 | " for order in self.splitOrders:\n", 691 | " portfolioAllocations = order.originalOrder.goal.portfolio.allocations\n", 692 | " #print(order.originalOrder.goal.portfolio.name)\n", 693 | " for idx, item in enumerate(portfolioAllocations):\n", 694 | " if item.ticker == order.ticker and order.originalOrder.transactionType == TransactionType(\"BUY\"):\n", 695 | " order.originalOrder.goal.portfolio.allocations[idx].units += order.units\n", 696 | " #print(item.ticker + \": \" + str(order.units))\n", 697 | " elif item.ticker == order.ticker and order.originalOrder.transactionType == TransactionType(\"SELL\"):\n", 698 | " order.originalOrder.goal.portfolio.allocations[idx].units -= order.units \n", 699 | "\n", 700 | " def orderFilled(self):\n", 701 | " newStatus = OrderStatus(\"FILLED\")\n", 702 | " self.status = newStatus\n", 703 | " for split in self.splitOrders:\n", 704 | " split.originalOrder.status = newStatus " 705 | ] 706 | }, 707 | { 708 | "cell_type": "code", 709 | "execution_count": 25, 710 | "metadata": { 711 | "id": "no6F6CTiRBwI" 712 | }, 713 | "outputs": [], 714 | "source": [ 715 | "newMasterOrder = MasterOrder()\n", 716 | "for split in splitOrders:\n", 717 | " newMasterOrder.addSplitOrder(split)" 718 | ] 719 | }, 720 | { 721 | "cell_type": "code", 722 | "execution_count": 26, 723 | "metadata": { 724 | "colab": { 725 | "base_uri": "https://localhost:8080/" 726 | }, 727 | "id": "FWpKWx46SG4o", 728 | "outputId": "24352bdf-8845-4934-b5c5-d4b4497ed27f" 729 | }, 730 | "outputs": [ 731 | { 732 | "name": "stdout", 733 | "output_type": "stream", 734 | "text": [ 735 | "[*********************100%***********************] 5 of 5 completed\n" 736 | ] 737 | } 738 | ], 739 | "source": [ 740 | "# Create second order for second account\n", 741 | "\n", 742 | "myPortfolio2 = Portfolio(\"VTI TLT IEI GLD DBC\", expectedReturn = 0.03, portfolioName = \"Conservative\", riskBucket = 2)\n", 743 | "myGoal2 = Goal(name=\"Car\", targetYear=2025, targetValue=5000, priority=\"Dreams\", portfolio=myPortfolio2)\n", 744 | "myAccount2=Account(number=\"987654321\", accountType=\"Taxable\", accountStatus=AccountStatus(\"APPROVED\"), cashBalance=21.0)\n", 745 | "myAccount2.goals.append(myGoal2)" 746 | ] 747 | }, 748 | { 749 | "cell_type": "code", 750 | "execution_count": 27, 751 | "metadata": { 752 | "colab": { 753 | "base_uri": "https://localhost:8080/" 754 | }, 755 | "id": "QuCrkw79WKG2", 756 | "outputId": "ac9f2e37-1ff5-4914-a950-5486c4a656f1" 757 | }, 758 | "outputs": [ 759 | { 760 | "name": "stdout", 761 | "output_type": "stream", 762 | "text": [ 763 | "False\n" 764 | ] 765 | } 766 | ], 767 | "source": [ 768 | "newOrder2 = Order(account=myAccount2, goal=myGoal2, transactionType=TransactionType(\"BUY\"), dollarAmount=20.0)\n", 769 | "print(newOrder2.checkOrderViability())\n", 770 | "splitOrders2 = newOrder2.split()" 771 | ] 772 | }, 773 | { 774 | "cell_type": "code", 775 | "execution_count": 28, 776 | "metadata": { 777 | "colab": { 778 | "base_uri": "https://localhost:8080/" 779 | }, 780 | "id": "-W0nc1-CeEum", 781 | "outputId": "68b6f6b0-c8e4-4b24-9717-25d94e5b3d6e" 782 | }, 783 | "outputs": [ 784 | { 785 | "name": "stdout", 786 | "output_type": "stream", 787 | "text": [ 788 | "Portfolio Name: Conservative\n", 789 | "Risk Bucket: 2\n", 790 | "Expected Return: 0.03\n", 791 | "Expected Risk: 0.0513595407888017\n", 792 | "Allocations: \n", 793 | "Ticker: GLD, Percentage: 0.15874\n", 794 | "Ticker: DBC, Percentage: 0.0\n", 795 | "Ticker: IEI, Percentage: 0.66319\n", 796 | "Ticker: VTI, Percentage: 0.17806\n", 797 | "Ticker: TLT, Percentage: 0.0\n" 798 | ] 799 | } 800 | ], 801 | "source": [ 802 | "newOrder2.goal.portfolio.printPortfolio()" 803 | ] 804 | }, 805 | { 806 | "cell_type": "code", 807 | "execution_count": 29, 808 | "metadata": { 809 | "colab": { 810 | "base_uri": "https://localhost:8080/" 811 | }, 812 | "id": "dPZTkwfgeG5_", 813 | "outputId": "fdfb6480-854f-4eba-e028-264417bcde62" 814 | }, 815 | "outputs": [ 816 | { 817 | "name": "stdout", 818 | "output_type": "stream", 819 | "text": [ 820 | "BUY: GLD, $3.1748\n", 821 | "BUY: DBC, $0.0\n", 822 | "BUY: IEI, $13.2638\n", 823 | "BUY: VTI, $3.5612\n", 824 | "BUY: TLT, $0.0\n" 825 | ] 826 | } 827 | ], 828 | "source": [ 829 | "for allocation in newOrder2.goal.portfolio.allocations:\n", 830 | " print(newOrder2.transactionType.value + \": \" + allocation.ticker + \", $\" + str(allocation.percentage * newOrder2.dollarAmount))" 831 | ] 832 | }, 833 | { 834 | "cell_type": "code", 835 | "execution_count": 30, 836 | "metadata": { 837 | "colab": { 838 | "base_uri": "https://localhost:8080/" 839 | }, 840 | "id": "uqFNnUyweCU8", 841 | "outputId": "3f598024-c348-4d8e-ddf8-a311e9f939cd" 842 | }, 843 | "outputs": [ 844 | { 845 | "name": "stdout", 846 | "output_type": "stream", 847 | "text": [ 848 | "GLD, $3.1748\n", 849 | "IEI, $13.2638\n", 850 | "VTI, $3.5612\n" 851 | ] 852 | } 853 | ], 854 | "source": [ 855 | "for split in splitOrders2:\n", 856 | " print(split.ticker + \", $\" + str(split.dollarAmount))" 857 | ] 858 | }, 859 | { 860 | "cell_type": "code", 861 | "execution_count": 31, 862 | "metadata": { 863 | "colab": { 864 | "base_uri": "https://localhost:8080/" 865 | }, 866 | "id": "gG3DGklRVV-E", 867 | "outputId": "d0e4a377-0f0e-4469-ac0c-1baf505ff707" 868 | }, 869 | "outputs": [ 870 | { 871 | "name": "stdout", 872 | "output_type": "stream", 873 | "text": [ 874 | " Symbol Type DollarAmount\n", 875 | "0 GLD BUY 6.8606\n", 876 | "1 IEI BUY 15.9037\n", 877 | "2 TLT BUY 0.6466\n", 878 | "3 VTI BUY 6.5890\n" 879 | ] 880 | } 881 | ], 882 | "source": [ 883 | "# Aggregate second order\n", 884 | "for split in splitOrders2:\n", 885 | " newMasterOrder.addSplitOrder(split)\n", 886 | "newMasterTable = newMasterOrder.aggregate()\n", 887 | "print(newMasterTable)" 888 | ] 889 | }, 890 | { 891 | "cell_type": "code", 892 | "execution_count": 32, 893 | "metadata": { 894 | "id": "hIJwo6WtUo0V" 895 | }, 896 | "outputs": [], 897 | "source": [ 898 | "newMasterTable.to_csv('./Data/MasterOrder.csv')" 899 | ] 900 | }, 901 | { 902 | "cell_type": "code", 903 | "execution_count": 33, 904 | "metadata": { 905 | "colab": { 906 | "base_uri": "https://localhost:8080/" 907 | }, 908 | "id": "1_oxg4cHYOlB", 909 | "outputId": "ef409d56-c0a1-4d09-92c1-d20c8cf2af36" 910 | }, 911 | "outputs": [ 912 | { 913 | "name": "stdout", 914 | "output_type": "stream", 915 | "text": [ 916 | "PENDING\n" 917 | ] 918 | } 919 | ], 920 | "source": [ 921 | "# Send to broker via file/JSON/FTP\n", 922 | "newMasterOrder.orderSent()\n", 923 | "print(newMasterOrder.splitOrders[0].originalOrder.status.value)" 924 | ] 925 | }, 926 | { 927 | "cell_type": "code", 928 | "execution_count": 34, 929 | "metadata": { 930 | "colab": { 931 | "base_uri": "https://localhost:8080/" 932 | }, 933 | "id": "jN-ZoqWWYPmv", 934 | "outputId": "5709d399-697d-4559-8872-c53ac3b09d5e" 935 | }, 936 | "outputs": [ 937 | { 938 | "name": "stdout", 939 | "output_type": "stream", 940 | "text": [ 941 | " Symbol Type Units Price\n", 942 | "OrderID \n", 943 | "0 GLD BUY 0.041210 163.22\n", 944 | "1 IEI BUY 0.130827 115.30\n", 945 | "2 TLT BUY 0.009743 102.90\n", 946 | "3 VTI BUY 0.035659 201.54\n" 947 | ] 948 | } 949 | ], 950 | "source": [ 951 | "# Receive filled orders, read from CSV\n", 952 | "filledMasterOrder = pd.read_csv('./Data/MasterOrder_Filled.csv')\n", 953 | "filledMasterOrder = filledMasterOrder.set_index(filledMasterOrder.columns[0])\n", 954 | "print(filledMasterOrder)" 955 | ] 956 | }, 957 | { 958 | "cell_type": "code", 959 | "execution_count": 35, 960 | "metadata": { 961 | "colab": { 962 | "base_uri": "https://localhost:8080/" 963 | }, 964 | "id": "q3_k3XePb8jD", 965 | "outputId": "538324c2-f9cc-46b6-d6c3-eea45bb045ed" 966 | }, 967 | "outputs": [ 968 | { 969 | "name": "stdout", 970 | "output_type": "stream", 971 | "text": [ 972 | " Account Symbol Type DollarAmount\n", 973 | "0 123456789 GLD BUY 3.6858\n", 974 | "1 123456789 IEI BUY 2.6399\n", 975 | "2 123456789 VTI BUY 3.0278\n", 976 | "3 123456789 TLT BUY 0.6466\n", 977 | "4 987654321 GLD BUY 3.1748\n", 978 | "5 987654321 IEI BUY 13.2638\n", 979 | "6 987654321 VTI BUY 3.5612\n", 980 | " Account Symbol Type Units\n", 981 | "0 123456789 GLD BUY 0.02214\n", 982 | "1 123456789 IEI BUY 0.021716\n", 983 | "2 123456789 VTI BUY 0.016386\n", 984 | "3 123456789 TLT BUY 0.009743\n", 985 | "4 987654321 GLD BUY 0.01907\n", 986 | "5 987654321 IEI BUY 0.10911\n", 987 | "6 987654321 VTI BUY 0.019273\n" 988 | ] 989 | } 990 | ], 991 | "source": [ 992 | "# Allocate back units from master to split to order\n", 993 | "print(newMasterOrder.masterTable)\n", 994 | "accountAllocations = newMasterOrder.allocateAccounts(filledMasterOrder)\n", 995 | "print(accountAllocations)\n", 996 | "# Send accountAllocations to custodian somehow\n", 997 | "# NOTE: Any difference must be processed by rules set by broker (decimal places)" 998 | ] 999 | }, 1000 | { 1001 | "cell_type": "code", 1002 | "execution_count": 36, 1003 | "metadata": { 1004 | "colab": { 1005 | "base_uri": "https://localhost:8080/" 1006 | }, 1007 | "id": "qgRJc5amv-rE", 1008 | "outputId": "239f4f38-a742-4a5b-eac0-f86f6e1dc3a5" 1009 | }, 1010 | "outputs": [ 1011 | { 1012 | "name": "stdout", 1013 | "output_type": "stream", 1014 | "text": [ 1015 | "0.5773629503115147\n", 1016 | "1.4226370450281896\n" 1017 | ] 1018 | } 1019 | ], 1020 | "source": [ 1021 | "# Update cash balances for acct\n", 1022 | "print(myAccount.cashBalance)\n", 1023 | "print(myAccount2.cashBalance)" 1024 | ] 1025 | }, 1026 | { 1027 | "cell_type": "code", 1028 | "execution_count": 37, 1029 | "metadata": { 1030 | "id": "kjFoYQh1t7fV" 1031 | }, 1032 | "outputs": [], 1033 | "source": [ 1034 | "# UPDATE CUSTODIAN on individual account allocations\n", 1035 | "accountAllocations.to_csv('./Data/Account_Allocations.csv')" 1036 | ] 1037 | }, 1038 | { 1039 | "cell_type": "code", 1040 | "execution_count": 38, 1041 | "metadata": { 1042 | "id": "asJLjErgx7bL" 1043 | }, 1044 | "outputs": [], 1045 | "source": [ 1046 | "# Reconciliation from SOD file: updated cash positions vs. allocations" 1047 | ] 1048 | }, 1049 | { 1050 | "cell_type": "code", 1051 | "execution_count": 39, 1052 | "metadata": { 1053 | "colab": { 1054 | "base_uri": "https://localhost:8080/" 1055 | }, 1056 | "id": "coS_Es0QF3cR", 1057 | "outputId": "2d60d8e4-dc5b-4652-e8f4-c11b0bd1bfad" 1058 | }, 1059 | "outputs": [ 1060 | { 1061 | "name": "stdout", 1062 | "output_type": "stream", 1063 | "text": [ 1064 | "123456789, GLD, BUY, 3.6858000000000004, 0.02213974051911262\n", 1065 | "123456789, IEI, BUY, 2.6399, 0.02171626612838836\n", 1066 | "123456789, VTI, BUY, 3.0278, 0.0163863407640173\n", 1067 | "123456789, TLT, BUY, 0.6466, 0.009743440233\n", 1068 | "987654321, GLD, BUY, 3.1748, 0.019070282760887385\n", 1069 | "987654321, IEI, BUY, 13.2638, 0.10911027337161164\n", 1070 | "987654321, VTI, BUY, 3.5612, 0.019273081685982695\n" 1071 | ] 1072 | } 1073 | ], 1074 | "source": [ 1075 | "for split in newMasterOrder.splitOrders:\n", 1076 | " print(str(split.originalOrder.account.number) + \", \" + str(split.ticker) + \", \" + str(split.originalOrder.transactionType.value) + \", \" + str(split.dollarAmount) + \", \" + str(split.units))" 1077 | ] 1078 | }, 1079 | { 1080 | "cell_type": "code", 1081 | "execution_count": 40, 1082 | "metadata": { 1083 | "id": "f0a4RcyZKhLe" 1084 | }, 1085 | "outputs": [], 1086 | "source": [ 1087 | "newMasterOrder.allocateGoals()" 1088 | ] 1089 | }, 1090 | { 1091 | "cell_type": "code", 1092 | "execution_count": 41, 1093 | "metadata": { 1094 | "colab": { 1095 | "base_uri": "https://localhost:8080/" 1096 | }, 1097 | "id": "Zz1m1qb0tPMc", 1098 | "outputId": "26c67e7e-8053-4430-d4e2-5d926a569840" 1099 | }, 1100 | "outputs": [ 1101 | { 1102 | "name": "stdout", 1103 | "output_type": "stream", 1104 | "text": [ 1105 | "GLD: 0.019070282760887385\n" 1106 | ] 1107 | } 1108 | ], 1109 | "source": [ 1110 | "print(str(myGoal2.portfolio.allocations[0].ticker) + \": \" + str(myGoal2.portfolio.allocations[0].units))" 1111 | ] 1112 | }, 1113 | { 1114 | "cell_type": "code", 1115 | "execution_count": 42, 1116 | "metadata": { 1117 | "id": "endlOcKYHQhV" 1118 | }, 1119 | "outputs": [], 1120 | "source": [ 1121 | "newMasterOrder.orderFilled()" 1122 | ] 1123 | }, 1124 | { 1125 | "cell_type": "code", 1126 | "execution_count": 47, 1127 | "metadata": {}, 1128 | "outputs": [ 1129 | { 1130 | "name": "stdout", 1131 | "output_type": "stream", 1132 | "text": [ 1133 | "DBC, 0.0\n", 1134 | "GLD, 0.02213974051911262\n", 1135 | "IEI, 0.02171626612838836\n", 1136 | "VTI, 0.0163863407640173\n", 1137 | "TLT, 0.009743440233\n" 1138 | ] 1139 | } 1140 | ], 1141 | "source": [ 1142 | "for allocation in myPortfolio.allocations:\n", 1143 | " print(allocation.ticker + \", \" + str(allocation.units))" 1144 | ] 1145 | }, 1146 | { 1147 | "cell_type": "code", 1148 | "execution_count": 48, 1149 | "metadata": {}, 1150 | "outputs": [ 1151 | { 1152 | "name": "stdout", 1153 | "output_type": "stream", 1154 | "text": [ 1155 | "GLD, 0.019070282760887385\n", 1156 | "DBC, 0.0\n", 1157 | "IEI, 0.10911027337161164\n", 1158 | "VTI, 0.019273081685982695\n", 1159 | "TLT, 0.0\n" 1160 | ] 1161 | } 1162 | ], 1163 | "source": [ 1164 | "for allocation in myPortfolio2.allocations:\n", 1165 | " print(allocation.ticker + \", \" + str(allocation.units))" 1166 | ] 1167 | } 1168 | ], 1169 | "metadata": { 1170 | "colab": { 1171 | "provenance": [], 1172 | "toc_visible": true 1173 | }, 1174 | "kernelspec": { 1175 | "display_name": "Python 3", 1176 | "language": "python", 1177 | "name": "python3" 1178 | }, 1179 | "language_info": { 1180 | "codemirror_mode": { 1181 | "name": "ipython", 1182 | "version": 3 1183 | }, 1184 | "file_extension": ".py", 1185 | "mimetype": "text/x-python", 1186 | "name": "python", 1187 | "nbconvert_exporter": "python", 1188 | "pygments_lexer": "ipython3", 1189 | "version": "3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]" 1190 | }, 1191 | "vscode": { 1192 | "interpreter": { 1193 | "hash": "bf4014decd6db2e7270b0720910deef930124f2ac2cf190091d139b0105c57af" 1194 | } 1195 | } 1196 | }, 1197 | "nbformat": 4, 1198 | "nbformat_minor": 0 1199 | } 1200 | --------------------------------------------------------------------------------