Skip to content

calculator

Helpers for gathering calculator inputs and orchestrating core analysis.

CalculatorInputs dataclass

Inputs collected from the Streamlit UI.

Attributes:

Name Type Description
current_balance float

Current loan balance.

current_rate float

Current percentage rate on the existing loan.

current_remaining_years float

Remaining years on the current mortgage.

new_rate float

Candidate refinance rate percentage.

new_term_years float

Term for the new loan in years.

closing_costs float

Expected closing costs for the refinance.

cash_out float

Cash out amount requested with the refinance.

opportunity_rate float

Discount rate for NPV computations (percent).

marginal_tax_rate float

Marginal tax rate for after-tax calculations (percent).

npv_window_years int

Horizon used to compute NPV savings.

chart_horizon_years int

Years displayed on the cumulative savings chart.

maintain_payment bool

Whether the borrower maintains the current payment level.

sensitivity_max_reduction float

Max reduction below the current rate for sensitivity scenarios.

sensitivity_step float

Step size between successive sensitivity scenarios.

Source code in src/refi_calculator/web/calculator.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@dataclass
class CalculatorInputs:
    """Inputs collected from the Streamlit UI.

    Attributes:
        current_balance: Current loan balance.
        current_rate: Current percentage rate on the existing loan.
        current_remaining_years: Remaining years on the current mortgage.
        new_rate: Candidate refinance rate percentage.
        new_term_years: Term for the new loan in years.
        closing_costs: Expected closing costs for the refinance.
        cash_out: Cash out amount requested with the refinance.
        opportunity_rate: Discount rate for NPV computations (percent).
        marginal_tax_rate: Marginal tax rate for after-tax calculations (percent).
        npv_window_years: Horizon used to compute NPV savings.
        chart_horizon_years: Years displayed on the cumulative savings chart.
        maintain_payment: Whether the borrower maintains the current payment level.
        sensitivity_max_reduction: Max reduction below the current rate for sensitivity scenarios.
        sensitivity_step: Step size between successive sensitivity scenarios.
    """

    current_balance: float
    current_rate: float
    current_remaining_years: float
    new_rate: float
    new_term_years: float
    closing_costs: float
    cash_out: float
    opportunity_rate: float
    marginal_tax_rate: float
    npv_window_years: int
    chart_horizon_years: int
    maintain_payment: bool
    sensitivity_max_reduction: float
    sensitivity_step: float

collect_inputs()

Gather user inputs from Streamlit widgets.

Returns:

Type Description
CalculatorInputs

CalculatorInputs populated with the current values.

Source code in src/refi_calculator/web/calculator.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def collect_inputs() -> CalculatorInputs:
    """Gather user inputs from Streamlit widgets.

    Returns:
        CalculatorInputs populated with the current values.
    """
    st.subheader("Loan Inputs")
    current_col, new_col = st.columns(2)

    with current_col:
        current_balance = st.number_input(
            "Balance ($)",
            min_value=0.0,
            value=DEFAULT_CURRENT_BALANCE,
            step=1_000.0,
        )
        current_rate = st.number_input(
            "Rate (%):",
            min_value=0.0,
            value=DEFAULT_CURRENT_RATE,
            step=0.01,
        )
        current_remaining_years = st.number_input(
            "Years Remaining",
            min_value=0.5,
            value=DEFAULT_CURRENT_REMAINING,
            step=0.5,
        )

    with new_col:
        new_rate = st.number_input(
            "New Rate (%):",
            min_value=0.0,
            value=DEFAULT_NEW_RATE,
            step=0.01,
        )
        new_term_years = st.number_input(
            "Term (years)",
            min_value=1.0,
            value=DEFAULT_NEW_TERM,
            step=0.5,
        )
        closing_costs = st.number_input(
            "Closing Costs ($)",
            min_value=0.0,
            value=DEFAULT_CLOSING_COSTS,
            step=500.0,
        )
        cash_out = st.number_input(
            "Cash Out ($)",
            min_value=0.0,
            value=DEFAULT_CASH_OUT,
            step=500.0,
        )

    with st.expander("Advanced options", expanded=False):
        opportunity_rate = st.number_input(
            "Opportunity Rate (%)",
            min_value=0.0,
            max_value=100.0,
            value=st.session_state["opportunity_rate"],
            step=0.1,
            key="opportunity_rate",
        )
        marginal_tax_rate = st.number_input(
            "Marginal Tax Rate (%)",
            min_value=0.0,
            max_value=100.0,
            value=st.session_state["marginal_tax_rate"],
            step=0.1,
            key="marginal_tax_rate",
        )
        npv_window_years = int(
            st.number_input(
                "NPV Window (years)",
                min_value=1,
                max_value=30,
                value=st.session_state["npv_window_years"],
                step=1,
                key="npv_window_years",
            ),
        )
        maintain_payment = st.checkbox(
            "Maintain current payment (extra → principal)",
            value=st.session_state["maintain_payment"],
            key="maintain_payment",
        )
        st.caption("Opportunity cost and tax rate feed into the NPV and savings dashboard.")

    chart_horizon_years = int(st.session_state["chart_horizon_years"])
    sensitivity_max_reduction = float(st.session_state["sensitivity_max_reduction"])
    sensitivity_step = float(st.session_state["sensitivity_step"])

    return CalculatorInputs(
        current_balance=current_balance,
        current_rate=current_rate,
        current_remaining_years=current_remaining_years,
        new_rate=new_rate,
        new_term_years=new_term_years,
        closing_costs=closing_costs,
        cash_out=cash_out,
        opportunity_rate=opportunity_rate,
        marginal_tax_rate=marginal_tax_rate,
        npv_window_years=npv_window_years,
        chart_horizon_years=chart_horizon_years,
        maintain_payment=maintain_payment,
        sensitivity_max_reduction=sensitivity_max_reduction,
        sensitivity_step=sensitivity_step,
    )

ensure_option_state()

Restore default option values in Streamlit session state.

Source code in src/refi_calculator/web/calculator.py
251
252
253
254
255
256
def ensure_option_state() -> None:
    """Restore default option values in Streamlit session state."""
    for key, default in OPTION_STATE_DEFAULTS.items():
        st.session_state.setdefault(key, default)
    for key, default in ADVANCED_STATE_DEFAULTS.items():
        st.session_state.setdefault(key, default)

prepare_auxiliary_data(inputs)

Compute supporting tables for the analysis tab.

Parameters:

Name Type Description Default
inputs CalculatorInputs

Combination of all UI parameters.

required

Returns:

Type Description
tuple[list[dict], list[dict], list[dict]]

Tuple of sensitivity, holding period, and amortization data.

Source code in src/refi_calculator/web/calculator.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def prepare_auxiliary_data(
    inputs: CalculatorInputs,
) -> tuple[list[dict], list[dict], list[dict]]:
    """Compute supporting tables for the analysis tab.

    Args:
        inputs: Combination of all UI parameters.

    Returns:
        Tuple of sensitivity, holding period, and amortization data.
    """
    rate_steps = _build_rate_steps(
        inputs.current_rate,
        inputs.sensitivity_max_reduction,
        inputs.sensitivity_step,
    )
    sensitivity_data = run_sensitivity(
        inputs.current_balance,
        inputs.current_rate / 100,
        inputs.current_remaining_years,
        inputs.new_term_years,
        inputs.closing_costs,
        inputs.opportunity_rate / 100,
        rate_steps,
        inputs.npv_window_years,
    )
    holding_period_data = run_holding_period_analysis(
        inputs.current_balance,
        inputs.current_rate / 100,
        inputs.current_remaining_years,
        inputs.new_rate / 100,
        inputs.new_term_years,
        inputs.closing_costs,
        inputs.opportunity_rate / 100,
        inputs.marginal_tax_rate / 100,
        HOLDING_PERIODS,
        cash_out=inputs.cash_out,
    )
    amortization_data = generate_comparison_schedule(
        inputs.current_balance,
        inputs.current_rate / 100,
        inputs.current_remaining_years,
        inputs.new_rate / 100,
        inputs.new_term_years,
        inputs.closing_costs,
        cash_out=inputs.cash_out,
        maintain_payment=inputs.maintain_payment,
    )
    return sensitivity_data, holding_period_data, amortization_data

run_analysis(inputs)

Run the refinance analysis calculations.

Parameters:

Name Type Description Default
inputs CalculatorInputs

Inputs captured from the UI.

required

Returns:

Type Description
RefinanceAnalysis

Analysis results for the provided scenario.

Source code in src/refi_calculator/web/calculator.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def run_analysis(inputs: CalculatorInputs) -> RefinanceAnalysis:
    """Run the refinance analysis calculations.

    Args:
        inputs: Inputs captured from the UI.

    Returns:
        Analysis results for the provided scenario.
    """
    return analyze_refinance(
        current_balance=inputs.current_balance,
        current_rate=inputs.current_rate / 100,
        current_remaining_years=inputs.current_remaining_years,
        new_rate=inputs.new_rate / 100,
        new_term_years=inputs.new_term_years,
        closing_costs=inputs.closing_costs,
        cash_out=inputs.cash_out,
        opportunity_rate=inputs.opportunity_rate / 100,
        npv_window_years=inputs.npv_window_years,
        chart_horizon_years=inputs.chart_horizon_years,
        marginal_tax_rate=inputs.marginal_tax_rate / 100,
        maintain_payment=inputs.maintain_payment,
    )