import math from dataclasses import dataclass import numpy as np import pandas as pd import matplotlib.pyplot as plt import gradio as gr # ------------------------------------------------------------ # Modellparametre og beregninger # ------------------------------------------------------------ @dataclass class Params: P: float # Beløp: engangsinnskudd ELLER månedsbeløp (avhenger av monthly) inflation: float # årlig inflasjon (desimal) N: int # horisont N (år) for tabeller og grafer monthly: bool # True = månedlig; False = engangsbeløp buf_rate: float # bufferspar nominell rente p.a. tax_capital: float # skatt på bankrente loan_rate_eff: float # effektiv lånerente p.a. tax_interest_ded: float # rentefradrag (andel) ask_return: float # ASK nominell avkastning p.a. shield_rate: float # skjermingsrente p.a. tax_stock_eff: float # effektiv aksjeskatt på gevinst ved uttak wealth_tax_rate: float # formuesskatt p.a. disc_stock: float # verdsettingsrabatt aksjer disc_bank: float # verdsettingsrabatt bank def fv_lump(principal: float, r: float, t_years: float) -> float: return principal * ((1 + r) ** t_years) def fv_annuity(pmt_month: float, r_year: float, t_years: float) -> float: """FV av månedlig sparing ved månedsslutt.""" n = int(round(12 * t_years)) if n <= 0: return 0.0 i = r_year / 12.0 if abs(i) < 1e-12: return pmt_month * n return pmt_month * (((1 + i) ** n - 1) / i) def shielding_lump(principal: float, s: float, t_years: float) -> float: """Akkumulert skjerming for engangsinnskudd i t år.""" return principal * ((1 + s) ** t_years - 1.0) def shielding_monthly(pmt_month: float, s: float, t_years: float) -> float: """Presis skjerming per månedsinnskudd — hver betaling skjermes etter faktisk holdetid.""" n = int(round(12 * t_years)) if n <= 0: return 0.0 acc = 0.0 for m in range(1, n + 1): months_left = n - m + 1 years_left = months_left / 12.0 acc += pmt_month * ((1 + s) ** years_left - 1.0) return acc def real_value(nominal: float, inflation: float, t_years: float) -> float: return nominal / ((1 + inflation) ** t_years) def ann_real_rate_lump(real_end: float, P: float, t_years: int) -> float: if t_years <= 0 or P <= 0: return 0.0 return (real_end / P) ** (1.0 / t_years) - 1.0 def ann_real_rate_monthly(real_end: float, PMT: float, t_years: int) -> float: """ Løs årlig realrente r fra real_end = PMT * [((1+r/12)^(12t) - 1) / (r/12)]. Newton-Raphson med sikring. """ if t_years <= 0 or PMT <= 0: return 0.0 n = 12 * t_years # Startgjetning: enkel ratio guess = 0.03 r = guess for _ in range(100): i = r / 12.0 if abs(i) < 1e-12: i = 1e-12 f = PMT * (((1 + i) ** n - 1) / i) - real_end # derivert: d/d r av ovenstående # d/d i først: A = (1 + i) ** n df_di = PMT * (( (n * (1 + i) ** (n - 1)) * i - (A - 1) ) / (i ** 2)) df_dr = df_di / 12.0 if abs(df_dr) < 1e-16: break step = f / df_dr r_new = r - step # dempning for stabilitet if abs(r_new) > 1.0: r_new = np.sign(r_new) * 1.0 if abs(r_new - r) < 1e-10: r = r_new break r = r_new return float(r) def compute_series(par: Params, max_years: int = 50) -> pd.DataFrame: rows = [] r_buf_after_tax = par.buf_rate * (1 - par.tax_capital) r_loan_after_tax = par.loan_rate_eff * (1 - par.tax_interest_ded) for t in range(0, max_years + 1): # Bufferspar nom_buf = fv_annuity(par.P, r_buf_after_tax, t) if par.monthly else fv_lump(par.P, r_buf_after_tax, t) # Nedbetaling (sikker avkastning lik lånerente etter skatt) nom_loan = fv_annuity(par.P, r_loan_after_tax, t) if par.monthly else fv_lump(par.P, r_loan_after_tax, t) # ASK if par.monthly: fv = fv_annuity(par.P, par.ask_return, t) contrib = par.P * (12 * t) shield = shielding_monthly(par.P, par.shield_rate, t) taxbase = max(0.0, fv - contrib - shield) tax = par.tax_stock_eff * taxbase nom_ask = fv - tax else: fv = fv_lump(par.P, par.ask_return, t) shield = shielding_lump(par.P, par.shield_rate, t) taxbase = max(0.0, fv - par.P - shield) tax = par.tax_stock_eff * taxbase nom_ask = fv - tax # Realverdier real_buf = real_value(nom_buf, par.inflation, t) real_loan = real_value(nom_loan, par.inflation, t) real_ask = real_value(nom_ask, par.inflation, t) # Formuesskatt (enkel pr-år beregning) wealth_buf = nom_buf * (1 - par.disc_bank) * par.wealth_tax_rate wealth_ask = nom_ask * (1 - par.disc_stock) * par.wealth_tax_rate wealth_loan = nom_loan * par.wealth_tax_rate # Likviditet liq_buf = nom_buf liq_loan = 0.0 liq_ask = nom_ask rows.append(dict( År=t, Nom_Bufferspar=nom_buf, Nom_Nedbetaling=nom_loan, Nom_ASK=nom_ask, Real_Bufferspar=real_buf, Real_Nedbetaling=real_loan, Real_ASK=real_ask, WealthTax_Buf=wealth_buf, WealthTax_Loan=wealth_loan, WealthTax_ASK=wealth_ask, Likviditet_Buf=liq_buf, Likviditet_Nedbetaling=liq_loan, Likviditet_ASK=liq_ask )) return pd.DataFrame(rows) def build_small_table(df: pd.DataFrame, par: Params, t: int) -> pd.DataFrame: """Liten tabell for én horisont t med korrekt annuitetsr for månedlig.""" row = df.loc[df["År"] == t].iloc[0] out_rows = [] # Bufferspar if par.monthly: r_ann = ann_real_rate_monthly(row["Real_Bufferspar"], par.P, t) else: r_ann = ann_real_rate_lump(row["Real_Bufferspar"], par.P, t) out_rows.append({ "Metode": "Bufferspar (etter skatt)", "Sluttsum nominelt": round(row["Nom_Bufferspar"], 2), "Sluttsum real": round(row["Real_Bufferspar"], 2), "Årlig realavkastning etter skatt": round(r_ann, 4) }) # Nedbetaling if par.monthly: r_ann = ann_real_rate_monthly(row["Real_Nedbetaling"], par.P, t) else: r_ann = ann_real_rate_lump(row["Real_Nedbetaling"], par.P, t) out_rows.append({ "Metode": "Ekstra nedbetaling (etter skatt)", "Sluttsum nominelt": round(row["Nom_Nedbetaling"], 2), "Sluttsum real": round(row["Real_Nedbetaling"], 2), "Årlig realavkastning etter skatt": round(r_ann, 4) }) # ASK if par.monthly: r_ann = ann_real_rate_monthly(row["Real_ASK"], par.P, t) else: r_ann = ann_real_rate_lump(row["Real_ASK"], par.P, t) out_rows.append({ "Metode": "ASK (m/skjermingsfradrag)", "Sluttsum nominelt": round(row["Nom_ASK"], 2), "Sluttsum real": round(row["Real_ASK"], 2), "Årlig realavkastning etter skatt": round(r_ann, 4) }) return pd.DataFrame(out_rows) def make_line_plot(df: pd.DataFrame, cols: list, title: str, ylabel: str): fig, ax = plt.subplots(figsize=(8, 4)) for c in cols: ax.plot(df["År"], df[c], label=c.replace("_", " ")) ax.set_title(title) ax.set_xlabel("År") ax.set_ylabel(ylabel) ax.legend() ax.grid(True, alpha=0.3) fig.tight_layout() return fig INTRO_MD = """ # Sammenligning av sparemetoder (interaktiv) - **Bufferspar** (bankkonto) — nominell rente og 22 % skatt → etter-skatt rente brukes direkte. - **Ekstra nedbetaling** — “sikker avkastning” lik lånerente etter fradrag. - **ASK** — skatt ved uttak på gevinst utover **akkumulert skjerming**. Månedsparing beregner **presis skjerming per innskudd**. - **Realverdi** = deflatert med inflasjon. Vi viser også **formuesskatt** (estimat) og **likviditet** over tid. I resultatpanelet ser du små tabeller for **3, 5, 10 og N år** (hver i egen tab) med *sluttsum nominelt*, *sluttsum real* og **årlig realavkastning etter skatt**. For **månedlig sparing** løses årlig realrente korrekt fra annuitetsformelen. """ def run_calc(P, inflation, N, monthly, buf_rate, tax_capital, loan_rate_eff, tax_interest_ded, ask_return, shield_rate, tax_stock_eff, wealth_tax_rate, disc_stock, disc_bank): par = Params( P=float(P), inflation=float(inflation), N=int(N), monthly=bool(monthly), buf_rate=float(buf_rate), tax_capital=float(tax_capital), loan_rate_eff=float(loan_rate_eff), tax_interest_ded=float(tax_interest_ded), ask_return=float(ask_return), shield_rate=float(shield_rate), tax_stock_eff=float(tax_stock_eff), wealth_tax_rate=float(wealth_tax_rate), disc_stock=float(disc_stock), disc_bank=float(disc_bank), ) df = compute_series(par, max_years=max(10, par.N)) # Små tabeller per horisont t3 = build_small_table(df, par, 3 if par.N != 3 else 3) t5 = build_small_table(df, par, 5 if par.N != 5 else 5) t10 = build_small_table(df, par, 10 if par.N != 10 else 10) tN = build_small_table(df, par, par.N) # Grafer fig_nom = make_line_plot( df, ["Nom_Bufferspar", "Nom_Nedbetaling", "Nom_ASK"], "Nominell verdi", "kr" ) fig_real = make_line_plot( df, ["Real_Bufferspar", "Real_Nedbetaling", "Real_ASK"], "Realverdi (inflasjonsjustert)", "kr (2025-kroner)" ) fig_tax = make_line_plot( df, ["WealthTax_Buf", "WealthTax_Loan", "WealthTax_ASK"], "Årlig formuesskatt (estimat)", "kr/år" ) fig_liq = make_line_plot( df, ["Likviditet_Buf", "Likviditet_Nedbetaling", "Likviditet_ASK"], "Likviditet (tilgjengelige midler)", "kr" ) return t3, t5, t10, tN, fig_nom, fig_real, fig_tax, fig_liq # ------------------------------------------------------------ # Gradio UI # ------------------------------------------------------------ with gr.Blocks(title="Sparemetoder – interaktiv") as demo: gr.Markdown(INTRO_MD) with gr.Row(): with gr.Column(scale=1): gr.Markdown("## Parametere") P = gr.Number(value=100_000, label="Beløp (engangsbeløp ELLER månedsbeløp)") monthly = gr.Checkbox(value=True, label="Månedlig sparing?") N = gr.Slider(3, 40, value=15, step=1, label="Horisont N (år)") inflation = gr.Slider(0.0, 0.06, value=0.025, step=0.001, label="Inflasjon p.a.") gr.Markdown("### Bank / lån") buf_rate = gr.Slider(0.00, 0.10, value=0.035, step=0.0005, label="Bufferspar nominell rente p.a.") tax_capital = gr.Slider(0.0, 0.5, value=0.22, step=0.01, label="Skatt på kapitalinntekt (bank)") loan_rate_eff = gr.Slider(0.0, 0.10, value=0.0487, step=0.0005, label="Lånerente effektiv p.a.") tax_interest_ded = gr.Slider(0.0, 0.5, value=0.22, step=0.01, label="Rentefradrag (andel)") gr.Markdown("### ASK") ask_return = gr.Slider(0.00, 0.30, value=0.08, step=0.001, label="ASK — forventet avkastning p.a.") shield_rate = gr.Slider(0.00, 0.10, value=0.03, step=0.001, label="Skjermingsrente p.a.") tax_stock_eff = gr.Slider(0.0, 0.6, value=0.3784, step=0.0001, label="Effektiv skatt aksjegevinst (ASK)") gr.Markdown("### Formuesskatt / verdsettingsrabatter") wealth_tax_rate = gr.Slider(0.00, 0.02, value=0.0085, step=0.0001, label="Formuesskatt p.a.") disc_stock = gr.Slider(0.0, 0.5, value=0.20, step=0.01, label="Verdsettingsrabatt aksjer") disc_bank = gr.Slider(0.0, 0.5, value=0.00, step=0.01, label="Verdsettingsrabatt bank") with gr.Column(scale=2): gr.Markdown("## Resultater") with gr.Tabs(): with gr.Tab("3 år"): t3 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"]) with gr.Tab("5 år"): t5 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"]) with gr.Tab("10 år"): t10 = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"]) with gr.Tab("N år"): tN = gr.Dataframe(wrap=True, column_widths=["30%", "20%", "20%", "20%"]) with gr.Tabs(): with gr.Tab("Nominell verdi"): img_nom = gr.Plot() with gr.Tab("Realverdi"): img_real = gr.Plot() with gr.Tab("Formuesskatt"): img_tax = gr.Plot() with gr.Tab("Likviditet"): img_liq = gr.Plot() inputs = [P, inflation, N, monthly, buf_rate, tax_capital, loan_rate_eff, tax_interest_ded, ask_return, shield_rate, tax_stock_eff, wealth_tax_rate, disc_stock, disc_bank] outputs = [t3, t5, t10, tN, img_nom, img_real, img_tax, img_liq] for comp in inputs: comp.change(fn=run_calc, inputs=inputs, outputs=outputs) gr.Button("Oppdater nå").click(fn=run_calc, inputs=inputs, outputs=outputs) if __name__ == "__main__": demo.launch()