From e89de86af0ee5f297cfe4135039f6643ac083aa8 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 21 Jun 2026 20:06:29 +0000 Subject: [PATCH] =?UTF-8?q?wealth=20dashboard:=20spend-down=20table=20?= =?UTF-8?q?=E2=86=92=20three=20growth=20scenarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viktor wanted the spend-down card to compare three portfolio-growth scenarios rather than the previous floor-vs-4%-real pair. The table now has three rows, each a die-with-zero annuity (drain net worth to £0 by age 100) spending a constant number of ACTUAL (nominal) pounds, differing only by the assumed nominal growth rate: • No growth (0%) → £43/day, £1,315/mo, £15,776/yr (= NW ÷ years) • Inflation (3%) → £106/day, £3,233/mo, £38,792/yr (NEW) • Avg market (7%) → £220/day, £6,703/mo, £80,435/yr This keeps the £43 no-growth floor he anchored on. The old third row was "4% real" (£133) expressed in today's money; it's replaced by the 7%-nominal market row (£220, actual pounds) so all three rows share one basis (nominal pounds) and are directly comparable. 3%/7% are hardcoded (one-line SQL edit). Table height 4→5 for the extra row; panels below shifted down 1. Co-Authored-By: Claude Opus 4.8 --- .../modules/monitoring/dashboards/wealth.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/stacks/monitoring/modules/monitoring/dashboards/wealth.json b/stacks/monitoring/modules/monitoring/dashboards/wealth.json index 4a18c4c6..46353fc1 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/wealth.json +++ b/stacks/monitoring/modules/monitoring/dashboards/wealth.json @@ -838,7 +838,7 @@ "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 14 }, "panels": [] }, @@ -855,7 +855,7 @@ "h": 9, "w": 24, "x": 0, - "y": 14 + "y": 15 }, "fieldConfig": { "defaults": { @@ -927,7 +927,7 @@ "h": 9, "w": 12, "x": 0, - "y": 23 + "y": 24 }, "fieldConfig": { "defaults": { @@ -992,7 +992,7 @@ "h": 9, "w": 12, "x": 12, - "y": 23 + "y": 24 }, "fieldConfig": { "defaults": { @@ -1092,7 +1092,7 @@ "h": 1, "w": 24, "x": 0, - "y": 33 + "y": 34 }, "panels": [] }, @@ -1109,7 +1109,7 @@ "h": 10, "w": 24, "x": 0, - "y": 34 + "y": 35 }, "fieldConfig": { "defaults": { @@ -1204,7 +1204,7 @@ "h": 10, "w": 24, "x": 0, - "y": 44 + "y": 45 }, "fieldConfig": { "defaults": { @@ -1309,7 +1309,7 @@ "h": 10, "w": 24, "x": 0, - "y": 54 + "y": 55 }, "fieldConfig": { "defaults": { @@ -1385,7 +1385,7 @@ "h": 1, "w": 24, "x": 0, - "y": 65 + "y": 66 }, "panels": [] }, @@ -1402,7 +1402,7 @@ "h": 10, "w": 24, "x": 0, - "y": 66 + "y": 67 }, "fieldConfig": { "defaults": { @@ -1515,7 +1515,7 @@ "h": 1, "w": 24, "x": 0, - "y": 77 + "y": 78 }, "panels": [] }, @@ -1532,7 +1532,7 @@ "h": 10, "w": 12, "x": 0, - "y": 78 + "y": 79 }, "fieldConfig": { "defaults": { @@ -1729,7 +1729,7 @@ "h": 10, "w": 12, "x": 12, - "y": 78 + "y": 79 }, "fieldConfig": { "defaults": { @@ -1782,7 +1782,7 @@ "h": 1, "w": 24, "x": 0, - "y": 89 + "y": 90 }, "panels": [] }, @@ -1799,7 +1799,7 @@ "h": 9, "w": 12, "x": 0, - "y": 90 + "y": 91 }, "fieldConfig": { "defaults": { @@ -1916,7 +1916,7 @@ "h": 12, "w": 12, "x": 12, - "y": 90 + "y": 91 }, "fieldConfig": { "defaults": { @@ -2135,7 +2135,7 @@ "h": 1, "w": 24, "x": 0, - "y": 103 + "y": 104 }, "panels": [] }, @@ -2152,7 +2152,7 @@ "h": 12, "w": 24, "x": 0, - "y": 104 + "y": 105 }, "fieldConfig": { "defaults": { @@ -2252,14 +2252,14 @@ { "id": 9220, "title": "Spend-down to \u00a30 at age 100", - "description": "How much you can spend to exhaust your net worth (pension included) by your 100th birthday (2098-10-04). FLOOR = treats the money as cash, no growth or inflation \u2014 a conservative lower bound. 4% REAL = die-with-zero annuity assuming the balance keeps earning 4% after inflation: PMT = NW\u00b7r/(1\u2212(1+r)^\u2212n). Computed live, so it drifts as net worth and the horizon move.", + "description": "How much you can spend to exhaust your net worth (pension included) by your 100th birthday (2098-10-04), draining to \u00a30. Three scenarios by how the pot grows: No growth (0%), Inflation (3% nominal), Market (7% nominal \u2014 the dashboard's base return assumption). Each is a die-with-zero annuity (PMT = NW\u00b7r/(1\u2212(1+r)^\u2212n); NW\u00f7years when r=0) spending a constant number of pounds. Figures are ACTUAL (nominal) pounds you'd withdraw \u2014 at higher growth, later pounds buy less. Rates hardcoded (one-line SQL edit to change). Computed live.", "type": "table", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, "gridPos": { - "h": 4, + "h": 5, "w": 9, "x": 0, "y": 9 @@ -2277,7 +2277,7 @@ { "matcher": { "id": "byName", - "options": "Basis" + "options": "Scenario" }, "properties": [ { @@ -2314,7 +2314,7 @@ "rawQuery": true, "editorMode": "code", "format": "table", - "rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value FROM dav_corrected d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC), nw AS (SELECT SUM(total_value) AS pv FROM latest), calc AS (SELECT pv, (DATE '2098-10-04' - CURRENT_DATE)::float8 AS days, (DATE '2098-10-04' - CURRENT_DATE)::float8/365.25 AS years, 0.04::float8 AS r FROM nw), pmt AS (SELECT pv, days, years, r, pv*r/(1-power(1+r,-years)) AS annual FROM calc) SELECT b.label AS \"Basis\", round((CASE b.k WHEN 'floor' THEN pv/years/365.25 ELSE annual/365.25 END)::numeric,0) AS \"Per day\", round((CASE b.k WHEN 'floor' THEN pv/years/12 ELSE annual/12 END)::numeric,0) AS \"Per month\", round((CASE b.k WHEN 'floor' THEN pv/years ELSE annual END)::numeric,0) AS \"Per year\" FROM pmt, (VALUES (1,'floor','Floor'),(2,'real','4% real')) AS b(ord,k,label) ORDER BY b.ord" + "rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value FROM dav_corrected d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC), nw AS (SELECT SUM(total_value) AS pv FROM latest), hz AS (SELECT pv, (DATE '2098-10-04' - CURRENT_DATE)::float8/365.25 AS years FROM nw), scen AS (SELECT ord,label, CASE WHEN rate=0 THEN pv/years ELSE pv*rate/(1-power(1+rate,-years)) END AS annual FROM hz, (VALUES (1,'No growth',0.0::float8),(2,'Inflation (3%)',0.03::float8),(3,'Market (7%)',0.07::float8)) AS s(ord,label,rate)) SELECT label AS \"Scenario\", round((annual/365.25)::numeric,0) AS \"Per day\", round((annual/12)::numeric,0) AS \"Per month\", round(annual::numeric,0) AS \"Per year\" FROM scen ORDER BY ord" } ] }