Skip to main content

Calculations

All analysis is rule-based (no LLM), pure and covered by tests.

Net worth​

portfolio (investments) + off-portfolio assets (real estate, cash) βˆ’ debt (loans)

Assets and positions held in a foreign currency are converted to PLN at the current NBP rate (table A); the conversion refreshes when the app starts, and further calculations then operate on amounts already in PLN.

Cryptocurrencies. A crypto position with a quantity and a recognised symbol is valued at the spot rate from CoinGecko (public API, no key), just like currencies against the NBP: value = quantity Γ— rate, with the rate cached (and refreshed) and a fallback to the last known price when there is no network. The average purchase price = capital Γ· quantity; with the "Investment details" module the quantity, the weighted average (WAC) and the realised result are derived from the transaction history. Privacy: the request for a public rate contains no data about you or how much you hold β€” it is purely a market-price lookup.

Gold and metals. A metal position holds grams of pure metal (weight Γ— fineness/1000). Gold is valued at the NBP price (cenyzlota β€” the official price of 1 g of 1000-fineness gold in PLN, published once per business day; the same free API the currency rates come from): value = pure grams Γ— price per gram. The valuation refreshes on app start; a very old cache (offline) never overwrites anything. Silver, platinum and palladium are valued from the Stooq spot (XAG/XPT/XPD, USD per troy ounce) converted at the NBP rate into zΕ‚oty per gram of pure metal. Gain/loss is measured against the purchase price entered when adding. A monthly contribution to gold settles in grams (amount Γ· the month's NBP price; past months come from the NBP archive, the positional median of daily quotes) and lands in the position's purchase history β€” just like crypto contributions; the position's initial holding is protected by a seed entry in the history.

Retail treasury bonds (EDO/COI/TOS/OTS/ROR/ROS…). A position with series terms is valued by the bond engine: year 1 at the issue rate, years 2+ = CPI (GUS, y/y) + margin (deflation β†’ margin only), with annual capitalization (EDO/TOS/ROS/ROD) or interest payouts (COI yearly, ROR/DOR monthly; payouts don't raise the position value β€” they're cash flow). Within a period interest accrues linearly by days. CPI comes from a bundled GUS table; months outside it use the position's editable inflation assumption. The early-redemption fee (zΕ‚/unit, per series) is capped at accrued interest. Simplification: ROR/DOR use the purchase rate (the NBP reference rate has no free CORS feed). Monthly contributions skip bond positions β€” buying more in another month is a different series, i.e. a new position from the catalog.

Quantity computed from contributions (automatically). In the monthly entry you don't enter how many units you bought β€” you provide the contribution amount in zΕ‚oty, and the app converts it into units at the coin's rate for that month:

units = contribution amount (PLN) Γ· coin's rate for the given month (PLN)

The sum across months = the entire holding. We take the rate live for the current month, and for past months β€” from the CoinGecko archive (/coins/{id}/history, also public, no key). Each such contribution is saved as a transaction in the history (a computed purchase), so the quantity, the average price (WAC) and the value are calculated by the existing code. Saving the same month again overwrites the entry (no double counting), while manually entered transactions remain untouched. When the rate for a month cannot be fetched (offline / unmapped coin), the contribution waits for the next save of that month β€” we never block the save and never invent a quantity.

Per-person contributions and older entries. An entry keeps each person's contributions separately (contributionsByRole); the household total drives the calculations. Entries from before the split only have the total β€” on load we split it by holding owner (the adult's ownerId); shared and children's holdings go to whoever saved the entry, or the plan's first person. Such entries also lack the "already booked" record (appliedContributions), so their contributions are treated as already reflected in values β€” re-saving, or removing one person's part, never adds them twice (a loan overpayment doesn't reduce the balance twice). The same goes for loan instalments in such entries: without the "principal already booked" record (appliedLoanPayments) the instalment is treated as already in the balance β€” the app reconstructs the principal it would have taken off (principal = instalment βˆ’ (balance + principal) Γ— rate Γ· 12) and books only a change of the instalment.

PPK and other automatic holdings are contributions. A new entry adds the monthly plan of holdings marked as automatic (e.g. PPK, monthlyTarget) to the owner's column β€” exactly like a typed contribution. It counts towards total contributions, the savings rate, living costs (income βˆ’ contributions βˆ’ instalments), the envelope and, once saved, the PPK account value. A saved entry keeps the amount it holds; "Same as last month" copies the PPK amount from the previous entry, so the total equals last month's.

Who closed the month (reviewedBy). Saving adds the person on this device (the preference in Settings, not the column switch in the wizard) to the list of people who reviewed the month. The other person is added only when "… reviewed this with me" is ticked in step 4. "Closed by both of you" means both people are on the list. Filling in the other person's column ("Same as last month", "Also fill in … part", "Split the missing") changes amounts only, not the list.

Household envelope (Full mode). A preset only sets the envelope β€” it doesn't change contributions you've entered. "Split the missing X zΕ‚ by plan" works at household level (both columns, PPK included) and never lowers anything: missing = envelope βˆ’ all household contributions, gap[account] = max(0, plan[account] βˆ’ household contributions[account]). When missing ≀ Ξ£ gaps, the missing amount is split in proportion to the gaps (no account goes over its plan). When it's larger, every gap is closed first and the excess is split by plan (plan[account] / Ξ£ plan). Amounts are whole units (largest-remainder method), so the household total equals the envelope. Each amount goes to the account owner's column; a joint account is split in half under 50/50 joint ownership (jointSplit: "half"), otherwise it goes to the column of the person splitting. Example: contributions as in September (7,950 zΕ‚ with PPK), envelope 9,100 zΕ‚ β†’ 1,150 zΕ‚ missing; every account is already at plan, so the excess follows plan weights (7,600 zΕ‚): Ania's IKE gets about 151 zΕ‚ in Ania's column, Kuba's IKE about 227 zΕ‚ in Kuba's column.

Net worth after saving (entry closing). Step 4 runs the same save the "Save month" button will run (applyEntryToState) on a copy of the state: it adds contributions (PPK included) except those already booked, takes the principal part of each loan instalment off the balance, sets the step-3 values and converts contributions into units. The result is the net worth after saving, and the difference and the "contributions Β· principal repaid Β· valuations & FX" breakdown come from the same breakdown as the Dashboard hero (this month's snapshot vs last month's). After saving, the Dashboard shows the same numbers. The "(estimate)" note is only about unit prices: the preview starts from the price of the last valuation and fetches, in the background, the same prices the save will use. The comparison and breakdown are shown only for the current month β€” a past month's snapshot is frozen.

Gold, other metals and quoted ETFs work the same way. A monthly contribution to gold, silver, platinum, palladium, or an ETF/stock with a unit count and a Stooq symbol becomes grams or shares (units = amount Γ· price) and goes into the holding's purchase history. Price: gold β€” the NBP price for the month (live or archive); white metals and ETFs β€” the current quote (for the current month). When there's no price (offline, or you're filling in a past month β€” we have no Stooq archive), we use the price implied by the holding's last valuation (value Γ· quantity). It's an approximation, but the contribution always stays in your net worth: after revaluation the value = (previous quantity + new units) Γ— price. Deleting the entry removes those units (and only them β€” the value isn't reduced a second time).

Units only for the difference. Re-saving a month buys units only for the part of the contribution that isn't in the holding's value yet: purchase = month's contribution βˆ’ (already booked βˆ’ this month's derived purchase). Contributions from older entries (saved before booking was tracked, e.g. the demo data) are already in the value, so saving without changes buys nothing, and raising a contribution by 100 zΕ‚ buys units for 100 zΕ‚. Crypto that waited for a price on the first save is added on a later save with a price exactly once.

Months filled in later​

The net-worth snapshot (a point on the charts) is created when an entry is saved. For a month filled in later:

  • in order (no later closed month has a snapshot) β€” saving creates a regular snapshot; the values from step 3 are that month's values,
  • otherwise β€” the snapshot is estimated:
estimate(m) = net(previous snapshot) + Ξ£ net contributions (contributions βˆ’ withdrawals) up to m
+ market change Γ— (months from previous to m Γ· months to the next snapshot)
market change = net(next) βˆ’ net(previous) βˆ’ Ξ£ net contributions in that period

Without an earlier snapshot nothing is estimated; without a later one, only net contributions are added. Estimates are recalculated whenever entries change and when the plan is loaded, and a real snapshot always takes precedence. Existing snapshots of closed months stay frozen.

Cost basis on contributions and withdrawals​

A position that tracks its cost basis (invested) updates it with each entry: a contribution raises the cost by its amount (previously contributions showed up as "gain"), and a withdrawal lowers the cost by the same share as the value β€” cost Γ— withdrawal Γ· value before (like "Withdraw / sell" in the Portfolio), so no phantom loss appears. The removed cost is stored in the entry, so changing, re-saving or deleting the entry gives back exactly the same amount.

A "later snapshot" for months filled in later also includes the current month's snapshot when it has a saved entry β€” a past month filled in after the current one gets an estimate, not today's values.

Where the change in net worth comes from​

The hero sentence on the Dashboard ("October: +11,099 zΕ‚ β€” 7,950 zΕ‚ contributions Β· 679 zΕ‚ principal repaid Β· +2,470 zΕ‚ valuations & FX") splits the change in net worth between two monthly snapshots into three parts:

Ξ” net worth = net worth(month) βˆ’ net worth(previous snapshot)
contributions = Ξ£ contributions from entries in the period to items that are NOT debt
withdrawals = Ξ£ withdrawals / sales from accounts in entries in the period
repaid = Ξ£ (debt balance at start βˆ’ balance at end) β€” only debts present in both snapshots
market = Ξ” net worth βˆ’ contributions + withdrawals βˆ’ repaid
  • A withdrawal (money taken out of an account and spent) is a cash flow, not the market: selling an ETF for 5,147 zΕ‚, after which the account is 5,147 zΕ‚ smaller, gives "βˆ’5,147 zΕ‚ withdrawals Β· 0 zΕ‚ valuations & FX". An account closed during the month stays in that month's snapshot (value 0, flow = the withdrawal) so this adds up.

  • A loan overpayment lowers the balance, so it counts under repaid, not contributions β€” otherwise it would be counted twice.

  • Repaid covers principal from instalments and overpayments. A new loan (absent from the starting snapshot) isn't a "negative repayment" β€” its appearance goes to the rest. When a balance grew, the part is negative ("more debt").

  • Valuations & FX is everything other than contributions and repayment: prices, bond interest, property valuations, but also manual balance corrections and newly added accounts. That's why it always closes the sum.

  • Without holding snapshots (older data), repayment comes from the entries: booked principal from instalments + overpayments.

  • We round to whole zΕ‚oty so the parts still add up to Ξ” (market takes the rounding rest).

A person's and a child's scope. The same weights as the people switcher: a person = own (100%) + joint (100%, or 50% with the 50/50 split), a child = items and assets set aside for their goal. The weight applies to contributions, debt balances and changes in holding values. Assets without monthly snapshots (real estate, cash) are split by the person's share of their current value. With the 50/50 split, Kuba's and Ania's parts therefore add up exactly to the Family's.

The same function computes the breakdown over any number of months (e.g. the last 12) β€” contributions are then the sum of entries over the whole period, and repayment and market are the differences between the snapshot N months ago and the latest one.

Accounts and operations​

An account is instruments + free cash + an operation log. Operations are the source of truth:

instrument units = opening position + buys βˆ’ sells
instrument value = units Γ— price (live price, otherwise the last valuation)
free cash = opening balance + deposits βˆ’ withdrawals βˆ’ buys βˆ’ commissions
+ sells + dividends + interest βˆ’ fees βˆ’ taxes
account value = Ξ£ instrument values + free cash
  • The month's flows are only deposits (from current income) and withdrawals that were spent. They go into the month's entry as a deposit / withdrawal on the account's cash, so the change breakdown, living costs, savings rate, IKE limit, FIRE and returns (MWR/TWR) count them like any other contribution. Buys, sells, switches, dividends, interest, fees and taxes happen inside the account β€” market result, not saving. A deposit "from cash in your net worth" and a withdrawal "to cash in your net worth" are transfers (the "Cash" asset changes too).
  • Cost basis and realised result: securities (ETFs, stocks) β€” FIFO, as PIT-38 requires; crypto β€” pooled costs (average), as in PIT-38 for virtual currencies. Buy: cost = amount + commission; sell: proceeds = amount βˆ’ commission.
  • Editing or deleting an operation recomputes the account from scratch β€” nothing is counted twice. The monthly entry doesn't add amounts to account instruments.
  • Selling more than you hold (more units than there are, or, for an instrument without units, an amount above its value) isn't saved. Otherwise the cash would receive money for units that never existed.
  • Instrument without units (tracked by amount): a buy raises the cost basis by the amount plus commission, and a sale removes the same share of the cost as the share of value sold. Undoing a full sale goes back to the price before it (the app remembers the last unit price).
  • Free cash in a foreign currency: balance in the currency = base + the operations' effects converted at each operation's rate (PLN amount Γ· amount in currency; the PLN commission uses the same rate). PLN value = balance Γ— the position's last known rate (NBP, otherwise the last operation's rate).

PIT-38 (estimate)​

Calculated from operations in ordinary accounts (no type, "Brokerage account", "Crypto exchange"); IKE, IKZE and PPK are left out.

  • Securities: revenue = gross sales in the year; costs = FIFO cost of the units sold (including purchase commission) + sale commissions; income = revenue βˆ’ costs (can be negative β€” a loss for the year).
  • Crypto-assets (Polish PIT Act, art. 22(14)–(16)): costs = the year's spending on purchases (with commission) + sale commissions + excess costs from previous years. When costs exceed revenue, the excess carries over to the next year and income is 0. A coin-for-coin swap recorded as a "Switch" is neutral.
  • Tax: 19% of income; base and tax rounded to whole zΕ‚oty, separately for each part (a crypto loss doesn't reduce securities income).
  • Out of scope: securities losses from earlier years, foreign dividends (the tax difference), staking rewards outside operations.

Savings rate​

Net contributions (contributions βˆ’ withdrawals) divided by total income over the last 12 months with income: Ξ£ (contributions βˆ’ withdrawals) / Ξ£ income. Spent savings lower the rate β€” a month in which you withdrew more than you saved has negative "saved". This is the number on the "You save" card on the Dashboard and in the safety facts in Analysis β†’ Health, always with its window ("last 12 months").

History shows the rate over the months in its table (12 by default), the same in the subtitle and in the "Total" row β€” in the default view it is the same number as in Health. Health used to use 6 months, so two different rates stood on one screen. The rate in a single History row covers that month only; with all months expanded, History uses the whole table.

Loan overpayments. "Contributions" in the rate are all contributions from entries β€” loan overpayments included (an overpayment builds wealth too). The net-worth breakdown, however, counts them as principal repaid, not as a contribution. So that the two numbers aren't confused, when the window holds overpayments the app says so: "saving and overpaying 31.2% of income", and in History the "Saved and overpaid" column and the "savings rate incl. overpayments". Without overpayments it is plain "saving".

Living costs in History: income βˆ’ saved βˆ’ loan payments, where "saved" is contributions βˆ’ withdrawals β€” i.e. income + withdrawals βˆ’ contributions βˆ’ loan payments. Income, saved and loan payments are each rounded to whole zΕ‚oty, and living costs are derived from those rounded amounts β€” every row adds up and the instalment has the same value every month. "Total" is the sum of the displayed rows. FIRE annual expenses and the cushion's average spending use the same rounded cells, so the living-cost "Total" equals "Annual expenses" in FIRE to the zΕ‚oty. Whole columns used to be rounded with the largest-remainder method: the total matched, but a row didn't (15,955 βˆ’ 7,950 βˆ’ 3,804 showed 4,202 instead of 4,201, and the instalment was 3,804 zΕ‚ one month and 3,803 zΕ‚ another).

What changed your net worth (12 months)​

Analysis β†’ Health splits the change in net worth over the last 12 months into parts. The change is the difference between two monthly net-worth snapshots: the latest one (a provisional snapshot of the current month without an entry does not count) and the one from 12 months earlier (or the oldest one when the history is shorter). It is the same code as the Dashboard's monthly breakdown ("Where did this growth come from?"), just with a 12-month window, and the same rounding: contributions and principal are rounded to whole zΕ‚oty and the market takes the remainder, so the parts in zΕ‚oty always add up to the title.

  • Contributions = the sum of contributions from monthly entries after the starting snapshot, up to and including the end one, to accounts and investments. Loan overpayments are not included here β€” they already show up as a lower balance.
  • Principal repaid = how much loan balances fell between the two snapshots (from position snapshots). Without position snapshots we use the principal part of instalments booked in entries plus loan overpayments.
  • New and closed loans (only when it happened) = the balance of loans that appeared in the window (with a βˆ’ sign) plus the balance of loans that left the portfolio (with a + sign). Principal counts only loans present in both snapshots, so without this part a new loan would silently land in "market".
  • Withdrawals (only when there were any) = the sum of withdrawals / sales from accounts in entries in the window, with a βˆ’ sign. Money taken out of net worth and spent β€” without this part the drop after it would land in "market".
  • Valuations and FX = change βˆ’ contributions + withdrawals βˆ’ principal βˆ’ new/closed loans: prices, interest, property valuations and other value changes.

The parts always add up to the change in the card title.

Contributions by person (couples). When both adults have their own contributions, the card splits the "Contributions" part: each person's own contributions (positions they own) and joint ones (positions without an owner and anything that can't be assigned). Joint = contributions βˆ’ the sum of the people, so the three amounts add up to "Contributions". The person scope is the same as the person switcher on the Dashboard.

Emergency cushion​

The cushion pool is the accounts marked as the cushion in Portfolio. With nothing marked, we count interest-bearing positions (deposits, bonds) and assets whose name suggests cash ("account", "savings", "reserve"). Cushion months = pool Γ· average monthly spending including loan instalments over the last 12 months with income: spending = income βˆ’ saved (living costs + loan instalments). Instalments still fall due without a salary, so the cushion says how many months you would really last. Analysis shows it explicitly, e.g. "57,953 zΕ‚ (COI bonds) Γ· 8,587 zΕ‚ spending/mo (incl. loan payments, average of 12 mo.)" β€” 6.7 months in the demo data (without instalments it came out at 13.5, because it ignored the 3,804 zΕ‚ instalment). Loan overpayments sit on the "saved" side β€” you can pause them in a crisis.

The same 12-month window as the savings rate and History. FIRE takes annual expenses without instalments (below) β€” there the loan enters the target as the balance still to repay.

Financial health score (0–100)​

A weighted composition of components:

ComponentWhat it measuresWeight
Buffermonths of expenses in the reserve (target 6)25%
Savings rateagainst the 20% target20%
Debtdebt as a share of assets: 0 β†’ full points, zero points at 50% (mortgage: at 80%)15%
Diversification1 βˆ’ the portfolio's Herfindahl index15%
GoalsΞ£ saved Γ· Ξ£ amounts of goals in progress10%

The score is the sum of value Γ— weight normalised to the available components. The base weights in the table add up to 85%, so the "Where does this score come from?" sheet shows them normalised to 100% β€” with complete data 29 / 23 / 18 / 18 / 12% (rounded with the largest-remainder method, so they always total 100%); when an area is missing, the remaining weights are rescaled the same way. The score itself doesn't change. Each component gets a word: strong (β‰₯ 0.66), medium (β‰₯ 0.33), weak.

A mortgage is scored more gently. When at least half of the debt is a mortgage β€” linked to a property (the "linked liability" field on the asset) or named like one β€” the debt component reaches zero only at 80% of assets, and instead of "weak"/"medium" you see "typical with a mortgage". The home securing the loan sits on the asset side, so this debt is not the same risk as consumer credit.

Goal progress is the sum of saved amounts divided by the sum of the amounts of goals in progress (completed, archived goals are excluded) β€” the same measure as the Goals header, e.g. "125,000 zΕ‚ of 308,000 zΕ‚ Β· 40%". It used to be the average of individual goals' percentages (51% in the demo), so the score and Goals disagreed. When every goal is completed, the component is 100%.

In Analysis the score is one row: "Health score 82/100", a line that lists every area by status ("3 of 5 areas strong: cushion, savings, diversification Β· medium: goals Β· debt: typical with a mortgage") and the "Where does this score come from?" link. Every component with its number and weight is in that sheet. Diversification names the largest position (e.g. "Global ETF"), not its ticker, and its share of investments (not of total wealth).

FIRE​

The FIRE portfolio = the investment lens (accounts + cash + rentals), not total wealth (your home/car are left out). We treat the rates as real (after inflation).

Money set aside for someone else is left out. Positions and assets with a beneficiary (e.g. a child's 800+ bonds) are outside the investment lens, and their monthly contributions are outside the FIRE contribution pace. That isn't capital the couple will live on. In the demo: FIRE portfolio 405,118 zΕ‚ instead of 430,868 zΕ‚ (without Zosia's 25,750 zΕ‚ of bonds), pace 7,150 zΕ‚/mo instead of 7,950 zΕ‚, answer year 2038 instead of 2037. The same lens drives the "For freedom" view in Portfolio (the amount and the list of positions) and the effective after-tax SWR. Total net worth still counts this money β€” it belongs to the household.

Dated goals and the FIRE portfolio. When a dated goal's source is an account or asset inside the investment lens (a chosen source or a name written in the goal's description), Analysis names it next to the answer: "β€œBudowa domu” (2027) will take about 180,000 zΕ‚ of this money". The amount = goal amount βˆ’ saved, rounded as in Goals. The answer year and the freedom amount don't subtract this withdrawal yet β€” the sentence informs, the model doesn't change.

  • Annual expenses β€” from the FIRE settings, or otherwise living costs over the last 12 entries (Ξ£ (income βˆ’ saved βˆ’ loan payments) from amounts rounded to whole zΕ‚oty as in History, scaled to a year when the history is shorter). It is the same amount as the living-cost total in History. It used to be the 6-month average Γ— 12 β€” with a pricier December in the older half of the year that understated annual expenses (by 6,000 zΕ‚ in the demo data) and the freedom amount by about 170,000 zΕ‚.
  • Freedom amount (FIRE number) = (annual expenses βˆ’ fixed post-FIRE income) Γ· SWR + the loan balance to repay in the FIRE year. Multiplier = 1/SWR (4% β†’ Γ—25; 3.5% β†’ Γ—28.6; 3% β†’ Γ—33). Annual expenses exclude instalments (there is no instalment after FIRE), but the balance still left in the FIRE year has to be repaid from the portfolio. We take it from the single loan schedule: every month balance Γ— (1 + rate/12) βˆ’ (instalment + average actual overpayment over 12 entries) β€” the same calculation as the payoff date on the Dashboard and in Goals. The planned overpayment only counts when there are no entries yet (FIRE used to always take it, and in the demo it "repaid" the loan before 2035 although the entries had no overpayments). Loans linked to an asset inside the investment lens (a buy-to-let home) already reduce the FIRE portfolio, so we don't add them.
  • % FIRE = investment portfolio / freedom amount (with the FIRE-year loan balance).
  • SWR configurable (default 3.5%); monthly /12.
  • Coast FIRE = the freedom amount discounted to today at the real return over the years to retirement: target Γ· (1+r)^years, where target = expenses Γ· SWR + the loan balance in the retirement year. "You already have enough that, without adding more, it will grow to the target."
  • Years to freedom β€” iteratively: capital Γ— (1+r) + annual contribution, until β‰₯ the target of the same year (expenses Γ· SWR + that year's loan balance). The balance falls and the portfolio grows, so the first such year is the answer. Demo (entries without overpayments, the loan repaid by the instalment alone until 2051): 391,264 zΕ‚ of balance left in 2038, target 1,639,771 + 391,264 = 2,031,036 zΕ‚, answer 2038. The timeline in the FIRE tab ("FIRE portfolio over time") uses exactly the same recursion, the same portfolio and the same rate for today, 4 and 14 years and the year from the answer β€” so it crosses the freedom amount in the same year the answer gives. Your home, land and other assets outside the lens, and loans not linked to it, are listed by name and amount in "Where do these numbers come from?" next to the answer. The target tick on each bar is that year's target (with that year's loan balance). The annual contribution is 12 Γ— the monthly contributions that feed this portfolio: payments into investment accounts and overpayments on a buy-to-let mortgage. Instalments or overpayments on the mortgage for your own home don't count β€” they pay off the house but don't grow the capital you live on after FIRE. Contributions to positions with a beneficiary don't count either. The Monte Carlo simulation and the "+500 zΕ‚" scenario use the same figure.
  • Levels Lean / Full / Fat = 0.7Γ— / 1Γ— / 1.5Γ— annual expenses Γ· SWR + the loan balance in the year that level is reached (the same recursion as the answer, with that level's expenses). Demo: Lean 2036 (balance 426,080 zΕ‚), Full 2038 (391,264 zΕ‚), Fat 2042 (305,491 zΕ‚). All three used to take the balance from the Full FIRE year.
  • "+500 zΕ‚/mo" β€” the difference between two results of the same calculation in monthly steps: the portfolio compounds monthly at the rate equivalent to the yearly one, contributions arrive monthly, and the target moves as in the answer (expenses Γ· SWR + that month's loan balance). Demo: about 5 months sooner. The target used to be fixed (the freedom amount of the answer year).
  • Age (Coast FIRE, "at age N") comes from the birth date of each adult in the household; for the main person the date from their profile also works. FIRE amounts are in today's money β€” they come from current expenses and real returns.
  • Monte Carlo β€” 400 paths of annual returns ~N(mean, 12% volatility) with the annual contribution, deterministic thanks to a fixed seed. Each path runs to 40 years past the answer year (at most 100 years) and records the first year in which the portfolio covered that year's target (expenses Γ· SWR + that year's loan balance β€” the same moving target as the answer). From the same paths we take: the share of paths that reached the target by the answer year, and the earliest year by which 3 in 4 paths reached it. The answer year comes from the average return, so by definition it falls roughly in the middle of the distribution β€” we present it as the middle ("around 2038 β€” in half of the scenarios"), next to the more certain year ("more certain (3 in 4): 2040"), without a judgement word ("low"). Shares are rounded to 5%. When the share by the answer year falls outside 40–60%, the card states it ("in about 35% of scenarios"). A second line β€” to the retirement age in your assumptions β€” appears when that age falls later; it uses the same paths. Demo: 55% by 2038 (12 years) β†’ "in half", 3 in 4 by 2040, to age 60 over 95%.
  • Real return until FIRE β€” the portfolio's yearly return above inflation until FIRE, chosen in the assumptions: 3% (cautious), 5% (default, moderate for stocks and bonds), 7% (optimistic). It changes the answer year, the timeline, Coast FIRE and Monte Carlo. The note under FIRE lists only what you can set: SWR, real return, expenses and age.
  • The timeline names both amounts in every row: "portfolio X Β· target Y". The row with the answer year says "goal reached" (it used to say "freedom number" next to the portfolio value, which read as the target).
  • "If you went FIRE today" = portfolio Γ— SWR Γ· 12, noted as the amount before repaying the loan (today's balance) β€” a withdrawal from the portfolio doesn't repay the loan by itself.
  • Effective after-tax SWR β€” SWR Γ— (1 βˆ’ weighted average account tax: IKE 0, IKZE 10%, standard 19%), from the same positions as the FIRE portfolio (without positions that have a beneficiary). A directional simplification, not advice.
  • Main goal "Financial independence" β€” without your own amount, the target is the same freedom amount as in Analysis (same settings: SWR, expenses from the FIRE settings or from entries, fixed post-FIRE income, the FIRE-year loan balance), and progress is investment portfolio / target β€” i.e. "% FIRE". Previously the goal silently used 25 Γ— expenses (a 4% SWR) and counted investments only, so it differed from Analysis.

Goals plan​

  • A goal's monthly need = (amount βˆ’ saved) Γ· months until the deadline (min. 1).
  • Needed in total = the sum of monthly needs of goals with a deadline in the future or in the current month. Reached goals, goals past their deadline and goals without a deadline aren't included; the missing amount of overdue goals is shown separately.
  • Capacity (how much the couple puts aside monthly) β€” in order: the amount set on Goals ("Change amount"), the upper end of the savings range from the monthly entry (when the total need exceeds the lower end, the verdict says "fits only at the upper end"), the sum of the monthly contribution plan in the portfolio (no markup β€” it used to be Γ— 1.2; without automatic positions and without money set aside for someone else β€” positions with a beneficiary, e.g. a child's 800+, as in "For freedom" and FIRE), and with no data β€” a default 4000 zΕ‚. With the default estimate and a gap, the first action is entering your amount, not moving a deadline.
  • Verdict β€” it fits when the total need ≀ capacity (the difference is left over); otherwise "about need βˆ’ capacity short". A single goal (only in the form preview) is rated against what's left after the other goals in progress: free = capacity βˆ’ the other goals' need (the goal being edited excluded). Realistic up to 50% of that, tight up to 100%, ambitious above β€” and always ambitious when the total need including it exceeds capacity. The preview also shows the effect on the total: "After adding: (other goals + this one) in total Β· about … short / left over".
  • Move that closes the plan / new deadline for a goal past its deadline β€” the missing amount Γ· what's left after the other goals, keeping a 5% buffer of capacity (free = 95% of capacity βˆ’ the other goals' need; without the buffer when that's ≀ 0), so the plan doesn't close "to the zΕ‚oty". It's offered only up to 12 months ahead; beyond that (or when nothing is left) only a manual change of the deadline.
  • Debt-free around YYYY (main goal) β€” the same schedule as the Dashboard, Analysis and FIRE (loan payoff): instalment + average overpayment over the last 12 entries; the date = the latest-paid debt. When the planned overpayment is higher than the actual one, a line below says "With the planned overpayment of X zΕ‚/mo it would be around YYYY". When the date falls after the end of the contract β€” both dates. No date when the payment doesn't cover the interest.

Return on investment​

We show three numbers (Analysis β†’ Health):

  • Total gain = current value βˆ’ invested capital (absolute, in zΕ‚).
  • Market return (TWR) β€” time-weighted: a chain of sub-period rates between snapshots with contributions taken out of each period. It answers "how did the market/strategy perform", independently of contribution timing. Showing TWR and MWR side by side is the industry standard: TWR judges the strategy, MWR β€” your actual outcome.
  • Annual return (MWR) = money-weighted (XIRR) β€” the internal rate of return computed from cash flows: the oldest portfolio snapshot is the starting capital, subsequent contributions are added in their respective months, and the current value is the end state. This way the number accounts for how much and when you contributed β€” which the absolute gain alone doesn't capture (e.g. a large contribution just before a rise inflates the % gain but not the annual return). Computed monthly (Newton + bisection); requires at least 2 months of history, otherwise we show "β€”".

Both rates use the month's net flow: contributions βˆ’ withdrawals. A withdrawal is money returned to the investor (a positive flow in MWR, taken out of the period like a contribution in TWR), so the drop in an account's value after a sale doesn't lower the return. Closed accounts stay in the history of both rates β€” their snapshot values and the withdrawal in the month they closed belong to your result.

Value projection​

Future value = target contributions + compounding at the annual rate assumed for the position (e.g. ETF and IKE 8%, EDO 5.5%, ROS and COI 5.25%, PPK 6%). Debt is reduced by planned overpayments (conservatively, ignoring interest).

Crypto is an exception: we don't compound a fixed rate (it's not a deposit). The crypto projection is flat β€” current value + any contributions, with no assumed growth. In the projection table a crypto position shows "current value Β· no forecast" with a variable badge, and the editor has no "target" or "rate of return" fields (crypto is valued from the current rate).

Employer match (US 401(k) employer match). For a 401(k) account, in the editor you can set how much the employer adds to your contribution: a percentage of your monthly contribution (e.g. 50%) and an optional monthly match cap. Then the effective monthly contribution = your own + the match β€” and that is what compounds in the projection (the employer's capital grows too). The match counts as a contribution (capital), not as gain, so it doesn't inflate the result. With no match set, the projection stays unchanged (zero impact on the other accounts).

Tax on withdrawal β€” by account type​

We compute net worth depending on the account type (tax wrapper), not on a fixed basis. We keep the rules as data (limits and multipliers change every year):

  • IKE, OIPE, PPK, PPE β€” a qualified withdrawal is tax-free (net = gross).
  • IKZE β€” 10% of the entire withdrawal (not just the gain). The contribution lowers PIT in the year it is made β€” we show that relief separately (we don't add it to net worth), and only when you provide your PIT rate (we don't guess it).
  • Standard account / bank β€” Belka 19% on the gain (gain = value βˆ’ contributions βˆ’ capital).
  • Physical metals (gold, silver, platinum, palladium) in Poland β€” tax-free after 6 months. Selling movable property more than six months after the end of the month of purchase is not a source of income (art. 10(1)(8)(d) of the PIT Act). The projection (a horizon of at least 2 years) therefore deducts no tax and the badge says "tax-free after 6 months". An earlier sale is taxed on the PIT scale, not the 19% capital-gains tax. Investment gold is also VAT-exempt at purchase (art. 122 of the VAT Act). Outside Poland the badge neutrally says "tax depends on holding period" and the projection stays conservative (tax on the gain). The same rule applies to the after-tax withdrawal rate in FIRE: in Poland physical metals add no tax to the blended rate.

Positions with no assigned account type behave as before (the "taxable" flag = Belka), so the projection of existing plans doesn't change.

Other countries (per-country engine)​

The same engine computes the tax by country (country) β€” you choose it in Settings:

  • πŸ‡©πŸ‡ͺ Germany β€” Depot: Abgeltungsteuer 26.375% on the gain after Teilfreistellung (a portion exempt depending on the instrument) and the Sparerpauschbetrag allowance, plus an optional Kirchensteuer; RΓΌrup/Riester/bAV β€” a deduction on the way in and a taxed portion of the annuity (Besteuerungsanteil).
  • πŸ‡¬πŸ‡§ United Kingdom β€” GIA: CGT 18/24% on the gain after the annual Β£3,000 allowance (a separate Β£500 dividend allowance); ISA/SIPP tax-free; the tax year 6 April–5 April (not the calendar year) affects how the limits are counted.
  • πŸ‡ΊπŸ‡Έ USA β€” brokerage: LTCG 0/15/20% depending on the holding period; Roth vs Traditional as a toggle (tax-free on the way out vs deductible on the way in); 529 maps to a child's education goal.
  • πŸ‡«πŸ‡· France β€” CTO: PFU 31.4% split into the components PS 18.6% + IR 12.8%; PEA (0% IR after 5 years) and assurance-vie (after 8 years) computed by account tenure (opening date); PER β€” a deduction on the way in.

Projections don't fake a precision they don't have (e.g. the Vorabpauschale is explicitly not accounted for) β€” simplifying assumptions are flagged.

Annual contribution limit​

For accounts with a limit (IKE/IKZE/OIPE, DE RΓΌrup/Riester/bAV, UK ISA/SIPP, US 401(k)/IRA/HSA, FR PEA/PER) we track the use of the annual limit β€” the sum of a given person's contributions to accounts of that type in the year against the limit. IKZE has a higher limit for people running a business (JDG). In the UK the "year" is the tax year (6 April). Accounts with no tracked limit (PPK/PPE, GIA, brokerage, CTO…) don't show the bar.

Horizon and grouping​

In Portfolio we show the projection for a single chosen horizon (2/5/10/20 years) instead of four columns β€” no sideways scrolling. By default rows are laid out like the balance sheet (the same groups and order: retirement accounts, investments, bonds…, largest value first within a group). You can also group by person or account type, or turn grouping off.

Loan payoff and overpayment​

Every screen computes a loan from one schedule (since round 6): the Dashboard (goal card), Goals (the "Debt-free" main goal), Analysis (the payoff takeaway and the loan balance in FIRE) and the loan details in Portfolio. Previously each screen had its own assumptions and the same loan gave four different payoff years.

The schedule starts from the current balance and runs month by month: interest is balance Γ— rate / 12, and the monthly payment depends on the basis:

  • Actual (default) β€” the instalment + the average overpayment over the last 12 entries (months without an overpayment count as zero). The instalment comes from the holding, or β€” when it's missing β€” from the average of the instalments recorded in entries.
  • Planned β€” the instalment + the holding's "Target overpayment / mo". We only use it when there are no entries yet (the screen then says "per plan"), or as an explicit what-if: "With the planned overpayment of 4000 zΕ‚/mo it would be around 2034". The plan never replaces the answer based on facts.

number of months n = βˆ’ln(1 βˆ’ balance Γ— r / payment) / ln(1 + r), where r = rate / 12

  • Payoff date = the balance month + n (n rounded up to a whole month). The balance month is the loan's latest snapshot β€” if entries have been on hold for a few months, the date doesn't drift by those months.
  • The balance in any year (FIRE, net-worth projection) comes from the same amortisation: balance Γ— (1 + r)^k βˆ’ payment Γ— ((1 + r)^k βˆ’ 1) / r.
  • "Paid off in" in the loan details is n in years with one decimal place; interest to the end = payment Γ— n βˆ’ balance.
  • When the planned overpayment is higher than the actual one, the loan details show separate rows "With the planned overpayment" and "Interest saved with the planned overpayment", and the plan sentence gets a second, what-if line.
  • Principal in the instalment / mo = instalment βˆ’ this month's interest. Actual overpayments (the average from entries) get their own row, so the two add up to the balance drop in the history.
  • If the payment doesn't even cover the interest (the balance wouldn't fall), we give no date and the plan at the top of the sheet says "The installment doesn't cover the interest β€” the balance isn't going down.".
  • Repaid = the loan's starting amount βˆ’ the current balance (demo: 578,113 βˆ’ 543,542 = 34,571 zΕ‚, 5%). It's the only "repaid" baseline in the sheet; the balance chart shows recent months only.

Contract term vs instalment​

We treat the loan term ("Term (years)") as the original contract term counted from the start of the loan. The start is the holding's opening date, otherwise the first month the loan appears in the history (when it was added to the app). A loan taken 10 years ago with a 25-year term therefore has 15 years left, not 25. Without a known start we compare with the full term counted from today.

End of the contract = start + term. When the date from the schedule falls later than the end of the contract, we don't cap anything β€” the screens state both dates and ask you to check the instalment: "the contract ends in 2049; at the entered instalment it takes until 2053 β€” check the instalment". It's usually a typo in the instalment, the rate or the term. When the start is unknown, the loan details show the shorter hint "The installment doesn't fit the N years term β€” check the installment or the interest rate.".

The goal card on the Dashboard gives the year all debts will be gone (the latest-paid debt). "Debt shrinks by … zΕ‚/mo" is the actual balance drop from the snapshots of the last 12 months (the same number as "principal repaid" in the hero); without history β€” the principal part of this month's payment. A debt with no instalment and no overpayments is projected in a straight line from the balance drop in the snapshots.

Goal progress ("Debt-free") = 1 βˆ’ balance Γ· starting amount, written the same way on the Dashboard, in Goals and in the loan details: whole percent, rounded down below 100% (5.98% β†’ "5%").

Demo example: a 578,113 zΕ‚ loan taken 5 years ago for 30 years, 6.89%, instalment 3,803.59 zΕ‚, entries without overpayments. Balance 543,542 zΕ‚, about 680 zΕ‚ of principal in each instalment. The date from the instalment: October 2051 β€” the same as the end of the contract, so every screen says "around 2051". A planned overpayment of 4,000 zΕ‚/mo would give about 2034 and about 445k zΕ‚ less interest β€” a what-if, not the answer. With the term entered as 25 years the screens say "the contract ends in 2046; at the entered instalment it takes until 2051 β€” check the instalment".

Insights​

The rules generate tips with a severity of good / info / warn: loan payoff, savings-rate trend, a shortfall on dated goals, buffer, debt share, portfolio concentration (and crypto risk), % FIRE.

A shortfall on dated goals uses exactly what the Goals verdict uses: goals with their real "saved" (including goals following a source), the monthly capacity (the amount set in Goals, the range from Entry or the contribution plan), the sum of monthly needs of dated goals and the goal driving the gap. The takeaway says the same sentence: "About 15,700 zΕ‚/mo short for β€œBudowa domu” (until June 2027)", with the step "Consider a later date or a lower amount". Since the 2026-10-14 review this takeaway isn't shown in Analysis or the "Topics to review" panel β€” the amount lives only in Goals, and the Dashboard mentions it in one line without the amount ("Goals: behind pace …"). It still feeds notifications and the buffer step: while such a shortfall exists, the step for a full buffer doesn't send surpluses to investments β€” it says dated goals come first. The same rules power your notifications. The loan payoff date uses the same schedule as the Dashboard, Goals and FIRE (instalment + average overpayment over 12 entries) and is given as a year and in whole years (rounded, at least 1 year): "At your current repayments the loan will be paid off around 2051 β€” in about 25 years.". When the date falls after the end of the contract, the takeaway states both dates and asks you to check the instalment and the term. The "Other takeaways" list at the bottom of Health skips the takeaway that heads the card and the cushion, savings and debt takeaways β€” their numbers are already in the card.

Storage and base currency​

Storage currency and base currency. The plan stores every amount in PLN β€” crypto, gold, ETFs, the tax-free allowance (DE: €1,000 converted to PLN) and positions in euros or dollars (amount in the currency Γ— NBP rate). The household's base currency (PL: PLN, DE: EUR…) is a display and entry layer: the screen divides zlotys by the base-currency rate (PLN per 1 euro β€” the NBP average rate, or the ECB rate converted to PLN without NBP), and the monthly entry form multiplies what you type by the same rate. So changing the country converts nothing in the data and can be undone losslessly. Until a rate is known, amounts are shown in PLN β€” never as unconverted euros. Formatting (separators, "k/M", the "βˆ’" minus) follows the app language, not the currency.

Storage marker. A plan saved in this model carries storageModel: "pln" (new plans and PLN plans get it straight away). A euro plan without it may predate the 2 October 2026 fix, when euro amounts were stored unconverted β€” Settings then asks once: convert manually entered amounts Γ— the current rate (reversibly) or leave them.