cleanup
This commit is contained in:
parent
7a60da2a8b
commit
f73c8120ed
Binary file not shown.
183
demo.ipynb
183
demo.ipynb
|
@ -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
|
||||
}
|
92
mortgage.py
92
mortgage.py
|
@ -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
79
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user