├── LICENSE ├── README.md ├── about.py ├── app.py ├── assets ├── app.png ├── historic.csv └── mycss.css └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AnnMarieW 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 | # wealthdashboard.app 2 | 3 | See it live at [wealthdashboard.app](https://www.wealthdashboard.app/). 4 | 5 | 6 | __This app is featured in [The Book Of Dash](https://nostarch.com/book-dash) a step-by-step guide to making dashboards with Python, [Plotly and Dash.](https://dash.plotly.com/)__ 7 | 8 | If you would like to see the code as it appears in the book (and without the annoying ad :stuck_out_tongue: ) Please see the [book's github](https://github.com/DashBookProject/Plotly-Dash/tree/master/Chapter-6) 9 | 10 | 11 | ----- 12 | 13 | 14 | ![asset_allocation](https://user-images.githubusercontent.com/72614349/103412086-bf019f00-4b30-11eb-8420-d3b128b673dc.png) 15 | 16 | --------- 17 | 18 | ![The Book Of Dash](https://user-images.githubusercontent.com/72614349/185497519-733bdfc3-5731-4419-9a68-44c1cad04a78.png) 19 | 20 | ### [Order Your Copy Today!](https://nostarch.com/book-dash) -------------------------------------------------------------------------------- /about.py: -------------------------------------------------------------------------------- 1 | from dash import Dash, html, dcc 2 | import dash_bootstrap_components as dbc 3 | 4 | app = Dash(__name__, external_stylesheets=[dbc.themes.SPACELAB, dbc.icons.FONT_AWESOME]) 5 | 6 | book_img = "https://user-images.githubusercontent.com/72614349/185497519-733bdfc3-5731-4419-9a68-44c1cad04a78.png" 7 | nostarch = "https://nostarch.com/book-dash" 8 | github = "fa-brands fa-github" 9 | youtube = "fa-brands fa-youtube" 10 | info = "fa-solid fa-circle-info" 11 | plotly = "https://plotly.com/python/" 12 | dash_url = "https://dash.plotly.com/" 13 | plotly_logo = "https://user-images.githubusercontent.com/72614349/182969599-5ae4f531-ea01-4504-ac88-ee1c962c366d.png" 14 | plotly_logo_dark = "https://user-images.githubusercontent.com/72614349/182967824-c73218d8-acbf-4aab-b1ad-7eb35669b781.png" 15 | book_github = "https://github.com/DashBookProject/Plotly-Dash" 16 | amw = "https://github.com/AnnMarieW" 17 | adam = "https://www.youtube.com/c/CharmingData/featured" 18 | chris = "https://finxter.com/" 19 | 20 | 21 | def make_link(text, icon, link): 22 | return html.Span(html.A([html.I(className=icon + " ps-2"), text], href=link)) 23 | 24 | 25 | button = dbc.Button( 26 | "order", color="primary", href=nostarch, size="sm", className="mt-2 ms-1" 27 | ) 28 | 29 | cover_img = html.A( 30 | dbc.CardImg( 31 | src=book_img, 32 | className="img-fluid rounded-start", 33 | ), 34 | href=nostarch, 35 | ) 36 | 37 | text = dcc.Markdown( 38 | "Learn how to make this app in _The Book of Dash_ , a step-by-step" 39 | f" guide to making dashboards with Python, [Plotly and Dash.]({dash_url})", 40 | className="ps-2", 41 | ) 42 | 43 | see_github = html.Span( 44 | [ 45 | html.A([html.I(className=github + " p-1"), "GitHub"], href=book_github), 46 | ], 47 | className="lh-lg align-bottom", 48 | ) 49 | 50 | authors = html.P( 51 | [ 52 | "By ", 53 | make_link("Adam Schroeder ", youtube, adam), 54 | make_link("Christian Mayer", info, chris), 55 | make_link("Ann Marie Ward", github, amw), 56 | ], 57 | className="card-text p-2", 58 | ) 59 | 60 | card = dbc.Card( 61 | [ 62 | dbc.Row( 63 | [ 64 | dbc.Col(cover_img, width=2), 65 | dbc.Col( 66 | [text, button, see_github], 67 | width=10, 68 | ), 69 | ], 70 | className="g-0 d-flex align-items-center", 71 | ), 72 | dbc.Row(dbc.Col(authors)), 73 | ], 74 | className="my-5 small shadow", 75 | style={"maxWidth": "32rem"}, 76 | ) 77 | 78 | app.layout = dbc.Container(card, fluid=True) 79 | 80 | if __name__ == "__main__": 81 | app.run_server(debug=True) 82 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from dash import Dash, dcc, html, dash_table, Input, Output, State, callback_context 3 | import dash_bootstrap_components as dbc 4 | import plotly.graph_objects as go 5 | import pandas as pd 6 | import about 7 | 8 | app_description = """ 9 | How does asset allocation affect portfolio performance? Select the percentage of stocks, bonds and cash 10 | in a portfolio and see annual returns over any time period from 1928 to 2021. 11 | """ 12 | app_title = "Asset Allocation Visualizer" 13 | app_image = "https://www.wealthdashboard.app/assets/app.png" 14 | 15 | metas = [ 16 | {"name": "viewport", "content": "width=device-width, initial-scale=1"}, 17 | {"property": "twitter:card", "content": "summary_large_image"}, 18 | {"property": "twitter:url", "content": "https://www.wealthdashboard.app/"}, 19 | {"property": "twitter:title", "content": app_title}, 20 | {"property": "twitter:description", "content": app_description}, 21 | {"property": "twitter:image", "content": app_image}, 22 | {"property": "og:title", "content": app_title}, 23 | {"property": "og:type", "content": "website"}, 24 | {"property": "og:description", "content": app_description}, 25 | {"property": "og:image", "content": app_image}, 26 | ] 27 | 28 | app = Dash( 29 | __name__, 30 | external_stylesheets=[dbc.themes.SPACELAB, dbc.icons.FONT_AWESOME], 31 | meta_tags=metas, 32 | title=app_title, 33 | ) 34 | 35 | # make dataframe from spreadsheet: 36 | df = pd.read_csv("assets/historic.csv") 37 | 38 | MAX_YR = df.Year.max() 39 | MIN_YR = df.Year.min() 40 | START_YR = 2007 41 | 42 | # since data is as of year end, need to add start year 43 | df = ( 44 | pd.concat([df, pd.DataFrame([{"Year": MIN_YR - 1}])], ignore_index=True) 45 | .sort_values("Year", ignore_index=True) 46 | .fillna(0) 47 | ) 48 | 49 | COLORS = { 50 | "cash": "#3cb521", 51 | "bonds": "#fd7e14", 52 | "stocks": "#446e9b", 53 | "inflation": "#cd0200", 54 | "background": "whitesmoke", 55 | } 56 | 57 | """ 58 | ========================================================================== 59 | Markdown Text 60 | """ 61 | 62 | datasource_text = dcc.Markdown( 63 | """ 64 | [Data source:](http://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/histretSP.html) 65 | Historical Returns on Stocks, Bonds and Bills from NYU Stern School of 66 | Business 67 | """ 68 | ) 69 | 70 | asset_allocation_text = dcc.Markdown( 71 | """ 72 | > **Asset allocation** is one of the main factors that drive portfolio risk and returns. Play with the app and see for yourself! 73 | 74 | > Change the allocation to cash, bonds and stocks on the sliders and see how your portfolio performs over time in the graph. 75 | Try entering different time periods and dollar amounts too. 76 | """ 77 | ) 78 | 79 | learn_text = dcc.Markdown( 80 | """ 81 | Past performance certainly does not determine future results, but you can still 82 | learn a lot by reviewing how various asset classes have performed over time. 83 | 84 | Use the sliders to change the asset allocation (how much you invest in cash vs 85 | bonds vs stock) and see how this affects your returns. 86 | 87 | Note that the results shown in "My Portfolio" assumes rebalancing was done at 88 | the beginning of every year. Also, this information is based on the S&P 500 index 89 | as a proxy for "stocks", the 10 year US Treasury Bond for "bonds" and the 3 month 90 | US Treasury Bill for "cash." Your results of course, would be different based 91 | on your actual holdings. 92 | 93 | This is intended to help you determine your investment philosophy and understand 94 | what sort of risks and returns you might see for each asset category. 95 | 96 | The data is from [Aswath Damodaran](http://people.stern.nyu.edu/adamodar/New_Home_Page/home.htm) 97 | who teaches corporate finance and valuation at the Stern School of Business 98 | at New York University. 99 | 100 | Check out his excellent on-line course in 101 | [Investment Philosophies.](http://people.stern.nyu.edu/adamodar/New_Home_Page/webcastinvphil.htm) 102 | """ 103 | ) 104 | 105 | cagr_text = dcc.Markdown( 106 | """ 107 | (CAGR) is the compound annual growth rate. It measures the rate of return for an investment over a period of time, 108 | such as 5 or 10 years. The CAGR is also called a "smoothed" rate of return because it measures the growth of 109 | an investment as if it had grown at a steady rate on an annually compounded basis. 110 | """ 111 | ) 112 | 113 | footer = html.Div( 114 | [ 115 | dcc.Markdown( 116 | """ 117 | This information is intended solely as general information for educational 118 | and entertainment purposes only and is not a substitute for professional advice and 119 | services from qualified financial services providers familiar with your financial 120 | situation. 121 | """ 122 | ), 123 | 124 | ], 125 | className="p-2 mt-5 bg-primary text-white small", 126 | ) 127 | 128 | """ 129 | ========================================================================== 130 | Tables 131 | """ 132 | 133 | total_returns_table = dash_table.DataTable( 134 | id="total_returns", 135 | columns=[{"id": "Year", "name": "Year", "type": "text"}] 136 | + [ 137 | {"id": col, "name": col, "type": "numeric", "format": {"specifier": "$,.0f"}} 138 | for col in ["Cash", "Bonds", "Stocks", "Total"] 139 | ], 140 | page_size=15, 141 | style_table={"overflowX": "scroll"}, 142 | ) 143 | 144 | annual_returns_pct_table = dash_table.DataTable( 145 | id="annual_returns_pct", 146 | columns=( 147 | [{"id": "Year", "name": "Year", "type": "text"}] 148 | + [ 149 | {"id": col, "name": col, "type": "numeric", "format": {"specifier": ".1%"}} 150 | for col in df.columns[1:] 151 | ] 152 | ), 153 | data=df.to_dict("records"), 154 | sort_action="native", 155 | page_size=15, 156 | style_table={"overflowX": "scroll"}, 157 | ) 158 | 159 | 160 | def make_summary_table(dff): 161 | """Make html table to show cagr and best and worst periods""" 162 | 163 | table_class = "h5 text-body text-nowrap" 164 | cash = html.Span( 165 | [html.I(className="fa fa-money-bill-alt"), " Cash"], className=table_class 166 | ) 167 | bonds = html.Span( 168 | [html.I(className="fa fa-handshake"), " Bonds"], className=table_class 169 | ) 170 | stocks = html.Span( 171 | [html.I(className="fa fa-industry"), " Stocks"], className=table_class 172 | ) 173 | inflation = html.Span( 174 | [html.I(className="fa fa-ambulance"), " Inflation"], className=table_class 175 | ) 176 | 177 | start_yr = dff["Year"].iat[0] 178 | end_yr = dff["Year"].iat[-1] 179 | 180 | df_table = pd.DataFrame( 181 | { 182 | "": [cash, bonds, stocks, inflation], 183 | f"Rate of Return (CAGR) from {start_yr} to {end_yr}": [ 184 | cagr(dff["all_cash"]), 185 | cagr(dff["all_bonds"]), 186 | cagr(dff["all_stocks"]), 187 | cagr(dff["inflation_only"]), 188 | ], 189 | f"Worst 1 Year Return": [ 190 | worst(dff, "3-mon T.Bill"), 191 | worst(dff, "10yr T.Bond"), 192 | worst(dff, "S&P 500"), 193 | "", 194 | ], 195 | } 196 | ) 197 | return dbc.Table.from_dataframe(df_table, bordered=True, hover=True) 198 | 199 | 200 | """ 201 | ========================================================================== 202 | Figures 203 | """ 204 | 205 | 206 | def make_pie(slider_input, title): 207 | fig = go.Figure( 208 | data=[ 209 | go.Pie( 210 | labels=["Cash", "Bonds", "Stocks"], 211 | values=slider_input, 212 | textinfo="label+percent", 213 | textposition="inside", 214 | marker={"colors": [COLORS["cash"], COLORS["bonds"], COLORS["stocks"]]}, 215 | sort=False, 216 | hoverinfo="none", 217 | ) 218 | ] 219 | ) 220 | fig.update_layout( 221 | title_text=title, 222 | title_x=0.5, 223 | margin=dict(b=25, t=75, l=35, r=25), 224 | height=325, 225 | paper_bgcolor=COLORS["background"], 226 | ) 227 | return fig 228 | 229 | 230 | def make_line_chart(dff): 231 | start = dff.loc[1, "Year"] 232 | yrs = dff["Year"].size - 1 233 | dtick = 1 if yrs < 16 else 2 if yrs in range(16, 30) else 5 234 | 235 | fig = go.Figure() 236 | fig.add_trace( 237 | go.Scatter( 238 | x=dff["Year"], 239 | y=dff["all_cash"], 240 | name="All Cash", 241 | marker_color=COLORS["cash"], 242 | ) 243 | ) 244 | fig.add_trace( 245 | go.Scatter( 246 | x=dff["Year"], 247 | y=dff["all_bonds"], 248 | name="All Bonds (10yr T.Bonds)", 249 | marker_color=COLORS["bonds"], 250 | ) 251 | ) 252 | fig.add_trace( 253 | go.Scatter( 254 | x=dff["Year"], 255 | y=dff["all_stocks"], 256 | name="All Stocks (S&P500)", 257 | marker_color=COLORS["stocks"], 258 | ) 259 | ) 260 | fig.add_trace( 261 | go.Scatter( 262 | x=dff["Year"], 263 | y=dff["Total"], 264 | name="My Portfolio", 265 | marker_color="black", 266 | line=dict(width=6, dash="dot"), 267 | ) 268 | ) 269 | fig.add_trace( 270 | go.Scatter( 271 | x=dff["Year"], 272 | y=dff["inflation_only"], 273 | name="Inflation", 274 | visible=True, 275 | marker_color=COLORS["inflation"], 276 | ) 277 | ) 278 | fig.update_layout( 279 | title=f"Returns for {yrs} years starting {start}", 280 | template="none", 281 | showlegend=True, 282 | legend=dict(x=0.01, y=0.99), 283 | height=400, 284 | margin=dict(l=40, r=10, t=60, b=55), 285 | yaxis=dict(tickprefix="$", fixedrange=True), 286 | xaxis=dict(title="Year Ended", fixedrange=True, dtick=dtick), 287 | ) 288 | return fig 289 | 290 | 291 | """ 292 | ========================================================================== 293 | Make Tabs 294 | """ 295 | 296 | # =======Play tab components 297 | 298 | asset_allocation_card = dbc.Card(asset_allocation_text, className="mt-2") 299 | 300 | slider_card = dbc.Card( 301 | [ 302 | html.H4("First set cash allocation %:", className="card-title"), 303 | dcc.Slider( 304 | id="cash", 305 | marks={i: f"{i}%" for i in range(0, 101, 10)}, 306 | min=0, 307 | max=100, 308 | step=5, 309 | value=10, 310 | included=False, 311 | ), 312 | html.H4( 313 | "Then set stock allocation % ", 314 | className="card-title mt-3", 315 | ), 316 | html.Div("(The rest will be bonds)", className="card-title"), 317 | dcc.Slider( 318 | id="stock_bond", 319 | marks={i: f"{i}%" for i in range(0, 91, 10)}, 320 | min=0, 321 | max=90, 322 | step=5, 323 | value=50, 324 | included=False, 325 | ), 326 | ], 327 | body=True, 328 | className="mt-4", 329 | ) 330 | 331 | 332 | time_period_data = [ 333 | { 334 | "label": f"2007-2008: Great Financial Crisis to {MAX_YR}", 335 | "start_yr": 2007, 336 | "planning_time": MAX_YR - START_YR + 1, 337 | }, 338 | { 339 | "label": "1999-2010: The decade including 2000 Dotcom Bubble peak", 340 | "start_yr": 1999, 341 | "planning_time": 10, 342 | }, 343 | { 344 | "label": "1969-1979: The 1970s Energy Crisis", 345 | "start_yr": 1970, 346 | "planning_time": 10, 347 | }, 348 | { 349 | "label": "1929-1948: The 20 years following the start of the Great Depression", 350 | "start_yr": 1929, 351 | "planning_time": 20, 352 | }, 353 | { 354 | "label": f"{MIN_YR}-{MAX_YR}", 355 | "start_yr": "1928", 356 | "planning_time": MAX_YR - MIN_YR + 1, 357 | }, 358 | ] 359 | 360 | 361 | time_period_card = dbc.Card( 362 | [ 363 | html.H4( 364 | "Or select a time period:", 365 | className="card-title", 366 | ), 367 | dbc.RadioItems( 368 | id="time_period", 369 | options=[ 370 | {"label": period["label"], "value": i} 371 | for i, period in enumerate(time_period_data) 372 | ], 373 | value=0, 374 | labelClassName="mb-2", 375 | ), 376 | ], 377 | body=True, 378 | className="mt-4", 379 | ) 380 | 381 | # ======= InputGroup components 382 | 383 | start_amount = dbc.InputGroup( 384 | [ 385 | dbc.InputGroupText("Start Amount $"), 386 | dbc.Input( 387 | id="starting_amount", 388 | placeholder="Min $10", 389 | type="number", 390 | min=10, 391 | value=10000, 392 | ), 393 | ], 394 | className="mb-3", 395 | ) 396 | start_year = dbc.InputGroup( 397 | [ 398 | dbc.InputGroupText("Start Year"), 399 | dbc.Input( 400 | id="start_yr", 401 | placeholder=f"min {MIN_YR} max {MAX_YR}", 402 | type="number", 403 | min=MIN_YR, 404 | max=MAX_YR, 405 | value=START_YR, 406 | ), 407 | ], 408 | className="mb-3", 409 | ) 410 | number_of_years = dbc.InputGroup( 411 | [ 412 | dbc.InputGroupText("Number of Years:"), 413 | dbc.Input( 414 | id="planning_time", 415 | placeholder="# yrs", 416 | type="number", 417 | min=1, 418 | value=MAX_YR - START_YR + 1, 419 | ), 420 | ], 421 | className="mb-3", 422 | ) 423 | end_amount = dbc.InputGroup( 424 | [ 425 | dbc.InputGroupText("Ending Amount"), 426 | dbc.Input(id="ending_amount", disabled=True, className="text-black"), 427 | ], 428 | className="mb-3", 429 | ) 430 | rate_of_return = dbc.InputGroup( 431 | [ 432 | dbc.InputGroupText( 433 | "Rate of Return(CAGR)", 434 | id="tooltip_target", 435 | className="text-decoration-underline", 436 | ), 437 | dbc.Input(id="cagr", disabled=True, className="text-black"), 438 | dbc.Tooltip(cagr_text, target="tooltip_target"), 439 | ], 440 | className="mb-3", 441 | ) 442 | 443 | input_groups = html.Div( 444 | [start_amount, start_year, number_of_years, end_amount, rate_of_return], 445 | className="mt-4 p-4", 446 | ) 447 | 448 | 449 | # ===== Results Tab components 450 | 451 | results_card = dbc.Card( 452 | [ 453 | dbc.CardHeader("My Portfolio Returns - Rebalanced Annually"), 454 | html.Div(total_returns_table), 455 | ], 456 | className="mt-4", 457 | ) 458 | 459 | 460 | data_source_card = dbc.Card( 461 | [ 462 | dbc.CardHeader("Source Data: Annual Total Returns"), 463 | html.Div(annual_returns_pct_table), 464 | ], 465 | className="mt-4", 466 | ) 467 | 468 | 469 | # ========= Learn Tab Components 470 | learn_card = dbc.Card( 471 | [ 472 | dbc.CardHeader("An Introduction to Asset Allocation"), 473 | dbc.CardBody(learn_text), 474 | ], 475 | className="mt-4", 476 | ) 477 | 478 | 479 | # ========= Build tabs 480 | tabs = dbc.Tabs( 481 | [ 482 | dbc.Tab(learn_card, tab_id="tab1", label="Learn"), 483 | dbc.Tab( 484 | [asset_allocation_text, slider_card, input_groups, time_period_card], 485 | tab_id="tab-2", 486 | label="Play", 487 | className="pb-4", 488 | ), 489 | dbc.Tab([results_card, data_source_card], tab_id="tab-3", label="Results"), 490 | ], 491 | id="tabs", 492 | active_tab="tab-2", 493 | className="mt-2", 494 | ) 495 | 496 | 497 | """ 498 | ========================================================================== 499 | Helper functions to calculate investment results, cagr and worst periods 500 | """ 501 | 502 | 503 | def backtest(stocks, cash, start_bal, nper, start_yr): 504 | """calculates the investment returns for user selected asset allocation, 505 | rebalanced annually and returns a dataframe 506 | """ 507 | 508 | end_yr = start_yr + nper - 1 509 | cash_allocation = cash / 100 510 | stocks_allocation = stocks / 100 511 | bonds_allocation = (100 - stocks - cash) / 100 512 | 513 | # Select time period - since data is for year end, include year prior 514 | # for start ie year[0] 515 | dff = df[(df.Year >= start_yr - 1) & (df.Year <= end_yr)].set_index( 516 | "Year", drop=False 517 | ) 518 | dff["Year"] = dff["Year"].astype(int) 519 | 520 | # add columns for My Portfolio returns 521 | dff["Cash"] = cash_allocation * start_bal 522 | dff["Bonds"] = bonds_allocation * start_bal 523 | dff["Stocks"] = stocks_allocation * start_bal 524 | dff["Total"] = start_bal 525 | dff["Total"] = dff["Total"].astype(float) 526 | dff["Rebalance"] = True 527 | 528 | # calculate My Portfolio returns 529 | for yr in dff.Year + 1: 530 | if yr <= end_yr: 531 | # Rebalance at the beginning of the period by reallocating 532 | # last period's total ending balance 533 | if dff.loc[yr, "Rebalance"]: 534 | dff.loc[yr, "Cash"] = dff.loc[yr - 1, "Total"] * cash_allocation 535 | dff.loc[yr, "Stocks"] = dff.loc[yr - 1, "Total"] * stocks_allocation 536 | dff.loc[yr, "Bonds"] = dff.loc[yr - 1, "Total"] * bonds_allocation 537 | 538 | # calculate this period's returns 539 | dff.loc[yr, "Cash"] = dff.loc[yr, "Cash"] * ( 540 | 1 + dff.loc[yr, "3-mon T.Bill"] 541 | ) 542 | dff.loc[yr, "Stocks"] = dff.loc[yr, "Stocks"] * (1 + dff.loc[yr, "S&P 500"]) 543 | dff.loc[yr, "Bonds"] = dff.loc[yr, "Bonds"] * ( 544 | 1 + dff.loc[yr, "10yr T.Bond"] 545 | ) 546 | dff.loc[yr, "Total"] = dff.loc[yr, ["Cash", "Bonds", "Stocks"]].sum() 547 | 548 | dff = dff.reset_index(drop=True) 549 | columns = ["Cash", "Stocks", "Bonds", "Total"] 550 | dff[columns] = dff[columns].round(0) 551 | 552 | # create columns for when portfolio is all cash, all bonds or all stocks, 553 | # include inflation too 554 | # 555 | # create new df that starts in yr 1 rather than yr 0 556 | dff1 = (dff[(dff.Year >= start_yr) & (dff.Year <= end_yr)]).copy() 557 | # 558 | # calculate the returns in new df: 559 | columns = ["all_cash", "all_bonds", "all_stocks", "inflation_only"] 560 | annual_returns = ["3-mon T.Bill", "10yr T.Bond", "S&P 500", "Inflation"] 561 | for col, return_pct in zip(columns, annual_returns): 562 | dff1[col] = round(start_bal * (1 + (1 + dff1[return_pct]).cumprod() - 1), 0) 563 | # 564 | # select columns in the new df to merge with original 565 | dff1 = dff1[["Year"] + columns] 566 | dff = dff.merge(dff1, how="left") 567 | # fill in the starting balance for year[0] 568 | dff.loc[0, columns] = start_bal 569 | return dff 570 | 571 | 572 | def cagr(dff): 573 | """calculate Compound Annual Growth Rate for a series and returns a formated string""" 574 | 575 | start_bal = dff.iat[0] 576 | end_bal = dff.iat[-1] 577 | planning_time = len(dff) - 1 578 | cagr_result = ((end_bal / start_bal) ** (1 / planning_time)) - 1 579 | return f"{cagr_result:.1%}" 580 | 581 | 582 | def worst(dff, asset): 583 | """calculate worst returns for asset in selected period returns formated string""" 584 | 585 | worst_yr_loss = min(dff[asset]) 586 | worst_yr = dff.loc[dff[asset] == worst_yr_loss, "Year"].iloc[0] 587 | return f"{worst_yr_loss:.1%} in {worst_yr}" 588 | 589 | 590 | """ 591 | =========================================================================== 592 | Main Layout 593 | """ 594 | 595 | app.layout = dbc.Container( 596 | [ 597 | dbc.Row( 598 | dbc.Col( 599 | html.H2( 600 | "Asset Allocation Visualizer", 601 | className="text-center bg-primary text-white p-2", 602 | ), 603 | ) 604 | ), 605 | dbc.Row( 606 | [ 607 | dbc.Col(tabs, width=12, lg=5, className="mt-4 border"), 608 | dbc.Col( 609 | [ 610 | dcc.Graph(id="allocation_pie_chart", className="mb-2"), 611 | dcc.Graph(id="returns_chart", className="pb-4"), 612 | html.Hr(), 613 | html.Div(id="summary_table"), 614 | html.H6(datasource_text, className="my-2"), 615 | ], 616 | width=12, 617 | lg=7, 618 | className="pt-4", 619 | ), 620 | ], 621 | className="ms-1", 622 | ), 623 | dbc.Row(dbc.Col(footer)), 624 | dbc.Row(dbc.Col(about.card, width="auto"), justify="center") 625 | ], 626 | fluid=True, 627 | ) 628 | 629 | 630 | """ 631 | ========================================================================== 632 | Callbacks 633 | """ 634 | 635 | 636 | @app.callback( 637 | Output("allocation_pie_chart", "figure"), 638 | Input("stock_bond", "value"), 639 | Input("cash", "value"), 640 | ) 641 | def update_pie(stocks, cash): 642 | bonds = 100 - stocks - cash 643 | slider_input = [cash, bonds, stocks] 644 | 645 | if stocks >= 70: 646 | investment_style = "Aggressive" 647 | elif stocks <= 30: 648 | investment_style = "Conservative" 649 | else: 650 | investment_style = "Moderate" 651 | figure = make_pie(slider_input, investment_style + " Asset Allocation") 652 | return figure 653 | 654 | 655 | @app.callback( 656 | Output("stock_bond", "max"), 657 | Output("stock_bond", "marks"), 658 | Output("stock_bond", "value"), 659 | Input("cash", "value"), 660 | State("stock_bond", "value"), 661 | ) 662 | def update_stock_slider(cash, initial_stock_value): 663 | max_slider = 100 - int(cash) 664 | stocks = min(max_slider, initial_stock_value) 665 | 666 | # formats the slider scale 667 | if max_slider > 50: 668 | marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 10)} 669 | elif max_slider <= 15: 670 | marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 1)} 671 | else: 672 | marks_slider = {i: f"{i}%" for i in range(0, max_slider + 1, 5)} 673 | return max_slider, marks_slider, stocks 674 | 675 | 676 | @app.callback( 677 | Output("planning_time", "value"), 678 | Output("start_yr", "value"), 679 | Output("time_period", "value"), 680 | Input("planning_time", "value"), 681 | Input("start_yr", "value"), 682 | Input("time_period", "value"), 683 | ) 684 | def update_time_period(planning_time, start_yr, period_number): 685 | """syncs inputs and selected time periods""" 686 | ctx = callback_context 687 | input_id = ctx.triggered[0]["prop_id"].split(".")[0] 688 | 689 | if input_id == "time_period": 690 | planning_time = time_period_data[period_number]["planning_time"] 691 | start_yr = time_period_data[period_number]["start_yr"] 692 | 693 | if input_id in ["planning_time", "start_yr"]: 694 | period_number = None 695 | 696 | return planning_time, start_yr, period_number 697 | 698 | 699 | @app.callback( 700 | Output("total_returns", "data"), 701 | Output("returns_chart", "figure"), 702 | Output("summary_table", "children"), 703 | Output("ending_amount", "value"), 704 | Output("cagr", "value"), 705 | Input("stock_bond", "value"), 706 | Input("cash", "value"), 707 | Input("starting_amount", "value"), 708 | Input("planning_time", "value"), 709 | Input("start_yr", "value"), 710 | ) 711 | def update_totals(stocks, cash, start_bal, planning_time, start_yr): 712 | # set defaults for invalid inputs 713 | start_bal = 10 if start_bal is None else start_bal 714 | planning_time = 1 if planning_time is None else planning_time 715 | start_yr = MIN_YR if start_yr is None else int(start_yr) 716 | 717 | # calculate valid planning time start yr 718 | max_time = MAX_YR + 1 - start_yr 719 | planning_time = min(max_time, planning_time) 720 | if start_yr + planning_time > MAX_YR: 721 | start_yr = min(df.iloc[-planning_time, 0], MAX_YR) # 0 is Year column 722 | 723 | # create investment returns dataframe 724 | dff = backtest(stocks, cash, start_bal, planning_time, start_yr) 725 | 726 | # create data for DataTable 727 | data = dff.to_dict("records") 728 | 729 | # create the line chart 730 | fig = make_line_chart(dff) 731 | 732 | summary_table = make_summary_table(dff) 733 | 734 | # format ending balance 735 | ending_amount = f"${dff['Total'].iloc[-1]:0,.0f}" 736 | 737 | # calcluate cagr 738 | ending_cagr = cagr(dff["Total"]) 739 | 740 | return data, fig, summary_table, ending_amount, ending_cagr 741 | 742 | 743 | if __name__ == "__main__": 744 | app.run_server(debug=True) 745 | -------------------------------------------------------------------------------- /assets/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnnMarieW/wealthdashboard/3496e06300018b6c38bb4729140187dafb808b87/assets/app.png -------------------------------------------------------------------------------- /assets/historic.csv: -------------------------------------------------------------------------------- 1 | Year,S&P 500,3-mon T.Bill,10yr T.Bond, Baa Corp Bond,Inflation 2 | 1928,0.4381,0.0308,0.0084,0.0322,-0.0116 3 | 1929,-0.083,0.0316,0.042,0.0302,0.0058 4 | 1930,-0.2512,0.0455,0.0454,0.0054,-0.0640 5 | 1931,-0.4384,0.0231,-0.0256,-0.1568,-0.0932 6 | 1932,-0.0864,0.0107,0.0879,0.2359,-0.1027 7 | 1933,0.4998,0.0096,0.0186,0.1297,0.0076 8 | 1934,-0.0119,0.0028,0.0796,0.1882,0.0152 9 | 1935,0.4674,0.0017,0.0447,0.1331,0.0299 10 | 1936,0.3194,0.0017,0.0502,0.1138,0.0145 11 | 1937,-0.3534,0.0028,0.0138,-0.0442,0.0286 12 | 1938,0.2928,0.0007,0.0421,0.0924,-0.0278 13 | 1939,-0.011,0.0005,0.0441,0.0798,0.0000 14 | 1940,-0.1067,0.0004,0.054,0.0865,0.0071 15 | 1941,-0.1277,0.0013,-0.0202,0.0501,0.0993 16 | 1942,0.1917,0.0034,0.0229,0.0518,0.0903 17 | 1943,0.2506,0.0038,0.0249,0.0804,0.0296 18 | 1944,0.1903,0.0038,0.0258,0.0657,0.0230 19 | 1945,0.3582,0.0038,0.038,0.068,0.0225 20 | 1946,-0.0843,0.0038,0.0313,0.0251,0.1813 21 | 1947,0.052,0.006,0.0092,0.0026,0.0884 22 | 1948,0.057,0.0105,0.0195,0.0344,0.0273 23 | 1949,0.183,0.0112,0.0466,0.0538,-0.0183 24 | 1950,0.3081,0.012,0.0043,0.0424,0.0580 25 | 1951,0.2368,0.0152,-0.003,-0.0019,0.0596 26 | 1952,0.1815,0.0172,0.0227,0.0444,0.0091 27 | 1953,-0.0121,0.0189,0.0414,0.0162,0.0060 28 | 1954,0.5256,0.0094,0.0329,0.0616,-0.0037 29 | 1955,0.326,0.0173,-0.0134,0.0204,0.0037 30 | 1956,0.0744,0.0263,-0.0226,-0.0235,0.0283 31 | 1957,-0.1046,0.0323,0.068,-0.0072,0.0304 32 | 1958,0.4372,0.0177,-0.021,0.0643,0.0176 33 | 1959,0.1206,0.0339,-0.0265,0.0157,0.0152 34 | 1960,0.0034,0.0288,0.1164,0.0666,0.0136 35 | 1961,0.2664,0.0235,0.0206,0.051,0.0067 36 | 1962,-0.0881,0.0277,0.0569,0.065,0.0123 37 | 1963,0.2261,0.0316,0.0168,0.0546,0.0165 38 | 1964,0.1642,0.0355,0.0373,0.0516,0.0120 39 | 1965,0.124,0.0395,0.0072,0.0319,0.0192 40 | 1966,-0.0997,0.0486,0.0291,-0.0345,0.0336 41 | 1967,0.238,0.0431,-0.0158,0.009,0.0328 42 | 1968,0.1081,0.0534,0.0327,0.0485,0.0471 43 | 1969,-0.0824,0.0667,-0.0501,-0.0203,0.0590 44 | 1970,0.0356,0.0639,0.1675,0.0565,0.0557 45 | 1971,0.1422,0.0433,0.0979,0.14,0.0327 46 | 1972,0.1876,0.0407,0.0282,0.1141,0.0341 47 | 1973,-0.1431,0.0703,0.0366,0.0432,0.0894 48 | 1974,-0.259,0.0783,0.0199,-0.0438,0.1210 49 | 1975,0.37,0.0578,0.0361,0.1105,0.0713 50 | 1976,0.2383,0.0497,0.1598,0.1975,0.0504 51 | 1977,-0.0698,0.0527,0.0129,0.0995,0.0668 52 | 1978,0.0651,0.0719,-0.0078,0.0314,0.0899 53 | 1979,0.1852,0.1007,0.0067,-0.0201,0.1325 54 | 1980,0.3174,0.1143,-0.0299,-0.0332,0.1235 55 | 1981,-0.047,0.1403,0.082,0.0846,0.0891 56 | 1982,0.2042,0.1061,0.3281,0.2905,0.0383 57 | 1983,0.2234,0.0861,0.032,0.1619,0.0379 58 | 1984,0.0615,0.0952,0.1373,0.1562,0.0404 59 | 1985,0.3124,0.0748,0.2571,0.2386,0.0379 60 | 1986,0.1849,0.0598,0.2428,0.2149,0.0119 61 | 1987,0.0581,0.0578,-0.0496,0.0229,0.0433 62 | 1988,0.1654,0.0667,0.0822,0.1512,0.0441 63 | 1989,0.3148,0.0811,0.1769,0.1579,0.0464 64 | 1990,-0.0306,0.0749,0.0624,0.0614,0.0625 65 | 1991,0.3023,0.0538,0.15,0.1785,0.0298 66 | 1992,0.0749,0.0343,0.0936,0.1217,0.0297 67 | 1993,0.0997,0.03,0.1421,0.1643,0.0281 68 | 1994,0.0133,0.0425,-0.0804,-0.0132,0.0260 69 | 1995,0.372,0.0549,0.2348,0.2016,0.0253 70 | 1996,0.2268,0.0501,0.0143,0.0479,0.0338 71 | 1997,0.331,0.0506,0.0994,0.1183,0.0170 72 | 1998,0.2834,0.0478,0.1492,0.0795,0.0161 73 | 1999,0.2089,0.0464,-0.0825,0.0084,0.0268 74 | 2000,-0.0903,0.0582,0.1666,0.0933,0.0344 75 | 2001,-0.1185,0.0339,0.0557,0.0782,0.0160 76 | 2002,-0.2197,0.016,0.1512,0.1218,0.0248 77 | 2003,0.2836,0.0101,0.0038,0.1353,0.0204 78 | 2004,0.1074,0.0137,0.0449,0.0989,0.0334 79 | 2005,0.0483,0.0315,0.0287,0.0492,0.0334 80 | 2006,0.1561,0.0473,0.0196,0.0705,0.0252 81 | 2007,0.0548,0.0435,0.1021,0.0315,0.0411 82 | 2008,-0.3655,0.0137,0.201,-0.0507,-0.0002 83 | 2009,0.2594,0.0015,-0.1112,0.2333,0.0281 84 | 2010,0.1482,0.0014,0.0846,0.0835,0.0144 85 | 2011,0.021,0.0005,0.1604,0.1258,0.0306 86 | 2012,0.1589,0.0009,0.0297,0.1012,0.0176 87 | 2013,0.3215,0.0006,-0.091,-0.0106,0.0151 88 | 2014,0.1352,0.0003,0.1075,0.1038,0.0065 89 | 2015,0.0138,0.0005,0.0128,-0.007,0.0064 90 | 2016,0.1177,0.0032,0.0069,0.1037,0.0205 91 | 2017,0.2161,0.0093,0.028,0.0972,0.0213 92 | 2018,-0.0423,0.0194,-0.0002,-0.0276,0.0200 93 | 2019,0.3121,0.0206,0.0964,0.1533,0.0231 94 | 2020,0.1802,0.0035,0.1133,0.1041,0.0132 95 | 2021,0.2847,0.0005,-0.0443,0.0093,0.0719 96 | 2022,-0.1804,0.0202,-0.1783,-0.1514,0.0644 97 | 2023,0.2606,0.0507,0.0388,0.0874,0.0312 98 | 2024,0.2488,0.0497,-0.0164,0.0174,0.0275 -------------------------------------------------------------------------------- /assets/mycss.css: -------------------------------------------------------------------------------- 1 | .dash-table-container .row { 2 | display: block; 3 | margin: 0; 4 | } 5 | 6 | .input-group-text { 7 | width: 175px !important; 8 | } 9 | 10 | blockquote { 11 | border-left: 4px var(--bs-primary) solid; 12 | padding-left: 1rem; 13 | margin-top: 2rem; 14 | margin-bottom: 2rem; 15 | margin-left: 0rem; 16 | } 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dash>=2.0.0 2 | pandas 3 | dash-bootstrap-components>=1.0.0b3 4 | openpyxl 5 | 6 | 7 | --------------------------------------------------------------------------------