Spaces:
Sleeping
Sleeping
| 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 | |
| # ------------------------------------------------------------ | |
| 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() | |