Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,034 changes: 1,034 additions & 0 deletions docs/math_cn.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/usage_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**其他语言版本**:[English](./usage_en.md) | [日本語](./usage_ja.md)

**公式说明**:[math_cn.md](./math_cn.md)

## 概述

FIRE Balance 是一个全面的财务独立和提前退休(FIRE)规划工具,通过三阶段交互式分析帮助您制定和优化个人财务计划。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ export function StageNavigation({
profile.inflation_rate === null
)
errors.push(t('validation.inflation_rate_required'));
if (!profile.safety_buffer_months)
if (
profile.safety_buffer_months === undefined ||
profile.safety_buffer_months === null
)
errors.push(t('validation.safety_buffer_required'));

// 拦截年龄进展不一致:current_age <= FIRE <= legal retirement <= life expectancy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,18 @@ export function Stage3Content(): React.JSX.Element {
// 准备完整的UserProfile对象,确保portfolio配置完整
const completeUserProfile = {
...userProfile,
as_of_year: userProfile.as_of_year || new Date().getFullYear(),
current_net_worth: new Decimal(userProfile.current_net_worth || 0),
inflation_rate: new Decimal(userProfile.inflation_rate || 3),
birth_year: userProfile.birth_year ?? 1990,
as_of_year: userProfile.as_of_year ?? new Date().getFullYear(),
expected_fire_age: userProfile.expected_fire_age ?? 50,
legal_retirement_age: userProfile.legal_retirement_age ?? 65,
life_expectancy: userProfile.life_expectancy ?? 85,
current_net_worth: new Decimal(userProfile.current_net_worth ?? 0),
inflation_rate: new Decimal(userProfile.inflation_rate ?? 3),
safety_buffer_months: new Decimal(
userProfile.safety_buffer_months || 12
userProfile.safety_buffer_months ?? 6
),
bridge_discount_rate: new Decimal(
userProfile.bridge_discount_rate || 1.0
userProfile.bridge_discount_rate ?? 1.0
),
portfolio: userProfile.portfolio
? {
Expand Down Expand Up @@ -826,10 +830,10 @@ export function Stage3Content(): React.JSX.Element {
<NetWorthTrajectoryChart
yearlyStates={yearlyStates}
targetFireAge={
plannerStore.data.user_profile?.expected_fire_age || 65
plannerStore.data.user_profile?.expected_fire_age ?? 50
}
legalRetirementAge={
plannerStore.data.user_profile?.legal_retirement_age || 65
plannerStore.data.user_profile?.legal_retirement_age ?? 65
}
currentAge={
plannerStore.data.user_profile
Expand All @@ -839,14 +843,14 @@ export function Stage3Content(): React.JSX.Element {
: 30
}
lifeExpectancy={
plannerStore.data.user_profile?.life_expectancy || 85
plannerStore.data.user_profile?.life_expectancy ?? 85
}
fireNetWorth={fireCalculation.fire_net_worth}
safetyBufferMonths={
plannerStore.data.user_profile?.safety_buffer_months || 6
plannerStore.data.user_profile?.safety_buffer_months ?? 6
}
bridgeDiscountRate={
plannerStore.data.user_profile?.bridge_discount_rate || 1.0
plannerStore.data.user_profile?.bridge_discount_rate ?? 1.0
}
height={400}
showCashFlowArea={true}
Expand All @@ -870,16 +874,16 @@ export function Stage3Content(): React.JSX.Element {
<YearlyDataTableSection
data={yearlyStates}
safetyBufferMonths={
plannerStore.data.user_profile?.safety_buffer_months || 6
plannerStore.data.user_profile?.safety_buffer_months ?? 6
}
targetFireAge={
plannerStore.data.user_profile?.expected_fire_age || 65
plannerStore.data.user_profile?.expected_fire_age ?? 50
}
legalRetirementAge={
plannerStore.data.user_profile?.legal_retirement_age || 65
plannerStore.data.user_profile?.legal_retirement_age ?? 65
}
bridgeDiscountRate={
plannerStore.data.user_profile?.bridge_discount_rate || 1.0
plannerStore.data.user_profile?.bridge_discount_rate ?? 1.0
}
t={t}
/>
Expand Down Expand Up @@ -1311,7 +1315,7 @@ export function Stage3Content(): React.JSX.Element {
{t('fire_success_criteria', {
months:
plannerStore.data.user_profile
?.safety_buffer_months || 6,
?.safety_buffer_months ?? 6,
})}
</Alert>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('getRequiredSafetyBufferMonths', () => {
baseSafetyBufferMonths: baseMonths,
bridgeDiscountRatePercent: new Decimal(0),
}).toNumber()
).toBe(12);
).toBe(192); // fallback legal retirement age = max(65, expectedFireAge)
});

test('no-discount (0%) equals full remaining years', () => {
Expand Down
8 changes: 4 additions & 4 deletions implementations/typescript/src/core/data_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,10 @@ export function createUserProfile(
expected_fire_age: data.expected_fire_age || 50,
legal_retirement_age: data.legal_retirement_age || 65,
life_expectancy: data.life_expectancy || 85,
current_net_worth: new Decimal(data.current_net_worth || 0.0),
inflation_rate: new Decimal(data.inflation_rate || 3.0),
safety_buffer_months: new Decimal(data.safety_buffer_months || 6),
bridge_discount_rate: new Decimal(data.bridge_discount_rate || 1.0),
current_net_worth: new Decimal(data.current_net_worth ?? 0.0),
inflation_rate: new Decimal(data.inflation_rate ?? 3.0),
safety_buffer_months: new Decimal(data.safety_buffer_months ?? 6),
bridge_discount_rate: new Decimal(data.bridge_discount_rate ?? 1.0),
portfolio: data.portfolio || createPortfolioConfiguration({}),
};

Expand Down
8 changes: 2 additions & 6 deletions implementations/typescript/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
} from './data_models';
import { getCurrentAgeAsOf } from './data_models';
import { getRequiredSafetyBufferMonths } from './safety_buffer';
import { LiquidityAwareFlowStrategy, PortfolioSimulator } from './portfolio';
import { PortfolioSimulator } from './portfolio';

// =============================================================================
// Engine Input Data Structure
Expand Down Expand Up @@ -84,11 +84,7 @@ export class FIREEngine {
this.projection_df = engineInput.annual_financial_projection;

// Set up portfolio simulator
const cashFlowStrategy = new LiquidityAwareFlowStrategy();
this.portfolio_simulator = new PortfolioSimulator(
this.profile,
cashFlowStrategy
);
this.portfolio_simulator = new PortfolioSimulator(this.profile);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion implementations/typescript/src/core/planner_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export function configToPlannerData(config: PlannerConfigV1): PlannerData {
current_net_worth: new Decimal(config.profile.current_net_worth ?? 0),
inflation_rate: new Decimal(config.profile.inflation_rate ?? 3.0),
safety_buffer_months: new Decimal(
config.profile.safety_buffer_months ?? 12.0
config.profile.safety_buffer_months ?? 6.0
),
bridge_discount_rate: new Decimal(
config.profile.bridge_discount_rate ?? 1.0
Expand Down
5 changes: 4 additions & 1 deletion implementations/typescript/src/core/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,10 @@ export class PortfolioSimulator {
);
this.strategy =
cashFlowStrategy ||
new LiquidityAwareFlowStrategy(3, this.calculator.portfolio_config);
new LiquidityAwareFlowStrategy(
userProfile.safety_buffer_months.toNumber(),
this.calculator.portfolio_config
);
}

/**
Expand Down
13 changes: 6 additions & 7 deletions implementations/typescript/src/core/safety_buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ export function getRequiredSafetyBufferMonths(
params: RequiredSafetyBufferMonthsParams
): Decimal {
const baseMonths = new Decimal(params.baseSafetyBufferMonths);
const legalRetirementAge = params.legalRetirementAge;
const legalRetirementAge =
params.legalRetirementAge ?? Math.max(65, params.expectedFireAge);

if (
legalRetirementAge == null ||
params.age < params.expectedFireAge ||
params.age >= legalRetirementAge
) {
if (params.age < params.expectedFireAge || params.age >= legalRetirementAge) {
return baseMonths;
}

const yearsUntilLegal = legalRetirementAge - params.age;
if (yearsUntilLegal <= 0) return baseMonths;

const discountRate = new Decimal(params.bridgeDiscountRatePercent).div(100);
const discountRate = new Decimal(params.bridgeDiscountRatePercent ?? 0).div(
100
);
if (discountRate.lte(0)) {
return baseMonths.add(new Decimal(yearsUntilLegal).mul(12));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,13 @@ export class FIRECalculationService {

const convertedUserProfile = {
...plannerData.user_profile,
birth_year: plannerData.user_profile.birth_year ?? 1990,
as_of_year:
plannerData.user_profile.as_of_year ?? new Date().getFullYear(),
expected_fire_age: plannerData.user_profile.expected_fire_age ?? 50,
legal_retirement_age:
plannerData.user_profile.legal_retirement_age ?? 65,
life_expectancy: plannerData.user_profile.life_expectancy ?? 85,
current_net_worth: new Decimal(
plannerData.user_profile.current_net_worth ?? 0
),
Expand Down
Loading