refi-calculator 0.8.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.

Potentially problematic release.


This version of refi-calculator might be problematic. Click here for more details.

Files changed (35) hide show
  1. refi_calculator/__init__.py +9 -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/constants.py +24 -0
  8. refi_calculator/core/market/fred.py +62 -0
  9. refi_calculator/core/models.py +131 -0
  10. refi_calculator/environment.py +124 -0
  11. refi_calculator/gui/__init__.py +13 -0
  12. refi_calculator/gui/app.py +1008 -0
  13. refi_calculator/gui/builders/__init__.py +9 -0
  14. refi_calculator/gui/builders/analysis_tab.py +92 -0
  15. refi_calculator/gui/builders/helpers.py +90 -0
  16. refi_calculator/gui/builders/info_tab.py +195 -0
  17. refi_calculator/gui/builders/main_tab.py +173 -0
  18. refi_calculator/gui/builders/market_tab.py +115 -0
  19. refi_calculator/gui/builders/options_tab.py +81 -0
  20. refi_calculator/gui/builders/visuals_tab.py +128 -0
  21. refi_calculator/gui/chart.py +459 -0
  22. refi_calculator/gui/market_chart.py +192 -0
  23. refi_calculator/web/__init__.py +11 -0
  24. refi_calculator/web/app.py +117 -0
  25. refi_calculator/web/calculator.py +317 -0
  26. refi_calculator/web/formatting.py +90 -0
  27. refi_calculator/web/info.py +226 -0
  28. refi_calculator/web/market.py +270 -0
  29. refi_calculator/web/results.py +455 -0
  30. refi_calculator/web/runner.py +22 -0
  31. refi_calculator-0.8.0.dist-info/METADATA +146 -0
  32. refi_calculator-0.8.0.dist-info/RECORD +35 -0
  33. refi_calculator-0.8.0.dist-info/WHEEL +4 -0
  34. refi_calculator-0.8.0.dist-info/entry_points.txt +4 -0
  35. refi_calculator-0.8.0.dist-info/licenses/LICENSE.txt +201 -0
@@ -0,0 +1,455 @@
1
+ """Render refinance analysis output, visuals, and supporting tables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from logging import getLogger
6
+ from typing import cast
7
+
8
+ import pandas as pd
9
+ import plotly.graph_objects as go
10
+ import streamlit as st
11
+ from pandas.io.formats.style import Styler
12
+
13
+ from refi_calculator.core.models import RefinanceAnalysis
14
+ from refi_calculator.web.calculator import CalculatorInputs
15
+ from refi_calculator.web.formatting import (
16
+ format_currency,
17
+ format_months,
18
+ format_optional_currency,
19
+ format_savings_delta,
20
+ format_signed_currency,
21
+ )
22
+
23
+ logger = getLogger(__name__)
24
+
25
+
26
+ def render_results(
27
+ inputs: CalculatorInputs,
28
+ analysis: RefinanceAnalysis,
29
+ ) -> None:
30
+ """Render the calculator summary metrics for the provided analysis.
31
+
32
+ Args:
33
+ inputs: Inputs used to drive the calculations.
34
+ analysis: Computed refinance analysis.
35
+ """
36
+ st.subheader("Analysis Results")
37
+
38
+ payments = st.columns(3)
39
+ payments[0].metric("Current Payment", format_currency(analysis.current_payment))
40
+ payments[1].metric("New Payment", format_currency(analysis.new_payment))
41
+ payments[2].metric("Monthly Δ", format_savings_delta(analysis.monthly_savings))
42
+
43
+ st.divider()
44
+
45
+ balances = st.columns(2)
46
+ balances[0].metric("New Loan Balance", format_currency(analysis.new_loan_balance))
47
+ balances[1].metric("Cash Out", format_currency(analysis.cash_out_amount))
48
+
49
+ st.divider()
50
+
51
+ breakeven = st.columns(2)
52
+ breakeven[0].metric(
53
+ "Simple Breakeven",
54
+ format_months(analysis.simple_breakeven_months),
55
+ )
56
+ breakeven[1].metric(
57
+ "NPV Breakeven",
58
+ format_months(analysis.npv_breakeven_months),
59
+ )
60
+
61
+ st.divider()
62
+
63
+ interest = st.columns(3)
64
+ interest[0].metric(
65
+ "Current Total Interest",
66
+ format_currency(analysis.current_total_interest),
67
+ )
68
+ interest[1].metric(
69
+ "New Total Interest",
70
+ format_currency(analysis.new_total_interest),
71
+ )
72
+ interest[2].metric(
73
+ "Interest Δ",
74
+ format_signed_currency(analysis.interest_delta),
75
+ )
76
+
77
+ st.divider()
78
+
79
+ st.subheader("After-Tax Analysis")
80
+ after_tax_payments = st.columns(3)
81
+ after_tax_payments[0].metric(
82
+ "Current (After-Tax)",
83
+ format_currency(analysis.current_after_tax_payment),
84
+ )
85
+ after_tax_payments[1].metric(
86
+ "New (After-Tax)",
87
+ format_currency(analysis.new_after_tax_payment),
88
+ )
89
+ after_tax_payments[2].metric(
90
+ "Monthly Δ (A-T)",
91
+ format_savings_delta(analysis.after_tax_monthly_savings),
92
+ )
93
+
94
+ after_tax_breakeven = st.columns(3)
95
+ after_tax_breakeven[0].metric(
96
+ "Simple BE (A-T)",
97
+ format_months(analysis.after_tax_simple_breakeven_months),
98
+ )
99
+ after_tax_breakeven[1].metric(
100
+ "NPV BE (A-T)",
101
+ format_months(analysis.after_tax_npv_breakeven_months),
102
+ )
103
+ after_tax_breakeven[2].metric(
104
+ "Interest Δ (A-T)",
105
+ format_signed_currency(analysis.after_tax_interest_delta),
106
+ )
107
+
108
+ st.divider()
109
+
110
+ if inputs.maintain_payment and analysis.accelerated_months:
111
+ st.subheader("Accelerated Payoff (Maintain Payment)")
112
+ accel = st.columns(3)
113
+ accel[0].metric("Payoff Time", format_months(analysis.accelerated_months))
114
+ accel[1].metric(
115
+ "Time Saved",
116
+ format_months(analysis.accelerated_time_savings_months),
117
+ )
118
+ accel[2].metric(
119
+ "Interest Saved",
120
+ format_optional_currency(analysis.accelerated_interest_savings),
121
+ )
122
+ st.divider()
123
+
124
+ st.subheader("Total Cost NPV Analysis")
125
+ cost = st.columns(3)
126
+ cost[0].metric("Current Loan NPV", format_currency(analysis.current_total_cost_npv))
127
+ cost[1].metric("New Loan NPV", format_currency(analysis.new_total_cost_npv))
128
+ cost[2].metric(
129
+ "NPV Advantage",
130
+ format_signed_currency(analysis.total_cost_npv_advantage),
131
+ )
132
+
133
+ st.divider()
134
+
135
+ st.metric(
136
+ f"{inputs.npv_window_years}-Year NPV of Refinancing",
137
+ format_signed_currency(analysis.five_year_npv),
138
+ )
139
+
140
+
141
+ def render_cumulative_chart(analysis: RefinanceAnalysis) -> None:
142
+ """Render the cumulative savings chart for the current scenario.
143
+
144
+ Args:
145
+ analysis: Analysis output that contains the savings timeline.
146
+ """
147
+ if not analysis.cumulative_savings:
148
+ st.info("Savings chart is not available yet.")
149
+ return
150
+
151
+ chart_df = pd.DataFrame(
152
+ [
153
+ {
154
+ "Month": month,
155
+ "Nominal": nominal,
156
+ "NPV": npv_value,
157
+ }
158
+ for month, nominal, npv_value in analysis.cumulative_savings
159
+ ],
160
+ ).set_index("Month")
161
+
162
+ st.line_chart(chart_df, width="stretch")
163
+
164
+ if analysis.npv_breakeven_months:
165
+ st.caption(
166
+ f"NPV breakeven occurs at {analysis.npv_breakeven_months:.0f} months.",
167
+ )
168
+
169
+
170
+ def render_balance_comparison_chart(amortization_data: list[dict]) -> None:
171
+ """Plot loan balance comparison lines for current and new schedules."""
172
+ if not amortization_data:
173
+ st.info("Loan balance comparison will appear after running the calculator.")
174
+ return
175
+
176
+ df = pd.DataFrame(amortization_data)
177
+ if df.empty:
178
+ st.info("Loan balance comparison will appear after running the calculator.")
179
+ return
180
+
181
+ df = df[["year", "current_balance", "new_balance"]]
182
+ df.columns = ["Year", "Current Balance", "New Balance"]
183
+ df["Year"] = df["Year"].astype(int)
184
+
185
+ colors = {
186
+ "Current Balance": "#ef4444",
187
+ "New Balance": "#16a34a",
188
+ }
189
+ fig = go.Figure()
190
+ for loan in ["Current Balance", "New Balance"]:
191
+ fig.add_trace(
192
+ go.Scatter(
193
+ x=df["Year"],
194
+ y=df[loan],
195
+ mode="lines",
196
+ name=loan,
197
+ line=dict(color=colors[loan], width=3),
198
+ hovertemplate="Year=%{x}<br>Loan=%{text}<br>Balance=$%{y:,.0f}<extra></extra>",
199
+ text=[loan] * len(df),
200
+ ),
201
+ )
202
+
203
+ fig.update_layout(
204
+ xaxis=dict(
205
+ title="Year",
206
+ tickmode="linear",
207
+ dtick=1,
208
+ tickformat="d",
209
+ showgrid=False,
210
+ ),
211
+ yaxis=dict(
212
+ title="Loan Balance ($)",
213
+ tickprefix="$",
214
+ tickformat=",",
215
+ ),
216
+ legend=dict(title="Loan"),
217
+ margin=dict(t=5, b=30, l=60, r=10),
218
+ hovermode="x",
219
+ )
220
+
221
+ st.plotly_chart(fig, use_container_width=True)
222
+
223
+
224
+ def _interest_delta_style(value: str | float) -> str:
225
+ """Color interest delta values based on savings/costs."""
226
+ text = str(value).strip()
227
+ if text.startswith("-"):
228
+ return "color: green"
229
+ if text.startswith("+"):
230
+ return "color: red"
231
+ return ""
232
+
233
+
234
+ def build_sensitivity_display(
235
+ data: list[dict],
236
+ npv_years: int,
237
+ ) -> pd.DataFrame:
238
+ """Produce a display frame for rate sensitivity scenarios.
239
+
240
+ Args:
241
+ data: Raw sensitivity scenario data.
242
+ npv_years: Years window used for NPV calculations.
243
+
244
+ Returns:
245
+ Formatted DataFrame for display.
246
+ """
247
+ if not data:
248
+ return pd.DataFrame()
249
+
250
+ df = pd.DataFrame(data)
251
+ return pd.DataFrame(
252
+ {
253
+ "New Rate": df["new_rate"].map("{:.2f}%".format),
254
+ "Monthly Δ": df["monthly_savings"].map(format_savings_delta),
255
+ "Simple Breakeven": df["simple_be"].map(format_months),
256
+ "NPV Breakeven": df["npv_be"].map(format_months),
257
+ f"{npv_years}-Yr NPV": df["five_yr_npv"].map(format_signed_currency),
258
+ },
259
+ )
260
+
261
+
262
+ def build_holding_display(data: list[dict]) -> pd.DataFrame:
263
+ """Create a holding-period display DataFrame.
264
+
265
+ Args:
266
+ data: Raw holding period analysis data.
267
+
268
+ Returns:
269
+ Formatted DataFrame for the holding period tab.
270
+ """
271
+ if not data:
272
+ return pd.DataFrame()
273
+
274
+ df = pd.DataFrame(data)
275
+ return pd.DataFrame(
276
+ {
277
+ "Years": df["years"].map("{:.0f}".format),
278
+ "Nominal Savings": df["nominal_savings"].map(format_signed_currency),
279
+ "NPV": df["npv"].map(format_signed_currency),
280
+ "NPV (A-T)": df["npv_after_tax"].map(format_signed_currency),
281
+ "Recommendation": df["recommendation"],
282
+ },
283
+ )
284
+
285
+
286
+ RECOMMENDATION_COLORS = {
287
+ "Strong Yes": "green",
288
+ "Yes": "darkgreen",
289
+ "Marginal": "orange",
290
+ "No": "red",
291
+ }
292
+
293
+
294
+ def _recommendation_style(value: str) -> str:
295
+ """Return CSS style string for recommendation text."""
296
+ color = RECOMMENDATION_COLORS.get(value, "inherit")
297
+ return f"color: {color}"
298
+
299
+
300
+ def render_analysis_tab(
301
+ inputs: CalculatorInputs,
302
+ sensitivity_data: list[dict],
303
+ holding_period_data: list[dict],
304
+ ) -> None:
305
+ """Render the tables shown in the Analysis tab.
306
+
307
+ Args:
308
+ inputs: Inputs used to drive the scenario.
309
+ sensitivity_data: Precomputed sensitivity data.
310
+ holding_period_data: Precomputed holding period data.
311
+ """
312
+ st.subheader("Analysis Tables")
313
+ rate_tab, holding_tab = st.tabs(["Rate Sensitivity", "Holding Period"])
314
+
315
+ with rate_tab:
316
+ display = build_sensitivity_display(sensitivity_data, inputs.npv_window_years)
317
+ if display.empty:
318
+ st.info("Adjust the sensitivity controls to generate scenarios.")
319
+ else:
320
+ st.dataframe(display.style.hide(axis="index"), width="stretch")
321
+
322
+ with holding_tab:
323
+ display = build_holding_display(holding_period_data)
324
+ if display.empty:
325
+ st.info("Holding period analysis will populate once inputs are available.")
326
+ else:
327
+ styled = display.style.hide(axis="index").applymap(
328
+ _recommendation_style,
329
+ subset=["Recommendation"],
330
+ )
331
+ st.dataframe(styled, width="stretch")
332
+
333
+
334
+ def render_loan_visualizations_tab(
335
+ analysis: RefinanceAnalysis,
336
+ amortization_data: list[dict],
337
+ ) -> None:
338
+ """Render the loan visualization subtabs for charts and tables.
339
+
340
+ Args:
341
+ analysis: Analysis output for the current inputs.
342
+ amortization_data: Comparison schedule data.
343
+ """
344
+ st.subheader("Loan Visualizations")
345
+ chart_tab, amort_tab = st.tabs(["Chart", "Amortization"])
346
+
347
+ with chart_tab:
348
+ st.subheader("Cumulative Savings")
349
+ render_cumulative_chart(analysis)
350
+ st.subheader("Loan Balance Comparison")
351
+ render_balance_comparison_chart(amortization_data)
352
+
353
+ with amort_tab:
354
+ st.subheader("Amortization Comparison")
355
+ if not amortization_data:
356
+ st.info("Amortization data will appear after running the calculator.")
357
+ return
358
+
359
+ amort_df = pd.DataFrame(amortization_data)
360
+ display_df = amort_df.rename(
361
+ columns={
362
+ "year": "Year",
363
+ "current_principal": "Current Principal",
364
+ "current_interest": "Current Interest",
365
+ "current_balance": "Current Balance",
366
+ "new_principal": "New Principal",
367
+ "new_interest": "New Interest",
368
+ "new_balance": "New Balance",
369
+ "principal_diff": "Principal Δ",
370
+ "interest_diff": "Interest Δ",
371
+ "balance_diff": "Balance Δ",
372
+ },
373
+ )
374
+ display_df = display_df.reset_index(drop=True)
375
+
376
+ primary_columns = [
377
+ "Current Principal",
378
+ "Current Interest",
379
+ "Current Balance",
380
+ "New Principal",
381
+ "New Interest",
382
+ "New Balance",
383
+ ]
384
+ delta_columns = ["Principal Δ", "Interest Δ", "Balance Δ"]
385
+
386
+ styler = Styler(display_df)
387
+ styled = styler.format("${:,.0f}", subset=primary_columns).format(
388
+ "${:+,.0f}",
389
+ subset=delta_columns,
390
+ )
391
+ styled = cast(Styler, styled)
392
+ styled = styled.applymap(_interest_delta_style, subset=["Interest Δ"])
393
+ st.dataframe(styled, width="stretch")
394
+
395
+
396
+ def render_options_tab(inputs: CalculatorInputs) -> None:
397
+ """Render controls that affect chart and sensitivity behavior.
398
+
399
+ Args:
400
+ inputs: Inputs used to drive the scenario.
401
+ """
402
+ st.subheader("Application Options")
403
+ st.number_input(
404
+ "Chart Horizon (years)",
405
+ min_value=1,
406
+ max_value=30,
407
+ step=1,
408
+ value=int(st.session_state["chart_horizon_years"]),
409
+ key="chart_horizon_years",
410
+ )
411
+ st.number_input(
412
+ "Max Rate Reduction (%)",
413
+ min_value=0.0,
414
+ max_value=5.0,
415
+ step=0.1,
416
+ value=float(st.session_state["sensitivity_max_reduction"]),
417
+ key="sensitivity_max_reduction",
418
+ )
419
+ st.number_input(
420
+ "Rate Step (%)",
421
+ min_value=0.01,
422
+ max_value=1.0,
423
+ step=0.01,
424
+ value=float(st.session_state["sensitivity_step"]),
425
+ key="sensitivity_step",
426
+ )
427
+ st.caption(
428
+ "Adjust settings here to explore chart horizons and sensitivity detail; changes "
429
+ "take effect on the next calculation.",
430
+ )
431
+
432
+ st.divider()
433
+
434
+ st.subheader("Active Parameters")
435
+ cols = st.columns(3)
436
+ cols[0].metric("Opportunity Rate", f"{inputs.opportunity_rate:.2f}%")
437
+ cols[1].metric("Marginal Tax Rate", f"{inputs.marginal_tax_rate:.2f}%")
438
+ cols[2].metric("NPV Window", f"{inputs.npv_window_years} years")
439
+
440
+
441
+ logger.debug("Results rendering module initialized.")
442
+
443
+ __all__ = [
444
+ "render_results",
445
+ "render_analysis_tab",
446
+ "render_loan_visualizations_tab",
447
+ "render_options_tab",
448
+ "render_cumulative_chart",
449
+ "build_sensitivity_display",
450
+ "build_holding_display",
451
+ ]
452
+
453
+ __description__ = """
454
+ Functions responsible for rendering calculator results, tables, and visuals.
455
+ """
@@ -0,0 +1,22 @@
1
+ """Helper that launches the Streamlit web placeholder through the Streamlit CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from streamlit.web import cli as stcli
9
+
10
+
11
+ def main() -> None:
12
+ """Run the Streamlit app via the CLI to ensure a proper ScriptRunContext."""
13
+ script_path = Path(__file__).resolve().parent / "app.py"
14
+ sys.argv = ["streamlit", "run", str(script_path)]
15
+ stcli.main()
16
+
17
+
18
+ __all__ = ["main"]
19
+
20
+ __description__ = """
21
+ Entrypoint that runs the refinance calculator Streamlit app with the official CLI.
22
+ """
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: refi-calculator
3
+ Version: 0.8.0
4
+ Summary: Refi Calculator Package.
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE.txt
7
+ Keywords: Refi Calculator
8
+ Author: Jordan Pflum
9
+ Author-email: jordandpflum@gmail.com
10
+ Maintainer: Jordan Pflum
11
+ Maintainer-email: jordandpflum@gmail.com
12
+ Requires-Python: >=3.13, <3.14
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Provides-Extra: gui
16
+ Provides-Extra: web
17
+ Requires-Dist: plotly (>=5.18.0,<6.0.0) ; extra == "web"
18
+ Requires-Dist: streamlit (>=1.28.0,<2.0.0) ; extra == "web"
19
+ Project-URL: Homepage, https://github.com/jordandpflum/refi-calculator
20
+ Project-URL: Repository, https://github.com/jordandpflum/refi-calculator
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Refi-Calculator
24
+
25
+ ![CI](https://github.com/jordandpflum/refi-calculator/actions/workflows/ci.yml/badge.svg)
26
+ ![PyPI](https://img.shields.io/pypi/v/refi-calculator)
27
+ ![Python](https://img.shields.io/pypi/pyversions/refi-calculator)
28
+ ![License](https://img.shields.io/pypi/l/refi-calculator)
29
+ ![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)
30
+ ![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)
31
+ [![Docs](https://img.shields.io/badge/docs-MkDocs-blue)](https://jordandpflum.github.io/refi-calculator/)
32
+
33
+ Refi-Calculator helps borrowers analyze whether refinancing makes sense by comparing current and
34
+ proposed loan terms through breakeven, NPV, amortization, and holding-period views inside a Tkinter
35
+ GUI.
36
+
37
+ ## Features
38
+
39
+ - Side-by-side analysis of current vs. proposed mortgage payments (nominal and after-tax)
40
+ - Breakeven and holding-period tables with clear recommendations
41
+ - Accelerated payoff planning for maintaining current payment levels
42
+ - Loan Visualizations tab with the amortization comparison table (including cumulative interest Δ)
43
+ plus the refreshed cumulative savings chart (ticks, zero-line label) and a new balance comparison
44
+ chart for current vs. new loans
45
+ - Exportable CSV data for every tab
46
+ - Live market tab (when `FRED_API_KEY` is configured) surfaces historical 30-year fixed
47
+ rates and pre-fills the refinance rate so you start with a realistic benchmark.
48
+ - Interactive history chart in the Market tab plus the value table below it with 30Y/15Y
49
+ toggles for quick comparisons.
50
+
51
+ ## Getting Started
52
+
53
+ ### Requirements
54
+
55
+ - Python 3.13+
56
+ - Poetry for dependency management (the virtual environment lives in `.venv/`)
57
+
58
+ ### Installation
59
+
60
+ ```bash
61
+ poetry install
62
+ ```
63
+
64
+ This command creates the virtual environment, installs dependencies, and prepares the hooks defined
65
+ in `.pre-commit-config.yaml`.
66
+
67
+ If you only need the GUI experience, install with `pip install .[gui]`; to enable the Streamlit
68
+ placeholder or eventual web UI, install `pip install .[web]`.
69
+
70
+ For a global CLI install, use `pipx` once the package is published, or run a local install with:
71
+
72
+ ```bash
73
+ pipx install refi-calculator
74
+ # or (for development)
75
+ pip install -e .
76
+ ```
77
+
78
+ ### Running the GUI
79
+
80
+ ```bash
81
+ poetry run refi-calculator
82
+ ```
83
+
84
+ The `refi-calculator` console script now launches the Tkinter application exposed via
85
+ `refi_calculator.gui.app:main`, so you can start the GUI with `poetry run refi-calculator` or via
86
+ `pipx` once the package is installed globally.
87
+
88
+ ### Running the Web placeholder
89
+
90
+ After installing the web optional dependencies (`pip install .[web]`), run `poetry run
91
+ refi-calculator-web` to launch the Streamlit placeholder. That console script now invokes the
92
+ Streamlit CLI (`streamlit run .../src/refi_calculator/web/app.py`) so you get the normal script
93
+ context and browser experience while the full Streamlit workflow is still under construction.
94
+
95
+ ## Testing & Quality
96
+
97
+ - `poetry run pre-commit run --all-files` (runs formatting, linting, safety hooks, etc.)
98
+ - `poetry run pytest` (not yet populated, but ready for future tests)
99
+
100
+ ### Market Data Tab
101
+
102
+ - Set the `FRED_API_KEY` environment variable to pull historical 30-year fixed-rate data when
103
+ you launch the app. The Market tab will populate a refreshable table and update the default
104
+ refinance rate using the latest observation. If the key is missing or the FRED request fails,
105
+ the tab keeps working but reports the reason for the skip.
106
+ - Use the selectors inside the Market section to start with one year of history or extend the view
107
+ to two years, five years, or all available data. The chart overlays both tenors with a legend,
108
+ and the table below shows one column per tenor for the selected time window.
109
+
110
+ Add new tests under `tests/` following the `test_*.py` pattern whenever you enhance functionality.
111
+
112
+ ## Hosted Streamlit Preview
113
+
114
+ - Visit
115
+ [https://refi-calculator.streamlit.app/](https://refi-calculator.streamlit.app/)
116
+ for a hosted version of the Streamlit placeholder
117
+ so collaborators can explore the experience without installing dependencies locally.
118
+
119
+ ## Documentation
120
+
121
+ - The MkDocs site at
122
+ [https://jordandpflum.github.io/refi-calculator/](https://jordandpflum.github.io/refi-calculator/)
123
+ serves the full guide, API reference, and autogenerated content for the project.
124
+
125
+ ## Code Structure
126
+
127
+ - `src/refi_calculator/core/`: Shared calculations, models, and utility helpers used by every
128
+ interface.
129
+ - `src/refi_calculator/gui/`: Tkinter GUI composed of:
130
+ - `app.py`: The main application wiring.
131
+ - `chart.py`: Custom savings chart canvas relying on shared helpers.
132
+ - `builders/`: Tab-specific builders and UI helpers.
133
+ - `src/refi_calculator/web/`: Streamlit placeholder (future web UI) exposing `main()`.
134
+ - `src/refi_calculator/cli.py`: CLI launcher exposed via the `refi-calculator` console script.
135
+ - `bin/refi-calculator.py`: Entry point that runs the GUI.
136
+
137
+ ## Contributing
138
+
139
+ - Keep imports grouped by functionality (allow isort to do the heavy lifting through pre-commit).
140
+ - Update or add tests in `tests/` before opening a pull request.
141
+ - Run `poetry run pre-commit run --all-files` to catch formatting/lint issues early.
142
+
143
+ ## License
144
+
145
+ See [LICENSE.txt](LICENSE.txt) for licensing details.
146
+
@@ -0,0 +1,35 @@
1
+ refi_calculator/__init__.py,sha256=swftpQTQFYi8EeELF2r1Hd0GNHiPRmqAnosEmNXLTzY,176
2
+ refi_calculator/cli.py,sha256=LtqSF2KFUPQgt6-SUhIZk3atobhTKMMaMwqP26uHvLk,1676
3
+ refi_calculator/core/__init__.py,sha256=-bB0f8V_0tx_Jik7XohAw17k5S0cuKryKHYG_oxvRRA,1033
4
+ refi_calculator/core/calculations.py,sha256=cs9FvjaenmxmPHimEzJF1Nm1SECGSwbR3T65n8AFSZg,25818
5
+ refi_calculator/core/charts.py,sha256=mmpZHytx_7LJTIR8V839xeJnSQEBym09rNu1A02qi4M,1983
6
+ refi_calculator/core/market/__init__.py,sha256=1BqFg-2tZtlC_3Kx_j8yg4AXjxS8UqknwXyWRVB1Nc4,256
7
+ refi_calculator/core/market/constants.py,sha256=KXX3RAqy1MW52CP556kBR3jgo1FS401b7RNMbwFToIU,549
8
+ refi_calculator/core/market/fred.py,sha256=GAWWC456C7pWVNf2Ujsy6mHFknB50xNrwqFUDH4u33k,1734
9
+ refi_calculator/core/models.py,sha256=2LzYxWMWh66mW4f8_B0mjgz9YqiyJyxmILcU-VFcHw0,4377
10
+ refi_calculator/environment.py,sha256=4xkCLnWE2XkhkIgeAH-APY-l35ZaJ3jtEy0UWQfBdcs,3170
11
+ refi_calculator/gui/__init__.py,sha256=KIo8VUdVZLR_2P4Ln2kXjR7i_pDBvLW4c482Ck1C0C4,343
12
+ refi_calculator/gui/app.py,sha256=g2sKiSoYrP5w--g86R88X09aK8xnMYZqX1HPS0Oke5Y,41094
13
+ refi_calculator/gui/builders/__init__.py,sha256=CzVl2fXVWftWiii8ywWLb_bTH8ig9RQekbiWePQFKV0,161
14
+ refi_calculator/gui/builders/analysis_tab.py,sha256=-s-DI3htYSqQlBeTN3mXxO4O9WcMnTLjrRnsDMEZuwI,2929
15
+ refi_calculator/gui/builders/helpers.py,sha256=qbAYF10bXulY3qr0NmzIjjkn877BAelw_c5PfZ6Rxm0,2577
16
+ refi_calculator/gui/builders/info_tab.py,sha256=FJAhoDElSd6kfKZ_gxI04xQXolshnPPJ7EaKcgwQlkc,11598
17
+ refi_calculator/gui/builders/main_tab.py,sha256=eSNLEDpmOz2P7paezTKptUSwTAV_zTueW7R2GDYj_bM,6811
18
+ refi_calculator/gui/builders/market_tab.py,sha256=azT36DknQ8TuPaEznuHEfEx4T6UBl3r2KBRBm6qWc-k,3514
19
+ refi_calculator/gui/builders/options_tab.py,sha256=XI4pWZKNm72s8P9rdTZkujYFQfZCv_s0ldQaM5ZhrT8,2064
20
+ refi_calculator/gui/builders/visuals_tab.py,sha256=4oyfvP6XcBFFsmmH3erULuiscWf5MWmr8GkZ42B9aC4,4251
21
+ refi_calculator/gui/chart.py,sha256=M4ftSAQ4a8eJEOkbGY3omgaUOjMyFF_Epi8Awl7f6gE,13551
22
+ refi_calculator/gui/market_chart.py,sha256=ZusT6P89cubrQgxU_kpfp8ADDz65VW3SFQrH6-zkHGE,6034
23
+ refi_calculator/web/__init__.py,sha256=hzSN3XkBBC7iasNsQpaYEVZPNmqRGqxWwlf_SUXCVws,228
24
+ refi_calculator/web/app.py,sha256=VVoA2aHWkaxBlKcBf2rKSHRMk4fe4VcJDm9G6nAd-mM,3200
25
+ refi_calculator/web/calculator.py,sha256=IgOttPDASbGuTI6SJ-8WxE0IFmBt2rH9sRTyU-fsI0s,10168
26
+ refi_calculator/web/formatting.py,sha256=eWlD5Oi8DjGydWLbZfRFwE9nG-k10t9n4AW-p3SNqDI,2137
27
+ refi_calculator/web/info.py,sha256=_XaLtfWqgg2wZPCRRw9tZGfTb8cAVzyfMhaJ7wVP9PQ,10412
28
+ refi_calculator/web/market.py,sha256=5wBvqTDZ3pfzqfIZf2YCkoH3MDIRvU1LFV9f_pgbD_0,8043
29
+ refi_calculator/web/results.py,sha256=c6dASP-ctf8ZK08Q2VuJmmz-OXTuVERzjZVTdPBJNtk,13723
30
+ refi_calculator/web/runner.py,sha256=safoEi95oeH5lKTz83EbH30GH7edrc0ekjulwBsXjSU,563
31
+ refi_calculator-0.8.0.dist-info/METADATA,sha256=NKFljBWMRa1meRgv4vw0ZExHTK0exOr-7rGgoY13GJQ,6083
32
+ refi_calculator-0.8.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
33
+ refi_calculator-0.8.0.dist-info/entry_points.txt,sha256=6pZr90j6A0seCr9_zJ2D4Pzh8kwPS_-X-4UErhHpNAc,116
34
+ refi_calculator-0.8.0.dist-info/licenses/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
35
+ refi_calculator-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ refi-calculator=refi_calculator.gui.app:main
3
+ refi-calculator-web=refi_calculator.web.runner:main
4
+