This commit is contained in:
Harry Stuart 2024-08-16 08:26:12 +10:00
parent 7a60da2a8b
commit f73c8120ed
6 changed files with 116 additions and 293 deletions

View File

@ -1,183 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from dataclasses import dataclass\n",
"\n",
"@dataclass\n",
"class PropertyFinanceParams:\n",
" \"\"\"Tunable parameters that describe the finances of a property.\"\"\"\n",
" purchase_price: float\n",
" deposit_amount: float\n",
" loan_duration_years: int\n",
" lmi_amount: float\n",
" annual_rental_income: float\n",
" annual_rental_growth_rate: float\n",
" annual_interest_rate: float\n",
" annual_growth_rate: float"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"def calculate_stamp_duty(purchase_price: float, first_home: bool) -> float:\n",
" if first_home:\n",
" if purchase_price < 600000:\n",
" return 0\n",
" elif purchase_price < 605000:\n",
" return 1045\n",
" elif purchase_price < 625000:\n",
" return 5428\n",
" elif purchase_price < 650000:\n",
" return 11356\n",
" elif purchase_price < 675000:\n",
" return 17785\n",
" elif purchase_price < 700000:\n",
" return 24713\n",
" elif purchase_price < 725000:\n",
" return 32141\n",
" elif purchase_price < 745000:\n",
" return 38444\n",
" else:\n",
" return\n",
" else:\n",
" if purchase_price < 25000:\n",
" return purchase_price * 0.014\n",
" elif purchase_price < 130000:\n",
" return 350 + (purchase_price - 25000) * 0.024\n",
" elif purchase_price < 960000:\n",
" return 2870 + (purchase_price - 130000) * 0.06\n",
" elif purchase_price < 2000000:\n",
" return purchase_price * 0.055\n",
" else:\n",
" return 110000 + (purchase_price - 2000000) * 0.065\n",
" \n",
"def calculate_annual_repayments(principal: float, annual_interest_rate: float, loan_duration_years: int):\n",
" return (principal * annual_interest_rate * (1 + annual_interest_rate) ** loan_duration_years) / ((1 + annual_interest_rate) ** loan_duration_years - 1)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Upfront Costs: 321500.00 = 250000.00 deposit + 0.00 LMI + 71500.00 stamp duty\n",
"Annual Mortgage Payment: 81241.59\n",
"\n",
"Total Paid: 2437247.81\n",
"Property Value: 9895931.56\n",
"Total Interest Paid: 1387247.81\n",
"Total Rental Income: 116762.31\n",
"\n",
"Total Paid (10yr): 812415.94\n",
"Property Value (10yr): 2557296.76\n",
"Total Interest Paid (10yr): 650510.03\n",
"Total Principal Paid (10yr): 161905.91\n",
"Total Rental Income (10yr): 432219.86\n",
"Net Cash Position (10yr): -380196.08\n",
"Loan Balance (10yr): 888094.09\n",
"Total Equity (10yr): 1669202.67\n"
]
}
],
"source": [
"def run_simulation(property_finance_params: PropertyFinanceParams):\n",
" initial_loan_amount = property_finance_params.purchase_price - property_finance_params.deposit_amount\n",
" yearly_loan_balance = [initial_loan_amount]\n",
"\n",
" stamp_duty = calculate_stamp_duty(property_finance_params.purchase_price, first_home=False)\n",
" upfront_costs = stamp_duty + property_finance_params.deposit_amount + property_finance_params.lmi_amount\n",
"\n",
" print(f\"Upfront Costs: {upfront_costs:.2f} = {property_finance_params.deposit_amount:.2f} deposit + {property_finance_params.lmi_amount:.2f} LMI + {stamp_duty:.2f} stamp duty\")\n",
"\n",
" annual_mortgage_payment = calculate_annual_repayments(initial_loan_amount, property_finance_params.annual_interest_rate, \n",
" property_finance_params.loan_duration_years)\n",
" \n",
" print(f\"Annual Mortgage Payment: {annual_mortgage_payment:.2f}\")\n",
"\n",
" yearly_interest_paid = []\n",
" yearly_principal_paid = []\n",
" yearly_property_value = [property_finance_params.purchase_price]\n",
" yearly_rental_income = [property_finance_params.annual_rental_income]\n",
"\n",
" for year in range(property_finance_params.loan_duration_years):\n",
" interest_paid = yearly_loan_balance[-1] * property_finance_params.annual_interest_rate\n",
" yearly_interest_paid.append(interest_paid)\n",
" yearly_principal_paid.append(annual_mortgage_payment - interest_paid)\n",
"\n",
" yearly_loan_balance.append(yearly_loan_balance[-1] - yearly_principal_paid[-1])\n",
"\n",
" yearly_property_value.append(yearly_property_value[-1] * (1 + property_finance_params.annual_growth_rate))\n",
"\n",
" yearly_rental_income.append(yearly_rental_income[-1] * (1 + property_finance_params.annual_rental_growth_rate))\n",
"\n",
" if yearly_loan_balance[-1] <= 0:\n",
" break # Loan fully paid off\n",
"\n",
" print()\n",
"\n",
" print(f\"Total Paid: {annual_mortgage_payment * property_finance_params.loan_duration_years:.2f}\")\n",
" print(f\"Property Value: {yearly_property_value[-1]:.2f}\")\n",
" print(f\"Total Interest Paid: {sum(yearly_interest_paid):.2f}\")\n",
" print(f\"Total Rental Income: {yearly_rental_income[-1]:.2f}\")\n",
"\n",
" print()\n",
"\n",
" print(f\"Total Paid (10yr): {annual_mortgage_payment * 10:.2f}\")\n",
" print(f\"Property Value (10yr): {yearly_property_value[10]:.2f}\")\n",
" print(f\"Total Interest Paid (10yr): {sum(yearly_interest_paid[:10]):.2f}\")\n",
" print(f\"Total Principal Paid (10yr): {sum(yearly_principal_paid[:10]):.2f}\")\n",
" print(f\"Total Rental Income (10yr): {sum(yearly_rental_income[:10]):.2f}\")\n",
" print(f\"Net Cash Position (10yr): {sum(yearly_rental_income[:10]) - (annual_mortgage_payment * 10):.2f}\")\n",
" print(f\"Loan Balance (10yr): {yearly_loan_balance[10]:.2f}\")\n",
" print(f\"Total Equity (10yr): {yearly_property_value[10] - yearly_loan_balance[10]:.2f}\")\n",
"\n",
"\n",
"run_simulation(\n",
" PropertyFinanceParams(\n",
" 1300000,\n",
" 250000,\n",
" 30,\n",
" 0,\n",
" 36000,\n",
" 0.04,\n",
" 0.066,\n",
" 0.07)\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "investment-simulator-KPfGkdIO-py3.12",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -1,92 +0,0 @@
import matplotlib.pyplot as plt
periods_per_year = 12
loan_duration_years = 30
nominal_annual_interest_rate = 6.6 / 100
loan_principal = 600000
deposit = 32500
additional_upront_costs = 11356
annual_appreciation_rate = 5 / 100
initial_annual_rental_income = 14400
rental_income_annual_growth_rate = 6 / 100
baseline_annual_yield = 9 / 100 # ETF
rental_cost_annual_growth_rate = 6 / 100 # If I don't buy a PPOR
initial_annual_rental_cost = 20400 # Living in carlton
initial_property_value = loan_principal + deposit
period_interest_rate = nominal_annual_interest_rate / periods_per_year
total_loan_periods = periods_per_year * loan_duration_years
period_appreciation_rate = (1 + annual_appreciation_rate) ** (1 / periods_per_year) - 1
period_rental_income_growth_rate = (1 + rental_income_annual_growth_rate) ** (1 / periods_per_year) - 1
initial_period_rental_income = initial_annual_rental_income / periods_per_year
baseline_period_yield = (1 + baseline_annual_yield) ** (1 / periods_per_year) - 1 # ETF
period_rental_cost_growth_rate = (1 + rental_cost_annual_growth_rate) ** (1 / periods_per_year) - 1 # Renting
initial_period_rental_cost = initial_annual_rental_cost / periods_per_year
period_payment = (loan_principal * (period_interest_rate * (1 + period_interest_rate) ** total_loan_periods) /
((1 + period_interest_rate) ** total_loan_periods - 1))
property_value = initial_property_value
property_values = [property_value]
remaining_loan = loan_principal
remaining_loans = [remaining_loan]
equity = deposit
equities = [equity]
total_payments = 0
total_payments_list = [total_payments]
total_interest_paid = 0
total_interest_paid_list = [total_interest_paid]
total_rental_income = 0
total_rental_income_list = [total_rental_income]
rental_income_period = initial_period_rental_income
net_worth = deposit - additional_upront_costs
net_worths = [net_worth]
total_baseline_value = deposit + additional_upront_costs # ETF initial investment of all property upfront costs
total_baseline_values = [total_baseline_value]
rental_cost_period = initial_period_rental_cost # renting
for period in range(total_loan_periods):
total_payments += period_payment
interest_paid_period = remaining_loan * period_interest_rate
total_interest_paid += interest_paid_period
principal_paid_period = period_payment - interest_paid_period
remaining_loan -= principal_paid_period
appreciation_period = property_value * (1 + period_appreciation_rate) - property_value
property_value += appreciation_period
equity += principal_paid_period + appreciation_period
rental_income_period *= 1 + period_rental_income_growth_rate
total_rental_income += rental_income_period
cashflow_period = rental_income_period - interest_paid_period
net_worth += cashflow_period + appreciation_period + principal_paid_period
rental_cost_period *= 1 + period_rental_cost_growth_rate # My rent keeps increasing :((
total_baseline_value = total_baseline_value * (1 + baseline_period_yield) + max(period_payment - rental_cost_period, 0) # ETF grows from compounding and deposit. Assuming that deposit is equal to what would otherwise be building equity in a property - expense of rent
property_values.append(property_value)
remaining_loans.append(remaining_loan)
equities.append(equity)
total_payments_list.append(total_payments)
total_interest_paid_list.append(total_interest_paid)
total_rental_income_list.append(total_rental_income)
net_worths.append(net_worth)
total_baseline_values.append(total_baseline_value)
plt.figure(figsize=(14, 8))
plt.plot(property_values, label='Property Value', color='blue')
plt.plot(remaining_loans, label='Remaining Loan', color='red')
plt.plot(equities, label='Equity', color='green')
plt.plot(net_worths, label='Net Worth', color='purple')
plt.plot(total_payments_list, label='Total Payments', color='orange')
plt.plot(total_interest_paid_list, label='Total Interest Paid', color='brown')
plt.plot(total_rental_income_list, label='Total Rental Income', color='teal')
plt.plot(total_baseline_values, label='Total Baseline Value (ETF)', color='black')
plt.title('Financial Metrics and ETF Comparison over the Loan Term')
plt.xlabel('Months')
plt.ylabel('Dollar Amount')
plt.legend()
plt.grid(True)
plt.show()

79
poetry.lock generated
View File

@ -353,6 +353,29 @@ pyqt5 = ["pyqt5"]
pyside6 = ["pyside6"]
test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"]
[[package]]
name = "ipympl"
version = "0.9.4"
description = "Matplotlib Jupyter Extension"
optional = false
python-versions = ">=3.9"
files = [
{file = "ipympl-0.9.4-py3-none-any.whl", hash = "sha256:5b0c08c6f4f6ea655ba58239363457c10fb921557f5038c1a46db4457d6d6b0e"},
{file = "ipympl-0.9.4.tar.gz", hash = "sha256:cfb53c5b4fcbcee6d18f095eecfc6c6c474303d5b744e72cc66e7a2804708907"},
]
[package.dependencies]
ipython = "<9"
ipython-genutils = "*"
ipywidgets = ">=7.6.0,<9"
matplotlib = ">=3.4.0,<4"
numpy = "*"
pillow = "*"
traitlets = "<6"
[package.extras]
docs = ["myst-nb", "sphinx (>=1.5)", "sphinx-book-theme", "sphinx-copybutton", "sphinx-thebe", "sphinx-togglebutton"]
[[package]]
name = "ipython"
version = "8.26.0"
@ -390,6 +413,38 @@ qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
[[package]]
name = "ipython-genutils"
version = "0.2.0"
description = "Vestigial utilities from IPython"
optional = false
python-versions = "*"
files = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
{file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"},
]
[[package]]
name = "ipywidgets"
version = "8.1.3"
description = "Jupyter interactive widgets"
optional = false
python-versions = ">=3.7"
files = [
{file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"},
{file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"},
]
[package.dependencies]
comm = ">=0.1.3"
ipython = ">=6.1.0"
jupyterlab-widgets = ">=3.0.11,<3.1.0"
traitlets = ">=4.3.1"
widgetsnbextension = ">=4.0.11,<4.1.0"
[package.extras]
test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
[[package]]
name = "jedi"
version = "0.19.1"
@ -451,6 +506,17 @@ traitlets = ">=5.3"
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"]
[[package]]
name = "jupyterlab-widgets"
version = "3.0.11"
description = "Jupyter interactive widgets for JupyterLab"
optional = false
python-versions = ">=3.7"
files = [
{file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"},
{file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"},
]
[[package]]
name = "kiwisolver"
version = "1.4.5"
@ -1197,7 +1263,18 @@ files = [
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "widgetsnbextension"
version = "4.0.11"
description = "Jupyter interactive widgets for Jupyter Notebook"
optional = false
python-versions = ">=3.7"
files = [
{file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"},
{file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "2def5260115c723676f22a9db8fbe59552b940def30c8814ded40a3ea017b08f"
content-hash = "1dd3ee1c675ef9d755d767e74864d244ce5972b814bcf58c4df66ee8db8e3539"

View File

@ -10,6 +10,7 @@ python = "^3.11"
numpy = "^2.0.0"
transitions = "^0.9.1"
matplotlib = "^3.9.1.post1"
ipympl = "^0.9.4"
[tool.poetry.group.dev.dependencies]

File diff suppressed because one or more lines are too long