diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..195d97b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/__pycache__ +*.pkl +venv/ +API_KEY.py +*.db +*.ipynb_checkpoints \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0fac4e0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + # avoid manual add of files once black modifies them + # pre-commit will fail if black has modified the file + # this command will re-add all staged files once black has formatted them + #- entry: bash -c 'black "$@"; echo $(git diff --name-only --staged) | xargs git add' -- + + + +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort +# - language_version: python3.10 +# - verbose: true +# # avoid manual add of files once isort modifies them +# # pre-commit will fail if isort has modified the file +# # this command will re-add all staged files once isort has formatted them +# - entry: bash -c 'isort "$@"; echo $(git diff --name-only --staged) | xargs git add' -- +# +- repo: https://github.com/PyCQA/autoflake + rev: v2.0.1 + hooks: + - id: autoflake +# \ No newline at end of file diff --git a/calculator.py b/calculator.py index df632e2..2d8fc40 100644 --- a/calculator.py +++ b/calculator.py @@ -1,12 +1,13 @@ # Run this app with `python app.py` and # visit http://127.0.0.1:8050/ in your web browser. -from dash import Dash, html, dcc, dash_table, callback -from dash.dependencies import Input, Output -from payments import Mortgage -from pages.rentvest import layout as rentvest_layout import dash import pandas as pd +from dash import Dash, callback, dash_table, dcc, html +from dash.dependencies import Input, Output + +from pages.rentvest import layout as rentvest_layout +from payments import Mortgage app = Dash( __name__, diff --git a/house_prices.py b/house_prices.py index b37e230..c645bc0 100644 --- a/house_prices.py +++ b/house_prices.py @@ -1,16 +1,19 @@ +import argparse +import pickle as pkl +import sqlite3 +import sys + import requests from bs4 import BeautifulSoup -import sys -import sqlite3 -import pickle as pkl -import argparse + from API_KEY import API_KEY -import time URL_ADD = "https://api.domain.com.au/v1/addressLocators?searchLevel=Suburb&suburb={}&state=NSW&postcode={}" URL_PERF = "https://api.domain.com.au/v2/suburbPerformanceStatistics/{}/{}/{}?propertyCategory={}&bedrooms={}&periodSize={}&startingPeriodRelativeToCurrent={}&totalPeriods={}" URL_DEM = "https://api.domain.com.au/v2/demographics/{}/{}/{}?types=AgeGroupOfPopulation%2CCountryOfBirth%2CNatureOfOccupancy%2COccupation%2CGeographicalPopulation%2CGeographicalPopulation%2CEducationAttendance%2CHousingLoanRepayment%2CMaritalStatus%2CReligion%2CTransportToWork%2CFamilyComposition%2CHouseholdIncome%2CRent%2CLabourForceStatus&year={}" -state_map = {"Sydney": "NSW", "Melbourne": "VIC"} +state_map = {"Sydney": "NSW", "Melbourne": "VIC", "Perth": "WA", "Brisbane": "QLD"} + +# TODO -> Convert to ORM models def get_suburbs(city): @@ -58,6 +61,72 @@ def get_suburbs(city): print("No postcode data for {}".format(sub)) continue return MelSubs + elif city == "Perth": + URL = "https://www.homely.com.au/find-suburb-by-region/perth-greater-western-australia" + HTML = requests.get(URL) + if not HTML.status_code == 200: + sys.exit(URL + " is not available\n", "RESPONSE " + HTML.status_code) + soup = BeautifulSoup(HTML.text, "html.parser") + # allList = soup.find("div", "col-group") + links = soup.find_all("a") + Psubs = [] + for link in links: + Psubs.append(link.get_text()) + URL = "http://www.justweb.com.au/post-code/perth-postalcodes.html" + HTML = requests.get(URL) + if not HTML.status_code == 200: + sys.exit(URL + " is not available\n", "RESPONSE " + HTML.status_code) + soup = BeautifulSoup(HTML.text, "html.parser") + allList = soup.find_all("select") + subDict = {} + for entry in allList[1].find_all("option"): + txt = entry.get_text() + code = txt[-4:] + sub = txt[:-4].strip() + subDict[sub] = code + PerthSubs = [] + for sub in Psubs: + try: + PerthSubs.append((sub, subDict[sub])) + except: + print("No postcode data for {}".format(sub)) + continue + + return PerthSubs + + elif city == "Brisbane": + URL = "https://www.homely.com.au/find-suburb-by-region/brisbane-queensland" + HTML = requests.get(URL) + if not HTML.status_code == 200: + sys.exit(URL + " is not available\n", "RESPONSE " + HTML.status_code) + soup = BeautifulSoup(HTML.text, "html.parser") + # allList = soup.find("div", "col-group") + links = soup.find_all("a") + Bsubs = [] + for link in links: + Bsubs.append(link.get_text()) + URL = "http://www.justweb.com.au/post-code/brisbane-postalcodes.html" + HTML = requests.get(URL) + if not HTML.status_code == 200: + sys.exit(URL + " is not available\n", "RESPONSE " + HTML.status_code) + soup = BeautifulSoup(HTML.text, "html.parser") + allList = soup.find_all("select") + subDict = {} + for entry in allList[1].find_all("option"): + txt = entry.get_text() + code = txt[-4:] + sub = txt[:-4].strip() + subDict[sub] = code + BrisSubs = [] + for sub in Bsubs: + try: + BrisSubs.append((sub, subDict[sub])) + except: + print("No postcode data for {}".format(sub)) + continue + + return BrisSubs + else: sys.exit("No implementation for {}".format(city)) @@ -128,7 +197,7 @@ def create_table_performance(name): sql.execute(query) -def create_table_demographic(name): +def create_table_demographic(names): query = """DROP TABLE IF EXISTS {};""".format(name) sql.execute(query) query = """CREATE TABLE {} ( diff --git a/housing.db b/housing.db index 364a7bc..efc5e41 100644 Binary files a/housing.db and b/housing.db differ diff --git a/pages/__pycache__/rentvest.cpython-310.pyc b/pages/__pycache__/rentvest.cpython-310.pyc deleted file mode 100644 index 8d7ec17..0000000 Binary files a/pages/__pycache__/rentvest.cpython-310.pyc and /dev/null differ diff --git a/pages/rentvest.py b/pages/rentvest.py index 92ebe30..959b25d 100644 --- a/pages/rentvest.py +++ b/pages/rentvest.py @@ -1,9 +1,9 @@ -from dash import Dash, html, dcc, dash_table, callback -from dash.dependencies import Input, Output -from payments import Mortgage import dash import pandas as pd +from dash import Dash, callback, dash_table, dcc, html +from dash.dependencies import Input, Output +from payments import Mortgage layout = html.Div( [ @@ -94,6 +94,18 @@ style={"width": "15%", "margin": "1px"}, inline=False, ), + html.Label( + "Reinvest positive cash flow?", + style={"padding": "10px", "font-weight": "bold", "width": "15%"}, + ), + dcc.RadioItems( + options=["Yes", "No"], + value="Yes", + id="reinvest-rentvest", + className="form-control-sm", + style={"width": "15%", "margin": "1px"}, + inline=False, + ), ], style={ "padding": 10, @@ -171,7 +183,7 @@ className="form-control-sm", style={"width": "15%"}, ), - html.Label( + html.Label( "Personal Rent", style={"padding": "10px", "font-weight": "bold", "width": "15%"}, ), @@ -182,7 +194,6 @@ className="form-control-sm", style={"width": "15%"}, ), - ], style={ "padding": 10, @@ -222,7 +233,7 @@ Input("rent-rentvest", "value"), Input("monthly-cost-rentvest", "value"), Input("personal-rent-rentvest", "value"), - + Input("reinvest-rentvest", "value"), ) def update_graph( price, @@ -236,11 +247,19 @@ def update_graph( inflation, rent, monthly_cost, - personal_rent + personal_rent, + reinvest, ): m = Mortgage(interest, term, price, deposit, other) out = m.pl_report_rentvest( - years_hold, growth, inflation, monthly_cost, interest_only, rent, personal_rent + years_hold, + growth, + inflation, + monthly_cost, + interest_only, + rent, + personal_rent, + reinvest, ) out = [o.strip() for o in out if len(o.strip()) > 0] # elems = [html.Ul(o) for o in out if len(o)>0 ] diff --git a/payments.py b/payments.py index 52cd12e..f84033f 100644 --- a/payments.py +++ b/payments.py @@ -1,183 +1,679 @@ +from functools import lru_cache +from typing import Optional, Tuple + import numpy as np +# TODO: Add some tests +# TODO: Add functionality to borrow money from properties +# TODO: if borrowed money from offset account, take care of interest payments +# TODO: Any cash should be placed in offset account + + +class StrategyEnum(str): + """Enum for strategy""" + + # principal place of residence + ppor = "ppor" + # Property bought for renting + rentvest = "rentvest" + # Property bought as ppor and converted to rentvest + convert_to_rent = "convert_to_rent" + class Mortgage: - def __init__(self, interest, years, property_price, deposit, other_expenses): - # monthly interest - # interest in num + """A class to do some basic calculations for Mortgage + Extra payments can be made but they have to be constant across periods + """ + + def __init__( + self, interest: float, loan: float, years: int, interest_only: bool = False + ): + # Monthly interest self.interest = interest / (12 * 100) + self.loan = loan self.years = years + # Compounding is being done monthly, this may not align with what banks do (daily) but close self.months = self.years * 12 - self.principal = property_price + other_expenses - deposit - self.property_price = property_price - self.deposit = deposit - self.other_expenses = other_expenses + self.interest_only = interest_only + + def get_monthly_mortgage_payment(self) -> float: + """Get monthly payments for the mortgage + + Returns: + float: _Monthly payments + """ + if self.interest_only: + return self.get_monthly_interest_only_payment() - def get_monthly_payments(self): return np.ceil( - self.interest * self.principal / (1 - (1 + self.interest) ** (-self.months)) + self.interest * self.loan / (1 - (1 + self.interest) ** (-self.months)) ) - def get_monthly_interest_payment(self): - return np.ceil(self.interest * self.principal) + def get_monthly_interest_only_payment(self) -> float: + """Get monthly interest only payment + + Returns: + float: _Monthly interest only payment + """ + return np.ceil(self.interest * self.loan) + + def get_total_interest_paid(self, years: int, extra_payments: float = 0) -> float: + """Get total interest paid for the mortgage in years - def get_total_interest_paid(self, years, extra_payments=0): - monthly_payments = self.get_monthly_payments() + extra_payments + Args: + years (int): _Number of years_ + extra_payments (float, optional): _Extra payments_. Defaults to 0. + + Returns: + float: _Total interest paid_ + """ + monthly_payments = self.get_monthly_mortgage_payment() + extra_payments periods = years * 12 - return (self.principal * self.interest - monthly_payments) * ( + return (self.loan * self.interest - monthly_payments) * ( (1 + self.interest) ** periods - 1 ) / self.interest + monthly_payments * periods - def get_principal_remaining(self, years, extra_payments=0): + def get_principal_remaining(self, years: int, extra_payments: float = 0) -> float: + """Get principal remaining after years + + Args: + years (int): Number of years + extra_payments (float, optional): Extra payments. Defaults to 0. + Returns: + float: Principal remaining + """ interest = self.get_total_interest_paid(years, extra_payments) - total_paid = (self.get_monthly_payments() + extra_payments) * years * 12 - return self.principal - (total_paid - interest) - - def get_principal_paid(self, years, extra_payments=0): - return self.principal - self.get_principal_remaining(years, extra_payments) - - def get_stats_current_val(self, years, inflation, extra_payments=0): - monthly = self.get_monthly_payments() + extra_payments - princ = self.principal - current_interest = 0 - current_principal = 0 - # montly interest rate - rate = inflation / 1200 - total_princ = 0 - for i in range(years * 12): - interest = princ * self.interest - princ = princ - (monthly - interest) - total_princ = total_princ + (monthly - interest) - current_interest = current_interest + interest / (1 + rate) ** i - current_principal = ( - current_principal + (monthly - interest) / (1 + rate) ** i + total_paid = (self.get_monthly_mortgage_payment() + extra_payments) * years * 12 + return self.loan - (total_paid - interest) + + def get_principal_paid(self, years: int, extra_payments: float = 0) -> float: + """Get principal paid after years + + Args: + years (int): Number of years + extra_payments (float, optional): Extra payments. Defaults to 0. + + Returns: + float: Principal paid + """ + return self.loan - self.get_principal_remaining(years, extra_payments) + + +class Property(Mortgage): + """A class to do some basic calculations for Property relating to property value, + equity generated, cash flow, etc. The property can be ppor, rentvest or ppor converted to rentvest + Year = 1 means position after owning property for one year""" + + def __init__( + self, + price: float, + deposit: float, + buying_cost: float, + growth_rate: float, + interest_rate: float, + rent: float = 0, + extra_repayments: float = 0, + cost_growth_rate: float = 0, + running_cost: float = 0, + lmi: float = 0, + strategy: StrategyEnum = "ppor", + owner_occupied_years: Optional[float] = None, + ): + # Deposit is including buying cost + # By default mortgage is for 30 years + super().__init__( + interest_rate, + price - deposit + buying_cost + lmi, + years=30, + interest_only=True if strategy == "rentvest" else False, + ) + self.price = price + self.deposit = deposit + self.buying_cost = buying_cost + self.growth_rate = growth_rate + self.extra_repayments = extra_repayments + + self.rent = rent + self.running_cost = running_cost + self.lmi = lmi + self.strategy = strategy + self.owner_occupied_years = owner_occupied_years + # Growth rate in costs and rent recevied + # TODO -> have different growth rate for each + # monthly + self.cost_growth_rate = cost_growth_rate / 1200 + self.sanity_check() + + def sanity_check(self): + """Sanity check on input parameters""" + if self.strategy == "ppor": + assert self.rent == 0, "PPOR should not have rent" + assert not self.interest_only, "PPOR should not be interest only" + + if self.strategy == "rentvest": + assert self.rent > 0, "Rentvest should have rent" + + if self.strategy == "convert_to_rent": + assert self.rent > 0, "Convert to rent should have rent" + assert ( + self.owner_occupied_years > 0 + ), "Convert to rent should have owner occupied years" + + assert ( + self.deposit > self.buying_cost + ), "Deposit should be more than buying cost" + assert self.rent >= 0, "Rent should be positive" + assert self.strategy in [ + "ppor", + "rentvest", + "convert_to_rent", + ], "Strategy should be either ppor or rentvest" + if self.strategy == "convert_to_rent": + assert ( + self.owner_occupied_years >= 0 + ), "Owner occupied years should be positive" + + min_repayment = self.get_monthly_mortgage_payment() + if self.strategy == "rentvest": + if self.rent - self.running_cost - min_repayment < 0: + print( + "Running cost higher than rental income earned, property negatively geared" + ) + else: + print("Property positively geared") + + def get_property_position( + self, years: int, months: float = 0 + ) -> Tuple[float, float, float]: + """Get loan left, offset and any out of pocket expenses after holding property for years + Out of pocker expenses include mortgage payments, running cost and any extra payments + + Args: + years (int): Number of years property is held + months (int, optional): [description]. Defaults to 0. + + Returns: + Tuple(float, float, float): Loan left, offset and out of pocket expenses + """ + if self.strategy == "convert_to_rent": + # If property is to be converted to rent for first few years it will be normal property with 0 rent + years_ppor = min(years, self.owner_occupied_years) + loan_left, offset, oop = self.get_property_position_calc( + years_ppor, + self.get_monthly_mortgage_payment(), + 0, + self.running_cost, + 0, + self.loan, + months, + ) + # After that it will be rentvest property + years_rentvest = max(years - years_ppor, 0) + loan_left, offset_1, oop_1 = self.get_property_position_calc( + years_rentvest, + self.get_monthly_interest_only_payment(), + self.rent, + self.running_cost, + offset, + loan_left, + months, + ) + # offset adds on the previous offset + return loan_left, offset_1, oop + oop_1 + + else: + return self.get_property_position_calc( + years, + self.get_monthly_mortgage_payment(), + self.rent, + self.running_cost, + 0, + self.loan, + months, + ) + + @lru_cache(maxsize=32) + def get_property_position_calc( + self, + years: int, + min_repayment: float, + rent: float, + running_cost: float, + offset: float, + loan: float, + months: float = 0, + ) -> Tuple[float, float, float]: + """Brute force method to compute loan left, offset and out of pocket expenses after holding property for years + Compounding is done monthly. Money can be earned from rent. Any money left after paying mortgage and running cost is put in offset account which reduces interest paid + + """ + + out_of_pocket = 0 + + loan_left = loan + # min_repayment = self.get_monthly_mortgage_payment() + periods = years * 12 + months + for _ in range(0, periods): + + earning = rent + # interest is not charged on any money in offset account + interest_repayment = self.interest * (loan_left - offset) + + principal_repayments = min_repayment - interest_repayment + # Loan left is only for interest calculation purposes, loan is not actually decreasing, money is going to offset + loan_left = loan_left - principal_repayments + # If earnings + extra repayments is more than min repayments then I am paying extra, otherwise coming out of pocket + extra_repayments = ( + earning + + self.extra_repayments + - min_repayment + - running_cost * (1 + self.cost_growth_rate) ) - return current_principal, current_interest + out_of_pocket = out_of_pocket + min(0, extra_repayments) + # If any extra money is left, it will go to offset account + offset = offset + max(0, extra_repayments) + + return loan_left, offset, abs(out_of_pocket) + + def get_property_val(self, years: int) -> float: + """Property value compounding""" + return self.do_compounding(self.price, years, self.growth_rate) + + def get_principal_paid(self, years: int, extra_payments: float = 0) -> float: + """How much principal has been paid in years""" + if self.strategy == "ppor": + return super().get_principal_paid(years, extra_payments) + elif self.strategy in ["rentvest", "convert_to_rent"]: + loan_left, _, _ = self.get_property_position(years) + return self.loan - loan_left + else: + print(f"Strategy {self.strategy} not implemented") + return 0 + + def get_interest_paid(self, years: int) -> float: + """How much interest has been paid in years""" + if self.strategy in ["ppor", "rentvest"]: + return super().get_total_interest_paid(years, self.extra_repayments) + else: + print(f"Strategy {self.strategy} not implemented") + return 0 + + def total_equity_at_year(self, years: int, factor: float = 1.0) -> float: + """How much equity has been created? Equity = property value + principal paid - loan + Equity is proerty value minus loan left""" + property_value = self.get_property_val(years) + principal_paid = self.get_principal_paid(years) + equity = factor * property_value + principal_paid - self.loan + return equity + + def get_offset_balance(self, years: int) -> float: + """Offset balance after years""" + _, offset, _ = self.get_property_position(years) + return offset + + def get_oop_expenses(self, years: int) -> float: + """Out of pocket expenses after years""" + _, _, oop = self.get_property_position(years) + return oop + + def get_net_cash_flow(self, years: int) -> float: + """Net cash flow for this property if held for years, negative means cash out, positive means cash in""" + _, offset, oop = self.get_property_position(years) + return offset - oop + + def get_net_yearly_cash_flow(self, years: int) -> float: + """Net cash flow for this property at year (not cumulative), negative means cash out, positive means cash in""" + if years <= 0: + return 0 + + _, offset_1, oop_1 = self.get_property_position(years - 1) + _, offset, oop = self.get_property_position(years) + return (offset - oop) - (offset_1 - oop_1) + + def net_position_at_year(self, years: int) -> float: + """Net wealth position in years + Calculated as: + equity - deposit - running cost - interest paid + """ + loan_left, _, oop = self.get_property_position(years) + net_position = self.get_property_val(years) - loan_left - self.deposit - oop + + return net_position + + def get_avg_return_at_year(self, years: int) -> float: + """What's the profit generated from investment? + What is the net yearly return on investment? + """ + net_position = self.net_position_at_year(years) + total_owning_cost = -1.0 * self.get_net_cash_flow(years) + self.deposit + # Average yearly return? + avg_return = (net_position / total_owning_cost * 100) / years + return avg_return + + def get_lvr_at_year(self, years: int) -> float: + """LVR considering payments and property growth""" + loan_left = self.get_property_position(years)[0] + property_val = self.get_property_val(years) + return loan_left / property_val @staticmethod - def do_compounding(principal, years, interest): - # compounding yearly + def do_compounding(principal: float, years: int, interest: float) -> float: + """Simple compounding""" return principal * (1 + interest / 100) ** years - def pl_report( - self, years_hold, growth_rate, inflation, extra_payments, rent=0, index_rate=6 + +class Portfolio: + """Class for portfolio of property which tracks performance""" + + def __init__( + self, + properties: list[Property], + buy_year: list[float], + equity_use_fraction: list[ + float + ], # what fraction of deposit for a property comes from equity, rest comes from cash + cash: float, + monthly_income: float, + monthly_living_expenses: float, + monthly_living_rent: float, + income_growth_rate: float = 0, + expenses_growth_rate: float = 0, ): - sold_price = self.do_compounding(self.property_price, years_hold, growth_rate) - monthly = self.get_monthly_payments() - interest_paid = self.get_total_interest_paid(years_hold, extra_payments) - principal_paid = self.get_principal_paid(years_hold, extra_payments) - total_paid = principal_paid + self.deposit - sold_gain = sold_price - (self.principal - principal_paid) - owning_cost = total_paid + interest_paid + self.other_expenses - money_left = sold_gain - owning_cost - curr_val = money_left / (1 + growth_rate / 100) ** years_hold - current_principal, current_interest = self.get_stats_current_val( - years_hold, inflation, extra_payments + self.properties = properties + self.buy_year = buy_year + self.equity_use_fraction = equity_use_fraction + self.cash = cash + self.monthly_income = monthly_income + self.income_growth_rate = income_growth_rate + self.expenses_growth_rate = expenses_growth_rate + self.monthly_living_expenses = monthly_living_expenses + self.monthly_living_rent = monthly_living_rent + self.has_ppor = "ppor" in [prop.strategy for prop in self.properties] + + self.monthly_savings = self.monthly_income - self.monthly_living_expenses + + self.sanity_check() + + def sanity_check(self) -> None: + """Sanity checks for portfolio""" + # assert self.deposits <= self.cash, "Not enough cash to buy this portfolio of properties" + assert len(self.buy_year) == len( + self.properties + ), "Buy year must be specified for each property" + assert len(self.equity_use_fraction) == len( + self.properties + ), "Equity use fraction must be specified for each property" + if not self.has_ppor: + assert ( + self.monthly_living_rent > 0 + ), "Rent cannot be 0 if no ppor proerty in portfolio" + assert ( + len([prop for prop in self.properties if prop.strategy == "ppor"]) <= 1 + ), "Only one ppor property allowed" + assert ( + len( + [prop for prop in self.properties if prop.strategy == "convert_to_rent"] + ) + <= 1 + ), "Only one convert_to_rent property allowed" + + @lru_cache(maxsize=128) + def get_property_position( + self, years: int, months: float = 0 + ) -> Tuple[float, float, float]: + """Get loan left, offset and any out of pocket expenses after holding property for years + Out of pocker expenses include mortgage payments, running cost and any extra payments + + Args: + years (int): Number of years property is held + months (int, optional): [description]. Defaults to 0. + + Returns: + Tuple(float, float, float): Loan left, offset and out of pocket expenses + """ + # How much cash do we have at t=0? It's total savings minus any deposit paid + cash = self.cash + + n_properties = len(self.properties) + + loan_left_properties = np.zeros((years + 1, n_properties)) + oop_properties = np.zeros((years + 1, n_properties)) + + total_cash = np.zeros((years + 1, 1)) + + for i, property in enumerate(self.properties): + if self.buy_year[i] > years: + continue + loan_left_properties[self.buy_year[i], i] = property.loan + + total_cash[0] = cash - self.get_cash_deposit_paid_at_year(0) + + for i in range(1, years + 1): + + oop_all = 0 + offset_property = 0 + # At the start of the year we buy property + # cash = cash - self.get_cash_deposit_paid_at_year(i) + # This is the cash left after buying + # total_cash[i] = cash + + n_properties_active = len( + [prop for k, prop in enumerate(self.properties) if i > self.buy_year[k]] + ) + + for j, property in enumerate(self.properties): + + if i <= self.buy_year[j]: + continue + + if property.strategy == "convert_to_rent": + if i < self.buy_year[j] + property.owner_occupied_years: + loan_left, offset, oop = property.get_property_position_calc( + 1, + property.get_monthly_mortgage_payment(), + 0, # can't have rent during owner occupied periods + property.running_cost, + total_cash[i - 1, 0] * (1 / n_properties_active), + loan_left_properties[i - 1, j], + months, + ) + else: + loan_left, offset, oop = property.get_property_position_calc( + 1, + property.get_monthly_interest_only_payment(), + property.rent, + property.running_cost, + total_cash[i - 1, 0] * (1 / n_properties_active), + loan_left_properties[i - 1, j], + months, + ) + else: + loan_left, offset, oop = property.get_property_position_calc( + 1, + property.get_monthly_mortgage_payment(), + property.rent, + property.running_cost, + total_cash[i - 1, 0] * (1 / n_properties_active), + loan_left_properties[i - 1, j], + months, + ) + + # Loan left for the next year + loan_left_properties[i, j] = loan_left + # How much oop expenses at the end of the year + oop_properties[i, j] = oop + + offset_property = offset_property + offset + oop_all += oop + # At the end of this year how much cash do we have available? + # offset is what we earned from property, if we didn't earn anything offset will be same as input + cash = cash + (offset_property - cash) + cash = ( + cash + + self.monthly_savings * 12 + - self.get_cash_deposit_paid_at_year(i) + - self.get_personal_rent_expenditure_at_year(i) + - oop_all + ) + total_cash[i] = cash + return loan_left_properties, oop_properties, total_cash + + def get_portfolio_position(self, years: int) -> Tuple[float, float, float, float]: + """Get property value, cash, equity and oop expenses after holding property for years + + Args: + years (int): _description_ + + Returns: + Tuple[float, float, float, float]: _description_ + """ """""" + loan_left, oop, cash = self.get_property_position(years) + property_val = np.array([self.get_property_val(i) for i in range(years + 1)]) + loan_left = np.sum(loan_left[:, :], axis=1) + equity = property_val - loan_left + usable_equity = property_val * 0.8 - loan_left + equity_needed = np.cumsum( + [self.get_equity_deposit_paid_at_year(i) for i in range(years + 1)] ) - cur_sale = sold_price / (1 + inflation / 100) ** years_hold - current_own_cost = current_principal + current_interest + self.other_expenses - cur_sold_gain = sold_gain / (1 + inflation / 100) ** years_hold - current_profit = cur_sold_gain - current_own_cost - inflation_month = inflation / 1200 - rent_current = ( - rent - * 4 - * (1 - (1 + inflation_month) ** (-years_hold * 12)) - / inflation_month + + equity_sufficient = usable_equity - equity_needed + + # If we actually need equity at that point and usable equity is not enough + if np.any(equity_sufficient < 0): + idx = np.argwhere(equity_sufficient < 0)[0][0] + if equity_needed[idx] > 0: + print("Not enough usable equity to buy property") + + equity = equity - equity_needed + + return np.array(property_val), cash, equity, np.cumsum(oop), loan_left + + def get_cash_flow(self, years): + """Cash flow for the portfolio""" + + cash_flow = self.get_portfolio_position(years)[1] + return cash_flow + + def get_equity(self, years): + """Equity in the portfolio""" + _, _, equity, _, _ = self.get_portfolio_position(years) + return equity + + def get_deposit_needed_at_year(self, years): + """How much deposit is needed to buy properties?""" + deposit = 0 + for i, year in enumerate(self.buy_year): + if year <= years: + deposit = deposit + self.properties[i].deposit + return deposit + + def get_property_val(self, years): + """Total property value""" + property_vals = [ + prop.get_property_val(years - self.buy_year[i]) + if self.buy_year[i] <= years + else 0 + for i, prop in enumerate(self.properties) + ] + return sum(property_vals) + + def add_property(self): + pass + + def get_personal_rent_expenditure(self, years): + """How much is spent on rent?""" + # One portfolio can only have one ppor property + ppor = [ + (i, prop) + for i, prop in enumerate(self.properties) + if prop.strategy == "ppor" + ] + if ppor: + if self.buy_year[ppor[0][0]] < years: + return self.get_personal_rent_expenditure(self.buy_year[ppor[0][0]]) + if "convert_to_rent" in [prop.strategy for prop in self.properties]: + owner_occupied_years = [ + prop.owner_occupied_years + for prop in self.properties + if prop.strategy == "convert_to_rent" + ] + buy_years = [ + self.buy_year[i] + for i, prop in enumerate(self.properties) + if prop.strategy == "convert_to_rent" + ] + years_all = np.arange(1, years + 1) + for buy, occupied in zip(buy_years, owner_occupied_years): + years_all = years_all[(years_all <= buy) & (years_all > buy + occupied)] + return self.monthly_living_rent * 12 * len(years_all) + else: + return self.monthly_living_rent * 12 * years + + def get_personal_rent_expenditure_at_year(self, years): + """How much is spent on rent?""" + # One portfolio can only have one ppor property + if years <= 0: + return 0 + + return self.get_personal_rent_expenditure( + years + ) - self.get_personal_rent_expenditure(years - 1) + + def get_total_cash_deposit_paid(self, year): + """All properties are bought by using cash and savings""" + + return sum( + [ + prop.deposit * (1 - self.equity_use_fraction[i]) + if self.buy_year[i] <= year + else 0 + for i, prop in enumerate(self.properties) + ] ) - spare_cash_if_no_buy = monthly + extra_payments - rent * 4 - # if invested in an index fund what will be the value in years_hold - spare_val = ( - spare_cash_if_no_buy - * ((1 + index_rate / 1200) ** (12 * years_hold) - 1) - * 1200 - / index_rate + + def get_cash_deposit_paid_at_year(self, year): + """All properties are bought by using cash and savings""" + + return sum( + [ + prop.deposit * (1 - self.equity_use_fraction[i]) + if self.buy_year[i] == year + else 0 + for i, prop in enumerate(self.properties) + ] ) - # what is the current value of this considering inflation - cur_val_spare = spare_val / (1 + inflation / 100) ** years_hold - out_str = f"""Property price: AUD {self.property_price} \n - Loan after deposit and costs: AUD {self.principal} \n - LVR: {self.principal/self.property_price} \n - Minimum monthly repayments: AUD {monthly} \n - Extra payments: AUD {extra_payments} \n - Total monthly payments AUD: {monthly+extra_payments} \n - Held the property for: {years_hold} years \n - Property price in {years_hold} years with {growth_rate} percent compounding will be: AUD {sold_price} \n - Total interest paid: AUD {interest_paid} \n - Principal paid: AUD {principal_paid} \n - Profit from selling after settling with bank: AUD {sold_gain} \n - Total cost of owning: AUD {owning_cost} \n - Profit: AUD {money_left} \n - Current value of profit if {inflation}% discounting applied: AUD {curr_val} \n - Current value of principal: AUD {current_principal} \n - Current value of interest: AUD {current_interest} \n - Current value of sold price: AUD {cur_sale} \n - Profit based on current value: AUD {current_profit} \n - Total money, principal + profit: AUD {current_principal+current_profit} \n - If instead invested money left after rent in index fund, current val: AUD {cur_val_spare} \n - """ - return out_str.split("\n") - - def pl_report_rentvest( - self, - years_hold, - growth_rate, - inflation, - extra_cost, - interest_only, - rent_rentvest, - rent_personal, - ): - sold_price = self.do_compounding(self.property_price, years_hold, growth_rate) - if interest_only == "Yes": - monthly = self.get_monthly_interest_payment() - principal_paid = 0 - interest_paid = monthly*12*years_hold - else: - monthly = self.get_monthly_payments() - principal_paid = self.get_principal_paid(years_hold, extra_payments=0) - interest_paid = self.get_total_interest_paid(years_hold, extra_payments=0) - - #This included other_expenses -> deposit is total of other_expenses - total_paid = principal_paid + self.deposit - sold_gain = sold_price - (self.principal - principal_paid) - #rent is weekly - rental_yield = rent_rentvest*52*years_hold - rental_yield_after_expenses = (rent_rentvest-extra_cost)*52*years_hold - - money_left_after_mortgage = rental_yield_after_expenses - monthly*12*years_hold - if(money_left_after_mortgage<0): - after_tax = 0.7*money_left_after_mortgage - else: - after_tax = money_left_after_mortgage - - personal_rent = rent_personal*52*years_hold - - owning_cost = total_paid + interest_paid - rental_yield_after_expenses - money_left = sold_gain - owning_cost - - - - # what is the current value of this considering inflation - out_str = f"""Property price: AUD {self.property_price} \n - Loan after deposit and costs: AUD {self.principal} \n - LVR: {self.principal/self.property_price} \n - Minimum monthly repayments: AUD {monthly} \n - Held the property for: {years_hold} years \n - Property price in {years_hold} years with {growth_rate} percent compounding will be: AUD {sold_price} \n - Total interest paid: AUD {interest_paid} \n - Principal paid: AUD {principal_paid} \n - Total rent received after expenses: AUD {rental_yield_after_expenses} \n - Holding cost (deposit + interest paid - rent recevied): AUD {owning_cost} \n - Personal rent paid: AUD {personal_rent} \n - Money left after mortgage paid and rent received: AUD {money_left_after_mortgage} \n - Profit from selling after settling with bank: AUD {sold_gain} \n - Profit after accounting holding cost: AUD {money_left} \n - Money left after selling and accouting for mortagage paid and personal rent: AUD {money_left-personal_rent} - """ - return out_str.split("\n") - - def get_rent_expenditure(self, weekly, rate, years): - annual_payment = weekly * 52 - present_val = annual_payment * (1 - (1 + rate / 100) ** (-years)) * 100 / rate - return present_val + + def get_equity_deposit_paid(self, years): + """All properties are bought by using cash and savings""" + equity_needed = sum( + [ + prop.deposit * self.equity_use_fraction[i] + if self.buy_year[i] <= years + else 0 + for i, prop in enumerate(self.properties) + ] + ) + return equity_needed + + def get_equity_deposit_paid_at_year(self, year): + """All properties are bought by using cash and savings""" + equity_needed = sum( + [ + prop.deposit * self.equity_use_fraction[i] + if self.buy_year[i] == year + else 0 + for i, prop in enumerate(self.properties) + ] + ) + return equity_needed + + def get_total_cash(self, years): + """Total cash in the portfolio""" + cash = self.get_portfolio_position(years)[1] + return cash + + def get_portfolio_stats(self, years): + """Portfolio position at year""" + # Total property value + _, cash, equity, _, _ = self.get_portfolio_position(years) + return cash, equity diff --git a/viz_app.py b/viz_app.py index aed45c6..9c66302 100644 --- a/viz_app.py +++ b/viz_app.py @@ -1,19 +1,14 @@ import sqlite3 -import pandas as pd -import numpy as np -import dash -import dash_core_components as dcc -import dash_html_components as html -import plotly.express as px +import sys + import dash import dash_core_components as dcc import dash_html_components as html -from dash.dependencies import Input, Output +import numpy as np +import pandas as pd import plotly.express as px import plotly.graph_objects as go -import pandas as pd -import sys -import json +from dash.dependencies import Input, Output conn = sqlite3.connect("housing.db") city = sys.argv[1] @@ -70,7 +65,7 @@ dcc.Dropdown( id="filt2", options=[{"label": i, "value": i} for i in types], - value="Unit", + value="House", ) ], style={"display": "inline-block", "width": "33%"}, @@ -80,7 +75,7 @@ dcc.Dropdown( id="filt3", options=[{"label": i, "value": i} for i in beds], - value=[2], + value=[3], multi=True, ) ],