money scenarios: forecast horizon 1–100y (?horizon=), today’s-euro view, scaling gridlines, career paths with job hops and plateaus, crash cycles per portfolio, retirement at 68 with withdrawals, salary path table
All checks were successful
Deploy production / deploy (push) Successful in 2m49s
All checks were successful
Deploy production / deploy (push) Successful in 2m49s
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { defaultScenarios, runScenario, scenarioInputs, type ScenarioParams } from './scenarios';
|
||||
import {
|
||||
defaultScenarios,
|
||||
marketReturns,
|
||||
roleFor,
|
||||
runScenario,
|
||||
scenarioInputs,
|
||||
type ScenarioParams
|
||||
} from './scenarios';
|
||||
|
||||
const accounts = [
|
||||
{ id: 1, name: 'Revolut', kind: 'checking' },
|
||||
@@ -55,8 +62,11 @@ describe('runScenario', () => {
|
||||
const flat: ScenarioParams = {
|
||||
...defaultScenarios.realistic,
|
||||
returnPercent: 0,
|
||||
recoveryPercent: 0,
|
||||
crashPercent: 0,
|
||||
correctionPercent: 0,
|
||||
cashRatePercent: 0,
|
||||
inflationPercent: 0,
|
||||
hopEveryYears: 0,
|
||||
sideStartEuros: 0,
|
||||
sideEndEuros: 0,
|
||||
spendGrowthPercent: 0,
|
||||
@@ -97,17 +107,60 @@ describe('runScenario', () => {
|
||||
expect(pessimistic).toBeLessThan(realistic);
|
||||
expect(realistic).toBeLessThan(optimistic);
|
||||
});
|
||||
it('applies a crash in its year and a job gap after the contract', () => {
|
||||
const calm = runScenario(inputs, { ...flat, returnPercent: 5, recoveryPercent: 5 });
|
||||
it('applies crashes and job gaps', () => {
|
||||
const calm = runScenario(inputs, { ...flat, returnPercent: 5 });
|
||||
const crash = runScenario(inputs, {
|
||||
...flat,
|
||||
returnPercent: 5,
|
||||
recoveryPercent: 5,
|
||||
crashPercent: -30,
|
||||
crashYear: 2027
|
||||
crashEveryYears: 10,
|
||||
firstCrashYear: 2027
|
||||
});
|
||||
expect(crash.end.total).toBeLessThan(calm.end.total);
|
||||
// The crash year itself ends lower; later years catch up to the same long-run average.
|
||||
const at2027 = (result: typeof calm) => result.years.find((row) => row.year === 2027)!.total;
|
||||
expect(at2027(crash)).toBeLessThan(at2027(calm));
|
||||
const gap = runScenario(inputs, { ...flat, gapMonths: 3, gapNetEuros: 1000 });
|
||||
expect(gap.end.total).toBeLessThan(runScenario(inputs, flat).end.total);
|
||||
});
|
||||
it('averages the crash cycle out at the expected return', () => {
|
||||
const markets = marketReturns({
|
||||
...flat,
|
||||
returnPercent: 7,
|
||||
crashPercent: -30,
|
||||
crashEveryYears: 10,
|
||||
firstCrashYear: 2029,
|
||||
correctionPercent: -10
|
||||
});
|
||||
let growth = 1;
|
||||
for (let year = 2029; year < 2039; year++) growth *= 1 + markets.yearReturn(year).rate;
|
||||
expect(growth).toBeCloseTo(Math.pow(1.07, 10), 6);
|
||||
expect(markets.yearReturn(2029).kind).toBe('crash');
|
||||
expect(markets.yearReturn(2034).kind).toBe('correction');
|
||||
expect(markets.yearReturn(2028).kind).toBe('normal');
|
||||
});
|
||||
it('hops jobs, caps the salary and retires at the chosen age', () => {
|
||||
const long = { ...inputs, endDate: '2126-10-24' };
|
||||
const result = runScenario(long, {
|
||||
...flat,
|
||||
firstHopYear: 2029,
|
||||
hopEveryYears: 3,
|
||||
hopRaisePercent: 20,
|
||||
hopGapMonths: 4,
|
||||
salaryCapEuros: 60000
|
||||
});
|
||||
expect(result.points).toHaveLength(1201);
|
||||
const byYear = new Map(result.years.map((row) => [row.year, row]));
|
||||
expect(byYear.get(2029)!.events).toEqual(['Between jobs (4 mo)']);
|
||||
expect(byYear.get(2030)!.events).toEqual(['New job']);
|
||||
// The gap runs into 2030, when the raise lands, then the plateau caps it.
|
||||
expect(byYear.get(2030)!.grossMonthly).toBeCloseTo(500000, -1);
|
||||
expect(byYear.get(2071)!.role).toBe('Retired');
|
||||
expect(byYear.get(2071)!.events).toContain('Retire at 68');
|
||||
expect(byYear.get(2070)!.age).toBe(67);
|
||||
});
|
||||
it('labels roles by base salary in today’s euros', () => {
|
||||
expect(roleFor(44700)).toBe('Junior');
|
||||
expect(roleFor(70000)).toBe('Senior');
|
||||
expect(roleFor(150000)).toBe('Principal / big-tech staff');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user