Skip to content

nigelarcher/GlidePath

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GlidePath

A standalone, offline-capable portfolio projection tool. Models a user-definable set of account buckets (personal and superannuation) with contribution flow, optional rebalancing haircuts, leverage, downturn stress, wage and Super Guarantee modelling, and an inflation-indexed retirement drawdown with Age Pension support. Australian tax context throughout (AUD, marginal rates, the post-2026-Budget CGT regime, super preservation, Division 293/296).

Not financial advice — a personal modelling tool only. See the disclaimer at the end.

Running it

Open index.html in any modern browser. No build step, no server, no package manager. file:// works fine.

Two libraries load from CDN, version-pinned with SRI integrity hashes: Chart.js 4.4.0 and marked 12.0.0. For fully offline use, download those two files locally and update the <script> tags in index.html.

File structure

Eight source files, by deliberate convention — see CLAUDE.md before adding more.

index.html         Markup + DOM
styles.css         Styling (light + dark via prefers-color-scheme)
config.js          localStorage persistence, export/import, defaults, migration
calculator-core.js Pure shared math (no DOM) — loaded in the browser, require()'d by tests
calculator.js      Simulation engine, chart rendering, allocation table, action panel, event wiring
ai-advisor.js      Optional AI Advisor — Google Gemini chat + system prompt
ai-advisor.css     AI Advisor chat styling
README.md          This file
CLAUDE.md          Conventions for AI assistants working on this codebase
tests/             Test suite — NN-name.test.js files + run-all.js, helpers.js, sim-harness.js

Tests live in tests/ as NN-name.test.js files, run by tests/run-all.js. calculator-core.js is the source of truth for any math shared between the browser and the tests.

Core concepts

Buckets

The portfolio is split into buckets — a user-definable array, not a fixed set. Each bucket is { id, name, type, owner, monthly, objective, structure, risks }:

  • type is personal or super and is immutable once created (rename freely, never retype — delete and recreate instead). type drives every tax, preservation and pension decision.
  • There is always at least one personal and one super bucket.
  • Buckets are independent: contributions, rebalancing, flow redirect and haircuts never cross a bucket boundary. (One sanctioned exception: with reinvestPensionSurplus on, a forced ATO-minimum drawdown surplus may flow from a super bucket to a personal bucket.)
  • owner is a free-text person label, used for couple-mode Super Guarantee routing and per-person Division 296 grouping.

Targets

Each asset has a target percentage. Targets are bucket-internal — within a bucket they are intended to sum to 100. Cross-bucket allocation is whatever contributions and growth produce; the model never balances across buckets.

Scenarios

The whole config is a list of scenarios, each with its own global settings, buckets, assets and loans. Scenario tabs switch between them; one is active at a time.

Schema

The config schema is version 8. The authoritative shape lives in config.js — see SCENARIO_DEFAULTS for the defaults and the migration code for how older versions are upgraded. Top-level shape:

{
  schemaVersion: 8,
  activeScenarioId: '...',
  scenarios: [
    {
      id, name,
      global:  { ...simulation settings... },
      buckets: [ { id, name, type, owner, monthly, objective, structure, risks, superOverrides? } ],
      assets:  [ { id, name, account, start, costBasis, ret, income, target, ... } ],
      loans:   [ { id, name, bucket, balance, rate, ..., collateralAssetIds } ],
      actionsJournal: [...], advanceHistory: [...]
    }
  ]
}

Notable global fields (see config.js for the complete list and defaults):

  • Time / behaviour: years, rebalance, flowRedirect, inflationRate, wageInflation, lotMethod (HIFO/FIFO), realDollarView.
  • Drawdown: drawdown, drawdownStartYear, livingTarget, optional real spending glide (livingTargetEnd, livingGlideStartYear/EndYear), drawdownFunding (sellFirst/borrowFirst).
  • Tax: taxRate (personal marginal), superDefaults (preservation year + super income/CGT rates), concessionalCap, contribTaxEnabled, div293Threshold, div296Enabled/div296Threshold/div296IndexThreshold.
  • Wage / pension: wageEnabled, wage, sgRate, the wage-glide fields, partnerWage*, wageOwner/partnerWageOwner, agePensionEnabled, isCouple, homeowner.
  • Events: downturn, downturnEvents[], catchupEnabled, dipBuyEnabled, cashInjections[], cashWithdrawals[].

Each asset carries return (ret, with an optional earlyRet/decayYears glide), income yield, target (with an optional targetEnd lifecycle glide), costBasis, maxSell, volatility (downturn-event sensitivity — NOT a statistical std-dev), the isBuffer/illiquid/noLossSell flags, a region/style/sector taxonomy, and an account (bucket id).

Each loan is top-level, owned by one bucket, with collateralAssetIds, an interest rate, repayment, deductible-interest percentage, and three LVR thresholds (safetyLVR, marginCallTrigger, marginCallLVR).

Saved configs from older schema versions (v1–v7) migrate automatically on load — migrations never substitute defaults for stored values.

Simulation model

The engine (calculate() in calculator.js) runs month-by-month over the horizon. Each year:

Contributions & flow redirect

Each bucket's monthly contribution is distributed within the bucket by distributeContribution (in calculator-core.js). With flowRedirect on, money goes to underweight assets first (equal split), then to at-target/untargeted assets by target weight; it is never routed into an overweight asset. With it off, money is distributed by raw target weight. Illiquid assets are excluded.

Income

Each asset's income yield is taxed at its bucket's income-tax rate. Outside drawdown the net income is pooled within the bucket and reinvested via the same flow-redirect logic. In drawdown, spendable income offsets the living target.

Rebalancing haircut

When rebalance is on, and it is not a downturn or drawdown year, each bucket trims assets that are over their raw target and up year-on-year, capped by each asset's maxSell percentage. Proceeds (after CGT) redistribute to underweight assets in the same bucket. Buffer and target = 0 assets are skipped.

Downturn stress

downturnEvents[] each define a startYear, drawdownYears, flatYears and severity. During an event, each asset's return is scaled by its volatility. With catchupEnabled, the years after an event accelerate back toward the no-downturn trajectory. With dipBuyEnabled, buffer assets redeploy a capped share into under-target assets during an event.

Retirement drawdown

From drawdownStartYear, the portfolio funds the inflation-indexed livingTarget. Funding order: after-tax wage and Age Pension first, then the portfolio. Portfolio sales draw buffer assets first (in downturn years), then super (past preservation, including the forced ATO age-based minimum), then personal — each respecting maxSell. drawdownFunding chooses sell-first or borrow-first.

Leverage

Loans amortise monthly. LVR is tracked against pledged collateral. Crossing marginCallTrigger auto-sells collateral down to the cap; marginCallLVR is a hard borrowing ceiling; safetyLVR is the user's own early-warning line. Three distinct failure modes are reported separately: margin call, forced-sale failure (illiquid/maxSell-capped collateral), and credit-limit hit.

Reconciliation invariant

Per bucket per year: Close = Open + Contributions + Income + Buys − Sells + Growth. The test suite enforces this stays exact.

Tax model

Personal (taxable) buckets

The post-2026-Budget CGT regime, effective 1 July 2027, applied uniformly from Y0 (transitional rules are noise over a 10–20 year horizon):

  • Income tax at taxRate (marginal).
  • CGT = max(taxRate, 30%) × real_gain, where real_gain = proceeds − indexed_cost and indexed_cost = lot.cost × (1 + inflationRate)^years_held. Per-lot tracking, HIFO or FIFO.
  • Loss carryforward accumulates per bucket and offsets future gains in the same bucket; it does not expire and does not cross buckets.

Super buckets

The existing super regime — the 1/3 CGT discount is preserved (≈10% effective in accumulation), no cost-base indexation:

  • Income tax: superTaxAccum in accumulation, superTaxPension in pension.
  • CGT: superCGTAccum / superCGTPension.
  • Rates resolve from global.superDefaults, with optional per-bucket superOverrides. Loss carryforward is per bucket.

Concessional contribution tax (optional)

With contribTaxEnabled on, every concessional contribution into a super bucket (employer Super Guarantee + salary-sacrifice) is reduced by the standard 15% contributions tax, plus the Division 293 surcharge — an extra 15% on the slice of a person's concessional contributions that, stacked on their wage, clears div293Threshold ($250k). Default off.

Division 296 (guardrail only)

With div296Enabled on, the model flags the first projected year a person's super crosses div296Threshold ($3m, optionally CPI-indexed in $150k steps). The Div 296 earnings tax itself is not modelled — projected super past the crossing year is therefore mildly optimistic. (Modelling the tax is a recorded TODO; see CLAUDE.md.)

Wage & retirement income

With wageEnabled on, wage (gross employment income, today's $) drives an automatic employer Super Guarantee at sgRate. A super bucket's monthly contribution then becomes extra salary-sacrifice on top of the SG. The wage can wind down (transition to retirement): full until wageGlideStartYear, tapering to wageEnd by wageGlideEndYear. In a drawdown year the portfolio funds only the part of the living target the after-tax wage and Age Pension do not cover.

Couple mode (isCouple) adds a second earner (partnerWage) taxed separately at individual marginal brackets, each driving its own SG.

Age Pension (agePensionEnabled) computes a means-tested pension from preservation age + 7, applying both the assets and income tests and taking the lower result; it rises as the portfolio depletes.

UI surfaces

  • Header — export/import config, encrypted backup/restore, reset.
  • Scenario bar — scenario tabs, bucket filter, "view by" axis selector.
  • Sidebar — collapsible control sections: personal tax, time/horizon, wage & Super Guarantee, drawdown, Age Pension, super (tax, caps, Division 296/293), cash injections, cash withdrawals, downturn events, and AI Advisor config.
  • Investment thesis — a collapsible reasoning / invalidation-signal block.
  • Bucket tabs — one per bucket, each with its monthly contribution and asset cards; add/remove/rename buckets; an asset card's account dropdown moves it between buckets.
  • Loans — loan list with a collateral stress test.
  • Action panel — Y1–Y3 next-action cards: contribute, rebalance, drawdown, buffer refills, and tax, with a Division 296 banner when relevant.
  • Charts — a stacked projection chart (downturn/drawdown shading, preservation and Div 296 markers) plus allocation pies.
  • Allocation table — Balances / YoY change / Cash flows, with a density toggle (every year / 5 / 10).
  • Advance year — record actual end-of-year balances and roll the model forward one year.

AI Advisor (optional)

An optional sidebar chat backed by Google Gemini. It is off until you add an API key. When used, each turn sends your portfolio data and the conversation to Google. Granular context toggles control how much is disclosed (structure, dollar values, percentages, strategy notes, loans). The system prompt — including the calculator-semantics glossary that keeps the advice accurate — lives in ai-advisor.js.

Persistence

  • Auto-saves to localStorage on every change (debounced).
  • Export / import config as plain JSON — portfolio and scenarios only; the AI API key is deliberately excluded so an export cannot leak it.
  • Encrypted backup / restore — "Encrypted backup" encrypts the full config (portfolio, scenarios and AI settings, including the Gemini API key) under a passphrase and downloads it as a JSON file you can store anywhere, including Google Drive. "Restore backup" decrypts a backup file given its passphrase and reloads. Uses the Web Crypto API only — PBKDF2-SHA256 (600k iterations) + AES-256-GCM, random salt and IV per backup, no third-party dependency. The passphrase is never stored and cannot be recovered; lose it and the backup is permanently unreadable.
  • Reset to defaults wipes the stored config.
  • Storage keys: portfolio_calc_config_v3 (portfolio + scenarios — the v3 is a historical key name; the schema inside is version 8), portfolio_ai_config_v1 (AI Advisor settings + API key, kept separate so a plain export can't leak the key), plus portfolio_ai_chat_v1, portfolio_ai_consent_v1 and portfolio_sidebar_collapsed_v1 for UI state.
  • Older schema versions migrate automatically on load.

Known limitations (intentional)

  • Deterministic returns. No Monte Carlo; no sequence-of-returns variance beyond the optional downturn stress test. A run is reproducible.
  • CGT regime applied from Y0. No transitional rules for pre-12-May-2026 assets — noise over a 10–20 year horizon (and slightly overstates early-year tax, which biases plans safe).
  • No franking credits. Australian equity income is taxed gross — slightly pessimistic on an Australian-income strategy.
  • Division 296 is a guardrail, not a tax model — see above.
  • No excess-contributions-tax modelling beyond the concessional-cap warning, and no transfer balance cap.
  • Single CPI rate. No regime-change modelling.
  • Category targeting is descriptive only. Region/Style/Sector are lenses; rebalancing targets remain per-asset.

Testing

Run node tests/run-all.js from the project root. It discovers and runs every tests/*.test.js file and reports pass/fail counts (--only <substr> filters).

Tests are standalone Node scripts that either mock the minimum browser environment or drive the real calculate() through tests/sim-harness.js (which loads config.js + calculator.js with stubbed browser globals). tests/helpers.js holds shared assertion helpers. Each test file documents the spec for one area:

02-flow-redirect              13-drawdown-tax (ATO minimum drawdown, super cap)
03-haircut                    14-margin-call (LVR thresholds)
04-drawdown-order             15-super-caps-reinvest
05-cgt-new-regime             16-dynamic-buckets-migration (v7)
06-income-redirect            17-div296 (threshold indexation)
07-reconciliation             18-full-sim (end-to-end calculate())
08-illiquid                   19-wage-sg (wage + Super Guarantee)
09-leverage                   20-contrib-tax (15% base + Division 293)
10-loans (v4 migration)
11-downturn (v5 migration)
12-scenarios (v6 migration)

A passing test suite is the contract. Don't ship changes that break tests without explicit acknowledgement; add a test for every new feature and a regression test for every bug fix.

Support

Free to use. If it helped, the footer has a "buy me a coffee" link.

Development notes for AI assistants

See CLAUDE.md in this directory for project conventions, architectural principles and deferred design decisions. Read it first.

Disclaimer

Not financial advice. A personal modelling tool only — the author is not a financial adviser, accountant or tax agent and holds no AFS licence. Every figure is an estimate built on assumptions that will not match reality. Verify every assumption with a licensed professional before making any decision.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors