diff --git a/README.md b/README.md index 64a8de72..b83bfb31 100644 --- a/README.md +++ b/README.md @@ -10,5 +10,5 @@ bank regulation policy. The model incorporates owner-occupiers, renters, buy-to-let investors, a housing market, a rental market, banks, a central bank and a government. A more detailed description of the model can be found at this -[Bank of England Working Paper](http://www.bankofengland.co.uk/research/Pages/workingpapers/2016/swp619.aspx) and at the +[Bank of England Working Paper](https://www.inet.ox.ac.uk/files/swp619.pdf) and at the ModelDescriptionFeb16.pdf file. diff --git a/src/main/java/collectors/CollectorBase.java b/src/main/java/collectors/CollectorBase.java deleted file mode 100644 index ce8a1eb5..00000000 --- a/src/main/java/collectors/CollectorBase.java +++ /dev/null @@ -1,12 +0,0 @@ -package collectors; - -public class CollectorBase { - - private boolean active = false; - - public boolean isActive() { return active; } - - public void setActive(boolean active) { this.active = active; } - - -} diff --git a/src/main/java/collectors/CollectorBase.py b/src/main/java/collectors/CollectorBase.py new file mode 100644 index 00000000..c16609ef --- /dev/null +++ b/src/main/java/collectors/CollectorBase.py @@ -0,0 +1,9 @@ +class CollectorBase: + def __init__(self): + self.active = False + + def isActive(self): + return self.active + + def setActive(self, active: bool): + self.active = active diff --git a/src/main/java/collectors/CoreIndicators.java b/src/main/java/collectors/CoreIndicators.java deleted file mode 100644 index 2ca86308..00000000 --- a/src/main/java/collectors/CoreIndicators.java +++ /dev/null @@ -1,134 +0,0 @@ -package collectors; - -import housing.Config; -import housing.Model; -import utilities.MeanAboveMedian; - -/************************************************************************************************** - * Class to collect the information contained in the Bank of England "Core Indicators" set for LTV - * and LTI limits, as set out in the Bank of England's draft policy statement "The Financial policy - * committee's power over housing tools" (Feb 2015). See Table A for a list of these indicators and - * notes on their definition. - * - * @author danial, Adrian Carro - * - *************************************************************************************************/ -public class CoreIndicators extends CollectorBase { - - //------------------// - //----- Fields -----// - //------------------// - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - - //-------------------// - //----- Methods -----// - //-------------------// - - //----- Getter/setter methods -----// - - // Note that some of these methods are just wrappers around methods contained in other classes with the purpose of - // storing here a coherent set of core indicators getters - - @Override - public void setActive(boolean active) { - super.setActive(active); - Model.creditSupply.setActive(active); - Model.housingMarketStats.setActive(active); - Model.householdStats.setActive(active); - } - - // Owner-occupier mortgage LTI ratio (mean above the median) - double getOwnerOccupierLTIMeanAboveMedian() { - if (Model.creditSupply.oo_lti.getN() > 0) { - return Model.creditSupply.oo_lti.apply(new MeanAboveMedian()); - } else { - return 0.0; - } - } - - // Owner-occupier mortage LTV ratio (mean above the median) - double getOwnerOccupierLTVMeanAboveMedian() { - if (Model.creditSupply.oo_ltv.getN() > 0) { - return Model.creditSupply.oo_ltv.apply(new MeanAboveMedian()); - } else { - return 0.0; - } - } - - // Buy-to-let loan-to-value ratio (mean) - double getBuyToLetLTVMean() { - if (Model.creditSupply.btl_ltv.getN() > 0) { - return Model.creditSupply.btl_ltv.getMean(); - } else { - return 0.0; - } - } - - // Annualised household credit growth (credit growth: rate of change of credit, current month new credit divided by - // new credit in previous step) - double getHouseholdCreditGrowth() { return Model.creditSupply.netCreditGrowth*12.0*100.0; } - - // Household mortgage debt to income ratio (%) - double getDebtToIncome() { - return 100.0*(Model.creditSupply.totalBTLCredit + Model.creditSupply.totalOOCredit) - /(Model.householdStats.getOwnerOccupierAnnualisedTotalIncome() - + Model.householdStats.getActiveBTLAnnualisedTotalIncome() - + Model.householdStats.getNonOwnerAnnualisedTotalIncome()); - } - - // Household debt to income ratio (owner-occupier mortgages only) (%) - double getOODebtToIncome() { - return 100.0*Model.creditSupply.totalOOCredit/Model.householdStats.getOwnerOccupierAnnualisedTotalIncome(); - } - - // Number of mortgage approvals per month (scaled for 26.5 million households) - int getMortgageApprovals() { - return (int)(Model.creditSupply.nApprovedMortgages*config.getUKHouseholds() - /Model.households.size()); - } - - // Number of houses bought/sold per month (scaled for 26.5 million households) - int getHousingTransactions() { - return (int)(Model.housingMarketStats.getnSales()*config.getUKHouseholds() - /Model.households.size()); - } - - // Number of advances to first-time-buyers (scaled for 26.5 million households) - int getAdvancesToFTBs() { - return (int)(Model.creditSupply.nFTBMortgages*config.getUKHouseholds() - /Model.households.size()); - } - - // Number of advances to buy-to-let purchasers (scaled for 26.5 million households) - int getAdvancesToBTL() { - return (int)(Model.creditSupply.nBTLMortgages*config.getUKHouseholds() - /Model.households.size()); - } - - // Number of advances to home-movers (scaled for 26.5 million households) - int getAdvancesToHomeMovers() { return(getMortgageApprovals() - getAdvancesToFTBs() - getAdvancesToBTL()); } - - // House price to household disposable income ratio - // TODO: ATTENTION ---> Gross total income is used here, not disposable income! Post-tax income should be used! - public double getPriceToIncome() { - // TODO: Also, why to use HPI*HPIReference? Why not average house price? - return(Model.housingMarketStats.getHPI()*config.derivedParams.getHPIReference() - *(Model.households.size() - - Model.householdStats.getnRenting() - - Model.householdStats.getnHomeless()) - /(Model.householdStats.getOwnerOccupierAnnualisedTotalIncome() - + Model.householdStats.getActiveBTLAnnualisedTotalIncome())); - // TODO: Finally, for security, population count should be made with nActiveBTL and nOwnerOccupier - } - - // Wrapper around the HouseHoldStats method, which computes the average stock gross rental yield for all currently - // occupied rental properties (%) - double getAvStockYield() { return 100.0*Model.householdStats.getAvStockYield(); } - - // Wrapper around the HousingMarketStats method, which computes the quarter on quarter appreciation in HPI - double getQoQHousePriceGrowth() { return Model.housingMarketStats.getQoQHousePriceGrowth(); } - - // Spread between mortgage-lender interest rate and bank base-rate (%) - double getInterestRateSpread() { return 100.0*Model.bank.interestSpread; } -} diff --git a/src/main/java/collectors/CoreIndicators.py b/src/main/java/collectors/CoreIndicators.py new file mode 100644 index 00000000..27db7d3c --- /dev/null +++ b/src/main/java/collectors/CoreIndicators.py @@ -0,0 +1,115 @@ +from .CollectorBase import CollectorBase + +import housing.Config +import housing.Model +import utilities.MeanAboveMedian + +#************************************************************************************************** +#* Class to collect the information contained in the Bank of England "Core Indicators" set for LTV +#* and LTI limits, as set out in the Bank of England's draft policy statement "The Financial policy +#* committee's power over housing tools" (Feb 2015). See Table A for a list of these indicators and +#* notes on their definition. +#* +#* @author danial, Adrian Carro +#* +#*************************************************************************************************/ +class CoreIndicators(CollectorBase): + #------------------# + #----- Fields -----# + #------------------# + def __init__(self): + self.config = Model.config # Passes the Model's configuration parameters object to a private field + + #-------------------# + #----- Methods -----# + #-------------------# + + #----- Getter/setter methods -----# + + # Note that some of these methods are just wrappers around methods contained in other classes with the purpose of + # storing here a coherent set of core indicators getters + + def setActive(self, active: bool) -> None: + super().setActive(active) + Model.creditSupply.setActive(active) + Model.housingMarketStats.setActive(active) + Model.householdStats.setActive(active) + + # Owner-occupier mortgage LTI ratio (mean above the median) + def getOwnerOccupierLTIMeanAboveMedian(self) -> float: + if Model.creditSupply.oo_lti.getN() > 0: + return Model.creditSupply.oo_lti.apply(MeanAboveMedian()) + else: + return 0.0 + + # Owner-occupier mortage LTV ratio (mean above the median) + def getOwnerOccupierLTVMeanAboveMedian(self) -> float: + if Model.creditSupply.oo_ltv.getN() > 0: + return Model.creditSupply.oo_ltv.apply(MeanAboveMedian()) + else: + return 0.0 + + # Buy-to-let loan-to-value ratio (mean) + def getBuyToLetLTVMean(self) -> float: + if Model.creditSupply.btl_ltv.getN() > 0: + return Model.creditSupply.btl_ltv.getMean() + else: + return 0.0 + + # Annualised household credit growth (credit growth: rate of change of credit, current month new credit divided by + # new credit in previous step) + def getHouseholdCreditGrowth(self) -> float: + return Model.creditSupply.netCreditGrowth * 12.0 * 100.0 + + # Household mortgage debt to income ratio (%) + def getDebtToIncome(self) -> float: + denominator = (Model.householdStats.getOwnerOccupierAnnualisedTotalIncome() + Model.householdStats.getActiveBTLAnnualisedTotalIncome() + Model.householdStats.getNonOwnerAnnualisedTotalIncome()) + return 100.0 * (Model.creditSupply.totalBTLCredit + Model.creditSupply.totalOOCredit) / denominator + + # Household debt to income ratio (owner-occupier mortgages only) (%) + def getOODebtToIncome(self) -> float: + return 100.0 * Model.creditSupply.totalOOCredit / Model.householdStats.getOwnerOccupierAnnualisedTotalIncome() + + # Number of mortgage approvals per month (scaled for 26.5 million households) + def getMortgageApprovals(self) -> int: + return int(Model.creditSupply.nApprovedMortgages * config.getUKHouseholds() / Model.households.size()) + + # Number of houses bought/sold per month (scaled for 26.5 million households) + def getHousingTransactions(self) -> int: + return int(Model.housingMarketStats.getnSales() * config.getUKHouseholds() / Model.households.size()) + + # Number of advances to first-time-buyers (scaled for 26.5 million households) + def getAdvancesToFTBs(self) -> int: + return int(Model.creditSupply.nFTBMortgages * config.getUKHouseholds() / Model.households.size()) + + # Number of advances to buy-to-let purchasers (scaled for 26.5 million households) + def getAdvancesToBTL(self) -> int: + return int(Model.creditSupply.nBTLMortgages * config.getUKHouseholds() / Model.households.size()) + + # Number of advances to home-movers (scaled for 26.5 million households) + def getAdvancesToHomeMovers(self) -> int: + return self.getMortgageApprovals() - self.getAdvancesToFTBs() - self.getAdvancesToBTL() + + # House price to household disposable income ratio + # TODO: ATTENTION ---> Gross total income is used here, not disposable income! Post-tax income should be used! + def getPriceToIncome(self) -> float: + # TODO: Also, why to use HPI*HPIReference? Why not average house price? + return (Model.housingMarketStats.getHPI() * + config.derivedParams.getHPIReference() * + (Model.households.size() - Model.householdStats.getnRenting() - Model.householdStats.getnHomeless()) / + (Model.householdStats.getOwnerOccupierAnnualisedTotalIncome() + + Model.householdStats.getActiveBTLAnnualisedTotalIncome())) + # TODO: Finally, for security, population count should be made with nActiveBTL and nOwnerOccupier + + # Wrapper around the HouseHoldStats method, which computes the average stock gross rental yield for all currently + # occupied rental properties (%) + def getAvStockYield(self) -> float: + return 100.0 * Model.householdStats.getAvStockYield() + + # Wrapper around the HousingMarketStats method, which computes the quarter on quarter appreciation in HPI + def getQoQHousePriceGrowth(self) -> float: + return Model.housingMarketStats.getQoQHousePriceGrowth() + + # Spread between mortgage-lender interest rate and bank base-rate (%) + def getInterestRateSpread(self) -> float: + return 100.0 * Model.bank.interestSpread diff --git a/src/main/java/housing/Bank.java b/src/main/java/housing/Bank.java deleted file mode 100644 index becd63ec..00000000 --- a/src/main/java/housing/Bank.java +++ /dev/null @@ -1,327 +0,0 @@ -package housing; - -import java.util.HashSet; - -/************************************************************************************************** - * Class to represent a mortgage-lender (i.e. a bank or building society), whose only function is - * to approve/decline mortgage requests, so this is where mortgage-lending policy is encoded - * - * @author daniel, davidrpugh, Adrian Carro - * - *************************************************************************************************/ -public class Bank { - - //------------------// - //----- Fields -----// - //------------------// - - // General fields - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - - // Bank fields - public HashSet mortgages; // all unpaid mortgage contracts supplied by the bank - public double interestSpread; // current mortgage interest spread above base rate (monthly rate*12) - private double monthlyPaymentFactor; // Monthly payment as a fraction of the principal for non-BTL mortgages - private double monthlyPaymentFactorBTL; // Monthly payment as a fraction of the principal for BTL (interest-only) mortgages - private double baseRate; - - // Credit supply strategy fields - private double supplyTarget; // target supply of mortgage lending (pounds) - private double supplyVal; // monthly supply of mortgage loans (pounds) - private double dDemand_dInterest; // rate of change of demand with interest rate (pounds) - private int nOOMortgagesOverLTI; // Number of mortgages for owner-occupying that go over the LTI cap this time step - private int nOOMortgages; // Total number of mortgages for owner-occupying - - // LTV internal policy thresholds - private double firstTimeBuyerLTVLimit; // Loan-To-Value upper limit for first-time buyer mortgages - private double ownerOccupierLTVLimit; // Loan-To-Value upper limit for owner-occupying mortgages - private double buyToLetLTVLimit; // Loan-To-Value upper limit for buy-to-let mortgages - - // LTI internal policy thresholds - private double firstTimeBuyerLTILimit; // Loan-To-Income internal upper limit for first-time buyer mortgages - private double ownerOccupierLTILimit; // Loan-To-Income internal upper limit for owner-occupying mortgages - - //------------------------// - //----- Constructors -----// - //------------------------// - - public Bank() { - mortgages = new HashSet<>(); - init(); - } - - //-------------------// - //----- Methods -----// - //-------------------// - - void init() { - mortgages.clear(); - baseRate = config.BANK_INITIAL_BASE_RATE; - // TODO: Is this (dDemand_dInterest) a parameter? Shouldn't it depend somehow on other variables of the model? - dDemand_dInterest = 10*1e10; - // TODO: Is this (0.02) a parameter? Does it affect results in any significant way or is it just a dummy initialisation? - setMortgageInterestRate(0.02); - resetMonthlyCounters(); - // Setup initial LTV internal policy thresholds - firstTimeBuyerLTVLimit = config.BANK_MAX_FTB_LTV; - ownerOccupierLTVLimit= config.BANK_MAX_OO_LTV; - buyToLetLTVLimit = config.BANK_MAX_BTL_LTV; - // Setup initial LTI internal policy thresholds - firstTimeBuyerLTILimit = config.BANK_MAX_FTB_LTI; - ownerOccupierLTILimit = config.BANK_MAX_OO_LTI; - } - - /** - * Redo all necessary monthly calculations and reset counters. - */ - public void step(int totalPopulation) { - supplyTarget = config.BANK_CREDIT_SUPPLY_TARGET*totalPopulation; - setMortgageInterestRate(recalculateInterestRate()); - resetMonthlyCounters(); - } - - /** - * Reset counters for the next month - */ - private void resetMonthlyCounters() { - supplyVal = 0.0; - nOOMortgagesOverLTI = 0; - nOOMortgages = 0; - } - - /** - * Calculate the mortgage interest rate for next month based on the rate for this month and the resulting demand. - * This assumes a linear relationship between interest rate and demand, and aims to halve the difference between - * current demand and the target supply - */ - private double recalculateInterestRate() { - double rate = getMortgageInterestRate() + 0.5*(supplyVal - supplyTarget)/dDemand_dInterest; - if (rate < baseRate) rate = baseRate; - return rate; - } - - /** - * Get the interest rate on mortgages. - */ - double getMortgageInterestRate() { return baseRate + interestSpread; } - - - /** - * Set the interest rate on mortgages - */ - private void setMortgageInterestRate(double rate) { - interestSpread = rate - baseRate; - recalculateMonthlyPaymentFactor(); - } - - /** - * Compute the monthly payment factor, i.e., the monthly payment on a mortgage as a fraction of the mortgage - * principal for both BTL (interest-only) and non-BTL mortgages. - */ - private void recalculateMonthlyPaymentFactor() { - double r = getMortgageInterestRate()/config.constants.MONTHS_IN_YEAR; - monthlyPaymentFactor = r/(1.0 - Math.pow(1.0 + r, -config.derivedParams.N_PAYMENTS)); - monthlyPaymentFactorBTL = r; - } - - /** - * Get the monthly payment factor, i.e., the monthly payment on a mortgage as a fraction of the mortgage principal. - */ - private double getMonthlyPaymentFactor(boolean isHome) { - if (isHome) { - return monthlyPaymentFactor; // Monthly payment factor to pay off the principal in N_PAYMENTS - } else { - return monthlyPaymentFactorBTL; // Monthly payment factor for interest-only mortgages - } - } - - /** - * Method to arrange a Mortgage and get a MortgageAgreement object. - * - * @param h The household requesting the mortgage - * @param housePrice The price of the house that household h wants to buy - * @param isHome True if household h plans to live in the house (non-BTL mortgage) - * @return The MortgageApproval object, or NULL if the mortgage is declined - */ - MortgageAgreement requestLoan(Household h, double housePrice, double desiredDownPayment, boolean isHome, - House house) { - MortgageAgreement approval = requestApproval(h, housePrice, desiredDownPayment, isHome); - if(approval == null) return(null); - // --- if all's well, go ahead and arrange mortgage - supplyVal += approval.principal; - if(approval.principal > 0.0) { - mortgages.add(approval); - Model.creditSupply.recordLoan(h, approval, house); - if(isHome) { - ++nOOMortgages; - if(approval.principal/h.getAnnualGrossEmploymentIncome() > - Model.centralBank.getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome)) { - ++nOOMortgagesOverLTI; - } - } - } - return approval; - } - - /** - * Method to request a mortgage approval but not actually sign a mortgage contract. This is useful if you want to - * explore the details of the mortgage contract before deciding whether to actually go ahead and sign it. - * - * @param h The household requesting the mortgage - * @param housePrice The price of the house that household h wants to buy - * @param isHome True if household h plans to live in the house (non-BTL mortgage) - * @return The MortgageApproval object, or NULL if the mortgage is declined - */ - MortgageAgreement requestApproval(Household h, double housePrice, double desiredDownPayment, - boolean isHome) { - MortgageAgreement approval = new MortgageAgreement(h, !isHome); - double r = getMortgageInterestRate()/config.constants.MONTHS_IN_YEAR; // monthly interest rate - double lti_principal, affordable_principal, icr_principal; - double liquidWealth = h.getBankBalance(); // No home equity needs to be added here: home-movers always sell their homes before trying to buy new ones - - if(isHome) liquidWealth += h.getHomeEquity(); - - // --- LTV constraint - approval.principal = housePrice*getLoanToValueLimit(h.isFirstTimeBuyer(), isHome); - - if(isHome) { - // --- affordability constraint TODO: affordability for BTL? - affordable_principal = Math.max(0.0,config.CENTRAL_BANK_AFFORDABILITY_COEFF*h.getMonthlyNetTotalIncome()) - / getMonthlyPaymentFactor(isHome); - approval.principal = Math.min(approval.principal, affordable_principal); - - // --- lti constraint - lti_principal = h.getAnnualGrossEmploymentIncome()*getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome); - approval.principal = Math.min(approval.principal, lti_principal); - } else { - // --- BTL ICR constraint - icr_principal = Model.rentalMarketStats.getExpAvFlowYield()*housePrice - /(Model.centralBank.getInterestCoverRatioLimit(isHome)*config.CENTRAL_BANK_BTL_STRESSED_INTEREST); - approval.principal = Math.min(approval.principal, icr_principal); - } - - approval.downPayment = housePrice - approval.principal; - - if(liquidWealth < approval.downPayment) { - System.out.println("Failed down-payment constraint: bank balance = " + liquidWealth + " downpayment = " - + approval.downPayment); - System.exit(0); - } - // --- allow larger downpayments - if(desiredDownPayment < 0.0) desiredDownPayment = 0.0; - if(desiredDownPayment > liquidWealth) desiredDownPayment = liquidWealth; - if(desiredDownPayment > housePrice) desiredDownPayment = housePrice; - if(desiredDownPayment > approval.downPayment) { - approval.downPayment = desiredDownPayment; - approval.principal = housePrice - desiredDownPayment; - } - - approval.monthlyPayment = approval.principal* getMonthlyPaymentFactor(isHome); - approval.nPayments = config.derivedParams.N_PAYMENTS; - approval.monthlyInterestRate = r; - approval.purchasePrice = approval.principal + approval.downPayment; - - return approval; - } - - /** - * Find, for a given household, the maximum house price that this mortgage-lender is willing to approve a mortgage - * for. - * - * @param h The household applying for the mortgage - * @param isHome True if household h plans to live in the house (non-BTL mortgage) - * @return The maximum house price that this mortgage-lender is willing to approve a mortgage for - */ - double getMaxMortgage(Household h, boolean isHome) { - double max_price; - double affordability_max_price; // Affordability (disposable income) constraint for maximum house price - double lti_max_price; // Loan to income constraint for maximum house price - double icr_max_price; // Interest cover ratio constraint for maximum house price - double liquidWealth = h.getBankBalance(); // No home equity needs to be added here: households always sell their homes before trying to buy new ones - double max_downpayment = liquidWealth - 0.01; // Maximum down-payment the household could make, where 1 cent is subtracted to avoid rounding errors - - // LTV constraint: maximum house price the household could pay with the maximum mortgage the bank could provide - // to the household given the Loan-To-Value limit and the maximum down-payment the household could make - max_price = max_downpayment/(1.0 - getLoanToValueLimit(h.isFirstTimeBuyer(), isHome)); - - if(isHome) { // No LTI nor affordability constraints for BTL investors - // Affordability constraint - affordability_max_price = max_downpayment + Math.max(0.0, config.CENTRAL_BANK_AFFORDABILITY_COEFF - *h.getMonthlyNetTotalIncome())/getMonthlyPaymentFactor(isHome); - max_price = Math.min(max_price, affordability_max_price); - // Loan-To-Income constraint - lti_max_price = h.getAnnualGrossEmploymentIncome()*getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome) - + max_downpayment; - max_price = Math.min(max_price, lti_max_price); - } else { - // Interest-Cover-Ratio constraint - icr_max_price = max_downpayment/(1.0 - Model.rentalMarketStats.getExpAvFlowYield() - /(Model.centralBank.getInterestCoverRatioLimit(isHome)*config.CENTRAL_BANK_BTL_STRESSED_INTEREST)); - max_price = Math.min(max_price, icr_max_price); - } - - return max_price; - } - - /** - * This method removes a mortgage contract by removing it from the HashSet of mortgages - * - * @param mortgage The MortgageAgreement object to be removed - */ - void endMortgageContract(MortgageAgreement mortgage) { mortgages.remove(mortgage); } - - //----- Mortgage policy methods -----// - - /** - * Get the Loan-To-Value ratio limit applicable by this private bank to a given household. Note that this limit is - * self-imposed by the private bank. - * - * @param isFirstTimeBuyer True if the household is a first-time buyer - * @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) - * @return The Loan-To-Value ratio limit applicable to the given household - */ - private double getLoanToValueLimit(boolean isFirstTimeBuyer, boolean isHome) { - if(isHome) { - if(isFirstTimeBuyer) { - return firstTimeBuyerLTVLimit; - } else { - return ownerOccupierLTVLimit; - } - } - return buyToLetLTVLimit; - } - - /** - * Get the Loan-To-Income ratio limit applicable by this private bank to a given household. Note that Loan-To-Income - * constraints apply only to non-BTL applicants. The private bank always imposes its own (hard) limit. Apart from - * this, it also imposes the Central Bank regulated limit, which allows for a certain fraction of residential loans - * (mortgages for owner-occupying) to go over it (and thus it is considered here a soft limit). - * - * @param isFirstTimeBuyer true if the household is a first-time buyer - * @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) - * @return The Loan-To-Income ratio limit applicable to the given household - */ - private double getLoanToIncomeLimit(boolean isFirstTimeBuyer, boolean isHome) { - double limit; - // First compute the private bank self-imposed (hard) limit, which applies always - if (isHome) { - if (isFirstTimeBuyer) { - limit = firstTimeBuyerLTILimit; - } else { - limit = ownerOccupierLTILimit; - } - } else { - System.out.println("Strange: The bank is trying to impose a Loan-To-Income limit on a Buy-To-Let" + - "investor!"); - limit = 0.0; // Dummy limit value - } - // If the fraction of non-BTL mortgages already underwritten over the Central Bank LTI limit exceeds a certain - // maximum (regulated also by the Central Bank)... - if ((nOOMortgagesOverLTI + 1.0)/(nOOMortgages + 1.0) > - Model.centralBank.getMaxFractionOOMortgagesOverLTILimit()) { - // ... then compare the Central Bank LTI (soft) limit and that of the private bank (hard) and choose the smallest - limit = Math.min(limit, Model.centralBank.getLoanToIncomeLimit(isFirstTimeBuyer, isHome)); - } - return limit; - } -} diff --git a/src/main/java/housing/Bank.py b/src/main/java/housing/Bank.py new file mode 100644 index 00000000..386fa9ad --- /dev/null +++ b/src/main/java/housing/Bank.py @@ -0,0 +1,244 @@ +#************************************************************************************************* +# Class to represent a mortgage-lender (i.e. a bank or building society), whose only function is +# to approve/decline mortgage requests, so this is where mortgage-lending policy is encoded +# +# @author daniel, davidrpugh, Adrian Carro +# +#************************************************************************************************/ +class Bank: + def __init__(self): + # all unpaid mortgage contracts supplied by the bank + self.mortgages = [] + self.init() + + def init(self): + # General fields + self.config = Model.config # Passes the Model's configuration parameters object to a private field + + # Bank fields + interestSpread: float = 0.0 # current mortgage interest spread above base rate (monthly rate*12) + monthlyPaymentFactor: float = 0.0 # Monthly payment as a fraction of the principal for non-BTL mortgages + monthlyPaymentFactorBTL: float = 0.0 # Monthly payment as a fraction of the principal for BTL (interest-only) mortgages + self.baseRate: float = self.config.BANK_INITIAL_BASE_RATE + + # Credit supply strategy fields + supplyTarget: float = 0.0 # target supply of mortgage lending (pounds) + supplyVal: float = 0.0 # monthly supply of mortgage loans (pounds) + # TODO: Is this (dDemand_dInterest) a parameter? Shouldn't it depend somehow on other variables of the model? + self.dDemand_dInterest: float = 10 * 1e10 # rate of change of demand with interest rate (pounds) + nOOMortgagesOverLTI: int = 0 # Number of mortgages for owner-occupying that go over the LTI cap this time step + nOOMortgages: int = 0 # Total number of mortgages for owner-occupying + + # LTV internal policy thresholds + self.firstTimeBuyerLTVLimit: float = self.config.BANK_MAX_FTB_LTV # Loan-To-Value upper limit for first-time buyer mortgages + self.ownerOccupierLTVLimit: float = self.config.BANK_MAX_OO_LTV # Loan-To-Value upper limit for owner-occupying mortgages + self.buyToLetLTVLimit: float = self.config.BANK_MAX_BTL_LTV # Loan-To-Value upper limit for buy-to-let mortgages + + # LTI internal policy thresholds + self.firstTimeBuyerLTILimit: float = self.config.BANK_MAX_FTB_LTI # Loan-To-Income internal upper limit for first-time buyer mortgages + self.ownerOccupierLTILimit: float = self.config.BANK_MAX_OO_LTI # Loan-To-Income internal upper limit for owner-occupying mortgages + + self.mortgages.clear() + + # TODO: Is this (0.02) a parameter? Does it affect results in any significant way or is it just a dummy initialisation? + self.setMortgageInterestRate(0.02) + self.resetMonthlyCounters() + + # Redo all necessary monthly calculations and reset counters. + def step(self, totalPopulation: int) -> None: + supplyTarget = self.config.BANK_CREDIT_SUPPLY_TARGET * totalPopulation + self.setMortgageInterestRate(self.recalculateInterestRate()) + self.resetMonthlyCounters() + + # Reset counters for the next month + def resetMonthlyCounters(self) -> None: + self.supplyVal = 0.0 + self.nOOMortgagesOverLTI = 0 + self.nOOMortgages = 0 + + # Calculate the mortgage interest rate for next month based on the rate for this month and the resulting demand. + # This assumes a linear relationship between interest rate and demand, and aims to halve the difference between + # current demand and the target supply + def recalculateInterestRate(self) -> float: + rate = self.getMortgageInterestRate() + 0.5 * (self.supplyVal - self.supplyTarget) / self.dDemand_dInterest + if rate < self.baseRate: + rate = self.baseRate + return rate + + # Get the interest rate on mortgages. + def getMortgageInterestRate(self) -> float: + return self.baseRate + self.interestSpread + + # Set the interest rate on mortgages + def setMortgageInterestRate(self, rate: float) -> None: + self.interestSpread = rate - self.baseRate + self.recalculateMonthlyPaymentFactor() + + # Compute the monthly payment factor, i.e., the monthly payment on a mortgage as a fraction of the mortgage + # principal for both BTL (interest-only) and non-BTL mortgages. + def recalculateMonthlyPaymentFactor(self) -> None: + r = self.getMortgageInterestRate() / self.config.constants.MONTHS_IN_YEAR + self.monthlyPaymentFactor = r / (1.0 - pow(1.0 + r, -self.config.derivedParams.N_PAYMENTS)) + self.monthlyPaymentFactorBTL = r + + # Get the monthly payment factor, i.e., the monthly payment on a mortgage as a fraction of the mortgage principal. + def getMonthlyPaymentFactor(self, isHome: bool) -> float: + if isHome: + return self.monthlyPaymentFactor # Monthly payment factor to pay off the principal in N_PAYMENTS + else: + return self.monthlyPaymentFactorBTL # Monthly payment factor for interest-only mortgages + + # Method to arrange a Mortgage and get a MortgageAgreement object. + # @param h The household requesting the mortgage + # @param housePrice The price of the house that household h wants to buy + # @param isHome True if household h plans to live in the house (non-BTL mortgage) + # @return The MortgageApproval object, or NULL if the mortgage is declined + def requestLoan(self, h: Household, housePrice: float, desiredDownPayment: float, isHome: bool, + house: House) -> MortgageAgreement: + MortgageAgreement approval = requestApproval(h, housePrice, desiredDownPayment, isHome) + if approval is None: + return None + # --- if all's well, go ahead and arrange mortgage + supplyVal += approval.principal + if approval.principal > 0.0: + mortgages.append(approval) + self.Model.creditSupply.recordLoan(h, approval, house) + if isHome: + self.nOOMortgages += 1 + if approval.principal / h.getAnnualGrossEmploymentIncome() > self.Model.centralBank.getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome): + self.nOOMortgagesOverLTI += 1 + return approval + + # Method to request a mortgage approval but not actually sign a mortgage contract. This is useful if you want to + # explore the details of the mortgage contract before deciding whether to actually go ahead and sign it. + # + # @param h The household requesting the mortgage + # @param housePrice The price of the house that household h wants to buy + # @param isHome True if household h plans to live in the house (non-BTL mortgage) + # @return The MortgageApproval object, or NULL if the mortgage is declined + def requestApproval(self, h: Household, housePrice: float, desiredDownPayment: float, + isHome: bool) -> MortgageAgreement: + approval = MortgageAgreement(h, not isHome) + r = self.getMortgageInterestRate() / self.config.constants.MONTHS_IN_YEAR # monthly interest rate + double lti_principal, affordable_principal, icr_principal + liquidWealth: float = h.getBankBalance() # No home equity needs to be added here: home-movers always sell their homes before trying to buy new ones + + if isHome: + liquidWealth += h.getHomeEquity() + + # --- LTV constraint + approval.principal = housePrice * self.getLoanToValueLimit(h.isFirstTimeBuyer(), isHome) + + if isHome: + # --- affordability constraint TODO: affordability for BTL? + affordable_principal = max(0.0, self.config.CENTRAL_BANK_AFFORDABILITY_COEFF * h.getMonthlyNetTotalIncome()) / self.getMonthlyPaymentFactor(isHome) + approval.principal = min(approval.principal, affordable_principal) + + # --- lti constraint + lti_principal = h.getAnnualGrossEmploymentIncome() * self.getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome) + approval.principal = min(approval.principal, lti_principal) + else: + # --- BTL ICR constraint + icr_principal = self.Model.rentalMarketStats.getExpAvFlowYield() * housePrice / (self.Model.centralBank.getInterestCoverRatioLimit(isHome) * self.config.CENTRAL_BANK_BTL_STRESSED_INTEREST) + approval.principal = min(approval.principal, icr_principal) + + approval.downPayment = housePrice - approval.principal + + if liquidWealth < approval.downPayment: + print("Failed down-payment constraint: bank balance = " + str(liquidWealth) + " downpayment = " + str(approval.downPayment)) + exit() + + # --- allow larger downpayments + if desiredDownPayment < 0.0: + desiredDownPayment = 0.0 + if desiredDownPayment > liquidWealth: + desiredDownPayment = liquidWealth + if desiredDownPayment > housePrice: + desiredDownPayment = housePrice + if desiredDownPayment > approval.downPayment: + approval.downPayment = desiredDownPayment + approval.principal = housePrice - desiredDownPayment + + approval.monthlyPayment = approval.principal * self.getMonthlyPaymentFactor(isHome) + approval.nPayments = self.config.derivedParams.N_PAYMENTS + approval.monthlyInterestRate = r + approval.purchasePrice = approval.principal + approval.downPayment + + return approval + + #* Find, for a given household, the maximum house price that this mortgage-lender is willing to approve a mortgage + #* for. + #* + #* @param h The household applying for the mortgage + #* @param isHome True if household h plans to live in the house (non-BTL mortgage) + #* @return The maximum house price that this mortgage-lender is willing to approve a mortgage for + def getMaxMortgage(self, h: Household, isHome: bool) -> float: + affordability_max_price: float # Affordability (disposable income) constraint for maximum house price + lti_max_price: float # Loan to income constraint for maximum house price + icr_max_price: float # Interest cover ratio constraint for maximum house price + liquidWealth: float = h.getBankBalance() # No home equity needs to be added here: households always sell their homes before trying to buy new ones + max_downpayment: float = liquidWealth - 0.01 # Maximum down-payment the household could make, where 1 cent is subtracted to avoid rounding errors + + # LTV constraint: maximum house price the household could pay with the maximum mortgage the bank could provide + # to the household given the Loan-To-Value limit and the maximum down-payment the household could make + max_price: float = max_downpayment / (1.0 - self.getLoanToValueLimit(h.isFirstTimeBuyer(), isHome)) + + if isHome: # No LTI nor affordability constraints for BTL investors + # Affordability constraint + affordability_max_price = max_downpayment + max(0.0, self.config.CENTRAL_BANK_AFFORDABILITY_COEFF * h.getMonthlyNetTotalIncome()) / self.getMonthlyPaymentFactor(isHome) + max_price = min(max_price, affordability_max_price) + # Loan-To-Income constraint + lti_max_price = h.getAnnualGrossEmploymentIncome() * self.getLoanToIncomeLimit(h.isFirstTimeBuyer(), isHome) + max_downpayment + max_price = min(max_price, lti_max_price) + else: + # Interest-Cover-Ratio constraint + icr_max_price = max_downpayment / (1.0 - self.Model.rentalMarketStats.getExpAvFlowYield() / (self.Model.centralBank.getInterestCoverRatioLimit(isHome) * self.config.CENTRAL_BANK_BTL_STRESSED_INTEREST)) + max_price = min(max_price, icr_max_price) + + return max_price + + # This method removes a mortgage contract by removing it from the HashSet of mortgages + # @param mortgage The MortgageAgreement object to be removed + def endMortgageContract(self, mortgage: MortgageAgreement) -> None: + self.mortgages.remove(mortgage) + + #----- Mortgage policy methods -----# + + # Get the Loan-To-Value ratio limit applicable by this private bank to a given household. Note that this limit is + # self-imposed by the private bank. + # + # @param isFirstTimeBuyer True if the household is a first-time buyer + # @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) + # @return The Loan-To-Value ratio limit applicable to the given household + def getLoanToValueLimit(isFirstTimeBuyer: bool, isHome: bool) -> float: + if isHome: + if isFirstTimeBuyer: + return self.firstTimeBuyerLTVLimit + else: + return self.ownerOccupierLTVLimit + return self.buyToLetLTVLimit + + # Get the Loan-To-Income ratio limit applicable by this private bank to a given household. Note that Loan-To-Income + # constraints apply only to non-BTL applicants. The private bank always imposes its own (hard) limit. Apart from + # this, it also imposes the Central Bank regulated limit, which allows for a certain fraction of residential loans + # (mortgages for owner-occupying) to go over it (and thus it is considered here a soft limit). + # + # @param isFirstTimeBuyer true if the household is a first-time buyer + # @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) + # @return The Loan-To-Income ratio limit applicable to the given household + def getLoanToIncomeLimit(self, isFirstTimeBuyer: bool, isHome: bool) -> float: + # First compute the private bank self-imposed (hard) limit, which applies always + if isHome: + if isFirstTimeBuyer: + limit = firstTimeBuyerLTILimit + else: + limit = ownerOccupierLTILimit + else: + print("Strange: The bank is trying to impose a Loan-To-Income limit on a Buy-To-Let investor!") + limit = 0.0 # Dummy limit value + # If the fraction of non-BTL mortgages already underwritten over the Central Bank LTI limit exceeds a certain + # maximum (regulated also by the Central Bank)... + if (self.nOOMortgagesOverLTI + 1.0) / (self.nOOMortgages + 1.0) > self.Model.centralBank.getMaxFractionOOMortgagesOverLTILimit(): + # ... then compare the Central Bank LTI (soft) limit and that of the private bank (hard) and choose the smallest + limit = min(limit, self.Model.centralBank.getLoanToIncomeLimit(isFirstTimeBuyer, isHome)) + return limit diff --git a/src/main/java/housing/CentralBank.java b/src/main/java/housing/CentralBank.java deleted file mode 100644 index d76958d3..00000000 --- a/src/main/java/housing/CentralBank.java +++ /dev/null @@ -1,117 +0,0 @@ -package housing; - -/************************************************************************************************** - * Class to represent the mortgage policy regulator or Central Bank. It reads a number of policy - * thresholds from the config object into local variables with the purpose of allowing for dynamic - * policies varying those thresholds over time. - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ - -public class CentralBank { - - //------------------// - //----- Fields -----// - //------------------// - - // General fields - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - - // LTI policy thresholds - private double firstTimeBuyerLTILimit; // Loan-To-Income upper limit for first-time buying mortgages - private double ownerOccupierLTILimit; // Loan-To-Income upper limit for owner-occupying mortgages - private double maxFractionOOMortgagesOverLTILimit; // Fraction of owner-occupying mortgages allowed to exceed the Loan-To-Income limit - - // ICR policy thresholds - private double interestCoverRatioLimit; // Ratio of expected rental yield over interest monthly payment under stressed interest conditions - private double interestCoverRatioStressedRate; // Stressed interest rate used for Interest-Cover-Ratio assessments - - //-------------------// - //----- Methods -----// - //-------------------// - - void init() { - // Setup initial LTI policy thresholds - firstTimeBuyerLTILimit = config.CENTRAL_BANK_MAX_FTB_LTI; - ownerOccupierLTILimit = config.CENTRAL_BANK_MAX_OO_LTI; - maxFractionOOMortgagesOverLTILimit = config.CENTRAL_BANK_FRACTION_OVER_MAX_LTI; - // Setup initial ICR policy thresholds - interestCoverRatioLimit = config.CENTRAL_BANK_MAX_ICR; - interestCoverRatioStressedRate = config.CENTRAL_BANK_BTL_STRESSED_INTEREST; - } - - /** - * This method implements the policy strategy of the Central Bank - * - * @param coreIndicators The current value of the core indicators - */ - public void step(collectors.CoreIndicators coreIndicators) { - /* Use this method to express the policy strategy of the central bank by setting the value of the various limits - in response to the current value of the core indicators. - - Example policy: if house price growth is greater than 0.001 then FTB LTV limit is 0.75 otherwise (if house - price growth is less than or equal to 0.001) FTB LTV limit is 0.95 - Example code: - if(coreIndicators.getHousePriceGrowth() > 0.001) { - firstTimeBuyerLTVLimit = 0.75; - } else { - firstTimeBuyerLTVLimit = 0.95; - } - */ - } - - /** - * Get the Loan-To-Income ratio limit applicable to a given household. Note that Loan-To-Income constraints apply - * only to non-BTL applicants. The private bank always imposes its own limit. Apart from this, it also imposes the - * Central Bank regulated limit, which allows for a certain fraction of residential loans (mortgages for - * owner-occupying) to go over it - * - * @param isFirstTimeBuyer True if the household is first-time buyer - * @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) - * @return Loan-To-Income ratio limit applicable to the household - */ - double getLoanToIncomeLimit(boolean isFirstTimeBuyer, boolean isHome) { - if (isHome) { - if (isFirstTimeBuyer) { - return firstTimeBuyerLTILimit; - } else { - return ownerOccupierLTILimit; - } - } else { - System.out.println("Strange: The Central Bank is trying to impose a Loan-To-Income limit on a Buy-To-Let" + - "investor!"); - return 0.0; // Dummy return statement - } - } - - /** - * Get the maximum fraction of mortgages to owner-occupying households that can go over the Loan-To-Income limit - */ - double getMaxFractionOOMortgagesOverLTILimit() { return maxFractionOOMortgagesOverLTILimit; } - - /** - * Get the Interest-Cover-Ratio limit applicable to a particular household - */ - double getInterestCoverRatioLimit(boolean isHome) { - if (!isHome) { - return interestCoverRatioLimit; - } else { - System.out.println("Strange: Interest-Cover-Ratio limit is being imposed on an owner-occupying buyer!"); - return 0.0; // Dummy return statement - } - } - - /** - * Get the stressed interest rate for the Interest-Cover-Ratio assessment for a particular household - */ - public double getInterestCoverRatioStressedRate(boolean isHome) { - if (!isHome) { - return interestCoverRatioStressedRate; - } else { - System.out.println("Strange: Interest-Cover-Ratio rate is being used for assessing an owner-occupying" + - "buyer!"); - return 0.0; // Dummy return statement - } - } -} diff --git a/src/main/java/housing/CentralBank.py b/src/main/java/housing/CentralBank.py new file mode 100644 index 00000000..918467c4 --- /dev/null +++ b/src/main/java/housing/CentralBank.py @@ -0,0 +1,90 @@ +#************************************************************************************************* +# Class to represent the mortgage policy regulator or Central Bank. It reads a number of policy +# thresholds from the config object into local variables with the purpose of allowing for dynamic +# policies varying those thresholds over time. +# +# @author daniel, Adrian Carro +# +#************************************************************************************************/ +class CentralBank: + def __init__(self): + # General fields + self.config = Model.config # Passes the Model's configuration parameters object to a private field + + # LTI policy thresholds + self.firstTimeBuyerLTILimit: float = 0.0 # Loan-To-Income upper limit for first-time buying mortgages + self.ownerOccupierLTILimit: float = 0.0 # Loan-To-Income upper limit for owner-occupying mortgages + self.maxFractionOOMortgagesOverLTILimit: float = 0.0 # Fraction of owner-occupying mortgages allowed to exceed the Loan-To-Income limit + + # ICR policy thresholds + self.interestCoverRatioLimit: float = 0.0 # Ratio of expected rental yield over interest monthly payment under stressed interest conditions + self.interestCoverRatioStressedRate: float = 0.0 # Stressed interest rate used for Interest-Cover-Ratio assessments + self.init() + + #-------------------# + #----- Methods -----# + #-------------------# + def init(self): + # Setup initial LTI policy thresholds + self.firstTimeBuyerLTILimit = self.config.CENTRAL_BANK_MAX_FTB_LTI + self.ownerOccupierLTILimit = self.config.CENTRAL_BANK_MAX_OO_LTI + self.maxFractionOOMortgagesOverLTILimit = self.config.CENTRAL_BANK_FRACTION_OVER_MAX_LTI + # Setup initial ICR policy thresholds + self.interestCoverRatioLimit = self.config.CENTRAL_BANK_MAX_ICR + self.interestCoverRatioStressedRate = self.config.CENTRAL_BANK_BTL_STRESSED_INTEREST + + # This method implements the policy strategy of the Central Bank + # + # @param coreIndicators The current value of the core indicators + def step(self, coreIndicators: collectors.CoreIndicators): + pass + #/* Use this method to express the policy strategy of the central bank by setting the value of the various limits + # in response to the current value of the core indicators. + + # Example policy: if house price growth is greater than 0.001 then FTB LTV limit is 0.75 otherwise (if house + # price growth is less than or equal to 0.001) FTB LTV limit is 0.95 + # Example code: + # if(coreIndicators.getHousePriceGrowth() > 0.001) { + # firstTimeBuyerLTVLimit = 0.75 + # } else { + # firstTimeBuyerLTVLimit = 0.95 + # } + # */ + + # Get the Loan-To-Income ratio limit applicable to a given household. Note that Loan-To-Income constraints apply + # only to non-BTL applicants. The private bank always imposes its own limit. Apart from this, it also imposes the + # Central Bank regulated limit, which allows for a certain fraction of residential loans (mortgages for + # owner-occupying) to go over it + # + # @param isFirstTimeBuyer True if the household is first-time buyer + # @param isHome True if the mortgage is to buy a home for the household (non-BTL mortgage) + # @return Loan-To-Income ratio limit applicable to the household + def getLoanToIncomeLimit(self, isFirstTimeBuyer: bool, isHome: bool) -> float: + if isHome: + if isFirstTimeBuyer: + return self.firstTimeBuyerLTILimit + else: + return self.ownerOccupierLTILimit + else: + print("Strange: The Central Bank is trying to impose a Loan-To-Income limit on a Buy-To-Let investor!") + return 0.0 # Dummy return statement + + # Get the maximum fraction of mortgages to owner-occupying households that can go over the Loan-To-Income limit + def getMaxFractionOOMortgagesOverLTILimit(self) -> float: + return self.maxFractionOOMortgagesOverLTILimit + + # Get the Interest-Cover-Ratio limit applicable to a particular household + def getInterestCoverRatioLimit(self, isHome: bool) -> float: + if not isHome: + return self.interestCoverRatioLimit + else: + print("Strange: Interest-Cover-Ratio limit is being imposed on an owner-occupying buyer!") + return 0.0 # Dummy return statement + + # Get the stressed interest rate for the Interest-Cover-Ratio assessment for a particular household + def getInterestCoverRatioStressedRate(self, isHome: bool) -> float: + if not isHome: + return self.interestCoverRatioStressedRate + else: + print("Strange: Interest-Cover-Ratio rate is being used for assessing an owner-occupying buyer!") + return 0.0 # Dummy return statement diff --git a/src/main/java/housing/Config.java b/src/main/java/housing/Config.java deleted file mode 100644 index c2c2d6f6..00000000 --- a/src/main/java/housing/Config.java +++ /dev/null @@ -1,412 +0,0 @@ -package housing; - -import java.io.FileReader; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.Properties; -import java.lang.Integer; -import java.util.Set; - -/************************************************************************************************** - * Class to encapsulate all the configuration parameters of the model. It also contains all methods - * needed to read these parameter values from a configuration properties file. - * - * @author Adrian Carro - * @since 20/02/2017 - * - *************************************************************************************************/ -public class Config { - - //---------------------------------// - //----- Fields and subclasses -----// - //---------------------------------// - - /** Declaration of parameters **/ - - // General model control parameters - int SEED; // Seed for the random number generator - int N_STEPS; // Simulation duration in time steps - int TIME_TO_START_RECORDING; // Time steps before recording statistics (initialisation time) - int N_SIMS; // Number of simulations to run (monte-carlo) - boolean recordCoreIndicators; // True to write time series for each core indicator - boolean recordMicroData; // True to write micro data for each transaction made - - // House parameters - public int N_QUALITY; // Number of quality bands for houses - - // Housing market parameters - int DAYS_UNDER_OFFER; // Time (in days) that a house remains under offer - double BIDUP; // Smallest proportional increase in price that can cause a gazump - public double MARKET_AVERAGE_PRICE_DECAY; // Decay constant for the exponential moving average of sale prices - public double INITIAL_HPI; // Initial housing price index - double HPI_MEDIAN; // Median house price - public double HPI_SHAPE; // Shape parameter for the log-normal distribution of housing prices - public double RENT_GROSS_YIELD; // Profit margin for buy-to-let investors - - // Demographic parameters - public int TARGET_POPULATION; // Target number of households - public double FUTURE_BIRTH_RATE; // Future birth rate (births per year per capita), calibrated with flux of FTBs - - // Household parameters - double RETURN_ON_FINANCIAL_WEALTH; // Monthly percentage growth of financial investments - public int TENANCY_LENGTH_AVERAGE; // Average number of months a tenant will stay in a rented house - int TENANCY_LENGTH_EPSILON; // Standard deviation of the noise in determining the tenancy length - - // Household behaviour parameters: buy-to-let - double P_INVESTOR; // Prior probability of being (wanting to be) a BTL investor - double MIN_INVESTOR_PERCENTILE; // Minimum income percentile for a household to be a BTL investor - double FUNDAMENTALIST_CAP_GAIN_COEFF; // Weight that fundamentalists put on cap gain - double TREND_CAP_GAIN_COEFF; // Weight that trend-followers put on cap gain - double P_FUNDAMENTALIST; // Probability that a BTL investor is a fundamentalist versus a trend-follower - boolean BTL_YIELD_SCALING; // Chooses between two possible equations for BTL investors to make their buy/sell decisions - // Household behaviour parameters: rent - double DESIRED_RENT_INCOME_FRACTION; // Desired proportion of income to be spent on rent - double PSYCHOLOGICAL_COST_OF_RENTING; // Annual psychological cost of renting - double SENSITIVITY_RENT_OR_PURCHASE; // Sensitivity parameter of the decision between buying and renting - // Household behaviour parameters: general - double BANK_BALANCE_FOR_CASH_DOWNPAYMENT; // If bankBalance/housePrice is above this, payment will be made fully in cash - double HPA_EXPECTATION_FACTOR; // Weight assigned to current trend when computing expectations - public int HPA_YEARS_TO_CHECK; // Number of years of the HPI record to check when computing the annual HPA - double HOLD_PERIOD; // Average period, in years, for which owner-occupiers hold their houses - // Household behaviour parameters: sale price reduction - double P_SALE_PRICE_REDUCE; // Monthly probability of reducing the price of a house on the market - double REDUCTION_MU; // Mean percentage reduction for prices of houses on the market - double REDUCTION_SIGMA; // Standard deviation of percentage reductions for prices of houses on the market - // Household behaviour parameters: consumption - double CONSUMPTION_FRACTION; // Fraction of monthly budget for consumption (monthly budget = bank balance - minimum desired bank balance) - double ESSENTIAL_CONSUMPTION_FRACTION; // Fraction of Government support necessarily spent monthly by all households as essential consumption - // Household behaviour parameters: initial sale price - double SALE_MARKUP; // Initial markup over average price of same quality houses - double SALE_WEIGHT_DAYS_ON_MARKET; // Weight of the days-on-market effect - double SALE_EPSILON; // Standard deviation of the noise - // Household behaviour parameters: buyer's desired expenditure - double BUY_SCALE; // Scale, number of annual salaries the buyer is willing to spend for buying a house - double BUY_WEIGHT_HPA; // Weight given to house price appreciation when deciding how much to spend for buying a house - double BUY_EPSILON; // Standard deviation of the noise - // Household behaviour parameters: demand rent - double RENT_MARKUP; // Markup over average rent demanded for houses of the same quality - double RENT_EQ_MONTHS_ON_MARKET; // Number of months on the market in an equilibrium situation - double RENT_EPSILON; // Standard deviation of the noise - public double RENT_MAX_AMORTIZATION_PERIOD; // Maximum period BTL investors are ready to wait to get back their investment, this determines their minimum demanded rent - double RENT_REDUCTION; // Percentage reduction of demanded rent for every month the property is in the market, not rented - // Household behaviour parameters: downpayment - double DOWNPAYMENT_FTB_SCALE; // Scale parameter for the log-normal distribution of downpayments by first-time-buyers - double DOWNPAYMENT_FTB_SHAPE; // Shape parameter for the log-normal distribution of downpayments by first-time-buyers - double DOWNPAYMENT_OO_SCALE; // Scale parameter for the log-normal distribution of downpayments by owner-occupiers - double DOWNPAYMENT_OO_SHAPE; // Shape parameter for the log-normal distribution of downpayments by owner-occupiers - double DOWNPAYMENT_MIN_INCOME; // Minimum income percentile to consider any downpayment, below this level, downpayment is set to 0 - double DOWNPAYMENT_BTL_MEAN; // Average downpayment, as percentage of house price, by but-to-let investors - double DOWNPAYMENT_BTL_EPSILON; // Standard deviation of the noise - // Household behaviour parameters: desired bank balance - double DESIRED_BANK_BALANCE_ALPHA; - double DESIRED_BANK_BALANCE_BETA; - double DESIRED_BANK_BALANCE_EPSILON; - // Household behaviour parameters: selling decision - double DECISION_TO_SELL_ALPHA; // Weight of houses per capita effect - double DECISION_TO_SELL_BETA; // Weight of interest rate effect - double DECISION_TO_SELL_HPC; // TODO: fudge parameter, explicitly explained otherwise in the paper - double DECISION_TO_SELL_INTEREST; // TODO: fudge parameter, explicitly explained otherwise in the paper - // Household behaviour parameters: BTL buy/sell choice - double BTL_CHOICE_INTENSITY; // Shape parameter, or intensity of choice on effective yield - double BTL_CHOICE_MIN_BANK_BALANCE; // Minimun bank balance, as a percentage of the desired bank balance, to buy new properties - - // Bank parameters - int MORTGAGE_DURATION_YEARS; // Mortgage duration in years - double BANK_INITIAL_BASE_RATE; // Bank initial base-rate (currently remains unchanged) - double BANK_CREDIT_SUPPLY_TARGET; // Bank's target supply of credit per household per month - double BANK_MAX_FTB_LTV; // Maximum LTV ratio that the private bank would allow for first-time-buyers - double BANK_MAX_OO_LTV; // Maximum LTV ratio that the private bank would allow for owner-occupiers - double BANK_MAX_BTL_LTV; // Maximum LTV ratio that the private bank would allow for BTL investors - double BANK_MAX_FTB_LTI; // Maximum LTI ratio that the private bank would allow for first-time-buyers (private bank's hard limit) - double BANK_MAX_OO_LTI; // Maximum LTI ratio that the private bank would allow for owner-occupiers (private bank's hard limit) - - - // Central bank parameters - double CENTRAL_BANK_MAX_FTB_LTI; // Maximum LTI ratio that the bank would allow for first-time-buyers when not regulated - double CENTRAL_BANK_MAX_OO_LTI; // Maximum LTI ratio that the bank would allow for owner-occupiers when not regulated - double CENTRAL_BANK_FRACTION_OVER_MAX_LTI; // Maximum fraction of mortgages that the bank can give over the LTI ratio limit - double CENTRAL_BANK_AFFORDABILITY_COEFF; // Maximum fraction of the household's income to be spent on mortgage repayments under stressed conditions - double CENTRAL_BANK_BTL_STRESSED_INTEREST; // Interest rate under stressed condition for BTL investors when calculating interest coverage ratios (ICR) - double CENTRAL_BANK_MAX_ICR; // Interest coverage ratio (ICR) limit imposed by the central bank - - // Construction sector parameters - double CONSTRUCTION_HOUSES_PER_HOUSEHOLD; // Target ratio of houses per household - - // Government parameters - double GOVERNMENT_GENERAL_PERSONAL_ALLOWANCE; // General personal allowance to be deducted when computing taxable income - double GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE; // Limit of income above which personal allowance starts to decrease £1 for every £2 of income above this limit - public double GOVERNMENT_MONTHLY_INCOME_SUPPORT; // Minimum monthly earnings for a married couple from income support - - // Collectors parameters - double UK_HOUSEHOLDS; // Approximate number of households in UK, used to scale up results for core indicators - boolean MORTGAGE_DIAGNOSTICS_ACTIVE; // Whether to record mortgage statistics - - /** Declaration of addresses **/ // They must be public to be accessed from data package - - // Data addresses: Government - public String DATA_TAX_RATES; // Address for tax bands and rates data - public String DATA_NATIONAL_INSURANCE_RATES; // Address for national insurance bands and rates data - - // Data addresses: EmploymentIncome - public String DATA_INCOME_GIVEN_AGE; // Address for conditional probability of income band given age band - - // Data addresses: Demographics - public String DATA_HOUSEHOLD_AGE_AT_BIRTH_PDF; // Address for pdf of household representative person's age at household birth - public String DATA_DEATH_PROB_GIVEN_AGE; // Address for data on the probability of death given the age of the household representative person - - /** Construction of objects to contain derived parameters and constants **/ - - // Create object containing all constants - public Config.Constants constants = new Constants(); - - // Finally, create object containing all derived parameters - public Config.DerivedParams derivedParams = new DerivedParams(); - - /** - * Class to contain all parameters which are not read from the configuration (.properties) file, but derived, - * instead, from these configuration parameters - */ - public class DerivedParams { - // Housing market parameters - public int HPI_RECORD_LENGTH; // Number of months to record HPI (to compute price growth at different time scales) - double MONTHS_UNDER_OFFER; // Time (in months) that a house remains under offer - double T; // Characteristic number of data-points over which to average market statistics - public double E; // Decay constant for averaging days on market (in transactions) - public double G; // Decay constant for averageListPrice averaging (in transactions) - public double HPI_LOG_MEDIAN; // Logarithmic median house price (scale parameter of the log-normal distribution) - double HPI_REFERENCE; // Mean of reference house prices - // Household behaviour parameters: general - double MONTHLY_P_SELL; // Monthly probability for owner-occupiers to sell their houses - // Bank parameters - int N_PAYMENTS; // Number of monthly repayments (mortgage duration in months) - // House rental market parameters - public double K; // Decay factor for exponential moving average of gross yield from rentals (averageSoldGrossYield) - public double KL; // Decay factor for long-term exponential moving average of gross yield from rentals (longTermAverageGrossYield) - // Collectors parameters - double AFFORDABILITY_DECAY; // Decay constant for the exponential moving average of affordability - - public double getAffordabilityDecay() { - return AFFORDABILITY_DECAY; - } - - public double getE() { - return E; - } - - public int getHPIRecordLength() { - return HPI_RECORD_LENGTH; - } - - public double getHPIReference() { - return HPI_REFERENCE; - } - - } - - /** - * Class to contain all constants (not read from the configuration file nor derived from it) - */ - public class Constants { - final public int DAYS_IN_MONTH = 30; - final public int MONTHS_IN_YEAR = 12; - } - - //------------------------// - //----- Constructors -----// - //------------------------// - - /** - * Empty constructor, mainly used for copying local instances of the Model Config instance into other classes - */ - public Config () {} - - /** - * Constructor with full initialization, used only for the original Model Config instance - */ - public Config (String configFileName) { - getConfigValues(configFileName); - } - - //-------------------// - //----- Methods -----// - //-------------------// - - public boolean isMortgageDiagnosticsActive() { - return MORTGAGE_DIAGNOSTICS_ACTIVE; - } - - public double getUKHouseholds() { - return UK_HOUSEHOLDS; - } - - public double getPInvestor() { - return P_INVESTOR; - } - - /** - * Method to read configuration parameters from a configuration (.properties) file - * @param configFileName String with name of configuration (.properties) file (address inside source folder) - */ - private void getConfigValues(String configFileName) { - // Try-with-resources statement - try (FileReader fileReader = new FileReader(configFileName)) { - Properties prop = new Properties(); - prop.load(fileReader); - // Check that all parameters declared in the configuration (.properties) file are also declared in this class - try { - Set setOfFields = new HashSet<>(); - for (Field field : this.getClass().getDeclaredFields()) { - setOfFields.add(field.getName()); - } - for (String property: prop.stringPropertyNames()) { - if (!setOfFields.contains(property)) { - throw new UndeclaredPropertyException(property); - } - } - } catch (UndeclaredPropertyException upe) { - upe.printStackTrace(); - } - // Run through all the fields of the Class using reflection - for (Field field : this.getClass().getDeclaredFields()) { - try { - // For int fields, parse the int with appropriate exception handling - if (field.getType().toString().equals("int")) { - try { - if (prop.getProperty(field.getName()) == null) {throw new FieldNotInFileException(field);} - field.set(this, Integer.parseInt(prop.getProperty(field.getName()))); - } catch (NumberFormatException nfe) { - System.out.println("Exception " + nfe + " while trying to parse the field " + - field.getName() + " for an integer"); - nfe.printStackTrace(); - } catch (IllegalAccessException iae) { - System.out.println("Exception " + iae + " while trying to set the field " + - field.getName()); - iae.printStackTrace(); - } catch (FieldNotInFileException fnife) { - fnife.printStackTrace(); - } - // For double fields, parse the double with appropriate exception handling - } else if (field.getType().toString().equals("double")) { - try { - if (prop.getProperty(field.getName()) == null) {throw new FieldNotInFileException(field);} - field.set(this, Double.parseDouble(prop.getProperty(field.getName()))); - } catch (NumberFormatException nfe) { - System.out.println("Exception " + nfe + " while trying to parse the field " + - field.getName() + " for an double"); - nfe.printStackTrace(); - } catch (IllegalAccessException iae) { - System.out.println("Exception " + iae + " while trying to set the field " + - field.getName()); - iae.printStackTrace(); - } catch (FieldNotInFileException fnife) { - fnife.printStackTrace(); - } - // For boolean fields, parse the boolean with appropriate exception handling - } else if (field.getType().toString().equals("boolean")) { - try { - if (prop.getProperty(field.getName()) == null) {throw new FieldNotInFileException(field);} - if (prop.getProperty(field.getName()).equals("true") || - prop.getProperty(field.getName()).equals("false")) { - field.set(this, Boolean.parseBoolean(prop.getProperty(field.getName()))); - } else { - throw new BooleanFormatException("For input string \"" + - prop.getProperty(field.getName()) + "\""); - } - } catch (BooleanFormatException bfe) { - System.out.println("Exception " + bfe + " while trying to parse the field " + - field.getName() + " for a boolean"); - bfe.printStackTrace(); - } catch (IllegalAccessException iae) { - System.out.println("Exception " + iae + " while trying to set the field " + - field.getName()); - iae.printStackTrace(); - } catch (FieldNotInFileException fnife) { - fnife.printStackTrace(); - } - // For string fields, parse the string with appropriate exception handling - } else if (field.getType().toString().equals("class java.lang.String")) { - try { - if (prop.getProperty(field.getName()) == null) {throw new FieldNotInFileException(field);} - field.set(this, prop.getProperty(field.getName()).replace("\"", "").replace("\'", "")); - } catch (IllegalAccessException iae) { - System.out.println("Exception " + iae + " while trying to set the field " + - field.getName()); - iae.printStackTrace(); - } catch (FieldNotInFileException fnife) { - fnife.printStackTrace(); - } - // For unrecognised field types, except derivedParams and constants, throw exception - } else if (!field.getName().equals("derivedParams") && !field.getName().equals("constants")) { - throw new UnrecognisedFieldTypeException(field); - } - } catch (UnrecognisedFieldTypeException ufte) { - ufte.printStackTrace(); - } - } - } catch (IOException ioe) { - System.out.println("Exception " + ioe + " while trying to read file '" + configFileName + "'"); - ioe.printStackTrace(); - } - // Finally, compute and set values for all derived parameters - setDerivedParams(); - } - - /** - * Method to compute and set values for all derived parameters - */ - private void setDerivedParams() { - // Housing market parameters - derivedParams.HPI_RECORD_LENGTH = HPA_YEARS_TO_CHECK*constants.MONTHS_IN_YEAR + 3; // Plus three months in a quarter - derivedParams.MONTHS_UNDER_OFFER = (double)DAYS_UNDER_OFFER/constants.DAYS_IN_MONTH; - derivedParams.T = 0.02*TARGET_POPULATION; // TODO: Clarify where does this 0.2 come from, and provide explanation for this formula - derivedParams.E = Math.exp(-1.0/derivedParams.T); // TODO: Provide explanation for this formula - derivedParams.G = Math.exp(-N_QUALITY/derivedParams.T); // TODO: Provide explanation for this formula - derivedParams.HPI_LOG_MEDIAN = Math.log(HPI_MEDIAN); - derivedParams.HPI_REFERENCE = Math.exp(derivedParams.HPI_LOG_MEDIAN + HPI_SHAPE*HPI_SHAPE/2.0); - // Household behaviour parameters: general - derivedParams.MONTHLY_P_SELL = 1.0/(HOLD_PERIOD*constants.MONTHS_IN_YEAR); - // Bank parameters - derivedParams.N_PAYMENTS = MORTGAGE_DURATION_YEARS*constants.MONTHS_IN_YEAR; - // House rental market parameters - derivedParams.K = Math.exp(-10000.0/(TARGET_POPULATION*50.0)); // TODO: Are these decay factors well-suited? Any explanation, reasoning behind the numbers chosen? - derivedParams.KL = Math.exp(-10000.0/(TARGET_POPULATION*50.0*200.0)); // TODO: Also, they are not reported in the paper! - // Collectors parameters - derivedParams.AFFORDABILITY_DECAY = Math.exp(-1.0/100.0); - } - - /** - * Equivalent to NumberFormatException for detecting problems when parsing for boolean values - */ - public class BooleanFormatException extends RuntimeException { - BooleanFormatException(String message) { super(message); } - } - - /** - * Exception for detecting unrecognised (not implemented) field types - */ - public class UnrecognisedFieldTypeException extends Exception { - UnrecognisedFieldTypeException(Field field) { - super("Field type \"" + field.getType().toString() + "\", found at field \"" + field.getName() + - "\", could not be recognised as any of the implemented types"); - } - } - - /** - * Exception for detecting fields declared in the code but not present in the config.properties file - */ - public class FieldNotInFileException extends Exception { - FieldNotInFileException(Field field) { - super("Field \"" + field.getName() + "\" could not be found in the config.properties file"); - } - } - - /** - * Exception for detecting properties present in the config.properties file but not declared in the code - */ - public class UndeclaredPropertyException extends Exception { - UndeclaredPropertyException(Object property) { - super("Property \"" + property + "\" could not be found among the fields declared within the Config class"); - } - } -} diff --git a/src/main/java/housing/Config.py b/src/main/java/housing/Config.py new file mode 100644 index 00000000..df9f65a5 --- /dev/null +++ b/src/main/java/housing/Config.py @@ -0,0 +1,400 @@ +# Configuration file where all parameter values are set + +################################################## +################ General comments ################ +################################################## + +# Seed for random number generation is set by calling the program with argument +# "-seed ", where must be a positive integer. In the +# absence of this argument, seed is set from machine time + +#************************************************************************************************* +# Class to encapsulate all the configuration parameters of the model. It also contains all methods +# needed to read these parameter values from a configuration properties file. +# +# @author Adrian Carro +# @since 20/02/2017 +# +#************************************************************************************************/ +class Config: + #---------------------------------# + #----- Fields and subclasses -----# + #---------------------------------# + + #** Declaration of parameters **/ + + ################################################## + ######## General model control parameters ######## + ################################################## + + # Seed for random number generator (int) + SEED = 1 + # Simulation duration in time steps (int) + N_STEPS = 6000 + # Time steps before recording statistics, initialisation time (int) + TIME_TO_START_RECORDING = 0 + # Number of simulations to run (int) + N_SIMS = 1 + # True to write time series for each core indicator (boolean) + recordCoreIndicators = False + # True to write micro data for each transaction made (boolean) + recordMicroData = False + + ################################################## + ################ House parameters ################ + ################################################## + + # Number of quality bands for houses (int) + N_QUALITY = 48 + + ################################################## + ########### Housing market parameters ############ + ################################################## + + # Time, in days, that a house remains under offer (int) + DAYS_UNDER_OFFER = 7 + # Smallest proportional increase in price that can cause a gazump (double) + BIDUP = 1.0075 + # Decay constant for the exponential moving average of sale prices (double) + MARKET_AVERAGE_PRICE_DECAY = 0.25 + # Initial housing price index, HPI (double) + # TODO: Reference for this value and justification for this discounting are needed! (the parameter is declared, but no source nor justification is given) + INITIAL_HPI = 0.8 + # Median house price (double) + # TODO: Replace by the 2011 value + HPI_MEDIAN = 195000.0 + # Shape parameter for the log-normal distribution of housing prices, taken from the ONS (2013) house price index data tables, table 34 (double) + # TODO: Replace by the 2011 value, compare with Land Registry Paid Price data and decide whether to use real distribution + HPI_SHAPE = 0.555 + # Profit margin for buy-to-let investors (double) + # Yield on rent had average 6% between 2009/01 and 2015/01, minimum in 2009/10 maximum in 2012/04 peak-to-peak amplitude of 0.4%. Source: Bank of England, unpublished analysis based on Zoopla/Land Registry matching (Philippe Bracke) + RENT_GROSS_YIELD = 0.05 + + ################################################## + ############# Demographic parameters ############# + ################################################## + + # Target number of households (int) + TARGET_POPULATION = 10000 + # Future birth rate (births per year per capita), calibrated with flux of FTBs, Council of Mortgage Lenders Regulated Mortgage Survey, 2015 (double) + # TODO: Also described as "calibrated against average advances to first time buyers, core indicators 1987-2006". Check which explanation holds and replace by the 2011 value. + FUTURE_BIRTH_RATE = 0.018 + + ################################################## + ############## Household parameters ############## + ################################################## + + # Monthly percentage growth of financial investments (double) + RETURN_ON_FINANCIAL_WEALTH = 0.002 + # Average number of months a tenant will stay in a rented house (int) + # Source: ARLA - Members survey of the Private Rented Sector Q4 2013 + TENANCY_LENGTH_AVERAGE = 18 + # Standard deviation of the noise in determining the tenancy length (int) + TENANCY_LENGTH_EPSILON = 6 + + ################################################## + ######### Household behaviour parameters ######### + ################################################## + + ############# Buy-To-Let parameters ############## + # Prior probability of being (wanting to be) a BTL investor (double) + # TODO: Shouldn't this be 4% according to the article? + P_INVESTOR = 0.16 + # Minimum income percentile for a household to be a BTL investor (double) + MIN_INVESTOR_PERCENTILE = 0.5 + # Weight that fundamentalists put on cap gain (double) + FUNDAMENTALIST_CAP_GAIN_COEFF = 0.5 + # Weight that trend-followers put on cap gain (double) + TREND_CAP_GAIN_COEFF = 0.9 + # Probability that a BTL investor is a fundamentalist versus a trend-follower (double) + P_FUNDAMENTALIST = 0.5 + # Chooses between two possible equations for BTL investors to make their buy/sell decisions (boolean) + BTL_YIELD_SCALING = False + + ################ Rent parameters ################# + # Desired proportion of income to be spent on rent (double) + DESIRED_RENT_INCOME_FRACTION = 0.33 + # Annual psychological cost of renting (double) + # TODO: This value comes from 1.1/12.0... Where does that come from? + PSYCHOLOGICAL_COST_OF_RENTING = 0.0916666666667 + # Sensitivity parameter of the decision between buying and renting (double) + # TODO: This value comes from 1.0/3500.0... Where does that come from? + SENSITIVITY_RENT_OR_PURCHASE = 0.000285714285714 + + ############### General parameters ############### + # If the ratio between the buyer's bank balance and the house price is above this, + # payment will be made fully in cash (double) + # Calibrated against mortgage approval/housing transaction ratio, core indicators average 1987-2006 + # TODO: Find these sources and clarify this calibration! + BANK_BALANCE_FOR_CASH_DOWNPAYMENT = 2.0 + # Weight assigned to current trend when computing expectations + # Dampening or multiplier factor, depending on its value being <1 or >1, for the current trend when computing expectations as in + # HPI(t+DT) = HPI(t) + FACTOR*DT*dHPI/dt (double) + # TODO: According to John Muellbauer, this is a dampening factor (<1). Find a reference for this! + HPA_EXPECTATION_FACTOR = 0.5 + # Number of years of the HPI record to check when computing the annual HPA, i.e., how much backward looking households are (int) + HPA_YEARS_TO_CHECK = 1 + # Average period, in years, for which owner-occupiers hold their houses (double) + # British housing survey 2008 + HOLD_PERIOD = 11.0 + + ######### Sale price reduction parameters ######## + # This subsection was calibrated against Zoopla data at the BoE + # Monthly probability of reducing the price of a house on the market (double) + # This value comes from 1.0-0.945 + P_SALE_PRICE_REDUCE = 0.055 + # Mean percentage reduction for prices of houses on the market (double) + REDUCTION_MU = 1.603 + # Standard deviation of percentage reductions for prices of houses on the market (double) + REDUCTION_SIGMA = 0.617 + + ############# Comsumption parameters ############# + # Fraction of the monthly budget allocated for consumption, being the monthly + # budget equal to the bank balance minus the minimum desired bank balance (double) + CONSUMPTION_FRACTION = 0.5 + # Fraction of Government support representing the amount necessarily spent monthly by + # all households as essential consumption (double) + ESSENTIAL_CONSUMPTION_FRACTION = 0.8 + + ######### Initial sale price parameters ########## + # Initial markup over average price of same quality houses (double) + # TODO: Note says that, according to BoE calibration, this should be around 0.2. Check and solve this! + SALE_MARKUP = 0.04 + # Weight of the days-on-market effect (double) + SALE_WEIGHT_DAYS_ON_MARKET = 0.011 + # Standard deviation of the noise (double) + SALE_EPSILON = 0.05 + + ##### Buyer's desired expenditure parameters ##### + # Scale, number of annual salaries the buyer is willing to spend for buying a house (double) + # TODO: This has been macro-calibrated against owner-occupier LTI and LTV ration, core indicators average 1987-2006. Find sources! + BUY_SCALE = 4.5 + # Weight given to house price appreciation when deciding how much to spend for buying a house (double) + BUY_WEIGHT_HPA = 0.08 + # Standard deviation of the noise (double) + BUY_EPSILON = 0.14 + + ############ Demanded rent parameters ############ + # Markup over average rent demanded for houses of the same quality (double) + RENT_MARKUP = 0.00 + # Number of months on the market in an equilibrium situation (double) + RENT_EQ_MONTHS_ON_MARKET = 6.0 + # Standard deviation of the noise (double) + RENT_EPSILON = 0.05 + # Maximum period of time BTL investors are ready to wait to get back their investment through rents, + # this determines the minimum rent they are ready to accept (double) + # TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: This parameter and its associated mechanism are not declared in the article! Need to declare and reference! + RENT_MAX_AMORTIZATION_PERIOD = 20.833333333 + # Percentage reduction of demanded rent for every month the property is in the market, not rented (double) + RENT_REDUCTION = 0.05 + + ############# Downpayment parameters ############# + # Minimum income percentile to consider any downpayment, below this level, downpayment is set to 0 (double) + # TODO: Calibrated against PSD data, need clearer reference or disclose distribution! + DOWNPAYMENT_MIN_INCOME = 0.3 + # TODO: Both functional form and parameters are micro-calibrated against BoE data. Need reference or disclose distribution! + # Scale parameter for the log-normal distribution of downpayments by first-time-buyers (double) + DOWNPAYMENT_FTB_SCALE = 10.30 + # Shape parameter for the log-normal distribution of downpayments by first-time-buyers (double) + DOWNPAYMENT_FTB_SHAPE = 0.9093 + # Scale parameter for the log-normal distribution of downpayments by owner-occupiers (double) + DOWNPAYMENT_OO_SCALE = 11.155 + # Shape parameter for the log-normal distribution of downpayments by owner-occupiers (double) + DOWNPAYMENT_OO_SHAPE = 0.7538 + # Average downpayment, as percentage of house price, by but-to-let investors (double) + # TODO: Said to be calibrated to match LTV ratios, but no reference is given. Need reference! + # TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: Functional form slightly different to the one presented in the article + DOWNPAYMENT_BTL_MEAN = 0.3 + # Standard deviation of the noise (double) + DOWNPAYMENT_BTL_EPSILON = 0.1 + + ######## Desired bank balance parameters ######### + # Micro-calibrated to match the log-normal relationship between wealth and income from the Wealth and Assets Survey + # Log-normal function parameter (double) + DESIRED_BANK_BALANCE_ALPHA = -32.0013877 + # Log-normal function parameter (double) + DESIRED_BANK_BALANCE_BETA = 4.07 + # Standard deviation of a noise, it states a propensity to save (double) + DESIRED_BANK_BALANCE_EPSILON = 0.1 + + ########## Selling decision parameters ########### + # Weight of houses per capita effect + DECISION_TO_SELL_ALPHA = 4.0 + # Weight of interest rate effect + DECISION_TO_SELL_BETA = 5.0 + # TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: fudge parameter, explicitly explained otherwise in the article + DECISION_TO_SELL_HPC = 0.05 + # TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: fudge parameter, explicitly explained otherwise in the article + DECISION_TO_SELL_INTEREST = 0.03 + + ######### BTL buy/sell choice parameters ######### + # Shape parameter, or intensity of choice on effective yield (double) + BTL_CHOICE_INTENSITY = 50.0 + # Minimun bank balance, as a percentage of the desired bank balance, to buy new properties (double) + # TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: This parameter and its associated mechanism are not declared in the article! Need to declare and reference! + BTL_CHOICE_MIN_BANK_BALANCE = 0.75 + + ################################################## + ################ Bank parameters ################# + ################################################## + # TODO: We need references or justification for all these values! + + # Mortgage duration in years (int) + MORTGAGE_DURATION_YEARS = 25 + # Bank initial base-rate, which remains currently unchanged (double) + BANK_INITIAL_BASE_RATE = 0.005 + # Bank's target supply of credit per household per month (double) + BANK_CREDIT_SUPPLY_TARGET = 380 + # Maximum LTV ratio that the private bank would allow for first-time-buyers (double) + BANK_MAX_FTB_LTV = 0.95 + # Maximum LTV ratio that the private bank would allow for owner-occupiers (double) + BANK_MAX_OO_LTV = 0.9 + # Maximum LTV ratio that the private bank would allow for BTL investors (double) + BANK_MAX_BTL_LTV = 0.8 + # Maximum LTI ratio that the private bank would allow for first-time-buyers (private bank's hard limit) (double) + BANK_MAX_FTB_LTI = 6.0 + # Maximum LTI ratio that the private bank would allow for owner-occupiers (private bank's hard limit) (double) + BANK_MAX_OO_LTI = 6.0 + + ################################################## + ############# Central bank parameters ############ + ################################################## + # TODO: We need references or justification for all these values! Also, need to clarify meaning of "when not regulated" + + # Maximum LTI ratio that the bank would allow for first-time-buyers when not regulated (double) + CENTRAL_BANK_MAX_FTB_LTI = 6.0 + # Maximum LTI ratio that the bank would allow for owner-occupiers when not regulated (double) + CENTRAL_BANK_MAX_OO_LTI = 6.0 + # Maximum fraction of mortgages that the bank can give over the LTI ratio limit (double) + CENTRAL_BANK_FRACTION_OVER_MAX_LTI = 0.15 + # Maximum fraction of the household's income to be spent on mortgage repayments under stressed conditions (double) + CENTRAL_BANK_AFFORDABILITY_COEFF = 0.5 + # Interest rate under stressed condition for BTL investors when calculating interest coverage ratios, ICR (double) + CENTRAL_BANK_BTL_STRESSED_INTEREST = 0.05 + # Interest coverage ratio (ICR) limit imposed by the central bank (double) + CENTRAL_BANK_MAX_ICR = 1.25 + + ################################################## + ############ Construction parameters ############# + ################################################## + # TODO: We need references or justification for all these values! + + # Target ratio of houses per household (double) + CONSTRUCTION_HOUSES_PER_HOUSEHOLD = 0.82 + + ################################################## + ############# Government parameters ############## + ################################################## + + # General personal allowance to be deducted when computing taxable income (double) + GOVERNMENT_GENERAL_PERSONAL_ALLOWANCE = 9440.0 + # Limit of income above which personal allowance starts to decrease £1 for every £2 of income above this limit (double) + GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE = 100000.0 + # Minimum monthly earnings for a married couple from income support (double) + # TODO: We need a reference or justification for this value! + GOVERNMENT_MONTHLY_INCOME_SUPPORT = 492.7 + + ################################################## + ############## Collectors parameters ############# + ################################################## + + # Approximate number of households in UK, used to scale up results for core indicators (double) + # TODO: Reference needed + UK_HOUSEHOLDS = 26.5e6 + # Whether to record mortgage statistics (boolean) + MORTGAGE_DIAGNOSTICS_ACTIVE = true + + ################################################## + ################# Data addresses ################# + ################################################## + + ############ Government data addresses ########### + # TODO: We need clearer references for the values contained in these files! Also, current values are for 2013/2014, replace for 2011! + DATA_TAX_RATES = "src/main/resources/TaxRates.csv" + DATA_NATIONAL_INSURANCE_RATES = "src/main/resources/NationalInsuranceRates.csv" + + ############ Lifecycle data addresses ############ + DATA_INCOME_GIVEN_AGE = "src/main/resources/IncomeGivenAge.csv" + + ########### Demographics data addresses ########## + # Target probability density of age of representative person in the household at time t=0, calibrated against LCFS (2012) + DATA_HOUSEHOLD_AGE_AT_BIRTH_PDF = "src/main/resources/HouseholdAgeAtBirthPDF.csv" + DATA_DEATH_PROB_GIVEN_AGE = "src/main/resources/DeathProbGivenAge.csv" + + #** Construction of objects to contain derived parameters and constants **/ + + # Create object containing all constants + constants = Constants + + # Finally, create object containing all derived parameters + derivedParams = DerivedParams + +#* +# Class to contain all parameters which are not read from the configuration (.properties) file, but derived, +# instead, from these configuration parameters +#/ +class DerivedParams: + # Housing market parameters + public int HPI_RECORD_LENGTH # Number of months to record HPI (to compute price growth at different time scales) + double MONTHS_UNDER_OFFER # Time (in months) that a house remains under offer + double T # Characteristic number of data-points over which to average market statistics + public double E # Decay constant for averaging days on market (in transactions) + public double G # Decay constant for averageListPrice averaging (in transactions) + public double HPI_LOG_MEDIAN # Logarithmic median house price (scale parameter of the log-normal distribution) + double HPI_REFERENCE # Mean of reference house prices + # Household behaviour parameters: general + double MONTHLY_P_SELL # Monthly probability for owner-occupiers to sell their houses + # Bank parameters + int N_PAYMENTS # Number of monthly repayments (mortgage duration in months) + # House rental market parameters + public double K # Decay factor for exponential moving average of gross yield from rentals (averageSoldGrossYield) + public double KL # Decay factor for long-term exponential moving average of gross yield from rentals (longTermAverageGrossYield) + # Collectors parameters + double AFFORDABILITY_DECAY # Decay constant for the exponential moving average of affordability + + def getAffordabilityDecay(self) -> float: + return self.AFFORDABILITY_DECAY + + def getHPIRecordLength(self) -> int: + return self.HPI_RECORD_LENGTH + + def getHPIReference(self) -> float: + return self.HPI_REFERENCE + +# Class to contain all constants (not read from the configuration file nor derived from it) +class Constants: + DAYS_IN_MONTH = 30 + MONTHS_IN_YEAR = 12 + +#-------------------# +#----- Methods -----# +#-------------------# + +def isMortgageDiagnosticsActive() -> bool: + return MORTGAGE_DIAGNOSTICS_ACTIVE + +def getUKHouseholds() -> float: + return UK_HOUSEHOLDS + +def getPInvestor() -> float: + return P_INVESTOR + +# Method to compute and set values for all derived parameters +def setDerivedParams() -> None: + # Housing market parameters + derivedParams.HPI_RECORD_LENGTH = HPA_YEARS_TO_CHECK*constants.MONTHS_IN_YEAR + 3 # Plus three months in a quarter + derivedParams.MONTHS_UNDER_OFFER = (double)DAYS_UNDER_OFFER/constants.DAYS_IN_MONTH + derivedParams.T = 0.02*TARGET_POPULATION # TODO: Clarify where does this 0.2 come from, and provide explanation for this formula + derivedParams.E = Math.exp(-1.0/derivedParams.T) # TODO: Provide explanation for this formula + derivedParams.G = Math.exp(-N_QUALITY/derivedParams.T) # TODO: Provide explanation for this formula + derivedParams.HPI_LOG_MEDIAN = Math.log(HPI_MEDIAN) + derivedParams.HPI_REFERENCE = Math.exp(derivedParams.HPI_LOG_MEDIAN + HPI_SHAPE*HPI_SHAPE/2.0) + # Household behaviour parameters: general + derivedParams.MONTHLY_P_SELL = 1.0/(HOLD_PERIOD*constants.MONTHS_IN_YEAR) + # Bank parameters + derivedParams.N_PAYMENTS = MORTGAGE_DURATION_YEARS*constants.MONTHS_IN_YEAR + # House rental market parameters + derivedParams.K = Math.exp(-10000.0/(TARGET_POPULATION*50.0)) # TODO: Are these decay factors well-suited? Any explanation, reasoning behind the numbers chosen? + derivedParams.KL = Math.exp(-10000.0/(TARGET_POPULATION*50.0*200.0)) # TODO: Also, they are not reported in the paper! + # Collectors parameters + derivedParams.AFFORDABILITY_DECAY = Math.exp(-1.0/100.0) diff --git a/src/main/java/housing/Construction.java b/src/main/java/housing/Construction.java deleted file mode 100644 index c5e4c4e5..00000000 --- a/src/main/java/housing/Construction.java +++ /dev/null @@ -1,96 +0,0 @@ -package housing; - -import org.apache.commons.math3.random.MersenneTwister; - -import java.util.HashSet; - - -public class Construction implements IHouseOwner{ - - //------------------// - //----- Fields -----// - //------------------// - - private int housingStock; // Total number of houses in the whole model - private int nNewBuild; // Number of houses built this month - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - private MersenneTwister prng; - private HashSet onMarket; - - //------------------------// - //----- Constructors -----// - //------------------------// - - public Construction(MersenneTwister prng) { - housingStock = 0; - onMarket = new HashSet<>(); - this.prng = prng; - } - - //-------------------// - //----- Methods -----// - //-------------------// - - public void init() { - housingStock = 0; - onMarket.clear(); - } - - public void step() { - // Initialise to zero the number of houses built this month - nNewBuild = 0; - // First update prices of properties put on the market on previous time steps and still unsold - for(House h : onMarket) { - Model.houseSaleMarket.updateOffer(h.getSaleRecord(), h.getSaleRecord().getPrice()*0.95); - } - // Then, compute target housing stock dependent on current and target population - int targetStock; - if(Model.households.size() < config.TARGET_POPULATION) { - targetStock = (int)(Model.households.size()*config.CONSTRUCTION_HOUSES_PER_HOUSEHOLD); - } else { - targetStock = (int)(config.TARGET_POPULATION*config.CONSTRUCTION_HOUSES_PER_HOUSEHOLD); - } - // ...compute the shortfall of houses - int shortFall = targetStock - housingStock; - // ...if shortfall is positive... - if (shortFall > 0) { - // ...add this shortfall to the number of houses built this month - nNewBuild += shortFall; - } - // ...and while there is any positive shortfall... - House newHouse; - while(shortFall > 0) { - // ...create a new house with a random quality and with the construction sector as the owner - newHouse = new House((int)(prng.nextDouble()*config.N_QUALITY)); - newHouse.owner = this; - // ...put the house for sale in the house sale market at the reference price for that quality - Model.houseSaleMarket.offer(newHouse, - Model.housingMarketStats.getReferencePriceForQuality(newHouse.getQuality()), false); - // ...add the house to the portfolio of construction sector properties - onMarket.add(newHouse); - // ...and finally increase housing stocks, and decrease shortfall - ++housingStock; - --shortFall; - } - } - - @Override - public void completeHouseSale(HouseOfferRecord sale) { onMarket.remove(sale.getHouse()); } - - @Override - public void endOfLettingAgreement(House h, PaymentAgreement p) { - System.out.println("Strange: a tenant is moving out of a house owned by the construction sector!"); - } - - @Override - public void completeHouseLet(HouseOfferRecord sale) { - System.out.println("Strange: the construction sector is trying to let a house!"); - } - - //----- Getter/setter methods -----// - - public int getHousingStock() { return housingStock; } - - public int getnNewBuild() { return nNewBuild; } -} diff --git a/src/main/java/housing/Construction.py b/src/main/java/housing/Construction.py new file mode 100644 index 00000000..781c00b7 --- /dev/null +++ b/src/main/java/housing/Construction.py @@ -0,0 +1,71 @@ +import random +from typing import List + +class Construction(IHouseOwner): + def __init__(self, prng): + # Total number of houses in the whole model + self.housingStock: int = 0 + self.onMarket: List[House] = [] + self.prng = prng + # Number of houses built this month + self.nNewBuild = 0 + self.config = Model.config + self.Model = Model + + #-------------------# + #----- Methods -----# + #-------------------# + + def init(self) -> None: + self.housingStock = 0 + self.onMarket.clear() + + def step(self) -> None: + # Initialise to zero the number of houses built this month + self.nNewBuild = 0 + # First update prices of properties put on the market on previous time steps and still unsold + for h in self.onMarket: + self.Model.houseSaleMarket.updateOffer(h.getSaleRecord(), h.getSaleRecord().getPrice() * 0.95) + # Then, compute target housing stock dependent on current and target population + if len(self.Model.households) < self.config.TARGET_POPULATION: + targetStock = int(len(self.Model.households) * self.config.CONSTRUCTION_HOUSES_PER_HOUSEHOLD) + else: + targetStock = int(self.config.TARGET_POPULATION * self.config.CONSTRUCTION_HOUSES_PER_HOUSEHOLD) + # ...compute the shortfall of houses + shortFall = targetStock - self.housingStock + # ...if shortfall is positive... + if shortFall > 0: + # ...add this shortfall to the number of houses built this month + self.nNewBuild += shortFall + # ...and while there is any positive shortfall... + while shortFall > 0: + # ...create a new house with a random quality and with the construction sector as the owner + newHouse = House(int(random.random() * self.config.N_QUALITY)) + newHouse.owner = self + # ...put the house for sale in the house sale market at the reference price for that quality + self.Model.houseSaleMarket.offer( + newHouse, + self.Model.housingMarketStats.getReferencePriceForQuality(newHouse.getQuality()), False) + # ...add the house to the portfolio of construction sector properties + self.onMarket.append(newHouse) + # ...and finally increase housing stocks, and decrease shortfall + self.housingStock += 1 + shortFall -= 1 + + def completeHouseSale(self, sale: HouseOfferRecord) -> None: + self.onMarket.remove(sale.getHouse()) + + def endOfLettingAgreement(self, h: House, p: PaymentAgreement) -> None: + print("Strange: a tenant is moving out of a house owned by the construction sector!") + + def completeHouseLet(self, sale: HouseOfferRecord) -> None: + print("Strange: the construction sector is trying to let a house!") + + #----- Getter/setter methods -----# + + def getHousingStock(self) -> int: + return self.housingStock + + def getnNewBuild(self) -> int: + return self.nNewBuild +} diff --git a/src/main/java/housing/Demographics.java b/src/main/java/housing/Demographics.java deleted file mode 100644 index 7acff124..00000000 --- a/src/main/java/housing/Demographics.java +++ /dev/null @@ -1,50 +0,0 @@ -package housing; - -import java.util.Iterator; - -import org.apache.commons.math3.random.MersenneTwister; - -public class Demographics { - - //------------------// - //----- Fields -----// - //------------------// - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - private MersenneTwister prng; - - //------------------------// - //----- Constructors -----// - //------------------------// - - public Demographics(MersenneTwister prng) { this.prng = prng; } - - //-------------------// - //----- Methods -----// - //-------------------// - - /** - * Adds newly "born" households to the model and removes households that "die". - */ - public void step() { - // Birth: Add new households at a rate compatible with the age at birth distribution, the probability of - // death dependent on age, and the target population - int nBirths = (int) (config.TARGET_POPULATION * data.Demographics.getBirthRate() + prng.nextDouble()); - // Finally, add the households, with random ages drawn from the corresponding distribution - while (nBirths-- > 0) { - Model.households.add(new Household(prng)); - } - // Death: Kill households with a probability dependent on their age and organise inheritance - double pDeath; - Iterator iterator = Model.households.iterator(); - while (iterator.hasNext()) { - Household h = iterator.next(); - pDeath = data.Demographics.probDeathGivenAge(h.getAge())/config.constants.MONTHS_IN_YEAR; - if (prng.nextDouble() < pDeath) { - iterator.remove(); - // Inheritance - h.transferAllWealthTo(Model.households.get(prng.nextInt(Model.households.size()))); - } - } - } -} diff --git a/src/main/java/housing/Demographics.py b/src/main/java/housing/Demographics.py new file mode 100644 index 00000000..516cc67b --- /dev/null +++ b/src/main/java/housing/Demographics.py @@ -0,0 +1,26 @@ +import random + +from .housing.Household import Household + +class Demographics: + def __init__(self, Model): + self.config = Model.config + self.Model = Model + + # Adds newly "born" households to the model and removes households that "die". + def step(self) -> None: + # Birth: Add new households at a rate compatible with the age at birth distribution, the probability of + # death dependent on age, and the target population + nBirths = (int)(self.config.TARGET_POPULATION * data.Demographics.getBirthRate() + random.random()) + # Finally, add the households, with random ages drawn from the corresponding distribution + while nBirths > 0: + self.Model.households.add(Household()) + nBirths -= 1 + # Death: Kill households with a probability dependent on their age and organise inheritance + for h in self.Model.households: + pDeath = data.Demographics.probDeathGivenAge(h.getAge()) / self.config.constants.MONTHS_IN_YEAR + if random.random() < pDeath: + self.Model.households.remove(h) + # Inheritance + target = random.randint(0, len(self.Model.households) - 1) + h.transferAllWealthTo(self.Model.households[target]) diff --git a/src/main/java/housing/Government.java b/src/main/java/housing/Government.java deleted file mode 100644 index 4905432e..00000000 --- a/src/main/java/housing/Government.java +++ /dev/null @@ -1,90 +0,0 @@ -package housing; - -/************************************************************************************************** - * Class to represent the government, whose only role in the current model is to collect taxes, - * including both income tax and national insurance contributions - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class Government { - - //------------------// - //----- Fields -----// - //------------------// - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - - //-------------------// - //----- Methods -----// - //-------------------// - - /** - * Calculates the income tax due in one year for a given gross annual income, taking into account the dependence of - * the personal allowance on gross annual income, but not accounting for married couple's allowance - * - * @param grossIncome Gross annual income in pounds - * @return Annual income tax due in pounds - */ - double incomeTaxDue(double grossIncome) { - // First, the personal allowance is computed, starting from its general value - double personalAllowance = config.GOVERNMENT_GENERAL_PERSONAL_ALLOWANCE; - // If gross annual income is above the income limit for personal allowance... - if (grossIncome > config.GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE) { - // ...then £1 is subtracted from the personal allowance for every £2 of income above this income limit, till - // it reaches 0 - personalAllowance = Math.max(personalAllowance - (grossIncome - - config.GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE)/2.0, 0.0); - } - // Compute and return tax to be paid based on gross annual income and taking into account the computed personal - // allowance - return bandedPercentage(grossIncome, data.Government.tax.bands, data.Government.tax.rates, personalAllowance); - } - - /** - * Calculate the class 1 National Insurance Contributions due on a given gross annual income (under PAYE) - * - * Note that, since the untaxed allowance for national insurance contributions is the same for every household, it - * is already taken into account in the band thresholds, rather than as an untaxed allowance when calling the - * bandedPercentage method, thus untaxedAllowance is set to zero in that call - * - * @param grossIncome Gross annual income in pounds - * @return Annual class 1 NICs due - */ - double class1NICsDue(double grossIncome) { - return bandedPercentage(grossIncome, data.Government.nationalInsurance.bands, - data.Government.nationalInsurance.rates, 0.0); - } - - /** - * Calculate a "banded percentage" on a value. A "banded percentage" is a way of calculating a non-linear function, - * f(x), widely used by HMRC. The domain of values of f(x) is split into bands: from 0 to x1, from x1 to x2, etc. - * Each band is associated with a percentage p1, p2, etc. The final value of f(x) is the sum of the percentages of - * each band. So, for example, if x lies somewhere between x1 and x2, f(x) would be p1*x1 + p2*(x - x1) - * - * Note that bands are internally shifted to take into account any untaxed allowance. This is used take into account - * the particular personal allowance of a given household for income tax purposes. Given that the untaxed allowance - * for national insurance contributions is the same for every household, it is taken into account already in the - * bands' thresholds, rather than as an untaxed allowance within the call to this method - * - * @param taxableIncome The value to apply the banded percentage to - * @param bands An array holding the lower limit of each band - * @param rates An array holding the percentage applicable to each band - * @param untaxedAllowance Any untaxed allowance - * @return The banded percentage of "taxableIncome" - */ - private double bandedPercentage(double taxableIncome, Double [] bands, Double [] rates, double untaxedAllowance) { - // Set counters to zero - int i = 0; - double lastRate = 0.0; - double tax = 0.0; - // For each tax band, charge the relative rate (current band rate minus previous band rate) to any income above - // the band threshold (being this threshold shifted by any given untaxed allowance) - while (i < bands.length && taxableIncome > (bands[i] + untaxedAllowance)) { - tax += (taxableIncome - (bands[i] + untaxedAllowance))*(rates[i] - lastRate); - lastRate = rates[i]; - ++i; - } - return tax; - } -} diff --git a/src/main/java/housing/Government.py b/src/main/java/housing/Government.py new file mode 100644 index 00000000..3f3b5d60 --- /dev/null +++ b/src/main/java/housing/Government.py @@ -0,0 +1,68 @@ +from typing import List + +#************************************************************************************************** +#* Class to represent the government, whose only role in the current model is to collect taxes, +#* including both income tax and national insurance contributions +#* +#* @author daniel, Adrian Carro +#* +#*************************************************************************************************/ +class Government: + def __init__(self, Model): + self.config = Model.config + + # Calculates the income tax due in one year for a given gross annual income, taking into account the dependence of + # the personal allowance on gross annual income, but not accounting for married couple's allowance + # + # @param grossIncome Gross annual income in pounds + # @return Annual income tax due in pounds + def incomeTaxDue(self, grossIncome: float) -> float: + # First, the personal allowance is computed, starting from its general value + personalAllowance = self.config.GOVERNMENT_GENERAL_PERSONAL_ALLOWANCE + # If gross annual income is above the income limit for personal allowance... + if grossIncome > self.config.GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE: + # ...then £1 is subtracted from the personal allowance for every £2 of income above this income limit, till + # it reaches 0 + personalAllowance = max(personalAllowance - (grossIncome - self.config.GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE) / 2.0, 0.0) + # Compute and return tax to be paid based on gross annual income and taking into account the computed personal + # allowance + return self.bandedPercentage(grossIncome, data.Government.tax.bands, data.Government.tax.rates, personalAllowance) + + # Calculate the class 1 National Insurance Contributions due on a given gross annual income (under PAYE) + # + # Note that, since the untaxed allowance for national insurance contributions is the same for every household, it + # is already taken into account in the band thresholds, rather than as an untaxed allowance when calling the + # bandedPercentage method, thus untaxedAllowance is set to zero in that call + # + # @param grossIncome Gross annual income in pounds + # @return Annual class 1 NICs due + def class1NICsDue(self, grossIncome: float) -> float: + return self.bandedPercentage(grossIncome, data.Government.nationalInsurance.bands, data.Government.nationalInsurance.rates, 0.0) + + # Calculate a "banded percentage" on a value. A "banded percentage" is a way of calculating a non-linear function, + # f(x), widely used by HMRC. The domain of values of f(x) is split into bands: from 0 to x1, from x1 to x2, etc. + # Each band is associated with a percentage p1, p2, etc. The final value of f(x) is the sum of the percentages of + # each band. So, for example, if x lies somewhere between x1 and x2, f(x) would be p1*x1 + p2*(x - x1) + # + # Note that bands are internally shifted to take into account any untaxed allowance. This is used take into account + # the particular personal allowance of a given household for income tax purposes. Given that the untaxed allowance + # for national insurance contributions is the same for every household, it is taken into account already in the + # bands' thresholds, rather than as an untaxed allowance within the call to this method + # + # @param taxableIncome The value to apply the banded percentage to + # @param bands An array holding the lower limit of each band + # @param rates An array holding the percentage applicable to each band + # @param untaxedAllowance Any untaxed allowance + # @return The banded percentage of "taxableIncome" + def bandedPercentage(self, taxableIncome: float, bands: List[float], rates: List[float], untaxedAllowance: float) -> float: + # Set counters to zero + i = 0 + lastRate = 0.0 + tax = 0.0 + # For each tax band, charge the relative rate (current band rate minus previous band rate) to any income above + # the band threshold (being this threshold shifted by any given untaxed allowance) + while i < bands.length and taxableIncome > (bands[i] + untaxedAllowance): + tax += (taxableIncome - (bands[i] + untaxedAllowance)) * (rates[i] - lastRate) + lastRate = rates[i] + i += 1 + return tax diff --git a/src/main/java/housing/House.java b/src/main/java/housing/House.java deleted file mode 100644 index cefed506..00000000 --- a/src/main/java/housing/House.java +++ /dev/null @@ -1,65 +0,0 @@ -package housing; - -/************************************************************************************************** - * Class to represent a house with all its intrinsic characteristics. - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class House implements Comparable { - - //------------------// - //----- Fields -----// - //------------------// - - private static int id_pool = 0; - - public IHouseOwner owner; - public Household resident; - public int id; - - HouseOfferRecord saleRecord; - HouseOfferRecord rentalRecord; - - private int quality; - - //------------------------// - //----- Constructors -----// - //------------------------// - - /** - * Creates a house of quality quality in region region - * - * @param quality Quality band characterizing the house - */ - public House(int quality) { - this.id = ++id_pool; - this.owner = null; - this.resident = null; - this.quality = quality; - } - - //-------------------// - //----- Methods -----// - //-------------------// - - boolean isOnMarket() { return saleRecord != null; } - - HouseOfferRecord getSaleRecord() { return saleRecord; } - - HouseOfferRecord getRentalRecord() { return rentalRecord; } - - boolean isOnRentalMarket() { return rentalRecord != null; } - void putForSale(HouseOfferRecord saleRecord) { this.saleRecord = saleRecord; } - - void resetSaleRecord() { saleRecord = null; } - void putForRent(HouseOfferRecord rentalRecord) { this.rentalRecord = rentalRecord; } - - void resetRentalRecord() { rentalRecord = null; } - - public int getQuality() { return quality; } - - @Override - public int compareTo(House o) { return((int)Math.signum(id-o.id)); } - -} diff --git a/src/main/java/housing/House.py b/src/main/java/housing/House.py new file mode 100644 index 00000000..2bd62311 --- /dev/null +++ b/src/main/java/housing/House.py @@ -0,0 +1,57 @@ +import numpy as np + +#************************************************************************************************* +# Class to represent a house with all its intrinsic characteristics. +# +# @author daniel, Adrian Carro +# +#************************************************************************************************/ +class House: + id_pool = 0 + + # Creates a house of quality quality in region region + # + # @param quality Quality band characterizing the house + def __init__(self, quality: int): + self.id: int = House.id_pool + House.id_pool += 1 + self.owner: IHouseOwner = None + self.resident: Household = None + self.quality: int = quality + + self.saleRecord: HouseOfferRecord = None + self.rentalRecord: HouseOfferRecord = None + + #-------------------# + #----- Methods -----# + #-------------------# + + def isOnMarket(self) -> bool: + return self.saleRecord is not None + + def getSaleRecord(self) -> HouseOfferRecord: + return self.saleRecord + + def getRentalRecord(self) -> HouseOfferRecord: + return self.rentalRecord + + def isOnRentalMarket(self) -> bool: + return self.rentalRecord is not None + + def putForSale(self, saleRecord: HouseOfferRecord) -> None: + self.saleRecord = saleRecord + + def resetSaleRecord(self) -> None: + self.saleRecord = None + + def putForRent(rentalRecord: HouseOfferRecord) -> None: + self.rentalRecord = rentalRecord + + def resetRentalRecord(self) -> None: + self.rentalRecord = None + + def getQuality(self) -> int: + return self.quality + + def compareTo(House o) -> int: + return np.sign(self.id - o.id) diff --git a/src/main/java/housing/HouseBidderRecord.java b/src/main/java/housing/HouseBidderRecord.java deleted file mode 100644 index 8de0c351..00000000 --- a/src/main/java/housing/HouseBidderRecord.java +++ /dev/null @@ -1,63 +0,0 @@ -package housing; - -import java.util.Comparator; - -/************************************************************************************************** - * This class encapsulates information on a household that has placed a bid for a house on the - * rental or the ownership housing market. One can think of it as the file an estate agent would - * have on a customer who wants to buy or rent a house. - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class HouseBidderRecord extends HousingMarketRecord { - - //------------------// - //----- Fields -----// - //------------------// - - private Household bidder; // Household who is bidding to buy or rent a house - private boolean BTLBid; // True if the bid is for a buy-to-let property, false for a home bid (Note that rental bids are all set to false) - - //------------------------// - //----- Constructors -----// - //------------------------// - - HouseBidderRecord(Household h, double price, boolean BTLBid) { - super(price); - this.bidder = h; - this.BTLBid = BTLBid; - } - - //----------------------// - //----- Subclasses -----// - //----------------------// - - /** - * Class that implements a price comparator which solves the case of equal price by using the arguments' IDs. - */ - public static class PComparator implements Comparator { - @Override - public int compare(HouseBidderRecord arg0, HouseBidderRecord arg1) { - double diff = arg0.getPrice() - arg1.getPrice(); - if (diff == 0.0) { - diff = arg0.getId() - arg1.getId(); - } - return (int) Math.signum(diff); - } - } - - //-------------------// - //----- Methods -----// - //-------------------// - - //----- Getter/setter methods -----// - - public Household getBidder() { return bidder; } - - boolean isBTLBid() { return BTLBid; } - - // TODO: Check if the abstract method in HousingMarketRecord class is actually needed, otherwise this could be removed - @Override - public int getQuality() { return 0; } -} diff --git a/src/main/java/housing/HouseBidderRecord.py b/src/main/java/housing/HouseBidderRecord.py new file mode 100644 index 00000000..b4b954f8 --- /dev/null +++ b/src/main/java/housing/HouseBidderRecord.py @@ -0,0 +1,37 @@ +import numpy as np + +#************************************************************************************************** +#* This class encapsulates information on a household that has placed a bid for a house on the +#* rental or the ownership housing market. One can think of it as the file an estate agent would +#* have on a customer who wants to buy or rent a house. +#* +#* @author daniel, Adrian Carro +#* +#*************************************************************************************************/ +class HouseBidderRecord(HousingMarketRecord): + def __init__(self, h: Household, price: float, BTLBid: bool): + super().__init__(price) + # Household who is bidding to buy or rent a house + self.bidder = h + # True if the bid is for a buy-to-let property, false for a home bid (Note that rental bids are all set to false) + self.BTLBid = BTLBid + + #----- Getter/setter methods -----# + + def getBidder(self) -> Household: + return self.bidder + + def isBTLBid(self) -> bool: + return self.BTLBid + + # TODO: Check if the abstract method in HousingMarketRecord class is actually needed, otherwise this could be removed + def getQuality(self) -> int: + return 0 + +# Class that implements a price comparator which solves the case of equal price by using the arguments' IDs. +class PComparator: + def compare(HouseBidderRecord arg0, HouseBidderRecord arg1) -> int: + diff = arg0.getPrice() - arg1.getPrice() + if (diff == 0.0): + diff = arg0.getId() - arg1.getId() + return np.sign(diff) diff --git a/src/main/java/housing/HouseOfferRecord.java b/src/main/java/housing/HouseOfferRecord.java deleted file mode 100644 index 8f992ebc..00000000 --- a/src/main/java/housing/HouseOfferRecord.java +++ /dev/null @@ -1,102 +0,0 @@ -package housing; - -import java.util.ArrayList; - -/************************************************************************************************** - * This class encapsulates information on a house that is to be offered on the rental or the - * ownership housing market. One can think of it as the file an estate agent would have on each - * property managed. - * - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class HouseOfferRecord extends HousingMarketRecord { - - //------------------// - //----- Fields -----// - //------------------// - - private House house; - private ArrayList matchedBids; - private double initialListedPrice; - private int tInitialListing; // Time of initial listing - private double houseSpecificYield; - private boolean BTLOffer; // True if buy-to-let investor offering an investment property, false if homeowner offering home (Note that rental offers are all set to false) - - //------------------------// - //----- Constructors -----// - //------------------------// - - public HouseOfferRecord(House house, double price, boolean BTLOffer) { - super(price); - this.house = house; - this.BTLOffer = BTLOffer; - initialListedPrice = price; - tInitialListing = Model.getTime(); - matchedBids = new ArrayList<>(8); // TODO: Check if this initial size of 8 is good enough or can be improved - recalculateHouseSpecificYield(price); - } - - //-------------------// - //----- Methods -----// - //-------------------// - - /** - * Expected gross rental yield for this particular property, obtained by multiplying the average flow gross rental - * yield for houses of this quality in this particular region by the average sale price for houses of this quality - * in this region and dividing by the actual listed price of this property - * - * @param price Updated price of the property - */ - private void recalculateHouseSpecificYield(double price) { - int q = house.getQuality(); - if (price > 0) { - houseSpecificYield = Model.rentalMarketStats.getAvFlowYieldForQuality(q) - *Model.housingMarketStats.getExpAvSalePriceForQuality(q) - /price; - } - } - - /** - * Record the match of the offer of this property with a bid - * - * @param bid The bid being matched to the offer - */ - void matchWith(HouseBidderRecord bid) { matchedBids.add(bid); } - - //----- Getter/setter methods -----// - - /** - * Quality of this property - */ - @Override - public int getQuality() { return house.getQuality(); } - - /** - * Expected gross yield for this particular house, based on the current average flow yield and the actual listed - * price for the house, and taking into account both the quality and the expected occupancy levels - */ - @Override - public double getYield() { return houseSpecificYield; } - - /** - * Set the listed price for this property - * - * @param newPrice The new listed price for this property - */ - public void setPrice(double newPrice) { - super.setPrice(newPrice); - recalculateHouseSpecificYield(newPrice); - } - - public House getHouse() { return house; } - - ArrayList getMatchedBids() { return matchedBids; } - - public double getInitialListedPrice() { return initialListedPrice; } - - public int gettInitialListing() { return tInitialListing; } - - public boolean isBTLOffer() { return BTLOffer; } -} diff --git a/src/main/java/housing/HouseOfferRecord.py b/src/main/java/housing/HouseOfferRecord.py new file mode 100644 index 00000000..a41eea76 --- /dev/null +++ b/src/main/java/housing/HouseOfferRecord.py @@ -0,0 +1,84 @@ +from typing import List + +from .House import House + +#************************************************************************************************* +# This class encapsulates information on a house that is to be offered on the rental or the +# ownership housing market. One can think of it as the file an estate agent would have on each +# property managed. +# +# +# @author daniel, Adrian Carro +# +#************************************************************************************************/ +class HouseOfferRecord(HousingMarketRecord): + #------------------------# + #----- Constructors -----# + #------------------------# + + def __init__(self, Model, house: House, price: float, BTLOffer: bool): + super().__init__(price) + self.Model = Model + self.house = house + self.BTLOffer: bool = BTLOffer # True if buy-to-let investor offering an investment property, false if homeowner offering home (Note that rental offers are all set to false) + self.initialListedPrice: float = price + # Time of initial listing + self.tInitialListing: int = self.Model.getTime() + self.matchedBids: List[HouseBidderRecord] = [] # TODO: Check if this initial size of 8 is good enough or can be improved + self.houseSpecificYield: float = 0.0 + self.recalculateHouseSpecificYield(price) + + #-------------------# + #----- Methods -----# + #-------------------# + + #* + # Expected gross rental yield for this particular property, obtained by multiplying the average flow gross rental + # yield for houses of this quality in this particular region by the average sale price for houses of this quality + # in this region and dividing by the actual listed price of this property + # + # @param price Updated price of the property + #/ + def recalculateHouseSpecificYield(self, price: float) -> None: + q: int = self.house.getQuality() + if price > 0: + self.houseSpecificYield = self.Model.rentalMarketStats.getAvFlowYieldForQuality(q) * self.Model.housingMarketStats.getExpAvSalePriceForQuality(q) / price + + # Record the match of the offer of this property with a bid + # + # @param bid The bid being matched to the offer + def matchWith(self, bid: HouseBidderRecord) -> None: + self.matchedBids.append(bid) + + #----- Getter/setter methods -----# + + # Quality of this property + def getQuality(self) -> int: + return self.house.getQuality() + + # Expected gross yield for this particular house, based on the current average flow yield and the actual listed + # price for the house, and taking into account both the quality and the expected occupancy levels + def getYield(self) -> float: + return self.houseSpecificYield + + # Set the listed price for this property + # + # @param newPrice The new listed price for this property + def setPrice(self, newPrice: float) -> None: + super().setPrice(newPrice) + self.recalculateHouseSpecificYield(newPrice) + + def getHouse(self) -> House: + return self.house + + def getMatchedBids(self) -> List[HouseBidderRecord]: + return self.matchedBids + + def getInitialListedPrice(self) -> float: + return self.initialListedPrice + + def gettInitialListing(self) -> int: + return self.tInitialListing + + def isBTLOffer(self) -> bool: + return self.BTLOffer diff --git a/src/main/java/housing/HouseRentalMarket.java b/src/main/java/housing/HouseRentalMarket.java deleted file mode 100644 index d1d7c317..00000000 --- a/src/main/java/housing/HouseRentalMarket.java +++ /dev/null @@ -1,45 +0,0 @@ -package housing; - -import org.apache.commons.math3.random.MersenneTwister; - -/************************************************************************************************** - * Class to represent the rental market - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class HouseRentalMarket extends HousingMarket { - - //-------------------// - //----- Methods -----// - //-------------------// - - public HouseRentalMarket(MersenneTwister prng) { - super(prng); - } - - @Override - public void completeTransaction(HouseBidderRecord purchase, HouseOfferRecord sale) { - Model.rentalMarketStats.recordTransaction(sale); - sale.getHouse().rentalRecord = null; - purchase.getBidder().completeHouseRental(sale); - sale.getHouse().owner.completeHouseLet(sale); - Model.rentalMarketStats.recordSale(purchase, sale); - } - - @Override - public HouseOfferRecord offer(House house, double price, boolean BTLOffer) { - if(house.isOnMarket()) { - System.out.println("Got offer on rental market of house already on sale market"); - } - HouseOfferRecord hsr = super.offer(house, price, false); - house.putForRent(hsr); - return(hsr); - } - - @Override - public void removeOffer(HouseOfferRecord hsr) { - super.removeOffer(hsr); - hsr.getHouse().resetRentalRecord(); - } -} diff --git a/src/main/java/housing/HouseRentalMarket.py b/src/main/java/housing/HouseRentalMarket.py new file mode 100644 index 00000000..eea028c7 --- /dev/null +++ b/src/main/java/housing/HouseRentalMarket.py @@ -0,0 +1,24 @@ +#************************************************************************************************* +# Class to represent the rental market +# +# @author daniel, Adrian Carro +# +#************************************************************************************************/ +class HouseRentalMarket(HousingMarket): + def completeTransaction(self, purchase: HouseBidderRecord, sale: HouseOfferRecord) -> None: + self.Model.rentalMarketStats.recordTransaction(sale) + sale.getHouse().rentalRecord = None + purchase.getBidder().completeHouseRental(sale) + sale.getHouse().owner.completeHouseLet(sale) + self.Model.rentalMarketStats.recordSale(purchase, sale) + + def offer(self, house: House, price: float, BTLOffer: bool) -> HouseOfferRecord: + if house.isOnMarket(): + print("Got offer on rental market of house already on sale market") + hsr: HouseOfferRecord = super().offer(house, price, False) + house.putForRent(hsr) + return hsr + + def removeOffer(hsr: HouseOfferRecord) -> None: + super().removeOffer(hsr) + hsr.getHouse().resetRentalRecord() diff --git a/src/main/java/housing/HouseSaleMarket.java b/src/main/java/housing/HouseSaleMarket.java deleted file mode 100644 index c3efec17..00000000 --- a/src/main/java/housing/HouseSaleMarket.java +++ /dev/null @@ -1,119 +0,0 @@ -package housing; - -import java.util.Iterator; - -import org.apache.commons.math3.random.MersenneTwister; -import utilities.PriorityQueue2D; - -/******************************************************* - * Class that represents market for houses for-sale. - * - * @author daniel, Adrian Carro - * - *****************************************************/ -public class HouseSaleMarket extends HousingMarket { - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - private PriorityQueue2D offersPY; - - HouseSaleMarket(MersenneTwister prng) { - super(prng); - offersPY = new PriorityQueue2D<>(new HousingMarketRecord.PYComparator()); - } - - @Override - public void init() { - super.init(); - offersPY.clear(); - } - - /** - * This method deals with doing all the stuff necessary whenever a house gets sold. - */ - public void completeTransaction(HouseBidderRecord purchase, HouseOfferRecord sale) { - // TODO: Revise if it makes sense to have recordTransaction as a separate method from recordSale - Model.housingMarketStats.recordTransaction(sale); - sale.getHouse().saleRecord = null; - Household buyer = purchase.getBidder(); - if(buyer == sale.getHouse().owner) return; // TODO: Shouldn't this if be the first line in this method? - sale.getHouse().owner.completeHouseSale(sale); - buyer.completeHousePurchase(sale); - Model.housingMarketStats.recordSale(purchase, sale); - sale.getHouse().owner = buyer; - } - - @Override - public HouseOfferRecord offer(House house, double price, boolean BTLOffer) { - HouseOfferRecord hsr = super.offer(house, price, BTLOffer); - offersPY.add(hsr); - house.putForSale(hsr); - return(hsr); - } - - @Override - public void removeOffer(HouseOfferRecord hsr) { - super.removeOffer(hsr); - offersPY.remove(hsr); - hsr.getHouse().resetSaleRecord(); - } - - @Override - public void updateOffer(HouseOfferRecord hsr, double newPrice) { - offersPY.remove(hsr); - super.updateOffer(hsr, newPrice); - offersPY.add(hsr); - } - - /** - * This method overrides the main simulation step in order to sort the price-yield priorities. - */ - @Override - void clearMarket() { - // Before any use, priorities must be sorted by filling in the uncoveredElements TreeSet at the corresponding - // PriorityQueue2D. In particular, we sort here the price-yield priorities - offersPY.sortPriorities(); - // Then continue with the normal HousingMarket clearMarket mechanism - super.clearMarket(); - } - - @Override - protected HouseOfferRecord getBestOffer(HouseBidderRecord bid) { - if (bid.isBTLBid()) { // BTL bidder (yield driven) - HouseOfferRecord bestOffer = (HouseOfferRecord)offersPY.peek(bid); - if (bestOffer != null) { - double minDownpayment = bestOffer.getPrice()*(1.0 - - Model.rentalMarketStats.getExpAvFlowYield() - /(Model.centralBank.getInterestCoverRatioLimit(false) - *config.CENTRAL_BANK_BTL_STRESSED_INTEREST)); - if (bid.getBidder().getBankBalance() >= minDownpayment) { - return bestOffer; - } - } - return null; - } else { // must be OO buyer (quality driven) - return super.getBestOffer(bid); - } - } - - /** - * Overrides corresponding method at HousingMarket in order to remove successfully matched and cleared offers from - * the offersPY queue - * - * @param record Iterator over the HousingMarketRecord objects contained in offersPQ - * @param offer Offer to remove from queues - */ - @Override - void removeOfferFromQueues(Iterator record, HouseOfferRecord offer) { - record.remove(); - offersPY.remove(offer); - } - - /******************************************* - * Make a bid on the market as a Buy-to-let investor - * (i.e. make an offer on a (yet to be decided) house). - * - * @param buyer The household that is making the bid. - * @param maxPrice The maximum price that the household is willing to pay. - ******************************************/ - void BTLbid(Household buyer, double maxPrice) { bids.add(new HouseBidderRecord(buyer, maxPrice, true)); } -} diff --git a/src/main/java/housing/HouseSaleMarket.py b/src/main/java/housing/HouseSaleMarket.py new file mode 100644 index 00000000..054659b7 --- /dev/null +++ b/src/main/java/housing/HouseSaleMarket.py @@ -0,0 +1,88 @@ +from typing import Iterator + +#****************************************************** +# Class that represents market for houses for-sale. +# +# @author daniel, Adrian Carro +# +#****************************************************/ +class HouseSaleMarket(HousingMarket): + def __init__(self, Model): + super().__init__() + self.Model = Model + self.config = Model.config + self.offersPY = PriorityQueue2D(HousingMarketRecord.PYComparator()) + + def init() -> None: + super().init() + self.offersPY.clear() + + # This method deals with doing all the stuff necessary whenever a house gets sold. + def completeTransaction(self, purchase: HouseBidderRecord, sale: HouseOfferRecord) -> None: + # TODO: Revise if it makes sense to have recordTransaction as a separate method from recordSale + self.Model.housingMarketStats.recordTransaction(sale) + sale.getHouse().saleRecord = None + buyer: Household = purchase.getBidder() + if buyer == sale.getHouse().owner: + return # TODO: Shouldn't this if be the first line in this method? + sale.getHouse().owner.completeHouseSale(sale) + buyer.completeHousePurchase(sale) + self.Model.housingMarketStats.recordSale(purchase, sale) + sale.getHouse().owner = buyer + + def offer(self, house: House, price: float, BTLOffer: bool) -> HouseOfferRecord: + hsr: HouseOfferRecord = super().offer(house, price, BTLOffer) + self.offersPY.add(hsr) + house.putForSale(hsr) + return hsr + + def removeOffer(self, hsr: HouseOfferRecord) -> None: + super().removeOffer(hsr) + self.offersPY.remove(hsr) + hsr.getHouse().resetSaleRecord() + + def updateOffer(self, hsr: HouseOfferRecord, newPrice: float) -> None: + self.offersPY.remove(hsr) + super.updateOffer(hsr, newPrice) + self.offersPY.add(hsr) + + # This method overrides the main simulation step in order to sort the price-yield priorities. + def clearMarket(self) -> None: + # Before any use, priorities must be sorted by filling in the uncoveredElements TreeSet at the corresponding + # PriorityQueue2D. In particular, we sort here the price-yield priorities + self.offersPY.sortPriorities() + # Then continue with the normal HousingMarket clearMarket mechanism + super().clearMarket() + + def getBestOffer(self, bid: HouseBidderRecord) -> HouseOfferRecord: + if bid.isBTLBid(): # BTL bidder (yield driven) + bestOffer : HouseOfferRecord = HouseOfferRecord(offersPY.peek(bid)) + if bestOffer is not None: + minDownpayment: float = bestOffer.getPrice() * ( + 1.0 - self.Model.rentalMarketStats.getExpAvFlowYield() / + (self.Model.centralBank.getInterestCoverRatioLimit(False) * + self.config.CENTRAL_BANK_BTL_STRESSED_INTEREST)) + if bid.getBidder().getBankBalance() >= minDownpayment: + return bestOffer + return None + else: # must be OO buyer (quality driven) + return super().getBestOffer(bid) + + # Overrides corresponding method at HousingMarket in order to remove successfully matched and cleared offers from + # the offersPY queue + # + # @param record Iterator over the HousingMarketRecord objects contained in offersPQ + # @param offer Offer to remove from queues + def removeOfferFromQueues(record: Iterator[HousingMarketRecord], offer: HouseOfferRecord) -> None: + record.remove() + self.offersPY.remove(offer) + + #****************************************** + # Make a bid on the market as a Buy-to-let investor + # (i.e. make an offer on a (yet to be decided) house). + # + # @param buyer The household that is making the bid. + # @param maxPrice The maximum price that the household is willing to pay. + #*****************************************/ + def BTLbid(buyer: Household, maxPrice: float) -> None: + self.bids.add(HouseBidderRecord(buyer, maxPrice, True)) diff --git a/src/main/java/housing/Household.java b/src/main/java/housing/Household.java deleted file mode 100644 index a2183f54..00000000 --- a/src/main/java/housing/Household.java +++ /dev/null @@ -1,625 +0,0 @@ -package housing; - -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; - -import org.apache.commons.math3.random.MersenneTwister; - -/************************************************************************************************** - * This represents a household who receives an income, consumes, saves and can buy, sell, let, and - * invest in houses. - * - * @author daniel, davidrpugh, Adrian Carro - * - *************************************************************************************************/ - -public class Household implements IHouseOwner { - - //------------------// - //----- Fields -----// - //------------------// - - private static int id_pool; - - public int id; // Only used for identifying households within the class MicroDataRecorder - private double annualGrossEmploymentIncome; - private double monthlyGrossEmploymentIncome; - public HouseholdBehaviour behaviour; // Behavioural plugin - - double incomePercentile; // Fixed for the whole lifetime of the household - - private House home; - private Map housePayments = new TreeMap<>(); // Houses owned and their payment agreements - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - private MersenneTwister prng; - private double age; // Age of the household representative person - private double bankBalance; - private double monthlyGrossRentalIncome; // Keeps track of monthly rental income, as only tenants keep a reference to the rental contract, not landlords - private boolean isFirstTimeBuyer; - private boolean isBankrupt; - - //------------------------// - //----- Constructors -----// - //------------------------// - - /** - * Initialises behaviour (determine whether the household will be a BTL investor). Households start off in social - * housing and with their "desired bank balance" in the bank - */ - public Household(MersenneTwister prng) { - this.prng = prng; // Passes the Model's random number generator to a private field of each instance - home = null; - isFirstTimeBuyer = true; - isBankrupt = false; - id = ++id_pool; - age = data.Demographics.pdfHouseholdAgeAtBirth.nextDouble(this.prng); - incomePercentile = this.prng.nextDouble(); - behaviour = new HouseholdBehaviour(this.prng, incomePercentile); - // Find initial values for the annual and monthly gross employment income - annualGrossEmploymentIncome = data.EmploymentIncome.getAnnualGrossEmploymentIncome(age, incomePercentile); - monthlyGrossEmploymentIncome = annualGrossEmploymentIncome/config.constants.MONTHS_IN_YEAR; - bankBalance = behaviour.getDesiredBankBalance(getAnnualGrossTotalIncome()); // Desired bank balance is used as initial value for actual bank balance - monthlyGrossRentalIncome = 0.0; - } - - //-------------------// - //----- Methods -----// - //-------------------// - - //----- General methods -----// - - /** - * Main simulation step for each household. They age, receive employment and other forms of income, make their rent - * or mortgage payments, perform an essential consumption, make non-essential consumption decisions, manage their - * owned properties, and make their housing decisions depending on their current housing state: - * - Buy or rent if in social housing - * - Sell house if owner-occupier - * - Buy/sell/rent out properties if BTL investor - */ - public void step() { - isBankrupt = false; // Delete bankruptcies from previous time step - age += 1.0/config.constants.MONTHS_IN_YEAR; - // Update annual and monthly gross employment income - annualGrossEmploymentIncome = data.EmploymentIncome.getAnnualGrossEmploymentIncome(age, incomePercentile); - monthlyGrossEmploymentIncome = annualGrossEmploymentIncome/config.constants.MONTHS_IN_YEAR; - // Add monthly disposable income (net total income minus essential consumption and housing expenses) to bank balance - bankBalance += getMonthlyDisposableIncome(); - // Consume based on monthly disposable income (after essential consumption and house payments have been subtracted) - bankBalance -= behaviour.getDesiredConsumption(getBankBalance(), getAnnualGrossTotalIncome()); // Old implementation: if(isFirstTimeBuyer() || !isInSocialHousing()) bankBalance -= behaviour.getDesiredConsumption(getBankBalance(), getAnnualGrossTotalIncome()); - // Deal with bankruptcies - // TODO: Improve bankruptcy procedures (currently, simple cash injection), such as terminating contracts! - if (bankBalance < 0.0) { - bankBalance = 1.0; - isBankrupt = true; - } - // Manage owned properties and close debts on previously owned properties. To this end, first, create an - // iterator over the house-paymentAgreement pairs at the household's housePayments object - Iterator> paymentIt = housePayments.entrySet().iterator(); - Entry entry; - House h; - PaymentAgreement payment; - // Iterate over these house-paymentAgreement pairs... - while (paymentIt.hasNext()) { - entry = paymentIt.next(); - h = entry.getKey(); - payment = entry.getValue(); - // ...if the household is the owner of the house, then manage it - if (h.owner == this) { - manageHouse(h); - // ...otherwise, if the household is not the owner nor the resident, then it is an old debt due to - // the household's inability to pay the remaining principal off after selling a property... - } else if (h.resident != this) { - MortgageAgreement mortgage = (MortgageAgreement) payment; - // ...remove this type of houses from payments as soon as the household pays the debt off - if ((payment.nPayments == 0) & (mortgage.principal == 0.0)) { - paymentIt.remove(); - } - } - } - // Make housing decisions depending on current housing state - if (isInSocialHousing()) { - bidForAHome(); // When BTL households are born, they enter here the first time and until they manage to buy a home! - } else if (isRenting()) { - if (housePayments.get(home).nPayments == 0) { // End of rental period for this tenant - endTenancy(); - bidForAHome(); - } - } else if (behaviour.isPropertyInvestor()) { // Only BTL investors who already own a home enter here - double price = behaviour.btlPurchaseBid(this); - Model.householdStats.countBTLBidsAboveExpAvSalePrice(price); - if (behaviour.decideToBuyInvestmentProperty(this)) { - Model.houseSaleMarket.BTLbid(this, price); - } - } else if (!isHomeowner()){ - System.out.println("Strange: this household is not a type I recognize"); - } - } - - /** - * Subtracts the essential, necessary consumption and housing expenses (mortgage and rental payments) from the net - * total income (employment income, property income, financial returns minus taxes) - */ - private double getMonthlyDisposableIncome() { - // Start with net monthly income - double monthlyDisposableIncome = getMonthlyNetTotalIncome(); - // Subtract essential, necessary consumption - // TODO: ESSENTIAL_CONSUMPTION_FRACTION is not explained in the paper, all support is said to be consumed - monthlyDisposableIncome -= config.ESSENTIAL_CONSUMPTION_FRACTION*config.GOVERNMENT_MONTHLY_INCOME_SUPPORT; - // Subtract housing consumption - for(PaymentAgreement payment: housePayments.values()) { - monthlyDisposableIncome -= payment.makeMonthlyPayment(); - } - return monthlyDisposableIncome; - } - - /** - * Subtracts the monthly aliquot part of all due taxes from the monthly gross total income. Note that only income - * tax on employment income and national insurance contributions are implemented! - */ - double getMonthlyNetTotalIncome() { - // TODO: Note that this implies there is no tax on rental income nor on bank balance returns - return getMonthlyGrossTotalIncome() - - (Model.government.incomeTaxDue(annualGrossEmploymentIncome) // Employment income tax - + Model.government.class1NICsDue(annualGrossEmploymentIncome)) // National insurance contributions - /config.constants.MONTHS_IN_YEAR; - } - - /** - * Adds up all sources of (gross) income on a monthly basis: employment, property, returns on financial wealth - */ - public double getMonthlyGrossTotalIncome() { - if (bankBalance > 0.0) { - return monthlyGrossEmploymentIncome + monthlyGrossRentalIncome - + bankBalance*config.RETURN_ON_FINANCIAL_WEALTH; - } else { - return monthlyGrossEmploymentIncome + monthlyGrossRentalIncome; - } - } - - double getAnnualGrossTotalIncome() { return getMonthlyGrossTotalIncome()*config.constants.MONTHS_IN_YEAR; } - - //----- Methods for house owners -----// - - /** - * Decide what to do with a house h owned by the household: - * - if the household lives in the house, decide whether to sell it or not - * - if the house is up for sale, rethink its offer price, and possibly put it up for rent instead (only BTL investors) - * - if the house is up for rent, rethink the rent demanded - * - * @param house A house owned by the household - */ - private void manageHouse(House house) { - HouseOfferRecord forSale, forRent; - double newPrice; - - forSale = house.getSaleRecord(); - if(forSale != null) { // reprice house for sale - newPrice = behaviour.rethinkHouseSalePrice(forSale); - if(newPrice > mortgageFor(house).principal) { - Model.houseSaleMarket.updateOffer(forSale, newPrice); - } else { - Model.houseSaleMarket.removeOffer(forSale); - // TODO: Is first condition redundant? - if(house != home && house.resident == null) { - Model.houseRentalMarket.offer(house, buyToLetRent(house), false); - } - } - } else if(decideToSellHouse(house)) { // put house on market? - if(house.isOnRentalMarket()) Model.houseRentalMarket.removeOffer(house.getRentalRecord()); - putHouseForSale(house); - } - - forRent = house.getRentalRecord(); - if(forRent != null) { // reprice house for rent - newPrice = behaviour.rethinkBuyToLetRent(forRent); - Model.houseRentalMarket.updateOffer(forRent, newPrice); - } - } - - /****************************************************** - * Having decided to sell house h, decide its initial sale price and put it up in the market. - * - * @param h the house being sold - ******************************************************/ - private void putHouseForSale(House h) { - double principal; - MortgageAgreement mortgage = mortgageFor(h); - if(mortgage != null) { - principal = mortgage.principal; - } else { - principal = 0.0; - } - if (h == home) { - Model.houseSaleMarket.offer(h, behaviour.getInitialSalePrice(h.getQuality(), principal), false); - } else { - Model.houseSaleMarket.offer(h, behaviour.getInitialSalePrice(h.getQuality(), principal), true); - } - } - - ///////////////////////////////////////////////////////// - // Houseowner interface - ///////////////////////////////////////////////////////// - - /******************************************************** - * Do all the stuff necessary when this household - * buys a house: - * Give notice to landlord if renting, - * Get loan from mortgage-lender, - * Pay for house, - * Put house on rental market if buy-to-let and no tenant. - ********************************************************/ - void completeHousePurchase(HouseOfferRecord sale) { - if(isRenting()) { // give immediate notice to landlord and move out - if(sale.getHouse().resident != null) System.out.println("Strange: my new house has someone in it!"); - if(home == sale.getHouse()) { - System.out.println("Strange: I've just bought a house I'm renting out"); - } else { - endTenancy(); - } - } - MortgageAgreement mortgage = Model.bank.requestLoan(this, sale.getPrice(), behaviour.decideDownPayment(this,sale.getPrice()), home == null, sale.getHouse()); - if(mortgage == null) { - // TODO: need to either provide a way for house sales to fall through or to ensure that pre-approvals are always satisfiable - System.out.println("Can't afford to buy house: strange"); - System.out.println("Bank balance is "+bankBalance); - System.out.println("Annual income is "+ monthlyGrossEmploymentIncome *config.constants.MONTHS_IN_YEAR); - if(isRenting()) System.out.println("Is renting"); - if(isHomeowner()) System.out.println("Is homeowner"); - if(isInSocialHousing()) System.out.println("Is homeless"); - if(isFirstTimeBuyer()) System.out.println("Is firsttimebuyer"); - if(behaviour.isPropertyInvestor()) System.out.println("Is investor"); - System.out.println("House owner = "+ sale.getHouse().owner); - System.out.println("me = "+this); - } else { - bankBalance -= mortgage.downPayment; - housePayments.put(sale.getHouse(), mortgage); - if (home == null) { // move in to house - home = sale.getHouse(); - sale.getHouse().resident = this; - } else if (sale.getHouse().resident == null) { // put empty buy-to-let house on rental market - Model.houseRentalMarket.offer(sale.getHouse(), buyToLetRent(sale.getHouse()), false); - } - isFirstTimeBuyer = false; - } - } - - /******************************************************** - * Do all stuff necessary when this household sells a house - ********************************************************/ - public void completeHouseSale(HouseOfferRecord sale) { - // First, receive money from sale - bankBalance += sale.getPrice(); - // Second, find mortgage object and pay off as much outstanding debt as possible given bank balance - MortgageAgreement mortgage = mortgageFor(sale.getHouse()); - bankBalance -= mortgage.payoff(bankBalance); - // Third, if there is no more outstanding debt, remove the house from the household's housePayments object - if (mortgage.nPayments == 0) { - housePayments.remove(sale.getHouse()); - // TODO: Warning, if bankBalance is not enough to pay mortgage back, then the house stays in housePayments, - // TODO: consequences to be checked. Looking forward, properties and payment agreements should be kept apart - } - // Fourth, if the house is still being offered on the rental market, withdraw the offer - if (sale.getHouse().isOnRentalMarket()) { - Model.houseRentalMarket.removeOffer(sale); - } - // Fifth, if the house is the household's home, then the household moves out and becomes temporarily homeless... - if (sale.getHouse() == home) { - home.resident = null; - home = null; - // ...otherwise, if the house has a resident, it must be a renter, who must get evicted, also the rental income - // corresponding to this tenancy must be subtracted from the owner's monthly rental income - } else if (sale.getHouse().resident != null) { - monthlyGrossRentalIncome -= sale.getHouse().resident.housePayments.get(sale.getHouse()).monthlyPayment; - sale.getHouse().resident.getEvicted(); - } - } - - /******************************************************** - * A BTL investor receives this message when a tenant moves - * out of one of its buy-to-let houses. - * - * The household simply puts the house back on the rental - * market. - ********************************************************/ - @Override - public void endOfLettingAgreement(House h, PaymentAgreement contract) { - monthlyGrossRentalIncome -= contract.monthlyPayment; - - // put house back on rental market - if(!housePayments.containsKey(h)) { - System.out.println("Strange: I don't own this house in endOfLettingAgreement"); - } -// if(h.resident != null) System.out.println("Strange: renting out a house that has a resident"); -// if(h.resident != null && h.resident == h.owner) System.out.println("Strange: renting out a house that belongs to a homeowner"); - if(h.isOnRentalMarket()) System.out.println("Strange: got endOfLettingAgreement on house on rental market"); - if(!h.isOnMarket()) Model.houseRentalMarket.offer(h, buyToLetRent(h), false); - } - - /********************************************************** - * This household moves out of current rented accommodation - * and becomes homeless (possibly temporarily). Move out, - * inform landlord and delete rental agreement. - **********************************************************/ - private void endTenancy() { - home.owner.endOfLettingAgreement(home, housePayments.get(home)); - housePayments.remove(home); - home.resident = null; - home = null; - } - - /*** Landlord has told this household to get out: leave without informing landlord */ - private void getEvicted() { - if(home == null) { - System.out.println("Strange: got evicted but I'm homeless"); - } - if(home.owner == this) { - System.out.println("Strange: got evicted from a home I own"); - } - housePayments.remove(home); - home.resident = null; - home = null; - } - - - /******************************************************** - * Do all the stuff necessary when this household moves - * in to rented accommodation (i.e. set up a regular - * payment contract. At present we use a MortgageApproval). - ********************************************************/ - void completeHouseRental(HouseOfferRecord sale) { - if(sale.getHouse().owner != this) { // if renting own house, no need for contract - RentalAgreement rent = new RentalAgreement(); - rent.monthlyPayment = sale.getPrice(); - rent.nPayments = config.TENANCY_LENGTH_AVERAGE - + prng.nextInt(2*config.TENANCY_LENGTH_EPSILON + 1) - config.TENANCY_LENGTH_EPSILON; -// rent.principal = rent.monthlyPayment*rent.nPayments; - housePayments.put(sale.getHouse(), rent); - } - if(home != null) System.out.println("Strange: I'm renting a house but not homeless"); - home = sale.getHouse(); - if(sale.getHouse().resident != null) { - System.out.println("Strange: tenant moving into an occupied house"); - if(sale.getHouse().resident == this) System.out.println("...It's me!"); - if(sale.getHouse().owner == this) System.out.println("...It's my house!"); - if(sale.getHouse().owner == sale.getHouse().resident) System.out.println("...It's a homeowner!"); - } - sale.getHouse().resident = this; - } - - - /******************************************************** - * Make the decision whether to bid on the housing market or rental market. - * This is an "intensity of choice" decision (sigma function) - * on the cost of renting compared to the cost of owning, with - * COST_OF_RENTING being an intrinsic psychological cost of not - * owning. - ********************************************************/ - private void bidForAHome() { - // Find household's desired housing expenditure - double price = behaviour.getDesiredPurchasePrice(monthlyGrossEmploymentIncome); - // Cap this expenditure to the maximum mortgage available to the household - price = Math.min(price, Model.bank.getMaxMortgage(this, true)); - // Record the bid on householdStats for counting the number of bids above exponential moving average sale price - Model.householdStats.countNonBTLBidsAboveExpAvSalePrice(price); - // Compare costs to decide whether to buy or rent... - if (behaviour.decideRentOrPurchase(this, price)) { - // ... if buying, bid in the house sale market for the capped desired price - Model.houseSaleMarket.bid(this, price); - } else { - // ... if renting, bid in the house rental market for the desired rent price - Model.houseRentalMarket.bid(this, behaviour.desiredRent(monthlyGrossEmploymentIncome)); - } - } - - - /******************************************************** - * Decide whether to sell ones own house. - ********************************************************/ - private boolean decideToSellHouse(House h) { - if(h == home) { - return(behaviour.decideToSellHome()); - } else { - return(behaviour.decideToSellInvestmentProperty(h, this)); - } - } - - - - /*** - * Do stuff necessary when BTL investor lets out a rental - * property - */ - @Override - public void completeHouseLet(HouseOfferRecord sale) { - if(sale.getHouse().isOnMarket()) { - Model.houseSaleMarket.removeOffer(sale.getHouse().getSaleRecord()); - } - monthlyGrossRentalIncome += sale.getPrice(); - } - - private double buyToLetRent(House h) { - return(behaviour.buyToLetRent( - Model.rentalMarketStats.getExpAvSalePriceForQuality(h.getQuality()), - Model.rentalMarketStats.getExpAvDaysOnMarket(), h)); - } - - ///////////////////////////////////////////////////////// - // Inheritance behaviour - ///////////////////////////////////////////////////////// - - /** - * Implement inheritance: upon death, transfer all wealth to the previously selected household. - * - * Take all houses off the markets, evict any tenants, pay off mortgages, and give property and remaining - * bank balance to the beneficiary. - * @param beneficiary The household that will inherit the wealth - */ - void transferAllWealthTo(Household beneficiary) { - // Check if beneficiary is the same as the deceased household - if (beneficiary == this) { // TODO: I don't think this check is really necessary - System.out.println("Strange: I'm transferring all my wealth to myself"); - System.exit(0); - } - // Create an iterator over the house-paymentAgreement pairs at the deceased household's housePayments object - Iterator> paymentIt = housePayments.entrySet().iterator(); - Entry entry; - House h; - PaymentAgreement payment; - // Iterate over these house-paymentAgreement pairs - while (paymentIt.hasNext()) { - entry = paymentIt.next(); - h = entry.getKey(); - payment = entry.getValue(); - // If the deceased household owns the house, then... - if (h.owner == this) { - // ...first, withdraw the house from any market where it is currently being offered - if (h.isOnRentalMarket()) Model.houseRentalMarket.removeOffer(h.getRentalRecord()); - if (h.isOnMarket()) Model.houseSaleMarket.removeOffer(h.getSaleRecord()); - // ...then, if there is a resident in the house... - if (h.resident != null) { - // ...and this resident is different from the deceased household, then this resident must be a - // tenant, who must get evicted - if (h.resident != this) { - h.resident.getEvicted(); // TODO: Explain in paper that renters always get evicted, not just if heir needs the house - // ...otherwise, if the resident is the deceased household, remove it from the house - } else { - h.resident = null; - } - } - // ...finally, transfer the property to the beneficiary household - beneficiary.inheritHouse(h); - // Otherwise, if the deceased household does not own the house but it is living in it, then it must have - // been renting it: end the letting agreement - } else if (h == home) { - h.owner.endOfLettingAgreement(h, housePayments.get(h)); - h.resident = null; - } - // If payment agreement is a mortgage, then try to pay off as much as possible from the deceased household's bank balance - if (payment instanceof MortgageAgreement) { - bankBalance -= ((MortgageAgreement) payment).payoff(); - } - // Remove the house-paymentAgreement entry from the deceased household's housePayments object - paymentIt.remove(); // TODO: Not sure this is necessary. Note, though, that this implies erasing all outstanding debt - } - // Finally, transfer all remaining liquid wealth to the beneficiary household - beneficiary.bankBalance += Math.max(0.0, bankBalance); - } - - /** - * Inherit a house. - * - * Write off the mortgage for the house. Move into the house if renting or in social housing. - * - * @param h House to inherit - */ - private void inheritHouse(House h) { - // Create a null (zero payments) mortgage - MortgageAgreement nullMortgage = new MortgageAgreement(this,false); - nullMortgage.nPayments = 0; - nullMortgage.downPayment = 0.0; - nullMortgage.monthlyInterestRate = 0.0; - nullMortgage.monthlyPayment = 0.0; - nullMortgage.principal = 0.0; - nullMortgage.purchasePrice = 0.0; - // Become the owner of the inherited house and include it in my housePayments list (with a null mortgage) - // TODO: Make sure the paper correctly explains that no debt is inherited - housePayments.put(h, nullMortgage); - h.owner = this; - // Check for residents in the inherited house - if(h.resident != null) { - System.out.println("Strange: inheriting a house with a resident"); - System.exit(0); - } - // If renting or homeless, move into the inherited house - if(!isHomeowner()) { - // If renting, first cancel my current tenancy - if(isRenting()) { - endTenancy(); - } - home = h; - h.resident = this; - // If owning a home and having the BTL gene... - } else if(behaviour.isPropertyInvestor()) { - // ...decide whether to sell the inherited house - if(decideToSellHouse(h)) { - putHouseForSale(h); - // ...or rent it out - } else if(h.resident == null) { - Model.houseRentalMarket.offer(h, buyToLetRent(h), false); - } - // If being an owner-occupier, put inherited house for sale - } else { - putHouseForSale(h); - } - } - - //----- Helpers -----// - - public double getAge() { return age; } - - public boolean isHomeowner() { - if(home == null) return(false); - return(home.owner == this); - } - - public boolean isRenting() { - if(home == null) return(false); - return(home.owner != this); - } - - public boolean isInSocialHousing() { return home == null; } - - boolean isFirstTimeBuyer() { return isFirstTimeBuyer; } - - public boolean isBankrupt() { return isBankrupt; } - - public double getBankBalance() { return bankBalance; } - - public House getHome() { return home; } - - public Map getHousePayments() { return housePayments; } - - public double getAnnualGrossEmploymentIncome() { return annualGrossEmploymentIncome; } - - public double getMonthlyGrossEmploymentIncome() { return monthlyGrossEmploymentIncome; } - - /*** - * @return Number of properties this household currently has on the sale market - */ - public int nPropertiesForSale() { - int n=0; - for(House h : housePayments.keySet()) { - if(h.isOnMarket()) ++n; - } - return(n); - } - - public int nInvestmentProperties() { return housePayments.size() - 1; } - - /*** - * @return Current mark-to-market (with exponentially averaged prices per quality) equity in this household's home. - */ - double getHomeEquity() { - if(!isHomeowner()) return(0.0); - return Model.housingMarketStats.getExpAvSalePriceForQuality(home.getQuality()) - - mortgageFor(home).principal; - } - - public MortgageAgreement mortgageFor(House h) { - PaymentAgreement payment = housePayments.get(h); - if(payment instanceof MortgageAgreement) { - return((MortgageAgreement)payment); - } - return(null); - } - - public double monthlyPaymentOn(House h) { - PaymentAgreement payment = housePayments.get(h); - if(payment != null) { - return(payment.monthlyPayment); - } - return(0.0); - } -} diff --git a/src/main/java/housing/Household.py b/src/main/java/housing/Household.py new file mode 100644 index 00000000..2b943b1e --- /dev/null +++ b/src/main/java/housing/Household.py @@ -0,0 +1,510 @@ +import random + +from typing import Dict + +from .House import House + +#************************************************************************************************* +# This represents a household who receives an income, consumes, saves and can buy, sell, let, and +# invest in houses. +# +# @author daniel, davidrpugh, Adrian Carro +# +#************************************************************************************************/ + +class Household(IHouseOwner): + # Initialises behaviour (determine whether the household will be a BTL investor). Households start off in social + # housing and with their "desired bank balance" in the bank + id_pool = 0 + def __init__(self, Model): + self.config = Model.config + self.home: House = None + self.isFirstTimeBuyer = True + self.isBankrupt = False + # Only used for identifying households within the class MicroDataRecorder + self.id = Household.id_pool # TODOTODO verify this + Household.id_pool += 1 + # Age of the household representative person + self.age = data.Demographics.pdfHouseholdAgeAtBirth.nextDouble() + # Fixed for the whole lifetime of the household + self.incomePercentile = random.random() + # Behavioural plugin + self.behaviour = HouseholdBehaviour(incomePercentile) + # Find initial values for the annual and monthly gross employment income + self.annualGrossEmploymentIncome = data.EmploymentIncome.getAnnualGrossEmploymentIncome(self.age, self.incomePercentile) + self.monthlyGrossEmploymentIncome = self.annualGrossEmploymentIncome / self.config.constants.MONTHS_IN_YEAR + self.bankBalance = self.behaviour.getDesiredBankBalance(self.getAnnualGrossTotalIncome()) # Desired bank balance is used as initial value for actual bank balance + # Keeps track of monthly rental income, as only tenants keep a reference to the rental contract, not landlords + self.monthlyGrossRentalIncome = 0.0 + # Houses owned and their payment agreements + self.housePayments: Dict[House, PaymentAgreement] = {} + + #----- General methods -----# + + # Main simulation step for each household. They age, receive employment and other forms of income, make their rent + # or mortgage payments, perform an essential consumption, make non-essential consumption decisions, manage their + # owned properties, and make their housing decisions depending on their current housing state: + # - Buy or rent if in social housing + # - Sell house if owner-occupier + # - Buy/sell/rent out properties if BTL investor + def step(self) -> None: + self.isBankrupt = False # Delete bankruptcies from previous time step + self.age += 1.0 / self.config.constants.MONTHS_IN_YEAR + # Update annual and monthly gross employment income + self.annualGrossEmploymentIncome = data.EmploymentIncome.getAnnualGrossEmploymentIncome(self.age, self.incomePercentile) + self.monthlyGrossEmploymentIncome = self.annualGrossEmploymentIncome / self.config.constants.MONTHS_IN_YEAR + # Add monthly disposable income (net total income minus essential consumption and housing expenses) to bank balance + self.bankBalance += self.getMonthlyDisposableIncome() + # Consume based on monthly disposable income (after essential consumption and house payments have been subtracted) + self.bankBalance -= self.behaviour.getDesiredConsumption(self.getBankBalance(), self.getAnnualGrossTotalIncome()) # Old implementation: if(isFirstTimeBuyer() || !isInSocialHousing()) bankBalance -= behaviour.getDesiredConsumption(getBankBalance(), getAnnualGrossTotalIncome()) + # Deal with bankruptcies + # TODO: Improve bankruptcy procedures (currently, simple cash injection), such as terminating contracts! + if self.bankBalance < 0.0: + self.bankBalance = 1.0 + self.isBankrupt = True + # Manage owned properties and close debts on previously owned properties. To this end, first, create an + # iterator over the house-paymentAgreement pairs at the household's housePayments object + # Iterate over these house-paymentAgreement pairs... + for h, payment in self.housePayments.items(): + # ...if the household is the owner of the house, then manage it + if h.owner == self: + self.manageHouse(h) + # ...otherwise, if the household is not the owner nor the resident, then it is an old debt due to + # the household's inability to pay the remaining principal off after selling a property... + elif h.resident != self: + mortgage = MortgageAgreement(payment) + # ...remove this type of houses from payments as soon as the household pays the debt off + if (payment.nPayments == 0) and (mortgage.principal == 0.0): + del self.housePayments[h] + # Make housing decisions depending on current housing state + if self.isInSocialHousing(): + self.bidForAHome() # When BTL households are born, they enter here the first time and until they manage to buy a home! + elif self.isRenting(): + if housePayments.get(self.home).nPayments == 0: # End of rental period for this tenant + self.endTenancy() + self.bidForAHome() + elif self.behaviour.isPropertyInvestor(): # Only BTL investors who already own a home enter here + price = self.behaviour.btlPurchaseBid(self) + self.Model.householdStats.countBTLBidsAboveExpAvSalePrice(price) + if self.behaviour.decideToBuyInvestmentProperty(self): + self.Model.houseSaleMarket.BTLbid(self, price) + elif not isHomeowner(): + print("Strange: this household is not a type I recognize") + + # Subtracts the essential, necessary consumption and housing expenses (mortgage and rental payments) from the net + # total income (employment income, property income, financial returns minus taxes) + def getMonthlyDisposableIncome(self) -> float: + # Start with net monthly income + monthlyDisposableIncome = self.getMonthlyNetTotalIncome() + # Subtract essential, necessary consumption + # TODO: ESSENTIAL_CONSUMPTION_FRACTION is not explained in the paper, all support is said to be consumed + monthlyDisposableIncome -= self.config.ESSENTIAL_CONSUMPTION_FRACTION * self.config.GOVERNMENT_MONTHLY_INCOME_SUPPORT + # Subtract housing consumption + for payment in housePayments.values(): + monthlyDisposableIncome -= payment.makeMonthlyPayment() + return monthlyDisposableIncome + + # Subtracts the monthly aliquot part of all due taxes from the monthly gross total income. Note that only income + # tax on employment income and national insurance contributions are implemented! + def getMonthlyNetTotalIncome(self) -> float: + # TODO: Note that this implies there is no tax on rental income nor on bank balance returns + return (self.getMonthlyGrossTotalIncome() - + (self.Model.government.incomeTaxDue(self.annualGrossEmploymentIncome) + # Employment income tax + self.Model.government.class1NICsDue(self.annualGrossEmploymentIncome)) / # National insurance contributions + self.config.constants.MONTHS_IN_YEAR) + + # Adds up all sources of (gross) income on a monthly basis: employment, property, returns on financial wealth + def getMonthlyGrossTotalIncome(self) -> float: + if self.bankBalance > 0.0: + return self.monthlyGrossEmploymentIncome + self.monthlyGrossRentalIncome + self.bankBalance * self.config.RETURN_ON_FINANCIAL_WEALTH + else: + return self.monthlyGrossEmploymentIncome + self.monthlyGrossRentalIncome + + def getAnnualGrossTotalIncome(self) -> float: + return self.getMonthlyGrossTotalIncome() * self.config.constants.MONTHS_IN_YEAR + + #----- Methods for house owners -----# + + # Decide what to do with a house h owned by the household: + # - if the household lives in the house, decide whether to sell it or not + # - if the house is up for sale, rethink its offer price, and possibly put it up for rent instead (only BTL investors) + # - if the house is up for rent, rethink the rent demanded + # + # @param house A house owned by the household + def manageHouse(self, house: House) -> None: + forSale = house.getSaleRecord() + if forSale is not None: # reprice house for sale + newPrice = self.behaviour.rethinkHouseSalePrice(forSale) + if newPrice > mortgageFor(house).principal: + self.Model.houseSaleMarket.updateOffer(forSale, newPrice) + else: + self.Model.houseSaleMarket.removeOffer(forSale) + # TODO: Is first condition redundant? + if house != self.home and house.resident is None: + self.Model.houseRentalMarket.offer(house, buyToLetRent(house), false) + elif self.decideToSellHouse(house): # put house on market? + if house.isOnRentalMarket(): + self.Model.houseRentalMarket.removeOffer(house.getRentalRecord()) + self.putHouseForSale(house) + + forRent = house.getRentalRecord() + if forRent is not None: # reprice house for rent + newPrice = self.behaviour.rethinkBuyToLetRent(forRent) + self.Model.houseRentalMarket.updateOffer(forRent, newPrice) + + #***************************************************** + # Having decided to sell house h, decide its initial sale price and put it up in the market. + # + # @param h the house being sold + #*****************************************************/ + def putHouseForSale(self, h: House) -> None: + principal: float + mortgage: MortgageAgreement = mortgageFor(h) + if mortgage is not None: + principal = mortgage.principal + else: + principal = 0.0 + if h == self.home: + self.Model.houseSaleMarket.offer(h, behaviour.getInitialSalePrice(h.getQuality(), principal), False) + else: + self.Model.houseSaleMarket.offer(h, behaviour.getInitialSalePrice(h.getQuality(), principal), True) + + ############################/ + # Houseowner interface + ############################/ + + #******************************************************* + # Do all the stuff necessary when this household + # buys a house: + # Give notice to landlord if renting, + # Get loan from mortgage-lender, + # Pay for house, + # Put house on rental market if buy-to-let and no tenant. + #*******************************************************/ + def completeHousePurchase(self, sale: HouseOfferRecord) -> None: + if self.isRenting(): # give immediate notice to landlord and move out + if sale.getHouse().resident is not None: + print("Strange: my new house has someone in it!") + if self.home == sale.getHouse(): + print("Strange: I've just bought a house I'm renting out") + else: + self.endTenancy() + mortgage = self.Model.bank.requestLoan(self, sale.getPrice(), self.behaviour.decideDownPayment(self, sale.getPrice()), self.home is None, sale.getHouse()) + if mortgage is None: + # TODO: need to either provide a way for house sales to fall through or to ensure that pre-approvals are always satisfiable + print("Can't afford to buy house: strange") + print("Bank balance is " + str(bankBalance)) + print("Annual income is " + str(self.monthlyGrossEmploymentIncome * self.config.constants.MONTHS_IN_YEAR)) + if self.isRenting(): + print("Is renting") + if self.isHomeowner(): + print("Is homeowner") + if self.isInSocialHousing(): + print("Is homeless") + if self.isFirstTimeBuyer(): + print("Is firsttimebuyer") + if self.behaviour.isPropertyInvestor(): + print("Is investor") + print("House owner = " + str(sale.getHouse().owner)) + print("me = " + str(self)) + else: + self.bankBalance -= mortgage.downPayment + self.housePayments[sale.getHouse()] = mortgage + if self.home is None: # move in to house + self.home = sale.getHouse() + sale.getHouse().resident = self + elif sale.getHouse().resident is None: # put empty buy-to-let house on rental market + self.Model.houseRentalMarket.offer(sale.getHouse(), self.buyToLetRent(sale.getHouse()), False) + self.isFirstTimeBuyer = False + + #******************************************************* + # Do all stuff necessary when this household sells a house + #*******************************************************/ + def completeHouseSale(self, sale: HouseOfferRecord) -> None: + # First, receive money from sale + self.bankBalance += sale.getPrice() + # Second, find mortgage object and pay off as much outstanding debt as possible given bank balance + mortgage = self.mortgageFor(sale.getHouse()) + self.bankBalance -= mortgage.payoff(self.bankBalance) + # Third, if there is no more outstanding debt, remove the house from the household's housePayments object + if mortgage.nPayments == 0: + del housePayments[sale.getHouse()] + # TODO: Warning, if bankBalance is not enough to pay mortgage back, then the house stays in housePayments, + # TODO: consequences to be checked. Looking forward, properties and payment agreements should be kept apart + # Fourth, if the house is still being offered on the rental market, withdraw the offer + if sale.getHouse().isOnRentalMarket(): + self.Model.houseRentalMarket.removeOffer(sale) + # Fifth, if the house is the household's home, then the household moves out and becomes temporarily homeless... + if sale.getHouse() == self.home: + self.home.resident = None + self.home = None + # ...otherwise, if the house has a resident, it must be a renter, who must get evicted, also the rental income + # corresponding to this tenancy must be subtracted from the owner's monthly rental income + elif sale.getHouse().resident is not None: + self.monthlyGrossRentalIncome -= sale.getHouse().resident.housePayments.get(sale.getHouse()).monthlyPayment + sale.getHouse().resident.getEvicted() + + #******************************************************* + # A BTL investor receives this message when a tenant moves + # out of one of its buy-to-let houses. + # + # The household simply puts the house back on the rental + # market. + #*******************************************************/ + def endOfLettingAgreement(self, h: House, contract: PaymentAgreement) -> None: + self.monthlyGrossRentalIncome -= contract.monthlyPayment + + # put house back on rental market + if h not in self.housePayments: + print("Strange: I don't own this house in endOfLettingAgreement") +# if(h.resident != null) System.out.println("Strange: renting out a house that has a resident") +# if(h.resident != null && h.resident == h.owner) System.out.println("Strange: renting out a house that belongs to a homeowner") + if h.isOnRentalMarket(): + prnt("Strange: got endOfLettingAgreement on house on rental market") + if not h.isOnMarket(): + self.Model.houseRentalMarket.offer(h, self.buyToLetRent(h), False) + + #********************************************************* + # This household moves out of current rented accommodation + # and becomes homeless (possibly temporarily). Move out, + # inform landlord and delete rental agreement. + #*********************************************************/ + def endTenancy(self) -> None: + self.home.owner.endOfLettingAgreement(self.home, housePayments.get(self.home)) + del self.housePayments[self.home] + self.home.resident = null + self.home = null + + #*** Landlord has told this household to get out: leave without informing landlord */ + def getEvicted(self) -> None: + if self.home is None: + print("Strange: got evicted but I'm homeless") + if self.home.owner == self: + print("Strange: got evicted from a home I own") + del self.housePayments[self.home] + self.home.resident = None + self.home = None + + #******************************************************* + # Do all the stuff necessary when this household moves + # in to rented accommodation (i.e. set up a regular + # payment contract. At present we use a MortgageApproval). + #*******************************************************/ + def completeHouseRental(self, sale: HouseOfferRecord) -> None: + if sale.getHouse().owner != self: # if renting own house, no need for contract + rent = RentalAgreement() + rent.monthlyPayment = sale.getPrice() + rent.nPayments = self.config.TENANCY_LENGTH_AVERAGE + random.randint(0, 2 * self.config.TENANCY_LENGTH_EPSILON + 1) - self.config.TENANCY_LENGTH_EPSILON +# rent.principal = rent.monthlyPayment*rent.nPayments + self.housePayments[sale.getHouse()] = rent + if self.home is not None: + print("Strange: I'm renting a house but not homeless") + self.home = sale.getHouse() + if sale.getHouse().resident is not None: + print("Strange: tenant moving into an occupied house") + if sale.getHouse().resident == self: + print("...It's me!") + if sale.getHouse().owner == self: + print("...It's my house!") + if sale.getHouse().owner == sale.getHouse().resident: + print("...It's a homeowner!") + sale.getHouse().resident = self + + #******************************************************* + # Make the decision whether to bid on the housing market or rental market. + # This is an "intensity of choice" decision (sigma function) + # on the cost of renting compared to the cost of owning, with + # COST_OF_RENTING being an intrinsic psychological cost of not + # owning. + #*******************************************************/ + def bidForAHome(self) -> None: + # Find household's desired housing expenditure + price: float = self.behaviour.getDesiredPurchasePrice(self.monthlyGrossEmploymentIncome) + # Cap this expenditure to the maximum mortgage available to the household + price = min(price, self.Model.bank.getMaxMortgage(self, True)) + # Record the bid on householdStats for counting the number of bids above exponential moving average sale price + self.Model.householdStats.countNonBTLBidsAboveExpAvSalePrice(price) + # Compare costs to decide whether to buy or rent... + if self.behaviour.decideRentOrPurchase(self, price): + # ... if buying, bid in the house sale market for the capped desired price + self.Model.houseSaleMarket.bid(self, price) + else: + # ... if renting, bid in the house rental market for the desired rent price + self.Model.houseRentalMarket.bid(self, self.behaviour.desiredRent(self.monthlyGrossEmploymentIncome)) + + #******************************************************* + # Decide whether to sell ones own house. + #*******************************************************/ + def decideToSellHouse(self, h: House) -> bool: + if h == self.home: + return self.behaviour.decideToSellHome() + else: + return self.behaviour.decideToSellInvestmentProperty(h, self) + + # Do stuff necessary when BTL investor lets out a rental + # property + def completeHouseLet(self, sale: HouseOfferRecord) -> None: + if sale.getHouse().isOnMarket(): + self.Model.houseSaleMarket.removeOffer(sale.getHouse().getSaleRecord()) + self.monthlyGrossRentalIncome += sale.getPrice() + + def buyToLetRent(h: House) -> float: + return self.behaviour.buyToLetRent( + self.Model.rentalMarketStats.getExpAvSalePriceForQuality(h.getQuality()), + self.Model.rentalMarketStats.getExpAvDaysOnMarket(), h) + + ############################/ + # Inheritance behaviour + ############################/ + + # Implement inheritance: upon death, transfer all wealth to the previously selected household. + # + # Take all houses off the markets, evict any tenants, pay off mortgages, and give property and remaining + # bank balance to the beneficiary. + # @param beneficiary The household that will inherit the wealth + def transferAllWealthTo(beneficiary: Household) -> None: + # Check if beneficiary is the same as the deceased household + if beneficiary == self: # TODO: I don't think this check is really necessary + print("Strange: I'm transferring all my wealth to myself") + exit() + # Iterate over these house-paymentAgreement pairs + for h, payment in self.housePayments.items(): + # If the deceased household owns the house, then... + if h.owner == self: + # ...first, withdraw the house from any market where it is currently being offered + if h.isOnRentalMarket(): + self.Model.houseRentalMarket.removeOffer(h.getRentalRecord()) + if h.isOnMarket(): + self.Model.houseSaleMarket.removeOffer(h.getSaleRecord()) + # ...then, if there is a resident in the house... + if h.resident is not None: + # ...and this resident is different from the deceased household, then this resident must be a + # tenant, who must get evicted + if h.resident != self: + h.resident.getEvicted() # TODO: Explain in paper that renters always get evicted, not just if heir needs the house + # ...otherwise, if the resident is the deceased household, remove it from the house + else: + h.resident = None + # ...finally, transfer the property to the beneficiary household + beneficiary.inheritHouse(h) + # Otherwise, if the deceased household does not own the house but it is living in it, then it must have + # been renting it: end the letting agreement + elif h == self.home: + h.owner.endOfLettingAgreement(h, housePayments.get(h)) + h.resident = None + # If payment agreement is a mortgage, then try to pay off as much as possible from the deceased household's bank balance + if isinstance(payment, MortgageAgreement): + self.bankBalance -= payment.payoff() + # Remove the house-paymentAgreement entry from the deceased household's housePayments object + del self.housePayments[h] # TODO: Not sure this is necessary. Note, though, that this implies erasing all outstanding debt + # Finally, transfer all remaining liquid wealth to the beneficiary household + beneficiary.bankBalance += max(0.0, self.bankBalance) + + # Inherit a house. + # + # Write off the mortgage for the house. Move into the house if renting or in social housing. + # + # @param h House to inherit + def inheritHouse(h: House) -> None: + # Create a null (zero payments) mortgage + nullMortgage = MortgageAgreement(self, False) + nullMortgage.nPayments = 0 + nullMortgage.downPayment = 0.0 + nullMortgage.monthlyInterestRate = 0.0 + nullMortgage.monthlyPayment = 0.0 + nullMortgage.principal = 0.0 + nullMortgage.purchasePrice = 0.0 + # Become the owner of the inherited house and include it in my housePayments list (with a null mortgage) + # TODO: Make sure the paper correctly explains that no debt is inherited + self.housePayments.put[h] = nullMortgage + h.owner = self + # Check for residents in the inherited house + if h.resident is not None: + print("Strange: inheriting a house with a resident") + exit() + # If renting or homeless, move into the inherited house + if not self.isHomeowner(): + # If renting, first cancel my current tenancy + if self.isRenting(): + self.endTenancy() + self.home = h + h.resident = this + # If owning a home and having the BTL gene... + elif self.behaviour.isPropertyInvestor(): + # ...decide whether to sell the inherited house + if self.decideToSellHouse(h): + self.putHouseForSale(h) + # ...or rent it out + elif h.resident is None: + self.Model.houseRentalMarket.offer(h, self.buyToLetRent(h), False) + # If being an owner-occupier, put inherited house for sale + else: + self.putHouseForSale(h) + + #----- Helpers -----# + + def getAge(self) -> float: + return age + + def isHomeowner(self) -> bool: + if self.home is None: + return False + return self.home.owner == self + + def isRenting(self) -> bool: + if self.home is None: + return False + return self.home.owner != self + + def isInSocialHousing(self) -> bool: + return self.home is None + + def isFirstTimeBuyer(self) -> bool: + return self.isFirstTimeBuyer + + def isBankrupt(self) -> bool: + return self.isBankrupt + + def getBankBalance(self) -> float: + return self.bankBalance + + def getHome(self) -> House: + return self.home + + def getHousePayments(self) -> Dict[House, PaymentAgreement]: + return self.housePayments + + def getAnnualGrossEmploymentIncome(self) -> float: + return self.annualGrossEmploymentIncome + + def getMonthlyGrossEmploymentIncome(self) -> float: + return self.monthlyGrossEmploymentIncome + + # @return Number of properties this household currently has on the sale market + def nPropertiesForSale(self) -> int: + n = 0 + for h in self.housePayments.keys(): + if h.isOnMarket(): + n += 1 + return n + + def nInvestmentProperties(self) -> int: + return len(self.housePayments) - 1 + + # @return Current mark-to-market (with exponentially averaged prices per quality) equity in this household's home. + def getHomeEquity(self) -> float: + if not self.isHomeowner(): + return 0.0 + return self.Model.housingMarketStats.getExpAvSalePriceForQuality(self.home.getQuality()) - self.mortgageFor(self.home).principal + + def mortgageFor(self, h: House) -> MortgageAgreement: + payment = self.housePayments.get(h) + if isinstance(payment, MortgageAgreement): + return MortgageAgreement(payment) + return None + + def monthlyPaymentOn(self, h: House) -> float: + payment = self.housePayments.get(h) + if payment is not None: + return payment.monthlyPayment + return 0.0 diff --git a/src/main/java/housing/HouseholdBehaviour.java b/src/main/java/housing/HouseholdBehaviour.java deleted file mode 100644 index 256f032c..00000000 --- a/src/main/java/housing/HouseholdBehaviour.java +++ /dev/null @@ -1,398 +0,0 @@ -package housing; - -import org.apache.commons.math3.distribution.LogNormalDistribution; -import org.apache.commons.math3.random.MersenneTwister; - -/************************************************************************************************** - * Class to implement the behavioural decisions made by households - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ -public class HouseholdBehaviour { - - //------------------// - //----- Fields -----// - //------------------// - - private Config config = Model.config; // Passes the Model's configuration parameters object to a private field - private MersenneTwister prng; - private boolean BTLInvestor; - private double BTLCapGainCoefficient; // Sensitivity of BTL investors to capital gain, 0.0 cares only about rental yield, 1.0 cares only about cap gain - private double propensityToSave; - private LogNormalDistribution downpaymentDistFTB; // Size distribution for downpayments of first-time-buyers - private LogNormalDistribution downpaymentDistOO; // Size distribution for downpayments of owner-occupiers - - //------------------------// - //----- Constructors -----// - //------------------------// - - /** - * Initialise behavioural variables for a new household: propensity to save, whether the household will have the BTL - * investor "gene" (provided its income percentile is above a certain minimum), and whether the household will be a - * fundamentalist or a trend follower investor (provided it has received the BTL investor gene) - * - * @param incomePercentile Fixed income percentile for the household (assumed constant over a lifetime) - */ - HouseholdBehaviour(MersenneTwister prng, double incomePercentile) { - this.prng = prng; // initialize the random number generator - - // Set downpayment distributions for both first-time-buyers and owner-occupiers - downpaymentDistFTB = new LogNormalDistribution(this.prng, config.DOWNPAYMENT_FTB_SCALE, config.DOWNPAYMENT_FTB_SHAPE); - downpaymentDistOO = new LogNormalDistribution(this.prng, config.DOWNPAYMENT_OO_SCALE, config.DOWNPAYMENT_OO_SHAPE); - // Compute propensity to save, so that it is constant for a given household - propensityToSave = config.DESIRED_BANK_BALANCE_EPSILON * prng.nextGaussian(); - // Decide if household is a BTL investor and, if so, its tendency to seek capital gains or rental yields - BTLCapGainCoefficient = 0.0; - if(incomePercentile > config.MIN_INVESTOR_PERCENTILE && - prng.nextDouble() < config.getPInvestor()/config.MIN_INVESTOR_PERCENTILE) { - BTLInvestor = true; - if(prng.nextDouble() < config.P_FUNDAMENTALIST) { - BTLCapGainCoefficient = config.FUNDAMENTALIST_CAP_GAIN_COEFF; - } else { - BTLCapGainCoefficient = config.TREND_CAP_GAIN_COEFF; - } - } else { - BTLInvestor = false; - } - } - - //-------------------// - //----- Methods -----// - //-------------------// - - //----- General behaviour -----// - - /** - * Compute the monthly non-essential or optional consumption by a household. It is calibrated so that the output - * wealth distribution fits the ONS wealth data for Great Britain. - * - * @param bankBalance Household's liquid wealth - * @param annualGrossTotalIncome Household's annual gross total income - */ - double getDesiredConsumption(double bankBalance, double annualGrossTotalIncome) { - return config.CONSUMPTION_FRACTION*Math.max(bankBalance - getDesiredBankBalance(annualGrossTotalIncome), 0.0); - } - - /** - * Minimum bank balance each household is willing to have at the end of the month for the whole population to match - * the wealth distribution obtained from the household survey (LCFS). In particular, in line with the Wealth and - * Assets Survey, we model the relationship between liquid wealth and gross annual income as log-normal. This - * desired bank balance will be then used to determine non-essential consumption. - * TODO: Relationship described as log-normal here but power-law implemented! Dan's version of article described the - * TODO: the distributions of gross income and of liquid wealth as log-normal, not their relationship. Change paper! - * - * @param annualGrossTotalIncome Household - */ - double getDesiredBankBalance(double annualGrossTotalIncome) { - return Math.exp(config.DESIRED_BANK_BALANCE_ALPHA - + config.DESIRED_BANK_BALANCE_BETA*Math.log(annualGrossTotalIncome) + propensityToSave); - } - - //----- Owner-Occupier behaviour -----// - - /** - * Desired purchase price used to decide whether to buy a house and how much to bid for it - * - * @param monthlyGrossEmploymentIncome Monthly gross employment income of the household - */ - double getDesiredPurchasePrice(double monthlyGrossEmploymentIncome) { - // TODO: This product is generally so small that it barely has any impact on the results, need to rethink if - // TODO: it is necessary and if this small value makes any sense - double HPAFactor = config.BUY_WEIGHT_HPA*getLongTermHPAExpectation(); - // TODO: The capping of this factor intends to avoid negative and too large desired prices, the 0.9 is a - // TODO: purely artificial fudge parameter. This formula should be reviewed and changed! - if (HPAFactor > 0.9) HPAFactor = 0.9; - // TODO: Note that wealth is not used here, but only monthlyGrossEmploymentIncome - return config.BUY_SCALE*config.constants.MONTHS_IN_YEAR*monthlyGrossEmploymentIncome - *Math.exp(config.BUY_EPSILON*prng.nextGaussian()) - /(1.0 - HPAFactor); - } - - /** - * Initial sale price of a house to be listed - * - * @param quality Quality of the house ot be sold - * @param principal Amount of principal left on any mortgage on this house - */ - double getInitialSalePrice(int quality, double principal) { - double exponent = config.SALE_MARKUP - + Math.log(Model.housingMarketStats.getExpAvSalePriceForQuality(quality) + 1.0) - - config.SALE_WEIGHT_DAYS_ON_MARKET*Math.log((Model.housingMarketStats.getExpAvDaysOnMarket() - + 1.0)/(config.constants.DAYS_IN_MONTH + 1.0)) - + config.SALE_EPSILON*prng.nextGaussian(); - // TODO: ExpAv days on market could be computed for each quality band so as to use here only the correct one - return Math.max(Math.exp(exponent), principal); - } - - /** - * This method implements a household's decision to sell their owner-occupied property. On average, households sell - * owner-occupied houses every 11 years, due to exogenous reasons not addressed in the model. In order to prevent - * an unrealistic build-up of housing stock and unrealistic fluctuations of the interest rate, we modify this - * probability by introducing two extra factors, depending, respectively, on the number of houses per capita - * currently on the market and its exponential moving average, and on the interest rate and its exponential moving - * average. In this way, the long-term selling probability converges to 1/11. - * TODO: This method includes 2 unidentified fudge parameters, DECISION_TO_SELL_HPC (houses per capita) and - * TODO: DECISION_TO_SELL_INTEREST, which are explicitly explained otherwise in the manuscript. URGENT! - * TODO: Basically, need to implement both exponential moving averages referred above - * - * @return True if the owner-occupier decides to sell the house and false otherwise. - */ - boolean decideToSellHome() { - // TODO: This if implies BTL agents never sell their homes, need to explain in paper! - return !isPropertyInvestor() && (prng.nextDouble() < config.derivedParams.MONTHLY_P_SELL*(1.0 - + config.DECISION_TO_SELL_ALPHA*(config.DECISION_TO_SELL_HPC - - (double)Model.houseSaleMarket.getnHousesOnMarket()/Model.households.size()) - + config.DECISION_TO_SELL_BETA*(config.DECISION_TO_SELL_INTEREST - - Model.bank.getMortgageInterestRate()))); - } - - /** - * Decide amount to pay as initial downpayment - * - * @param me the household - * @param housePrice the price of the house - */ - double decideDownPayment(Household me, double housePrice) { - if (me.getBankBalance() > housePrice*config.BANK_BALANCE_FOR_CASH_DOWNPAYMENT) { - return housePrice; - } - double downpayment; - if (me.isFirstTimeBuyer()) { - // Since the function of the HPI is to move the down payments distribution upwards or downwards to - // accommodate current price levels, and the distribution is itself aggregate, we use the aggregate HPI - downpayment = Model.housingMarketStats.getHPI()*downpaymentDistFTB.inverseCumulativeProbability(Math.max(0.0, - (me.incomePercentile - config.DOWNPAYMENT_MIN_INCOME)/(1 - config.DOWNPAYMENT_MIN_INCOME))); - } else if (isPropertyInvestor()) { - downpayment = housePrice*(Math.max(0.0, - config.DOWNPAYMENT_BTL_MEAN + config.DOWNPAYMENT_BTL_EPSILON * prng.nextGaussian())); - } else { - downpayment = Model.housingMarketStats.getHPI()*downpaymentDistOO.inverseCumulativeProbability(Math.max(0.0, - (me.incomePercentile - config.DOWNPAYMENT_MIN_INCOME)/(1 - config.DOWNPAYMENT_MIN_INCOME))); - } - if (downpayment > me.getBankBalance()) downpayment = me.getBankBalance(); - return downpayment; - } - - /////////////////////////////////////////////////////////// - ///////////////////////// REVISED ///////////////////////// - /////////////////////////////////////////////////////////// - - /******************************************************** - * Decide how much to drop the list-price of a house if - * it has been on the market for (another) month and hasn't - * sold. Calibrated against Zoopla dataset in Bank of England - * - * @param sale The HouseOfferRecord of the house that is on the market. - ********************************************************/ - double rethinkHouseSalePrice(HouseOfferRecord sale) { - if(prng.nextDouble() < config.P_SALE_PRICE_REDUCE) { - double logReduction = config.REDUCTION_MU + (prng.nextGaussian()*config.REDUCTION_SIGMA); - return(sale.getPrice()*(1.0 - Math.exp(logReduction)/100.0)); - } - return(sale.getPrice()); - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // Renter behaviour - /////////////////////////////////////////////////////////////////////////////////////////////// - - /*** renters or OO after selling home decide whether to rent or buy - * N.B. even though the HH may not decide to rent a house of the - * same quality as they would buy, the cash value of the difference in quality - * is assumed to be the difference in rental price between the two qualities. - * @return true if we should buy a house, false if we should rent - */ - boolean decideRentOrPurchase(Household me, double purchasePrice) { - if(isPropertyInvestor()) return(true); - MortgageAgreement mortgageApproval = Model.bank.requestApproval(me, purchasePrice, - decideDownPayment(me, purchasePrice), true); - int newHouseQuality = Model.housingMarketStats.getMaxQualityForPrice(purchasePrice); - if (newHouseQuality < 0) return false; // can't afford a house anyway - double costOfHouse = mortgageApproval.monthlyPayment*config.constants.MONTHS_IN_YEAR - - purchasePrice*getLongTermHPAExpectation(); - double costOfRent = Model.rentalMarketStats.getExpAvSalePriceForQuality(newHouseQuality) - *config.constants.MONTHS_IN_YEAR; - return prng.nextDouble() < sigma(config.SENSITIVITY_RENT_OR_PURCHASE*(costOfRent*(1.0 - + config.PSYCHOLOGICAL_COST_OF_RENTING) - costOfHouse)); - } - - /******************************************************** - * Decide how much to bid on the rental market - * Source: Zoopla rental prices 2008-2009 (at Bank of England) - ********************************************************/ - double desiredRent(double monthlyGrossEmploymentIncome) { - return monthlyGrossEmploymentIncome*config.DESIRED_RENT_INCOME_FRACTION; - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - // Property investor behaviour - /////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Decide whether to sell or not an investment property. Investor households with only one investment property do - * never sell it. A sale is never attempted when the house is occupied by a tenant. Households with at least two - * investment properties will calculate the expected yield of the property in question based on two contributions: - * rental yield and capital gain (with their corresponding weights which depend on the type of investor) - * - * @param h The house in question - * @param me The investor household - * @return True if investor me decides to sell investment property h - */ - boolean decideToSellInvestmentProperty(House h, Household me) { - // Fast decisions... - // ...always keep at least one investment property - if(me.nInvestmentProperties() < 2) return false; - // ...don't sell while occupied by tenant - if(!h.isOnRentalMarket()) return false; - - // Find the expected equity yield rate of this property as a weighted mix of both rental yield and capital gain - // times the leverage - // ...find the mortgage agreement for this property - MortgageAgreement mortgage = me.mortgageFor(h); - // ...find its current (fair market value) sale price - double currentMarketPrice = Model.housingMarketStats.getExpAvSalePriceForQuality(h.getQuality()); - // ...find equity, or assets minus liabilities - double equity = Math.max(0.01, currentMarketPrice - mortgage.principal); // The 0.01 prevents possible divisions by zero later on - // ...find the leverage on that mortgage (Assets divided by equity, or return on equity) - double leverage = currentMarketPrice/equity; - // ...find the expected rental yield of this property as its current rental price divided by its current (fair market value) sale price - // TODO: ATTENTION ---> This rental yield is not accounting for expected occupancy... shouldn't it? - double currentRentalYield = h.getRentalRecord().getPrice()*config.constants.MONTHS_IN_YEAR/currentMarketPrice; - // ...find the mortgage rate (pounds paid a year per pound of equity) - double mortgageRate = mortgage.nextPayment()*config.constants.MONTHS_IN_YEAR/equity; - // ...finally, find expected equity yield, or yield on equity - double expectedEquityYield; - if(config.BTL_YIELD_SCALING) { - expectedEquityYield = leverage*((1.0 - BTLCapGainCoefficient)*currentRentalYield - + BTLCapGainCoefficient*(Model.rentalMarketStats.getLongTermExpAvFlowYield() - + getLongTermHPAExpectation())) - mortgageRate; - } else { - expectedEquityYield = leverage*((1.0 - BTLCapGainCoefficient)*currentRentalYield - + BTLCapGainCoefficient*getLongTermHPAExpectation()) - - mortgageRate; - } - // Compute a probability to keep the property as a function of the effective yield - double pKeep = Math.pow(sigma(config.BTL_CHOICE_INTENSITY*expectedEquityYield), - 1.0/config.constants.MONTHS_IN_YEAR); - // Return true or false as a random draw from the computed probability - return prng.nextDouble() < (1.0 - pKeep); - } - - /** - * Decide whether to buy or not a new investment property. Investor households with no investment properties always - * attempt to buy one. If the household's bank balance is below its desired bank balance, then no attempt to buy is - * made. If the resources available to the household (maximum mortgage) are below the average price for the lowest - * quality houses, then no attempt to buy is made. Households with at least one investment property will calculate - * the expected yield of a new property based on two contributions: rental yield and capital gain (with their - * corresponding weights which depend on the type of investor) - * - * @param me The investor household - * @return True if investor me decides to try to buy a new investment property - */ - boolean decideToBuyInvestmentProperty(Household me) { - // Fast decisions... - // ...always decide to buy if owning no investment property yet - if (me.nInvestmentProperties() < 1) { return true ; } - // ...never buy (keep on saving) if bank balance is below the household's desired bank balance - // TODO: This mechanism and its parameter are not declared in the article! Any reference for the value of the parameter? - if (me.getBankBalance() < getDesiredBankBalance(me.getAnnualGrossTotalIncome())*config.BTL_CHOICE_MIN_BANK_BALANCE) { return false; } - // ...find maximum price (maximum mortgage) the household could pay - double maxPrice = Model.bank.getMaxMortgage(me, false); - // ...never buy if that maximum price is below the average price for the lowest quality - if (maxPrice < Model.housingMarketStats.getExpAvSalePriceForQuality(0)) { return false; } - - // Find the expected equity yield rate for a hypothetical house maximising the leverage available to the - // household and assuming an average rental yield (over all qualities). This is found as a weighted mix of both - // rental yield and capital gain times the leverage - // ...find mortgage with maximum leverage by requesting maximum mortgage with minimum downpayment - MortgageAgreement mortgage = Model.bank.requestApproval(me, maxPrice, 0.0, false); - // ...find equity, or assets minus liabilities (which, initially, is simply the downpayment) - double equity = Math.max(0.01, mortgage.downPayment); // The 0.01 prevents possible divisions by zero later on - // ...find the leverage on that mortgage (Assets divided by equity, or return on equity) - double leverage = mortgage.purchasePrice/equity; - // ...find the expected rental yield as an (exponential) average over all house qualities - double rentalYield = Model.rentalMarketStats.getExpAvFlowYield(); - // ...find the mortgage rate (pounds paid a year per pound of equity) - double mortgageRate = mortgage.nextPayment()*config.constants.MONTHS_IN_YEAR/equity; - // ...finally, find expected equity yield, or yield on equity - double expectedEquityYield; - if(config.BTL_YIELD_SCALING) { - expectedEquityYield = leverage*((1.0 - BTLCapGainCoefficient)*rentalYield - + BTLCapGainCoefficient*(Model.rentalMarketStats.getLongTermExpAvFlowYield() - + getLongTermHPAExpectation())) - mortgageRate; - } else { - expectedEquityYield = leverage*((1.0 - BTLCapGainCoefficient)*rentalYield - + BTLCapGainCoefficient*getLongTermHPAExpectation()) - - mortgageRate; - } - // Compute the probability to decide to buy an investment property as a function of the expected equity yield - // TODO: This probability has been changed to correctly reflect the conversion from annual to monthly - // TODO: probability. This needs to be explained in the article -// double pBuy = Math.pow(sigma(config.BTL_CHOICE_INTENSITY*expectedEquityYield), -// 1.0/config.constants.MONTHS_IN_YEAR); - double pBuy = 1.0 - Math.pow((1.0 - sigma(config.BTL_CHOICE_INTENSITY*expectedEquityYield)), - 1.0/config.constants.MONTHS_IN_YEAR); - // Return true or false as a random draw from the computed probability - return prng.nextDouble() < pBuy; - } - - double btlPurchaseBid(Household me) { - // TODO: What is this 1.1 factor? Another fudge parameter? It prevents wealthy investors from offering more than - // TODO: 10% above the average price of top quality houses. The effect of this is to prevent fast increases of - // TODO: price as BTL investors buy all supply till prices are too high for everybody. Fairly unclear mechanism, - // TODO: check for removal! - return(Math.min(Model.bank.getMaxMortgage(me, false), - 1.1*Model.housingMarketStats.getExpAvSalePriceForQuality(config.N_QUALITY-1))); - } - - /** - * How much rent does an investor decide to charge on a buy-to-let house? - * @param rbar exponential average rent for house of this quality - * @param d average days on market - * @param h house being offered for rent - */ - double buyToLetRent(double rbar, double d, House h) { - // TODO: What? Where does this equation come from? - final double beta = config.RENT_MARKUP/Math.log(config.RENT_EQ_MONTHS_ON_MARKET); // Weight of days-on-market effect - - double exponent = config.RENT_MARKUP + Math.log(rbar + 1.0) - - beta*Math.log((d + 1.0)/(config.constants.DAYS_IN_MONTH + 1)) - + config.RENT_EPSILON * prng.nextGaussian(); - double result = Math.exp(exponent); - // TODO: The following contains a fudge (config.RENT_MAX_AMORTIZATION_PERIOD) to keep rental yield up - double minAcceptable = Model.housingMarketStats.getExpAvSalePriceForQuality(h.getQuality()) - /(config.RENT_MAX_AMORTIZATION_PERIOD*config.constants.MONTHS_IN_YEAR); - if (result < minAcceptable) result = minAcceptable; - return result; - - } - - /** - * Update the demanded rent for a property - * - * @param sale the HouseOfferRecord of the property for rent - * @return the new rent - */ - double rethinkBuyToLetRent(HouseOfferRecord sale) { return (1.0 - config.RENT_REDUCTION)*sale.getPrice(); } - - /** - * Logistic function, sometimes called sigma function, 1/1+e^(-x) - * - * @param x Parameter of the sigma or logistic function - */ - private double sigma(double x) { return 1.0/(1.0 + Math.exp(-1.0*x)); } - - /** - * @return expectation value of HPI in one year's time divided by today's HPI - */ - private double getLongTermHPAExpectation() { - // Dampening or multiplier factor, depending on its value being <1 or >1, for the current trend of HPA when - // computing expectations as in HPI(t+DT) = HPI(t) + FACTOR*DT*dHPI/dt (double) - return(Model.housingMarketStats.getLongTermHPA()*config.HPA_EXPECTATION_FACTOR); - } - - public double getBTLCapGainCoefficient() { return BTLCapGainCoefficient; } - - public boolean isPropertyInvestor() { return BTLInvestor; } -} diff --git a/src/main/java/housing/HouseholdBehaviour.py b/src/main/java/housing/HouseholdBehaviour.py new file mode 100644 index 00000000..8d03f799 --- /dev/null +++ b/src/main/java/housing/HouseholdBehaviour.py @@ -0,0 +1,342 @@ +import math +import random + +#************************************************************************************************** +#* Class to implement the behavioural decisions made by households +#* +#* @author daniel, Adrian Carro +#* +#*************************************************************************************************/ +class HouseholdBehaviour: + # Initialise behavioural variables for a new household: propensity to save, whether the household will have the BTL + # investor "gene" (provided its income percentile is above a certain minimum), and whether the household will be a + # fundamentalist or a trend follower investor (provided it has received the BTL investor gene) + # + # @param incomePercentile Fixed income percentile for the household (assumed constant over a lifetime) + def __init__(self, Model, incomePercentile: float): + self.config = Model.config + self.Model = Model + + # Set downpayment distributions for both first-time-buyers and owner-occupiers + # Size distribution for downpayments of first-time-buyers + self.downpaymentDistFTB = LogNormalDistribution(self.config.DOWNPAYMENT_FTB_SCALE, self.config.DOWNPAYMENT_FTB_SHAPE) + # Size distribution for downpayments of owner-occupiers + self.downpaymentDistOO = LogNormalDistribution(self.config.DOWNPAYMENT_OO_SCALE, self.config.DOWNPAYMENT_OO_SHAPE) + # Compute propensity to save, so that it is constant for a given household + self.propensityToSave = self.config.DESIRED_BANK_BALANCE_EPSILON * random.normalvariate(0, 1) + # Decide if household is a BTL investor and, if so, its tendency to seek capital gains or rental yields + # Sensitivity of BTL investors to capital gain, 0.0 cares only about rental yield, 1.0 cares only about cap gain + self.BTLCapGainCoefficient = 0.0 + if incomePercentile > self.config.MIN_INVESTOR_PERCENTILE and random.random() < self.config.getPInvestor() / self.config.MIN_INVESTOR_PERCENTILE: + self.BTLInvestor = True + if random.random() < config.P_FUNDAMENTALIST: + self.BTLCapGainCoefficient = self.config.FUNDAMENTALIST_CAP_GAIN_COEFF + else: + self.BTLCapGainCoefficient = self.config.TREND_CAP_GAIN_COEFF + else: + self.BTLInvestor = False + + #-------------------# + #----- Methods -----# + #-------------------# + + #----- General behaviour -----# + + #* Compute the monthly non-essential or optional consumption by a household. It is calibrated so that the output + #* wealth distribution fits the ONS wealth data for Great Britain. + #* + #* @param bankBalance Household's liquid wealth + #* @param annualGrossTotalIncome Household's annual gross total income + def getDesiredConsumption(self, bankBalance: float, annualGrossTotalIncome: float) -> float: + return self.config.CONSUMPTION_FRACTION * max(bankBalance - self.getDesiredBankBalance(annualGrossTotalIncome), 0.0) + + # Minimum bank balance each household is willing to have at the end of the month for the whole population to match + # the wealth distribution obtained from the household survey (LCFS). In particular, in line with the Wealth and + # Assets Survey, we model the relationship between liquid wealth and gross annual income as log-normal. This + # desired bank balance will be then used to determine non-essential consumption. + # TODO: Relationship described as log-normal here but power-law implemented! Dan's version of article described the + # TODO: the distributions of gross income and of liquid wealth as log-normal, not their relationship. Change paper! + # + # @param annualGrossTotalIncome Household + def getDesiredBankBalance(self, annualGrossTotalIncome: float) -> float: + return math.exp(self.config.DESIRED_BANK_BALANCE_ALPHA + self.config.DESIRED_BANK_BALANCE_BETA * math.log(annualGrossTotalIncome) + self.propensityToSave) + + #----- Owner-Occupier behaviour -----# + + # Desired purchase price used to decide whether to buy a house and how much to bid for it + # + # @param monthlyGrossEmploymentIncome Monthly gross employment income of the household + def getDesiredPurchasePrice(self, monthlyGrossEmploymentIncome: float) -> float: + # TODO: This product is generally so small that it barely has any impact on the results, need to rethink if + # TODO: it is necessary and if this small value makes any sense + HPAFactor: float = self.config.BUY_WEIGHT_HPA * self.getLongTermHPAExpectation() + # TODO: The capping of this factor intends to avoid negative and too large desired prices, the 0.9 is a + # TODO: purely artificial fudge parameter. This formula should be reviewed and changed! + if HPAFactor > 0.9: + HPAFactor = 0.9 + # TODO: Note that wealth is not used here, but only monthlyGrossEmploymentIncome + return self.config.BUY_SCALE * self.config.constants.MONTHS_IN_YEAR * monthlyGrossEmploymentIncome * math.exp(self.config.BUY_EPSILON * random.normalvariate(0, 1)) / (1.0 - HPAFactor) + + # Initial sale price of a house to be listed + # + # @param quality Quality of the house ot be sold + # @param principal Amount of principal left on any mortgage on this house + def getInitialSalePrice(self, quality: int, principal: float) -> float: + exponent = (self.config.SALE_MARKUP + + math.log(self.Model.housingMarketStats.getExpAvSalePriceForQuality(quality) + 1.0) - + self.config.SALE_WEIGHT_DAYS_ON_MARKET * math.log((self.Model.housingMarketStats.getExpAvDaysOnMarket() + 1.0) / (self.config.constants.DAYS_IN_MONTH + 1.0)) + + self.config.SALE_EPSILON * random.normalvariate(0, 1)) + # TODO: ExpAv days on market could be computed for each quality band so as to use here only the correct one + return max(math.exp(exponent), principal) + + # This method implements a household's decision to sell their owner-occupied property. On average, households sell + # owner-occupied houses every 11 years, due to exogenous reasons not addressed in the model. In order to prevent + # an unrealistic build-up of housing stock and unrealistic fluctuations of the interest rate, we modify this + # probability by introducing two extra factors, depending, respectively, on the number of houses per capita + # currently on the market and its exponential moving average, and on the interest rate and its exponential moving + # average. In this way, the long-term selling probability converges to 1/11. + # TODO: This method includes 2 unidentified fudge parameters, DECISION_TO_SELL_HPC (houses per capita) and + # TODO: DECISION_TO_SELL_INTEREST, which are explicitly explained otherwise in the manuscript. URGENT! + # TODO: Basically, need to implement both exponential moving averages referred above + # + # @return True if the owner-occupier decides to sell the house and false otherwise. + def decideToSellHome(self) -> bool: + # TODO: This if implies BTL agents never sell their homes, need to explain in paper! + return (not self.isPropertyInvestor()) and (random.random() < self.config.derivedParams.MONTHLY_P_SELL * ( + 1.0 + + self.config.DECISION_TO_SELL_ALPHA * (self.config.DECISION_TO_SELL_HPC - self.Model.houseSaleMarket.getnHousesOnMarket() / len(Model.households)) + + self.config.DECISION_TO_SELL_BETA * (self.config.DECISION_TO_SELL_INTEREST - self.Model.bank.getMortgageInterestRate()))) + + # Decide amount to pay as initial downpayment + # + # @param me the household + # @param housePrice the price of the house + def decideDownPayment(self, me: Household, housePrice: float) -> float: + if me.getBankBalance() > housePrice * self.config.BANK_BALANCE_FOR_CASH_DOWNPAYMENT: + return housePrice + + if me.isFirstTimeBuyer(): + # Since the function of the HPI is to move the down payments distribution upwards or downwards to + # accommodate current price levels, and the distribution is itself aggregate, we use the aggregate HPI + downpayment = self.Model.housingMarketStats.getHPI() * self.downpaymentDistFTB.inverseCumulativeProbability(max(0.0, (me.incomePercentile - self.config.DOWNPAYMENT_MIN_INCOME) / (1 - self.config.DOWNPAYMENT_MIN_INCOME))) + elif self.isPropertyInvestor(): + downpayment = housePrice * (max(0.0, self.config.DOWNPAYMENT_BTL_MEAN + self.config.DOWNPAYMENT_BTL_EPSILON * random.normalvariate(0, 1))) + else: + downpayment = self.Model.housingMarketStats.getHPI() * self.downpaymentDistOO.inverseCumulativeProbability(max(0.0, (me.incomePercentile - self.config.DOWNPAYMENT_MIN_INCOME) / (1 - self.config.DOWNPAYMENT_MIN_INCOME))) + + if downpayment > me.getBankBalance(): + downpayment = me.getBankBalance() + return downpayment + + #############################/ + ############/ REVISED ############/ + #############################/ + + #******************************************************* + # Decide how much to drop the list-price of a house if + # it has been on the market for (another) month and hasn't + # sold. Calibrated against Zoopla dataset in Bank of England + # + # @param sale The HouseOfferRecord of the house that is on the market. + #*******************************************************/ + def rethinkHouseSalePrice(self, sale: HouseOfferRecord) -> float: + if random.random() < self.config.P_SALE_PRICE_REDUCE: + logReduction = self.config.REDUCTION_MU + (random.normalvariate(0, 1) * self.config.REDUCTION_SIGMA) + return sale.getPrice() * (1.0 - math.exp(logReduction) / 100.0) + return sale.getPrice() + + ###############################################/ + # Renter behaviour + ###############################################/ + + #** renters or OO after selling home decide whether to rent or buy + # N.B. even though the HH may not decide to rent a house of the + # same quality as they would buy, the cash value of the difference in quality + # is assumed to be the difference in rental price between the two qualities. + # @return true if we should buy a house, false if we should rent + #/ + def decideRentOrPurchase(self, me: Household, purchasePrice: float) -> bool: + if self.isPropertyInvestor(): + return True + mortgageApproval = self.Model.bank.requestApproval(me, purchasePrice, self.decideDownPayment(me, purchasePrice), True) + newHouseQuality: int = self.Model.housingMarketStats.getMaxQualityForPrice(purchasePrice) + if newHouseQuality < 0: + return False # can't afford a house anyway + costOfHouse: float = mortgageApproval.monthlyPayment * self.config.constants.MONTHS_IN_YEAR - purchasePrice * self.getLongTermHPAExpectation() + costOfRent: float = self.Model.rentalMarketStats.getExpAvSalePriceForQuality(newHouseQuality) * self.config.constants.MONTHS_IN_YEAR + return random.random() < self.sigma(self.config.SENSITIVITY_RENT_OR_PURCHASE * (costOfRent * (1.0 + self.config.PSYCHOLOGICAL_COST_OF_RENTING) - costOfHouse)) + + #******************************************************* + # Decide how much to bid on the rental market + # Source: Zoopla rental prices 2008-2009 (at Bank of England) + #*******************************************************/ + def desiredRent(self, monthlyGrossEmploymentIncome: float) -> float: + return monthlyGrossEmploymentIncome * self.config.DESIRED_RENT_INCOME_FRACTION + + ###############################################/ + # Property investor behaviour + ###############################################/ + + #* + # Decide whether to sell or not an investment property. Investor households with only one investment property do + # never sell it. A sale is never attempted when the house is occupied by a tenant. Households with at least two + # investment properties will calculate the expected yield of the property in question based on two contributions: + # rental yield and capital gain (with their corresponding weights which depend on the type of investor) + # + # @param h The house in question + # @param me The investor household + # @return True if investor me decides to sell investment property h + #/ + def decideToSellInvestmentProperty(self, h: House, me: Household) -> bool: + # Fast decisions... + # ...always keep at least one investment property + if me.nInvestmentProperties() < 2: + return False + # ...don't sell while occupied by tenant + if not h.isOnRentalMarket(): + return False + + # Find the expected equity yield rate of this property as a weighted mix of both rental yield and capital gain + # times the leverage + # ...find the mortgage agreement for this property + mortgage: MortgageAgreement = me.mortgageFor(h) + # ...find its current (fair market value) sale price + currentMarketPrice: float = self.Model.housingMarketStats.getExpAvSalePriceForQuality(h.getQuality()) + # ...find equity, or assets minus liabilities + equity: float = max(0.01, currentMarketPrice - mortgage.principal) # The 0.01 prevents possible divisions by zero later on + # ...find the leverage on that mortgage (Assets divided by equity, or return on equity) + leverage: float = currentMarketPrice / equity + # ...find the expected rental yield of this property as its current rental price divided by its current (fair market value) sale price + # TODO: ATTENTION ---> This rental yield is not accounting for expected occupancy... shouldn't it? + currentRentalYield: float = h.getRentalRecord().getPrice() * self.config.constants.MONTHS_IN_YEAR / currentMarketPrice + # ...find the mortgage rate (pounds paid a year per pound of equity) + mortgageRate: float = mortgage.nextPayment() * self.config.constants.MONTHS_IN_YEAR / equity + # ...finally, find expected equity yield, or yield on equity + expectedEquityYield: float + if self.config.BTL_YIELD_SCALING: + expectedEquityYield = ( + leverage * ((1.0 - self.BTLCapGainCoefficient) * currentRentalYield + + self.BTLCapGainCoefficient * (self.Model.rentalMarketStats.getLongTermExpAvFlowYield() + + self.getLongTermHPAExpectation())) - + mortgageRate) + else: + expectedEquityYield = ( + leverage * ((1.0 - self.BTLCapGainCoefficient) * currentRentalYield + + self.BTLCapGainCoefficient * self.getLongTermHPAExpectation()) - + mortgageRate) + # Compute a probability to keep the property as a function of the effective yield + pKeep: float = pow(self.sigma(self.config.BTL_CHOICE_INTENSITY * expectedEquityYield), 1.0 / self.config.constants.MONTHS_IN_YEAR) + # Return true or false as a random draw from the computed probability + return random.random() < (1.0 - pKeep) + + # Decide whether to buy or not a new investment property. Investor households with no investment properties always + # attempt to buy one. If the household's bank balance is below its desired bank balance, then no attempt to buy is + # made. If the resources available to the household (maximum mortgage) are below the average price for the lowest + # quality houses, then no attempt to buy is made. Households with at least one investment property will calculate + # the expected yield of a new property based on two contributions: rental yield and capital gain (with their + # corresponding weights which depend on the type of investor) + # + # @param me The investor household + # @return True if investor me decides to try to buy a new investment property + def decideToBuyInvestmentProperty(self, me: Household) -> bool: + # Fast decisions... + # ...always decide to buy if owning no investment property yet + if me.nInvestmentProperties() < 1: + return True + # ...never buy (keep on saving) if bank balance is below the household's desired bank balance + # TODO: This mechanism and its parameter are not declared in the article! Any reference for the value of the parameter? + if me.getBankBalance() < self.getDesiredBankBalance(me.getAnnualGrossTotalIncome()) * self.config.BTL_CHOICE_MIN_BANK_BALANCE: + return False + # ...find maximum price (maximum mortgage) the household could pay + maxPrice: float = self.Model.bank.getMaxMortgage(me, False) + # ...never buy if that maximum price is below the average price for the lowest quality + if maxPrice < self.Model.housingMarketStats.getExpAvSalePriceForQuality(0): + return False + + # Find the expected equity yield rate for a hypothetical house maximising the leverage available to the + # household and assuming an average rental yield (over all qualities). This is found as a weighted mix of both + # rental yield and capital gain times the leverage + # ...find mortgage with maximum leverage by requesting maximum mortgage with minimum downpayment + mortgage: MortgageAgreement = self.Model.bank.requestApproval(me, maxPrice, 0.0, False) + # ...find equity, or assets minus liabilities (which, initially, is simply the downpayment) + equity: float = max(0.01, mortgage.downPayment) # The 0.01 prevents possible divisions by zero later on + # ...find the leverage on that mortgage (Assets divided by equity, or return on equity) + leverage: float = mortgage.purchasePrice / equity + # ...find the expected rental yield as an (exponential) average over all house qualities + rentalYield: float = self.Model.rentalMarketStats.getExpAvFlowYield() + # ...find the mortgage rate (pounds paid a year per pound of equity) + mortgageRate: float = mortgage.nextPayment() * self.config.constants.MONTHS_IN_YEAR / equity + # ...finally, find expected equity yield, or yield on equity + expectedEquityYield: float + if self.config.BTL_YIELD_SCALING: + expectedEquityYield = leverage * ( + (1.0 - self.BTLCapGainCoefficient) * rentalYield + + self.BTLCapGainCoefficient * (self.Model.rentalMarketStats.getLongTermExpAvFlowYield() + + self.getLongTermHPAExpectation())) - mortgageRate + else: + expectedEquityYield = leverage * ( + (1.0 - self.BTLCapGainCoefficient) * rentalYield + + self.BTLCapGainCoefficient * self.getLongTermHPAExpectation()) - mortgageRate + + # Compute the probability to decide to buy an investment property as a function of the expected equity yield + # TODO: This probability has been changed to correctly reflect the conversion from annual to monthly + # TODO: probability. This needs to be explained in the article + # double pBuy = Math.pow(sigma(config.BTL_CHOICE_INTENSITY*expectedEquityYield), + # 1.0/config.constants.MONTHS_IN_YEAR) + pBuy: float = 1.0 - pow((1.0 - self.sigma(self.config.BTL_CHOICE_INTENSITY * expectedEquityYield)), + 1.0 / self.config.constants.MONTHS_IN_YEAR) + # Return true or false as a random draw from the computed probability + return random.random() < pBuy + + def btlPurchaseBid(self, me: Household) -> float: + # TODO: What is this 1.1 factor? Another fudge parameter? It prevents wealthy investors from offering more than + # TODO: 10% above the average price of top quality houses. The effect of this is to prevent fast increases of + # TODO: price as BTL investors buy all supply till prices are too high for everybody. Fairly unclear mechanism, + # TODO: check for removal! + return (min(self.Model.bank.getMaxMortgage(me, False), + 1.1 * self.Model.housingMarketStats.getExpAvSalePriceForQuality(self.config.N_QUALITY - 1))) + + # How much rent does an investor decide to charge on a buy-to-let house? + # @param rbar exponential average rent for house of this quality + # @param d average days on market + # @param h house being offered for rent + def buyToLetRent(self, rbar: float, d: float, h: House) -> float: + # TODO: What? Where does this equation come from? + beta: float = self.config.RENT_MARKUP / math.log(self.config.RENT_EQ_MONTHS_ON_MARKET) # Weight of days-on-market effect + + exponent: float = ( + self.config.RENT_MARKUP + math.log(rbar + 1.0) - + beta * math.log((d + 1.0) / (self.config.constants.DAYS_IN_MONTH + 1)) + + self.config.RENT_EPSILON * random.normalvariate(0, 1) + ) + result: float = math.exp(exponent) + # TODO: The following contains a fudge (config.RENT_MAX_AMORTIZATION_PERIOD) to keep rental yield up + minAcceptable: float = self.Model.housingMarketStats.getExpAvSalePriceForQuality(h.getQuality()) / (self.config.RENT_MAX_AMORTIZATION_PERIOD * self.config.constants.MONTHS_IN_YEAR) + if result < minAcceptable: + result = minAcceptable + return result + + # Update the demanded rent for a property + # + # @param sale the HouseOfferRecord of the property for rent + # @return the new rent + def rethinkBuyToLetRent(self, sale: HouseOfferRecord) -> float: + return (1.0 - self.config.RENT_REDUCTION) * sale.getPrice() + + # Logistic function, sometimes called sigma function, 1/1+e^(-x) + # + # @param x Parameter of the sigma or logistic function + def sigma(self, x: float) -> float: + return 1.0 / (1.0 + math.exp(-x)) + + # @return expectation value of HPI in one year's time divided by today's HPI + def getLongTermHPAExpectation(self) -> float: + # Dampening or multiplier factor, depending on its value being <1 or >1, for the current trend of HPA when + # computing expectations as in HPI(t+DT) = HPI(t) + FACTOR*DT*dHPI/dt (double) + return self.Model.housingMarketStats.getLongTermHPA() * self.config.HPA_EXPECTATION_FACTOR + + def getBTLCapGainCoefficient(self) -> float: + return self.BTLCapGainCoefficient + + def isPropertyInvestor(self) -> bool: + return self.BTLInvestor diff --git a/src/main/java/housing/Model.java b/src/main/java/housing/Model.java deleted file mode 100644 index 9c07a70c..00000000 --- a/src/main/java/housing/Model.java +++ /dev/null @@ -1,332 +0,0 @@ -package housing; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Scanner; -import java.time.Instant; - -import collectors.*; - -import org.apache.commons.math3.random.MersenneTwister; -import org.apache.commons.cli.*; -import org.apache.commons.io.FileUtils; - -/************************************************************************************************** - * This is the root object of the simulation. Upon creation it creates and initialises all the - * agents in the model. - * - * The project is prepared to be run with maven, and it takes the following command line input - * arguments: - * - * -configFile Configuration file to be used (address within project folder). By default, - * 'src/main/resources/config.properties' is used. - * -outputFolder Folder in which to collect all results (address within project folder). By - * default, 'Results//' is used. The folder will be - * created if it does not exist. - * -dev Removes security question before erasing the content inside output folder - * (if the folder already exists). - * -help Print input arguments usage information. - * - * Note that the seed for random number generation is set from the config file. - * - * @author daniel, Adrian Carro - * - *************************************************************************************************/ - -public class Model { - - //------------------// - //----- Fields -----// - //------------------// - - public static Config config; - public static Construction construction; - public static CentralBank centralBank; - public static Bank bank; - public static HouseSaleMarket houseSaleMarket; - public static HouseRentalMarket houseRentalMarket; - public static ArrayList households; - public static CreditSupply creditSupply; - public static CoreIndicators coreIndicators; - public static HouseholdStats householdStats; - public static HousingMarketStats housingMarketStats; - public static RentalMarketStats rentalMarketStats; - public static MicroDataRecorder transactionRecorder; - public static int nSimulation; // To keep track of the simulation number - public static int t; // To keep track of time (in months) - - static Government government; - - private static MersenneTwister prng; - private static Demographics demographics; - private static Recorder recorder; - private static String configFileName; - private static String outputFolder; - - //------------------------// - //----- Constructors -----// - //------------------------// - - /** - * @param configFileName String with the address of the configuration file - * @param outputFolder String with the address of the folder for storing results - */ - public Model(String configFileName, String outputFolder) { - config = new Config(configFileName); - prng = new MersenneTwister(config.SEED); - - government = new Government(); - demographics = new Demographics(prng); - construction = new Construction(prng); - centralBank = new CentralBank(); - bank = new Bank(); - households = new ArrayList<>(config.TARGET_POPULATION*2); - houseSaleMarket = new HouseSaleMarket(prng); - houseRentalMarket = new HouseRentalMarket(prng); - - recorder = new collectors.Recorder(outputFolder); - transactionRecorder = new collectors.MicroDataRecorder(outputFolder); - creditSupply = new collectors.CreditSupply(outputFolder); - coreIndicators = new collectors.CoreIndicators(); - householdStats = new collectors.HouseholdStats(); - housingMarketStats = new collectors.HousingMarketStats(houseSaleMarket); - rentalMarketStats = new collectors.RentalMarketStats(housingMarketStats, houseRentalMarket); - - nSimulation = 0; - } - - //-------------------// - //----- Methods -----// - //-------------------// - - public static void main(String[] args) { - - // Handle input arguments from command line - handleInputArguments(args); - - // Create an instance of Model in order to initialise it (reading config file) - new Model(configFileName, outputFolder); - - // Start data recorders for output - setupStatics(); - - // Open files for writing multiple runs results - recorder.openMultiRunFiles(config.recordCoreIndicators); - - // Perform config.N_SIMS simulations - for (nSimulation = 1; nSimulation <= config.N_SIMS; nSimulation += 1) { - - // For each simulation, open files for writing single-run results - recorder.openSingleRunFiles(nSimulation); - - // For each simulation, initialise both houseSaleMarket and houseRentalMarket variables (including HPI) - init(); - - // For each simulation, run config.N_STEPS time steps - for (t = 0; t <= config.N_STEPS; t += 1) { - - // Steps model and stores sale and rental markets bid and offer prices, and their averages, into their - // respective variables - modelStep(); - -// if (t >= config.TIME_TO_START_RECORDING) { - // Write results of this time step and run to both multi- and single-run files - recorder.writeTimeStampResults(config.recordCoreIndicators, t); -// } - - // Print time information to screen - if (t % 100 == 0) { - System.out.println("Simulation: " + nSimulation + ", time: " + t); - } - } - - // Finish each simulation within the recorders (closing single-run files, changing line in multi-run files) - recorder.finishRun(config.recordCoreIndicators); - // TODO: Check what this is actually doing and if it is necessary - if(config.recordMicroData) transactionRecorder.endOfSim(); - } - - // After the last simulation, clean up - recorder.finish(config.recordCoreIndicators); - if(config.recordMicroData) transactionRecorder.finish(); - - //Stop the program when finished - System.exit(0); - } - - private static void setupStatics() { - setRecordGeneral(); - setRecordCoreIndicators(config.recordCoreIndicators); - setRecordMicroData(config.recordMicroData); - } - - private static void init() { - construction.init(); - houseSaleMarket.init(); - houseRentalMarket.init(); - bank.init(); - centralBank.init(); - housingMarketStats.init(); - rentalMarketStats.init(); - householdStats.init(); - households.clear(); - } - - private static void modelStep() { - // Update population with births and deaths - demographics.step(); - // Update number of houses - construction.step(); - // Updates regional households consumption, housing decisions, and corresponding regional bids and offers - for(Household h : households) h.step(); - // Stores sale market bid and offer prices and averages before bids are matched by clearing the market - housingMarketStats.preClearingRecord(); - // Clears sale market and updates the HPI - houseSaleMarket.clearMarket(); - // Computes and stores several housing market statistics after bids are matched by clearing the market (such as HPI, HPA) - housingMarketStats.postClearingRecord(); - // Stores rental market bid and offer prices and averages before bids are matched by clearing the market - rentalMarketStats.preClearingRecord(); - // Clears rental market - houseRentalMarket.clearMarket(); - // Computes and stores several rental market statistics after bids are matched by clearing the market (such as HPI, HPA) - rentalMarketStats.postClearingRecord(); - // Stores household statistics after both regional markets have been cleared - householdStats.record(); - // Update credit supply statistics // TODO: Check what this actually does and if it should go elsewhere! - creditSupply.step(); - // Update bank and interest rate for new mortgages - bank.step(Model.households.size()); - // Update central bank policies (currently empty!) - centralBank.step(coreIndicators); - } - - /** - * This method handles command line input arguments to - * determine the address of the input config file and - * the folder for outputs - * - * @param args String with the command line arguments - */ - private static void handleInputArguments(String[] args) { - - // Create Options object - Options options = new Options(); - - // Add configFile and outputFolder options - options.addOption("configFile", true, "Configuration file to be used (address within " + - "project folder). By default, 'src/main/resources/config.properties' is used."); - options.addOption("outputFolder", true, "Folder in which to collect all results " + - "(address within project folder). By default, 'Results//' is used. The " + - "folder will be created if it does not exist."); - options.addOption("dev", false, "Removes security question before erasing the content" + - "inside output folder (if the folder already exists)."); - options.addOption("help", false, "Print input arguments usage information."); - - // Create help formatter in case it will be needed - HelpFormatter formatter = new HelpFormatter(); - - // Parse command line arguments and perform appropriate actions - // Create a parser and a boolean variable for later control - CommandLineParser parser = new DefaultParser(); - boolean devBoolean = false; - try { - // Parse command line arguments into a CommandLine instance - CommandLine cmd = parser.parse(options, args); - // Check if help argument has been passed - if(cmd.hasOption("help")) { - // If it has, then print formatted help to screen and stop program - formatter.printHelp( "spatial-housing-model", options ); - System.exit(0); - } - // Check if dev argument has been passed - if(cmd.hasOption("dev")) { - // If it has, then activate boolean variable for later control - devBoolean = true; - } - // Check if configFile argument has been passed - if(cmd.hasOption("configFile")) { - // If it has, then use its value to initialise the respective member variable - configFileName = cmd.getOptionValue("configFile"); - } else { - // If not, use the default value to initialise the respective member variable - configFileName = "src/main/resources/config.properties"; - } - // Check if outputFolder argument has been passed - if(cmd.hasOption("outputFolder")) { - // If it has, then use its value to initialise the respective member variable - outputFolder = cmd.getOptionValue("outputFolder"); - // If outputFolder does not end with "/", add it - if (!outputFolder.endsWith("/")) { outputFolder += "/"; } - } else { - // If not, use the default value to initialise the respective member variable - outputFolder = "Results/" + Instant.now().toString().replace(":", "-") + "/"; - } - } - catch(ParseException pex) { - // Catch possible parsing errors - System.err.println("Parsing failed. Reason: " + pex.getMessage()); - // And print input arguments usage information - formatter.printHelp( "spatial-housing-model", options ); - } - - // Check if outputFolder directory already exists - File f = new File(outputFolder); - if (f.exists() && !devBoolean) { - // If it does, try removing everything inside (with a warning that requests approval!) - Scanner reader = new Scanner(System.in); - System.out.println("\nATTENTION:\n\nThe folder chosen for output, '" + outputFolder + "', already exists and " + - "might contain relevant files.\nDo you still want to proceed and erase all content?"); - String reply = reader.next(); - if (!reply.equalsIgnoreCase("yes") && !reply.equalsIgnoreCase("y")) { - // If user does not clearly reply "yes", then stop the program - System.exit(0); - } else { - // Otherwise, try to erase everything inside the folder - try { - FileUtils.cleanDirectory(f); - } catch (IOException ioe) { - // Catch possible folder cleaning errors - System.err.println("Folder cleaning failed. Reason: " + ioe.getMessage()); - } - } - } else { - // If it doesn't, simply create it - f.mkdirs(); - } - - // Copy config file to output folder - try { - FileUtils.copyFileToDirectory(new File(configFileName), new File(outputFolder)); - } catch (IOException ioe) { - System.err.println("Copying config file to output folder failed. Reason: " + ioe.getMessage()); - } - } - - /** - * @return Simulated time in months - */ - static public int getTime() { return t; } - - /** - * @return Current month of the simulation - */ - static public int getMonth() { return t%12 + 1; } - - public MersenneTwister getPrng() { return prng; } - - private static void setRecordGeneral() { - creditSupply.setActive(true); - householdStats.setActive(true); - housingMarketStats.setActive(true); - rentalMarketStats.setActive(true); - } - - private static void setRecordCoreIndicators(boolean recordCoreIndicators) { - coreIndicators.setActive(recordCoreIndicators); - } - - private static void setRecordMicroData(boolean record) { transactionRecorder.setActive(record); } - -} diff --git a/src/main/java/housing/Model.py b/src/main/java/housing/Model.py new file mode 100644 index 00000000..77d5f50b --- /dev/null +++ b/src/main/java/housing/Model.py @@ -0,0 +1,214 @@ +import os +import shutil +from time import gmtime, strftime +from typing import List + +import collectors + +#************************************************************************************************** +#* This is the root object of the simulation. Upon creation it creates and initialises all the +#* agents in the model. +#* +#* The project is prepared to be run with maven, and it takes the following command line input +#* arguments: +#* +#* -configFile Configuration file to be used (address within project folder). By default, +#* 'src/main/resources/config.properties' is used. +#* -outputFolder Folder in which to collect all results (address within project folder). By +#* default, 'Results//' is used. The folder will be +#* created if it does not exist. +#* -dev Removes security question before erasing the content inside output folder +#* (if the folder already exists). +#* -help Print input arguments usage information. +#* +#* Note that the seed for random number generation is set from the config file. +#* +#* @author daniel, Adrian Carro +#* +#*************************************************************************************************/ + +class Model: + + #------------------------# + #----- Constructors -----# + #------------------------# + + #** + #* @param configFileName String with the address of the configuration file + #* @param outputFolder String with the address of the folder for storing results + #*/ + def __init__(self, configFileName: str, outputFolder: str): + self.config = Config(configFileName) + self.prng = MersenneTwister(config.SEED) + + self.government = Government() + self.demographics = Demographics(self.prng) + self.construction = Construction(self.prng) + self.centralBank = CentralBank() + self.bank = Bank() + self.households: List[Household] = [] + self.houseSaleMarket = HouseSaleMarket(self.prng) + self.houseRentalMarket = HouseRentalMarket(self.prng) + + self.recorder = collectors.Recorder(outputFolder) + self.transactionRecorder = collectors.MicroDataRecorder(outputFolder) + self.creditSupply = collectors.CreditSupply(outputFolder) + self.coreIndicators = collectors.CoreIndicators() + self.householdStats = collectors.HouseholdStats() + self.housingMarketStats = collectors.HousingMarketStats(self.houseSaleMarket) + self.rentalMarketStats = collectors.RentalMarketStats(self.housingMarketStats, self.houseRentalMarket) + # To keep track of the simulation number + self.nSimulation = 0 + # To keep track of time (in months) + self.t = 0 + + #private static MersenneTwister prng + #private static Demographics demographics + #private static Recorder recorder + #private static String configFileName + #private static String outputFolder + + #-------------------# + #----- Methods -----# + #-------------------# + def main(self) -> None: + + # Handle input arguments from command line + self.handleInputArguments() + + # Create an instance of Model in order to initialise it (reading config file) + model = Model(configFileName, outputFolder) + + # Start data recorders for output + self.setupStatics() + + # Open files for writing multiple runs results + self.recorder.openMultiRunFiles(config.recordCoreIndicators) + + # Perform config.N_SIMS simulations + for nSimulation in range(1, self.config.N_SIMS + 1): + # For each simulation, open files for writing single-run results + self.recorder.openSingleRunFiles(nSimulation) + + # For each simulation, initialise both houseSaleMarket and houseRentalMarket variables (including HPI) + self.init() + + # For each simulation, run config.N_STEPS time steps + for t in range(config.N_STEPS): + # Steps model and stores sale and rental markets bid and offer prices, and their averages, into their + # respective variables + self.modelStep() + + # Write results of this time step and run to both multi- and single-run files + self.recorder.writeTimeStampResults(self.config.recordCoreIndicators, t) + + # Print time information to screen + if t % 100 == 0: + print("Simulation: " + str(nSimulation) + ", time: " + str(t)) + + # Finish each simulation within the recorders (closing single-run files, changing line in multi-run files) + self.recorder.finishRun(self.config.recordCoreIndicators) + # TODO: Check what this is actually doing and if it is necessary + if config.recordMicroData: + self.transactionRecorder.endOfSim() + + # After the last simulation, clean up + self.recorder.finish(config.recordCoreIndicators) + if self.config.recordMicroData: + self.transactionRecorder.finish() + + #Stop the program when finished + exit() + + def setupStatics(self) -> None: + self.setRecordGeneral() + self.setRecordCoreIndicators(self.config.recordCoreIndicators) + self.setRecordMicroData(self.config.recordMicroData) + + def init(self) -> None: + self.construction.init() + self.houseSaleMarket.init() + self.houseRentalMarket.init() + self.bank.init() + self.centralBank.init() + self.housingMarketStats.init() + self.rentalMarketStats.init() + self.householdStats.init() + self.households.clear() + + def modelStep(self) -> None: + # Update population with births and deaths + self.demographics.step() + # Update number of houses + self.construction.step() + # Updates regional households consumption, housing decisions, and corresponding regional bids and offers + for h in self.households: + h.step() + # Stores sale market bid and offer prices and averages before bids are matched by clearing the market + self.housingMarketStats.preClearingRecord() + # Clears sale market and updates the HPI + self.houseSaleMarket.clearMarket() + # Computes and stores several housing market statistics after bids are matched by clearing the market (such as HPI, HPA) + self.housingMarketStats.postClearingRecord() + # Stores rental market bid and offer prices and averages before bids are matched by clearing the market + self.rentalMarketStats.preClearingRecord() + # Clears rental market + self.houseRentalMarket.clearMarket() + # Computes and stores several rental market statistics after bids are matched by clearing the market (such as HPI, HPA) + self.rentalMarketStats.postClearingRecord() + # Stores household statistics after both regional markets have been cleared + self.householdStats.record() + # Update credit supply statistics # TODO: Check what this actually does and if it should go elsewhere! + self.creditSupply.step() + # Update bank and interest rate for new mortgages + self.bank.step(Model.households.size()) + # Update central bank policies (currently empty!) + self.centralBank.step(self.coreIndicators) + + # This method handles command line input arguments to + # determine the address of the input config file and + # the folder for outputs + # + # @param args String with the command line arguments + def handleInputArguments(self) -> None: + # "Configuration file to be used (address within project folder). + configFile = "src/main/resources/config.properties" + # Folder in which to collect all results (address within project + # folder). By default, 'Results//' is used. The + # folder will be created if it does not exist. + outputFolder = "Results/" + strftime("%Y-%m-%d-%H-%M-%S", gmtime()) + "/" + # Removes security question before erasing the content inside output + # folder (if the folder already exists). + devBoolean = False + + # Check if outputFolder directory already exists + os.makedirs(outputFolder, exist_ok=True) + # TODO remove existing files + #print("\nATTENTION:\n\nThe folder chosen for output, '" + outputFolder + "', already exists and " + + # "might contain relevant files.\nDo you still want to proceed and erase all content?") + + # Copy config file to output folder + shutil.copyfile(configFile, outputFolder + 'config.properties') + + # @return Simulated time in months + def getTime(self) -> int: + return t + + # @return Current month of the simulation + def getMonth(self) -> int: + return t % 12 + 1 + + def getPrng(self): + return self.prng + + def setRecordGeneral(self) -> None: + self.creditSupply.setActive(true) + self.householdStats.setActive(true) + self.housingMarketStats.setActive(true) + self.rentalMarketStats.setActive(true) + + def setRecordCoreIndicators(recordCoreIndicators: bool) -> None: + self.coreIndicators.setActive(recordCoreIndicators) + + def setRecordMicroData(record: bool) -> None: + self.transactionRecorder.setActive(record) diff --git a/src/main/java/utilities/BinnedData.java b/src/main/java/utilities/BinnedData.java deleted file mode 100644 index 6c656b42..00000000 --- a/src/main/java/utilities/BinnedData.java +++ /dev/null @@ -1,39 +0,0 @@ -package utilities; - -import java.util.ArrayList; - -public class BinnedData extends ArrayList { - - public BinnedData(double firstBinMin, double binWidth) { - this.firstBinMin = firstBinMin; - this.binWidth = binWidth; - } - - public double getSupportLowerBound() { - return(firstBinMin); - } - - public double getSupportUpperBound() { - return(firstBinMin + size()*binWidth); - } - - public double getBinWidth() { - return(binWidth); - } - - - public DATA getBinAt(double val) { - return(get((int)((val-firstBinMin)/binWidth))); - } - - public void setBinWidth(double width) { - binWidth = width; - } - - public void setFirstBinMin(double min) { - firstBinMin = min; - } - - public double firstBinMin; - public double binWidth; -} diff --git a/src/main/java/utilities/BinnedData.py b/src/main/java/utilities/BinnedData.py new file mode 100644 index 00000000..96370203 --- /dev/null +++ b/src/main/java/utilities/BinnedData.py @@ -0,0 +1,24 @@ +from typing import Any + +class BinnedData(list): + def __init__(self, firstBinMin: float, binWidth: float): + self.firstBinMin = firstBinMin + self.binWidth = binWidth + + def getSupportLowerBound(self) -> float: + return self.firstBinMin + + def getSupportUpperBound(self) -> float: + return self.firstBinMin + len(self) * self.binWidth + + def getBinWidth(self) -> float: + return self.binWidth + + def getBinAt(self, val: float) -> Any: + return self[int((val - self.firstBinMin) / self.binWidth)] + + def setBinWidth(self, width: float) -> None: + self.binWidth = width + + def setFirstBinMin(self, _min: float) -> None: + self.firstBinMin = _min diff --git a/src/main/java/utilities/BinnedDataDouble.java b/src/main/java/utilities/BinnedDataDouble.java deleted file mode 100644 index 1ecbbfa1..00000000 --- a/src/main/java/utilities/BinnedDataDouble.java +++ /dev/null @@ -1,70 +0,0 @@ -package utilities; - -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.util.Iterator; - -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVRecord; - -/** - * Utility class to that expands BinnedData with a constructor that reads data from a source file - * - * @author daniel, Adrian Carro - */ -public class BinnedDataDouble extends BinnedData { - - /** - * Loads data from a .csv file. The file should be in the format {bin min, min max, value}, with as many initial - * rows as needed for comments but always marked with an initial "#" character - * - * @param filename Address of the file to read data from - */ - public BinnedDataDouble(String filename) { - super(0.0,0.0); - try { - // Open file and buffered readers - FileReader in = new FileReader(filename); - BufferedReader buffReader = new BufferedReader(in); - // Skip initial comment lines keeping mark of previous position to return to if line is not comment - buffReader.mark(1000); // 1000 is just the number of characters that can be read while preserving the mark - String line = buffReader.readLine(); - while (line.charAt(0) == '#') { - buffReader.mark(1000); - line = buffReader.readLine(); - } - buffReader.reset(); // Return to previous position (before reading the first line that was not a comment) - // Pass advanced buffered reader to CSVFormat parser - Iterator records = CSVFormat.EXCEL.parse(buffReader).iterator(); - CSVRecord record; - // Read through records - if(records.hasNext()) { - record = records.next(); - // Use the first record to set the first bin minimum and the bin width... - this.setFirstBinMin(Double.valueOf(record.get(0))); - this.setBinWidth(Double.valueOf(record.get(1))-firstBinMin); - // ...before actually adding it to the array - add(Double.valueOf(record.get(2))); - while(records.hasNext()) { - record = records.next(); - // Next records are just added to the array - add(Double.valueOf(record.get(2))); - } - } - } catch (IOException e) { - System.out.println("Problem while loading data from " + filename - + " for creating a BinnedDataDouble object"); - e.printStackTrace(); - } - } - - /** - * This constructor creates a BinnedDataDouble object with a given first bin minimum and a given bin width, but - * without reading any data. Thus, data is to be added manually via the add method of the ArrayList - * - * @param firstBinMin First bin minimum - * @param binWidth Bin width - */ - public BinnedDataDouble(double firstBinMin, double binWidth) { super(firstBinMin, binWidth); } -} diff --git a/src/main/java/utilities/BinnedDataDouble.py b/src/main/java/utilities/BinnedDataDouble.py new file mode 100644 index 00000000..2084bd0f --- /dev/null +++ b/src/main/java/utilities/BinnedDataDouble.py @@ -0,0 +1,60 @@ +from typing import Optional + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVRecord; + +#** +#* Utility class to that expands BinnedData with a constructor that reads data from a source file +#* +#* @author daniel, Adrian Carro +#*/ +class BinnedDataDouble(BinnedData): + #** + #* Loads data from a .csv file. The file should be in the format {bin min, min max, value}, with as many initial + #* rows as needed for comments but always marked with an initial "#" character + #* + #* @param filename Address of the file to read data from + #*/ + def __init__(self, + filename: Optional[str] = None, + firstBinMin: Optional[float] = None, + binWidth: Optional[float] = None): + if firstBinMin is not None: + # This constructor creates a BinnedDataDouble object with a given first bin minimum and a given bin width, but + # without reading any data. Thus, data is to be added manually via the add method of the ArrayList + # + # @param firstBinMin First bin minimum + # @param binWidth Bin width + super().__init__(firstBinMin, binWidth) + return + + super().__init__(0.0,0.0) + try: + # Open file and buffered readers + FileReader in = new FileReader(filename); + BufferedReader buffReader = new BufferedReader(in); + # Skip initial comment lines keeping mark of previous position to return to if line is not comment + buffReader.mark(1000); # 1000 is just the number of characters that can be read while preserving the mark + String line = buffReader.readLine(); + while (line.charAt(0) == '#'): + buffReader.mark(1000); + line = buffReader.readLine(); + buffReader.reset(); # Return to previous position (before reading the first line that was not a comment) + # Pass advanced buffered reader to CSVFormat parser + Iterator records = CSVFormat.EXCEL.parse(buffReader).iterator(); + CSVRecord record; + # Read through records + if(records.hasNext()): + record = records.next(); + # Use the first record to set the first bin minimum and the bin width... + this.setFirstBinMin(Double.valueOf(record.get(0))); + this.setBinWidth(Double.valueOf(record.get(1))-firstBinMin); + # ...before actually adding it to the array + add(Double.valueOf(record.get(2))); + while(records.hasNext()): + record = records.next(); + # Next records are just added to the array + add(Double.valueOf(record.get(2))); + except Exception as e: + print("Problem while loading data from " + filename + " for creating a BinnedDataDouble object"); + print(e) diff --git a/src/main/java/utilities/DoubleUnaryOperator.java b/src/main/java/utilities/DoubleUnaryOperator.java deleted file mode 100644 index c8bb8b4a..00000000 --- a/src/main/java/utilities/DoubleUnaryOperator.java +++ /dev/null @@ -1,98 +0,0 @@ -package utilities; - - -/**** - * Need to include this for compatibility with Java 1.7 - */ - - /* - * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -// Paragraph commented to avoid error in Javadoc references because of a lacking UnaryOperator -// /** -// * Represents an operation on a single {@code double}-valued operand that produces -// * a {@code double}-valued result. This is the primitive type specialization of -// * {@link UnaryOperator} for {@code double}. -// * -// *

This is a functional interface -// * whose functional method is {@link #applyAsDouble(double)}. -// * -// * @see UnaryOperator -// * @since 1.8 -// */ - public interface DoubleUnaryOperator { - - /** - * Applies this operator to the given operand. - * - * @param operand the operand - * @return the operator result - */ - double applyAsDouble(double operand); - - /** - * Returns a composed operator that first applies the {@code before} - * operator to its input, and then applies this operator to the result. - * If evaluation of either operator throws an exception, it is relayed to - * the caller of the composed operator. - * - * @param before the operator to apply before this operator is applied - * @return a composed operator that first applies the {@code before} - * operator and then applies this operator - * @throws NullPointerException if before is null - * - * @see #andThen(DoubleUnaryOperator) - */ -// default DoubleUnaryOperator compose(DoubleUnaryOperator before) { -// return (double v) -> applyAsDouble(before.applyAsDouble(v)); -// } - - /** - * Returns a composed operator that first applies this operator to - * its input, and then applies the {@code after} operator to the result. - * If evaluation of either operator throws an exception, it is relayed to - * the caller of the composed operator. - * - * @param after the operator to apply after this operator is applied - * @return a composed operator that first applies this operator and then - * applies the {@code after} operator - * @throws NullPointerException if after is null - * - * @see #compose(DoubleUnaryOperator) - */ -// default DoubleUnaryOperator andThen(DoubleUnaryOperator after) { -// return (double t) -> after.applyAsDouble(applyAsDouble(t)); -// } - - /** - * Returns a unary operator that always returns its input argument. - * - * @return a unary operator that always returns its input argument - */ -// static DoubleUnaryOperator identity() { -// return t -> t; -// } - } - diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties deleted file mode 100755 index a8b454be..00000000 --- a/src/main/resources/config.properties +++ /dev/null @@ -1,307 +0,0 @@ -# Configuration file where all parameter values are set -# All parameters in this file must be declared in the config.properties file in order to be recognised and correctly read - -################################################## -################ General comments ################ -################################################## - -# Seed for random number generation is set by calling the program with argument -# "-seed ", where must be a positive integer. In the -# absence of this argument, seed is set from machine time - -################################################## -######## General model control parameters ######## -################################################## - -# Seed for random number generator (int) -SEED = 1 -# Simulation duration in time steps (int) -N_STEPS = 6000 -# Time steps before recording statistics, initialisation time (int) -TIME_TO_START_RECORDING = 0 -# Number of simulations to run (int) -N_SIMS = 1 -# True to write time series for each core indicator (boolean) -recordCoreIndicators = false -# True to write micro data for each transaction made (boolean) -recordMicroData = false - -################################################## -################ House parameters ################ -################################################## - -# Number of quality bands for houses (int) -N_QUALITY = 48 - -################################################## -########### Housing market parameters ############ -################################################## - -# Time, in days, that a house remains under offer (int) -DAYS_UNDER_OFFER = 7 -# Smallest proportional increase in price that can cause a gazump (double) -BIDUP = 1.0075 -# Decay constant for the exponential moving average of sale prices (double) -MARKET_AVERAGE_PRICE_DECAY = 0.25 -# Initial housing price index, HPI (double) -# TODO: Reference for this value and justification for this discounting are needed! (the parameter is declared, but no source nor justification is given) -INITIAL_HPI = 0.8 -# Median house price (double) -# TODO: Replace by the 2011 value -HPI_MEDIAN = 195000.0 -# Shape parameter for the log-normal distribution of housing prices, taken from the ONS (2013) house price index data tables, table 34 (double) -# TODO: Replace by the 2011 value, compare with Land Registry Paid Price data and decide whether to use real distribution -HPI_SHAPE = 0.555 -# Profit margin for buy-to-let investors (double) -# Yield on rent had average 6% between 2009/01 and 2015/01, minimum in 2009/10 maximum in 2012/04 peak-to-peak amplitude of 0.4%. Source: Bank of England, unpublished analysis based on Zoopla/Land Registry matching (Philippe Bracke) -RENT_GROSS_YIELD = 0.05 - -################################################## -############# Demographic parameters ############# -################################################## - -# Target number of households (int) -TARGET_POPULATION = 10000 -# Future birth rate (births per year per capita), calibrated with flux of FTBs, Council of Mortgage Lenders Regulated Mortgage Survey, 2015 (double) -# TODO: Also described as "calibrated against average advances to first time buyers, core indicators 1987-2006". Check which explanation holds and replace by the 2011 value. -FUTURE_BIRTH_RATE = 0.018 - -################################################## -############## Household parameters ############## -################################################## - -# Monthly percentage growth of financial investments (double) -RETURN_ON_FINANCIAL_WEALTH = 0.002 -# Average number of months a tenant will stay in a rented house (int) -# Source: ARLA - Members survey of the Private Rented Sector Q4 2013 -TENANCY_LENGTH_AVERAGE = 18 -# Standard deviation of the noise in determining the tenancy length (int) -TENANCY_LENGTH_EPSILON = 6 - -################################################## -######### Household behaviour parameters ######### -################################################## - -############# Buy-To-Let parameters ############## -# Prior probability of being (wanting to be) a BTL investor (double) -# TODO: Shouldn't this be 4% according to the article? -P_INVESTOR = 0.16 -# Minimum income percentile for a household to be a BTL investor (double) -MIN_INVESTOR_PERCENTILE = 0.5 -# Weight that fundamentalists put on cap gain (double) -FUNDAMENTALIST_CAP_GAIN_COEFF = 0.5 -# Weight that trend-followers put on cap gain (double) -TREND_CAP_GAIN_COEFF = 0.9 -# Probability that a BTL investor is a fundamentalist versus a trend-follower (double) -P_FUNDAMENTALIST = 0.5 -# Chooses between two possible equations for BTL investors to make their buy/sell decisions (boolean) -BTL_YIELD_SCALING = false - -################ Rent parameters ################# -# Desired proportion of income to be spent on rent (double) -DESIRED_RENT_INCOME_FRACTION = 0.33 -# Annual psychological cost of renting (double) -# TODO: This value comes from 1.1/12.0... Where does that come from? -PSYCHOLOGICAL_COST_OF_RENTING = 0.0916666666667 -# Sensitivity parameter of the decision between buying and renting (double) -# TODO: This value comes from 1.0/3500.0... Where does that come from? -SENSITIVITY_RENT_OR_PURCHASE = 0.000285714285714 - -############### General parameters ############### -# If the ratio between the buyer's bank balance and the house price is above this, -# payment will be made fully in cash (double) -# Calibrated against mortgage approval/housing transaction ratio, core indicators average 1987-2006 -# TODO: Find these sources and clarify this calibration! -BANK_BALANCE_FOR_CASH_DOWNPAYMENT = 2.0 -# Dampening or multiplier factor, depending on its value being <1 or >1, for the current trend when computing expectations as in -# HPI(t+DT) = HPI(t) + FACTOR*DT*dHPI/dt (double) -# TODO: According to John Muellbauer, this is a dampening factor (<1). Find a reference for this! -HPA_EXPECTATION_FACTOR = 0.5 -# Number of years of the HPI record to check when computing the annual HPA, i.e., how much backward looking households are (int) -HPA_YEARS_TO_CHECK = 1 -# Average period, in years, for which owner-occupiers hold their houses (double) -# British housing survey 2008 -HOLD_PERIOD = 11.0 - -######### Sale price reduction parameters ######## -# This subsection was calibrated against Zoopla data at the BoE -# Monthly probability of reducing the price of a house on the market (double) -# This value comes from 1.0-0.945 -P_SALE_PRICE_REDUCE = 0.055 -# Mean percentage reduction for prices of houses on the market (double) -REDUCTION_MU = 1.603 -# Standard deviation of percentage reductions for prices of houses on the market (double) -REDUCTION_SIGMA = 0.617 - -############# Comsumption parameters ############# -# Fraction of the monthly budget allocated for consumption, being the monthly -# budget equal to the bank balance minus the minimum desired bank balance (double) -CONSUMPTION_FRACTION = 0.5 -# Fraction of Government support representing the amount necessarily spent monthly by -# all households as essential consumption (double) -ESSENTIAL_CONSUMPTION_FRACTION = 0.8 - -######### Initial sale price parameters ########## -# Initial markup over average price of same quality houses (double) -# TODO: Note says that, according to BoE calibration, this should be around 0.2. Check and solve this! -SALE_MARKUP = 0.04 -# Weight of the days-on-market effect (double) -SALE_WEIGHT_DAYS_ON_MARKET = 0.011 -# Standard deviation of the noise (double) -SALE_EPSILON = 0.05 - -##### Buyer's desired expenditure parameters ##### -# Scale, number of annual salaries the buyer is willing to spend for buying a house (double) -# TODO: This has been macro-calibrated against owner-occupier LTI and LTV ration, core indicators average 1987-2006. Find sources! -BUY_SCALE = 4.5 -# Weight given to house price appreciation when deciding how much to spend for buying a house (double) -BUY_WEIGHT_HPA = 0.08 -# Standard deviation of the noise (double) -BUY_EPSILON = 0.14 - -############ Demanded rent parameters ############ -# Markup over average rent demanded for houses of the same quality (double) -RENT_MARKUP = 0.00 -# Number of months on the market in an equilibrium situation (double) -RENT_EQ_MONTHS_ON_MARKET = 6.0 -# Standard deviation of the noise (double) -RENT_EPSILON = 0.05 -# Maximum period of time BTL investors are ready to wait to get back their investment through rents, -# this determines the minimum rent they are ready to accept (double) -# TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: This parameter and its associated mechanism are not declared in the article! Need to declare and reference! -RENT_MAX_AMORTIZATION_PERIOD = 20.833333333 -# Percentage reduction of demanded rent for every month the property is in the market, not rented (double) -RENT_REDUCTION = 0.05 - -############# Downpayment parameters ############# -# Minimum income percentile to consider any downpayment, below this level, downpayment is set to 0 (double) -# TODO: Calibrated against PSD data, need clearer reference or disclose distribution! -DOWNPAYMENT_MIN_INCOME = 0.3 -# TODO: Both functional form and parameters are micro-calibrated against BoE data. Need reference or disclose distribution! -# Scale parameter for the log-normal distribution of downpayments by first-time-buyers (double) -DOWNPAYMENT_FTB_SCALE = 10.30 -# Shape parameter for the log-normal distribution of downpayments by first-time-buyers (double) -DOWNPAYMENT_FTB_SHAPE = 0.9093 -# Scale parameter for the log-normal distribution of downpayments by owner-occupiers (double) -DOWNPAYMENT_OO_SCALE = 11.155 -# Shape parameter for the log-normal distribution of downpayments by owner-occupiers (double) -DOWNPAYMENT_OO_SHAPE = 0.7538 -# Average downpayment, as percentage of house price, by but-to-let investors (double) -# TODO: Said to be calibrated to match LTV ratios, but no reference is given. Need reference! -# TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: Functional form slightly different to the one presented in the article -DOWNPAYMENT_BTL_MEAN = 0.3 -# Standard deviation of the noise (double) -DOWNPAYMENT_BTL_EPSILON = 0.1 - -######## Desired bank balance parameters ######### -# Micro-calibrated to match the log-normal relationship between wealth and income from the Wealth and Assets Survey -# Log-normal function parameter (double) -DESIRED_BANK_BALANCE_ALPHA = -32.0013877 -# Log-normal function parameter (double) -DESIRED_BANK_BALANCE_BETA = 4.07 -# Standard deviation of a noise, it states a propensity to save (double) -DESIRED_BANK_BALANCE_EPSILON = 0.1 - -########## Selling decision parameters ########### -# Weight of houses per capita effect -DECISION_TO_SELL_ALPHA = 4.0 -# Weight of interest rate effect -DECISION_TO_SELL_BETA = 5.0 -# TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: fudge parameter, explicitly explained otherwise in the article -DECISION_TO_SELL_HPC = 0.05 -# TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: fudge parameter, explicitly explained otherwise in the article -DECISION_TO_SELL_INTEREST = 0.03 - -######### BTL buy/sell choice parameters ######### -# Shape parameter, or intensity of choice on effective yield (double) -BTL_CHOICE_INTENSITY = 50.0 -# Minimun bank balance, as a percentage of the desired bank balance, to buy new properties (double) -# TODO: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Attention: This parameter and its associated mechanism are not declared in the article! Need to declare and reference! -BTL_CHOICE_MIN_BANK_BALANCE = 0.75 - -################################################## -################ Bank parameters ################# -################################################## -# TODO: We need references or justification for all these values! - -# Mortgage duration in years (int) -MORTGAGE_DURATION_YEARS = 25 -# Bank initial base-rate, which remains currently unchanged (double) -BANK_INITIAL_BASE_RATE = 0.005 -# Bank's target supply of credit per household per month (double) -BANK_CREDIT_SUPPLY_TARGET = 380 -# Maximum LTV ratio that the private bank would allow for first-time-buyers (double) -BANK_MAX_FTB_LTV = 0.95 -# Maximum LTV ratio that the private bank would allow for owner-occupiers (double) -BANK_MAX_OO_LTV = 0.9 -# Maximum LTV ratio that the private bank would allow for BTL investors (double) -BANK_MAX_BTL_LTV = 0.8 -# Maximum LTI ratio that the private bank would allow for first-time-buyers (private bank's hard limit) (double) -BANK_MAX_FTB_LTI = 6.0 -# Maximum LTI ratio that the private bank would allow for owner-occupiers (private bank's hard limit) (double) -BANK_MAX_OO_LTI = 6.0 - -################################################## -############# Central bank parameters ############ -################################################## -# TODO: We need references or justification for all these values! Also, need to clarify meaning of "when not regulated" - -# Maximum LTI ratio that the bank would allow for first-time-buyers when not regulated (double) -CENTRAL_BANK_MAX_FTB_LTI = 6.0 -# Maximum LTI ratio that the bank would allow for owner-occupiers when not regulated (double) -CENTRAL_BANK_MAX_OO_LTI = 6.0 -# Maximum fraction of mortgages that the bank can give over the LTI ratio limit (double) -CENTRAL_BANK_FRACTION_OVER_MAX_LTI = 0.15 -# Maximum fraction of the household's income to be spent on mortgage repayments under stressed conditions (double) -CENTRAL_BANK_AFFORDABILITY_COEFF = 0.5 -# Interest rate under stressed condition for BTL investors when calculating interest coverage ratios, ICR (double) -CENTRAL_BANK_BTL_STRESSED_INTEREST = 0.05 -# Interest coverage ratio (ICR) limit imposed by the central bank (double) -CENTRAL_BANK_MAX_ICR = 1.25 - -################################################## -############ Construction parameters ############# -################################################## -# TODO: We need references or justification for all these values! - -# Target ratio of houses per household (double) -CONSTRUCTION_HOUSES_PER_HOUSEHOLD = 0.82 - -################################################## -############# Government parameters ############## -################################################## - -# General personal allowance to be deducted when computing taxable income (double) -GOVERNMENT_GENERAL_PERSONAL_ALLOWANCE = 9440.0 -# Limit of income above which personal allowance starts to decrease £1 for every £2 of income above this limit (double) -GOVERNMENT_INCOME_LIMIT_FOR_PERSONAL_ALLOWANCE = 100000.0 -# Minimum monthly earnings for a married couple from income support (double) -# TODO: We need a reference or justification for this value! -GOVERNMENT_MONTHLY_INCOME_SUPPORT = 492.7 - -################################################## -############## Collectors parameters ############# -################################################## - -# Approximate number of households in UK, used to scale up results for core indicators (double) -# TODO: Reference needed -UK_HOUSEHOLDS = 26.5e6 -# Whether to record mortgage statistics (boolean) -MORTGAGE_DIAGNOSTICS_ACTIVE = true - -################################################## -################# Data addresses ################# -################################################## - -############ Government data addresses ########### -# TODO: We need clearer references for the values contained in these files! Also, current values are for 2013/2014, replace for 2011! -DATA_TAX_RATES = "src/main/resources/TaxRates.csv" -DATA_NATIONAL_INSURANCE_RATES = "src/main/resources/NationalInsuranceRates.csv" - -############ Lifecycle data addresses ############ -DATA_INCOME_GIVEN_AGE = "src/main/resources/IncomeGivenAge.csv" - -########### Demographics data addresses ########## -# Target probability density of age of representative person in the household at time t=0, calibrated against LCFS (2012) -DATA_HOUSEHOLD_AGE_AT_BIRTH_PDF = "src/main/resources/HouseholdAgeAtBirthPDF.csv" -DATA_DEATH_PROB_GIVEN_AGE = "src/main/resources/DeathProbGivenAge.csv"