diff --git a/.eslintrc.js b/.eslintrc.js index 07a2163..73ebe9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,33 +1,34 @@ module.exports = { - "env": { - "browser": true, - "es2021": true + env: { + browser: true, + es2021: true, + }, + extends: [ + 'standard-with-typescript', + 'plugin:prettier/recommended', + 'plugin:jest/recommended', + ], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, }, - "extends": ["standard-with-typescript", - "plugin:prettier/recommended", - "plugin:jest/recommended" - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - // other plugins - "jest" - ], - "rules": { - } -} + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: [ + // other plugins + 'jest', + ], + ignorePatterns: ['dist/', 'node_modules/', 'examples/', '*.js'], + rules: { + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + }, +}; diff --git a/README.md b/README.md index cc69c21..61b6a25 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ To address these challenges, I started developing the Retirement Calculator pack - **Contribution Calculation**: Determine monthly contributions needed to reach your desired retirement balance. - **Withdrawal Estimation**: Understand how much you can safely spend from your retirement savings each year. - **Inflation Adjustment**: Take into account the impact of inflation on your retirement savings and planning. +- **Dynamic Interest Glidepaths**: Age-aware return calculations with three powerful strategies: + - **Fixed Return Glidepath**: Linear decline from aggressive to conservative returns + - **Allocation-Based Glidepath**: Target-date fund style with equity/bond blending + - **Custom Waypoints**: Flexible strategies with user-defined age/return targets ## Usage @@ -54,30 +58,114 @@ const contributionsNeeded = getContributionNeededForDesiredBalance(1000,10000,10 const balance = calculator.getCompoundInterestWithAdditionalContributions(1000, contributionsNeeded.contributionNeededPerPeriod, 10, .1, 12, 12); ``` +### Dynamic Interest Glidepath Calculations + +**NEW**: Age-aware retirement calculations with sophisticated glidepath strategies. + +#### Fixed Return Glidepath +Perfect for modeling target-date funds or declining return assumptions: + +```typescript +const calculator = new RetirementCalculator(); +const result = calculator.getCompoundInterestWithGlidepath( + 25000, // Starting balance + 1000, // Monthly contribution + 25, // Starting age + 65, // Retirement age + { + mode: 'fixed-return', + startReturn: 0.10, // 10% returns at age 25 + endReturn: 0.055 // 5.5% returns at age 65 + } +); + +console.log(`Final balance: $${calculator.formatNumberWithCommas(result.finalBalance)}`); +console.log(`Effective annual return: ${(result.effectiveAnnualReturn * 100).toFixed(2)}%`); +``` + +#### Allocation-Based Glidepath +Model target-date funds with changing equity/bond allocations: + +```typescript +const targetDateResult = calculator.getCompoundInterestWithGlidepath( + 50000, 1500, 30, 65, + { + mode: 'allocation-based', + startEquityWeight: 0.90, // 90% stocks at 30 + endEquityWeight: 0.30, // 30% stocks at 65 + equityReturn: 0.12, // 12% stock returns + bondReturn: 0.04 // 4% bond returns + } +); +``` + +#### Custom Waypoints Glidepath +Create sophisticated strategies with precise control: + +```typescript +const customResult = calculator.getCompoundInterestWithGlidepath( + 15000, 800, 25, 65, + { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 25, value: 1.0 }, // 100% equity at 25 + { age: 35, value: 0.85 }, // 85% equity at 35 + { age: 45, value: 0.70 }, // 70% equity at 45 + { age: 55, value: 0.50 }, // 50% equity at 55 + { age: 65, value: 0.25 } // 25% equity at 65 + ], + equityReturn: 0.11, + bondReturn: 0.035 + } +); + +// Rich timeline data for visualization +console.log(`Timeline entries: ${customResult.monthlyTimeline.length}`); +customResult.monthlyTimeline.forEach(entry => { + console.log(`Age ${entry.age.toFixed(1)}: ${(entry.currentAnnualReturn * 100).toFixed(2)}% return`); +}); +``` + ### Example Scenarios -I have made a couple example scenarios that can be found [here](examples). This may give inspiration on how to best use this tool to plan for retirement. There is a lot more to retirement than simply plugging numbers into a compounding interest calculator. +I have created example scenarios that can be found [here](examples). These demonstrate both traditional and dynamic glidepath approaches to retirement planning. -#### Scenario 1 -Perhaps you have a starting balance in your retirement account, and want to get to the "prized" goal of $1,000,000. Calculate how well you are doing now, and where you need to be in order to achieve your goal. Also, potentially plan with inflation as this could severely impact your results. To run, use ts-node in your console. +#### Basic Retirement Gap Analysis +Perfect for when you have a specific retirement balance goal (like "$1 million") and want to see if your current savings rate is sufficient. This example shows you how to calculate your "retirement gap" and demonstrates the power of compound interest and why inflation matters for long-term planning. -Running the script will output the following: +```bash +npx ts-node examples/basic-retirement-gap-analysis.ts +``` -![Results from scenario 1](images/example1.png) +#### Lifestyle-Based Retirement Planning +Ideal for people who think in terms of "I want to spend $X per year in retirement" rather than accumulating a lump sum. This example works backwards from your desired lifestyle to required savings and shows how the 4% withdrawal rule works in practice. -#### Scenario 2 -Perhaps you don't know how much you want to have in retirement. Instead, you would like to be able to spend $80,000 a year in retirement and not run out of money in 30 years based on the 4% rule. You could also see what that would mean if you included inflation and wanted your $80,000 a year to go as far in 25 years as it does now. To run, use ts-node in your console. +```bash +npx ts-node examples/lifestyle-based-retirement-planning.ts +``` + +#### Advanced Dynamic Investment Strategies ✨ **NEW** +For advanced users who want to model changing investment strategies over time (like target-date funds) and compare sophisticated approaches. This comprehensive example demonstrates all glidepath modes with detailed comparisons and educational insights. -Running the script will output the following: +```bash +npx ts-node examples/advanced-dynamic-investment-strategies.ts +``` -![Results from scenario 2](images/example2.png) +This example showcases: +- **Fixed return glidepaths** for declining return assumptions +- **Allocation-based strategies** mimicking target-date funds +- **Custom waypoint strategies** for precise control +- **Performance comparisons** between traditional and dynamic approaches +- **Timeline data usage** for creating charts and visualizations +- **Educational insights** about why different strategies work -More scenarios will be added as I add planned capabilities in the future. +More scenarios will be added as I continue to enhance the calculator's capabilities. ## Planned Enhancements -- **Detailed Periodic Reporting**: Provide a detailed breakdown of investments and interest accrued over each period, ideal for visualization. - **Fee Management**: Include functionality to account for management fees and their long-term impact. -- **Dynamic Interest Rates**: Adapt to changing interest rates to reflect different stages of financial planning. - **Loan and Withdrawal Impact**: Assess the effect of loans or withdrawals on your retirement savings. +- **Monte Carlo Simulations**: Probabilistic modeling for market volatility and uncertainty. +- **Tax-Advantaged Account Modeling**: Support for 401(k), IRA, Roth IRA contribution limits and tax implications. ## Upcoming Integration - **Interactive UI**: Developing an intuitive interface for easy retirement planning. diff --git a/docs/assets/highlight.css b/docs/assets/highlight.css index c7af87d..6ee61fb 100644 --- a/docs/assets/highlight.css +++ b/docs/assets/highlight.css @@ -17,6 +17,10 @@ --dark-hl-7: #B5CEA8; --light-hl-8: #008000; --dark-hl-8: #6A9955; + --light-hl-9: #000000FF; + --dark-hl-9: #D4D4D4; + --light-hl-10: #267F99; + --dark-hl-10: #4EC9B0; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @@ -31,6 +35,8 @@ --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); + --hl-9: var(--light-hl-9); + --hl-10: var(--light-hl-10); --code-background: var(--light-code-background); } } @@ -44,6 +50,8 @@ --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); + --hl-9: var(--dark-hl-9); + --hl-10: var(--dark-hl-10); --code-background: var(--dark-code-background); } } @@ -57,6 +65,8 @@ --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); + --hl-9: var(--light-hl-9); + --hl-10: var(--light-hl-10); --code-background: var(--light-code-background); } @@ -70,6 +80,8 @@ --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); + --hl-9: var(--dark-hl-9); + --hl-10: var(--dark-hl-10); --code-background: var(--dark-code-background); } @@ -82,4 +94,6 @@ .hl-6 { color: var(--hl-6); } .hl-7 { color: var(--hl-7); } .hl-8 { color: var(--hl-8); } +.hl-9 { color: var(--hl-9); } +.hl-10 { color: var(--hl-10); } pre, code { background: var(--code-background); } diff --git a/docs/assets/navigation.js b/docs/assets/navigation.js index 2ebb324..05e37ea 100644 --- a/docs/assets/navigation.js +++ b/docs/assets/navigation.js @@ -1 +1 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE42PUUvDMBRG/0ueh8OBInu0btCXdpbuYYhIll7ddclNTW7AIv53I4hrWVP2nPOdc/P0JRg+WSxFBYwODBBnUqugJVsnZqKVfIivSkvvwc/HqKsDGx3RI1IjlteLu+/ZvzUri7rK77d1XhYv62r1uF0V2e7kBQrGz8epoXdgtcQO94HR0trBRwBSXd21cBIjMbhXqeLNSXoYWNzc9hIPEPcGCfrrZCJJTyUya1obqEF624BD20SLRO0nPpIeXBjKf23gudy/g+JLSueLqdQOpNNdb/534mglBZ8Hnn8AkTxPAagCAAA=" \ No newline at end of file +window.navigationData = "data:application/octet-stream;base64,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" \ No newline at end of file diff --git a/docs/assets/search.js b/docs/assets/search.js index 45c1b39..7dcaf57 100644 --- a/docs/assets/search.js +++ b/docs/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file +window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file diff --git a/docs/classes/RetirementCalculator.html b/docs/classes/RetirementCalculator.html deleted file mode 100644 index b71e9d9..0000000 --- a/docs/classes/RetirementCalculator.html +++ /dev/null @@ -1,47 +0,0 @@ -RetirementCalculator | retirement-calculator

RetirementCalculator provides various methods to calculate retirement finances, -including inflation adjustments, balance after inflation, and compound interest calculations.

-

Hierarchy

  • RetirementCalculator

Constructors

Methods

  • Formats a number with commas and limits it to two decimal places.

    -

    Parameters

    • value: number

      The number to be formatted.

      -

    Returns string

    A string representation of the number with formatted commas.

    -
  • Adjust how much a balance would be with inflation added.

    -

    Parameters

    • desiredBalance: number
    • years: number
    • inflationRate: number

    Returns number

  • Private

    Calculate the value of something subtracting inflation over a period of time.

    -

    Parameters

    • balance: number
    • inflationRate: number
    • years: number

    Returns number

  • Calculate how much would need to be in retirement in order to spend $yearlySpend a year

    -

    Parameters

    • yearlySpend: number
    • yearlyWithdrawalRate: number = 0.04

    Returns number

  • Calculate how much could be spent in retirement based on an x% rule.

    -

    Parameters

    • balance: number
    • yearlyWithdrawalRate: number

    Returns number

  • Private

    Calculate the interest of a balance over a given period of time.

    -

    Parameters

    • startingBalance: number
    • interestRate: number
    • periods: number

    Returns number

  • Private

    Calculate the total interest multiplier used for determining contributions needed to hit a goal.

    -

    Parameters

    • interestRate: number
    • periods: number

    Returns number

  • Private

    Calculate the total number of periods based on years and periods per year.

    -

    Parameters

    • years: number
    • periodsPerYear: number

    Returns number

  • Private

    Calculate the interest rate per period based on the frequency of compounding.

    -

    Parameters

    • interestRate: number
    • compoundingFrequency: number

    Returns number

  • Private

    Calculate how often compounding should occur based on contribution and compounding frequencies.

    -

    Parameters

    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Private

    Calculate the compound multiplier based on contribution and compounding frequencies. -ex. If we contribute yearly, but compound monthly, then our compound multiplier would be 12.

    -

    Parameters

    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Private

    Convert the balance to match contribution and compounding being the same.

    -

    Parameters

    • balance: number
    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Determine the contribution needed at what frequency to reach a desired balance.

    -

    Parameters

    • startingBalance: number
    • desiredBalance: number
    • years: number
    • interestRate: number
    • contributionFrequency: number
    • compoundingFrequency: number
    • inflationRate: number = 0.02

    Returns DetermineContributionType

  • Calculate compound interest with additional contributions made over a given period of time.

    -

    Parameters

    • initialBalance: number

      The initial balance.

      -
    • additionalContributionAmount: number

      The additional contribution amount.

      -
    • years: number

      The number of years.

      -
    • interestRate: number

      The interest rate.

      -
    • contributionFrequency: number

      The contribution frequency.

      -
    • compoundingFrequency: number

      The compounding frequency.

      -

    Returns CompoundingInterestObjectType

    An object that contains the results, and a history.

    -

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_RetirementCalculator.default.html b/docs/classes/src_RetirementCalculator.default.html new file mode 100644 index 0000000..3601cd6 --- /dev/null +++ b/docs/classes/src_RetirementCalculator.default.html @@ -0,0 +1,121 @@ +default | retirement-calculator - v1.1.0

RetirementCalculator provides various methods to calculate retirement finances, +including inflation adjustments, balance after inflation, and compound interest calculations.

+

Hierarchy

  • default

Constructors

Methods

  • Formats a number with commas and limits it to two decimal places.

    +

    Parameters

    • value: number

      The number to be formatted.

      +

    Returns string

    A string representation of the number with formatted commas.

    +
  • Adjust how much a balance would be with inflation added.

    +

    Parameters

    • desiredBalance: number
    • years: number
    • inflationRate: number

    Returns number

  • Private

    Calculate the value of something subtracting inflation over a period of time.

    +

    Parameters

    • balance: number
    • inflationRate: number
    • years: number

    Returns number

  • Calculate how much would need to be in retirement in order to spend $yearlySpend a year

    +

    Parameters

    • yearlySpend: number
    • yearlyWithdrawalRate: number = 0.04

    Returns number

  • Calculate how much could be spent in retirement based on an x% rule.

    +

    Parameters

    • balance: number
    • yearlyWithdrawalRate: number

    Returns number

  • Private

    Calculate the interest of a balance over a given period of time.

    +

    Parameters

    • startingBalance: number
    • interestRate: number
    • periods: number

    Returns number

  • Private

    Calculate the total interest multiplier used for determining contributions needed to hit a goal. +Uses the geometric series formula for efficiency: sum = a(r^n - 1)/(r - 1)

    +

    Parameters

    • interestRate: number
    • periods: number

    Returns number

  • Private

    Calculate the total number of periods based on years and periods per year.

    +

    Parameters

    • years: number
    • periodsPerYear: number

    Returns number

  • Private

    Calculate the interest rate per period based on the frequency of compounding.

    +

    Parameters

    • interestRate: number
    • compoundingFrequency: number

    Returns number

  • Private

    Calculate how often compounding should occur based on contribution and compounding frequencies.

    +

    Parameters

    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Private

    Calculate the compound multiplier based on contribution and compounding frequencies. +ex. If we contribute yearly, but compound monthly, then our compound multiplier would be 12.

    +

    Parameters

    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Private

    Convert the balance to match contribution and compounding being the same.

    +

    Parameters

    • balance: number
    • contributionFrequency: number
    • compoundingFrequency: number

    Returns number

  • Determine the contribution needed at what frequency to reach a desired balance.

    +

    Parameters

    • startingBalance: number
    • desiredBalance: number
    • years: number
    • interestRate: number
    • contributionFrequency: number
    • compoundingFrequency: number
    • inflationRate: number = 0.02

    Returns DetermineContributionType

  • Calculate compound interest with additional contributions made over a given period of time.

    +

    Parameters

    • initialBalance: number

      The initial balance.

      +
    • additionalContributionAmount: number

      The additional contribution amount.

      +
    • years: number

      The number of years.

      +
    • interestRate: number

      The interest rate.

      +
    • contributionFrequency: number

      The contribution frequency.

      +
    • compoundingFrequency: number

      The compounding frequency.

      +

    Returns CompoundingInterestObjectType

    An object that contains the results, and a history.

    +
  • Calculate compound interest with age-aware glidepath strategies. +Supports fixed returns, allocation-based strategies, and custom waypoints.

    +

    Parameters

    • initialBalance: number

      Starting account balance

      +
    • contributionAmount: number

      Amount contributed per contribution period

      +
    • startAge: number

      Starting age for calculation

      +
    • endAge: number

      Ending age for calculation

      +
    • glidepathConfig: DynamicGlidepathConfig

      Strategy configuration (fixed, allocation-based, or custom)

      +
    • contributionFrequency: number = 12

      Number of contributions per year (default: 12)

      +
    • compoundingFrequency: number = 12

      Number of compounding periods per year (default: 12)

      +
    • contributionTiming: ContributionTiming = 'start'

      When contributions are added ('start' or 'end' of period)

      +

    Returns DynamicGlidepathResult

    Detailed glidepath calculation results with timeline data

    +
  • Private

    Calculate the annual return rate for a given age using the specified glidepath strategy.

    +

    Parameters

    • age: number

      Current age for calculation

      +
    • config: DynamicGlidepathConfig

      Glidepath configuration

      +
    • startAge: number

      Starting age for age-based calculations

      +
    • endAge: number

      Ending age for age-based calculations

      +

    Returns number

    Annual return rate (decimal format)

    +
  • Private

    Calculate linear interpolation progress between start and end ages.

    +

    Parameters

    • age: number

      Current age

      +
    • startAge: number

      Starting age

      +
    • endAge: number

      Ending age

      +

    Returns number

    Clamped progress value between 0 and 1

    +
  • Private

    Calculate return for fixed-return glidepath (linear interpolation).

    +

    Parameters

    • age: number

      Current age

      +
    • config: FixedReturnGlidepathConfig

      Fixed return configuration

      +
    • startAge: number

      Starting age for calculation

      +
    • endAge: number

      Ending age for calculation

      +

    Returns number

    Annual return rate

    +
  • Private

    Calculate return for stepped-return glidepath (Money Guy style).

    +

    Parameters

    Returns number

    Annual return rate

    +
  • Private

    Calculate return for allocation-based glidepath.

    +

    Parameters

    • age: number

      Current age

      +
    • config: AllocationBasedGlidepathConfig

      Allocation-based configuration

      +
    • startAge: number

      Starting age for calculation

      +
    • endAge: number

      Ending age for calculation

      +

    Returns number

    Annual return rate

    +
  • Private

    Interpolate a value between waypoints for a given age.

    +

    Parameters

    • age: number

      Current age

      +
    • waypoints: {
          age: number;
          value: number;
      }[]

      Array of waypoints (already sorted)

      +

    Returns number

    Interpolated value at the given age

    +
  • Private

    Calculate blended return based on equity and bond allocation.

    +

    Parameters

    • equityWeight: number

      Equity allocation weight (0.0 to 1.0)

      +
    • equityReturn: number

      Expected annual equity return

      +
    • bondReturn: number

      Expected annual bond return

      +

    Returns number

    Blended annual return rate

    +
  • Private

    Get the current equity weight for allocation-based and custom waypoints configurations. +Used for timeline data enrichment.

    +

    Parameters

    • age: number

      Current age

      +
    • config: DynamicGlidepathConfig

      Glidepath configuration

      +
    • startAge: number

      Starting age for age-based calculations

      +
    • endAge: number

      Ending age for age-based calculations

      +

    Returns undefined | number

    Equity weight (0.0 to 1.0) or undefined for non-allocation modes

    +
  • Private

    Convert annual interest rate to monthly compounding rate. +Formula: monthlyRate = (1 + annualRate)^(1/12) - 1

    +

    Parameters

    • annualRate: number

      Annual interest rate (decimal)

      +

    Returns number

    Monthly compounding rate (decimal)

    +

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathCalculationError.html b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathCalculationError.html new file mode 100644 index 0000000..24b53a2 --- /dev/null +++ b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathCalculationError.html @@ -0,0 +1,28 @@ +DynamicGlidepathCalculationError | retirement-calculator - v1.1.0

Specific error class for calculation runtime errors. +Used for errors that occur during the calculation process.

+

Hierarchy

Constructors

Methods

  • Create .stack property on a target object

    +

    Parameters

    • targetObject: object
    • Optional constructorOpt: Function

    Returns void

Properties

prepareStackTrace?: ((err, stackTraces) => any)

Type declaration

stackTraceLimit: number
message: string
stack?: string
field: string
value: unknown
constraint: string
suggestion: string
severity: ErrorSeverity
category: ErrorCategory
context: Record<string, unknown>
timestamp: Date
name: string = 'DynamicGlidepathCalculationError'

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathConfigurationError.html b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathConfigurationError.html new file mode 100644 index 0000000..4ced2a5 --- /dev/null +++ b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathConfigurationError.html @@ -0,0 +1,28 @@ +DynamicGlidepathConfigurationError | retirement-calculator - v1.1.0

Specific error class for configuration errors. +Used for glidepath configuration structure issues.

+

Hierarchy

Constructors

Methods

  • Create .stack property on a target object

    +

    Parameters

    • targetObject: object
    • Optional constructorOpt: Function

    Returns void

Properties

prepareStackTrace?: ((err, stackTraces) => any)

Type declaration

stackTraceLimit: number
message: string
stack?: string
field: string
value: unknown
constraint: string
suggestion: string
severity: ErrorSeverity
category: ErrorCategory
context: Record<string, unknown>
timestamp: Date
name: string = 'DynamicGlidepathConfigurationError'

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathError.html b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathError.html new file mode 100644 index 0000000..27be27f --- /dev/null +++ b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathError.html @@ -0,0 +1,30 @@ +DynamicGlidepathError | retirement-calculator - v1.1.0

Base class for all dynamic glidepath validation errors. +Extends the native Error class with enhanced error information.

+

Hierarchy

Constructors

Methods

  • Create .stack property on a target object

    +

    Parameters

    • targetObject: object
    • Optional constructorOpt: Function

    Returns void

Properties

prepareStackTrace?: ((err, stackTraces) => any)

Type declaration

stackTraceLimit: number
message: string
stack?: string
name: string = 'DynamicGlidepathError'
field: string
value: unknown
constraint: string
suggestion: string
severity: ErrorSeverity
category: ErrorCategory
context: Record<string, unknown>
timestamp: Date

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathValidationError.html b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathValidationError.html new file mode 100644 index 0000000..13763f5 --- /dev/null +++ b/docs/classes/src_errors_DynamicGlidepathErrors.DynamicGlidepathValidationError.html @@ -0,0 +1,28 @@ +DynamicGlidepathValidationError | retirement-calculator - v1.1.0

Specific error class for validation errors. +Used for input parameter validation failures.

+

Hierarchy

Constructors

Methods

  • Create .stack property on a target object

    +

    Parameters

    • targetObject: object
    • Optional constructorOpt: Function

    Returns void

Properties

prepareStackTrace?: ((err, stackTraces) => any)

Type declaration

stackTraceLimit: number
message: string
stack?: string
field: string
value: unknown
constraint: string
suggestion: string
severity: ErrorSeverity
category: ErrorCategory
context: Record<string, unknown>
timestamp: Date
name: string = 'DynamicGlidepathValidationError'

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/src_errors_DynamicGlidepathErrors.ValidationResultBuilder.html b/docs/classes/src_errors_DynamicGlidepathErrors.ValidationResultBuilder.html new file mode 100644 index 0000000..fa55a58 --- /dev/null +++ b/docs/classes/src_errors_DynamicGlidepathErrors.ValidationResultBuilder.html @@ -0,0 +1,23 @@ +ValidationResultBuilder | retirement-calculator - v1.1.0

Builder class for constructing validation results.

+

Hierarchy

  • ValidationResultBuilder

Constructors

Methods

Properties

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/enums/CONTRIBUTION_FREQUENCY.html b/docs/enums/CONTRIBUTION_FREQUENCY.html deleted file mode 100644 index d00a4a2..0000000 --- a/docs/enums/CONTRIBUTION_FREQUENCY.html +++ /dev/null @@ -1,8 +0,0 @@ -CONTRIBUTION_FREQUENCY | retirement-calculator

Enumeration CONTRIBUTION_FREQUENCYReadonly

Object representing contribution frequency options.

-

Enumeration Members

Enumeration Members

YEARLY: number

Yearly contribution frequency.

-
MONTHLY: number

Monthly contribution frequency.

-
WEEKLY: number

Weekly contribution frequency.

-

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/enums/src_constants_retirementCalculatorConstants.CONTRIBUTION_FREQUENCY.html b/docs/enums/src_constants_retirementCalculatorConstants.CONTRIBUTION_FREQUENCY.html new file mode 100644 index 0000000..2dd2d63 --- /dev/null +++ b/docs/enums/src_constants_retirementCalculatorConstants.CONTRIBUTION_FREQUENCY.html @@ -0,0 +1,8 @@ +CONTRIBUTION_FREQUENCY | retirement-calculator - v1.1.0

Object representing contribution frequency options.

+

Enumeration Members

Enumeration Members

YEARLY: number

Yearly contribution frequency.

+
MONTHLY: number

Monthly contribution frequency.

+
WEEKLY: number

Weekly contribution frequency.

+

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createAgeError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createAgeError.html new file mode 100644 index 0000000..3ab062a --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createAgeError.html @@ -0,0 +1,2 @@ +createAgeError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createAllocationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createAllocationError.html new file mode 100644 index 0000000..fd9cec8 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createAllocationError.html @@ -0,0 +1,2 @@ +createAllocationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createCalculationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createCalculationError.html new file mode 100644 index 0000000..15e0775 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createCalculationError.html @@ -0,0 +1,2 @@ +createCalculationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createConfigurationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createConfigurationError.html new file mode 100644 index 0000000..68f50bc --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createConfigurationError.html @@ -0,0 +1,2 @@ +createConfigurationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createFinancialError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createFinancialError.html new file mode 100644 index 0000000..5e2bb8a --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createFinancialError.html @@ -0,0 +1,2 @@ +createFinancialError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createReturnRateError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createReturnRateError.html new file mode 100644 index 0000000..4993805 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createReturnRateError.html @@ -0,0 +1,2 @@ +createReturnRateError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createWarning.html b/docs/functions/src_errors_DynamicGlidepathErrors.createWarning.html new file mode 100644 index 0000000..b855cb3 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createWarning.html @@ -0,0 +1,2 @@ +createWarning | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.createWaypointError.html b/docs/functions/src_errors_DynamicGlidepathErrors.createWaypointError.html new file mode 100644 index 0000000..95d2547 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.createWaypointError.html @@ -0,0 +1,2 @@ +createWaypointError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.formatForAPI.html b/docs/functions/src_errors_DynamicGlidepathErrors.formatForAPI.html new file mode 100644 index 0000000..961ea0f --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.formatForAPI.html @@ -0,0 +1,2 @@ +formatForAPI | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.formatForConsole.html b/docs/functions/src_errors_DynamicGlidepathErrors.formatForConsole.html new file mode 100644 index 0000000..2a1a008 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.formatForConsole.html @@ -0,0 +1,2 @@ +formatForConsole | retirement-calculator - v1.1.0
  • Format errors for console display.

    +

    Parameters

    • errors: DynamicGlidepathError[]
    • options: {
          showSuggestions?: boolean;
          showErrorCodes?: boolean;
          groupByCategory?: boolean;
      } = {}
      • Optional showSuggestions?: boolean
      • Optional showErrorCodes?: boolean
      • Optional groupByCategory?: boolean

    Returns string

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.getSummary.html b/docs/functions/src_errors_DynamicGlidepathErrors.getSummary.html new file mode 100644 index 0000000..795c88c --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.getSummary.html @@ -0,0 +1,2 @@ +getSummary | retirement-calculator - v1.1.0
  • Get summary statistics for error collection.

    +

    Parameters

    Returns {
        total: number;
        errorCount: number;
        warningCount: number;
        categories: Record<ErrorCategory, number>;
        topCodes: {
            code: GlidepathErrorCode;
            count: number;
        }[];
    }

    • total: number
    • errorCount: number
    • warningCount: number
    • categories: Record<ErrorCategory, number>
    • topCodes: {
          code: GlidepathErrorCode;
          count: number;
      }[]

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.groupByCategory.html b/docs/functions/src_errors_DynamicGlidepathErrors.groupByCategory.html new file mode 100644 index 0000000..609ea4b --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.groupByCategory.html @@ -0,0 +1,2 @@ +groupByCategory | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.groupBySeverity.html b/docs/functions/src_errors_DynamicGlidepathErrors.groupBySeverity.html new file mode 100644 index 0000000..64cecc1 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.groupBySeverity.html @@ -0,0 +1,2 @@ +groupBySeverity | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isBlockingError.html b/docs/functions/src_errors_DynamicGlidepathErrors.isBlockingError.html new file mode 100644 index 0000000..4fcce5f --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isBlockingError.html @@ -0,0 +1,2 @@ +isBlockingError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isCalculationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.isCalculationError.html new file mode 100644 index 0000000..11e4327 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isCalculationError.html @@ -0,0 +1,2 @@ +isCalculationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isConfigurationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.isConfigurationError.html new file mode 100644 index 0000000..db889af --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isConfigurationError.html @@ -0,0 +1,2 @@ +isConfigurationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isDynamicGlidepathError.html b/docs/functions/src_errors_DynamicGlidepathErrors.isDynamicGlidepathError.html new file mode 100644 index 0000000..c12852a --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isDynamicGlidepathError.html @@ -0,0 +1,2 @@ +isDynamicGlidepathError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isValidationError.html b/docs/functions/src_errors_DynamicGlidepathErrors.isValidationError.html new file mode 100644 index 0000000..4a06efc --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isValidationError.html @@ -0,0 +1,2 @@ +isValidationError | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_errors_DynamicGlidepathErrors.isWarning.html b/docs/functions/src_errors_DynamicGlidepathErrors.isWarning.html new file mode 100644 index 0000000..dc64e97 --- /dev/null +++ b/docs/functions/src_errors_DynamicGlidepathErrors.isWarning.html @@ -0,0 +1,2 @@ +isWarning | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_types_retirementCalculatorTypes.isAllocationBasedConfig.html b/docs/functions/src_types_retirementCalculatorTypes.isAllocationBasedConfig.html new file mode 100644 index 0000000..fd0ea16 --- /dev/null +++ b/docs/functions/src_types_retirementCalculatorTypes.isAllocationBasedConfig.html @@ -0,0 +1,2 @@ +isAllocationBasedConfig | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_types_retirementCalculatorTypes.isCustomWaypointsConfig.html b/docs/functions/src_types_retirementCalculatorTypes.isCustomWaypointsConfig.html new file mode 100644 index 0000000..158c6e6 --- /dev/null +++ b/docs/functions/src_types_retirementCalculatorTypes.isCustomWaypointsConfig.html @@ -0,0 +1,2 @@ +isCustomWaypointsConfig | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_types_retirementCalculatorTypes.isFixedReturnConfig.html b/docs/functions/src_types_retirementCalculatorTypes.isFixedReturnConfig.html new file mode 100644 index 0000000..1df3300 --- /dev/null +++ b/docs/functions/src_types_retirementCalculatorTypes.isFixedReturnConfig.html @@ -0,0 +1,2 @@ +isFixedReturnConfig | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/functions/src_types_retirementCalculatorTypes.isSteppedReturnConfig.html b/docs/functions/src_types_retirementCalculatorTypes.isSteppedReturnConfig.html new file mode 100644 index 0000000..0c863c1 --- /dev/null +++ b/docs/functions/src_types_retirementCalculatorTypes.isSteppedReturnConfig.html @@ -0,0 +1,2 @@ +isSteppedReturnConfig | retirement-calculator - v1.1.0

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 2ffdfff..d825fbb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,4 +1,4 @@ -retirement-calculator

retirement-calculator

Retirement Calculator

Welcome to the Retirement Calculator – your go-to tool for comprehensive retirement planning!

+retirement-calculator - v1.1.0

retirement-calculator - v1.1.0

Retirement Calculator

Welcome to the Retirement Calculator – your go-to tool for comprehensive retirement planning!

Introduction

Are you ready to embark on the journey of securing your financial future? Planning for retirement can be both exciting and challenging. As someone with a deep passion for personal finance and a desire for a brighter financial future, I've created this tool to help you navigate the complexities of retirement planning.

Installation

To use the Retirement Calculator in your project, install it via npm:

npm i retirement-calculator
@@ -14,6 +14,12 @@
 
  • Contribution Calculation: Determine monthly contributions needed to reach your desired retirement balance.
  • Withdrawal Estimation: Understand how much you can safely spend from your retirement savings each year.
  • Inflation Adjustment: Take into account the impact of inflation on your retirement savings and planning.
  • +
  • Dynamic Interest Glidepaths: Age-aware return calculations with three powerful strategies:
      +
    • Fixed Return Glidepath: Linear decline from aggressive to conservative returns
    • +
    • Allocation-Based Glidepath: Target-date fund style with equity/bond blending
    • +
    • Custom Waypoints: Flexible strategies with user-defined age/return targets
    • +
    +
  • Usage

    Importing the Calculator

    import { RetirementCalculator } from 'retirement-calculator';
     
    @@ -21,19 +27,41 @@

    Calculate Contributions Needed to Achieve a Desired Balance

    const calculator = new RetirementCalculator();
    const contributionsNeeded = getContributionNeededForDesiredBalance(1000,10000,10,.1,12,12);

    // You won't necessarily hit your exact goal, so to find out what the exact total would be, run the following
    const balance = calculator.getCompoundInterestWithAdditionalContributions(1000, contributionsNeeded.contributionNeededPerPeriod, 10, .1, 12, 12);
    -

    Example Scenarios

    I have made a couple example scenarios that can be found here. This may give inspiration on how to best use this tool to plan for retirement. There is a lot more to retirement than simply plugging numbers into a compounding interest calculator.

    -

    Scenario 1

    Perhaps you have a starting balance in your retirement account, and want to get to the "prized" goal of $1,000,000. Calculate how well you are doing now, and where you need to be in order to achieve your goal. Also, potentially plan with inflation as this could severely impact your results. To run, use ts-node in your console.

    -

    Running the script will output the following:

    -

    Results from scenario 1

    -

    Scenario 2

    Perhaps you don't know how much you want to have in retirement. Instead, you would like to be able to spend $80,000 a year in retirement and not run out of money in 30 years based on the 4% rule. You could also see what that would mean if you included inflation and wanted your $80,000 a year to go as far in 25 years as it does now. To run, use ts-node in your console.

    -

    Running the script will output the following:

    -

    Results from scenario 2

    -

    More scenarios will be added as I add planned capabilities in the future.

    +

    Dynamic Interest Glidepath Calculations

    NEW: Age-aware retirement calculations with sophisticated glidepath strategies.

    +

    Fixed Return Glidepath

    Perfect for modeling target-date funds or declining return assumptions:

    +
    const calculator = new RetirementCalculator();
    const result = calculator.getCompoundInterestWithGlidepath(
    25000, // Starting balance
    1000, // Monthly contribution
    25, // Starting age
    65, // Retirement age
    {
    mode: 'fixed-return',
    startReturn: 0.10, // 10% returns at age 25
    endReturn: 0.055 // 5.5% returns at age 65
    }
    );

    console.log(`Final balance: $${calculator.formatNumberWithCommas(result.finalBalance)}`);
    console.log(`Effective annual return: ${(result.effectiveAnnualReturn * 100).toFixed(2)}%`); +
    +

    Allocation-Based Glidepath

    Model target-date funds with changing equity/bond allocations:

    +
    const targetDateResult = calculator.getCompoundInterestWithGlidepath(
    50000, 1500, 30, 65,
    {
    mode: 'allocation-based',
    startEquityWeight: 0.90, // 90% stocks at 30
    endEquityWeight: 0.30, // 30% stocks at 65
    equityReturn: 0.12, // 12% stock returns
    bondReturn: 0.04 // 4% bond returns
    }
    ); +
    +

    Custom Waypoints Glidepath

    Create sophisticated strategies with precise control:

    +
    const customResult = calculator.getCompoundInterestWithGlidepath(
    15000, 800, 25, 65,
    {
    mode: 'custom-waypoints',
    valueType: 'equityWeight',
    waypoints: [
    { age: 25, value: 1.0 }, // 100% equity at 25
    { age: 35, value: 0.85 }, // 85% equity at 35
    { age: 45, value: 0.70 }, // 70% equity at 45
    { age: 55, value: 0.50 }, // 50% equity at 55
    { age: 65, value: 0.25 } // 25% equity at 65
    ],
    equityReturn: 0.11,
    bondReturn: 0.035
    }
    );

    // Rich timeline data for visualization
    console.log(`Timeline entries: ${customResult.monthlyTimeline.length}`);
    customResult.monthlyTimeline.forEach(entry => {
    console.log(`Age ${entry.age.toFixed(1)}: ${(entry.currentAnnualReturn * 100).toFixed(2)}% return`);
    }); +
    +

    Example Scenarios

    I have created example scenarios that can be found here. These demonstrate both traditional and dynamic glidepath approaches to retirement planning.

    +

    Basic Retirement Gap Analysis

    Perfect for when you have a specific retirement balance goal (like "$1 million") and want to see if your current savings rate is sufficient. This example shows you how to calculate your "retirement gap" and demonstrates the power of compound interest and why inflation matters for long-term planning.

    +
    npx ts-node examples/basic-retirement-gap-analysis.ts
    +
    +

    Lifestyle-Based Retirement Planning

    Ideal for people who think in terms of "I want to spend $X per year in retirement" rather than accumulating a lump sum. This example works backwards from your desired lifestyle to required savings and shows how the 4% withdrawal rule works in practice.

    +
    npx ts-node examples/lifestyle-based-retirement-planning.ts
    +
    +

    Advanced Dynamic Investment Strategies ✨ NEW

    For advanced users who want to model changing investment strategies over time (like target-date funds) and compare sophisticated approaches. This comprehensive example demonstrates all glidepath modes with detailed comparisons and educational insights.

    +
    npx ts-node examples/advanced-dynamic-investment-strategies.ts
    +
    +

    This example showcases:

    +
      +
    • Fixed return glidepaths for declining return assumptions
    • +
    • Allocation-based strategies mimicking target-date funds
    • +
    • Custom waypoint strategies for precise control
    • +
    • Performance comparisons between traditional and dynamic approaches
    • +
    • Timeline data usage for creating charts and visualizations
    • +
    • Educational insights about why different strategies work
    • +
    +

    More scenarios will be added as I continue to enhance the calculator's capabilities.

    Planned Enhancements

      -
    • Detailed Periodic Reporting: Provide a detailed breakdown of investments and interest accrued over each period, ideal for visualization.
    • Fee Management: Include functionality to account for management fees and their long-term impact.
    • -
    • Dynamic Interest Rates: Adapt to changing interest rates to reflect different stages of financial planning.
    • Loan and Withdrawal Impact: Assess the effect of loans or withdrawals on your retirement savings.
    • +
    • Monte Carlo Simulations: Probabilistic modeling for market volatility and uncertainty.
    • +
    • Tax-Advantaged Account Modeling: Support for 401(k), IRA, Roth IRA contribution limits and tax implications.

    Upcoming Integration

    • Interactive UI: Developing an intuitive interface for easy retirement planning.
    • @@ -43,4 +71,4 @@

      Docs

      Docs were made with TypeDoc and can be found here.

      License

      This project is licensed under the MIT License.

      Contact

      For any inquiries or collaboration ideas, please feel free to reach out through GitHub issues on this repository. You can also connect with me on LinkedIn for professional networking and discussions.

      -

    Generated using TypeDoc

    \ No newline at end of file +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/CompoundingInterestObjectType.html b/docs/interfaces/CompoundingInterestObjectType.html deleted file mode 100644 index a994c43..0000000 --- a/docs/interfaces/CompoundingInterestObjectType.html +++ /dev/null @@ -1,16 +0,0 @@ -CompoundingInterestObjectType | retirement-calculator

    Interface CompoundingInterestObjectType

    Object representing compounding interest calculations.

    -

    Hierarchy

    • CompoundingInterestObjectType

    Properties

    balance: number

    Balance amount.

    -
    totalContributions: number

    Total contributions made.

    -
    totalInterestEarned: number

    Total interest earned.

    -
    years: number

    Number of years.

    -
    contributionFrequency: number

    Contribution frequency.

    -
    compoundingFrequency: number

    Compounding frequency.

    -
    compoundingPeriodDetails: CompoundingPeriodDetailsType[]

    Compounding period details.

    -

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/CompoundingPeriodDetailsType.html b/docs/interfaces/CompoundingPeriodDetailsType.html deleted file mode 100644 index b715982..0000000 --- a/docs/interfaces/CompoundingPeriodDetailsType.html +++ /dev/null @@ -1,18 +0,0 @@ -CompoundingPeriodDetailsType | retirement-calculator

    Interface CompoundingPeriodDetailsType

    Represents the details of a single compounding period in the compound interest calculation. -This includes information on the total balance, contributions, and interest for each period.

    -

    CompoundingPeriodDetailsType

    -

    Hierarchy

    • CompoundingPeriodDetailsType

    Properties

    period: number

    The specific compounding period, starting from 1.

    -
    balance: number

    The total balance at the end of this compounding period.

    -
    contributionTotal: number

    The cumulative total of contributions made up to this period.

    -
    interestTotal: number

    The cumulative total of interest earned up to this period.

    -
    interestEarnedThisPeriod: number

    The amount of interest earned during this specific period.

    -
    balanceFromContributions: number

    The portion of the current balance attributable to contributions.

    -
    balanceFromInterest: number

    The portion of the current balance attributable to accumulated interest.

    -

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/DetermineContributionType.html b/docs/interfaces/DetermineContributionType.html deleted file mode 100644 index 4e660bd..0000000 --- a/docs/interfaces/DetermineContributionType.html +++ /dev/null @@ -1,12 +0,0 @@ -DetermineContributionType | retirement-calculator

    Interface DetermineContributionType

    Object representing the details needed to determine contributions.

    -

    Hierarchy

    • DetermineContributionType

    Properties

    contributionNeededPerPeriod: number

    Contribution needed per period.

    -
    contributionNeededPerPeriodWithInflation: number

    Contribution needed per period with inflation.

    -
    desiredBalance: number

    Desired balance.

    -
    desiredBalanceWithInflation: number

    Desired balance with inflation.

    -
    desiredBalanceValueAfterInflation: number

    Desired balance value after inflation.

    -

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/YearlyCompoundingDetails.html b/docs/interfaces/YearlyCompoundingDetails.html deleted file mode 100644 index 82bd15c..0000000 --- a/docs/interfaces/YearlyCompoundingDetails.html +++ /dev/null @@ -1,11 +0,0 @@ -YearlyCompoundingDetails | retirement-calculator

    Interface YearlyCompoundingDetails

    Represents the aggregated data for a single year in the compound interest calculation. - YearlyCompoundingDetails

    -

    Hierarchy

    • YearlyCompoundingDetails

    Properties

    year: number

    The year number starting from 1.

    -
    cumulativeContributions: number

    The cumulative total contributions made up to the end of the year.

    -
    cumulativeInterest: number

    The cumulative total interest earned up to the end of the year.

    -
    endOfYearBalance: number

    The balance at the end of the year.

    -

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingInterestObjectType.html b/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingInterestObjectType.html new file mode 100644 index 0000000..3abff84 --- /dev/null +++ b/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingInterestObjectType.html @@ -0,0 +1,16 @@ +CompoundingInterestObjectType | retirement-calculator - v1.1.0

    Object representing compounding interest calculations.

    +

    Hierarchy

    • CompoundingInterestObjectType

    Properties

    balance: number

    Balance amount.

    +
    totalContributions: number

    Total contributions made.

    +
    totalInterestEarned: number

    Total interest earned.

    +
    years: number

    Number of years.

    +
    contributionFrequency: number

    Contribution frequency.

    +
    compoundingFrequency: number

    Compounding frequency.

    +
    compoundingPeriodDetails: CompoundingPeriodDetailsType[]

    Compounding period details.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingPeriodDetailsType.html b/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingPeriodDetailsType.html new file mode 100644 index 0000000..52e489e --- /dev/null +++ b/docs/interfaces/src_types_retirementCalculatorTypes.CompoundingPeriodDetailsType.html @@ -0,0 +1,18 @@ +CompoundingPeriodDetailsType | retirement-calculator - v1.1.0

    Represents the details of a single compounding period in the compound interest calculation. +This includes information on the total balance, contributions, and interest for each period.

    +

    CompoundingPeriodDetailsType

    +

    Hierarchy

    • CompoundingPeriodDetailsType

    Properties

    period: number

    The specific compounding period, starting from 1.

    +
    balance: number

    The total balance at the end of this compounding period.

    +
    contributionTotal: number

    The cumulative total of contributions made up to this period.

    +
    interestTotal: number

    The cumulative total of interest earned up to this period.

    +
    interestEarnedThisPeriod: number

    The amount of interest earned during this specific period.

    +
    balanceFromContributions: number

    The portion of the current balance attributable to contributions.

    +
    balanceFromInterest: number

    The portion of the current balance attributable to accumulated interest.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/src_types_retirementCalculatorTypes.ContributionFrequencyType.html b/docs/interfaces/src_types_retirementCalculatorTypes.ContributionFrequencyType.html new file mode 100644 index 0000000..8550e41 --- /dev/null +++ b/docs/interfaces/src_types_retirementCalculatorTypes.ContributionFrequencyType.html @@ -0,0 +1,8 @@ +ContributionFrequencyType | retirement-calculator - v1.1.0

    Object representing contribution frequency options.

    +

    Hierarchy

    • ContributionFrequencyType

    Properties

    Properties

    YEARLY: number

    Yearly contribution frequency.

    +
    MONTHLY: number

    Monthly contribution frequency.

    +
    WEEKLY: number

    Weekly contribution frequency.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/src_types_retirementCalculatorTypes.DetermineContributionType.html b/docs/interfaces/src_types_retirementCalculatorTypes.DetermineContributionType.html new file mode 100644 index 0000000..23b390a --- /dev/null +++ b/docs/interfaces/src_types_retirementCalculatorTypes.DetermineContributionType.html @@ -0,0 +1,12 @@ +DetermineContributionType | retirement-calculator - v1.1.0

    Object representing the details needed to determine contributions.

    +

    Hierarchy

    • DetermineContributionType

    Properties

    contributionNeededPerPeriod: number

    Contribution needed per period.

    +
    contributionNeededPerPeriodWithInflation: number

    Contribution needed per period with inflation.

    +
    desiredBalance: number

    Desired balance.

    +
    desiredBalanceWithInflation: number

    Desired balance with inflation.

    +
    desiredBalanceValueAfterInflation: number

    Desired balance value after inflation.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/src_types_retirementCalculatorTypes.YearlyCompoundingDetails.html b/docs/interfaces/src_types_retirementCalculatorTypes.YearlyCompoundingDetails.html new file mode 100644 index 0000000..ac851e1 --- /dev/null +++ b/docs/interfaces/src_types_retirementCalculatorTypes.YearlyCompoundingDetails.html @@ -0,0 +1,11 @@ +YearlyCompoundingDetails | retirement-calculator - v1.1.0

    Represents the aggregated data for a single year in the compound interest calculation. + YearlyCompoundingDetails

    +

    Hierarchy

    • YearlyCompoundingDetails

    Properties

    year: number

    The year number starting from 1.

    +
    cumulativeContributions: number

    The cumulative total contributions made up to the end of the year.

    +
    cumulativeInterest: number

    The cumulative total interest earned up to the end of the year.

    +
    endOfYearBalance: number

    The balance at the end of the year.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules.html b/docs/modules.html deleted file mode 100644 index 231a099..0000000 --- a/docs/modules.html +++ /dev/null @@ -1,8 +0,0 @@ -retirement-calculator

    retirement-calculator

    Index

    Classes

    Interfaces

    Enumerations

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/index.html b/docs/modules/index.html new file mode 100644 index 0000000..c02cc39 --- /dev/null +++ b/docs/modules/index.html @@ -0,0 +1,62 @@ +index | retirement-calculator - v1.1.0

    References

    Renames and re-exports default
    Re-exports CONTRIBUTION_FREQUENCY
    Re-exports GLIDEPATH_DEFAULTS
    Re-exports GLIDEPATH_VALIDATION
    Re-exports GLIDEPATH_MATH
    Re-exports GLIDEPATH_TEMPLATES
    Re-exports GLIDEPATH_ERROR_MESSAGES
    Re-exports GLIDEPATH_PERFORMANCE
    Re-exports GLIDEPATH_PRESETS
    Re-exports ContributionFrequencyType
    Re-exports DetermineContributionType
    Re-exports CompoundingInterestObjectType
    Re-exports CompoundingPeriodDetailsType
    Re-exports YearlyCompoundingDetails
    Re-exports ContributionTiming
    Re-exports GlidepathMode
    Re-exports FixedReturnGlidepathConfig
    Re-exports AllocationBasedGlidepathConfig
    Re-exports CustomWaypointsGlidepathConfig
    Re-exports SteppedReturnGlidepathConfig
    Re-exports GlidepathWaypoint
    Re-exports DynamicGlidepathConfig
    Re-exports MonthlyTimelineEntry
    Re-exports DynamicGlidepathResult
    Re-exports ConfigForMode
    Re-exports RequiredFields
    Re-exports CustomWaypointsWithReturns
    Re-exports isFixedReturnConfig
    Re-exports isAllocationBasedConfig
    Re-exports isCustomWaypointsConfig
    Re-exports isSteppedReturnConfig
    Re-exports GLIDEPATH_ERROR_CODES
    Re-exports DynamicGlidepathError
    Re-exports DynamicGlidepathValidationError
    Re-exports DynamicGlidepathConfigurationError
    Re-exports DynamicGlidepathCalculationError
    Re-exports ValidationResultBuilder
    Re-exports createAgeError
    Re-exports createFinancialError
    Re-exports createReturnRateError
    Re-exports createAllocationError
    Re-exports createConfigurationError
    Re-exports createWaypointError
    Re-exports createWarning
    Re-exports createCalculationError
    Re-exports groupByCategory
    Re-exports groupBySeverity
    Re-exports getSummary
    Re-exports formatForConsole
    Re-exports formatForAPI
    Re-exports isDynamicGlidepathError
    Re-exports isValidationError
    Re-exports isConfigurationError
    Re-exports isCalculationError
    Re-exports isWarning
    Re-exports isBlockingError
    Re-exports GlidepathErrorCode
    Re-exports ErrorSeverity
    Re-exports ErrorCategory
    Re-exports GlidepathErrorInfo
    Re-exports ValidationResult

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/src_RetirementCalculator.html b/docs/modules/src_RetirementCalculator.html new file mode 100644 index 0000000..03d10f3 --- /dev/null +++ b/docs/modules/src_RetirementCalculator.html @@ -0,0 +1,2 @@ +src/RetirementCalculator | retirement-calculator - v1.1.0

    Module src/RetirementCalculator

    Index

    Classes

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/src_constants_retirementCalculatorConstants.html b/docs/modules/src_constants_retirementCalculatorConstants.html new file mode 100644 index 0000000..ddcb981 --- /dev/null +++ b/docs/modules/src_constants_retirementCalculatorConstants.html @@ -0,0 +1,9 @@ +src/constants/retirementCalculatorConstants | retirement-calculator - v1.1.0

    Module src/constants/retirementCalculatorConstants

    Index

    Enumerations

    Variables

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/src_errors_DynamicGlidepathErrors.html b/docs/modules/src_errors_DynamicGlidepathErrors.html new file mode 100644 index 0000000..3968a77 --- /dev/null +++ b/docs/modules/src_errors_DynamicGlidepathErrors.html @@ -0,0 +1,31 @@ +src/errors/DynamicGlidepathErrors | retirement-calculator - v1.1.0

    Module src/errors/DynamicGlidepathErrors

    Index

    Classes

    Functions

    Type Aliases

    Variables

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/src_types_retirementCalculatorTypes.html b/docs/modules/src_types_retirementCalculatorTypes.html new file mode 100644 index 0000000..1d10f4c --- /dev/null +++ b/docs/modules/src_types_retirementCalculatorTypes.html @@ -0,0 +1,32 @@ +src/types/retirementCalculatorTypes | retirement-calculator - v1.1.0

    Module src/types/retirementCalculatorTypes

    Index

    Interfaces

    Functions

    Type Aliases

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_errors_DynamicGlidepathErrors.ErrorCategory.html b/docs/types/src_errors_DynamicGlidepathErrors.ErrorCategory.html new file mode 100644 index 0000000..9d05c79 --- /dev/null +++ b/docs/types/src_errors_DynamicGlidepathErrors.ErrorCategory.html @@ -0,0 +1,2 @@ +ErrorCategory | retirement-calculator - v1.1.0
    ErrorCategory: "validation" | "configuration" | "calculation" | "performance" | "usability"

    Error categories for grouping related errors.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_errors_DynamicGlidepathErrors.ErrorSeverity.html b/docs/types/src_errors_DynamicGlidepathErrors.ErrorSeverity.html new file mode 100644 index 0000000..75791da --- /dev/null +++ b/docs/types/src_errors_DynamicGlidepathErrors.ErrorSeverity.html @@ -0,0 +1,2 @@ +ErrorSeverity | retirement-calculator - v1.1.0
    ErrorSeverity: "error" | "warning"

    Error severity levels.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorCode.html b/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorCode.html new file mode 100644 index 0000000..76bb804 --- /dev/null +++ b/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorCode.html @@ -0,0 +1,2 @@ +GlidepathErrorCode | retirement-calculator - v1.1.0
    GlidepathErrorCode: typeof GLIDEPATH_ERROR_CODES[keyof typeof GLIDEPATH_ERROR_CODES]

    Type for error code values.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorInfo.html b/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorInfo.html new file mode 100644 index 0000000..029efa1 --- /dev/null +++ b/docs/types/src_errors_DynamicGlidepathErrors.GlidepathErrorInfo.html @@ -0,0 +1,11 @@ +GlidepathErrorInfo | retirement-calculator - v1.1.0
    GlidepathErrorInfo: {
        code: GlidepathErrorCode;
        field: string;
        value: unknown;
        constraint: string;
        suggestion: string;
        severity: ErrorSeverity;
        category: ErrorCategory;
        context?: Record<string, unknown>;
    }

    Enhanced error information interface that extends the base Error. +Provides comprehensive context for debugging and user-facing applications.

    +

    Type declaration

    • code: GlidepathErrorCode

      Machine-readable error code

      +
    • field: string

      Field name that caused the error

      +
    • value: unknown

      The invalid value that was provided

      +
    • constraint: string

      Description of the constraint that was violated

      +
    • suggestion: string

      Helpful suggestion for fixing the error

      +
    • severity: ErrorSeverity

      Error severity level

      +
    • category: ErrorCategory

      Error category for grouping

      +
    • Optional context?: Record<string, unknown>

      Additional context data

      +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_errors_DynamicGlidepathErrors.ValidationResult.html b/docs/types/src_errors_DynamicGlidepathErrors.ValidationResult.html new file mode 100644 index 0000000..5430ce9 --- /dev/null +++ b/docs/types/src_errors_DynamicGlidepathErrors.ValidationResult.html @@ -0,0 +1,5 @@ +ValidationResult | retirement-calculator - v1.1.0
    ValidationResult: {
        isValid: boolean;
        errors: DynamicGlidepathValidationError[];
        warnings: DynamicGlidepathValidationError[];
    }

    Validation result interface for comprehensive error reporting.

    +

    Type declaration

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.AllocationBasedGlidepathConfig.html b/docs/types/src_types_retirementCalculatorTypes.AllocationBasedGlidepathConfig.html new file mode 100644 index 0000000..39bb454 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.AllocationBasedGlidepathConfig.html @@ -0,0 +1,21 @@ +AllocationBasedGlidepathConfig | retirement-calculator - v1.1.0
    AllocationBasedGlidepathConfig: {
        mode: "allocation-based";
        startEquityWeight: number;
        endEquityWeight: number;
        equityReturn: number;
        bondReturn: number;
    }

    Configuration for allocation-based glidepath that blends equity and bond returns +based on linearly interpolated equity allocation by age.

    +

    Use case: Age-based asset allocation strategies (e.g., "120 minus age" rule)

    +

    Type declaration

    • mode: "allocation-based"

      Discriminator for the glidepath configuration type.

      +
    • startEquityWeight: number

      Equity allocation percentage at starting age (0.0 to 1.0).

      +

      Example

      0.90 represents 90% equity allocation
      +
      +
    • endEquityWeight: number

      Equity allocation percentage at ending age (0.0 to 1.0).

      +

      Example

      0.30 represents 30% equity allocation
      +
      +
    • equityReturn: number

      Expected annual return for equity portion (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.12 represents 12% annual equity return
      +
      +
    • bondReturn: number

      Expected annual return for bond portion (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.04 represents 4% annual bond return
      +
      +

    Example

    const config: AllocationBasedGlidepathConfig = {
    mode: 'allocation-based',
    startEquityWeight: 0.90, // 90% equity at start
    endEquityWeight: 0.30, // 30% equity at end
    equityReturn: 0.12, // 12% equity returns
    bondReturn: 0.04 // 4% bond returns
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.CalculationOverflowError.html b/docs/types/src_types_retirementCalculatorTypes.CalculationOverflowError.html new file mode 100644 index 0000000..2619afa --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.CalculationOverflowError.html @@ -0,0 +1,2 @@ +CalculationOverflowError | retirement-calculator - v1.1.0
    CalculationOverflowError: RetirementCalculatorError & {
        name: "CalculationOverflowError";
        message: "Calculation resulted in numeric overflow";
    }

    Error thrown when calculation results in numeric overflow.

    +

    Type declaration

    • name: "CalculationOverflowError"
    • message: "Calculation resulted in numeric overflow"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.ConfigForMode.html b/docs/types/src_types_retirementCalculatorTypes.ConfigForMode.html new file mode 100644 index 0000000..c271061 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.ConfigForMode.html @@ -0,0 +1,2 @@ +ConfigForMode | retirement-calculator - v1.1.0
    ConfigForMode<T>: T extends "fixed-return"
        ? FixedReturnGlidepathConfig
        : T extends "allocation-based"
            ? AllocationBasedGlidepathConfig
            : T extends "custom-waypoints"
                ? CustomWaypointsGlidepathConfig
                : T extends "stepped-return"
                    ? SteppedReturnGlidepathConfig
                    : never

    Utility type to extract the configuration type for a specific glidepath mode.

    +

    Type Parameters

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.ContributionTiming.html b/docs/types/src_types_retirementCalculatorTypes.ContributionTiming.html new file mode 100644 index 0000000..79a1000 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.ContributionTiming.html @@ -0,0 +1,6 @@ +ContributionTiming | retirement-calculator - v1.1.0
    ContributionTiming: "start" | "end"

    Defines when monthly contributions are added within each month.

    +
      +
    • 'start': Add contribution first, then apply interest
    • +
    • 'end': Apply interest first, then add contribution
    • +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsGlidepathConfig.html b/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsGlidepathConfig.html new file mode 100644 index 0000000..9a1f7a2 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsGlidepathConfig.html @@ -0,0 +1,25 @@ +CustomWaypointsGlidepathConfig | retirement-calculator - v1.1.0
    CustomWaypointsGlidepathConfig: {
        mode: "custom-waypoints";
        waypoints: GlidepathWaypoint[];
        valueType: "return" | "equityWeight";
        equityReturn?: number;
        bondReturn?: number;
    }

    Configuration for custom waypoints glidepath that uses user-defined +age/value pairs with linear interpolation between points.

    +

    Use case: Custom investment strategies with specific target allocations or returns at key ages

    +

    Type declaration

    • mode: "custom-waypoints"

      Discriminator for the glidepath configuration type.

      +
    • waypoints: GlidepathWaypoint[]

      Array of waypoints defining the glidepath curve. +Must contain at least one waypoint. +Waypoints will be automatically sorted by age.

      +
    • valueType: "return" | "equityWeight"

      Specifies what the waypoint values represent.

      +
        +
      • 'return': Waypoint values are annual return rates
      • +
      • 'equityWeight': Waypoint values are equity allocation percentages
      • +
      +
    • Optional equityReturn?: number

      Expected annual return for equity portion (decimal format). +Required when valueType is 'equityWeight', ignored when valueType is 'return'. +Must be > -1.0 (cannot lose more than 100%).

      +
    • Optional bondReturn?: number

      Expected annual return for bond portion (decimal format). +Required when valueType is 'equityWeight', ignored when valueType is 'return'. +Must be > -1.0 (cannot lose more than 100%).

      +

    Example

    Return-based waypoints

    +
    const config: CustomWaypointsGlidepathConfig = {
    mode: 'custom-waypoints',
    valueType: 'return',
    waypoints: [
    { age: 25, value: 0.12 }, // 12% at 25
    { age: 40, value: 0.08 }, // 8% at 40
    { age: 65, value: 0.05 } // 5% at 65
    ]
    }; +
    +

    Example

    Equity allocation waypoints

    +
    const config: CustomWaypointsGlidepathConfig = {
    mode: 'custom-waypoints',
    valueType: 'equityWeight',
    waypoints: [
    { age: 20, value: 1.0 }, // 100% equity at 20
    { age: 35, value: 0.8 }, // 80% equity at 35
    { age: 50, value: 0.6 }, // 60% equity at 50
    { age: 65, value: 0.3 } // 30% equity at 65
    ],
    equityReturn: 0.11,
    bondReturn: 0.035
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsWithReturns.html b/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsWithReturns.html new file mode 100644 index 0000000..4034bce --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.CustomWaypointsWithReturns.html @@ -0,0 +1,2 @@ +CustomWaypointsWithReturns | retirement-calculator - v1.1.0
    CustomWaypointsWithReturns: RequiredFields<CustomWaypointsGlidepathConfig, "equityReturn" | "bondReturn">

    Utility type for custom waypoints configuration with required equity/bond returns.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathConfig.html b/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathConfig.html new file mode 100644 index 0000000..0ba20fb --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathConfig.html @@ -0,0 +1,3 @@ +DynamicGlidepathConfig | retirement-calculator - v1.1.0

    Union type representing all possible glidepath configuration modes. +Uses discriminated union pattern with 'mode' as the discriminator.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathResult.html b/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathResult.html new file mode 100644 index 0000000..579db25 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.DynamicGlidepathResult.html @@ -0,0 +1,21 @@ +DynamicGlidepathResult | retirement-calculator - v1.1.0
    DynamicGlidepathResult: {
        finalBalance: number;
        totalContributions: number;
        totalInterestEarned: number;
        totalMonths: number;
        startAge: number;
        endAge: number;
        glidepathMode: GlidepathMode;
        monthlyTimeline: MonthlyTimelineEntry[];
        effectiveAnnualReturn: number;
        averageMonthlyReturn: number;
    }

    Comprehensive result object returned by the dynamic glidepath method. +Contains final calculations, metadata, timeline data, and summary statistics.

    +

    Type declaration

    • finalBalance: number

      Total account balance at the end of the simulation period. +Rounded to the nearest cent for precision.

      +
    • totalContributions: number

      Sum of all monthly contributions made during the simulation period.

      +
    • totalInterestEarned: number

      Total interest earned through compound growth during the simulation period.

      +
    • totalMonths: number

      Total number of months simulated. +Calculated as Math.ceil((endAge - startAge) * 12)

      +
    • startAge: number

      Starting age from the input parameters.

      +
    • endAge: number

      Ending age from the input parameters.

      +
    • glidepathMode: GlidepathMode

      The glidepath mode that was used for this calculation.

      +
    • monthlyTimeline: MonthlyTimelineEntry[]

      Detailed month-by-month timeline data. +Each entry represents the state at the end of that month. +Useful for creating charts and detailed analysis.

      +
    • effectiveAnnualReturn: number

      Effective compound annual growth rate over the entire simulation period. +Calculated as (finalBalance / initialBalance)^(1/years) - 1

      +
    • averageMonthlyReturn: number

      Average monthly return rate across all months in the simulation. +Simple arithmetic mean of all monthly return rates used.

      +

    Example

    const result: DynamicGlidepathResult = calculator.getCompoundInterestWithDynamicGlidepath(
    25000, 1000, 30, 65, config
    );

    console.log(`Final balance: $${result.finalBalance.toLocaleString()}`);
    console.log(`Total months: ${result.totalMonths}`);
    console.log(`Effective annual return: ${(result.effectiveAnnualReturn * 100).toFixed(2)}%`); +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.FixedReturnGlidepathConfig.html b/docs/types/src_types_retirementCalculatorTypes.FixedReturnGlidepathConfig.html new file mode 100644 index 0000000..a4f85e4 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.FixedReturnGlidepathConfig.html @@ -0,0 +1,15 @@ +FixedReturnGlidepathConfig | retirement-calculator - v1.1.0
    FixedReturnGlidepathConfig: {
        mode: "fixed-return";
        startReturn: number;
        endReturn: number;
    }

    Configuration for fixed return glidepath that linearly interpolates +between start and end return rates based on age.

    +

    Use case: Target-date funds that reduce returns as retirement approaches

    +

    Type declaration

    • mode: "fixed-return"

      Discriminator for the glidepath configuration type.

      +
    • startReturn: number

      Annual return rate at the starting age (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.10 represents 10% annual return
      +
      +
    • endReturn: number

      Annual return rate at the ending age (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.055 represents 5.5% annual return
      +
      +

    Example

    const config: FixedReturnGlidepathConfig = {
    mode: 'fixed-return',
    startReturn: 0.10, // 10% at starting age
    endReturn: 0.055 // 5.5% at ending age
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.GlidepathMode.html b/docs/types/src_types_retirementCalculatorTypes.GlidepathMode.html new file mode 100644 index 0000000..2c9acfd --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.GlidepathMode.html @@ -0,0 +1,2 @@ +GlidepathMode | retirement-calculator - v1.1.0
    GlidepathMode: "fixed-return" | "allocation-based" | "custom-waypoints" | "stepped-return"

    Identifies the type of glidepath strategy being used.

    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.GlidepathWaypoint.html b/docs/types/src_types_retirementCalculatorTypes.GlidepathWaypoint.html new file mode 100644 index 0000000..77ef7c2 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.GlidepathWaypoint.html @@ -0,0 +1,11 @@ +GlidepathWaypoint | retirement-calculator - v1.1.0
    GlidepathWaypoint: {
        age: number;
        value: number;
    }

    Represents a single waypoint in a custom glidepath configuration.

    +

    Type declaration

    • age: number

      Age at which this waypoint applies. +Must be positive.

      +
    • value: number

      Value at this waypoint.

      +
        +
      • If valueType is 'return': annual return rate (must be > -1.0)
      • +
      • If valueType is 'equityWeight': equity allocation percentage (0.0 to 1.0)
      • +
      +

    Example

    const waypoint: GlidepathWaypoint = {
    age: 35,
    value: 0.08 // 8% return or 80% equity weight, depending on valueType
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidAgeRangeError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidAgeRangeError.html new file mode 100644 index 0000000..db5f9fe --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidAgeRangeError.html @@ -0,0 +1,2 @@ +InvalidAgeRangeError | retirement-calculator - v1.1.0
    InvalidAgeRangeError: RetirementCalculatorError & {
        name: "InvalidAgeRangeError";
        message: "startAge must be less than endAge and both must be positive";
    }

    Error thrown when age parameters are invalid.

    +

    Type declaration

    • name: "InvalidAgeRangeError"
    • message: "startAge must be less than endAge and both must be positive"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidAllocationError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidAllocationError.html new file mode 100644 index 0000000..1302766 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidAllocationError.html @@ -0,0 +1,2 @@ +InvalidAllocationError | retirement-calculator - v1.1.0
    InvalidAllocationError: RetirementCalculatorError & {
        name: "InvalidAllocationError";
        message: "Equity weights must be between 0.0 and 1.0 inclusive";
    }

    Error thrown when allocation weights are invalid.

    +

    Type declaration

    • name: "InvalidAllocationError"
    • message: "Equity weights must be between 0.0 and 1.0 inclusive"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidFinancialParameterError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidFinancialParameterError.html new file mode 100644 index 0000000..8d75408 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidFinancialParameterError.html @@ -0,0 +1,2 @@ +InvalidFinancialParameterError | retirement-calculator - v1.1.0
    InvalidFinancialParameterError: RetirementCalculatorError & {
        name: "InvalidFinancialParameterError";
        message: "initialBalance and monthlyContribution must be non-negative";
    }

    Error thrown when financial parameters are invalid.

    +

    Type declaration

    • name: "InvalidFinancialParameterError"
    • message: "initialBalance and monthlyContribution must be non-negative"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidGlidepathConfigError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidGlidepathConfigError.html new file mode 100644 index 0000000..c30feb8 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidGlidepathConfigError.html @@ -0,0 +1,2 @@ +InvalidGlidepathConfigError | retirement-calculator - v1.1.0
    InvalidGlidepathConfigError: RetirementCalculatorError & {
        name: "InvalidGlidepathConfigError";
        message: string;
    }

    Error thrown when glidepath configuration is invalid.

    +

    Type declaration

    • name: "InvalidGlidepathConfigError"
    • message: string

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidReturnRateError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidReturnRateError.html new file mode 100644 index 0000000..1ec8d78 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidReturnRateError.html @@ -0,0 +1,2 @@ +InvalidReturnRateError | retirement-calculator - v1.1.0
    InvalidReturnRateError: RetirementCalculatorError & {
        name: "InvalidReturnRateError";
        message: "Return rates must be greater than -1.0 (cannot lose more than 100%)";
    }

    Error thrown when return rates are invalid.

    +

    Type declaration

    • name: "InvalidReturnRateError"
    • message: "Return rates must be greater than -1.0 (cannot lose more than 100%)"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.InvalidWaypointsError.html b/docs/types/src_types_retirementCalculatorTypes.InvalidWaypointsError.html new file mode 100644 index 0000000..60d534f --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.InvalidWaypointsError.html @@ -0,0 +1,2 @@ +InvalidWaypointsError | retirement-calculator - v1.1.0
    InvalidWaypointsError: RetirementCalculatorError & {
        name: "InvalidWaypointsError";
        message: "Waypoints array must contain at least one valid waypoint";
    }

    Error thrown when waypoints configuration is invalid.

    +

    Type declaration

    • name: "InvalidWaypointsError"
    • message: "Waypoints array must contain at least one valid waypoint"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.MonthlyTimelineEntry.html b/docs/types/src_types_retirementCalculatorTypes.MonthlyTimelineEntry.html new file mode 100644 index 0000000..6e3d834 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.MonthlyTimelineEntry.html @@ -0,0 +1,21 @@ +MonthlyTimelineEntry | retirement-calculator - v1.1.0
    MonthlyTimelineEntry: {
        month: number;
        age: number;
        currentBalance: number;
        cumulativeContributions: number;
        cumulativeInterest: number;
        monthlyInterestEarned: number;
        currentAnnualReturn: number;
        currentMonthlyReturn: number;
        currentEquityWeight?: number;
    }

    Represents the detailed state of the account at the end of a specific month. +Used for timeline visualization and detailed analysis.

    +

    Type declaration

    • month: number

      Month number in the simulation (1-based). +Month 1 is the first month, month 12 is the end of the first year, etc.

      +
    • age: number

      User's age at the end of this month. +Increments by 1/12 each month from the starting age.

      +
    • currentBalance: number

      Total account balance at the end of this month. +Includes initial balance, all contributions, and compound interest.

      +
    • cumulativeContributions: number

      Cumulative total of all contributions made up to and including this month.

      +
    • cumulativeInterest: number

      Cumulative total of all interest earned up to and including this month.

      +
    • monthlyInterestEarned: number

      Amount of interest earned during this specific month.

      +
    • currentAnnualReturn: number

      Annual return rate that was used for this month's calculation. +This is the rate determined by the glidepath strategy for the user's age this month.

      +
    • currentMonthlyReturn: number

      Monthly return rate that was applied during this month. +Converted from annual rate using: (1 + annualRate)^(1/12) - 1

      +
    • Optional currentEquityWeight?: number

      Current equity allocation weight for this month. +Only present for allocation-based and custom waypoints with equityWeight valueType. +Undefined for fixed-return glidepaths.

      +

    Example

    const entry: MonthlyTimelineEntry = {
    month: 120,
    age: 39.92,
    currentBalance: 125750.50,
    cumulativeContributions: 120000,
    cumulativeInterest: 5750.50,
    monthlyInterestEarned: 825.33,
    currentAnnualReturn: 0.085,
    currentMonthlyReturn: 0.006825,
    currentEquityWeight: 0.75 // Only present for allocation-based modes
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.NumericalPrecisionError.html b/docs/types/src_types_retirementCalculatorTypes.NumericalPrecisionError.html new file mode 100644 index 0000000..ac3dd9a --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.NumericalPrecisionError.html @@ -0,0 +1,2 @@ +NumericalPrecisionError | retirement-calculator - v1.1.0
    NumericalPrecisionError: RetirementCalculatorError & {
        name: "NumericalPrecisionError";
        message: "Numerical precision loss detected in calculation";
    }

    Error thrown when numerical precision loss is detected.

    +

    Type declaration

    • name: "NumericalPrecisionError"
    • message: "Numerical precision loss detected in calculation"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.RequiredFields.html b/docs/types/src_types_retirementCalculatorTypes.RequiredFields.html new file mode 100644 index 0000000..6db4e77 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.RequiredFields.html @@ -0,0 +1,2 @@ +RequiredFields | retirement-calculator - v1.1.0
    RequiredFields<T, K>: T & Required<Pick<T, K>>

    Utility type to make certain properties of a type required.

    +

    Type Parameters

    • T

    • K extends keyof T

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/ContributionFrequencyType.html b/docs/types/src_types_retirementCalculatorTypes.RetirementCalculatorError.html similarity index 56% rename from docs/interfaces/ContributionFrequencyType.html rename to docs/types/src_types_retirementCalculatorTypes.RetirementCalculatorError.html index 8fe3291..2adf76d 100644 --- a/docs/interfaces/ContributionFrequencyType.html +++ b/docs/types/src_types_retirementCalculatorTypes.RetirementCalculatorError.html @@ -1,8 +1,2 @@ -ContributionFrequencyType | retirement-calculator

    Interface ContributionFrequencyType

    Object representing contribution frequency options.

    -

    Hierarchy

    • ContributionFrequencyType

    Properties

    Properties

    YEARLY: number

    Yearly contribution frequency.

    -
    MONTHLY: number

    Monthly contribution frequency.

    -
    WEEKLY: number

    Weekly contribution frequency.

    -

    Generated using TypeDoc

    \ No newline at end of file +RetirementCalculatorError | retirement-calculator - v1.1.0
    RetirementCalculatorError: Error & {
        name: "RetirementCalculatorError";
    }

    Base error type for retirement calculator-specific errors.

    +

    Type declaration

    • name: "RetirementCalculatorError"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/types/src_types_retirementCalculatorTypes.SteppedReturnGlidepathConfig.html b/docs/types/src_types_retirementCalculatorTypes.SteppedReturnGlidepathConfig.html new file mode 100644 index 0000000..487a0c1 --- /dev/null +++ b/docs/types/src_types_retirementCalculatorTypes.SteppedReturnGlidepathConfig.html @@ -0,0 +1,28 @@ +SteppedReturnGlidepathConfig | retirement-calculator - v1.1.0
    SteppedReturnGlidepathConfig: {
        mode: "stepped-return";
        baseReturn: number;
        declineRate: number;
        terminalReturn: number;
        declineStartAge: number;
        terminalAge: number;
    }

    Configuration for stepped return glidepath that declines by a fixed rate per year +until reaching a terminal return, then holds that return.

    +

    Use case: Money Guy Show strategy and other step-down approaches

    +

    Type declaration

    • mode: "stepped-return"

      Discriminator for the glidepath configuration type.

      +
    • baseReturn: number

      Starting annual return rate before decline begins (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.10 represents 10% annual return
      +
      +
    • declineRate: number

      Annual decline rate applied each year (decimal format). +Must be >= 0 (cannot have negative decline rates).

      +

      Example

      0.001 represents 0.1% decline per year
      +
      +
    • terminalReturn: number

      Terminal annual return rate that is held after terminalAge (decimal format). +Must be > -1.0 (cannot lose more than 100%).

      +

      Example

      0.055 represents 5.5% annual return floor
      +
      +
    • declineStartAge: number

      Age at which the decline begins. +Must be positive and less than terminalAge.

      +

      Example

      20 means decline starts at age 20
      +
      +
    • terminalAge: number

      Age at which the terminal return is reached and held. +Must be greater than declineStartAge. +Defaults to 65 if not specified.

      +

      Example

      65 means terminal return is reached at age 65
      +
      +

    Example

    const config: SteppedReturnGlidepathConfig = {
    mode: 'stepped-return',
    baseReturn: 0.10, // 10% starting return
    declineRate: 0.001, // 0.1% decline per year
    terminalReturn: 0.055, // 5.5% floor
    declineStartAge: 20, // Start declining at 20
    terminalAge: 65 // Reach terminal at 65
    }; +
    +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_DEFAULTS.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_DEFAULTS.html new file mode 100644 index 0000000..dfce80c --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_DEFAULTS.html @@ -0,0 +1,16 @@ +GLIDEPATH_DEFAULTS | retirement-calculator - v1.1.0
    GLIDEPATH_DEFAULTS: {
        FIXED_RETURN: {
            START_RETURN: 0.1;
            END_RETURN: 0.055;
        };
        ALLOCATION_BASED: {
            START_EQUITY_WEIGHT: 0.9;
            END_EQUITY_WEIGHT: 0.3;
            EQUITY_RETURN: 0.12;
            BOND_RETURN: 0.04;
        };
        AGES: {
            START_AGE: 25;
            END_AGE: 65;
        };
    } = ...

    Default configuration values for dynamic glidepath calculations. +These provide sensible starting points for retirement planning scenarios.

    +

    Type declaration

    • Readonly FIXED_RETURN: {
          START_RETURN: 0.1;
          END_RETURN: 0.055;
      }

      Default fixed return glidepath configuration. +Represents a typical target-date fund progression.

      +
      • Readonly START_RETURN: 0.1

        Starting return rate for aggressive investing (10%)

        +
      • Readonly END_RETURN: 0.055

        Ending return rate for conservative investing (5.5%)

        +
    • Readonly ALLOCATION_BASED: {
          START_EQUITY_WEIGHT: 0.9;
          END_EQUITY_WEIGHT: 0.3;
          EQUITY_RETURN: 0.12;
          BOND_RETURN: 0.04;
      }

      Default allocation-based glidepath configuration. +Follows the common "120 minus age" equity allocation rule progression.

      +
      • Readonly START_EQUITY_WEIGHT: 0.9

        Starting equity weight for young investors (90%)

        +
      • Readonly END_EQUITY_WEIGHT: 0.3

        Ending equity weight near retirement (30%)

        +
      • Readonly EQUITY_RETURN: 0.12

        Expected annual return for equity investments (12%)

        +
      • Readonly BOND_RETURN: 0.04

        Expected annual return for bond investments (4%)

        +
    • Readonly AGES: {
          START_AGE: 25;
          END_AGE: 65;
      }

      Default ages for glidepath calculations.

      +
      • Readonly START_AGE: 25

        Typical starting career age

        +
      • Readonly END_AGE: 65

        Standard retirement age

        +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_ERROR_MESSAGES.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_ERROR_MESSAGES.html new file mode 100644 index 0000000..9044c1e --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_ERROR_MESSAGES.html @@ -0,0 +1,10 @@ +GLIDEPATH_ERROR_MESSAGES | retirement-calculator - v1.1.0
    GLIDEPATH_ERROR_MESSAGES: {
        AGES: {
            NEGATIVE_START_AGE: "startAge must be positive";
            NEGATIVE_END_AGE: "endAge must be positive";
            START_AGE_TOO_HIGH: "startAge must be less than endAge";
            AGE_DIFFERENCE_TOO_SMALL: "Age difference (endAge - startAge) must be at least 1 year";
            AGE_OUT_OF_BOUNDS: "Ages must be between 0.1 and 150 years";
        };
        FINANCIAL: {
            NEGATIVE_BALANCE: "initialBalance must be non-negative";
            NEGATIVE_CONTRIBUTION: "monthlyContribution must be non-negative";
            BALANCE_TOO_LARGE: "initialBalance exceeds maximum safe calculation limit";
        };
        RETURNS: {
            RETURN_TOO_LOW: "Return rates must be greater than -0.99 (cannot lose more than 99%)";
            RETURN_TOO_HIGH: "Return rates above 100% annual return are not supported";
            HIGH_RETURN_WARNING: "Return rates above 25% should be used with caution";
        };
        ALLOCATIONS: {
            WEIGHT_BELOW_ZERO: "Allocation weights must be between 0.0 and 1.0 inclusive";
            WEIGHT_ABOVE_ONE: "Allocation weights must be between 0.0 and 1.0 inclusive";
        };
        MODES: {
            INVALID_MODE: "Invalid glidepath mode specified";
            MISSING_REQUIRED_FIELDS: "Required fields are missing for the specified glidepath mode";
        };
        WAYPOINTS: {
            EMPTY_WAYPOINTS: "Waypoints array must contain at least one waypoint";
            INVALID_WAYPOINT_AGE: "Waypoint ages must be positive";
            INVALID_WAYPOINT_VALUE: "Waypoint values must be valid for the specified value type";
            TOO_MANY_WAYPOINTS: "Too many waypoints may impact performance";
            MISSING_EQUITY_RETURN: "equityReturn is required when valueType is \"equityWeight\"";
            MISSING_BOND_RETURN: "bondReturn is required when valueType is \"equityWeight\"";
        };
        GENERAL: {
            INVALID_CONTRIBUTION_TIMING: "contributionTiming must be \"start\" or \"end\"";
            CONFIGURATION_MISMATCH: "Glidepath configuration does not match the specified mode";
        };
    } = ...

    Error messages for dynamic glidepath validation failures. +These provide clear, actionable feedback to developers and users.

    +

    Type declaration

    • Readonly AGES: {
          NEGATIVE_START_AGE: "startAge must be positive";
          NEGATIVE_END_AGE: "endAge must be positive";
          START_AGE_TOO_HIGH: "startAge must be less than endAge";
          AGE_DIFFERENCE_TOO_SMALL: "Age difference (endAge - startAge) must be at least 1 year";
          AGE_OUT_OF_BOUNDS: "Ages must be between 0.1 and 150 years";
      }

      Age-related validation errors.

      +
      • Readonly NEGATIVE_START_AGE: "startAge must be positive"
      • Readonly NEGATIVE_END_AGE: "endAge must be positive"
      • Readonly START_AGE_TOO_HIGH: "startAge must be less than endAge"
      • Readonly AGE_DIFFERENCE_TOO_SMALL: "Age difference (endAge - startAge) must be at least 1 year"
      • Readonly AGE_OUT_OF_BOUNDS: "Ages must be between 0.1 and 150 years"
    • Readonly FINANCIAL: {
          NEGATIVE_BALANCE: "initialBalance must be non-negative";
          NEGATIVE_CONTRIBUTION: "monthlyContribution must be non-negative";
          BALANCE_TOO_LARGE: "initialBalance exceeds maximum safe calculation limit";
      }

      Financial parameter validation errors.

      +
      • Readonly NEGATIVE_BALANCE: "initialBalance must be non-negative"
      • Readonly NEGATIVE_CONTRIBUTION: "monthlyContribution must be non-negative"
      • Readonly BALANCE_TOO_LARGE: "initialBalance exceeds maximum safe calculation limit"
    • Readonly RETURNS: {
          RETURN_TOO_LOW: "Return rates must be greater than -0.99 (cannot lose more than 99%)";
          RETURN_TOO_HIGH: "Return rates above 100% annual return are not supported";
          HIGH_RETURN_WARNING: "Return rates above 25% should be used with caution";
      }

      Return rate validation errors.

      +
      • Readonly RETURN_TOO_LOW: "Return rates must be greater than -0.99 (cannot lose more than 99%)"
      • Readonly RETURN_TOO_HIGH: "Return rates above 100% annual return are not supported"
      • Readonly HIGH_RETURN_WARNING: "Return rates above 25% should be used with caution"
    • Readonly ALLOCATIONS: {
          WEIGHT_BELOW_ZERO: "Allocation weights must be between 0.0 and 1.0 inclusive";
          WEIGHT_ABOVE_ONE: "Allocation weights must be between 0.0 and 1.0 inclusive";
      }

      Allocation weight validation errors.

      +
      • Readonly WEIGHT_BELOW_ZERO: "Allocation weights must be between 0.0 and 1.0 inclusive"
      • Readonly WEIGHT_ABOVE_ONE: "Allocation weights must be between 0.0 and 1.0 inclusive"
    • Readonly MODES: {
          INVALID_MODE: "Invalid glidepath mode specified";
          MISSING_REQUIRED_FIELDS: "Required fields are missing for the specified glidepath mode";
      }

      Glidepath mode validation errors.

      +
      • Readonly INVALID_MODE: "Invalid glidepath mode specified"
      • Readonly MISSING_REQUIRED_FIELDS: "Required fields are missing for the specified glidepath mode"
    • Readonly WAYPOINTS: {
          EMPTY_WAYPOINTS: "Waypoints array must contain at least one waypoint";
          INVALID_WAYPOINT_AGE: "Waypoint ages must be positive";
          INVALID_WAYPOINT_VALUE: "Waypoint values must be valid for the specified value type";
          TOO_MANY_WAYPOINTS: "Too many waypoints may impact performance";
          MISSING_EQUITY_RETURN: "equityReturn is required when valueType is \"equityWeight\"";
          MISSING_BOND_RETURN: "bondReturn is required when valueType is \"equityWeight\"";
      }

      Waypoint validation errors.

      +
      • Readonly EMPTY_WAYPOINTS: "Waypoints array must contain at least one waypoint"
      • Readonly INVALID_WAYPOINT_AGE: "Waypoint ages must be positive"
      • Readonly INVALID_WAYPOINT_VALUE: "Waypoint values must be valid for the specified value type"
      • Readonly TOO_MANY_WAYPOINTS: "Too many waypoints may impact performance"
      • Readonly MISSING_EQUITY_RETURN: "equityReturn is required when valueType is \"equityWeight\""
      • Readonly MISSING_BOND_RETURN: "bondReturn is required when valueType is \"equityWeight\""
    • Readonly GENERAL: {
          INVALID_CONTRIBUTION_TIMING: "contributionTiming must be \"start\" or \"end\"";
          CONFIGURATION_MISMATCH: "Glidepath configuration does not match the specified mode";
      }

      General configuration errors.

      +
      • Readonly INVALID_CONTRIBUTION_TIMING: "contributionTiming must be \"start\" or \"end\""
      • Readonly CONFIGURATION_MISMATCH: "Glidepath configuration does not match the specified mode"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_MATH.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_MATH.html new file mode 100644 index 0000000..08d7190 --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_MATH.html @@ -0,0 +1,16 @@ +GLIDEPATH_MATH | retirement-calculator - v1.1.0
    GLIDEPATH_MATH: {
        PRECISION: {
            CURRENCY_DECIMALS: 2;
            CURRENCY_MULTIPLIER: 100;
            PERCENTAGE_DECIMALS: 6;
            INTEREST_RATE_DECIMALS: 10;
        };
        EPSILON: {
            GENERAL: 1e-10;
            CURRENCY: 0.0001;
            PERCENTAGE: 1e-8;
        };
        CONVERSION: {
            MONTHS_PER_YEAR: 12;
            WEEKS_PER_YEAR: 52;
            DAYS_PER_YEAR: 365.25;
        };
    } = ...

    Mathematical constants and precision settings for glidepath calculations. +These ensure numerical stability and consistent rounding behavior.

    +

    Type declaration

    • Readonly PRECISION: {
          CURRENCY_DECIMALS: 2;
          CURRENCY_MULTIPLIER: 100;
          PERCENTAGE_DECIMALS: 6;
          INTEREST_RATE_DECIMALS: 10;
      }

      Precision and rounding constants.

      +
      • Readonly CURRENCY_DECIMALS: 2

        Number of decimal places for monetary amounts

        +
      • Readonly CURRENCY_MULTIPLIER: 100

        Multiplier for currency rounding (100 for cents)

        +
      • Readonly PERCENTAGE_DECIMALS: 6

        Number of decimal places for percentage calculations

        +
      • Readonly INTEREST_RATE_DECIMALS: 10

        Number of decimal places for interest rate calculations

        +
    • Readonly EPSILON: {
          GENERAL: 1e-10;
          CURRENCY: 0.0001;
          PERCENTAGE: 1e-8;
      }

      Epsilon values for floating-point comparisons.

      +
      • Readonly GENERAL: 1e-10

        General floating-point comparison tolerance

        +
      • Readonly CURRENCY: 0.0001

        Currency comparison tolerance (0.01 cents)

        +
      • Readonly PERCENTAGE: 1e-8

        Percentage comparison tolerance

        +
    • Readonly CONVERSION: {
          MONTHS_PER_YEAR: 12;
          WEEKS_PER_YEAR: 52;
          DAYS_PER_YEAR: 365.25;
      }

      Mathematical conversion constants.

      +
      • Readonly MONTHS_PER_YEAR: 12

        Months per year for age calculations

        +
      • Readonly WEEKS_PER_YEAR: 52

        Weeks per year for contribution calculations

        +
      • Readonly DAYS_PER_YEAR: 365.25

        Days per year for precise calculations

        +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PERFORMANCE.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PERFORMANCE.html new file mode 100644 index 0000000..12eb62f --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PERFORMANCE.html @@ -0,0 +1,9 @@ +GLIDEPATH_PERFORMANCE | retirement-calculator - v1.1.0
    GLIDEPATH_PERFORMANCE: {
        TIMELINE: {
            LARGE_SIMULATION_MONTHS: 600;
            PERFORMANCE_WARNING_MONTHS: 1200;
        };
        CACHE: {
            MAX_RATE_CACHE_SIZE: 1000;
            MAX_INTERPOLATION_CACHE_SIZE: 500;
        };
    } = ...

    Performance optimization thresholds for glidepath calculations. +These help determine when to use memory-optimized calculation modes.

    +

    Type declaration

    • Readonly TIMELINE: {
          LARGE_SIMULATION_MONTHS: 600;
          PERFORMANCE_WARNING_MONTHS: 1200;
      }

      Timeline generation thresholds.

      +
      • Readonly LARGE_SIMULATION_MONTHS: 600

        Number of months above which to consider memory optimization

        +
      • Readonly PERFORMANCE_WARNING_MONTHS: 1200

        Number of months above which to warn about performance

        +
    • Readonly CACHE: {
          MAX_RATE_CACHE_SIZE: 1000;
          MAX_INTERPOLATION_CACHE_SIZE: 500;
      }

      Cache size limits for performance optimization.

      +
      • Readonly MAX_RATE_CACHE_SIZE: 1000

        Maximum number of cached monthly rate conversions

        +
      • Readonly MAX_INTERPOLATION_CACHE_SIZE: 500

        Maximum number of cached waypoint interpolations

        +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PRESETS.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PRESETS.html new file mode 100644 index 0000000..651670f --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_PRESETS.html @@ -0,0 +1,23 @@ +GLIDEPATH_PRESETS | retirement-calculator - v1.1.0
    GLIDEPATH_PRESETS: {
        MONEY_GUY_SHOW: {
            mode: "stepped-return";
            baseReturn: 0.1;
            declineRate: 0.001;
            terminalReturn: 0.055;
            declineStartAge: 20;
            terminalAge: 65;
        };
        BOGLEHEADS_100_MINUS_AGE: {
            mode: "custom-waypoints";
            valueType: "equityWeight";
            waypoints: GlidepathWaypoint[];
            equityReturn: number;
            bondReturn: number;
        };
        BOGLEHEADS_110_MINUS_AGE: {
            mode: "custom-waypoints";
            valueType: "equityWeight";
            waypoints: GlidepathWaypoint[];
            equityReturn: number;
            bondReturn: number;
        };
        BOGLEHEADS_120_MINUS_AGE: {
            mode: "custom-waypoints";
            valueType: "equityWeight";
            waypoints: GlidepathWaypoint[];
            equityReturn: number;
            bondReturn: number;
        };
    } = ...

    Pre-configured glidepath strategies based on popular financial planning approaches. +Each preset includes documentation links to the original methodology.

    +

    Type declaration

    • Readonly MONEY_GUY_SHOW: {
          mode: "stepped-return";
          baseReturn: 0.1;
          declineRate: 0.001;
          terminalReturn: 0.055;
          declineStartAge: 20;
          terminalAge: 65;
      }

      Money Guy Show strategy: 10% returns declining 0.1% per year to 5.5% floor at age 65.

      +

      This strategy holds 10% returns until age 20, then declines by exactly 0.1% per year +until reaching the 5.5% floor at age 65, then holds that terminal return.

      +

      Source: https://www.moneyguy.com/ +Reference: Financial Order of Operations and investment return assumptions

      +
      • Readonly mode: "stepped-return"
      • Readonly baseReturn: 0.1
      • Readonly declineRate: 0.001
      • Readonly terminalReturn: 0.055
      • Readonly declineStartAge: 20
      • Readonly terminalAge: 65
    • Readonly BOGLEHEADS_100_MINUS_AGE: {
          mode: "custom-waypoints";
          valueType: "equityWeight";
          waypoints: GlidepathWaypoint[];
          equityReturn: number;
          bondReturn: number;
      }

      Bogleheads "100 minus age" equity allocation strategy.

      +

      Conservative interpretation with minimum 20% equity allocation. +Uses historical US total stock market returns (10%) and intermediate bonds (4%).

      +

      Source: https://www.bogleheads.org/wiki/Asset_allocation +Reference: Age-based allocation guidelines and three-fund portfolio

      +
      • mode: "custom-waypoints"
      • valueType: "equityWeight"
      • waypoints: GlidepathWaypoint[]
      • equityReturn: number
      • bondReturn: number
    • Readonly BOGLEHEADS_110_MINUS_AGE: {
          mode: "custom-waypoints";
          valueType: "equityWeight";
          waypoints: GlidepathWaypoint[];
          equityReturn: number;
          bondReturn: number;
      }

      Bogleheads "110 minus age" more aggressive equity allocation strategy.

      +

      More aggressive interpretation for longer time horizons and higher risk tolerance. +Maintains higher equity allocation throughout the lifecycle.

      +

      Source: https://www.bogleheads.org/wiki/Asset_allocation +Reference: Age-based allocation variations for aggressive investors

      +
      • mode: "custom-waypoints"
      • valueType: "equityWeight"
      • waypoints: GlidepathWaypoint[]
      • equityReturn: number
      • bondReturn: number
    • Readonly BOGLEHEADS_120_MINUS_AGE: {
          mode: "custom-waypoints";
          valueType: "equityWeight";
          waypoints: GlidepathWaypoint[];
          equityReturn: number;
          bondReturn: number;
      }

      Bogleheads "120 minus age" very aggressive equity allocation strategy.

      +

      Most aggressive interpretation for very long time horizons and high risk tolerance. +Suitable for young investors with decades until retirement.

      +

      Source: https://www.bogleheads.org/wiki/Asset_allocation +Reference: Age-based allocation variations for very aggressive investors

      +
      • mode: "custom-waypoints"
      • valueType: "equityWeight"
      • waypoints: GlidepathWaypoint[]
      • equityReturn: number
      • bondReturn: number

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_TEMPLATES.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_TEMPLATES.html new file mode 100644 index 0000000..dca3da2 --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_TEMPLATES.html @@ -0,0 +1,12 @@ +GLIDEPATH_TEMPLATES | retirement-calculator - v1.1.0
    GLIDEPATH_TEMPLATES: {
        CONSERVATIVE: {
            FIXED_RETURN: {
                START_RETURN: 0.07;
                END_RETURN: 0.04;
            };
            ALLOCATION_BASED: {
                START_EQUITY_WEIGHT: 0.6;
                END_EQUITY_WEIGHT: 0.2;
                EQUITY_RETURN: 0.1;
                BOND_RETURN: 0.035;
            };
        };
        AGGRESSIVE: {
            FIXED_RETURN: {
                START_RETURN: 0.13;
                END_RETURN: 0.07;
            };
            ALLOCATION_BASED: {
                START_EQUITY_WEIGHT: 1;
                END_EQUITY_WEIGHT: 0.5;
                EQUITY_RETURN: 0.15;
                BOND_RETURN: 0.04;
            };
        };
        MODERATE: {
            FIXED_RETURN: {
                START_RETURN: 0.1;
                END_RETURN: 0.055;
            };
            ALLOCATION_BASED: {
                START_EQUITY_WEIGHT: 0.9;
                END_EQUITY_WEIGHT: 0.3;
                EQUITY_RETURN: 0.12;
                BOND_RETURN: 0.04;
            };
        };
    } = ...

    Configuration templates for common glidepath scenarios. +These provide ready-to-use configurations for typical retirement planning needs.

    +

    Type declaration

    • Readonly CONSERVATIVE: {
          FIXED_RETURN: {
              START_RETURN: 0.07;
              END_RETURN: 0.04;
          };
          ALLOCATION_BASED: {
              START_EQUITY_WEIGHT: 0.6;
              END_EQUITY_WEIGHT: 0.2;
              EQUITY_RETURN: 0.1;
              BOND_RETURN: 0.035;
          };
      }

      Conservative glidepath with lower volatility.

      +
      • Readonly FIXED_RETURN: {
            START_RETURN: 0.07;
            END_RETURN: 0.04;
        }
        • Readonly START_RETURN: 0.07
        • Readonly END_RETURN: 0.04
      • Readonly ALLOCATION_BASED: {
            START_EQUITY_WEIGHT: 0.6;
            END_EQUITY_WEIGHT: 0.2;
            EQUITY_RETURN: 0.1;
            BOND_RETURN: 0.035;
        }
        • Readonly START_EQUITY_WEIGHT: 0.6
        • Readonly END_EQUITY_WEIGHT: 0.2
        • Readonly EQUITY_RETURN: 0.1
        • Readonly BOND_RETURN: 0.035
    • Readonly AGGRESSIVE: {
          FIXED_RETURN: {
              START_RETURN: 0.13;
              END_RETURN: 0.07;
          };
          ALLOCATION_BASED: {
              START_EQUITY_WEIGHT: 1;
              END_EQUITY_WEIGHT: 0.5;
              EQUITY_RETURN: 0.15;
              BOND_RETURN: 0.04;
          };
      }

      Aggressive glidepath with higher growth potential.

      +
      • Readonly FIXED_RETURN: {
            START_RETURN: 0.13;
            END_RETURN: 0.07;
        }
        • Readonly START_RETURN: 0.13
        • Readonly END_RETURN: 0.07
      • Readonly ALLOCATION_BASED: {
            START_EQUITY_WEIGHT: 1;
            END_EQUITY_WEIGHT: 0.5;
            EQUITY_RETURN: 0.15;
            BOND_RETURN: 0.04;
        }
        • Readonly START_EQUITY_WEIGHT: 1
        • Readonly END_EQUITY_WEIGHT: 0.5
        • Readonly EQUITY_RETURN: 0.15
        • Readonly BOND_RETURN: 0.04
    • Readonly MODERATE: {
          FIXED_RETURN: {
              START_RETURN: 0.1;
              END_RETURN: 0.055;
          };
          ALLOCATION_BASED: {
              START_EQUITY_WEIGHT: 0.9;
              END_EQUITY_WEIGHT: 0.3;
              EQUITY_RETURN: 0.12;
              BOND_RETURN: 0.04;
          };
      }

      Moderate/balanced glidepath (same as defaults).

      +
      • Readonly FIXED_RETURN: {
            START_RETURN: 0.1;
            END_RETURN: 0.055;
        }
        • Readonly START_RETURN: 0.1

          Starting return rate for aggressive investing (10%)

          +
        • Readonly END_RETURN: 0.055

          Ending return rate for conservative investing (5.5%)

          +
      • Readonly ALLOCATION_BASED: {
            START_EQUITY_WEIGHT: 0.9;
            END_EQUITY_WEIGHT: 0.3;
            EQUITY_RETURN: 0.12;
            BOND_RETURN: 0.04;
        }
        • Readonly START_EQUITY_WEIGHT: 0.9

          Starting equity weight for young investors (90%)

          +
        • Readonly END_EQUITY_WEIGHT: 0.3

          Ending equity weight near retirement (30%)

          +
        • Readonly EQUITY_RETURN: 0.12

          Expected annual return for equity investments (12%)

          +
        • Readonly BOND_RETURN: 0.04

          Expected annual return for bond investments (4%)

          +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_VALIDATION.html b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_VALIDATION.html new file mode 100644 index 0000000..5d5b5fc --- /dev/null +++ b/docs/variables/src_constants_retirementCalculatorConstants.GLIDEPATH_VALIDATION.html @@ -0,0 +1,21 @@ +GLIDEPATH_VALIDATION | retirement-calculator - v1.1.0
    GLIDEPATH_VALIDATION: {
        AGES: {
            MIN_AGE: 0.1;
            MAX_AGE: 150;
            MIN_AGE_DIFFERENCE: 1;
        };
        RETURNS: {
            MIN_RETURN: -0.99;
            MAX_RETURN: 1;
            HIGH_RETURN_WARNING: 0.25;
        };
        ALLOCATIONS: {
            MIN_WEIGHT: 0;
            MAX_WEIGHT: 1;
        };
        FINANCIAL: {
            MIN_BALANCE: 0;
            MIN_CONTRIBUTION: 0;
            MAX_BALANCE: number;
        };
        WAYPOINTS: {
            MIN_WAYPOINTS: 1;
            MAX_WAYPOINTS: 100;
        };
    } = ...

    Validation constraints for dynamic glidepath parameters. +These ensure mathematical soundness and prevent invalid configurations.

    +

    Type declaration

    • Readonly AGES: {
          MIN_AGE: 0.1;
          MAX_AGE: 150;
          MIN_AGE_DIFFERENCE: 1;
      }

      Age validation limits.

      +
      • Readonly MIN_AGE: 0.1

        Minimum allowed age (must be positive)

        +
      • Readonly MAX_AGE: 150

        Maximum reasonable age for calculations

        +
      • Readonly MIN_AGE_DIFFERENCE: 1

        Minimum age difference for meaningful glidepath

        +
    • Readonly RETURNS: {
          MIN_RETURN: -0.99;
          MAX_RETURN: 1;
          HIGH_RETURN_WARNING: 0.25;
      }

      Return rate validation limits.

      +
      • Readonly MIN_RETURN: -0.99

        Minimum return rate (-99% loss maximum)

        +
      • Readonly MAX_RETURN: 1

        Maximum reasonable return rate (100% annual gain)

        +
      • Readonly HIGH_RETURN_WARNING: 0.25

        Typical range warning threshold for high returns

        +
    • Readonly ALLOCATIONS: {
          MIN_WEIGHT: 0;
          MAX_WEIGHT: 1;
      }

      Allocation weight validation limits.

      +
      • Readonly MIN_WEIGHT: 0

        Minimum allocation weight (0%)

        +
      • Readonly MAX_WEIGHT: 1

        Maximum allocation weight (100%)

        +
    • Readonly FINANCIAL: {
          MIN_BALANCE: 0;
          MIN_CONTRIBUTION: 0;
          MAX_BALANCE: number;
      }

      Financial parameter validation limits.

      +
      • Readonly MIN_BALANCE: 0

        Minimum balance (must be non-negative)

        +
      • Readonly MIN_CONTRIBUTION: 0

        Minimum contribution (must be non-negative)

        +
      • Readonly MAX_BALANCE: number

        Maximum reasonable balance for calculations

        +
    • Readonly WAYPOINTS: {
          MIN_WAYPOINTS: 1;
          MAX_WAYPOINTS: 100;
      }

      Waypoint validation limits.

      +
      • Readonly MIN_WAYPOINTS: 1

        Minimum number of waypoints required

        +
      • Readonly MAX_WAYPOINTS: 100

        Maximum recommended waypoints for performance

        +

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/variables/src_errors_DynamicGlidepathErrors.GLIDEPATH_ERROR_CODES.html b/docs/variables/src_errors_DynamicGlidepathErrors.GLIDEPATH_ERROR_CODES.html new file mode 100644 index 0000000..471fef9 --- /dev/null +++ b/docs/variables/src_errors_DynamicGlidepathErrors.GLIDEPATH_ERROR_CODES.html @@ -0,0 +1,3 @@ +GLIDEPATH_ERROR_CODES | retirement-calculator - v1.1.0
    GLIDEPATH_ERROR_CODES: {
        NEGATIVE_AGE: "NEGATIVE_AGE";
        INVALID_AGE_RANGE: "INVALID_AGE_RANGE";
        INSUFFICIENT_TIME_HORIZON: "INSUFFICIENT_TIME_HORIZON";
        NEGATIVE_BALANCE: "NEGATIVE_BALANCE";
        NEGATIVE_CONTRIBUTION: "NEGATIVE_CONTRIBUTION";
        IMPOSSIBLE_LOSS: "IMPOSSIBLE_LOSS";
        INVALID_RETURN_TYPE: "INVALID_RETURN_TYPE";
        INVALID_ALLOCATION: "INVALID_ALLOCATION";
        INVALID_ALLOCATION_TYPE: "INVALID_ALLOCATION_TYPE";
        INVALID_CONFIG_STRUCTURE: "INVALID_CONFIG_STRUCTURE";
        UNKNOWN_GLIDEPATH_MODE: "UNKNOWN_GLIDEPATH_MODE";
        INVALID_CONTRIBUTION_TIMING: "INVALID_CONTRIBUTION_TIMING";
        EMPTY_WAYPOINTS: "EMPTY_WAYPOINTS";
        INVALID_WAYPOINT_AGE: "INVALID_WAYPOINT_AGE";
        INVALID_WAYPOINT_VALUE: "INVALID_WAYPOINT_VALUE";
        INVALID_VALUE_TYPE: "INVALID_VALUE_TYPE";
        MISSING_EQUITY_RETURN: "MISSING_EQUITY_RETURN";
        MISSING_BOND_RETURN: "MISSING_BOND_RETURN";
        LONG_SIMULATION_WARNING: "LONG_SIMULATION_WARNING";
        LARGE_CONTRIBUTION_WARNING: "LARGE_CONTRIBUTION_WARNING";
        UNUSUAL_ALLOCATION_PROGRESSION: "UNUSUAL_ALLOCATION_PROGRESSION";
        EXTREME_RETURN_WARNING: "EXTREME_RETURN_WARNING";
        EXTREME_LOSS_WARNING: "EXTREME_LOSS_WARNING";
        WAYPOINTS_OUTSIDE_RANGE: "WAYPOINTS_OUTSIDE_RANGE";
        WAYPOINT_GAP_WARNING: "WAYPOINT_GAP_WARNING";
        CALCULATION_ERROR: "CALCULATION_ERROR";
        STRATEGY_CREATION_ERROR: "STRATEGY_CREATION_ERROR";
        SIMULATION_ERROR: "SIMULATION_ERROR";
    } = ...

    Enumeration of all possible error codes for dynamic glidepath validation. +These codes provide machine-readable error identification.

    +

    Type declaration

    • Readonly NEGATIVE_AGE: "NEGATIVE_AGE"
    • Readonly INVALID_AGE_RANGE: "INVALID_AGE_RANGE"
    • Readonly INSUFFICIENT_TIME_HORIZON: "INSUFFICIENT_TIME_HORIZON"
    • Readonly NEGATIVE_BALANCE: "NEGATIVE_BALANCE"
    • Readonly NEGATIVE_CONTRIBUTION: "NEGATIVE_CONTRIBUTION"
    • Readonly IMPOSSIBLE_LOSS: "IMPOSSIBLE_LOSS"
    • Readonly INVALID_RETURN_TYPE: "INVALID_RETURN_TYPE"
    • Readonly INVALID_ALLOCATION: "INVALID_ALLOCATION"
    • Readonly INVALID_ALLOCATION_TYPE: "INVALID_ALLOCATION_TYPE"
    • Readonly INVALID_CONFIG_STRUCTURE: "INVALID_CONFIG_STRUCTURE"
    • Readonly UNKNOWN_GLIDEPATH_MODE: "UNKNOWN_GLIDEPATH_MODE"
    • Readonly INVALID_CONTRIBUTION_TIMING: "INVALID_CONTRIBUTION_TIMING"
    • Readonly EMPTY_WAYPOINTS: "EMPTY_WAYPOINTS"
    • Readonly INVALID_WAYPOINT_AGE: "INVALID_WAYPOINT_AGE"
    • Readonly INVALID_WAYPOINT_VALUE: "INVALID_WAYPOINT_VALUE"
    • Readonly INVALID_VALUE_TYPE: "INVALID_VALUE_TYPE"
    • Readonly MISSING_EQUITY_RETURN: "MISSING_EQUITY_RETURN"
    • Readonly MISSING_BOND_RETURN: "MISSING_BOND_RETURN"
    • Readonly LONG_SIMULATION_WARNING: "LONG_SIMULATION_WARNING"
    • Readonly LARGE_CONTRIBUTION_WARNING: "LARGE_CONTRIBUTION_WARNING"
    • Readonly UNUSUAL_ALLOCATION_PROGRESSION: "UNUSUAL_ALLOCATION_PROGRESSION"
    • Readonly EXTREME_RETURN_WARNING: "EXTREME_RETURN_WARNING"
    • Readonly EXTREME_LOSS_WARNING: "EXTREME_LOSS_WARNING"
    • Readonly WAYPOINTS_OUTSIDE_RANGE: "WAYPOINTS_OUTSIDE_RANGE"
    • Readonly WAYPOINT_GAP_WARNING: "WAYPOINT_GAP_WARNING"
    • Readonly CALCULATION_ERROR: "CALCULATION_ERROR"
    • Readonly STRATEGY_CREATION_ERROR: "STRATEGY_CREATION_ERROR"
    • Readonly SIMULATION_ERROR: "SIMULATION_ERROR"

    Generated using TypeDoc

    \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..76c95b1 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,138 @@ +# 💰 Retirement Calculator Examples + +Welcome to the retirement planning examples! These examples show real-world scenarios to help you understand different approaches to retirement planning using this free, open-source retirement calculator package. + +## 🚀 Quick Start + +```bash +# Run any example +npx ts-node examples/basic-retirement-gap-analysis.ts +npx ts-node examples/lifestyle-based-retirement-planning.ts +npx ts-node examples/advanced-dynamic-investment-strategies.ts +``` + +## 📊 Examples Overview + +### 1. **Basic Retirement Gap Analysis** +**File:** `basic-retirement-gap-analysis.ts` + +**Perfect for:** People who have a specific retirement balance goal (like "$1 million") and want to see if their current savings rate is sufficient. + +**What you'll learn:** +- ✅ How to calculate your "retirement gap" +- ✅ The power of compound interest over time +- ✅ Why inflation matters for long-term planning +- ✅ How small increases in contributions make huge differences + +**Sample output:** +``` +🚨 RETIREMENT GAP: $234,567 (23.5% short) +💡 Additional monthly needed: $89 + That's only $2.97 more per day! +``` + +--- + +### 2. **Lifestyle-Based Retirement Planning** +**File:** `lifestyle-based-retirement-planning.ts` + +**Perfect for:** People who think in terms of "I want to spend $X per year in retirement" rather than accumulating a lump sum. + +**What you'll learn:** +- ✅ How the 4% withdrawal rule works in practice +- ✅ Working backwards from desired lifestyle to savings needed +- ✅ Comparing different retirement lifestyle scenarios +- ✅ The shocking impact of inflation over 30-40 years + +**Sample output:** +``` +🏠 To spend $80,000/year, you need: $2,000,000 in retirement savings +💸 Monthly contribution needed: $547 +🔥 That's only $18.23 per day! +``` + +--- + +### 3. **Advanced Dynamic Investment Strategies** +**File:** `advanced-dynamic-investment-strategies.ts` + +**Perfect for:** Advanced users who want to model changing investment strategies over time (like target-date funds) and compare sophisticated approaches. + +**What you'll learn:** +- ✅ How investment allocation changes as you age +- ✅ Different glidepath strategies (conservative to aggressive) +- ✅ Popular strategies like "100 minus age" equity allocation +- ✅ How dynamic strategies can optimize returns over time +- ✅ Why front-loaded returns compound more effectively + +**Sample output:** +``` +🎉 YOUR MONEY GUY SHOW RESULTS: + Final retirement balance: $1,847,293 + Total contributions: $360,000 + FREE money from compound growth: $1,487,293 + Your effective annual return: 8.1% + Monthly retirement income (4% rule): $6,156 +``` + +## 🎯 Which Example Should You Use? + +| Your Situation | Recommended Example | +|----------------|-------------------| +| "I want to save $1 million for retirement" | Basic Retirement Gap Analysis | +| "I want to spend $60K/year in retirement" | Lifestyle-Based Planning | +| "I want to optimize my investment strategy over time" | Advanced Dynamic Investment Strategies | +| "I'm just starting and want to understand the basics" | Basic Retirement Gap Analysis | +| "I want to see different retirement lifestyle costs" | Lifestyle-Based Planning | + +## 💡 Key Educational Concepts + +### The 4% Withdrawal Rule +For every **$40,000/year** you want in retirement income, you need about **$1,000,000** in savings. + +### The Power of Starting Early +Starting retirement savings at 25 vs 35 can mean the difference between saving $300/month vs $800/month for the same retirement outcome. + +### Inflation is a Silent Wealth Killer +At 2.5% inflation, prices double every 28 years. $50,000 today will cost $100,000 in 28 years. + +### Compound Interest is Your Best Friend +Albert Einstein allegedly called compound interest "the eighth wonder of the world." These examples show why. + +## 🛠 Customizing the Examples + +Each example is heavily commented and easy to modify: + +```typescript +// Change these variables to match your situation: +const startingBalance: number = 25000; // Your current savings +const desiredBalance: number = 1500000; // Your retirement goal +const yearsUntilRetirement: number = 30; // Time horizon +const interestRate: number = 7; // Expected annual return +const currentContributionAmount: number = 400; // Monthly savings +``` + +## 📚 Educational Use + +These examples are designed to be: +- **📖 Self-explanatory** - Lots of comments and context +- **🎓 Educational** - Learn financial concepts through real examples +- **💡 Actionable** - Get specific advice on what to do next +- **🔧 Customizable** - Easy to modify for your personal situation + +## 🚀 Next Steps + +1. **Run the examples** to see how they work +2. **Modify the variables** to match your situation +3. **Compare different scenarios** to optimize your strategy +4. **Use the insights** to make real changes to your retirement planning + +## 💬 Questions? + +This free, open-source package makes retirement planning accessible to everyone. Each example teaches core concepts while providing actionable insights for your financial future. + +**Remember:** These examples assume steady returns and inflation rates. Real markets fluctuate. Consider multiple scenarios and consult with financial professionals for personalized advice. + +--- + +*Happy planning! 🎯 Your future self will thank you for starting today.* \ No newline at end of file diff --git a/examples/advanced-dynamic-investment-strategies.ts b/examples/advanced-dynamic-investment-strategies.ts new file mode 100644 index 0000000..6f6bada --- /dev/null +++ b/examples/advanced-dynamic-investment-strategies.ts @@ -0,0 +1,476 @@ +/** + * @fileoverview Advanced Dynamic Investment Strategies Example + * + * WHEN TO USE THIS: + * - You want to model changing investment strategies over time (like target-date funds) + * - You're comparing different glidepath approaches for retirement planning + * - You want to understand how investment allocation changes as you age + * - You need sophisticated modeling beyond simple fixed returns + * + * PERFECT FOR: + * - Advanced investors who want to optimize returns over time + * - People using target-date funds who want to understand the underlying strategy + * - Financial planners modeling complex allocation strategies + * - Anyone interested in age-based investment approaches + * + * KEY LEARNING OUTCOMES: + * - How dynamic investment strategies can optimize returns over decades + * - Popular strategies like "100 minus age" equity allocation + * - The difference between simple and sophisticated retirement planning + * - How front-loaded returns compound more than back-loaded returns + * - Why Monte Carlo analysis is needed for real investment decisions + * + * IMPORTANT NOTE: + * These examples compare different strategies for educational purposes only. + * They assume steady returns in idealized conditions. For actual investment + * decisions, use Monte Carlo analysis (coming soon) to account for market + * volatility and find the strategy that matches your risk tolerance. + */ + +import RetirementCalculator from '../src/RetirementCalculator'; +import type { + FixedReturnGlidepathConfig, + AllocationBasedGlidepathConfig, + CustomWaypointsGlidepathConfig, +} from '../src/types/retirementCalculatorTypes'; +import { GLIDEPATH_PRESETS } from '../src/constants/retirementCalculatorConstants'; + +const calculator = new RetirementCalculator(); + +console.log('🚀 ADVANCED DYNAMIC INVESTMENT STRATEGIES'); +console.log('=' .repeat(70)); +console.log('🎯 MASTER-LEVEL RETIREMENT PLANNING:'); +console.log(' Learn how sophisticated investors optimize returns over time'); +console.log(' Compare different strategies used by target-date funds'); +console.log(' See why "set it and forget it" isn\'t always optimal'); +console.log(); + +// ============================================================================ +// SCENARIO 1: THE YOUNG PROFESSIONAL'S STRATEGY +// ============================================================================ + +console.log('📈 SCENARIO 1: The Young Professional\'s Strategy'); +console.log('-'.repeat(55)); +console.log('👤 YOU: 25-year-old professional, just starting your career'); +console.log('🎯 GOAL: Retire comfortably at 65 with maximum growth early on'); +console.log('📊 STRATEGY: Start aggressive (10% returns), get conservative as you age'); +console.log('💡 WHY: Higher returns early compound longer = bigger retirement balance'); +console.log(); + +const fixedReturnConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, // 10% at age 25 + endReturn: 0.055, // 5.5% at age 65 +}; + +try { + const fixedResult = calculator.getCompoundInterestWithGlidepath( + 25000, // Starting with $25,000 + 1000, // Contributing $1,000 monthly + 25, // Starting at age 25 + 65, // Retiring at age 65 + fixedReturnConfig + ); + + console.log(`🎉 YOUR RESULTS:`); + console.log(` Final retirement balance: $${calculator.formatNumberWithCommas(fixedResult.finalBalance)}`); + console.log(` Your total contributions: $${calculator.formatNumberWithCommas(fixedResult.totalContributions)}`); + console.log(` FREE money from compound growth: $${calculator.formatNumberWithCommas(fixedResult.totalInterestEarned)}`); + console.log(` Your effective annual return: ${(fixedResult.effectiveAnnualReturn * 100).toFixed(2)}%`); + console.log(` Years of financial freedom this buys: ${Math.floor(fixedResult.finalBalance / 50000)} years at $50K/year!`); + + console.log(`\n📊 How Your Strategy Changes Over Time:`); + [25, 35, 45, 55, 65].forEach((age) => { + const ageEntry = fixedResult.monthlyTimeline.find( + (entry) => Math.abs(entry.age - age) < 0.1 + ); + if (ageEntry) { + const stage = age <= 30 ? '🚀 AGGRESSIVE' : age <= 45 ? '📈 GROWTH' : age <= 55 ? '⚖️ BALANCED' : '🛡️ CONSERVATIVE'; + console.log( + ` Age ${age}: ${(ageEntry.currentAnnualReturn * 100).toFixed(2)}% return (${stage})` + ); + } + }); + + const dailyCost = (fixedResult.totalContributions / (40 * 365)).toFixed(2); + console.log(`\n💡 PERSPECTIVE: You invested only $${dailyCost} per day for 40 years`); + console.log(` That turned into $${calculator.formatNumberWithCommas(fixedResult.finalBalance)} - that\'s $${(fixedResult.finalBalance / (40 * 365)).toFixed(2)} per day of value!`); +} catch (error) { + console.error('❌ Fixed return calculation failed:', error); +} + +// ============================================================================ +// SCENARIO 2: THE TARGET-DATE FUND APPROACH +// ============================================================================ + +console.log('\n\n🎯 SCENARIO 2: The Target-Date Fund Approach'); +console.log('-'.repeat(60)); +console.log('👤 YOU: 30-year-old who wants a "set it and forget it" strategy'); +console.log('🎯 GOAL: Let professionals manage your allocation as you age'); +console.log('📊 STRATEGY: Start 90% stocks/10% bonds → End 30% stocks/70% bonds'); +console.log('💡 WHY: Reduces risk as you near retirement (can\'t afford big losses)'); +console.log(); + +const allocationConfig: AllocationBasedGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.9, // 90% equity at age 30 + endEquityWeight: 0.3, // 30% equity at age 65 + equityReturn: 0.12, // 12% equity returns + bondReturn: 0.04, // 4% bond returns +}; + +try { + const allocationResult = calculator.getCompoundInterestWithGlidepath( + 50000, // Starting with $50,000 + 1500, // Contributing $1,500 monthly + 30, // Starting at age 30 + 65, // Retiring at age 65 + allocationConfig + ); + + console.log(`🎉 YOUR TARGET-DATE RESULTS:`); + console.log(` Final retirement balance: $${calculator.formatNumberWithCommas(allocationResult.finalBalance)}`); + console.log(` Your total contributions: $${calculator.formatNumberWithCommas(allocationResult.totalContributions)}`); + console.log(` FREE money from compound growth: $${calculator.formatNumberWithCommas(allocationResult.totalInterestEarned)}`); + console.log(` Your effective annual return: ${(allocationResult.effectiveAnnualReturn * 100).toFixed(2)}%`); + + const monthlyWithdrawal = Math.floor(allocationResult.finalBalance * 0.04 / 12); + console.log(` Monthly retirement income (4% rule): $${calculator.formatNumberWithCommas(monthlyWithdrawal)}`); + + console.log(`\n📊 How Your Target-Date Fund Changes Over Time:`); + [30, 40, 50, 60, 65].forEach((age) => { + const ageEntry = allocationResult.monthlyTimeline.find( + (entry) => Math.abs(entry.age - age) < 0.1 + ); + if (ageEntry && ageEntry.currentEquityWeight !== undefined) { + const equityWeight = ageEntry.currentEquityWeight * 100; + const equityPercent = equityWeight.toFixed(0); + const bondPercent = ((1 - ageEntry.currentEquityWeight) * 100).toFixed(0); + const returnPercent = (ageEntry.currentAnnualReturn * 100).toFixed(2); + const riskLevel = equityWeight >= 70 ? '🔥 HIGH GROWTH' : equityWeight >= 50 ? '📈 MODERATE' : '🛡️ CONSERVATIVE'; + console.log( + ` Age ${age}: ${equityPercent}% stocks/${bondPercent}% bonds → ${returnPercent}% return (${riskLevel})` + ); + } + }); + + console.log(`\n💰 This is why target-date funds are popular - professional management`); + console.log(` without you having to think about rebalancing!`); +} catch (error) { + console.error('❌ Allocation-based calculation failed:', error); +} + +// ============================================================================ +// SCENARIO 3: THE SOPHISTICATED INVESTOR'S CUSTOM STRATEGY +// ============================================================================ + +console.log('\n\n🎨 SCENARIO 3: The Sophisticated Investor\'s Custom Strategy'); +console.log('-'.repeat(65)); +console.log('👤 YOU: Savvy investor who wants precise control over your strategy'); +console.log('🎯 GOAL: Custom allocation targets that match your specific risk tolerance'); +console.log('📊 STRATEGY: You set exact equity percentages at key life stages'); +console.log('💡 WHY: Maximum control - you decide exactly when and how to reduce risk'); +console.log(); + +const customConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 25, value: 1.0 }, // 100% equity at 25 + { age: 35, value: 0.85 }, // 85% equity at 35 + { age: 45, value: 0.7 }, // 70% equity at 45 + { age: 55, value: 0.5 }, // 50% equity at 55 + { age: 65, value: 0.25 }, // 25% equity at retirement + ], + equityReturn: 0.11, // 11% equity returns + bondReturn: 0.035, // 3.5% bond returns +}; + +try { + const customResult = calculator.getCompoundInterestWithGlidepath( + 15000, // Starting with $15,000 + 800, // Contributing $800 monthly + 25, // Starting at age 25 + 65, // Retiring at age 65 + customConfig + ); + + console.log(`🎉 YOUR CUSTOM STRATEGY RESULTS:`); + console.log(` Final retirement balance: $${calculator.formatNumberWithCommas(customResult.finalBalance)}`); + console.log(` Your total contributions: $${calculator.formatNumberWithCommas(customResult.totalContributions)}`); + console.log(` FREE money from compound growth: $${calculator.formatNumberWithCommas(customResult.totalInterestEarned)}`); + console.log(` Your effective annual return: ${(customResult.effectiveAnnualReturn * 100).toFixed(2)}%`); + + const growthMultiple = (customResult.finalBalance / customResult.totalContributions).toFixed(1); + console.log(` Your money grew ${growthMultiple}x - every dollar became $${growthMultiple}!`); + + console.log(`\n📊 Your Precisely-Controlled Strategy:`); + customConfig.waypoints.forEach((waypoint) => { + const ageEntry = customResult.monthlyTimeline.find( + (entry) => Math.abs(entry.age - waypoint.age) < 0.1 + ); + if (ageEntry && ageEntry.currentEquityWeight !== undefined) { + const equityPercent = (ageEntry.currentEquityWeight * 100).toFixed(0); + const bondPercent = ((1 - ageEntry.currentEquityWeight) * 100).toFixed(0); + const returnPercent = (ageEntry.currentAnnualReturn * 100).toFixed(2); + const lifeStage = waypoint.age <= 30 ? '(Career Building)' : waypoint.age <= 45 ? '(Peak Earning)' : waypoint.age <= 55 ? '(Pre-Retirement)' : '(Near Retirement)'; + console.log( + ` Age ${waypoint.age}: ${equityPercent}% stocks/${bondPercent}% bonds → ${returnPercent}% return ${lifeStage}` + ); + } + }); + + console.log(`\n💡 This level of control lets you optimize for your specific situation!`); +} catch (error) { + console.error('❌ Custom waypoints calculation failed:', error); +} + +// ============================================================================ +// SCENARIO 4: THE MONEY GUY SHOW STRATEGY (POPULAR PRESET) +// ============================================================================ + +console.log('\n\n💰 SCENARIO 4: The Money Guy Show Strategy (Popular Preset)'); +console.log('-'.repeat(65)); +console.log('👤 YOU: Fan of The Money Guy Show podcast (or similar financial advice)'); +console.log('🎯 GOAL: Use their proven, conservative return assumptions'); +console.log('📊 STRATEGY: 10% returns until 20, then decline 0.1% yearly to 5.5% floor'); +console.log('💡 WHY: Based on long-term market analysis - realistic but optimistic'); +console.log(); + +try { + const moneyGuyResult = calculator.getCompoundInterestWithGlidepath( + 30000, // Starting with $30,000 + 1200, // Contributing $1,200 monthly + 22, // Starting at age 22 + 65, // Retiring at age 65 + GLIDEPATH_PRESETS.MONEY_GUY_SHOW + ); + + console.log(`🎉 YOUR MONEY GUY SHOW RESULTS:`); + console.log(` Final retirement balance: $${calculator.formatNumberWithCommas(moneyGuyResult.finalBalance)}`); + console.log(` Your total contributions: $${calculator.formatNumberWithCommas(moneyGuyResult.totalContributions)}`); + console.log(` FREE money from compound growth: $${calculator.formatNumberWithCommas(moneyGuyResult.totalInterestEarned)}`); + console.log(` Your effective annual return: ${(moneyGuyResult.effectiveAnnualReturn * 100).toFixed(2)}%`); + + const yearlyIncome = Math.floor(moneyGuyResult.finalBalance * 0.04); + console.log(` Annual retirement income (4% rule): $${calculator.formatNumberWithCommas(yearlyIncome)}`); + console.log(` That\'s $${Math.floor(yearlyIncome / 12).toLocaleString()} per month for life!`); + + console.log(`\n📊 The Money Guy Show\'s Conservative Step-Down:`); + [22, 25, 30, 40, 50, 60, 65, 70].forEach((age) => { + const ageEntry = moneyGuyResult.monthlyTimeline.find( + (entry) => Math.abs(entry.age - age) < 0.1 + ); + if (ageEntry) { + const phase = age <= 25 ? '(Maximum Growth)' : age <= 40 ? '(Building Wealth)' : age <= 55 ? '(Protecting Gains)' : '(Preserving Capital)'; + console.log( + ` Age ${age}: ${(ageEntry.currentAnnualReturn * 100).toFixed(2)}% return ${phase}` + ); + } + }); + + console.log(`\n💡 This strategy balances optimism with realism - popular for good reason!`); +} catch (error) { + console.error('❌ Money Guy preset calculation failed:', error); +} + +// ============================================================================ +// COMPARISON: WHY DYNAMIC STRATEGIES MATTER +// ============================================================================ + +console.log('\n\n⚖️ THE BIG COMPARISON: Dynamic vs. Traditional Investing'); +console.log('-'.repeat(65)); +console.log('🤔 THE QUESTION: Does all this complexity actually matter?'); +console.log('📊 Let\'s compare a simple 8% fixed return vs. dynamic strategy...'); +console.log(); + +try { + // Traditional fixed-rate calculation + const traditionalResult = + calculator.getCompoundInterestWithAdditionalContributions( + 25000, // Initial balance + 1000, // Monthly contribution + 40, // Years + 0.08, // Fixed 8% return + 12, // Monthly contributions + 12 // Monthly compounding + ); + + // Dynamic glidepath with average 8% return + const dynamicConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, // 10% early + endReturn: 0.06, // 6% later (averages ~8%) + }; + + const dynamicResult = calculator.getCompoundInterestWithGlidepath( + 25000, // Same initial balance + 1000, // Same monthly contribution + 25, // Starting age + 65, // Ending age (40 years) + dynamicConfig + ); + + console.log(`🔄 TRADITIONAL APPROACH (8% fixed forever):`); + console.log(` Final balance: $${calculator.formatNumberWithCommas(traditionalResult.balance)}`); + console.log(` Total contributions: $${calculator.formatNumberWithCommas(traditionalResult.totalContributions)}`); + console.log(` Total interest: $${calculator.formatNumberWithCommas(traditionalResult.totalInterestEarned)}`); + console.log(` Strategy: "Set it and forget it" - simple but unrealistic`); + + console.log(`\n🎆 DYNAMIC GLIDEPATH (10% early → 6% later, avg ~8%):`); + console.log(` Final balance: $${calculator.formatNumberWithCommas(dynamicResult.finalBalance)}`); + console.log(` Total contributions: $${calculator.formatNumberWithCommas(dynamicResult.totalContributions)}`); + console.log(` Total interest: $${calculator.formatNumberWithCommas(dynamicResult.totalInterestEarned)}`); + console.log(` Effective return: ${(dynamicResult.effectiveAnnualReturn * 100).toFixed(2)}%`); + console.log(` Strategy: Age-appropriate risk management`); + + const difference = dynamicResult.finalBalance - traditionalResult.balance; + const percentDiff = ((difference / traditionalResult.balance) * 100).toFixed(2); + + console.log(`\n🏆 THE WINNER: Dynamic strategy by $${calculator.formatNumberWithCommas(Math.abs(difference))} (${percentDiff}% ${difference >= 0 ? 'better' : 'worse'})!`); + + if (difference > 0) { + const extraYearsOfSpending = Math.floor(difference / 50000); + console.log(` That\'s ${extraYearsOfSpending} extra years of $50,000/year retirement spending!`); + console.log(` 💡 WHY: Higher returns early in your career compound for DECADES`); + console.log(` The extra growth from your 20s and 30s creates massive wealth later!`); + } else { + console.log(` 💡 Interesting: In this case, steady returns performed slightly better`); + console.log(` This shows why it\'s important to test different scenarios!`); + } +} catch (error) { + console.error('❌ Comparison calculation failed:', error); +} + +// ============================================================================ +// BONUS: USING TIMELINE DATA FOR ANALYSIS +// ============================================================================ + +console.log('\n\n📈 BONUS: Timeline Data for Charts and Analysis'); +console.log('-'.repeat(55)); +console.log('🔧 FOR DEVELOPERS: How to extract data for building charts and apps'); +console.log('📊 Every calculation returns month-by-month timeline data'); +console.log(); + +try { + const timelineConfig: AllocationBasedGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.8, + endEquityWeight: 0.2, + equityReturn: 0.1, + bondReturn: 0.03, + }; + + const timelineResult = calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 30, + 50, + timelineConfig + ); + + console.log(`\n📊 Sample Timeline Data (every 5 years):`); + console.log( + 'Year | Age | Balance | Equity% | Annual Return | Interest Earned' + ); + console.log('-'.repeat(75)); + + // Show data every 5 years + const yearlyData = timelineResult.monthlyTimeline.filter( + (entry, index) => index % 60 === 59 // Every 60 months (5 years), at end of year + ); + + yearlyData.forEach((entry, index) => { + const year = (index + 1) * 5; + const equityPercent = entry.currentEquityWeight + ? (entry.currentEquityWeight * 100).toFixed(0) + '%' + : 'N/A'; + const returnPercent = (entry.currentAnnualReturn * 100).toFixed(2) + '%'; + const monthlyInterest = entry.monthlyInterestEarned; + + console.log( + `${year.toString().padStart(4)} | ` + + `${entry.age.toFixed(1).padStart(5)} | ` + + `$${calculator + .formatNumberWithCommas(entry.currentBalance) + .padStart(9)} | ` + + `${equityPercent.padStart(7)} | ` + + `${returnPercent.padStart(11)} | ` + + `$${monthlyInterest.toFixed(2).padStart(6)}` + ); + }); + + console.log(`\n🔥 DEVELOPER POWER: ${timelineResult.monthlyTimeline.length} monthly data points available!`); + console.log(` • Build interactive charts showing balance growth over time`); + console.log(` • Create allocation pie charts that change with age`); + console.log(` • Show return rate progression graphs`); + console.log(` • Build financial planning apps with detailed projections`); +} catch (error) { + console.error('❌ Timeline demonstration failed:', error); +} + +// ============================================================================ +// KEY INSIGHTS & YOUR ACTION PLAN +// ============================================================================ + +console.log('\n' + '='.repeat(70)); +console.log('🎓 KEY INSIGHTS FROM ADVANCED STRATEGIES'); +console.log('='.repeat(70)); + +console.log(` +💡 WHAT YOU LEARNED TODAY: + +1️⃣ FRONT-LOADED RETURNS ARE GOLD: + Higher returns in your 20s and 30s compound for DECADES. + A 25-year-old earning 10% beats a 25-year-old earning 8% by hundreds of thousands! + +2️⃣ RISK MANAGEMENT MATTERS: + Being 100% stocks at 64 is dangerous - you can\'t afford a 2008-style crash. + Glidepath strategies protect your gains as you near retirement. + +3️⃣ ONE SIZE DOESN\'T FIT ALL: + Your optimal strategy depends on your risk tolerance, timeline, and goals. + Custom strategies can significantly outperform generic approaches. + +4️⃣ SIMPLE ISN\'T ALWAYS BEST: + While "8% fixed forever" is easy to understand, it\'s unrealistic. + Age-appropriate strategies can boost your final balance significantly. + +🎯 WHICH STRATEGY IS RIGHT FOR YOU? + +🚀 Young Professional (20s-30s): Custom Waypoints or Fixed Return Glidepath + You can afford high risk early for maximum compound growth + +🏆 Busy Professional (30s-40s): Target-Date Fund Approach + "Set it and forget it" with professional risk management + +💰 Conservative Planner (Any age): Money Guy Show Preset + Proven, realistic assumptions that account for market reality + +🔥 Advanced Investor (Any age): Custom Waypoints + Maximum control to optimize for your specific situation`); + +console.log(`\n🚀 YOUR ACTION PLAN: + +✅ Experiment with these examples using YOUR real numbers +✅ Compare results - see which strategy works best for your situation +✅ Consider your risk tolerance - aggressive early, conservative later +✅ Automate your investments - consistency beats perfect timing +✅ Review annually - adjust as your life situation changes + +⚠️ IMPORTANT DISCLAIMER: + These examples are for EDUCATIONAL COMPARISON only - we're not recommending + any specific strategy as "best." Each shows different approaches under + idealized conditions with steady returns. + + 📊 For REAL decision-making, you'll want to use Monte Carlo analysis + (coming soon to this package!) which models thousands of scenarios + with realistic market volatility to find the optimal strategy for + YOUR specific risk tolerance and timeline. + +💸 Remember: The best investment strategy is the one you'll actually stick to! + Consistency over 30-40 years beats trying to time the market. + +🔧 For developers: Use this rich timeline data to build amazing financial tools!`); + +console.log('='.repeat(70)); diff --git a/examples/basic-retirement-gap-analysis.ts b/examples/basic-retirement-gap-analysis.ts new file mode 100644 index 0000000..c6b3b96 --- /dev/null +++ b/examples/basic-retirement-gap-analysis.ts @@ -0,0 +1,169 @@ +/** + * @fileoverview Basic Retirement Gap Analysis Example + * + * WHEN TO USE THIS: + * - You have a specific retirement balance goal in mind + * - You want to see if your current contributions are enough + * - You need to calculate the "retirement gap" (how much more you need to save) + * - You want to understand the impact of inflation on your retirement goals + * + * PERFECT FOR: + * - People who have heard "you need $1 million to retire" and want to check if they're on track + * - Someone currently saving but unsure if it's enough + * - Understanding why starting early makes such a huge difference + * + * KEY LEARNING OUTCOMES: + * - How compound interest works over time + * - The power of consistent monthly contributions + * - Why inflation matters for long-term planning + * - How to calculate if you're on track for retirement goals + */ + +// You would import from 'retirement-calculator' +import { RetirementCalculator } from '../index'; + +const calculator = new RetirementCalculator(); + +// ============================================================================ +// SCENARIO: 30-year-old with $50K saved, wants $1M by age 55 +// Currently contributing $200/month, wondering if that's enough +// ============================================================================ + +const startingBalance: number = 50000; // Current 401k/IRA balance +const desiredBalance: number = 1000000; // Classic "$1M retirement" goal +const yearsUntilRetirement: number = 25; // Age 30 to 55 +const interestRate: number = 8; // 8% annual return (historical stock market average) +const contributionFrequency: number = 12; // Monthly contributions +const compoundingFrequency: number = 12; // Monthly compounding +const currentContributionAmount: number = 200; // Currently saving $200/month +const inflationRate: number = 2; // 2% inflation assumption + +// Calculate what contribution is actually needed to hit the goal +const contributionRequired = calculator.getContributionNeededForDesiredBalance( + startingBalance, + desiredBalance, + yearsUntilRetirement, + interestRate / 100, // Convert percentage to decimal + contributionFrequency, + compoundingFrequency, + inflationRate / 100, // Convert percentage to decimal +); + +// See where current trajectory leads +const currentTrajectory = calculator.getCompoundInterestWithAdditionalContributions( + startingBalance, + currentContributionAmount, + yearsUntilRetirement, + interestRate / 100, + contributionFrequency, + compoundingFrequency +); + +// Calculate retirement income using 4% withdrawal rule +const yearlyWithdrawalRate: number = 4; +const currentYearlySpend: number = calculator.getYearlyWithdrawalAmountByBalance( + currentTrajectory.balance, + yearlyWithdrawalRate / 100 +); +const goalYearlySpend: number = calculator.getYearlyWithdrawalAmountByBalance( + contributionRequired.desiredBalance, + yearlyWithdrawalRate / 100 +); + +// Show inflation impact +const totalInflation: number = calculator.adjustDesiredBalanceDueToInflation( + 100, + yearsUntilRetirement, + inflationRate / 100 +); + +// ============================================================================ +// RESULTS & EDUCATIONAL INSIGHTS +// ============================================================================ + +console.log("🎯 RETIREMENT GAP ANALYSIS"); +console.log("=" .repeat(60)); +console.log(`📊 STARTING SITUATION:`); +console.log(` Current savings: $${calculator.formatNumberWithCommas(startingBalance)}`); +console.log(` Monthly contributions: $${calculator.formatNumberWithCommas(currentContributionAmount)}`); +console.log(` Years to retirement: ${yearsUntilRetirement}`); +console.log(` Target balance: $${calculator.formatNumberWithCommas(desiredBalance)}`); +console.log(` Expected return: ${interestRate}% annually`); +console.log(); + +console.log("📈 CURRENT TRAJECTORY VS. GOAL:"); +console.log(` Where you're headed: $${calculator.formatNumberWithCommas(currentTrajectory.balance)}`); +console.log(` Where you want to be: $${calculator.formatNumberWithCommas(desiredBalance)}`); + +const shortfall = desiredBalance - currentTrajectory.balance; +const percentShort = (shortfall / desiredBalance * 100); + +if (shortfall > 0) { + console.log(` 🚨 RETIREMENT GAP: $${calculator.formatNumberWithCommas(shortfall)} (${percentShort.toFixed(1)}% short)`); +} else { + console.log(` ✅ SURPLUS: $${calculator.formatNumberWithCommas(Math.abs(shortfall))} (${Math.abs(percentShort).toFixed(1)}% over goal!)`); +} +console.log(); + +console.log("💡 WHAT YOU NEED TO DO:"); +console.log(` Required monthly contribution: $${calculator.formatNumberWithCommas(contributionRequired.contributionNeededPerPeriod)}`); +console.log(` Additional monthly needed: $${calculator.formatNumberWithCommas(contributionRequired.contributionNeededPerPeriod - currentContributionAmount)}`); +console.log(` That's only $${((contributionRequired.contributionNeededPerPeriod - currentContributionAmount) / 30).toFixed(2)} more per day!`); +console.log(); + +console.log("🏠 RETIREMENT INCOME (4% Withdrawal Rule):"); +console.log(` Current trajectory income: $${calculator.formatNumberWithCommas(currentYearlySpend)}/year`); +console.log(` Goal trajectory income: $${calculator.formatNumberWithCommas(goalYearlySpend)}/year`); +console.log(` Monthly difference: $${calculator.formatNumberWithCommas((goalYearlySpend - currentYearlySpend) / 12)}`); +console.log(); + +console.log("💸 INFLATION IMPACT:"); +console.log(` $100 today = $${calculator.formatNumberWithCommas(totalInflation)} in ${yearsUntilRetirement} years`); +console.log(` Your $${calculator.formatNumberWithCommas(desiredBalance)} will have purchasing power of:`); +console.log(` $${calculator.formatNumberWithCommas(contributionRequired.desiredBalanceValueAfterInflation)} in today's dollars`); +console.log(); + +// ============================================================================ +// KEY TAKEAWAYS & TIPS +// ============================================================================ + +console.log("🎓 KEY LESSONS FROM THIS ANALYSIS:"); +console.log("=" .repeat(60)); +console.log("1️⃣ THE POWER OF COMPOUND INTEREST:"); +console.log(` Your $${calculator.formatNumberWithCommas(startingBalance)} starting balance will grow to:`); +console.log(` $${calculator.formatNumberWithCommas(calculator.getCompoundInterestWithAdditionalContributions(startingBalance, 0, yearsUntilRetirement, interestRate / 100, 1, 1).balance)}`); +console.log(` That's $${calculator.formatNumberWithCommas(calculator.getCompoundInterestWithAdditionalContributions(startingBalance, 0, yearsUntilRetirement, interestRate / 100, 1, 1).totalInterestEarned)} in FREE money from compound interest!`); +console.log(); + +console.log("2️⃣ WHY MONTHLY CONTRIBUTIONS MATTER:"); +console.log(` Without additional contributions: $${calculator.formatNumberWithCommas(calculator.getCompoundInterestWithAdditionalContributions(startingBalance, 0, yearsUntilRetirement, interestRate / 100, 1, 1).balance)}`); +console.log(` With $${currentContributionAmount}/month: $${calculator.formatNumberWithCommas(currentTrajectory.balance)}`); +console.log(` Extra from contributions: $${calculator.formatNumberWithCommas(currentTrajectory.balance - calculator.getCompoundInterestWithAdditionalContributions(startingBalance, 0, yearsUntilRetirement, interestRate / 100, 1, 1).balance)}`); +console.log(); + +console.log("3️⃣ INFLATION IS A SILENT WEALTH KILLER:"); +console.log(` Without considering inflation, $${calculator.formatNumberWithCommas(desiredBalance)} seems like a lot`); +console.log(` But with ${inflationRate}% inflation, it's only worth $${calculator.formatNumberWithCommas(contributionRequired.desiredBalanceValueAfterInflation)} in today's purchasing power`); +console.log(); + +console.log("🚀 ACTION ITEMS:"); +console.log("=" .repeat(60)); +if (shortfall > 0) { + console.log(`✅ Increase monthly contributions by $${calculator.formatNumberWithCommas(contributionRequired.contributionNeededPerPeriod - currentContributionAmount)}`); + console.log(`✅ Consider increasing your 401(k) contribution percentage`); + console.log(`✅ Look for ways to reduce expenses by $${((contributionRequired.contributionNeededPerPeriod - currentContributionAmount) / 30).toFixed(2)}/day`); + console.log(`✅ Consider a side hustle to fund the additional retirement savings`); +} else { + console.log(`🎉 Congratulations! You're on track to exceed your retirement goal!`); + console.log(`✅ Consider increasing your retirement goal for more financial security`); + console.log(`✅ Explore early retirement options with your surplus`); +} + +console.log(`✅ Automate your retirement contributions to make saving effortless`); +console.log(`✅ Review and increase contributions annually with pay raises`); +console.log(`✅ Consider tax-advantaged accounts (401k, IRA, Roth IRA)`); + +console.log(); +console.log("💭 REMEMBER: This analysis assumes a constant ${interestRate}% return."); +console.log(" Real markets fluctuate. Consider using our Monte Carlo simulation"); +console.log(" for a more realistic range of outcomes!"); \ No newline at end of file diff --git a/examples/example1.ts b/examples/example1.ts deleted file mode 100644 index 77aa149..0000000 --- a/examples/example1.ts +++ /dev/null @@ -1,60 +0,0 @@ -// You would import from 'retirement-calculator' -import { RetirementCalculator } from '../index'; - -const calculator = new RetirementCalculator(); -const startingBalance: number = 50000; -const desiredBalance: number = 1000000; -const yearsUntilRetirement: number = 25; -const interestRate: number = 8; -const contributionFrequency: number = 12; -const compoundingFrequency: number = 12; -const currentContributionAmount: number = 200; -const inflationRate: number = 2; -const totalInflation: number = calculator.adjustDesiredBalanceDueToInflation(100, yearsUntilRetirement, inflationRate / 100); - -const contributionRequired = calculator.getContributionNeededForDesiredBalance( - startingBalance, - desiredBalance, - yearsUntilRetirement, - interestRate / 100, // Assuming the input is in percentage - contributionFrequency, - compoundingFrequency, - inflationRate / 100, -); - -const currentTrajectory = calculator.getCompoundInterestWithAdditionalContributions(startingBalance,currentContributionAmount, yearsUntilRetirement, interestRate / 100, contributionFrequency, compoundingFrequency); -const yearlyWithdrawalRate: number = 4; - -const currentYearlySpend: number = calculator.getYearlyWithdrawalAmountByBalance(currentTrajectory.balance, yearlyWithdrawalRate / 100); -const yearlySpendWithoutInflation: number = calculator.getYearlyWithdrawalAmountByBalance(contributionRequired.desiredBalance, yearlyWithdrawalRate / 100); -const yearlySpendWithInflation: number = calculator.getYearlyWithdrawalAmountByBalance(contributionRequired.desiredBalanceWithInflation, yearlyWithdrawalRate / 100); - -console.log("============== SCENARIO #1 =============="); -console.log(`Starting balance of $${calculator.formatNumberWithCommas(startingBalance)} and currently contributing $${calculator.formatNumberWithCommas(currentContributionAmount)} per month.`); -console.log(`Desired balance of $${calculator.formatNumberWithCommas(desiredBalance)} with ${yearsUntilRetirement} years until retirement, an interest rate of ${interestRate}%,`); -console.log(`compounding and contributing monthly`); -console.log(); -console.log(`Current trajectory based on contributions: $${calculator.formatNumberWithCommas(currentTrajectory.balance)}`); -console.log(`Yearly income with current trajectory at ${yearlyWithdrawalRate}% withdrawal per year: $${calculator.formatNumberWithCommas(currentYearlySpend)}`); - -console.log(); -console.log(`Additional contribution needed per month to hit goal: $${calculator.formatNumberWithCommas((contributionRequired.contributionNeededPerPeriod - currentContributionAmount))}`); -console.log(`Percent off from hitting desired balance: ${calculator.formatNumberWithCommas(( currentTrajectory.balance / contributionRequired.desiredBalance -1) * -100)}%`); -console.log(`Total contribution needed per month: $${calculator.formatNumberWithCommas(contributionRequired.contributionNeededPerPeriod)}`); -console.log(`Yearly income with desired balance at ${yearlyWithdrawalRate}% withdrawal per year: $${calculator.formatNumberWithCommas(yearlySpendWithoutInflation)}`); - - -console.log(); -console.log(); -console.log(`You may want to take inflation into account because something that costs $100.00 today, will cost ~$${calculator.formatNumberWithCommas(totalInflation)} in ${yearsUntilRetirement} years, assuming inflation rate of ${inflationRate}%.`); -console.log(`$${calculator.formatNumberWithCommas(desiredBalance)} will be worth ~$${calculator.formatNumberWithCommas(contributionRequired.desiredBalanceValueAfterInflation)} after ${inflationRate}% inflation for ${yearsUntilRetirement} years.`); - -console.log(); -console.log(`Additional contribution needed per month to hit goal with inflation: $${calculator.formatNumberWithCommas((contributionRequired.contributionNeededPerPeriodWithInflation - currentContributionAmount))}`); -console.log(`Percent off from hitting desired balance with inflation: ${calculator.formatNumberWithCommas(( currentTrajectory.balance / contributionRequired.desiredBalanceWithInflation -1) * -100)}%`); -console.log(`Total contribution needed per month with inflation: $${calculator.formatNumberWithCommas(contributionRequired.contributionNeededPerPeriodWithInflation)}`); -console.log(`Total balance with inflation accounted for: $${calculator.formatNumberWithCommas(contributionRequired.desiredBalanceWithInflation)}`); -console.log(`Yearly income with desired balance with inflation at ${yearlyWithdrawalRate}% withdrawal per year: $${calculator.formatNumberWithCommas(yearlySpendWithInflation)}`); -console.log(); - -console.log(); \ No newline at end of file diff --git a/examples/example2.ts b/examples/example2.ts deleted file mode 100644 index 997046e..0000000 --- a/examples/example2.ts +++ /dev/null @@ -1,55 +0,0 @@ -// You would import from 'retirement-calculator' -import { RetirementCalculator } from '../index'; - -const calculator = new RetirementCalculator(); -const startingBalance: number = 0; -const desiredYearlySpend: number = 80000; -const yearsUntilRetirement: number = 25; -const interestRate: number = 8; -const contributionFrequency: number = 12; -const compoundingFrequency: number = 12; -const inflationRate: number = 2; -const yearlyWithdrawalRate: number = 4; - -const totalInflation: number = calculator.adjustDesiredBalanceDueToInflation(100, yearsUntilRetirement, inflationRate / 100); - -const desiredYearlySpendWithInflation = calculator.adjustDesiredBalanceDueToInflation(desiredYearlySpend, yearsUntilRetirement, inflationRate / 100); -const neededBalanceBasedOnYearlySpend = calculator.getDesiredBalanceByYearlySpend(desiredYearlySpend, yearlyWithdrawalRate / 100); -const neededBalanceWithInflationBasedOnYearlySpend = calculator.getDesiredBalanceByYearlySpend(desiredYearlySpendWithInflation, yearlyWithdrawalRate / 100); - -const contributionRequiredWithoutInflation = calculator.getContributionNeededForDesiredBalance( - startingBalance, - neededBalanceBasedOnYearlySpend, - yearsUntilRetirement, - interestRate / 100, // Assuming the input is in percentage - contributionFrequency, - compoundingFrequency, - inflationRate / 100, -); - -const contributionRequiredWithInflation = calculator.getContributionNeededForDesiredBalance( - startingBalance, - neededBalanceWithInflationBasedOnYearlySpend, - yearsUntilRetirement, - interestRate / 100, // Assuming the input is in percentage - contributionFrequency, - compoundingFrequency, - inflationRate / 100, -); - - -console.log("============== SCENARIO #2 =============="); -console.log(`Desire to be able to spend $${calculator.formatNumberWithCommas(desiredYearlySpend)} per year when retired in ${yearsUntilRetirement} years and withdrawing ${yearlyWithdrawalRate}% per year and dealing with ${inflationRate}% inflation per year.`); - - -console.log(); -console.log(`Balance needed at retirement: $${calculator.formatNumberWithCommas(neededBalanceBasedOnYearlySpend)}`); -console.log(`Contribution needed per month assuming an interest rate of ${interestRate}%: $${calculator.formatNumberWithCommas(contributionRequiredWithoutInflation.contributionNeededPerPeriod)}`); - -console.log(); -console.log(); - -console.log(`You may want to take inflation into account because something that costs $100.00 today, will cost ~$${calculator.formatNumberWithCommas(totalInflation)} in ${yearsUntilRetirement} years, assuming inflation rate of ${inflationRate}%.`); -console.log(`Something that costs $${calculator.formatNumberWithCommas(desiredYearlySpend)} now, will cost $${calculator.formatNumberWithCommas(desiredYearlySpendWithInflation)} after ${inflationRate}% inflation for ${yearsUntilRetirement} years.`); -console.log(`Balance needed at retirement with inflation: $${calculator.formatNumberWithCommas(neededBalanceWithInflationBasedOnYearlySpend)}`); -console.log(`Contribution needed per month with inflation assuming an interest rate of ${interestRate}%: $${calculator.formatNumberWithCommas(contributionRequiredWithInflation.contributionNeededPerPeriod)}`); diff --git a/examples/lifestyle-based-retirement-planning.ts b/examples/lifestyle-based-retirement-planning.ts new file mode 100644 index 0000000..1698c5b --- /dev/null +++ b/examples/lifestyle-based-retirement-planning.ts @@ -0,0 +1,227 @@ +/** + * @fileoverview Lifestyle-Based Retirement Planning Example + * + * WHEN TO USE THIS: + * - You think in terms of "I want to spend $X per year in retirement" + * - You want to work backwards from your desired lifestyle to required savings + * - You're trying to understand how much you need to maintain your current lifestyle + * - You want to see the difference between thinking in terms of income vs. lump sum + * + * PERFECT FOR: + * - People who say "I just want to maintain my current lifestyle in retirement" + * - Someone planning to replace 70-80% of their current income + * - Understanding the relationship between retirement income and required savings + * - Comparing different retirement income scenarios + * + * KEY LEARNING OUTCOMES: + * - How the 4% withdrawal rule works in practice + * - Why thinking in terms of income is more intuitive than lump sums + * - The massive difference inflation makes over long time periods + * - How to reverse-engineer your retirement savings from your lifestyle goals + */ + +// You would import from 'retirement-calculator' +import { RetirementCalculator } from '../index'; + +const calculator = new RetirementCalculator(); + +// ============================================================================ +// SCENARIO: 25-year-old wants to spend $80K/year in retirement (today's dollars) +// Starting from $0, has 40 years to save +// ============================================================================ + +const startingBalance: number = 0; // Starting from scratch (realistic for 25-year-old) +const desiredYearlySpend: number = 80000; // Want $80K/year lifestyle in retirement +const yearsUntilRetirement: number = 40; // Age 25 to 65 +const interestRate: number = 8; // 8% annual return assumption +const contributionFrequency: number = 12; // Monthly contributions +const compoundingFrequency: number = 12; // Monthly compounding +const inflationRate: number = 2.5; // Slightly higher inflation assumption +const yearlyWithdrawalRate: number = 4; // 4% withdrawal rule + +// Show what $80K today will cost in the future due to inflation +const desiredYearlySpendWithInflation = calculator.adjustDesiredBalanceDueToInflation( + desiredYearlySpend, + yearsUntilRetirement, + inflationRate / 100 +); + +// Calculate required retirement balances +const neededBalanceForTodaysDollars = calculator.getDesiredBalanceByYearlySpend( + desiredYearlySpend, + yearlyWithdrawalRate / 100 +); + +const neededBalanceForInflatedDollars = calculator.getDesiredBalanceByYearlySpend( + desiredYearlySpendWithInflation, + yearlyWithdrawalRate / 100 +); + +// Calculate required monthly contributions for both scenarios +const contributionForTodaysPurchasingPower = calculator.getContributionNeededForDesiredBalance( + startingBalance, + neededBalanceForTodaysDollars, + yearsUntilRetirement, + interestRate / 100, + contributionFrequency, + compoundingFrequency, + inflationRate / 100, +); + +const contributionForFutureDollars = calculator.getContributionNeededForDesiredBalance( + startingBalance, + neededBalanceForInflatedDollars, + yearsUntilRetirement, + interestRate / 100, + contributionFrequency, + compoundingFrequency, + inflationRate / 100, +); + +// Show different lifestyle scenarios +const lifestyleScenarios = [ + { name: "Basic Lifestyle", yearlySpend: 50000, description: "Modest retirement, basic needs covered" }, + { name: "Comfortable Lifestyle", yearlySpend: 80000, description: "Maintain middle-class lifestyle" }, + { name: "Luxury Lifestyle", yearlySpend: 120000, description: "High-end retirement with travel/hobbies" }, + { name: "Ultra-Luxury Lifestyle", yearlySpend: 200000, description: "Premium retirement lifestyle" } +]; + +// Calculate inflation impact over different time horizons +const inflationImpact = [ + { years: 10, cost: calculator.adjustDesiredBalanceDueToInflation(100, 10, inflationRate / 100) }, + { years: 20, cost: calculator.adjustDesiredBalanceDueToInflation(100, 20, inflationRate / 100) }, + { years: 30, cost: calculator.adjustDesiredBalanceDueToInflation(100, 30, inflationRate / 100) }, + { years: 40, cost: calculator.adjustDesiredBalanceDueToInflation(100, 40, inflationRate / 100) } +]; + +// ============================================================================ +// RESULTS & EDUCATIONAL INSIGHTS +// ============================================================================ + +console.log("🏡 LIFESTYLE-BASED RETIREMENT PLANNING"); +console.log("=" .repeat(70)); +console.log(`🎯 YOUR RETIREMENT LIFESTYLE GOAL:`); +console.log(` Desired annual spending: $${calculator.formatNumberWithCommas(desiredYearlySpend)}`); +console.log(` Years until retirement: ${yearsUntilRetirement}`); +console.log(` Assumed inflation rate: ${inflationRate}%`); +console.log(` Expected investment return: ${interestRate}%`); +console.log(); + +console.log("💰 THE MATH BEHIND YOUR LIFESTYLE:"); +console.log(` Using the 4% withdrawal rule:`); +console.log(` To spend $${calculator.formatNumberWithCommas(desiredYearlySpend)}/year, you need:`); +console.log(` $${calculator.formatNumberWithCommas(neededBalanceForTodaysDollars)} in retirement savings`); +console.log(` (This assumes 4% annual withdrawal rate)`); +console.log(); + +console.log("🚨 THE INFLATION REALITY CHECK:"); +console.log(` $${calculator.formatNumberWithCommas(desiredYearlySpend)} today = $${calculator.formatNumberWithCommas(desiredYearlySpendWithInflation)} in ${yearsUntilRetirement} years`); +console.log(` To maintain the SAME purchasing power, you'll need:`); +console.log(` $${calculator.formatNumberWithCommas(neededBalanceForInflatedDollars)} in retirement savings`); +console.log(` That's ${((neededBalanceForInflatedDollars / neededBalanceForTodaysDollars - 1) * 100).toFixed(0)}% more than if you ignore inflation!`); +console.log(); + +console.log("📊 WHAT YOU NEED TO SAVE MONTHLY:"); +console.log(` Ignoring inflation: $${calculator.formatNumberWithCommas(contributionForTodaysPurchasingPower.contributionNeededPerPeriod)}/month`); +console.log(` Accounting for inflation: $${calculator.formatNumberWithCommas(contributionForFutureDollars.contributionNeededPerPeriod)}/month`); +console.log(` Extra needed due to inflation: $${calculator.formatNumberWithCommas(contributionForFutureDollars.contributionNeededPerPeriod - contributionForTodaysPurchasingPower.contributionNeededPerPeriod)}/month`); +console.log(); + +console.log("🔥 PERSPECTIVE: That's only $" + (contributionForFutureDollars.contributionNeededPerPeriod / 30).toFixed(2) + " per day!"); +console.log(" Less than most people spend on coffee and lunch!"); +console.log(); + +// ============================================================================ +// LIFESTYLE COMPARISON SCENARIOS +// ============================================================================ + +console.log("🏠 COMPARE DIFFERENT RETIREMENT LIFESTYLES:"); +console.log("=" .repeat(70)); + +lifestyleScenarios.forEach((scenario) => { + const requiredBalance = calculator.getDesiredBalanceByYearlySpend(scenario.yearlySpend, yearlyWithdrawalRate / 100); + const monthlyContribution = calculator.getContributionNeededForDesiredBalance( + 0, + requiredBalance, + yearsUntilRetirement, + interestRate / 100, + 12, + 12, + inflationRate / 100 + ).contributionNeededPerPeriod; + + console.log(`💸 ${scenario.name.toUpperCase()}:`); + console.log(` Annual spending: $${calculator.formatNumberWithCommas(scenario.yearlySpend)}`); + console.log(` Required savings: $${calculator.formatNumberWithCommas(requiredBalance)}`); + console.log(` Monthly contribution needed: $${calculator.formatNumberWithCommas(monthlyContribution)}`); + console.log(` ${scenario.description}`); + console.log(); +}); + +// ============================================================================ +// INFLATION EDUCATION SECTION +// ============================================================================ + +console.log("📈 UNDERSTANDING INFLATION'S IMPACT:"); +console.log("=" .repeat(70)); +console.log("See how $100 worth of goods today will cost in the future:"); +console.log(); + +inflationImpact.forEach((impact) => { + const purchasing_power = 100 / (impact.cost / 100); + console.log(` In ${impact.years} years: $${impact.cost.toFixed(2)}`); + console.log(` (Your $100 will only buy $${purchasing_power.toFixed(2)} worth of goods)`); +}); + +console.log(); +console.log(`💡 This is why you need $${calculator.formatNumberWithCommas(neededBalanceForInflatedDollars)} instead of`); +console.log(` just $${calculator.formatNumberWithCommas(neededBalanceForTodaysDollars)} to maintain your lifestyle!`); +console.log(); + +// ============================================================================ +// KEY INSIGHTS & ACTION ITEMS +// ============================================================================ + +console.log("🎓 KEY INSIGHTS:"); +console.log("=" .repeat(70)); +console.log("1️⃣ THE 4% WITHDRAWAL RULE:"); +console.log(" For every $40,000/year you want to spend in retirement,"); +console.log(" you need about $1,000,000 in savings."); +console.log(" This rule helps your money last 30+ years."); +console.log(); + +console.log("2️⃣ THINK IN INCOME, NOT LUMP SUMS:"); +console.log(" Instead of 'I need $1M to retire,' think:"); +console.log(" 'I need $40,000/year in retirement income.'"); +console.log(" This makes it easier to plan and understand."); +console.log(); + +console.log("3️⃣ INFLATION IS YOUR BIGGEST ENEMY:"); +console.log(` At ${inflationRate}% inflation, prices double every ${Math.round(70/inflationRate)} years.`); +console.log(" What costs $80,000 today will cost " + + `$${calculator.formatNumberWithCommas(desiredYearlySpendWithInflation)} in retirement.`); +console.log(); + +console.log("4️⃣ TIME IS YOUR BIGGEST ALLY:"); +console.log(` Starting at 25 vs 35 means:`); +const late_starter = calculator.getContributionNeededForDesiredBalance( + 0, neededBalanceForInflatedDollars, 30, interestRate / 100, 12, 12, inflationRate / 100 +).contributionNeededPerPeriod; +console.log(` Start at 25: $${calculator.formatNumberWithCommas(contributionForFutureDollars.contributionNeededPerPeriod)}/month`); +console.log(` Start at 35: $${calculator.formatNumberWithCommas(late_starter)}/month`); +console.log(` Waiting 10 years costs you $${calculator.formatNumberWithCommas(late_starter - contributionForFutureDollars.contributionNeededPerPeriod)}/month FOREVER!`); +console.log(); + +console.log("🚀 YOUR ACTION PLAN:"); +console.log("=" .repeat(70)); +console.log(`✅ Set up automatic monthly investment of $${calculator.formatNumberWithCommas(contributionForFutureDollars.contributionNeededPerPeriod)}`); +console.log("✅ Use tax-advantaged accounts (401k, IRA, Roth IRA) first"); +console.log("✅ Invest in low-cost index funds for the ${interestRate}% return assumption"); +console.log("✅ Increase contributions by 3-5% annually with pay raises"); +console.log("✅ Review and adjust your retirement lifestyle goals yearly"); +console.log(); + +console.log("💭 REMEMBER:"); +console.log(" This assumes steady ${interestRate}% returns and ${inflationRate}% inflation."); +console.log(" Real life has ups and downs - consider our Monte Carlo"); +console.log(" simulation for a more realistic analysis!"); \ No newline at end of file diff --git a/images/example1.png b/images/example1.png deleted file mode 100644 index a24eb03..0000000 Binary files a/images/example1.png and /dev/null differ diff --git a/images/example2.png b/images/example2.png deleted file mode 100644 index 5df132f..0000000 Binary files a/images/example2.png and /dev/null differ diff --git a/index.ts b/index.ts index 5a831d3..c3f05a2 100644 --- a/index.ts +++ b/index.ts @@ -1,9 +1,85 @@ export { default as RetirementCalculator } from './src/RetirementCalculator'; -export { CONTRIBUTION_FREQUENCY } from './src/constants/retirementCalculatorConstants'; + +// Constants +export { + CONTRIBUTION_FREQUENCY, + GLIDEPATH_DEFAULTS, + GLIDEPATH_VALIDATION, + GLIDEPATH_MATH, + GLIDEPATH_TEMPLATES, + GLIDEPATH_ERROR_MESSAGES, + GLIDEPATH_PERFORMANCE, + GLIDEPATH_PRESETS, +} from './src/constants/retirementCalculatorConstants'; + +// Traditional types export type { ContributionFrequencyType, DetermineContributionType, CompoundingInterestObjectType, CompoundingPeriodDetailsType, - YearlyCompoundingDetails + YearlyCompoundingDetails, } from './src/types/retirementCalculatorTypes'; + +// Dynamic glidepath types +export type { + ContributionTiming, + GlidepathMode, + FixedReturnGlidepathConfig, + AllocationBasedGlidepathConfig, + CustomWaypointsGlidepathConfig, + SteppedReturnGlidepathConfig, + GlidepathWaypoint, + DynamicGlidepathConfig, + MonthlyTimelineEntry, + DynamicGlidepathResult, + ConfigForMode, + RequiredFields, + CustomWaypointsWithReturns, +} from './src/types/retirementCalculatorTypes'; + +// Type guards +export { + isFixedReturnConfig, + isAllocationBasedConfig, + isCustomWaypointsConfig, + isSteppedReturnConfig, +} from './src/types/retirementCalculatorTypes'; + +// Error handling +export { + GLIDEPATH_ERROR_CODES, + DynamicGlidepathError, + DynamicGlidepathValidationError, + DynamicGlidepathConfigurationError, + DynamicGlidepathCalculationError, + ValidationResultBuilder, + createAgeError, + createFinancialError, + createReturnRateError, + createAllocationError, + createConfigurationError, + createWaypointError, + createWarning, + createCalculationError, + groupByCategory, + groupBySeverity, + getSummary, + formatForConsole, + formatForAPI, + isDynamicGlidepathError, + isValidationError, + isConfigurationError, + isCalculationError, + isWarning, + isBlockingError, +} from './src/errors/DynamicGlidepathErrors'; + +// Error types +export type { + GlidepathErrorCode, + ErrorSeverity, + ErrorCategory, + GlidepathErrorInfo, + ValidationResult, +} from './src/errors/DynamicGlidepathErrors'; diff --git a/package-lock.json b/package-lock.json index fcd319f..302ae6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "retirement-calculator", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "retirement-calculator", - "version": "0.1.0", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.8", diff --git a/package.json b/package.json index 0e60c70..a77c353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "retirement-calculator", - "version": "1.0.0", + "version": "1.1.0", "description": "A versatile retirement financial planning tool for calculating savings, inflation impact, and withdrawal strategies.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/RetirementCalculator.ts b/src/RetirementCalculator.ts index 832a5d8..148f0b6 100644 --- a/src/RetirementCalculator.ts +++ b/src/RetirementCalculator.ts @@ -4,6 +4,14 @@ import type { CompoundingPeriodDetailsType, DetermineContributionType, YearlyCompoundingDetails, + DynamicGlidepathConfig, + ContributionTiming, + DynamicGlidepathResult, + MonthlyTimelineEntry, + FixedReturnGlidepathConfig, + SteppedReturnGlidepathConfig, + AllocationBasedGlidepathConfig, + CustomWaypointsGlidepathConfig, } from './types/retirementCalculatorTypes'; /** @@ -94,6 +102,7 @@ export default class RetirementCalculator { /** * Calculate the total interest multiplier used for determining contributions needed to hit a goal. + * Uses the geometric series formula for efficiency: sum = a(r^n - 1)/(r - 1) * @param interestRate * @param periods * @private @@ -102,11 +111,15 @@ export default class RetirementCalculator { interestRate: number, periods: number ): number { - let totalInterestMultiplier: number = 0; - for (let i: number = 1; i <= periods; i++) { - totalInterestMultiplier += (1 + interestRate) ** i; + // Handle special case where interest rate is 0 + if (interestRate === 0) { + return periods; } - return totalInterestMultiplier; + + // Geometric series formula: sum of (1+r)^i from i=1 to n + // This equals: ((1+r)^(n+1) - (1+r)) / r + const r = 1 + interestRate; + return (Math.pow(r, periods + 1) - r) / interestRate; } /** @@ -396,4 +409,452 @@ export default class RetirementCalculator { return yearlyData; } + + // ============================================================================ + // DYNAMIC GLIDEPATH METHODS + // ============================================================================ + + /** + * Calculate compound interest with age-aware glidepath strategies. + * Supports fixed returns, allocation-based strategies, and custom waypoints. + * + * @param initialBalance Starting account balance + * @param contributionAmount Amount contributed per contribution period + * @param startAge Starting age for calculation + * @param endAge Ending age for calculation + * @param glidepathConfig Strategy configuration (fixed, allocation-based, or custom) + * @param contributionFrequency Number of contributions per year (default: 12) + * @param compoundingFrequency Number of compounding periods per year (default: 12) + * @param contributionTiming When contributions are added ('start' or 'end' of period) + * @returns Detailed glidepath calculation results with timeline data + */ + public getCompoundInterestWithGlidepath( + initialBalance: number, + contributionAmount: number, + startAge: number, + endAge: number, + glidepathConfig: DynamicGlidepathConfig, + contributionFrequency: number = 12, + compoundingFrequency: number = 12, + contributionTiming: ContributionTiming = 'start' + ): DynamicGlidepathResult { + // Input validation + if (initialBalance < 0) { + throw new Error('Initial balance must be non-negative'); + } + + if (contributionAmount < 0) { + throw new Error('Contribution amount must be non-negative'); + } + + if (startAge <= 0 || endAge <= 0) { + throw new Error('Ages must be positive'); + } + + if (startAge >= endAge) { + throw new Error('Start age must be less than end age'); + } + + if (contributionFrequency <= 0 || compoundingFrequency <= 0) { + throw new Error('Frequencies must be positive'); + } + + // Calculate simulation parameters + const totalYears = endAge - startAge; + const totalMonths = Math.ceil(totalYears * 12); + + // Initialize simulation state + let balance = initialBalance; + let totalContributions = 0; + let totalInterestEarned = 0; + + const monthlyTimeline: MonthlyTimelineEntry[] = []; + + // Calculate compound multiplier and contribution timing + const compoundMultiplier = this.getCompoundMultiplier( + contributionFrequency, + compoundingFrequency + ); + const howOftenToCompound = this.getHowOftenToCompound( + contributionFrequency, + compoundingFrequency + ); + + // Monthly simulation loop + for (let month = 1; month <= totalMonths; month++) { + // Calculate current age + const currentAge = startAge + (month - 1) / 12; + + // Get annual return rate for current age + const annualReturnRate = this.calculateGlidepathReturn( + currentAge, + glidepathConfig, + startAge, + endAge + ); + + // Convert to monthly compounding rate + const monthlyReturnRate = + this.convertAnnualToMonthlyRate(annualReturnRate); + + // Handle contribution timing (match existing method logic) + let contributionThisMonth = 0; + if (month % howOftenToCompound === 0) { + contributionThisMonth = contributionAmount * compoundMultiplier; + } + + // Add contributions at start or end of month + if (contributionTiming === 'start' && contributionThisMonth > 0) { + balance += contributionThisMonth; + totalContributions += contributionThisMonth; + } + + // Apply monthly compounding + const interestEarnedThisMonth = balance * monthlyReturnRate; + balance += interestEarnedThisMonth; + totalInterestEarned += interestEarnedThisMonth; + + // Add contributions at end of month + if (contributionTiming === 'end' && contributionThisMonth > 0) { + balance += contributionThisMonth; + totalContributions += contributionThisMonth; + } + + // Get current equity weight for timeline data + const currentEquityWeight = this.getCurrentEquityWeight( + currentAge, + glidepathConfig, + startAge, + endAge + ); + + // Create timeline entry + const timelineEntry: MonthlyTimelineEntry = { + month, + age: currentAge, + currentBalance: balance, + cumulativeContributions: totalContributions, + cumulativeInterest: totalInterestEarned, + monthlyInterestEarned: interestEarnedThisMonth, + currentAnnualReturn: annualReturnRate, + currentMonthlyReturn: monthlyReturnRate, + currentEquityWeight, + }; + + monthlyTimeline.push(timelineEntry); + } + + // Calculate summary statistics + const effectiveAnnualReturn = + Math.pow(balance / initialBalance, 1 / totalYears) - 1; + const averageMonthlyReturn = + monthlyTimeline.reduce( + (sum, entry) => sum + entry.currentMonthlyReturn, + 0 + ) / monthlyTimeline.length; + + // Round final balance to nearest cent + const finalBalance = Math.round(balance * 100) / 100; + + return { + finalBalance, + totalContributions, + totalInterestEarned, + totalMonths, + startAge, + endAge, + glidepathMode: glidepathConfig.mode, + monthlyTimeline, + effectiveAnnualReturn, + averageMonthlyReturn, + }; + } + + /** + * Calculate the annual return rate for a given age using the specified glidepath strategy. + * @param age Current age for calculation + * @param config Glidepath configuration + * @param startAge Starting age for age-based calculations + * @param endAge Ending age for age-based calculations + * @returns Annual return rate (decimal format) + * @private + */ + private calculateGlidepathReturn( + age: number, + config: DynamicGlidepathConfig, + startAge: number, + endAge: number + ): number { + switch (config.mode) { + case 'fixed-return': + return this.calculateFixedReturnGlidepath( + age, + config, + startAge, + endAge + ); + case 'stepped-return': + return this.calculateSteppedReturnGlidepath(age, config); + case 'allocation-based': + return this.calculateAllocationBasedGlidepath( + age, + config, + startAge, + endAge + ); + case 'custom-waypoints': + return this.calculateCustomWaypointsGlidepath(age, config); + default: { + // TypeScript exhaustive check + const _exhaustive: never = config; + throw new Error( + `Unsupported glidepath mode: ${JSON.stringify(_exhaustive)}` + ); + } + } + } + + /** + * Calculate linear interpolation progress between start and end ages. + * @param age Current age + * @param startAge Starting age + * @param endAge Ending age + * @returns Clamped progress value between 0 and 1 + * @private + */ + private calculateAgeProgress( + age: number, + startAge: number, + endAge: number + ): number { + const ageProgress = (age - startAge) / (endAge - startAge); + return Math.max(0, Math.min(1, ageProgress)); + } + + /** + * Calculate return for fixed-return glidepath (linear interpolation). + * @param age Current age + * @param config Fixed return configuration + * @param startAge Starting age for calculation + * @param endAge Ending age for calculation + * @returns Annual return rate + * @private + */ + private calculateFixedReturnGlidepath( + age: number, + config: FixedReturnGlidepathConfig, + startAge: number, + endAge: number + ): number { + const progress = this.calculateAgeProgress(age, startAge, endAge); + return ( + config.startReturn + (config.endReturn - config.startReturn) * progress + ); + } + + /** + * Calculate return for stepped-return glidepath (Money Guy style). + * @param age Current age + * @param config Stepped return configuration + * @returns Annual return rate + * @private + */ + private calculateSteppedReturnGlidepath( + age: number, + config: SteppedReturnGlidepathConfig + ): number { + // Hold base return until decline start age + if (age < config.declineStartAge) { + return config.baseReturn; + } + + // Hold terminal return after terminal age + if (age >= config.terminalAge) { + return config.terminalReturn; + } + + // Linear decline between decline start and terminal age + const yearsFromStart = age - config.declineStartAge; + const calculatedReturn = + config.baseReturn - yearsFromStart * config.declineRate; + + // Ensure we don't go below terminal return + return Math.max(calculatedReturn, config.terminalReturn); + } + + /** + * Calculate return for allocation-based glidepath. + * @param age Current age + * @param config Allocation-based configuration + * @param startAge Starting age for calculation + * @param endAge Ending age for calculation + * @returns Annual return rate + * @private + */ + private calculateAllocationBasedGlidepath( + age: number, + config: AllocationBasedGlidepathConfig, + startAge: number, + endAge: number + ): number { + const progress = this.calculateAgeProgress(age, startAge, endAge); + + const currentEquityWeight = + config.startEquityWeight + + (config.endEquityWeight - config.startEquityWeight) * progress; + + return this.calculateBlendedReturn( + currentEquityWeight, + config.equityReturn, + config.bondReturn + ); + } + + /** + * Interpolate a value between waypoints for a given age. + * @param age Current age + * @param waypoints Array of waypoints (already sorted) + * @returns Interpolated value at the given age + * @private + */ + private interpolateWaypoints( + age: number, + waypoints: Array<{ age: number; value: number }> + ): number { + if (waypoints.length === 0) { + throw new Error('Waypoints array must contain at least one waypoint'); + } + + if (waypoints.length === 1) { + return waypoints[0].value; + } + + // Handle age outside waypoint range + if (age <= waypoints[0].age) { + return waypoints[0].value; + } + if (age >= waypoints[waypoints.length - 1].age) { + return waypoints[waypoints.length - 1].value; + } + + // Find surrounding waypoints + for (let i = 0; i < waypoints.length - 1; i++) { + if (age >= waypoints[i].age && age <= waypoints[i + 1].age) { + const lowerWaypoint = waypoints[i]; + const upperWaypoint = waypoints[i + 1]; + + // Linear interpolation + const ageProgress = + (age - lowerWaypoint.age) / (upperWaypoint.age - lowerWaypoint.age); + return ( + lowerWaypoint.value + + (upperWaypoint.value - lowerWaypoint.value) * ageProgress + ); + } + } + + // Should never reach here, but return last waypoint value as fallback + return waypoints[waypoints.length - 1].value; + } + + /** + * Calculate blended return based on equity and bond allocation. + * @param equityWeight Equity allocation weight (0.0 to 1.0) + * @param equityReturn Expected annual equity return + * @param bondReturn Expected annual bond return + * @returns Blended annual return rate + * @private + */ + private calculateBlendedReturn( + equityWeight: number, + equityReturn: number, + bondReturn: number + ): number { + return equityWeight * equityReturn + (1 - equityWeight) * bondReturn; + } + + /** + * Calculate return for custom waypoints glidepath. + * @param age Current age + * @param config Custom waypoints configuration + * @returns Annual return rate + * @private + */ + private calculateCustomWaypointsGlidepath( + age: number, + config: CustomWaypointsGlidepathConfig + ): number { + const waypoints = [...config.waypoints].sort((a, b) => a.age - b.age); + + if (waypoints.length === 0) { + throw new Error( + 'Custom waypoints configuration must have at least one waypoint' + ); + } + + const interpolatedValue = this.interpolateWaypoints(age, waypoints); + + // Convert to return rate based on value type + if (config.valueType === 'return') { + return interpolatedValue; + } else { + // equityWeight - blend with equity/bond returns + const equityReturn = config.equityReturn ?? 0.1; + const bondReturn = config.bondReturn ?? 0.04; + return this.calculateBlendedReturn( + interpolatedValue, + equityReturn, + bondReturn + ); + } + } + + /** + * Get the current equity weight for allocation-based and custom waypoints configurations. + * Used for timeline data enrichment. + * @param age Current age + * @param config Glidepath configuration + * @param startAge Starting age for age-based calculations + * @param endAge Ending age for age-based calculations + * @returns Equity weight (0.0 to 1.0) or undefined for non-allocation modes + * @private + */ + private getCurrentEquityWeight( + age: number, + config: DynamicGlidepathConfig, + startAge: number, + endAge: number + ): number | undefined { + switch (config.mode) { + case 'allocation-based': { + const progress = this.calculateAgeProgress(age, startAge, endAge); + return ( + config.startEquityWeight + + (config.endEquityWeight - config.startEquityWeight) * progress + ); + } + case 'custom-waypoints': { + if (config.valueType !== 'equityWeight') { + return undefined; + } + const waypoints = [...config.waypoints].sort((a, b) => a.age - b.age); + if (waypoints.length === 0) return undefined; + + return this.interpolateWaypoints(age, waypoints); + } + default: + return undefined; + } + } + + /** + * Convert annual interest rate to monthly compounding rate. + * Formula: monthlyRate = (1 + annualRate)^(1/12) - 1 + * @param annualRate Annual interest rate (decimal) + * @returns Monthly compounding rate (decimal) + * @private + */ + private convertAnnualToMonthlyRate(annualRate: number): number { + return Math.pow(1 + annualRate, 1 / 12) - 1; + } } diff --git a/src/constants/retirementCalculatorConstants.ts b/src/constants/retirementCalculatorConstants.ts index 7636889..22369b5 100644 --- a/src/constants/retirementCalculatorConstants.ts +++ b/src/constants/retirementCalculatorConstants.ts @@ -1,4 +1,9 @@ -import type { ContributionFrequencyType } from '../types/retirementCalculatorTypes'; +import type { + ContributionFrequencyType, + SteppedReturnGlidepathConfig, + CustomWaypointsGlidepathConfig, + GlidepathWaypoint, +} from '../types/retirementCalculatorTypes'; /** * Object representing contribution frequency options. @@ -24,3 +29,412 @@ export const CONTRIBUTION_FREQUENCY: ContributionFrequencyType = { */ WEEKLY: 52, }; + +// ======================================== +// Dynamic Glidepath Constants +// ======================================== + +/** + * Default configuration values for dynamic glidepath calculations. + * These provide sensible starting points for retirement planning scenarios. + * @readonly + */ +export const GLIDEPATH_DEFAULTS = { + /** + * Default fixed return glidepath configuration. + * Represents a typical target-date fund progression. + */ + FIXED_RETURN: { + /** Starting return rate for aggressive investing (10%) */ + START_RETURN: 0.1, + /** Ending return rate for conservative investing (5.5%) */ + END_RETURN: 0.055, + }, + + /** + * Default allocation-based glidepath configuration. + * Follows the common "120 minus age" equity allocation rule progression. + */ + ALLOCATION_BASED: { + /** Starting equity weight for young investors (90%) */ + START_EQUITY_WEIGHT: 0.9, + /** Ending equity weight near retirement (30%) */ + END_EQUITY_WEIGHT: 0.3, + /** Expected annual return for equity investments (12%) */ + EQUITY_RETURN: 0.12, + /** Expected annual return for bond investments (4%) */ + BOND_RETURN: 0.04, + }, + + /** + * Default ages for glidepath calculations. + */ + AGES: { + /** Typical starting career age */ + START_AGE: 25, + /** Standard retirement age */ + END_AGE: 65, + }, +} as const; + +/** + * Validation constraints for dynamic glidepath parameters. + * These ensure mathematical soundness and prevent invalid configurations. + * @readonly + */ +export const GLIDEPATH_VALIDATION = { + /** + * Age validation limits. + */ + AGES: { + /** Minimum allowed age (must be positive) */ + MIN_AGE: 0.1, + /** Maximum reasonable age for calculations */ + MAX_AGE: 150, + /** Minimum age difference for meaningful glidepath */ + MIN_AGE_DIFFERENCE: 1, + }, + + /** + * Return rate validation limits. + */ + RETURNS: { + /** Minimum return rate (-99% loss maximum) */ + MIN_RETURN: -0.99, + /** Maximum reasonable return rate (100% annual gain) */ + MAX_RETURN: 1.0, + /** Typical range warning threshold for high returns */ + HIGH_RETURN_WARNING: 0.25, + }, + + /** + * Allocation weight validation limits. + */ + ALLOCATIONS: { + /** Minimum allocation weight (0%) */ + MIN_WEIGHT: 0.0, + /** Maximum allocation weight (100%) */ + MAX_WEIGHT: 1.0, + }, + + /** + * Financial parameter validation limits. + */ + FINANCIAL: { + /** Minimum balance (must be non-negative) */ + MIN_BALANCE: 0, + /** Minimum contribution (must be non-negative) */ + MIN_CONTRIBUTION: 0, + /** Maximum reasonable balance for calculations */ + MAX_BALANCE: Number.MAX_SAFE_INTEGER / 1000, + }, + + /** + * Waypoint validation limits. + */ + WAYPOINTS: { + /** Minimum number of waypoints required */ + MIN_WAYPOINTS: 1, + /** Maximum recommended waypoints for performance */ + MAX_WAYPOINTS: 100, + }, +} as const; + +/** + * Mathematical constants and precision settings for glidepath calculations. + * These ensure numerical stability and consistent rounding behavior. + * @readonly + */ +export const GLIDEPATH_MATH = { + /** + * Precision and rounding constants. + */ + PRECISION: { + /** Number of decimal places for monetary amounts */ + CURRENCY_DECIMALS: 2, + /** Multiplier for currency rounding (100 for cents) */ + CURRENCY_MULTIPLIER: 100, + /** Number of decimal places for percentage calculations */ + PERCENTAGE_DECIMALS: 6, + /** Number of decimal places for interest rate calculations */ + INTEREST_RATE_DECIMALS: 10, + }, + + /** + * Epsilon values for floating-point comparisons. + */ + EPSILON: { + /** General floating-point comparison tolerance */ + GENERAL: 1e-10, + /** Currency comparison tolerance (0.01 cents) */ + CURRENCY: 1e-4, + /** Percentage comparison tolerance */ + PERCENTAGE: 1e-8, + }, + + /** + * Mathematical conversion constants. + */ + CONVERSION: { + /** Months per year for age calculations */ + MONTHS_PER_YEAR: 12, + /** Weeks per year for contribution calculations */ + WEEKS_PER_YEAR: 52, + /** Days per year for precise calculations */ + DAYS_PER_YEAR: 365.25, + }, +} as const; + +/** + * Configuration templates for common glidepath scenarios. + * These provide ready-to-use configurations for typical retirement planning needs. + * @readonly + */ +export const GLIDEPATH_TEMPLATES = { + /** + * Conservative glidepath with lower volatility. + */ + CONSERVATIVE: { + FIXED_RETURN: { + START_RETURN: 0.07, + END_RETURN: 0.04, + }, + ALLOCATION_BASED: { + START_EQUITY_WEIGHT: 0.6, + END_EQUITY_WEIGHT: 0.2, + EQUITY_RETURN: 0.1, + BOND_RETURN: 0.035, + }, + }, + + /** + * Aggressive glidepath with higher growth potential. + */ + AGGRESSIVE: { + FIXED_RETURN: { + START_RETURN: 0.13, + END_RETURN: 0.07, + }, + ALLOCATION_BASED: { + START_EQUITY_WEIGHT: 1.0, + END_EQUITY_WEIGHT: 0.5, + EQUITY_RETURN: 0.15, + BOND_RETURN: 0.04, + }, + }, + + /** + * Moderate/balanced glidepath (same as defaults). + */ + MODERATE: { + FIXED_RETURN: GLIDEPATH_DEFAULTS.FIXED_RETURN, + ALLOCATION_BASED: GLIDEPATH_DEFAULTS.ALLOCATION_BASED, + }, +} as const; + +/** + * Error messages for dynamic glidepath validation failures. + * These provide clear, actionable feedback to developers and users. + * @readonly + */ +export const GLIDEPATH_ERROR_MESSAGES = { + /** + * Age-related validation errors. + */ + AGES: { + NEGATIVE_START_AGE: 'startAge must be positive', + NEGATIVE_END_AGE: 'endAge must be positive', + START_AGE_TOO_HIGH: 'startAge must be less than endAge', + AGE_DIFFERENCE_TOO_SMALL: + 'Age difference (endAge - startAge) must be at least 1 year', + AGE_OUT_OF_BOUNDS: 'Ages must be between 0.1 and 150 years', + }, + + /** + * Financial parameter validation errors. + */ + FINANCIAL: { + NEGATIVE_BALANCE: 'initialBalance must be non-negative', + NEGATIVE_CONTRIBUTION: 'monthlyContribution must be non-negative', + BALANCE_TOO_LARGE: 'initialBalance exceeds maximum safe calculation limit', + }, + + /** + * Return rate validation errors. + */ + RETURNS: { + RETURN_TOO_LOW: + 'Return rates must be greater than -0.99 (cannot lose more than 99%)', + RETURN_TOO_HIGH: 'Return rates above 100% annual return are not supported', + HIGH_RETURN_WARNING: 'Return rates above 25% should be used with caution', + }, + + /** + * Allocation weight validation errors. + */ + ALLOCATIONS: { + WEIGHT_BELOW_ZERO: + 'Allocation weights must be between 0.0 and 1.0 inclusive', + WEIGHT_ABOVE_ONE: + 'Allocation weights must be between 0.0 and 1.0 inclusive', + }, + + /** + * Glidepath mode validation errors. + */ + MODES: { + INVALID_MODE: 'Invalid glidepath mode specified', + MISSING_REQUIRED_FIELDS: + 'Required fields are missing for the specified glidepath mode', + }, + + /** + * Waypoint validation errors. + */ + WAYPOINTS: { + EMPTY_WAYPOINTS: 'Waypoints array must contain at least one waypoint', + INVALID_WAYPOINT_AGE: 'Waypoint ages must be positive', + INVALID_WAYPOINT_VALUE: + 'Waypoint values must be valid for the specified value type', + TOO_MANY_WAYPOINTS: 'Too many waypoints may impact performance', + MISSING_EQUITY_RETURN: + 'equityReturn is required when valueType is "equityWeight"', + MISSING_BOND_RETURN: + 'bondReturn is required when valueType is "equityWeight"', + }, + + /** + * General configuration errors. + */ + GENERAL: { + INVALID_CONTRIBUTION_TIMING: 'contributionTiming must be "start" or "end"', + CONFIGURATION_MISMATCH: + 'Glidepath configuration does not match the specified mode', + }, +} as const; + +/** + * Performance optimization thresholds for glidepath calculations. + * These help determine when to use memory-optimized calculation modes. + * @readonly + */ +export const GLIDEPATH_PERFORMANCE = { + /** + * Timeline generation thresholds. + */ + TIMELINE: { + /** Number of months above which to consider memory optimization */ + LARGE_SIMULATION_MONTHS: 600, // 50 years + /** Number of months above which to warn about performance */ + PERFORMANCE_WARNING_MONTHS: 1200, // 100 years + }, + + /** + * Cache size limits for performance optimization. + */ + CACHE: { + /** Maximum number of cached monthly rate conversions */ + MAX_RATE_CACHE_SIZE: 1000, + /** Maximum number of cached waypoint interpolations */ + MAX_INTERPOLATION_CACHE_SIZE: 500, + }, +} as const; + +/** + * Pre-configured glidepath strategies based on popular financial planning approaches. + * Each preset includes documentation links to the original methodology. + */ +export const GLIDEPATH_PRESETS = { + /** + * Money Guy Show strategy: 10% returns declining 0.1% per year to 5.5% floor at age 65. + * + * This strategy holds 10% returns until age 20, then declines by exactly 0.1% per year + * until reaching the 5.5% floor at age 65, then holds that terminal return. + * + * Source: https://www.moneyguy.com/ + * Reference: Financial Order of Operations and investment return assumptions + */ + MONEY_GUY_SHOW: { + mode: 'stepped-return', + baseReturn: 0.1, // 10% base return + declineRate: 0.001, // 0.1% decline per year + terminalReturn: 0.055, // 5.5% terminal return + declineStartAge: 20, // Start declining at age 20 + terminalAge: 65, // Reach terminal at age 65 + } as const satisfies SteppedReturnGlidepathConfig, + + /** + * Bogleheads "100 minus age" equity allocation strategy. + * + * Conservative interpretation with minimum 20% equity allocation. + * Uses historical US total stock market returns (10%) and intermediate bonds (4%). + * + * Source: https://www.bogleheads.org/wiki/Asset_allocation + * Reference: Age-based allocation guidelines and three-fund portfolio + */ + BOGLEHEADS_100_MINUS_AGE: { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 20, value: 0.8 }, // 100-20 = 80% equity + { age: 30, value: 0.7 }, // 100-30 = 70% equity + { age: 40, value: 0.6 }, // 100-40 = 60% equity + { age: 50, value: 0.5 }, // 100-50 = 50% equity + { age: 60, value: 0.4 }, // 100-60 = 40% equity + { age: 65, value: 0.35 }, // 100-65 = 35% equity + { age: 80, value: 0.2 }, // Minimum 20% equity floor + ] as GlidepathWaypoint[], + equityReturn: 0.1, // Historical US total stock market + bondReturn: 0.04, // Intermediate-term government/corporate bonds + } satisfies CustomWaypointsGlidepathConfig, + + /** + * Bogleheads "110 minus age" more aggressive equity allocation strategy. + * + * More aggressive interpretation for longer time horizons and higher risk tolerance. + * Maintains higher equity allocation throughout the lifecycle. + * + * Source: https://www.bogleheads.org/wiki/Asset_allocation + * Reference: Age-based allocation variations for aggressive investors + */ + BOGLEHEADS_110_MINUS_AGE: { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 20, value: 0.9 }, // 110-20 = 90% equity + { age: 30, value: 0.8 }, // 110-30 = 80% equity + { age: 40, value: 0.7 }, // 110-40 = 70% equity + { age: 50, value: 0.6 }, // 110-50 = 60% equity + { age: 60, value: 0.5 }, // 110-60 = 50% equity + { age: 65, value: 0.45 }, // 110-65 = 45% equity + { age: 80, value: 0.3 }, // Minimum 30% equity floor + ] as GlidepathWaypoint[], + equityReturn: 0.1, + bondReturn: 0.04, + } satisfies CustomWaypointsGlidepathConfig, + + /** + * Bogleheads "120 minus age" very aggressive equity allocation strategy. + * + * Most aggressive interpretation for very long time horizons and high risk tolerance. + * Suitable for young investors with decades until retirement. + * + * Source: https://www.bogleheads.org/wiki/Asset_allocation + * Reference: Age-based allocation variations for very aggressive investors + */ + BOGLEHEADS_120_MINUS_AGE: { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 20, value: 1.0 }, // 120-20 = 100% equity + { age: 30, value: 0.9 }, // 120-30 = 90% equity + { age: 40, value: 0.8 }, // 120-40 = 80% equity + { age: 50, value: 0.7 }, // 120-50 = 70% equity + { age: 60, value: 0.6 }, // 120-60 = 60% equity + { age: 65, value: 0.55 }, // 120-65 = 55% equity + { age: 80, value: 0.4 }, // Minimum 40% equity floor + ] as GlidepathWaypoint[], + equityReturn: 0.1, + bondReturn: 0.04, + } satisfies CustomWaypointsGlidepathConfig, +} as const; diff --git a/src/errors/DynamicGlidepathErrors.ts b/src/errors/DynamicGlidepathErrors.ts new file mode 100644 index 0000000..2506d94 --- /dev/null +++ b/src/errors/DynamicGlidepathErrors.ts @@ -0,0 +1,901 @@ +/** + * @fileoverview Comprehensive error handling infrastructure for Dynamic Glidepath calculations. + * + * This module provides: + * - Custom error classes for all validation scenarios + * - Error codes and categorization system + * - User-friendly error messages with context + * - Type guards and utilities for error handling + * - Integration with existing TypeScript patterns + * + * @version 1.0.0 + */ + +// ============================================================================ +// ERROR CODE DEFINITIONS +// ============================================================================ + +/** + * Enumeration of all possible error codes for dynamic glidepath validation. + * These codes provide machine-readable error identification. + */ +export const GLIDEPATH_ERROR_CODES = { + // Age-related validation errors + NEGATIVE_AGE: 'NEGATIVE_AGE', + INVALID_AGE_RANGE: 'INVALID_AGE_RANGE', + INSUFFICIENT_TIME_HORIZON: 'INSUFFICIENT_TIME_HORIZON', + + // Financial parameter validation errors + NEGATIVE_BALANCE: 'NEGATIVE_BALANCE', + NEGATIVE_CONTRIBUTION: 'NEGATIVE_CONTRIBUTION', + + // Return rate validation errors + IMPOSSIBLE_LOSS: 'IMPOSSIBLE_LOSS', + INVALID_RETURN_TYPE: 'INVALID_RETURN_TYPE', + + // Allocation validation errors + INVALID_ALLOCATION: 'INVALID_ALLOCATION', + INVALID_ALLOCATION_TYPE: 'INVALID_ALLOCATION_TYPE', + + // Configuration structure errors + INVALID_CONFIG_STRUCTURE: 'INVALID_CONFIG_STRUCTURE', + UNKNOWN_GLIDEPATH_MODE: 'UNKNOWN_GLIDEPATH_MODE', + INVALID_CONTRIBUTION_TIMING: 'INVALID_CONTRIBUTION_TIMING', + + // Waypoint validation errors + EMPTY_WAYPOINTS: 'EMPTY_WAYPOINTS', + INVALID_WAYPOINT_AGE: 'INVALID_WAYPOINT_AGE', + INVALID_WAYPOINT_VALUE: 'INVALID_WAYPOINT_VALUE', + INVALID_VALUE_TYPE: 'INVALID_VALUE_TYPE', + MISSING_EQUITY_RETURN: 'MISSING_EQUITY_RETURN', + MISSING_BOND_RETURN: 'MISSING_BOND_RETURN', + + // Warning codes + LONG_SIMULATION_WARNING: 'LONG_SIMULATION_WARNING', + LARGE_CONTRIBUTION_WARNING: 'LARGE_CONTRIBUTION_WARNING', + UNUSUAL_ALLOCATION_PROGRESSION: 'UNUSUAL_ALLOCATION_PROGRESSION', + EXTREME_RETURN_WARNING: 'EXTREME_RETURN_WARNING', + EXTREME_LOSS_WARNING: 'EXTREME_LOSS_WARNING', + WAYPOINTS_OUTSIDE_RANGE: 'WAYPOINTS_OUTSIDE_RANGE', + WAYPOINT_GAP_WARNING: 'WAYPOINT_GAP_WARNING', + + // Runtime errors + CALCULATION_ERROR: 'CALCULATION_ERROR', + STRATEGY_CREATION_ERROR: 'STRATEGY_CREATION_ERROR', + SIMULATION_ERROR: 'SIMULATION_ERROR', +} as const; + +/** + * Type for error code values. + */ +export type GlidepathErrorCode = + (typeof GLIDEPATH_ERROR_CODES)[keyof typeof GLIDEPATH_ERROR_CODES]; + +/** + * Error severity levels. + */ +export type ErrorSeverity = 'error' | 'warning'; + +/** + * Error categories for grouping related errors. + */ +export type ErrorCategory = + | 'validation' + | 'configuration' + | 'calculation' + | 'performance' + | 'usability'; + +// ============================================================================ +// CORE ERROR INTERFACES +// ============================================================================ + +/** + * Enhanced error information interface that extends the base Error. + * Provides comprehensive context for debugging and user-facing applications. + */ +export type GlidepathErrorInfo = { + /** Machine-readable error code */ + code: GlidepathErrorCode; + /** Field name that caused the error */ + field: string; + /** The invalid value that was provided */ + value: unknown; + /** Description of the constraint that was violated */ + constraint: string; + /** Helpful suggestion for fixing the error */ + suggestion: string; + /** Error severity level */ + severity: ErrorSeverity; + /** Error category for grouping */ + category: ErrorCategory; + /** Additional context data */ + context?: Record; +}; + +/** + * Validation result interface for comprehensive error reporting. + */ +export type ValidationResult = { + /** Whether validation passed */ + isValid: boolean; + /** Array of validation errors */ + errors: DynamicGlidepathValidationError[]; + /** Array of validation warnings */ + warnings: DynamicGlidepathValidationError[]; +}; + +// ============================================================================ +// CUSTOM ERROR CLASSES +// ============================================================================ + +/** + * Base class for all dynamic glidepath validation errors. + * Extends the native Error class with enhanced error information. + */ +export class DynamicGlidepathError extends Error { + public readonly name: string = 'DynamicGlidepathError'; + public readonly code: GlidepathErrorCode; + public readonly field: string; + public readonly value: unknown; + public readonly constraint: string; + public readonly suggestion: string; + public readonly severity: ErrorSeverity; + public readonly category: ErrorCategory; + public readonly context: Record; + public readonly timestamp: Date; + + constructor(errorInfo: GlidepathErrorInfo) { + const message = DynamicGlidepathError.formatErrorMessage(errorInfo); + super(message); + + this.code = errorInfo.code; + this.field = errorInfo.field; + this.value = errorInfo.value; + this.constraint = errorInfo.constraint; + this.suggestion = errorInfo.suggestion; + this.severity = errorInfo.severity; + this.category = errorInfo.category; + this.context = errorInfo.context ?? {}; + this.timestamp = new Date(); + + // Maintain proper stack trace + if (Error.captureStackTrace !== undefined) { + Error.captureStackTrace(this, DynamicGlidepathError); + } + } + + /** + * Format error message with consistent structure. + */ + private static formatErrorMessage(errorInfo: GlidepathErrorInfo): string { + const valueStr = + errorInfo.value !== undefined + ? ` Received: ${String(errorInfo.value)}.` + : ''; + return `Invalid ${errorInfo.field}:${valueStr} ${errorInfo.constraint}. ${errorInfo.suggestion}`; + } + + /** + * Convert error to JSON for serialization. + */ + public toJSON(): Record { + return { + name: this.name, + message: this.message, + code: this.code, + field: this.field, + value: this.value, + constraint: this.constraint, + suggestion: this.suggestion, + severity: this.severity, + category: this.category, + context: this.context, + timestamp: this.timestamp.toISOString(), + stack: this.stack, + }; + } + + /** + * Get user-friendly error summary for display. + */ + public getUserFriendlyMessage(): string { + return `${this.constraint}. ${this.suggestion}`; + } + + /** + * Check if this error should prevent calculation from proceeding. + */ + public isBlockingError(): boolean { + return this.severity === 'error'; + } +} + +/** + * Specific error class for validation errors. + * Used for input parameter validation failures. + */ +export class DynamicGlidepathValidationError extends DynamicGlidepathError { + public readonly name: string = 'DynamicGlidepathValidationError'; + + constructor(errorInfo: GlidepathErrorInfo) { + super({ ...errorInfo, category: 'validation' }); + this.name = 'DynamicGlidepathValidationError'; + } +} + +/** + * Specific error class for configuration errors. + * Used for glidepath configuration structure issues. + */ +export class DynamicGlidepathConfigurationError extends DynamicGlidepathError { + public readonly name: string = 'DynamicGlidepathConfigurationError'; + + constructor(errorInfo: GlidepathErrorInfo) { + super({ ...errorInfo, category: 'configuration' }); + this.name = 'DynamicGlidepathConfigurationError'; + } +} + +/** + * Specific error class for calculation runtime errors. + * Used for errors that occur during the calculation process. + */ +export class DynamicGlidepathCalculationError extends DynamicGlidepathError { + public readonly name: string = 'DynamicGlidepathCalculationError'; + + constructor(errorInfo: GlidepathErrorInfo) { + super({ ...errorInfo, category: 'calculation' }); + this.name = 'DynamicGlidepathCalculationError'; + } +} + +// ============================================================================ +// ERROR FACTORY FUNCTIONS +// ============================================================================ + +/** + * Create age-related validation errors. + */ +export function createAgeError( + field: string, + value: number, + endAge?: number +): DynamicGlidepathValidationError { + if (value <= 0) { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.NEGATIVE_AGE, + field, + value, + constraint: 'Must be greater than 0', + suggestion: + 'Try: startAge=25 for early career, startAge=40 for mid-career', + severity: 'error', + category: 'validation', + }); + } + + if (endAge !== undefined && endAge !== null && value >= endAge) { + const suggestedStartAge = Math.max(18, endAge - 40); + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_AGE_RANGE, + field, + value, + constraint: 'startAge must be less than endAge', + suggestion: `Try: startAge=${suggestedStartAge}, endAge=${endAge} for a ${ + endAge - suggestedStartAge + }-year plan`, + severity: 'error', + category: 'validation', + context: { endAge, suggestedStartAge }, + }); + } + + throw new Error('Invalid age error parameters'); +} + +/** + * Create financial parameter validation errors. + */ +export function createFinancialError( + field: string, + value: number +): DynamicGlidepathValidationError { + return new DynamicGlidepathValidationError({ + code: + field === 'initialBalance' + ? GLIDEPATH_ERROR_CODES.NEGATIVE_BALANCE + : GLIDEPATH_ERROR_CODES.NEGATIVE_CONTRIBUTION, + field, + value, + constraint: 'Must be non-negative', + suggestion: + field === 'initialBalance' + ? 'Try: 0 if starting with no savings, or 25000 for $25K initial balance' + : 'Try: 0 if not making regular contributions, or 1000 for $1K monthly contributions', + severity: 'error', + category: 'validation', + }); +} + +/** + * Create return rate validation errors. + */ +export function createReturnRateError( + field: string, + value: number +): DynamicGlidepathValidationError { + if (typeof value !== 'number') { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_RETURN_TYPE, + field, + value, + constraint: 'Must be a number', + suggestion: 'Try: 0.08 for 8% annual return', + severity: 'error', + category: 'validation', + }); + } + + if (value <= -1.0) { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.IMPOSSIBLE_LOSS, + field, + value, + constraint: 'Must be greater than -1.0 (cannot lose more than 100%)', + suggestion: 'Try: -0.30 for 30% loss, or 0.08 for 8% annual return', + severity: 'error', + category: 'validation', + }); + } + + throw new Error('Invalid return rate error parameters'); +} + +/** + * Create allocation weight validation errors. + */ +export function createAllocationError( + field: string, + value: number +): DynamicGlidepathValidationError { + if (typeof value !== 'number') { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION_TYPE, + field, + value, + constraint: 'Must be a number', + suggestion: 'Try: 0.70 for 70% allocation', + severity: 'error', + category: 'validation', + }); + } + + if (value < 0 || value > 1) { + const suggestion = + value > 1 + ? `Did you mean ${(value / 100).toFixed(2)}? (${value}% as decimal)` + : 'Try: 0.70 for 70% allocation'; + + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION, + field, + value, + constraint: 'Must be between 0.0 and 1.0 inclusive', + suggestion, + severity: 'error', + category: 'validation', + }); + } + + throw new Error('Invalid allocation error parameters'); +} + +/** + * Create configuration structure errors. + */ +export function createConfigurationError( + field: string, + value: unknown, + expectedValues?: string[] +): DynamicGlidepathConfigurationError { + if ( + field === 'mode' && + expectedValues !== undefined && + expectedValues.length > 0 + ) { + return new DynamicGlidepathConfigurationError({ + code: GLIDEPATH_ERROR_CODES.UNKNOWN_GLIDEPATH_MODE, + field, + value, + constraint: `Must be one of: ${expectedValues.join(', ')}`, + suggestion: 'Try: mode: "fixed-return" for simple linear return changes', + severity: 'error', + category: 'configuration', + }); + } + + if (field === 'glidepathConfig') { + return new DynamicGlidepathConfigurationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_CONFIG_STRUCTURE, + field, + value, + constraint: 'Must be a valid configuration object', + suggestion: + 'Try: { mode: "fixed-return", startReturn: 0.08, endReturn: 0.06 }', + severity: 'error', + category: 'configuration', + }); + } + + throw new Error('Invalid configuration error parameters'); +} + +/** + * Create waypoint-specific validation errors. + */ +export function createWaypointError( + field: string, + value: unknown, + index?: number +): DynamicGlidepathValidationError { + const fieldName = typeof index === 'number' ? `${field}[${index}]` : field; + + if (field === 'waypoints' && Array.isArray(value) && value.length === 0) { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.EMPTY_WAYPOINTS, + field: fieldName, + value, + constraint: 'Must contain at least one waypoint', + suggestion: 'Try: [{ age: 30, value: 0.80 }, { age: 65, value: 0.30 }]', + severity: 'error', + category: 'validation', + }); + } + + if (field.includes('age')) { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_WAYPOINT_AGE, + field: fieldName, + value, + constraint: 'Must be a positive number', + suggestion: 'Try: age: 35 for waypoint at age 35', + severity: 'error', + category: 'validation', + }); + } + + if (field.includes('value')) { + return new DynamicGlidepathValidationError({ + code: GLIDEPATH_ERROR_CODES.INVALID_WAYPOINT_VALUE, + field: fieldName, + value, + constraint: 'Must be a number', + suggestion: 'Try: 0.08 for 8% return or 0.70 for 70% equity allocation', + severity: 'error', + category: 'validation', + }); + } + + throw new Error('Invalid waypoint error parameters'); +} + +/** + * Create warning errors for performance and usability issues. + */ +export function createWarning( + code: GlidepathErrorCode, + field: string, + value: unknown, + context?: Record +): DynamicGlidepathValidationError { + const warningTemplates: Record< + string, + { constraint: string; suggestion: string } + > = { + [GLIDEPATH_ERROR_CODES.LONG_SIMULATION_WARNING]: { + constraint: 'Very long simulation may impact performance', + suggestion: 'Consider breaking into shorter periods for analysis', + }, + [GLIDEPATH_ERROR_CODES.LARGE_CONTRIBUTION_WARNING]: { + constraint: 'Very large monthly contribution detected', + suggestion: 'Verify this amount is correct (typical range: $100-$5,000)', + }, + [GLIDEPATH_ERROR_CODES.UNUSUAL_ALLOCATION_PROGRESSION]: { + constraint: 'Equity allocation increases with age (unusual pattern)', + suggestion: + 'Consider decreasing equity allocation over time for typical retirement planning', + }, + [GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING]: { + constraint: 'Very high return rate detected', + suggestion: + 'Typical returns range from 4% to 15% annually (0.04 to 0.15)', + }, + [GLIDEPATH_ERROR_CODES.EXTREME_LOSS_WARNING]: { + constraint: 'Very large loss rate detected', + suggestion: + 'Consider more moderate loss scenarios for realistic planning', + }, + }; + + const template = warningTemplates[code]; + if (template === undefined) { + throw new Error(`No template found for warning code: ${code}`); + } + + return new DynamicGlidepathValidationError({ + code, + field, + value, + constraint: template.constraint, + suggestion: template.suggestion, + severity: 'warning', + category: 'usability', + context, + }); +} + +/** + * Create runtime calculation errors. + */ +export function createCalculationError( + message: string, + context?: Record +): DynamicGlidepathCalculationError { + return new DynamicGlidepathCalculationError({ + code: GLIDEPATH_ERROR_CODES.CALCULATION_ERROR, + field: 'calculation', + value: undefined, + constraint: 'Calculation failed during execution', + suggestion: 'Please check your input parameters and try again', + severity: 'error', + category: 'calculation', + context: { originalMessage: message, ...context }, + }); +} + +// ============================================================================ +// TYPE GUARDS AND UTILITIES +// ============================================================================ + +/** + * Type guard to check if an error is a DynamicGlidepathError. + */ +export function isDynamicGlidepathError( + error: unknown +): error is DynamicGlidepathError { + return error instanceof DynamicGlidepathError; +} + +/** + * Type guard to check if an error is a validation error. + */ +export function isValidationError( + error: unknown +): error is DynamicGlidepathValidationError { + return ( + error instanceof DynamicGlidepathValidationError && + error.category === 'validation' + ); +} + +/** + * Type guard to check if an error is a configuration error. + */ +export function isConfigurationError( + error: unknown +): error is DynamicGlidepathConfigurationError { + return ( + error instanceof DynamicGlidepathConfigurationError && + error.category === 'configuration' + ); +} + +/** + * Type guard to check if an error is a calculation error. + */ +export function isCalculationError( + error: unknown +): error is DynamicGlidepathCalculationError { + return ( + error instanceof DynamicGlidepathCalculationError && + error.category === 'calculation' + ); +} + +/** + * Type guard to check if an error is a warning (non-blocking). + */ +export function isWarning(error: DynamicGlidepathError): boolean { + return error.severity === 'warning'; +} + +/** + * Type guard to check if an error is blocking (prevents calculation). + */ +export function isBlockingError(error: DynamicGlidepathError): boolean { + return error.severity === 'error'; +} + +// ============================================================================ +// ERROR AGGREGATION AND FORMATTING +// ============================================================================ + +/** + * Group errors by category for organized display. + */ +export function groupByCategory( + errors: DynamicGlidepathError[] +): Record { + const groups: Record = { + validation: [], + configuration: [], + calculation: [], + performance: [], + usability: [], + }; + + errors.forEach((error) => { + groups[error.category].push(error); + }); + + return groups; +} + +/** + * Group errors by severity for prioritized handling. + */ +export function groupBySeverity(errors: DynamicGlidepathError[]): { + errors: DynamicGlidepathError[]; + warnings: DynamicGlidepathError[]; +} { + return { + errors: errors.filter((e) => e.severity === 'error'), + warnings: errors.filter((e) => e.severity === 'warning'), + }; +} + +/** + * Get summary statistics for error collection. + */ +export function getSummary(errors: DynamicGlidepathError[]): { + total: number; + errorCount: number; + warningCount: number; + categories: Record; + topCodes: Array<{ code: GlidepathErrorCode; count: number }>; +} { + const severity = groupBySeverity(errors); + const categories = groupByCategory(errors); + + // Count by error code + const codeCounts = new Map(); + errors.forEach((error) => { + codeCounts.set(error.code, (codeCounts.get(error.code) ?? 0) + 1); + }); + + const topCodes = Array.from(codeCounts.entries()) + .map(([code, count]) => ({ code, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + + return { + total: errors.length, + errorCount: severity.errors.length, + warningCount: severity.warnings.length, + categories: { + validation: categories.validation.length, + configuration: categories.configuration.length, + calculation: categories.calculation.length, + performance: categories.performance.length, + usability: categories.usability.length, + }, + topCodes, + }; +} + +/** + * Format errors for console display. + */ +export function formatForConsole( + errors: DynamicGlidepathError[], + options: { + showSuggestions?: boolean; + showErrorCodes?: boolean; + groupByCategory?: boolean; + } = {} +): string { + const { + showSuggestions = true, + showErrorCodes = false, + groupByCategory: shouldGroupByCategory = false, + } = options; + + if (errors.length === 0) { + return '✅ No errors found'; + } + + let output = ''; + + if (shouldGroupByCategory) { + const groups = groupByCategory(errors); + Object.entries(groups).forEach(([category, categoryErrors]) => { + if (categoryErrors.length > 0) { + output += `\n📁 ${category.toUpperCase()} ERRORS:\n`; + categoryErrors.forEach( + (error: DynamicGlidepathError, index: number) => { + output += formatSingleError( + error, + index + 1, + showSuggestions, + showErrorCodes + ); + } + ); + } + }); + } else { + const severity = groupBySeverity(errors); + + if (severity.errors.length > 0) { + output += '❌ ERRORS:\n'; + severity.errors.forEach((error, index) => { + output += formatSingleError( + error, + index + 1, + showSuggestions, + showErrorCodes + ); + }); + } + + if (severity.warnings.length > 0) { + output += '\n⚠️ WARNINGS:\n'; + severity.warnings.forEach((warning, index) => { + output += formatSingleError( + warning, + index + 1, + showSuggestions, + showErrorCodes + ); + }); + } + } + + return output.trim(); +} + +/** + * Format a single error for console display. + */ +function formatSingleError( + error: DynamicGlidepathError, + index: number, + showSuggestions: boolean, + showErrorCodes: boolean +): string { + let output = ` ${index}. ${error.message}\n`; + + if (showSuggestions && error.suggestion !== '') { + output += ` 💡 ${error.suggestion}\n`; + } + + if (showErrorCodes) { + output += ` 📋 Code: ${error.code}\n`; + output += ` 🏷️ Field: ${error.field}\n`; + } + + return output + '\n'; +} + +/** + * Convert errors to API-friendly format. + */ +export function formatForAPI(errors: DynamicGlidepathError[]): { + errors: Array<{ + code: GlidepathErrorCode; + field: string; + message: string; + suggestion: string; + severity: ErrorSeverity; + category: ErrorCategory; + }>; + summary: ReturnType; +} { + return { + errors: errors.map((error) => ({ + code: error.code, + field: error.field, + message: error.message, + suggestion: error.suggestion, + severity: error.severity, + category: error.category, + })), + summary: getSummary(errors), + }; +} + +// ============================================================================ +// VALIDATION RESULT BUILDER +// ============================================================================ + +/** + * Builder class for constructing validation results. + */ +export class ValidationResultBuilder { + private errors: DynamicGlidepathValidationError[] = []; + private warnings: DynamicGlidepathValidationError[] = []; + + /** + * Add an error to the validation result. + */ + addError(error: DynamicGlidepathValidationError): this { + this.errors.push(error); + return this; + } + + /** + * Add a warning to the validation result. + */ + addWarning(warning: DynamicGlidepathValidationError): this { + this.warnings.push(warning); + return this; + } + + /** + * Add multiple errors. + */ + addErrors(errors: DynamicGlidepathValidationError[]): this { + this.errors.push(...errors); + return this; + } + + /** + * Add multiple warnings. + */ + addWarnings(warnings: DynamicGlidepathValidationError[]): this { + this.warnings.push(...warnings); + return this; + } + + /** + * Build the final validation result. + */ + build(): ValidationResult { + return { + isValid: this.errors.length === 0, + errors: [...this.errors], + warnings: [...this.warnings], + }; + } + + /** + * Clear all errors and warnings. + */ + clear(): this { + this.errors = []; + this.warnings = []; + return this; + } + + /** + * Get current error count. + */ + getErrorCount(): number { + return this.errors.length; + } + + /** + * Get current warning count. + */ + getWarningCount(): number { + return this.warnings.length; + } + + /** + * Check if there are any blocking errors. + */ + hasBlockingErrors(): boolean { + return this.errors.some((error) => isBlockingError(error)); + } +} diff --git a/src/types/retirementCalculatorTypes.ts b/src/types/retirementCalculatorTypes.ts index 417b371..4502056 100644 --- a/src/types/retirementCalculatorTypes.ts +++ b/src/types/retirementCalculatorTypes.ts @@ -2,7 +2,7 @@ * Object representing contribution frequency options. * @interface */ -export interface ContributionFrequencyType { +export type ContributionFrequencyType = { /** * Yearly contribution frequency. * @type {number} @@ -20,13 +20,13 @@ export interface ContributionFrequencyType { * @type {number} */ WEEKLY: number; -} +}; /** * Object representing the details needed to determine contributions. * @interface */ -export interface DetermineContributionType { +export type DetermineContributionType = { /** * Contribution needed per period. * @type {number} @@ -56,7 +56,7 @@ export interface DetermineContributionType { * @type {number} */ desiredBalanceValueAfterInflation: number; -} +}; /** * Represents the details of a single compounding period in the compound interest calculation. @@ -71,7 +71,7 @@ export interface DetermineContributionType { * @property {number} balanceFromContributions - The portion of the current balance attributable to contributions. * @property {number} balanceFromInterest - The portion of the current balance attributable to accumulated interest. */ -export interface CompoundingPeriodDetailsType { +export type CompoundingPeriodDetailsType = { period: number; balance: number; contributionTotal: number; @@ -79,13 +79,13 @@ export interface CompoundingPeriodDetailsType { interestEarnedThisPeriod: number; balanceFromContributions: number; balanceFromInterest: number; -} +}; /** * Object representing compounding interest calculations. * @interface */ -export interface CompoundingInterestObjectType { +export type CompoundingInterestObjectType = { /** * Balance amount. * @type {number} @@ -127,13 +127,13 @@ export interface CompoundingInterestObjectType { * @type {CompoundingPeriodDetailsType[]} */ compoundingPeriodDetails: CompoundingPeriodDetailsType[]; -} +}; /** * Represents the aggregated data for a single year in the compound interest calculation. * @interface YearlyCompoundingDetails */ -export interface YearlyCompoundingDetails { +export type YearlyCompoundingDetails = { /** * The year number starting from 1. * @type {number} @@ -157,4 +157,595 @@ export interface YearlyCompoundingDetails { * @type {number} */ endOfYearBalance: number; +}; + +// ============================================================================ +// DYNAMIC GLIDEPATH TYPE DEFINITIONS +// ============================================================================ + +/** + * Defines when monthly contributions are added within each month. + * - 'start': Add contribution first, then apply interest + * - 'end': Apply interest first, then add contribution + */ +export type ContributionTiming = 'start' | 'end'; + +/** + * Identifies the type of glidepath strategy being used. + */ +export type GlidepathMode = + | 'fixed-return' + | 'allocation-based' + | 'custom-waypoints' + | 'stepped-return'; + +// ============================================================================ +// GLIDEPATH CONFIGURATION TYPES +// ============================================================================ + +/** + * Configuration for fixed return glidepath that linearly interpolates + * between start and end return rates based on age. + * + * Use case: Target-date funds that reduce returns as retirement approaches + * + * @example + * ```typescript + * const config: FixedReturnGlidepathConfig = { + * mode: 'fixed-return', + * startReturn: 0.10, // 10% at starting age + * endReturn: 0.055 // 5.5% at ending age + * }; + * ``` + */ +export type FixedReturnGlidepathConfig = { + /** + * Discriminator for the glidepath configuration type. + */ + mode: 'fixed-return'; + + /** + * Annual return rate at the starting age (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.10 represents 10% annual return + */ + startReturn: number; + + /** + * Annual return rate at the ending age (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.055 represents 5.5% annual return + */ + endReturn: number; +}; + +/** + * Configuration for allocation-based glidepath that blends equity and bond returns + * based on linearly interpolated equity allocation by age. + * + * Use case: Age-based asset allocation strategies (e.g., "120 minus age" rule) + * + * @example + * ```typescript + * const config: AllocationBasedGlidepathConfig = { + * mode: 'allocation-based', + * startEquityWeight: 0.90, // 90% equity at start + * endEquityWeight: 0.30, // 30% equity at end + * equityReturn: 0.12, // 12% equity returns + * bondReturn: 0.04 // 4% bond returns + * }; + * ``` + */ +export type AllocationBasedGlidepathConfig = { + /** + * Discriminator for the glidepath configuration type. + */ + mode: 'allocation-based'; + + /** + * Equity allocation percentage at starting age (0.0 to 1.0). + * + * @example 0.90 represents 90% equity allocation + */ + startEquityWeight: number; + + /** + * Equity allocation percentage at ending age (0.0 to 1.0). + * + * @example 0.30 represents 30% equity allocation + */ + endEquityWeight: number; + + /** + * Expected annual return for equity portion (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.12 represents 12% annual equity return + */ + equityReturn: number; + + /** + * Expected annual return for bond portion (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.04 represents 4% annual bond return + */ + bondReturn: number; +}; + +/** + * Represents a single waypoint in a custom glidepath configuration. + * + * @example + * ```typescript + * const waypoint: GlidepathWaypoint = { + * age: 35, + * value: 0.08 // 8% return or 80% equity weight, depending on valueType + * }; + * ``` + */ +export type GlidepathWaypoint = { + /** + * Age at which this waypoint applies. + * Must be positive. + */ + age: number; + + /** + * Value at this waypoint. + * - If valueType is 'return': annual return rate (must be > -1.0) + * - If valueType is 'equityWeight': equity allocation percentage (0.0 to 1.0) + */ + value: number; +}; + +/** + * Configuration for custom waypoints glidepath that uses user-defined + * age/value pairs with linear interpolation between points. + * + * Use case: Custom investment strategies with specific target allocations or returns at key ages + * + * @example Return-based waypoints + * ```typescript + * const config: CustomWaypointsGlidepathConfig = { + * mode: 'custom-waypoints', + * valueType: 'return', + * waypoints: [ + * { age: 25, value: 0.12 }, // 12% at 25 + * { age: 40, value: 0.08 }, // 8% at 40 + * { age: 65, value: 0.05 } // 5% at 65 + * ] + * }; + * ``` + * + * @example Equity allocation waypoints + * ```typescript + * const config: CustomWaypointsGlidepathConfig = { + * mode: 'custom-waypoints', + * valueType: 'equityWeight', + * waypoints: [ + * { age: 20, value: 1.0 }, // 100% equity at 20 + * { age: 35, value: 0.8 }, // 80% equity at 35 + * { age: 50, value: 0.6 }, // 60% equity at 50 + * { age: 65, value: 0.3 } // 30% equity at 65 + * ], + * equityReturn: 0.11, + * bondReturn: 0.035 + * }; + * ``` + */ +export type CustomWaypointsGlidepathConfig = { + /** + * Discriminator for the glidepath configuration type. + */ + mode: 'custom-waypoints'; + + /** + * Array of waypoints defining the glidepath curve. + * Must contain at least one waypoint. + * Waypoints will be automatically sorted by age. + */ + waypoints: GlidepathWaypoint[]; + + /** + * Specifies what the waypoint values represent. + * - 'return': Waypoint values are annual return rates + * - 'equityWeight': Waypoint values are equity allocation percentages + */ + valueType: 'return' | 'equityWeight'; + + /** + * Expected annual return for equity portion (decimal format). + * Required when valueType is 'equityWeight', ignored when valueType is 'return'. + * Must be > -1.0 (cannot lose more than 100%). + */ + equityReturn?: number; + + /** + * Expected annual return for bond portion (decimal format). + * Required when valueType is 'equityWeight', ignored when valueType is 'return'. + * Must be > -1.0 (cannot lose more than 100%). + */ + bondReturn?: number; +}; + +/** + * Configuration for stepped return glidepath that declines by a fixed rate per year + * until reaching a terminal return, then holds that return. + * + * Use case: Money Guy Show strategy and other step-down approaches + * + * @example + * ```typescript + * const config: SteppedReturnGlidepathConfig = { + * mode: 'stepped-return', + * baseReturn: 0.10, // 10% starting return + * declineRate: 0.001, // 0.1% decline per year + * terminalReturn: 0.055, // 5.5% floor + * declineStartAge: 20, // Start declining at 20 + * terminalAge: 65 // Reach terminal at 65 + * }; + * ``` + */ +export type SteppedReturnGlidepathConfig = { + /** + * Discriminator for the glidepath configuration type. + */ + mode: 'stepped-return'; + + /** + * Starting annual return rate before decline begins (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.10 represents 10% annual return + */ + baseReturn: number; + + /** + * Annual decline rate applied each year (decimal format). + * Must be >= 0 (cannot have negative decline rates). + * + * @example 0.001 represents 0.1% decline per year + */ + declineRate: number; + + /** + * Terminal annual return rate that is held after terminalAge (decimal format). + * Must be > -1.0 (cannot lose more than 100%). + * + * @example 0.055 represents 5.5% annual return floor + */ + terminalReturn: number; + + /** + * Age at which the decline begins. + * Must be positive and less than terminalAge. + * + * @example 20 means decline starts at age 20 + */ + declineStartAge: number; + + /** + * Age at which the terminal return is reached and held. + * Must be greater than declineStartAge. + * Defaults to 65 if not specified. + * + * @example 65 means terminal return is reached at age 65 + */ + terminalAge: number; +}; + +/** + * Union type representing all possible glidepath configuration modes. + * Uses discriminated union pattern with 'mode' as the discriminator. + */ +export type DynamicGlidepathConfig = + | FixedReturnGlidepathConfig + | AllocationBasedGlidepathConfig + | CustomWaypointsGlidepathConfig + | SteppedReturnGlidepathConfig; + +// ============================================================================ +// RESULT TYPES +// ============================================================================ + +/** + * Represents the detailed state of the account at the end of a specific month. + * Used for timeline visualization and detailed analysis. + * + * @example + * ```typescript + * const entry: MonthlyTimelineEntry = { + * month: 120, + * age: 39.92, + * currentBalance: 125750.50, + * cumulativeContributions: 120000, + * cumulativeInterest: 5750.50, + * monthlyInterestEarned: 825.33, + * currentAnnualReturn: 0.085, + * currentMonthlyReturn: 0.006825, + * currentEquityWeight: 0.75 // Only present for allocation-based modes + * }; + * ``` + */ +export type MonthlyTimelineEntry = { + /** + * Month number in the simulation (1-based). + * Month 1 is the first month, month 12 is the end of the first year, etc. + */ + month: number; + + /** + * User's age at the end of this month. + * Increments by 1/12 each month from the starting age. + */ + age: number; + + /** + * Total account balance at the end of this month. + * Includes initial balance, all contributions, and compound interest. + */ + currentBalance: number; + + /** + * Cumulative total of all contributions made up to and including this month. + */ + cumulativeContributions: number; + + /** + * Cumulative total of all interest earned up to and including this month. + */ + cumulativeInterest: number; + + /** + * Amount of interest earned during this specific month. + */ + monthlyInterestEarned: number; + + /** + * Annual return rate that was used for this month's calculation. + * This is the rate determined by the glidepath strategy for the user's age this month. + */ + currentAnnualReturn: number; + + /** + * Monthly return rate that was applied during this month. + * Converted from annual rate using: (1 + annualRate)^(1/12) - 1 + */ + currentMonthlyReturn: number; + + /** + * Current equity allocation weight for this month. + * Only present for allocation-based and custom waypoints with equityWeight valueType. + * Undefined for fixed-return glidepaths. + */ + currentEquityWeight?: number; +}; + +/** + * Comprehensive result object returned by the dynamic glidepath method. + * Contains final calculations, metadata, timeline data, and summary statistics. + * + * @example + * ```typescript + * const result: DynamicGlidepathResult = calculator.getCompoundInterestWithDynamicGlidepath( + * 25000, 1000, 30, 65, config + * ); + * + * console.log(`Final balance: $${result.finalBalance.toLocaleString()}`); + * console.log(`Total months: ${result.totalMonths}`); + * console.log(`Effective annual return: ${(result.effectiveAnnualReturn * 100).toFixed(2)}%`); + * ``` + */ +export type DynamicGlidepathResult = { + // Final calculation results + + /** + * Total account balance at the end of the simulation period. + * Rounded to the nearest cent for precision. + */ + finalBalance: number; + + /** + * Sum of all monthly contributions made during the simulation period. + */ + totalContributions: number; + + /** + * Total interest earned through compound growth during the simulation period. + */ + totalInterestEarned: number; + + // Simulation metadata + + /** + * Total number of months simulated. + * Calculated as Math.ceil((endAge - startAge) * 12) + */ + totalMonths: number; + + /** + * Starting age from the input parameters. + */ + startAge: number; + + /** + * Ending age from the input parameters. + */ + endAge: number; + + /** + * The glidepath mode that was used for this calculation. + */ + glidepathMode: GlidepathMode; + + // Timeline data for analysis and visualization + + /** + * Detailed month-by-month timeline data. + * Each entry represents the state at the end of that month. + * Useful for creating charts and detailed analysis. + */ + monthlyTimeline: MonthlyTimelineEntry[]; + + // Summary statistics + + /** + * Effective compound annual growth rate over the entire simulation period. + * Calculated as (finalBalance / initialBalance)^(1/years) - 1 + */ + effectiveAnnualReturn: number; + + /** + * Average monthly return rate across all months in the simulation. + * Simple arithmetic mean of all monthly return rates used. + */ + averageMonthlyReturn: number; +}; + +// ============================================================================ +// ERROR TYPES +// ============================================================================ + +/** + * Base error type for retirement calculator-specific errors. + */ +export type RetirementCalculatorError = Error & { + name: 'RetirementCalculatorError'; +}; + +/** + * Error thrown when age parameters are invalid. + */ +export type InvalidAgeRangeError = RetirementCalculatorError & { + name: 'InvalidAgeRangeError'; + message: 'startAge must be less than endAge and both must be positive'; +}; + +/** + * Error thrown when financial parameters are invalid. + */ +export type InvalidFinancialParameterError = RetirementCalculatorError & { + name: 'InvalidFinancialParameterError'; + message: 'initialBalance and monthlyContribution must be non-negative'; +}; + +/** + * Error thrown when return rates are invalid. + */ +export type InvalidReturnRateError = RetirementCalculatorError & { + name: 'InvalidReturnRateError'; + message: 'Return rates must be greater than -1.0 (cannot lose more than 100%)'; +}; + +/** + * Error thrown when allocation weights are invalid. + */ +export type InvalidAllocationError = RetirementCalculatorError & { + name: 'InvalidAllocationError'; + message: 'Equity weights must be between 0.0 and 1.0 inclusive'; +}; + +/** + * Error thrown when glidepath configuration is invalid. + */ +export type InvalidGlidepathConfigError = RetirementCalculatorError & { + name: 'InvalidGlidepathConfigError'; + message: string; // Variable message based on specific validation failure +}; + +/** + * Error thrown when waypoints configuration is invalid. + */ +export type InvalidWaypointsError = RetirementCalculatorError & { + name: 'InvalidWaypointsError'; + message: 'Waypoints array must contain at least one valid waypoint'; +}; + +/** + * Error thrown when calculation results in numeric overflow. + */ +export type CalculationOverflowError = RetirementCalculatorError & { + name: 'CalculationOverflowError'; + message: 'Calculation resulted in numeric overflow'; +}; + +/** + * Error thrown when numerical precision loss is detected. + */ +export type NumericalPrecisionError = RetirementCalculatorError & { + name: 'NumericalPrecisionError'; + message: 'Numerical precision loss detected in calculation'; +}; + +// ============================================================================ +// TYPE GUARDS AND UTILITIES +// ============================================================================ + +/** + * Type guard to check if a configuration is a fixed return configuration. + */ +export function isFixedReturnConfig( + config: DynamicGlidepathConfig +): config is FixedReturnGlidepathConfig { + return config.mode === 'fixed-return'; +} + +/** + * Type guard to check if a configuration is an allocation-based configuration. + */ +export function isAllocationBasedConfig( + config: DynamicGlidepathConfig +): config is AllocationBasedGlidepathConfig { + return config.mode === 'allocation-based'; +} + +/** + * Type guard to check if a configuration is a custom waypoints configuration. + */ +export function isCustomWaypointsConfig( + config: DynamicGlidepathConfig +): config is CustomWaypointsGlidepathConfig { + return config.mode === 'custom-waypoints'; } + +/** + * Type guard to check if a configuration is a stepped return configuration. + */ +export function isSteppedReturnConfig( + config: DynamicGlidepathConfig +): config is SteppedReturnGlidepathConfig { + return config.mode === 'stepped-return'; +} + +// ============================================================================ +// UTILITY TYPES +// ============================================================================ + +/** + * Utility type to extract the configuration type for a specific glidepath mode. + */ +export type ConfigForMode = T extends 'fixed-return' + ? FixedReturnGlidepathConfig + : T extends 'allocation-based' + ? AllocationBasedGlidepathConfig + : T extends 'custom-waypoints' + ? CustomWaypointsGlidepathConfig + : T extends 'stepped-return' + ? SteppedReturnGlidepathConfig + : never; + +/** + * Utility type to make certain properties of a type required. + */ +export type RequiredFields = T & Required>; + +/** + * Utility type for custom waypoints configuration with required equity/bond returns. + */ +export type CustomWaypointsWithReturns = RequiredFields< + CustomWaypointsGlidepathConfig, + 'equityReturn' | 'bondReturn' +>; diff --git a/tests/RetirementCalculator-glidepath.spec.ts b/tests/RetirementCalculator-glidepath.spec.ts new file mode 100644 index 0000000..3f69ec3 --- /dev/null +++ b/tests/RetirementCalculator-glidepath.spec.ts @@ -0,0 +1,1340 @@ +/** + * Comprehensive tests for Dynamic Glidepath functionality in RetirementCalculator + * Tests all four glidepath modes, edge cases, error conditions, and presets + */ + +import RetirementCalculator from '../src/RetirementCalculator'; +import { GLIDEPATH_PRESETS } from '../src/constants/retirementCalculatorConstants'; +import type { + FixedReturnGlidepathConfig, + AllocationBasedGlidepathConfig, + CustomWaypointsGlidepathConfig, + SteppedReturnGlidepathConfig, +} from '../src/types/retirementCalculatorTypes'; +import { isSteppedReturnConfig } from '../src/types/retirementCalculatorTypes'; + +describe('RetirementCalculator - Dynamic Glidepath Functionality', () => { + let calculator: RetirementCalculator; + + beforeEach(() => { + calculator = new RetirementCalculator(); + }); + + // ============================================================================ + // INPUT VALIDATION TESTS + // ============================================================================ + + describe('Input Validation', () => { + const validConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, + endReturn: 0.055, + }; + + test('should throw error for negative initial balance', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + -1000, + 500, + 25, + 65, + validConfig + ); + }).toThrow('Initial balance must be non-negative'); + }); + + test('should throw error for negative contribution amount', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + -500, + 25, + 65, + validConfig + ); + }).toThrow('Contribution amount must be non-negative'); + }); + + test('should throw error for non-positive start age', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 0, + 65, + validConfig + ); + }).toThrow('Ages must be positive'); + }); + + test('should throw error for non-positive end age', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 25, + 0, + validConfig + ); + }).toThrow('Ages must be positive'); + }); + + test('should throw error when start age >= end age', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 65, + 25, + validConfig + ); + }).toThrow('Start age must be less than end age'); + }); + + test('should throw error for non-positive contribution frequency', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 25, + 65, + validConfig, + 0 + ); + }).toThrow('Frequencies must be positive'); + }); + + test('should throw error for non-positive compounding frequency', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 25, + 65, + validConfig, + 12, + 0 + ); + }).toThrow('Frequencies must be positive'); + }); + + test('should accept valid inputs without throwing', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 500, + 25, + 65, + validConfig + ); + }).not.toThrow(); + }); + + test('should accept zero initial balance', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 0, + 500, + 25, + 65, + validConfig + ); + }).not.toThrow(); + }); + + test('should accept zero contribution amount', () => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 0, + 25, + 65, + validConfig + ); + }).not.toThrow(); + }); + }); + + // ============================================================================ + // FIXED RETURN GLIDEPATH TESTS + // ============================================================================ + + describe('Fixed Return Glidepath', () => { + const fixedConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, // 10% + endReturn: 0.05, // 5% + }; + + test('should calculate fixed return glidepath correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + fixedConfig // 10 years + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('fixed-return'); + expect(result.startAge).toBe(25); + expect(result.endAge).toBe(35); + expect(result.totalMonths).toBe(120); + expect(result.finalBalance).toBeGreaterThan(0); + expect(result.totalContributions).toBe(120000); // 10 years * 12 months * 1000 + }); + + test('should have declining returns over time', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + fixedConfig + ); + + const firstMonth = result.monthlyTimeline[0]; + const lastMonth = + result.monthlyTimeline[result.monthlyTimeline.length - 1]; + + expect(firstMonth.currentAnnualReturn).toBeCloseTo(0.1, 3); + expect(lastMonth.currentAnnualReturn).toBeCloseTo(0.05, 3); + expect(firstMonth.currentAnnualReturn).toBeGreaterThan( + lastMonth.currentAnnualReturn + ); + }); + + test('should have proper linear interpolation', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 20, + 40, + fixedConfig // 20 years + ); + + // Find middle age entry (age 30) + const middleEntry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 30) < 0.1 + ); + + expect(middleEntry).toBeDefined(); + // At middle age, should be halfway between start and end return + expect(middleEntry?.currentAnnualReturn).toBeCloseTo(0.075, 3); // (0.1 + 0.05) / 2 + }); + + test('should handle same start and end return', () => { + const flatConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + flatConfig + ); + + const firstMonth = result.monthlyTimeline[0]; + const lastMonth = + result.monthlyTimeline[result.monthlyTimeline.length - 1]; + + expect(firstMonth.currentAnnualReturn).toBeCloseTo(0.08, 6); + expect(lastMonth.currentAnnualReturn).toBeCloseTo(0.08, 6); + }); + }); + + // ============================================================================ + // STEPPED RETURN GLIDEPATH TESTS + // ============================================================================ + + describe('Stepped Return Glidepath', () => { + const steppedConfig: SteppedReturnGlidepathConfig = { + mode: 'stepped-return', + baseReturn: 0.1, + declineRate: 0.001, // 0.1% per year + terminalReturn: 0.055, + declineStartAge: 20, + terminalAge: 65, + }; + + test('should calculate stepped return glidepath correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 22, + 40, + steppedConfig + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('stepped-return'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should decline by specified rate per year', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 20, + 30, + steppedConfig + ); + + // At age 20, should be at base return + const age20Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 20) < 0.1 + ); + expect(age20Entry?.currentAnnualReturn).toBeCloseTo(0.1, 6); + + // At age 25, should be 0.1 - (5 * 0.001) = 0.095 + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + expect(age25Entry?.currentAnnualReturn).toBeCloseTo(0.095, 3); + }); + + test('should hold base return before decline start age', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 18, + 22, + steppedConfig + ); + + const age18Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 18) < 0.1 + ); + const age19Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 19) < 0.1 + ); + + expect(age18Entry?.currentAnnualReturn).toBeCloseTo(0.1, 6); + expect(age19Entry?.currentAnnualReturn).toBeCloseTo(0.1, 6); + }); + + test('should hold terminal return after terminal age', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 60, + 70, + steppedConfig + ); + + const age65Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 65) < 0.1 + ); + const age70Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 70) < 0.1 + ); + + expect(age65Entry?.currentAnnualReturn).toBeCloseTo(0.055, 3); + expect(age70Entry?.currentAnnualReturn).toBeCloseTo(0.055, 3); + }); + + test('should not go below terminal return', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 20, + 70, + steppedConfig + ); + + // All entries should be >= terminal return + result.monthlyTimeline.forEach((entry) => { + expect(entry.currentAnnualReturn).toBeGreaterThanOrEqual( + 0.055 - 0.0001 + ); // Small tolerance + }); + }); + }); + + // ============================================================================ + // ALLOCATION-BASED GLIDEPATH TESTS + // ============================================================================ + + describe('Allocation-Based Glidepath', () => { + const allocationConfig: AllocationBasedGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.9, // 90% + endEquityWeight: 0.3, // 30% + equityReturn: 0.12, // 12% + bondReturn: 0.04, // 4% + }; + + test('should calculate allocation-based glidepath correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + allocationConfig + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('allocation-based'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should have decreasing equity allocation over time', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + allocationConfig + ); + + const firstMonth = result.monthlyTimeline[0]; + const lastMonth = + result.monthlyTimeline[result.monthlyTimeline.length - 1]; + + expect(firstMonth.currentEquityWeight).toBeCloseTo(0.9, 2); + expect(lastMonth.currentEquityWeight).toBeCloseTo(0.3, 2); + expect(firstMonth.currentEquityWeight ?? 0).toBeGreaterThan( + lastMonth.currentEquityWeight ?? 0 + ); + }); + + test('should blend returns based on allocation', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + allocationConfig + ); + + const firstMonth = result.monthlyTimeline[0]; + const lastMonth = + result.monthlyTimeline[result.monthlyTimeline.length - 1]; + + // First month: 90% equity, 10% bonds = 0.9 * 0.12 + 0.1 * 0.04 = 0.112 + expect(firstMonth.currentAnnualReturn).toBeCloseTo(0.112, 3); + + // Last month: 30% equity, 70% bonds = 0.3 * 0.12 + 0.7 * 0.04 = 0.064 + expect(lastMonth.currentAnnualReturn).toBeCloseTo(0.064, 3); + }); + + test('should have proper linear allocation interpolation', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 20, + 40, + allocationConfig // 20 years + ); + + // Find middle age entry (age 30) + const middleEntry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 30) < 0.1 + ); + + expect(middleEntry).toBeDefined(); + // At middle age, should be halfway between start and end equity weight + expect(middleEntry?.currentEquityWeight).toBeCloseTo(0.6, 2); // (0.9 + 0.3) / 2 + }); + }); + + // ============================================================================ + // CUSTOM WAYPOINTS GLIDEPATH TESTS + // ============================================================================ + + describe('Custom Waypoints Glidepath', () => { + const customReturnConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [ + { age: 25, value: 0.12 }, + { age: 35, value: 0.1 }, + { age: 45, value: 0.08 }, + { age: 55, value: 0.06 }, + { age: 65, value: 0.04 }, + ], + }; + + const customEquityConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [ + { age: 25, value: 1.0 }, + { age: 35, value: 0.8 }, + { age: 45, value: 0.6 }, + { age: 55, value: 0.4 }, + { age: 65, value: 0.2 }, + ], + equityReturn: 0.11, + bondReturn: 0.03, + }; + + test('should calculate custom return waypoints correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customReturnConfig + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('custom-waypoints'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should hit exact return values at waypoint ages', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customReturnConfig + ); + + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + const age35Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 35) < 0.1 + ); + const age45Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 45) < 0.1 + ); + + expect(age25Entry?.currentAnnualReturn).toBeCloseTo(0.12, 3); + expect(age35Entry?.currentAnnualReturn).toBeCloseTo(0.1, 3); + expect(age45Entry?.currentAnnualReturn).toBeCloseTo(0.08, 3); + }); + + test('should interpolate between waypoints', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customReturnConfig + ); + + // Age 30 should be halfway between age 25 (0.12) and age 35 (0.10) = 0.11 + const age30Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 30) < 0.1 + ); + expect(age30Entry?.currentAnnualReturn).toBeCloseTo(0.11, 3); + }); + + test('should calculate custom equity waypoints correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customEquityConfig + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('custom-waypoints'); + }); + + test('should hit exact equity weights at waypoint ages', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customEquityConfig + ); + + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + const age35Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 35) < 0.1 + ); + + expect(age25Entry?.currentEquityWeight).toBeCloseTo(1.0, 2); + expect(age35Entry?.currentEquityWeight).toBeCloseTo(0.8, 2); + }); + + test('should blend equity returns correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + customEquityConfig + ); + + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + + // Age 25: 100% equity = 1.0 * 0.11 + 0.0 * 0.03 = 0.11 + expect(age25Entry?.currentAnnualReturn).toBeCloseTo(0.11, 3); + }); + + test('should handle before first waypoint', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 20, + 30, + customReturnConfig + ); + + const age20Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 20) < 0.1 + ); + // Should use first waypoint value + expect(age20Entry?.currentAnnualReturn).toBeCloseTo(0.12, 3); + }); + + test('should handle after last waypoint', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 60, + 70, + customReturnConfig + ); + + const age70Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 70) < 0.1 + ); + // Should use last waypoint value + expect(age70Entry?.currentAnnualReturn).toBeCloseTo(0.04, 3); + }); + + test('should throw error for empty waypoints', () => { + const emptyConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [], + }; + + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + emptyConfig + ); + }).toThrow( + 'Custom waypoints configuration must have at least one waypoint' + ); + }); + + test('should handle single waypoint', () => { + const singleConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [{ age: 40, value: 0.08 }], + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + singleConfig + ); + + // All ages should use the single waypoint value + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + const age65Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 65) < 0.1 + ); + + expect(age25Entry?.currentAnnualReturn).toBeCloseTo(0.08, 6); + expect(age65Entry?.currentAnnualReturn).toBeCloseTo(0.08, 6); + }); + + test('should handle single waypoint with equity weight', () => { + const singleEquityConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [{ age: 40, value: 0.7 }], + equityReturn: 0.12, + bondReturn: 0.04, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 65, + singleEquityConfig + ); + + // Should calculate: 0.7 * 0.12 + 0.3 * 0.04 = 0.096 + const age25Entry = result.monthlyTimeline.find( + (entry) => Math.abs(entry.age - 25) < 0.1 + ); + expect(age25Entry?.currentAnnualReturn).toBeCloseTo(0.096, 3); + expect(age25Entry?.currentEquityWeight).toBeCloseTo(0.7, 6); + }); + }); + + // ============================================================================ + // GLIDEPATH PRESETS TESTS + // ============================================================================ + + describe('Glidepath Presets', () => { + test('should work with Money Guy Show preset', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + GLIDEPATH_PRESETS.MONEY_GUY_SHOW + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('stepped-return'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should work with Bogleheads 100 minus age preset', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + GLIDEPATH_PRESETS.BOGLEHEADS_100_MINUS_AGE + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('custom-waypoints'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should work with Bogleheads 110 minus age preset', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + GLIDEPATH_PRESETS.BOGLEHEADS_110_MINUS_AGE + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('custom-waypoints'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + + test('should work with Bogleheads 120 minus age preset', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + GLIDEPATH_PRESETS.BOGLEHEADS_120_MINUS_AGE + ); + + expect(result).toBeDefined(); + expect(result.glidepathMode).toBe('custom-waypoints'); + expect(result.finalBalance).toBeGreaterThan(0); + }); + }); + + // ============================================================================ + // CONTRIBUTION TIMING TESTS + // ============================================================================ + + describe('Contribution Timing', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + test('should handle start-of-period contributions', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 0, + 1000, + 25, + 26, + config, + 12, + 12, + 'start' + ); + + expect(result.finalBalance).toBeGreaterThan(12000); // Should be > contributions due to interest + }); + + test('should handle end-of-period contributions', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 0, + 1000, + 25, + 26, + config, + 12, + 12, + 'end' + ); + + expect(result.finalBalance).toBeGreaterThan(12000); // Should be > contributions but less than start timing + }); + + test('should produce different results for different contribution timing', () => { + const startResult = calculator.getCompoundInterestWithGlidepath( + 0, + 1000, + 25, + 26, + config, + 12, + 12, + 'start' + ); + + const endResult = calculator.getCompoundInterestWithGlidepath( + 0, + 1000, + 25, + 26, + config, + 12, + 12, + 'end' + ); + + expect(startResult.finalBalance).toBeGreaterThan(endResult.finalBalance); + }); + }); + + // ============================================================================ + // EDGE CASES AND ROBUSTNESS TESTS + // ============================================================================ + + describe('Edge Cases and Robustness', () => { + test('should handle very short time periods', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 25.1, + config // Just over 1 month + ); + + expect(result.totalMonths).toBe(2); // Math.ceil(0.1 * 12) + expect(result.monthlyTimeline).toHaveLength(2); + }); + + test('should handle zero initial balance', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 0, + 1000, + 25, + 26, + config + ); + + expect(result.finalBalance).toBeGreaterThan(0); + expect(result.totalContributions).toBeGreaterThan(0); + }); + + test('should handle zero contributions', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 0, + 25, + 26, + config + ); + + expect(result.finalBalance).toBeGreaterThan(10000); // Should grow due to interest + expect(result.totalContributions).toBe(0); + }); + + test('should handle negative returns in custom waypoints', () => { + const negativeConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [ + { age: 25, value: -0.1 }, // -10% return + { age: 35, value: 0.05 }, + ], + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + negativeConfig + ); + + expect(result).toBeDefined(); + expect(result.finalBalance).toBeGreaterThan(0); // Should still be positive due to contributions + }); + + test('should handle fractional ages correctly', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, + endReturn: 0.05, + }; + + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25.5, + 35.5, + config + ); + + expect(result.startAge).toBe(25.5); + expect(result.endAge).toBe(35.5); + expect(result.totalMonths).toBe(120); // Math.ceil(10 * 12) + }); + }); + + // ============================================================================ + // RESULT STRUCTURE TESTS + // ============================================================================ + + describe('Result Structure and Data Integrity', () => { + const config: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.08, + }; + + test('should return properly structured result object', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 26, + config + ); + + expect(result).toMatchObject({ + finalBalance: expect.any(Number), + totalContributions: expect.any(Number), + totalInterestEarned: expect.any(Number), + totalMonths: expect.any(Number), + startAge: expect.any(Number), + endAge: expect.any(Number), + glidepathMode: expect.any(String), + monthlyTimeline: expect.any(Array), + effectiveAnnualReturn: expect.any(Number), + averageMonthlyReturn: expect.any(Number), + }); + }); + + test('should have consistent timeline data', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 26, + config + ); + + expect(result.monthlyTimeline).toHaveLength(result.totalMonths); + + result.monthlyTimeline.forEach((entry, index) => { + expect(entry).toMatchObject({ + month: index + 1, + age: expect.any(Number), + currentBalance: expect.any(Number), + cumulativeContributions: expect.any(Number), + cumulativeInterest: expect.any(Number), + monthlyInterestEarned: expect.any(Number), + currentAnnualReturn: expect.any(Number), + currentMonthlyReturn: expect.any(Number), + }); + + // Age should increase each month + const expectedAge = result.startAge + index / 12; + expect(entry.age).toBeCloseTo(expectedAge, 5); + }); + }); + + test('should have accurate totals', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 26, + config + ); + + const lastEntry = + result.monthlyTimeline[result.monthlyTimeline.length - 1]; + + expect(result.finalBalance).toBeCloseTo(lastEntry.currentBalance, 2); + expect(result.totalContributions).toBeCloseTo( + lastEntry.cumulativeContributions, + 2 + ); + expect(result.totalInterestEarned).toBeCloseTo( + lastEntry.cumulativeInterest, + 2 + ); + }); + + test('should round final balance to nearest cent', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 26, + config + ); + + // Final balance should be rounded to 2 decimal places + expect(result.finalBalance).toBe( + Math.round(result.finalBalance * 100) / 100 + ); + }); + + test('should calculate effective annual return correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 0, + 25, + 26, + config // No contributions to test return calculation + ); + + const expectedReturn = Math.pow(result.finalBalance / 10000, 1) - 1; + expect(result.effectiveAnnualReturn).toBeCloseTo(expectedReturn, 5); + }); + + test('should calculate average monthly return correctly', () => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 26, + config + ); + + const calculatedAverage = + result.monthlyTimeline.reduce( + (sum, entry) => sum + entry.currentMonthlyReturn, + 0 + ) / result.monthlyTimeline.length; + + expect(result.averageMonthlyReturn).toBeCloseTo(calculatedAverage, 10); + }); + }); + + // ============================================================================ + // EXHAUSTIVE SWITCH CASE TEST + // ============================================================================ + + describe('Exhaustive Mode Coverage', () => { + test('should handle all four glidepath modes without error', () => { + const fixedConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, + endReturn: 0.05, + }; + + const steppedConfig: SteppedReturnGlidepathConfig = { + mode: 'stepped-return', + baseReturn: 0.1, + declineRate: 0.001, + terminalReturn: 0.055, + declineStartAge: 20, + terminalAge: 65, + }; + + const allocationConfig: AllocationBasedGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.9, + endEquityWeight: 0.3, + equityReturn: 0.12, + bondReturn: 0.04, + }; + + const customConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [{ age: 25, value: 0.08 }], + }; + + const configs = [ + fixedConfig, + steppedConfig, + allocationConfig, + customConfig, + ]; + + configs.forEach((config) => { + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + config + ); + }).not.toThrow(); + }); + }); + }); + + // ============================================================================ + // TYPE GUARD TESTS + // ============================================================================ + + describe('Type Guards', () => { + test('isSteppedReturnConfig should correctly identify stepped return configs', () => { + const steppedConfig: SteppedReturnGlidepathConfig = { + mode: 'stepped-return', + baseReturn: 0.1, + declineRate: 0.001, + terminalReturn: 0.055, + declineStartAge: 20, + terminalAge: 65, + }; + + const fixedConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, + endReturn: 0.05, + }; + + expect(isSteppedReturnConfig(steppedConfig)).toBe(true); + expect(isSteppedReturnConfig(fixedConfig)).toBe(false); + }); + }); + + // ============================================================================ + // EDGE CASE TESTS FOR FULL COVERAGE + // ============================================================================ + + describe('Edge Cases for Complete Coverage', () => { + test('should handle empty waypoints array error', () => { + // This tests the private interpolateWaypoints method through custom waypoints + const invalidConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [], + }; + + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + invalidConfig + ); + }).toThrow( + 'Custom waypoints configuration must have at least one waypoint' + ); + }); + + test('should handle edge cases that trigger interpolation fallback paths', () => { + // Test a very specific scenario that might trigger different code paths + // in the waypoint interpolation logic + const edgeCaseConfigs = [ + // Config with waypoints that have identical ages (edge case) + { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [ + { age: 30, value: 0.08 }, + { age: 30, value: 0.07 }, // Same age, different value + ], + }, + // Config with waypoints in reverse order (should be sorted) + { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [ + { age: 40, value: 0.06 }, + { age: 20, value: 0.1 }, // Out of order + { age: 30, value: 0.08 }, + ], + }, + ]; + + edgeCaseConfigs.forEach((config, index) => { + // Test that function either succeeds or throws a proper error + expect(() => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + config + ); + // If we get here, verify it's a valid result + expect(result).toBeDefined(); + expect(result.finalBalance).toBeGreaterThan(0); + }).not.toThrow('unexpected error type'); + }); + }); + + test('should handle invalid glidepath mode', () => { + // Create an invalid config to trigger the default case + const invalidConfig = { + mode: 'invalid-mode', + // other properties... + } as any; + + expect(() => { + calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25, + 35, + invalidConfig + ); + }).toThrow('Unsupported glidepath mode'); + }); + + test('should handle waypoint interpolation edge cases', () => { + // Test multiple edge cases with waypoint interpolation + const testCases = [ + // Case 1: Age exactly at waypoint boundary + { + config: { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [ + { age: 20, value: 0.1 }, + { age: 30, value: 0.08 }, + ], + }, + startAge: 30, + endAge: 31, + expectedRate: 0.08, + }, + // Case 2: Single waypoint + { + config: { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [{ age: 25, value: 0.075 }], + }, + startAge: 20, + endAge: 30, + expectedRate: 0.075, + }, + // Case 3: Age outside waypoint range (before) + { + config: { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [ + { age: 30, value: 0.08 }, + { age: 40, value: 0.06 }, + ], + }, + startAge: 25, // Before first waypoint + endAge: 26, + expectedRate: 0.08, // Should use first waypoint + }, + // Case 4: Age outside waypoint range (after) + { + config: { + mode: 'custom-waypoints' as const, + valueType: 'return' as const, + waypoints: [ + { age: 20, value: 0.1 }, + { age: 30, value: 0.08 }, + ], + }, + startAge: 35, // After last waypoint + endAge: 36, + expectedRate: 0.08, // Should use last waypoint + }, + ]; + + testCases.forEach(({ config, startAge, endAge, expectedRate }, index) => { + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + startAge, + endAge, + config + ); + + expect(result).toBeDefined(); + expect(result.finalBalance).toBeGreaterThan(0); + + const firstEntry = result.monthlyTimeline[0]; + expect(firstEntry.currentAnnualReturn).toBeCloseTo(expectedRate, 3); + }); + }); + + test('should handle zero interest rate in traditional calculations', () => { + const result = calculator.getCompoundInterestWithAdditionalContributions( + 10000, // initial balance + 500, // monthly contribution + 5, // years + 0, // 0% interest rate + 12, // monthly contributions + 12 // monthly compounding + ); + + expect(result).toBeDefined(); + expect(result.totalInterestEarned).toBe(0); // No interest with 0% rate + expect(result.totalContributions).toBe(500 * 12 * 5); // 30,000 + expect(result.balance).toBe(10000 + 30000); // Initial + contributions only + }); + + test('should handle floating-point precision in waypoint interpolation', () => { + // Test with floating-point ages that might cause precision issues + const precisionConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [ + { age: 25.000001, value: 0.09 }, + { age: 35.000002, value: 0.07 }, + { age: 45.000003, value: 0.05 }, + ], + }; + + // Test with age values that involve decimal precision + const result = calculator.getCompoundInterestWithGlidepath( + 10000, + 1000, + 25.0000015, // Between first and second waypoint, testing precision + 25.1, + precisionConfig + ); + + expect(result).toBeDefined(); + expect(result.finalBalance).toBeGreaterThan(0); + expect(result.monthlyTimeline[0].currentAnnualReturn).toBeGreaterThan( + 0.05 + ); + }); + }); +}); diff --git a/tests/RetirementCalculator.spec.ts b/tests/RetirementCalculator-traditional.spec.ts similarity index 96% rename from tests/RetirementCalculator.spec.ts rename to tests/RetirementCalculator-traditional.spec.ts index aca069c..f04c8c1 100644 --- a/tests/RetirementCalculator.spec.ts +++ b/tests/RetirementCalculator-traditional.spec.ts @@ -154,6 +154,32 @@ describe('RetirementCalculator', (): void => { -164069.65997503104 ); }); + + it('should handle zero interest rate correctly', (): void => { + const startingBalance: number = 10000; + const desiredBalance: number = 20000; + const years: number = 10; + const interestRate: number = 0; // Zero interest rate + const contributionFrequency: number = 12; // Monthly + const compoundingFrequency: number = 12; // Monthly + + const result: DetermineContributionType = + calculator.getContributionNeededForDesiredBalance( + startingBalance, + desiredBalance, + years, + interestRate, + contributionFrequency, + compoundingFrequency + ); + + expect(result.contributionNeededPerPeriod).toBeGreaterThan(0); + // With zero interest, we need to contribute the difference divided by periods + expect(result.contributionNeededPerPeriod).toBeCloseTo( + (desiredBalance - startingBalance) / (years * contributionFrequency), + 2 + ); + }); }); describe('getCompoundInterestWithAdditionalContributions', (): void => { diff --git a/tests/foundation-integration-test.spec.ts b/tests/foundation-integration-test.spec.ts new file mode 100644 index 0000000..5d4494a --- /dev/null +++ b/tests/foundation-integration-test.spec.ts @@ -0,0 +1,925 @@ +/** + * @fileoverview Comprehensive integration test for Dynamic Glidepath foundation layer components. + * + * This test verifies that all foundation components integrate correctly: + * - Type system extensions + * - Constants and validation constants + * - Error handling infrastructure + * - Cross-component compatibility + * + * @version 1.0.0 + */ + +/* eslint-disable jest/no-conditional-expect */ + +// ============================================================================ +// FOUNDATION COMPONENT IMPORTS +// ============================================================================ + +// Type system imports +import type { + // Core existing types + ContributionFrequencyType, + CompoundingInterestObjectType, + YearlyCompoundingDetails, + + // New dynamic glidepath types + FixedReturnGlidepathConfig, + AllocationBasedGlidepathConfig, + CustomWaypointsGlidepathConfig, + DynamicGlidepathConfig, + MonthlyTimelineEntry, + + // Utility types + ConfigForMode, +} from '../src/types/retirementCalculatorTypes'; + +// Type guard function imports +import { + isFixedReturnConfig, + isAllocationBasedConfig, + isCustomWaypointsConfig, +} from '../src/types/retirementCalculatorTypes'; + +// Constants imports +import { + CONTRIBUTION_FREQUENCY, + GLIDEPATH_DEFAULTS, + GLIDEPATH_VALIDATION, + GLIDEPATH_MATH, + GLIDEPATH_TEMPLATES, + GLIDEPATH_ERROR_MESSAGES, +} from '../src/constants/retirementCalculatorConstants'; + +// Error handling imports +import { + // Error codes + GLIDEPATH_ERROR_CODES, + + // Error classes + DynamicGlidepathError, + DynamicGlidepathValidationError, + + // Factory functions + createAgeError, + createFinancialError, + createReturnRateError, + createAllocationError, + createConfigurationError, + createWaypointError, + createWarning, + createCalculationError, + + // Type guards + isDynamicGlidepathError, + isValidationError, + isConfigurationError, + isCalculationError, + isBlockingError, + isWarning, + + // Utilities + groupBySeverity, + groupByCategory, + getSummary, + formatForConsole, + formatForAPI, + ValidationResultBuilder, +} from '../src/errors/DynamicGlidepathErrors'; + +// Main calculator for integration testing +import RetirementCalculator from '../src/RetirementCalculator'; + +// ============================================================================ +// INTEGRATION TEST SUITE +// ============================================================================ + +describe('Dynamic Glidepath Foundation Layer Integration', () => { + let calculator: RetirementCalculator; + + beforeEach(() => { + calculator = new RetirementCalculator(); + }); + + // ======================================================================== + // TYPE SYSTEM INTEGRATION TESTS + // ======================================================================== + + describe('Type System Integration', () => { + it('should properly import and use all existing types', () => { + // Test existing types still work + const contributionFreq: ContributionFrequencyType = + CONTRIBUTION_FREQUENCY; + expect(contributionFreq.YEARLY).toBe(1); + expect(contributionFreq.MONTHLY).toBe(12); + expect(contributionFreq.WEEKLY).toBe(52); + + // Test that these types are properly structured + expect(typeof contributionFreq.YEARLY).toBe('number'); + expect(typeof contributionFreq.MONTHLY).toBe('number'); + expect(typeof contributionFreq.WEEKLY).toBe('number'); + }); + + it('should properly define and use new glidepath configuration types', () => { + // Test FixedReturnGlidepathConfig + const fixedConfig: FixedReturnGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.1, + endReturn: 0.055, + }; + expect(fixedConfig.mode).toBe('fixed-return'); + expect(fixedConfig.startReturn).toBe(0.1); + expect(fixedConfig.endReturn).toBe(0.055); + + // Test AllocationBasedGlidepathConfig + const allocationConfig: AllocationBasedGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.9, + endEquityWeight: 0.3, + equityReturn: 0.12, + bondReturn: 0.04, + }; + expect(allocationConfig.mode).toBe('allocation-based'); + expect(allocationConfig.startEquityWeight).toBe(0.9); + expect(allocationConfig.endEquityWeight).toBe(0.3); + + // Test CustomWaypointsGlidepathConfig + const waypointsConfig: CustomWaypointsGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [ + { age: 25, value: 0.12 }, + { age: 65, value: 0.05 }, + ], + }; + expect(waypointsConfig.mode).toBe('custom-waypoints'); + expect(waypointsConfig.valueType).toBe('return'); + expect(waypointsConfig.waypoints).toHaveLength(2); + }); + + it('should properly work with union type DynamicGlidepathConfig', () => { + const configs: DynamicGlidepathConfig[] = [ + { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.06, + }, + { + mode: 'allocation-based', + startEquityWeight: 0.8, + endEquityWeight: 0.4, + equityReturn: 0.11, + bondReturn: 0.035, + }, + { + mode: 'custom-waypoints', + valueType: 'equityWeight', + waypoints: [{ age: 30, value: 0.75 }], + equityReturn: 0.1, + bondReturn: 0.04, + }, + ]; + + expect(configs).toHaveLength(3); + configs.forEach((config) => { + expect([ + 'fixed-return', + 'allocation-based', + 'custom-waypoints', + ]).toContain(config.mode); + }); + }); + + it('should properly define result types', () => { + // Test MonthlyTimelineEntry structure + const timelineEntry: MonthlyTimelineEntry = { + month: 120, + age: 39.92, + currentBalance: 125750.5, + cumulativeContributions: 120000, + cumulativeInterest: 5750.5, + monthlyInterestEarned: 825.33, + currentAnnualReturn: 0.085, + currentMonthlyReturn: 0.006825, + currentEquityWeight: 0.75, + }; + + expect(timelineEntry.month).toBe(120); + expect(timelineEntry.age).toBeCloseTo(39.92, 2); + expect(timelineEntry.currentBalance).toBeCloseTo(125750.5, 2); + expect(typeof timelineEntry.currentEquityWeight).toBe('number'); + }); + + it('should work with utility types', () => { + // Test ConfigForMode utility type + type FixedConfig = ConfigForMode<'fixed-return'>; + type AllocationConfig = ConfigForMode<'allocation-based'>; + + const fixedConfig: FixedConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.06, + }; + + const allocationConfig: AllocationConfig = { + mode: 'allocation-based', + startEquityWeight: 0.8, + endEquityWeight: 0.4, + equityReturn: 0.11, + bondReturn: 0.035, + }; + + expect(fixedConfig.mode).toBe('fixed-return'); + expect(allocationConfig.mode).toBe('allocation-based'); + }); + }); + + // ======================================================================== + // TYPE GUARDS INTEGRATION TESTS + // ======================================================================== + + describe('Type Guards Integration', () => { + it('should correctly identify configuration types', () => { + const fixedConfig: DynamicGlidepathConfig = { + mode: 'fixed-return', + startReturn: 0.08, + endReturn: 0.06, + }; + + const allocationConfig: DynamicGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.8, + endEquityWeight: 0.4, + equityReturn: 0.11, + bondReturn: 0.035, + }; + + const waypointsConfig: DynamicGlidepathConfig = { + mode: 'custom-waypoints', + valueType: 'return', + waypoints: [{ age: 30, value: 0.08 }], + }; + + // Test type guards + expect(isFixedReturnConfig(fixedConfig)).toBe(true); + expect(isFixedReturnConfig(allocationConfig)).toBe(false); + expect(isFixedReturnConfig(waypointsConfig)).toBe(false); + + expect(isAllocationBasedConfig(allocationConfig)).toBe(true); + expect(isAllocationBasedConfig(fixedConfig)).toBe(false); + expect(isAllocationBasedConfig(waypointsConfig)).toBe(false); + + expect(isCustomWaypointsConfig(waypointsConfig)).toBe(true); + expect(isCustomWaypointsConfig(fixedConfig)).toBe(false); + expect(isCustomWaypointsConfig(allocationConfig)).toBe(false); + }); + + it('should enable proper TypeScript narrowing', () => { + const config: DynamicGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: 0.8, + endEquityWeight: 0.4, + equityReturn: 0.11, + bondReturn: 0.035, + }; + + // Test that type guard correctly identifies the config + expect(isAllocationBasedConfig(config)).toBe(true); + + // Access properties after type guard validation + if (isAllocationBasedConfig(config)) { + expect(config.equityReturn).toBe(0.11); + expect(config.bondReturn).toBe(0.035); + expect(config.startEquityWeight).toBe(0.8); + } + }); + }); + + // ======================================================================== + // CONSTANTS INTEGRATION TESTS + // ======================================================================== + + describe('Constants Integration', () => { + it('should provide consistent contribution frequency constants', () => { + expect(CONTRIBUTION_FREQUENCY.YEARLY).toBe(1); + expect(CONTRIBUTION_FREQUENCY.MONTHLY).toBe(12); + expect(CONTRIBUTION_FREQUENCY.WEEKLY).toBe(52); + }); + + it('should provide comprehensive glidepath defaults', () => { + expect(GLIDEPATH_DEFAULTS.FIXED_RETURN.START_RETURN).toBe(0.1); + expect(GLIDEPATH_DEFAULTS.FIXED_RETURN.END_RETURN).toBe(0.055); + + expect(GLIDEPATH_DEFAULTS.ALLOCATION_BASED.START_EQUITY_WEIGHT).toBe(0.9); + expect(GLIDEPATH_DEFAULTS.ALLOCATION_BASED.END_EQUITY_WEIGHT).toBe(0.3); + expect(GLIDEPATH_DEFAULTS.ALLOCATION_BASED.EQUITY_RETURN).toBe(0.12); + expect(GLIDEPATH_DEFAULTS.ALLOCATION_BASED.BOND_RETURN).toBe(0.04); + + expect(GLIDEPATH_DEFAULTS.AGES.START_AGE).toBe(25); + expect(GLIDEPATH_DEFAULTS.AGES.END_AGE).toBe(65); + }); + + it('should provide comprehensive validation constants', () => { + expect(GLIDEPATH_VALIDATION.AGES.MIN_AGE).toBe(0.1); + expect(GLIDEPATH_VALIDATION.AGES.MAX_AGE).toBe(150); + expect(GLIDEPATH_VALIDATION.AGES.MIN_AGE_DIFFERENCE).toBe(1); + + expect(GLIDEPATH_VALIDATION.RETURNS.MIN_RETURN).toBe(-0.99); + expect(GLIDEPATH_VALIDATION.RETURNS.MAX_RETURN).toBe(1.0); + + expect(GLIDEPATH_VALIDATION.ALLOCATIONS.MIN_WEIGHT).toBe(0.0); + expect(GLIDEPATH_VALIDATION.ALLOCATIONS.MAX_WEIGHT).toBe(1.0); + }); + + it('should provide mathematical constants', () => { + expect(GLIDEPATH_MATH.PRECISION.CURRENCY_DECIMALS).toBe(2); + expect(GLIDEPATH_MATH.PRECISION.CURRENCY_MULTIPLIER).toBe(100); + + expect(GLIDEPATH_MATH.CONVERSION.MONTHS_PER_YEAR).toBe(12); + expect(GLIDEPATH_MATH.CONVERSION.WEEKS_PER_YEAR).toBe(52); + expect(GLIDEPATH_MATH.CONVERSION.DAYS_PER_YEAR).toBe(365.25); + }); + + it('should provide configuration templates', () => { + expect(GLIDEPATH_TEMPLATES.CONSERVATIVE.FIXED_RETURN.START_RETURN).toBe( + 0.07 + ); + expect( + GLIDEPATH_TEMPLATES.AGGRESSIVE.ALLOCATION_BASED.START_EQUITY_WEIGHT + ).toBe(1.0); + expect(GLIDEPATH_TEMPLATES.MODERATE.FIXED_RETURN).toEqual( + GLIDEPATH_DEFAULTS.FIXED_RETURN + ); + }); + + it('should provide comprehensive error messages', () => { + expect(GLIDEPATH_ERROR_MESSAGES.AGES.NEGATIVE_START_AGE).toBe( + 'startAge must be positive' + ); + expect(GLIDEPATH_ERROR_MESSAGES.FINANCIAL.NEGATIVE_BALANCE).toBe( + 'initialBalance must be non-negative' + ); + expect(GLIDEPATH_ERROR_MESSAGES.RETURNS.RETURN_TOO_LOW).toContain( + 'must be greater than -0.99' + ); + }); + }); + + // ======================================================================== + // ERROR HANDLING INTEGRATION TESTS + // ======================================================================== + + describe('Error Handling Integration', () => { + it('should provide comprehensive error codes', () => { + expect(GLIDEPATH_ERROR_CODES.NEGATIVE_AGE).toBe('NEGATIVE_AGE'); + expect(GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION).toBe( + 'INVALID_ALLOCATION' + ); + expect(GLIDEPATH_ERROR_CODES.EMPTY_WAYPOINTS).toBe('EMPTY_WAYPOINTS'); + expect(GLIDEPATH_ERROR_CODES.CALCULATION_ERROR).toBe('CALCULATION_ERROR'); + }); + + it('should create and work with error classes', () => { + const ageError = createAgeError('startAge', -5); + + expect(ageError).toBeInstanceOf(DynamicGlidepathValidationError); + expect(ageError).toBeInstanceOf(DynamicGlidepathError); + expect(ageError.code).toBe(GLIDEPATH_ERROR_CODES.NEGATIVE_AGE); + expect(ageError.field).toBe('startAge'); + expect(ageError.value).toBe(-5); + expect(ageError.severity).toBe('error'); + expect(ageError.category).toBe('validation'); + }); + + it('should work with error factory functions', () => { + // Test age error + const ageError = createAgeError('startAge', 70, 65); + expect(ageError.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_AGE_RANGE); + + // Test financial error + const financialError = createFinancialError('initialBalance', -1000); + expect(financialError.code).toBe(GLIDEPATH_ERROR_CODES.NEGATIVE_BALANCE); + + // Test return rate error + const returnError = createReturnRateError('startReturn', -1.5); + expect(returnError.code).toBe(GLIDEPATH_ERROR_CODES.IMPOSSIBLE_LOSS); + + // Test allocation error + const allocationError = createAllocationError('startEquityWeight', 1.5); + expect(allocationError.code).toBe( + GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION + ); + }); + + it('should work with type guards for errors', () => { + const validationError = createAgeError('startAge', -5); + const configError = createConfigurationError('mode', 'invalid-mode', [ + 'fixed-return', + 'allocation-based', + ]); + const calcError = createCalculationError('Simulation failed'); + + expect(isDynamicGlidepathError(validationError)).toBe(true); + expect(isValidationError(validationError)).toBe(true); + expect(isConfigurationError(validationError)).toBe(false); + + expect(isConfigurationError(configError)).toBe(true); + expect(isValidationError(configError)).toBe(false); + + expect(isCalculationError(calcError)).toBe(true); + expect(isBlockingError(calcError)).toBe(true); + }); + + it('should work with error aggregation utilities', () => { + const errors = [ + createAgeError('startAge', -5), + createFinancialError('initialBalance', -1000), + createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ), + ]; + + const grouped = groupBySeverity(errors); + expect(grouped.errors).toHaveLength(2); + expect(grouped.warnings).toHaveLength(1); + + const summary = getSummary(errors); + expect(summary.total).toBe(3); + expect(summary.errorCount).toBe(2); + expect(summary.warningCount).toBe(1); + }); + + it('should work with ValidationResultBuilder', () => { + const builder = new ValidationResultBuilder(); + + builder + .addError(createAgeError('startAge', -5)) + .addWarning( + createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ) + ); + + const result = builder.build(); + expect(result.isValid).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.warnings).toHaveLength(1); + + expect(builder.hasBlockingErrors()).toBe(true); + expect(builder.getErrorCount()).toBe(1); + expect(builder.getWarningCount()).toBe(1); + }); + + it('should support bulk operations in ValidationResultBuilder', () => { + const builder = new ValidationResultBuilder(); + + const errors = [ + createAgeError('startAge', -5), + createFinancialError('initialBalance', -1000), + ]; + + const warnings = [ + createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ), + createWarning( + GLIDEPATH_ERROR_CODES.LONG_SIMULATION_WARNING, + 'endAge', + 120 + ), + ]; + + // Test bulk add methods + builder.addErrors(errors).addWarnings(warnings); + + expect(builder.getErrorCount()).toBe(2); + expect(builder.getWarningCount()).toBe(2); + expect(builder.hasBlockingErrors()).toBe(true); + + // Test clear method + builder.clear(); + + expect(builder.getErrorCount()).toBe(0); + expect(builder.getWarningCount()).toBe(0); + expect(builder.hasBlockingErrors()).toBe(false); + + const result = builder.build(); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + expect(result.warnings).toHaveLength(0); + }); + + it('should serialize errors to JSON correctly', () => { + const error = createAgeError('startAge', -5); + const json = error.toJSON(); + + expect(json).toMatchObject({ + name: 'DynamicGlidepathValidationError', + code: GLIDEPATH_ERROR_CODES.NEGATIVE_AGE, + field: 'startAge', + value: -5, + severity: 'error', + category: 'validation', + constraint: expect.any(String), + suggestion: expect.any(String), + message: expect.any(String), + timestamp: expect.any(String), + context: expect.any(Object), + }); + + // Verify timestamp is valid ISO string + expect(() => new Date(json.timestamp as string)).not.toThrow(); + + // Verify the structure allows JSON serialization + expect(() => JSON.stringify(json)).not.toThrow(); + }); + + it('should provide user-friendly error messages', () => { + const error = createAgeError('startAge', -5); + const userMessage = error.getUserFriendlyMessage(); + + expect(userMessage).toContain(error.constraint); + expect(userMessage).toContain(error.suggestion); + expect(userMessage).not.toContain('Received:'); // Should not include technical details + }); + + it('should correctly identify blocking vs non-blocking errors', () => { + const error = createAgeError('startAge', -5); + const warning = createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ); + + expect(error.isBlockingError()).toBe(true); + expect(warning.isBlockingError()).toBe(false); + + expect(error.severity).toBe('error'); + expect(warning.severity).toBe('warning'); + }); + + describe('Edge Cases and Error Conditions', () => { + it('should handle invalid parameters in createAgeError', () => { + expect(() => createAgeError('invalidField', 25)).toThrow( + 'Invalid age error parameters' + ); + }); + + it('should handle invalid parameters in createReturnRateError', () => { + expect(() => createReturnRateError('invalidField', 0.08)).toThrow( + 'Invalid return rate error parameters' + ); + }); + + it('should handle invalid parameters in createAllocationError', () => { + expect(() => createAllocationError('invalidField', 0.5)).toThrow( + 'Invalid allocation error parameters' + ); + }); + + it('should handle invalid parameters in createConfigurationError', () => { + expect(() => + createConfigurationError('invalidField', 'value', ['option1']) + ).toThrow('Invalid configuration error parameters'); + }); + + it('should handle invalid parameters in createWaypointError', () => { + expect(() => createWaypointError('invalidField', 'value')).toThrow( + 'Invalid waypoint error parameters' + ); + }); + + it('should handle invalid warning codes in createWarning', () => { + expect(() => + createWarning('INVALID_CODE' as any, 'field', 'value') + ).toThrow('No template found for warning code: INVALID_CODE'); + }); + + it('should handle non-number values in createReturnRateError', () => { + const error = createReturnRateError( + 'startReturn', + 'not-a-number' as any + ); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_RETURN_TYPE); + expect(error.constraint).toBe('Must be a number'); + expect(error.suggestion).toBe('Try: 0.08 for 8% annual return'); + }); + + it('should handle non-number values in createAllocationError', () => { + const error = createAllocationError( + 'startEquityWeight', + 'not-a-number' as any + ); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION_TYPE); + expect(error.constraint).toBe('Must be a number'); + expect(error.suggestion).toBe('Try: 0.70 for 70% allocation'); + }); + + it('should handle allocation values > 1 with percentage suggestion', () => { + const error = createAllocationError('startEquityWeight', 80); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_ALLOCATION); + expect(error.suggestion).toContain( + 'Did you mean 0.80? (80% as decimal)' + ); + }); + + it('should handle glidepathConfig field in createConfigurationError', () => { + const error = createConfigurationError( + 'glidepathConfig', + { invalid: 'config' }, + [] + ); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_CONFIG_STRUCTURE); + expect(error.suggestion).toContain('mode: "fixed-return"'); + }); + + it('should handle waypoints field in createWaypointError', () => { + const error = createWaypointError('waypoints', []); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.EMPTY_WAYPOINTS); + expect(error.suggestion).toContain('age: 30, value: 0.80'); + }); + + it('should handle age field in createWaypointError', () => { + const error = createWaypointError('age', -5, 0); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_WAYPOINT_AGE); + expect(error.field).toBe('age[0]'); + expect(error.suggestion).toContain('age: 35 for waypoint at age 35'); + }); + + it('should handle value field in createWaypointError', () => { + const error = createWaypointError('value', 'invalid', 1); + expect(error.code).toBe(GLIDEPATH_ERROR_CODES.INVALID_WAYPOINT_VALUE); + expect(error.field).toBe('value[1]'); + expect(error.suggestion).toContain('0.08 for 8% return'); + }); + }); + + describe('Utility Functions', () => { + it('should correctly identify warnings with isWarning', () => { + const error = createAgeError('startAge', -5); + const warning = createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ); + + expect(isWarning(error)).toBe(false); + expect(isWarning(warning)).toBe(true); + }); + + it('should group errors by category correctly', () => { + const errors = [ + createAgeError('startAge', -5), // validation + createConfigurationError('mode', 'invalid', ['fixed']), // configuration + createCalculationError('Test calc error'), // calculation + ]; + + const grouped = groupByCategory(errors); + expect(grouped.validation).toHaveLength(1); + expect(grouped.configuration).toHaveLength(1); + expect(grouped.calculation).toHaveLength(1); + expect(grouped.performance).toHaveLength(0); + expect(grouped.usability).toHaveLength(0); + }); + + it('should test all warning templates', () => { + const warningCodes = [ + GLIDEPATH_ERROR_CODES.LONG_SIMULATION_WARNING, + GLIDEPATH_ERROR_CODES.LARGE_CONTRIBUTION_WARNING, + GLIDEPATH_ERROR_CODES.UNUSUAL_ALLOCATION_PROGRESSION, + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + GLIDEPATH_ERROR_CODES.EXTREME_LOSS_WARNING, + ]; + + warningCodes.forEach((code) => { + const warning = createWarning(code, 'testField', 'testValue'); + expect(warning.code).toBe(code); + expect(warning.severity).toBe('warning'); + expect(['usability', 'validation', 'performance']).toContain( + warning.category + ); + expect(warning.constraint).toBeTruthy(); + expect(warning.suggestion).toBeTruthy(); + }); + }); + }); + }); + + // ======================================================================== + // CROSS-COMPONENT COMPATIBILITY TESTS + // ======================================================================== + + describe('Cross-Component Compatibility', () => { + it('should maintain compatibility with existing RetirementCalculator', () => { + // Test that existing calculator methods still work + const result = calculator.getCompoundInterestWithAdditionalContributions( + 10000, + 1000, + 10, + 0.08, + CONTRIBUTION_FREQUENCY.MONTHLY, + CONTRIBUTION_FREQUENCY.MONTHLY + ); + + expect(result.balance).toBeGreaterThan(0); + expect(result.totalContributions).toBeGreaterThan(0); + expect(result.totalInterestEarned).toBeGreaterThan(0); + expect(result.compoundingPeriodDetails).toHaveLength(120); // 10 years * 12 months + }); + + it('should work with existing type system', () => { + // Test that new types can coexist with existing ones + const compoundingResult: CompoundingInterestObjectType = + calculator.getCompoundInterestWithAdditionalContributions( + 25000, + 1000, + 30, + 0.08, + CONTRIBUTION_FREQUENCY.MONTHLY, + CONTRIBUTION_FREQUENCY.MONTHLY + ); + + const yearlyData: YearlyCompoundingDetails[] = + calculator.aggregateDataByYear(compoundingResult); + + expect(yearlyData).toHaveLength(30); + expect(yearlyData[0].year).toBe(1); + expect(yearlyData[29].year).toBe(30); + }); + + it('should validate that constants work with error handling', () => { + // Test that validation constants can be used in error creation + const invalidAge = GLIDEPATH_VALIDATION.AGES.MIN_AGE - 1; // -0.9 + const ageError = createAgeError('startAge', invalidAge); + + expect(ageError.value).toBe(invalidAge); + expect(ageError.code).toBe(GLIDEPATH_ERROR_CODES.NEGATIVE_AGE); + }); + + it('should validate that error messages reference validation constants', () => { + // Ensure error messages are consistent with validation limits + expect(GLIDEPATH_ERROR_MESSAGES.RETURNS.RETURN_TOO_LOW).toContain( + '-0.99' + ); + expect(GLIDEPATH_VALIDATION.RETURNS.MIN_RETURN).toBe(-0.99); + + expect(GLIDEPATH_ERROR_MESSAGES.AGES.AGE_DIFFERENCE_TOO_SMALL).toContain( + '1 year' + ); + expect(GLIDEPATH_VALIDATION.AGES.MIN_AGE_DIFFERENCE).toBe(1); + }); + + it('should validate template configurations against validation rules', () => { + // Conservative template + expect( + GLIDEPATH_TEMPLATES.CONSERVATIVE.FIXED_RETURN.START_RETURN + ).toBeGreaterThan(GLIDEPATH_VALIDATION.RETURNS.MIN_RETURN); + expect( + GLIDEPATH_TEMPLATES.CONSERVATIVE.ALLOCATION_BASED.START_EQUITY_WEIGHT + ).toBeLessThanOrEqual(GLIDEPATH_VALIDATION.ALLOCATIONS.MAX_WEIGHT); + + // Aggressive template + expect( + GLIDEPATH_TEMPLATES.AGGRESSIVE.ALLOCATION_BASED.END_EQUITY_WEIGHT + ).toBeGreaterThanOrEqual(GLIDEPATH_VALIDATION.ALLOCATIONS.MIN_WEIGHT); + }); + }); + + // ======================================================================== + // INTEGRATION SMOKE TESTS + // ======================================================================== + + describe('Integration Smoke Tests', () => { + it('should successfully create a complete glidepath configuration', () => { + const config: DynamicGlidepathConfig = { + mode: 'allocation-based', + startEquityWeight: + GLIDEPATH_DEFAULTS.ALLOCATION_BASED.START_EQUITY_WEIGHT, + endEquityWeight: GLIDEPATH_DEFAULTS.ALLOCATION_BASED.END_EQUITY_WEIGHT, + equityReturn: GLIDEPATH_DEFAULTS.ALLOCATION_BASED.EQUITY_RETURN, + bondReturn: GLIDEPATH_DEFAULTS.ALLOCATION_BASED.BOND_RETURN, + }; + + expect(isAllocationBasedConfig(config)).toBe(true); + + // Access properties after type guard validation + if (isAllocationBasedConfig(config)) { + expect(config.startEquityWeight).toBe(0.9); + expect(config.endEquityWeight).toBe(0.3); + expect(config.equityReturn).toBe(0.12); + expect(config.bondReturn).toBe(0.04); + } + }); + + it('should successfully validate configurations and produce meaningful errors', () => { + const builder = new ValidationResultBuilder(); + + // Test various validation scenarios + try { + createAgeError('startAge', -5); + } catch (error) { + // Expected for this test - the error should be caught in production + } + + // Create a valid error and test it + const error = createFinancialError('monthlyContribution', -500); + builder.addError(error); + + const result = builder.build(); + expect(result.isValid).toBe(false); + expect(result.errors[0].suggestion).toContain( + 'Try: 0 if not making regular contributions' + ); + }); + + it('should format errors for display correctly', () => { + const errors = [ + createAgeError('startAge', -5), + createReturnRateError('equityReturn', -1.2), + ]; + + const consoleOutput = formatForConsole(errors); + expect(consoleOutput).toContain('❌ ERRORS:'); + expect(consoleOutput).toContain('startAge'); + expect(consoleOutput).toContain('equityReturn'); + + const apiOutput = formatForAPI(errors); + expect(apiOutput.errors).toHaveLength(2); + expect(apiOutput.summary.errorCount).toBe(2); + }); + + it('should format console output with different options', () => { + const errors = [ + createAgeError('startAge', -5), + createWarning( + GLIDEPATH_ERROR_CODES.EXTREME_RETURN_WARNING, + 'startReturn', + 0.25 + ), + ]; + + // Test with all options enabled + const fullOutput = formatForConsole(errors, { + showSuggestions: true, + showErrorCodes: true, + groupByCategory: true, + }); + + expect(fullOutput).toContain('💡 Try:'); + expect(fullOutput).toContain('NEGATIVE_AGE'); + expect(fullOutput).toContain('VALIDATION'); + + // Test with minimal options + const minimalOutput = formatForConsole(errors, { + showSuggestions: false, + showErrorCodes: false, + groupByCategory: false, + }); + + expect(minimalOutput).not.toContain('💡 Try:'); + expect(minimalOutput).not.toContain('NEGATIVE_AGE'); + }); + + it('should handle empty error array in formatForConsole', () => { + const output = formatForConsole([]); + expect(output).toBe('✅ No errors found'); + }); + + it('should maintain numeric precision with math constants', () => { + const testValue = 123.456789; + const rounded = + Math.round(testValue * GLIDEPATH_MATH.PRECISION.CURRENCY_MULTIPLIER) / + GLIDEPATH_MATH.PRECISION.CURRENCY_MULTIPLIER; + + expect(rounded).toBe(123.46); + const decimalPart = rounded.toString().split('.')[1]; + const decimalLength = decimalPart !== undefined ? decimalPart.length : 0; + expect(decimalLength).toBeLessThanOrEqual( + GLIDEPATH_MATH.PRECISION.CURRENCY_DECIMALS + ); + }); + + it('should work with all template configurations', () => { + const templates = [ + GLIDEPATH_TEMPLATES.CONSERVATIVE, + GLIDEPATH_TEMPLATES.MODERATE, + GLIDEPATH_TEMPLATES.AGGRESSIVE, + ]; + + templates.forEach((template) => { + expect(template.FIXED_RETURN.START_RETURN).toBeGreaterThan( + template.FIXED_RETURN.END_RETURN + ); + expect( + template.ALLOCATION_BASED.START_EQUITY_WEIGHT + ).toBeGreaterThanOrEqual(template.ALLOCATION_BASED.END_EQUITY_WEIGHT); + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 08694b5..d6b170f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "include": [ "index.ts", "src/**/*", - "tests/**/*" + "tests/**/*", + "examples/**/*" ], "exclude": [ "node_modules" diff --git a/typedoc.json b/typedoc.json index a1e7b73..2304439 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,6 +1,8 @@ { - "entryPoints": ["./index.ts"], + "entryPoints": ["./index.ts", "./src/**/*.ts"], "out": "./docs", - "exclude": ["node_modules", "**/*.spec.ts"], - "groupOrder": ["Classes", "Interfaces", "Enums","*"] -} \ No newline at end of file + "exclude": ["**/*.spec.ts"], + "groupOrder": ["Classes", "Types", "Interfaces", "Enums", "Functions", "*"], + "categorizeByGroup": true, + "includeVersion": true +}