Skip to content

Commit d8aab19

Browse files
committed
feat: occupational pension
1 parent 239912f commit d8aab19

3 files changed

Lines changed: 99 additions & 42 deletions

File tree

src/app.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<meta charset="utf-8" />
55
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
7-
<title>William Levén</title>
8-
<meta name="description" content="Personal landing page for William Levén" />
7+
<title>Ekonomisk Framtid</title>
8+
<meta name="description" content="Financial forecast of savings in Sweden." />
99
%sveltekit.head%
1010
</head>
1111
<body data-sveltekit-preload-data="hover">

src/lib/ForecastGraph.svelte

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@
66
export let simulation: Array<YearState>;
77
88
$: data = simulation.map(
9-
({ iskSavings, year, cashFlow, premiumPensionRights, incomePensionRights }) => ({
9+
({
10+
iskSavings,
11+
year,
12+
cashFlow,
13+
premiumPensionRights,
14+
incomePensionRights,
15+
occupationalPensionSavings
16+
}) => ({
1017
year: new Date(year, 1),
1118
savings: iskSavings,
1219
cashFlow,
13-
pensionRights: premiumPensionRights + incomePensionRights
20+
pensionRights: premiumPensionRights + incomePensionRights,
21+
pensionSavings: occupationalPensionSavings
1422
})
1523
);
1624
@@ -36,6 +44,11 @@
3644
.x((d) => xScale(d.year))
3745
.y((d) => yScale(d.savings));
3846
47+
$: pensionSavingsLine = d3
48+
.line<{ year: Date; pensionSavings: number }>()
49+
.x((d) => xScale(d.year))
50+
.y((d) => yScale(d.pensionSavings));
51+
3952
$: pensionRightsLine = d3
4053
.line<{ year: Date; pensionRights: number }>()
4154
.x((d) => xScale(d.year))
@@ -81,6 +94,7 @@
8194
<path d={savingsLine(data)} fill="none" stroke="steelblue" stroke-width="1.5" />
8295
<path d={cashFlowLine(data)} fill="none" stroke="green" stroke-width="1.5" />
8396
<path d={pensionRightsLine(data)} fill="none" stroke="red" stroke-width="1.5" />
97+
<path d={pensionSavingsLine(data)} fill="none" stroke="blue" stroke-width="1.5" />
8498
<path d={zeroLine(data)} fill="none" stroke="grey" stroke-width="0.5" />
8599
</svg>
86100

src/lib/simulation.ts

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface YearState {
1515
cashFlow: number;
1616
incomePensionRights: number;
1717
premiumPensionRights: number;
18-
//servicePensionSavings: number;
18+
occupationalPensionSavings: number;
1919
}
2020

2121
export interface Parameters {
@@ -36,16 +36,27 @@ export interface Parameters {
3636
premiumPensionRate: number;
3737
statePensionAge: number;
3838
generalLifeExpectancy: number;
39-
39+
occupationalPensionInterestRate: number;
40+
occupationalPensionAge: number;
41+
occupationalPensionDuration: number;
4042
}
4143

42-
export interface TimeBoundCashFlow {
44+
export type TimeBoundCashFlow = GrossTaxableEarnedIncome | NetExpense;
45+
46+
interface BaseTimeBoundCashFlow {
4347
startYear: number; // Inclusive
4448
endYear?: number; // Exclusive
4549
yearlyCashFlow: number;
46-
kind: 'GrossTaxableEarnedIncome' | 'NetExpense';
47-
publicPensionContributing: boolean; // Only used for GrossTaxableEarnedIncome
48-
//servicePensionContribution: number;
50+
}
51+
52+
interface GrossTaxableEarnedIncome extends BaseTimeBoundCashFlow {
53+
kind: 'GrossTaxableEarnedIncome';
54+
publicPensionContributing: boolean;
55+
occupationalPensionContributionRate: number;
56+
}
57+
58+
interface NetExpense extends BaseTimeBoundCashFlow {
59+
kind: 'NetExpense';
4960
}
5061

5162
export interface Scenario {
@@ -74,23 +85,23 @@ export function defaultScenario(): Scenario {
7485
premiumPensionRate: 0.074,
7586
statePensionAge: 70,
7687
generalLifeExpectancy: 80,
88+
occupationalPensionInterestRate: 0.07,
89+
occupationalPensionAge: 65,
90+
occupationalPensionDuration: 10,
7791
cashFlows: [
7892
{
7993
startYear: currentYear,
80-
endYear: currentYear + 9,
94+
endYear: currentYear + 8,
8195
yearlyCashFlow: 52000 * 12,
8296
kind: 'GrossTaxableEarnedIncome',
83-
publicPensionContributing: true
84-
// servicePensionContribution: 4.5
97+
publicPensionContributing: true,
98+
occupationalPensionContributionRate: 0.045
8599
},
86100
{
87101
startYear: currentYear,
88102
endYear: undefined,
89103
yearlyCashFlow: 15000 * 12,
90-
kind: 'NetExpense',
91-
publicPensionContributing: false
92-
// publicPensionContributing: false,
93-
// servicePensionContribution: 0
104+
kind: 'NetExpense'
94105
}
95106
]
96107
},
@@ -101,7 +112,8 @@ export function defaultScenario(): Scenario {
101112
compoundedInflation: 1,
102113
cashFlow: 0,
103114
premiumPensionRights: 0,
104-
incomePensionRights: 0
115+
incomePensionRights: 0,
116+
occupationalPensionSavings: 0
105117
},
106118
endYear: 2080
107119
};
@@ -126,11 +138,12 @@ function simulateYear(state: YearState, params: Parameters): YearState {
126138

127139
const yearlyGrossPublicPensionContributingIncomePostInflation =
128140
Math.min(
129-
yearlyCashFlows
130-
.filter(
131-
({ kind, publicPensionContributing }) =>
132-
kind === 'GrossTaxableEarnedIncome' && publicPensionContributing
133-
)
141+
(
142+
yearlyCashFlows.filter(
143+
({ kind }) => kind === 'GrossTaxableEarnedIncome'
144+
) as GrossTaxableEarnedIncome[]
145+
)
146+
.filter(({ publicPensionContributing }) => publicPensionContributing)
134147
.reduce((acc, cf) => acc + cf.yearlyCashFlow, 0),
135148
params.publicPensionContributionThreshold
136149
) * compoundedInflationYearlyAverage;
@@ -141,14 +154,18 @@ function simulateYear(state: YearState, params: Parameters): YearState {
141154
yearlyGrossPublicPensionContributingIncomePostInflation * params.premiumPensionContributionRate;
142155

143156
// Simplification: No new pension rights after pension age
144-
const incomePensionRights = newAge <= params.statePensionAge ?
145-
(state.incomePensionRights + incomePensionContributionPostInflation / 2) *
146-
(1 + params.incomePensionRate) +
147-
incomePensionContributionPostInflation / 2 : state.incomePensionRights;
148-
const premiumPensionRights = newAge <= params.statePensionAge ?
149-
(state.premiumPensionRights + premiumPensionContributionPostInflation / 2) *
150-
(1 + params.premiumPensionRate) +
151-
premiumPensionContributionPostInflation / 2 : state.premiumPensionRights;
157+
const incomePensionRights =
158+
newAge <= params.statePensionAge
159+
? (state.incomePensionRights + incomePensionContributionPostInflation / 2) *
160+
(1 + params.incomePensionRate) +
161+
incomePensionContributionPostInflation / 2
162+
: state.incomePensionRights;
163+
const premiumPensionRights =
164+
newAge <= params.statePensionAge
165+
? (state.premiumPensionRights + premiumPensionContributionPostInflation / 2) *
166+
(1 + params.premiumPensionRate) +
167+
premiumPensionContributionPostInflation / 2
168+
: state.premiumPensionRights;
152169

153170
// Simplification: arvsvinst: https://www.pensionsmyndigheten.se/statistik/publikationer/orange-rapport-2021/a-berakningsfaktorer.html
154171
// Current we instead simulate arvsvinst by pretending that the pension is paid out based on life expectancy
@@ -159,14 +176,46 @@ function simulateYear(state: YearState, params: Parameters): YearState {
159176
// api grundavdrag: https://skatteverket.entryscape.net/rowstore/dataset/ebbd8d70-9b9c-4327-b2ce-a371ee66744c/html
160177

161178
// Simplification: state pension payout is taxed as normal income
162-
const accumulatedPensionRights = state.incomePensionRights + state.premiumPensionRights;
163-
const grossPensionPayout = newAge > params.statePensionAge ? accumulatedPensionRights / (params.generalLifeExpectancy - params.statePensionAge): 0;
179+
const accumulatedPublicPensionRights = state.incomePensionRights + state.premiumPensionRights;
180+
const grossPublicPensionPayout =
181+
newAge > params.statePensionAge
182+
? accumulatedPublicPensionRights / (params.generalLifeExpectancy - params.statePensionAge)
183+
: 0;
164184

185+
const yearlyGrossOccupationalPensionContributionPostInflation =
186+
(
187+
yearlyCashFlows.filter(
188+
({ kind }) => kind === 'GrossTaxableEarnedIncome'
189+
) as GrossTaxableEarnedIncome[]
190+
).reduce((acc, cf) => acc + cf.yearlyCashFlow * cf.occupationalPensionContributionRate, 0) *
191+
compoundedInflationYearlyAverage;
192+
193+
const grossOccupationalPensionPayout =
194+
state.age >= params.occupationalPensionAge &&
195+
state.age < params.occupationalPensionAge + params.occupationalPensionDuration
196+
? state.occupationalPensionSavings *
197+
(1 / (params.occupationalPensionDuration + params.occupationalPensionAge - state.age))
198+
: 0;
199+
200+
const accumulatedOccupationalPensionInterest =
201+
(state.occupationalPensionSavings +
202+
(yearlyGrossOccupationalPensionContributionPostInflation - grossOccupationalPensionPayout) /
203+
2) *
204+
params.occupationalPensionInterestRate;
205+
206+
const accumulatedOccupationalPensionSavings =
207+
state.occupationalPensionSavings +
208+
yearlyGrossOccupationalPensionContributionPostInflation +
209+
accumulatedOccupationalPensionInterest -
210+
grossOccupationalPensionPayout;
165211

166212
const yearlyGrossTaxableEarnedIncomePostInflation =
167213
yearlyCashFlows
168214
.filter(({ kind }) => kind === 'GrossTaxableEarnedIncome')
169-
.reduce((acc, cf) => acc + cf.yearlyCashFlow, 0) * compoundedInflationYearlyAverage + grossPensionPayout;
215+
.reduce((acc, cf) => acc + cf.yearlyCashFlow, 0) *
216+
compoundedInflationYearlyAverage +
217+
grossPublicPensionPayout +
218+
grossOccupationalPensionPayout;
170219

171220
const thisYearCountyTaxBracketRate =
172221
params.countyTaxRate + (params.churchTax ? params.churchTaxRate : 0);
@@ -196,12 +245,6 @@ function simulateYear(state: YearState, params: Parameters): YearState {
196245
const yearlyNetCashFlowPostInflation =
197246
yearlyNetIncomePostInflation - yearlyNetExpensePostInflation;
198247

199-
// const publicPensionContributingCashFlows = yearlyCashFlows.filter(cf => cf.publicPensionContributing);
200-
// const publicPensionContributingCashFlow = publicPensionContributingCashFlows.reduce((acc, cf) => acc + cf.yearlyNetCashFlow, 0);
201-
202-
// const yearlyIncomePensionRights = publicPensionContributingCashFlow * 0.16; // TODO: This should be calculated
203-
// const yearlyPremiumPensionRights = publicPensionContributingCashFlow * 0.025;
204-
205248
const iskSavingsYearlyAverage = state.iskSavings + yearlyNetCashFlowPostInflation / 2;
206249
const iskInterest = iskSavingsYearlyAverage * params.iskInterestRate;
207250
const iskTax = iskSavingsYearlyAverage * params.iskTax;
@@ -214,8 +257,8 @@ function simulateYear(state: YearState, params: Parameters): YearState {
214257
compoundedInflation: compoundedInflation,
215258
cashFlow: yearlyNetCashFlowPostInflation,
216259
incomePensionRights: incomePensionRights,
217-
premiumPensionRights: premiumPensionRights
218-
// servicePensionSavings: state.servicePensionSavings,
260+
premiumPensionRights: premiumPensionRights,
261+
occupationalPensionSavings: accumulatedOccupationalPensionSavings
219262
};
220263
}
221264

0 commit comments

Comments
 (0)