0
$\begingroup$

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.

$\endgroup$
2
  • $\begingroup$ It would be great if you can write the math model you are trying to solve by math notation instead of having only the code. $\endgroup$
    – A.Omidi
    Sep 10 at 19:34
  • $\begingroup$ You are multiplying mortgaged_amount with your Z's, which are both decision variables, hence it is indeed not linear. You have two possibilities: You can try to linearize this statement, which is not so trivial in your case or you can look for solvers that can handle non-linearities but that come with costs of less efficient computation compared to a linear counterpart. $\endgroup$
    – PeterD
    Sep 10 at 19:54

1 Answer 1

2
$\begingroup$

Since it's a product of a continuous (mortgaged amt), say $a$ and binary $z$ you can linearize each of the product terms as below
$ 0 \le x \le Az$
$a + A(z-1) \le x \le a + A(1-z) $
where $x$ is continuous var & $A$ is some big number like max mortgaged amt possible.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.