Skip to content

Commit 6f06c93

Browse files
committed
refactor: break out simulations
1 parent d8aab19 commit 6f06c93

1 file changed

Lines changed: 185 additions & 85 deletions

File tree

src/lib/simulation.ts

Lines changed: 185 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ export function defaultScenario(): Scenario {
120120
}
121121

122122
function simulateYear(state: YearState, params: Parameters): YearState {
123-
const newYear = state.year + 1;
124-
const newAge = state.age + 1;
123+
const year = state.year + 1;
124+
const age = state.age + 1;
125125

126126
const compoundedInflation = state.compoundedInflation * (1 + params.inflationRate);
127127
const compoundedInflationYearlyAverage =
@@ -131,83 +131,48 @@ function simulateYear(state: YearState, params: Parameters): YearState {
131131
const thisYearsTaxFreeThreshold = params.taxFreeThreshold * compoundedInflationYearlyAverage;
132132

133133
const yearlyCashFlows = params.cashFlows.filter(({ startYear, endYear }) => {
134-
if (startYear > newYear) return false;
134+
if (startYear > year) return false;
135135
if (endYear === undefined) return true;
136-
return endYear > newYear;
136+
return endYear > year;
137137
});
138138

139-
const yearlyGrossPublicPensionContributingIncomePostInflation =
140-
Math.min(
141-
(
142-
yearlyCashFlows.filter(
143-
({ kind }) => kind === 'GrossTaxableEarnedIncome'
144-
) as GrossTaxableEarnedIncome[]
145-
)
146-
.filter(({ publicPensionContributing }) => publicPensionContributing)
147-
.reduce((acc, cf) => acc + cf.yearlyCashFlow, 0),
148-
params.publicPensionContributionThreshold
149-
) * compoundedInflationYearlyAverage;
150-
151-
const incomePensionContributionPostInflation =
152-
yearlyGrossPublicPensionContributingIncomePostInflation * params.incomePensionContributionRate;
153-
const premiumPensionContributionPostInflation =
154-
yearlyGrossPublicPensionContributingIncomePostInflation * params.premiumPensionContributionRate;
155-
156-
// Simplification: No new pension rights after pension age
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;
169-
170-
// Simplification: arvsvinst: https://www.pensionsmyndigheten.se/statistik/publikationer/orange-rapport-2021/a-berakningsfaktorer.html
171-
// Current we instead simulate arvsvinst by pretending that the pension is paid out based on life expectancy
172-
173-
// Jobbskatteavdrag: https://www.bjornlunden.se/skatt/jobbskatteavdrag__196
174-
175-
// grundavdrag https://www4.skatteverket.se/rattsligvagledning/27071.html?date=2024-01-01#section63-3
176-
// api grundavdrag: https://skatteverket.entryscape.net/rowstore/dataset/ebbd8d70-9b9c-4327-b2ce-a371ee66744c/html
177-
178-
// Simplification: state pension payout is taxed as normal income
179-
const accumulatedPublicPensionRights = state.incomePensionRights + state.premiumPensionRights;
180-
const grossPublicPensionPayout =
181-
newAge > params.statePensionAge
182-
? accumulatedPublicPensionRights / (params.generalLifeExpectancy - params.statePensionAge)
183-
: 0;
184-
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;
139+
const inflationAdjustedCashFlows = yearlyCashFlows.map((cf) => {
140+
return {
141+
...cf,
142+
yearlyCashFlow: cf.yearlyCashFlow * compoundedInflationYearlyAverage
143+
};
144+
});
199145

200-
const accumulatedOccupationalPensionInterest =
201-
(state.occupationalPensionSavings +
202-
(yearlyGrossOccupationalPensionContributionPostInflation - grossOccupationalPensionPayout) /
203-
2) *
204-
params.occupationalPensionInterestRate;
146+
const {
147+
grossPayout: grossPublicPensionPayout,
148+
outgoingRights: { incomePension: incomePensionRights, premiumPension: premiumPensionRights }
149+
} = simulatePublicPension({
150+
cashFlows: inflationAdjustedCashFlows,
151+
ingoingRights: {
152+
incomePension: state.incomePensionRights,
153+
premiumPension: state.premiumPensionRights
154+
},
155+
contributionThreshold: params.publicPensionContributionThreshold,
156+
incomePensionContributionRate: params.incomePensionContributionRate,
157+
premiumPensionContributionRate: params.premiumPensionContributionRate,
158+
age: age,
159+
pensionAge: params.statePensionAge,
160+
incomePensionInterestRate: params.incomePensionRate,
161+
premiumPensionInterestRate: params.premiumPensionRate,
162+
generalLifeExpectancy: params.generalLifeExpectancy
163+
});
205164

206-
const accumulatedOccupationalPensionSavings =
207-
state.occupationalPensionSavings +
208-
yearlyGrossOccupationalPensionContributionPostInflation +
209-
accumulatedOccupationalPensionInterest -
210-
grossOccupationalPensionPayout;
165+
const {
166+
outgoingBalance: occupationalPensionSavings,
167+
grossPayout: grossOccupationalPensionPayout
168+
} = simulateOccupationalPensionYear({
169+
ingoingBalance: state.occupationalPensionSavings,
170+
cashFlows: inflationAdjustedCashFlows,
171+
age: age,
172+
pensionAge: params.occupationalPensionAge,
173+
pensionDuration: params.occupationalPensionDuration,
174+
interestRate: params.occupationalPensionInterestRate
175+
});
211176

212177
const yearlyGrossTaxableEarnedIncomePostInflation =
213178
yearlyCashFlows
@@ -245,20 +210,155 @@ function simulateYear(state: YearState, params: Parameters): YearState {
245210
const yearlyNetCashFlowPostInflation =
246211
yearlyNetIncomePostInflation - yearlyNetExpensePostInflation;
247212

248-
const iskSavingsYearlyAverage = state.iskSavings + yearlyNetCashFlowPostInflation / 2;
249-
const iskInterest = iskSavingsYearlyAverage * params.iskInterestRate;
250-
const iskTax = iskSavingsYearlyAverage * params.iskTax;
251-
const newSavings = state.iskSavings + yearlyNetCashFlowPostInflation + iskInterest - iskTax;
213+
const { outgoingBalance: iskSavings } = simulateISKYear({
214+
ingoingBalance: state.iskSavings,
215+
interestRate: params.iskInterestRate,
216+
taxRate: params.iskTax,
217+
deposit: yearlyNetCashFlowPostInflation
218+
});
252219

253220
return {
254-
year: newYear,
255-
age: newAge,
256-
iskSavings: Math.floor(newSavings),
257-
compoundedInflation: compoundedInflation,
221+
year,
222+
age,
223+
iskSavings,
224+
compoundedInflation,
258225
cashFlow: yearlyNetCashFlowPostInflation,
259-
incomePensionRights: incomePensionRights,
260-
premiumPensionRights: premiumPensionRights,
261-
occupationalPensionSavings: accumulatedOccupationalPensionSavings
226+
incomePensionRights,
227+
premiumPensionRights,
228+
occupationalPensionSavings
229+
};
230+
}
231+
232+
function simulatePublicPension({
233+
cashFlows,
234+
ingoingRights,
235+
contributionThreshold,
236+
premiumPensionContributionRate,
237+
incomePensionContributionRate,
238+
age,
239+
pensionAge,
240+
incomePensionInterestRate,
241+
premiumPensionInterestRate,
242+
generalLifeExpectancy
243+
}: {
244+
cashFlows: TimeBoundCashFlow[]; // Inflation adjusted before passed to this function
245+
ingoingRights: { incomePension: number; premiumPension: number };
246+
contributionThreshold: number;
247+
incomePensionContributionRate: number;
248+
premiumPensionContributionRate: number;
249+
age: number;
250+
pensionAge: number;
251+
incomePensionInterestRate: number;
252+
premiumPensionInterestRate: number;
253+
generalLifeExpectancy: number;
254+
}): { outgoingRights: { incomePension: number; premiumPension: number }; grossPayout: number } {
255+
const grossContributingIncome = Math.min(
256+
(
257+
cashFlows.filter(
258+
({ kind }) => kind === 'GrossTaxableEarnedIncome'
259+
) as GrossTaxableEarnedIncome[]
260+
)
261+
.filter(({ publicPensionContributing }) => publicPensionContributing)
262+
.reduce((acc, cf) => acc + cf.yearlyCashFlow, 0),
263+
contributionThreshold
264+
);
265+
266+
const incomePensionContribution = grossContributingIncome * incomePensionContributionRate;
267+
const premiumPensionContribution = grossContributingIncome * premiumPensionContributionRate;
268+
269+
const incomePensionInterest =
270+
(ingoingRights.incomePension + incomePensionContribution / 2) * incomePensionInterestRate;
271+
const outgoingIncomePensionRights =
272+
age <= pensionAge
273+
? ingoingRights.incomePension + incomePensionInterest + incomePensionContribution
274+
: ingoingRights.incomePension;
275+
276+
// Simplification: No new pension rights after pension age
277+
const premiumPensionInterest =
278+
(ingoingRights.premiumPension + premiumPensionContribution / 2) * premiumPensionInterestRate;
279+
const outgoingPremiumPensionRights =
280+
age <= pensionAge
281+
? ingoingRights.premiumPension + premiumPensionInterest + premiumPensionContribution
282+
: ingoingRights.premiumPension;
283+
284+
// Simplification: arvsvinst: https://www.pensionsmyndigheten.se/statistik/publikationer/orange-rapport-2021/a-berakningsfaktorer.html
285+
// Current we instead simulate arvsvinst by pretending that the pension is paid out based on life expectancy
286+
287+
// Jobbskatteavdrag: https://www.bjornlunden.se/skatt/jobbskatteavdrag__196
288+
289+
// grundavdrag https://www4.skatteverket.se/rattsligvagledning/27071.html?date=2024-01-01#section63-3
290+
// api grundavdrag: https://skatteverket.entryscape.net/rowstore/dataset/ebbd8d70-9b9c-4327-b2ce-a371ee66744c/html
291+
292+
// Simplification: state pension payout is taxed as normal income
293+
const grossPayout =
294+
age > pensionAge
295+
? (outgoingIncomePensionRights + outgoingPremiumPensionRights) /
296+
(generalLifeExpectancy - pensionAge)
297+
: 0;
298+
299+
return {
300+
outgoingRights: {
301+
incomePension: outgoingIncomePensionRights,
302+
premiumPension: outgoingPremiumPensionRights
303+
},
304+
grossPayout: grossPayout
305+
};
306+
}
307+
308+
function simulateOccupationalPensionYear({
309+
ingoingBalance,
310+
cashFlows,
311+
age,
312+
pensionAge,
313+
pensionDuration,
314+
interestRate
315+
}: {
316+
ingoingBalance: number;
317+
cashFlows: TimeBoundCashFlow[]; // Inflation adjusted before passed to this function
318+
age: number;
319+
pensionAge: number;
320+
pensionDuration: number;
321+
interestRate: number;
322+
}): {
323+
outgoingBalance: number;
324+
grossPayout: number;
325+
} {
326+
const contribution = (
327+
cashFlows.filter(
328+
({ kind }) => kind === 'GrossTaxableEarnedIncome'
329+
) as GrossTaxableEarnedIncome[]
330+
).reduce((acc, cf) => acc + cf.yearlyCashFlow * cf.occupationalPensionContributionRate, 0);
331+
332+
const grossPayout =
333+
age >= pensionAge && age < pensionAge + pensionDuration
334+
? ingoingBalance * (1 / (pensionDuration + pensionAge - age))
335+
: 0;
336+
337+
const interest = (ingoingBalance + (contribution - grossPayout) / 2) * interestRate;
338+
339+
const outgoingBalance = ingoingBalance + contribution + interest - grossPayout;
340+
341+
return { outgoingBalance, grossPayout };
342+
}
343+
344+
function simulateISKYear({
345+
ingoingBalance,
346+
interestRate,
347+
taxRate,
348+
deposit
349+
}: {
350+
ingoingBalance: number;
351+
interestRate: number;
352+
taxRate: number;
353+
deposit: number;
354+
}): { outgoingBalance: number } {
355+
const iskSavingsYearlyAverage = ingoingBalance + deposit / 2;
356+
const iskInterest = iskSavingsYearlyAverage * interestRate;
357+
const iskTax = iskSavingsYearlyAverage * taxRate;
358+
const newSavings = ingoingBalance + deposit + iskInterest - iskTax;
359+
360+
return {
361+
outgoingBalance: Math.floor(newSavings)
262362
};
263363
}
264364

0 commit comments

Comments
 (0)