The background is I'm building a toy rent vs. buy mortgage calculator. I am an experienced software engineer but my math skills are 20 years behind me and I admit to being very lost.
I've been using pulp, however perhaps I should have used z3 given that it may have a friendlier interface for newbies like me. Any advice welcome.
This is a reduced example
from pulp import *
# assumptions, obviously absurd
thirty_year_fixed_rate = 6.78
property_price = 1_000_000
cash_on_hand = 50_000
monthly_expenses = 5000
monthly_income = 30_000
monthly_interest_rate = thirty_year_fixed_rate / (12.0 * 100)
mortgage_payment = LpVariable("mortgage_payment", 0, None, LpContinuous)
pmi_payment = LpVariable("pmi_payment", 0, None, LpContinuous)
mortgaged_amount = LpVariable("mortgaged_amount", 0, None, LpContinuous)
down_payment_amount = LpVariable("down_payment_amount", 0, None, LpContinuous)
debt_to_income = LpVariable("debt_to_income", 0, None, LpContinuous)
loan_to_value = mortgaged_amount * (1.0 / property_price)
prob = LpProblem(name="minimize_monthlies", sense=LpMinimize)
closing_costs = 0.04 * mortgaged_amount
prob += mortgaged_amount == property_price - (down_payment_amount + closing_costs)
prob += down_payment_amount == cash_on_hand
mortgage_payment = (
mortgaged_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** 360
) / ((1 + monthly_interest_rate) ** 360 - 1)
# housing expenses must be kept under 43% DTI to be conforming
prob += debt_to_income == ((mortgage_payment + monthly_expenses) / monthly_income)
prob += debt_to_income <= 0.43
# these are thresholds for the loan_to_value ratio, below .80
# there is no payment required, but between each other range the
# payment percent scales up
t1, t2, t3, t4, M = 0.8, 0.85, 0.9, 0.95, 1
Z1 = pulp.LpVariable("Z1", cat="Binary")
Z2 = pulp.LpVariable("Z2", cat="Binary")
Z3 = pulp.LpVariable("Z3", cat="Binary")
Z4 = pulp.LpVariable("Z4", cat="Binary")
Z5 = pulp.LpVariable("Z5", cat="Binary")
prob.addConstraint(pulp.lpSum([Z1, Z2, Z3, Z4, Z5]) == 1)
prob.addConstraint(
loan_to_value <= t1 * Z1 + t2 * Z2 + t3 * Z3 + t4 * Z4 + M * Z5 - 1e-6
)
prob.addConstraint(t1 * Z2 + t2 * Z3 + t3 * Z4 + t4 * Z5 <= loan_to_value)
# HERE IS THE ISSUE: this is not correct
# this is just calculating the rate, NOT the payment
# I need to multiply this times the mortgaged_amount to get the actual payment
prob.addConstraint(
(t1 * Z1 * 0)
+ (t2 * Z2 * (0.002 / 12))
+ (t3 * Z3 * (0.003 / 12))
+ (t4 * Z4 * (0.004 / 12))
+ (M * Z5 * (0.005 / 12))
== pmi_payment
)
monthly_payment = mortgage_payment + pmi_payment
prob.setObjective(monthly_payment)
prob.solve()
for name, var in prob.variablesDict().items():
print(name, var.varValue)
Note my comment, if I modify this to read:
prob.addConstraint(
mortgaged_amount
* (
(t1 * Z1 * 0)
+ (t2 * Z2 * (0.002 / 12))
+ (t3 * Z3 * (0.003 / 12))
+ (t4 * Z4 * (0.004 / 12))
+ (M * Z5 * (0.005 / 12))
)
== pmi_payment
)
I'll receive: TypeError: Non-constant expressions cannot be multiplied
I believe (again I'm ignorant here) that this would make the expression quadratic in nature since I'd be asking the solver to explore the product of two independent variables (?) and so pulp blows up.
How do I go about calculating that amount properly?
Note again that this a reduced example of a more complicated problem with lots of interdependencies - many payments change, like interest rate, as a result of loan to value, debt to income, etc. The goal will be to optimize monthly payments by distributing assets which isn't shown here quite yet as it isn't relevant.