refi-calculator 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. refi_calculator/__init__.py +37 -0
  2. refi_calculator/cli.py +64 -0
  3. refi_calculator/core/__init__.py +36 -0
  4. refi_calculator/core/calculations.py +713 -0
  5. refi_calculator/core/charts.py +77 -0
  6. refi_calculator/core/market/__init__.py +11 -0
  7. refi_calculator/core/market/fred.py +62 -0
  8. refi_calculator/core/models.py +131 -0
  9. refi_calculator/environment.py +124 -0
  10. refi_calculator/gui/__init__.py +12 -0
  11. refi_calculator/gui/app.py +1008 -0
  12. refi_calculator/gui/builders/analysis_tab.py +92 -0
  13. refi_calculator/gui/builders/helpers.py +90 -0
  14. refi_calculator/gui/builders/info_tab.py +195 -0
  15. refi_calculator/gui/builders/main_tab.py +173 -0
  16. refi_calculator/gui/builders/market_tab.py +115 -0
  17. refi_calculator/gui/builders/options_tab.py +81 -0
  18. refi_calculator/gui/builders/visuals_tab.py +128 -0
  19. refi_calculator/gui/chart.py +459 -0
  20. refi_calculator/gui/market_chart.py +192 -0
  21. refi_calculator/gui/market_constants.py +24 -0
  22. refi_calculator/web/__init__.py +11 -0
  23. refi_calculator/web/app.py +92 -0
  24. refi_calculator/web/calculator.py +317 -0
  25. refi_calculator/web/formatting.py +90 -0
  26. refi_calculator/web/info.py +226 -0
  27. refi_calculator/web/market.py +270 -0
  28. refi_calculator/web/results.py +455 -0
  29. refi_calculator/web/runner.py +22 -0
  30. refi_calculator-0.7.0.dist-info/METADATA +132 -0
  31. refi_calculator-0.7.0.dist-info/RECORD +34 -0
  32. refi_calculator-0.7.0.dist-info/WHEEL +4 -0
  33. refi_calculator-0.7.0.dist-info/entry_points.txt +4 -0
  34. refi_calculator-0.7.0.dist-info/licenses/LICENSE.txt +201 -0
@@ -0,0 +1,192 @@
1
+ """Canvas for plotting historical market rates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tkinter as tk
6
+ from datetime import datetime
7
+
8
+
9
+ class MarketChart(tk.Canvas):
10
+ """Simple line chart for market rate series.
11
+
12
+ Attributes:
13
+ width: Canvas width.
14
+ height: Canvas height.
15
+ padding: Chart padding.
16
+ """
17
+
18
+ width: int
19
+ height: int
20
+ padding: dict[str, int]
21
+
22
+ def __init__(self, parent: tk.Misc, width: int = 780, height: int = 220):
23
+ """Initialize the canvas.
24
+
25
+ Args:
26
+ parent: Parent widget for the chart.
27
+ width: Chart width in pixels.
28
+ height: Chart height in pixels.
29
+ """
30
+ super().__init__(
31
+ parent,
32
+ width=width,
33
+ height=height,
34
+ bg="white",
35
+ highlightthickness=1,
36
+ highlightbackground="#ccc",
37
+ )
38
+ self.width = width
39
+ self.height = height
40
+ self.padding = {"left": 70, "right": 20, "top": 20, "bottom": 40}
41
+
42
+ def plot(self, series_data: dict[str, list[tuple[datetime, float]]]) -> None:
43
+ """Draw a multi-line chart for the supplied rate series.
44
+
45
+ Args:
46
+ series_data: Mapping of series label to date/rate pairs (newest-first).
47
+ """
48
+ self.delete("all")
49
+ filtered = {
50
+ label: list(reversed(points)) for label, points in series_data.items() if points
51
+ }
52
+ if not filtered:
53
+ return
54
+
55
+ all_values = [rate for points in filtered.values() for _, rate in points]
56
+ if not all_values:
57
+ return
58
+
59
+ min_rate = min(all_values)
60
+ max_rate = max(all_values)
61
+ rate_range = max_rate - min_rate if max_rate != min_rate else 1
62
+
63
+ plot_width = self.width - self.padding["left"] - self.padding["right"]
64
+ plot_height = self.height - self.padding["top"] - self.padding["bottom"]
65
+
66
+ def x_coord(idx: int, total: int) -> float:
67
+ return self.padding["left"] + (idx / max(total - 1, 1)) * plot_width
68
+
69
+ def y_coord(value: float) -> float:
70
+ return self.padding["top"] + (1 - (value - min_rate) / rate_range) * plot_height
71
+
72
+ colors = ["#2563eb", "#ec4899", "#16a34a", "#f59e0b"]
73
+ for idx, (label, points) in enumerate(filtered.items()):
74
+ coords = [
75
+ (x_coord(i, len(points)), y_coord(rate)) for i, (_, rate) in enumerate(points)
76
+ ]
77
+ min_coords = 2
78
+ if len(coords) < min_coords:
79
+ continue
80
+ self.create_line(
81
+ *[component for point in coords for component in point],
82
+ fill=colors[idx % len(colors)],
83
+ width=2,
84
+ )
85
+
86
+ self.create_line(
87
+ self.padding["left"],
88
+ self.padding["top"],
89
+ self.padding["left"],
90
+ self.height - self.padding["bottom"],
91
+ fill="#333",
92
+ )
93
+ self.create_line(
94
+ self.padding["left"],
95
+ self.height - self.padding["bottom"],
96
+ self.width - self.padding["right"],
97
+ self.height - self.padding["bottom"],
98
+ fill="#333",
99
+ )
100
+
101
+ # Y-axis ticks
102
+ tick_count_y = 4
103
+ for idx in range(tick_count_y + 1):
104
+ rate_value = min_rate + (rate_range / tick_count_y) * idx
105
+ y = y_coord(rate_value)
106
+ self.create_line(
107
+ self.padding["left"] - 8,
108
+ y,
109
+ self.padding["left"],
110
+ y,
111
+ fill="#333",
112
+ )
113
+ self.create_text(
114
+ self.padding["left"] - 14,
115
+ y,
116
+ text=f"{rate_value:.2f}%",
117
+ anchor=tk.E,
118
+ font=("Segoe UI", 7),
119
+ fill="#666",
120
+ )
121
+
122
+ # X-axis ticks
123
+ sample_points = next(iter(filtered.values()))
124
+ total_points = len(sample_points)
125
+ tick_step = max(1, total_points // 5)
126
+ tick_indices = list(range(0, total_points, tick_step))
127
+ if total_points - 1 not in tick_indices:
128
+ tick_indices.append(total_points - 1)
129
+
130
+ for idx in tick_indices:
131
+ x = x_coord(idx, total_points)
132
+ self.create_line(
133
+ x,
134
+ self.height - self.padding["bottom"],
135
+ x,
136
+ self.height - self.padding["bottom"] + 4,
137
+ fill="#333",
138
+ )
139
+ date_label = sample_points[idx][0].strftime("%Y-%m-%d")
140
+ self.create_text(
141
+ x,
142
+ self.height - self.padding["bottom"] + 14,
143
+ text=date_label,
144
+ anchor=tk.N,
145
+ font=("Segoe UI", 7),
146
+ fill="#666",
147
+ )
148
+
149
+ # Axis labels
150
+ self.create_text(
151
+ self.width // 2,
152
+ self.height - 10,
153
+ text="Date (oldest → newest)",
154
+ font=("Segoe UI", 8, "bold"),
155
+ fill="#444",
156
+ )
157
+ self.create_text(
158
+ self.padding["left"] - 55,
159
+ (self.height + self.padding["top"] - self.padding["bottom"]) // 2,
160
+ text="Rate (%)",
161
+ angle=90,
162
+ font=("Segoe UI", 8, "bold"),
163
+ fill="#444",
164
+ )
165
+
166
+ legend_x = self.width - self.padding["right"] - 110
167
+ legend_y = self.padding["top"] + 10
168
+ for idx, label in enumerate(filtered.keys()):
169
+ color = colors[idx % len(colors)]
170
+ self.create_line(
171
+ legend_x,
172
+ legend_y + idx * 16,
173
+ legend_x + 20,
174
+ legend_y + idx * 16,
175
+ fill=color,
176
+ width=2,
177
+ )
178
+ self.create_text(
179
+ legend_x + 25,
180
+ legend_y + idx * 16,
181
+ text=label,
182
+ anchor=tk.W,
183
+ font=("Segoe UI", 8),
184
+ fill="#444",
185
+ )
186
+
187
+
188
+ __all__ = ["MarketChart"]
189
+
190
+ __description__ = """
191
+ Canvas helper for plotting historical mortgage rate series.
192
+ """
@@ -0,0 +1,24 @@
1
+ """Constants shared by the market data UI components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ MARKET_SERIES: list[tuple[str, str]] = [
6
+ ("30-Year", "MORTGAGE30US"),
7
+ ("15-Year", "MORTGAGE15US"),
8
+ ]
9
+
10
+ MARKET_PERIOD_OPTIONS: list[tuple[str, str]] = [
11
+ ("1 Year", "12"),
12
+ ("2 Years", "24"),
13
+ ("5 Years", "60"),
14
+ ("All", "0"),
15
+ ]
16
+
17
+ MARKET_DEFAULT_PERIOD = "12"
18
+
19
+
20
+ __all__ = ["MARKET_SERIES", "MARKET_PERIOD_OPTIONS", "MARKET_DEFAULT_PERIOD"]
21
+
22
+ __description__ = """
23
+ Constants used by the market data tab.
24
+ """
@@ -0,0 +1,11 @@
1
+ """Web interface exports for the refinance calculator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .app import main
6
+
7
+ __all__ = ["main"]
8
+
9
+ __description__ = """
10
+ Streamlit web application exports for the refinance calculator.
11
+ """
@@ -0,0 +1,92 @@
1
+ """Streamlit web interface for the refinance calculator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from logging import getLogger
6
+
7
+ import streamlit as st
8
+
9
+ from refi_calculator.core.models import RefinanceAnalysis
10
+ from refi_calculator.web.calculator import (
11
+ CalculatorInputs,
12
+ collect_inputs,
13
+ ensure_option_state,
14
+ prepare_auxiliary_data,
15
+ run_analysis,
16
+ )
17
+ from refi_calculator.web.info import render_info_tab
18
+ from refi_calculator.web.market import render_market_tab
19
+ from refi_calculator.web.results import (
20
+ render_analysis_tab,
21
+ render_loan_visualizations_tab,
22
+ render_options_tab,
23
+ render_results,
24
+ )
25
+
26
+ logger = getLogger(__name__)
27
+
28
+
29
+ def main() -> None:
30
+ """Render the refinance calculator Streamlit application."""
31
+ logger.debug("Rendering Streamlit refinance calculator main screen.")
32
+ ensure_option_state()
33
+ st.set_page_config(
34
+ page_title="Refinance Calculator",
35
+ layout="wide",
36
+ )
37
+
38
+ st.title("Refinance Calculator")
39
+ st.write(
40
+ "Use the inputs below to compare refinancing scenarios, cash-out needs, "
41
+ "and after-tax impacts before reviewing the cumulative savings timeline.",
42
+ )
43
+
44
+ calc_tab, analysis_tab, visuals_tab, market_tab, options_tab, info_tab = st.tabs(
45
+ [
46
+ "Calculator",
47
+ "Analysis",
48
+ "Loan Visualizations",
49
+ "Market",
50
+ "Options",
51
+ "Info",
52
+ ],
53
+ )
54
+
55
+ inputs: CalculatorInputs | None = None
56
+ analysis: RefinanceAnalysis | None = None
57
+
58
+ with calc_tab:
59
+ inputs = collect_inputs()
60
+ analysis = run_analysis(inputs)
61
+ render_results(inputs, analysis)
62
+
63
+ if inputs is None or analysis is None:
64
+ return
65
+
66
+ sensitivity_data, holding_period_data, amortization_data = prepare_auxiliary_data(inputs)
67
+
68
+ with analysis_tab:
69
+ render_analysis_tab(inputs, sensitivity_data, holding_period_data)
70
+
71
+ with visuals_tab:
72
+ render_loan_visualizations_tab(analysis, amortization_data)
73
+
74
+ with market_tab:
75
+ render_market_tab()
76
+
77
+ with options_tab:
78
+ render_options_tab(inputs)
79
+
80
+ with info_tab:
81
+ render_info_tab()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
86
+
87
+
88
+ __all__ = ["main"]
89
+
90
+ __description__ = """
91
+ Streamlit app wiring that mirrors the desktop refinance calculator experience.
92
+ """
@@ -0,0 +1,317 @@
1
+ """Helpers for gathering calculator inputs and orchestrating core analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from logging import getLogger
7
+
8
+ import streamlit as st
9
+
10
+ from refi_calculator.core.calculations import (
11
+ analyze_refinance,
12
+ generate_comparison_schedule,
13
+ run_holding_period_analysis,
14
+ run_sensitivity,
15
+ )
16
+ from refi_calculator.core.models import RefinanceAnalysis
17
+
18
+ logger = getLogger(__name__)
19
+
20
+ DEFAULT_CURRENT_BALANCE = 400_000.0
21
+ DEFAULT_CURRENT_RATE = 6.5
22
+ DEFAULT_CURRENT_REMAINING = 25.0
23
+ DEFAULT_NEW_RATE = 5.75
24
+ DEFAULT_NEW_TERM = 30.0
25
+ DEFAULT_CLOSING_COSTS = 8_000.0
26
+ DEFAULT_CASH_OUT = 0.0
27
+ DEFAULT_OPPORTUNITY_RATE = 5.0
28
+ DEFAULT_MARGINAL_TAX_RATE = 0.0
29
+ DEFAULT_NPV_WINDOW_YEARS = 5
30
+ DEFAULT_CHART_HORIZON_YEARS = 10
31
+ DEFAULT_SENSITIVITY_MAX_REDUCTION = 2.5
32
+ DEFAULT_SENSITIVITY_STEP = 0.125
33
+
34
+ OPTION_STATE_DEFAULTS: dict[str, float] = {
35
+ "chart_horizon_years": DEFAULT_CHART_HORIZON_YEARS,
36
+ "sensitivity_max_reduction": DEFAULT_SENSITIVITY_MAX_REDUCTION,
37
+ "sensitivity_step": DEFAULT_SENSITIVITY_STEP,
38
+ }
39
+ ADVANCED_STATE_DEFAULTS: dict[str, float | bool] = {
40
+ "opportunity_rate": DEFAULT_OPPORTUNITY_RATE,
41
+ "marginal_tax_rate": DEFAULT_MARGINAL_TAX_RATE,
42
+ "npv_window_years": DEFAULT_NPV_WINDOW_YEARS,
43
+ "maintain_payment": False,
44
+ }
45
+
46
+ HOLDING_PERIODS = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15, 20]
47
+
48
+
49
+ @dataclass
50
+ class CalculatorInputs:
51
+ """Inputs collected from the Streamlit UI.
52
+
53
+ Attributes:
54
+ current_balance: Current loan balance.
55
+ current_rate: Current percentage rate on the existing loan.
56
+ current_remaining_years: Remaining years on the current mortgage.
57
+ new_rate: Candidate refinance rate percentage.
58
+ new_term_years: Term for the new loan in years.
59
+ closing_costs: Expected closing costs for the refinance.
60
+ cash_out: Cash out amount requested with the refinance.
61
+ opportunity_rate: Discount rate for NPV computations (percent).
62
+ marginal_tax_rate: Marginal tax rate for after-tax calculations (percent).
63
+ npv_window_years: Horizon used to compute NPV savings.
64
+ chart_horizon_years: Years displayed on the cumulative savings chart.
65
+ maintain_payment: Whether the borrower maintains the current payment level.
66
+ sensitivity_max_reduction: Max reduction below the current rate for sensitivity scenarios.
67
+ sensitivity_step: Step size between successive sensitivity scenarios.
68
+ """
69
+
70
+ current_balance: float
71
+ current_rate: float
72
+ current_remaining_years: float
73
+ new_rate: float
74
+ new_term_years: float
75
+ closing_costs: float
76
+ cash_out: float
77
+ opportunity_rate: float
78
+ marginal_tax_rate: float
79
+ npv_window_years: int
80
+ chart_horizon_years: int
81
+ maintain_payment: bool
82
+ sensitivity_max_reduction: float
83
+ sensitivity_step: float
84
+
85
+
86
+ def collect_inputs() -> CalculatorInputs:
87
+ """Gather user inputs from Streamlit widgets.
88
+
89
+ Returns:
90
+ CalculatorInputs populated with the current values.
91
+ """
92
+ st.subheader("Loan Inputs")
93
+ current_col, new_col = st.columns(2)
94
+
95
+ with current_col:
96
+ current_balance = st.number_input(
97
+ "Balance ($)",
98
+ min_value=0.0,
99
+ value=DEFAULT_CURRENT_BALANCE,
100
+ step=1_000.0,
101
+ )
102
+ current_rate = st.number_input(
103
+ "Rate (%):",
104
+ min_value=0.0,
105
+ value=DEFAULT_CURRENT_RATE,
106
+ step=0.01,
107
+ )
108
+ current_remaining_years = st.number_input(
109
+ "Years Remaining",
110
+ min_value=0.5,
111
+ value=DEFAULT_CURRENT_REMAINING,
112
+ step=0.5,
113
+ )
114
+
115
+ with new_col:
116
+ new_rate = st.number_input(
117
+ "New Rate (%):",
118
+ min_value=0.0,
119
+ value=DEFAULT_NEW_RATE,
120
+ step=0.01,
121
+ )
122
+ new_term_years = st.number_input(
123
+ "Term (years)",
124
+ min_value=1.0,
125
+ value=DEFAULT_NEW_TERM,
126
+ step=0.5,
127
+ )
128
+ closing_costs = st.number_input(
129
+ "Closing Costs ($)",
130
+ min_value=0.0,
131
+ value=DEFAULT_CLOSING_COSTS,
132
+ step=500.0,
133
+ )
134
+ cash_out = st.number_input(
135
+ "Cash Out ($)",
136
+ min_value=0.0,
137
+ value=DEFAULT_CASH_OUT,
138
+ step=500.0,
139
+ )
140
+
141
+ with st.expander("Advanced options", expanded=False):
142
+ opportunity_rate = st.number_input(
143
+ "Opportunity Rate (%)",
144
+ min_value=0.0,
145
+ max_value=100.0,
146
+ value=st.session_state["opportunity_rate"],
147
+ step=0.1,
148
+ key="opportunity_rate",
149
+ )
150
+ marginal_tax_rate = st.number_input(
151
+ "Marginal Tax Rate (%)",
152
+ min_value=0.0,
153
+ max_value=100.0,
154
+ value=st.session_state["marginal_tax_rate"],
155
+ step=0.1,
156
+ key="marginal_tax_rate",
157
+ )
158
+ npv_window_years = int(
159
+ st.number_input(
160
+ "NPV Window (years)",
161
+ min_value=1,
162
+ max_value=30,
163
+ value=st.session_state["npv_window_years"],
164
+ step=1,
165
+ key="npv_window_years",
166
+ ),
167
+ )
168
+ maintain_payment = st.checkbox(
169
+ "Maintain current payment (extra → principal)",
170
+ value=st.session_state["maintain_payment"],
171
+ key="maintain_payment",
172
+ )
173
+ st.caption("Opportunity cost and tax rate feed into the NPV and savings dashboard.")
174
+
175
+ chart_horizon_years = int(st.session_state["chart_horizon_years"])
176
+ sensitivity_max_reduction = float(st.session_state["sensitivity_max_reduction"])
177
+ sensitivity_step = float(st.session_state["sensitivity_step"])
178
+
179
+ return CalculatorInputs(
180
+ current_balance=current_balance,
181
+ current_rate=current_rate,
182
+ current_remaining_years=current_remaining_years,
183
+ new_rate=new_rate,
184
+ new_term_years=new_term_years,
185
+ closing_costs=closing_costs,
186
+ cash_out=cash_out,
187
+ opportunity_rate=opportunity_rate,
188
+ marginal_tax_rate=marginal_tax_rate,
189
+ npv_window_years=npv_window_years,
190
+ chart_horizon_years=chart_horizon_years,
191
+ maintain_payment=maintain_payment,
192
+ sensitivity_max_reduction=sensitivity_max_reduction,
193
+ sensitivity_step=sensitivity_step,
194
+ )
195
+
196
+
197
+ def run_analysis(inputs: CalculatorInputs) -> RefinanceAnalysis:
198
+ """Run the refinance analysis calculations.
199
+
200
+ Args:
201
+ inputs: Inputs captured from the UI.
202
+
203
+ Returns:
204
+ Analysis results for the provided scenario.
205
+ """
206
+ return analyze_refinance(
207
+ current_balance=inputs.current_balance,
208
+ current_rate=inputs.current_rate / 100,
209
+ current_remaining_years=inputs.current_remaining_years,
210
+ new_rate=inputs.new_rate / 100,
211
+ new_term_years=inputs.new_term_years,
212
+ closing_costs=inputs.closing_costs,
213
+ cash_out=inputs.cash_out,
214
+ opportunity_rate=inputs.opportunity_rate / 100,
215
+ npv_window_years=inputs.npv_window_years,
216
+ chart_horizon_years=inputs.chart_horizon_years,
217
+ marginal_tax_rate=inputs.marginal_tax_rate / 100,
218
+ maintain_payment=inputs.maintain_payment,
219
+ )
220
+
221
+
222
+ def _build_rate_steps(
223
+ current_rate_pct: float,
224
+ max_reduction: float,
225
+ step: float,
226
+ ) -> list[float]:
227
+ """Build new-rate steps for sensitivity analysis loops.
228
+
229
+ Args:
230
+ current_rate_pct: Current rate in percent.
231
+ max_reduction: Max percent reduction to explore.
232
+ step: Step between subsequent rows (percent).
233
+
234
+ Returns:
235
+ List of new rates expressed as decimals.
236
+ """
237
+ if step <= 0:
238
+ return []
239
+
240
+ rate_steps: list[float] = []
241
+ reduction = step
242
+ max_steps = 20
243
+ while reduction <= max_reduction + 0.001 and len(rate_steps) < max_steps:
244
+ new_rate_pct = current_rate_pct - reduction
245
+ if new_rate_pct > 0:
246
+ rate_steps.append(new_rate_pct / 100)
247
+ reduction += step
248
+ return rate_steps
249
+
250
+
251
+ def ensure_option_state() -> None:
252
+ """Restore default option values in Streamlit session state."""
253
+ for key, default in OPTION_STATE_DEFAULTS.items():
254
+ st.session_state.setdefault(key, default)
255
+ for key, default in ADVANCED_STATE_DEFAULTS.items():
256
+ st.session_state.setdefault(key, default)
257
+
258
+
259
+ def prepare_auxiliary_data(
260
+ inputs: CalculatorInputs,
261
+ ) -> tuple[list[dict], list[dict], list[dict]]:
262
+ """Compute supporting tables for the analysis tab.
263
+
264
+ Args:
265
+ inputs: Combination of all UI parameters.
266
+
267
+ Returns:
268
+ Tuple of sensitivity, holding period, and amortization data.
269
+ """
270
+ rate_steps = _build_rate_steps(
271
+ inputs.current_rate,
272
+ inputs.sensitivity_max_reduction,
273
+ inputs.sensitivity_step,
274
+ )
275
+ sensitivity_data = run_sensitivity(
276
+ inputs.current_balance,
277
+ inputs.current_rate / 100,
278
+ inputs.current_remaining_years,
279
+ inputs.new_term_years,
280
+ inputs.closing_costs,
281
+ inputs.opportunity_rate / 100,
282
+ rate_steps,
283
+ inputs.npv_window_years,
284
+ )
285
+ holding_period_data = run_holding_period_analysis(
286
+ inputs.current_balance,
287
+ inputs.current_rate / 100,
288
+ inputs.current_remaining_years,
289
+ inputs.new_rate / 100,
290
+ inputs.new_term_years,
291
+ inputs.closing_costs,
292
+ inputs.opportunity_rate / 100,
293
+ inputs.marginal_tax_rate / 100,
294
+ HOLDING_PERIODS,
295
+ cash_out=inputs.cash_out,
296
+ )
297
+ amortization_data = generate_comparison_schedule(
298
+ inputs.current_balance,
299
+ inputs.current_rate / 100,
300
+ inputs.current_remaining_years,
301
+ inputs.new_rate / 100,
302
+ inputs.new_term_years,
303
+ inputs.closing_costs,
304
+ cash_out=inputs.cash_out,
305
+ maintain_payment=inputs.maintain_payment,
306
+ )
307
+ return sensitivity_data, holding_period_data, amortization_data
308
+
309
+
310
+ logger.debug("Calculator helpers module initialized.")
311
+
312
+
313
+ __all__ = ["CalculatorInputs", "collect_inputs", "run_analysis", "prepare_auxiliary_data"]
314
+
315
+ __description__ = """
316
+ Helpers for collecting inputs and driving core refinance calculations.
317
+ """