From 77bed10a51fe69e84ce9a39d9e407a5b6814a31f Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 25 Apr 2026 23:25:42 +0000 Subject: [PATCH] monitoring: investment-only returns + YoY YTD gross line chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wealth dashboard: - "Yearly growth %" → "Yearly investment return %": switched to modified-Dietz formula `market_gain / (nw_start + 0.5 × contributions)` so contributions don't inflate the return. New money in is excluded — this is portfolio performance, not net-worth change. - "Trailing 12-month growth %" → "Trailing 12-month investment return %": same formula, applied to the trailing 12mo window. Pre-fix vs post-fix: 2020: 155.0% → 5.12% (large contributions on small base) 2021: 344.7% → 26.45% 2022: 26.9% → -25.65% (the actual 2022 bear market) 2023: 123.2% → 41.60% 2024: 87.4% → 25.70% 2025: 46.8% → 8.43% 2026: 16.7% → 3.28% (YTD) UK Payslip dashboard: - Replaced the per-tax-year stacked bar with a year-over-year line chart: one line per tax year, X = month-of-tax-year (April→March, projected onto a 1970/71 fiscal calendar so years overlay), Y = cumulative YTD gross. Five+ lines visible at a glance for trend comparison. --- .../monitoring/dashboards/uk-payslip.json | 66 ++++--------------- .../modules/monitoring/dashboards/wealth.json | 12 ++-- 2 files changed, 20 insertions(+), 58 deletions(-) diff --git a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json index c96080e7..5bd89a58 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json +++ b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json @@ -2538,9 +2538,9 @@ }, { "id": 17, - "title": "Gross composition by tax year — salary / bonus / RSU / other", - "description": "Per-tax-year stacked bar of gross pay broken into earned components: salary, bonus, RSU vest value, and other (overtime, benefits-in-kind, etc.). Bar height = total gross. Compare year-over-year trends in base salary growth, bonus levels, and RSU vest sizing. Always shows all years — ignores the time picker.", - "type": "barchart", + "title": "YTD gross salary — year-over-year comparison", + "description": "Cumulative gross pay built up month by month within each UK tax year (April → March). One line per tax year. Pay dates are projected onto a 1970/71 fiscal calendar so years overlay cleanly — the X-axis shows month-of-tax-year (April first, March last). Always shows all years; ignores the time picker.", + "type": "timeseries", "datasource": { "type": "grafana-postgresql-datasource", "uid": "payslips-pg" @@ -2557,58 +2557,20 @@ "unit": "currencyGBP", "decimals": 0, "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": true, "axisPlacement": "auto", - "axisLabel": "", - "fillOpacity": 80, - "gradientMode": "none", - "lineWidth": 1 + "stacking": {"group": "A", "mode": "none"} } }, - "overrides": [ - { - "matcher": {"id": "byName", "options": "salary"}, - "properties": [ - {"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}, - {"id": "displayName", "value": "Salary"} - ] - }, - { - "matcher": {"id": "byName", "options": "bonus"}, - "properties": [ - {"id": "color", "value": {"mode": "fixed", "fixedColor": "#FADE2A"}}, - {"id": "displayName", "value": "Bonus"} - ] - }, - { - "matcher": {"id": "byName", "options": "rsu"}, - "properties": [ - {"id": "color", "value": {"mode": "fixed", "fixedColor": "#3274D9"}}, - {"id": "displayName", "value": "RSU vest"} - ] - }, - { - "matcher": {"id": "byName", "options": "other"}, - "properties": [ - {"id": "color", "value": {"mode": "fixed", "fixedColor": "#888888"}}, - {"id": "displayName", "value": "Other"} - ] - } - ] + "overrides": [] }, "options": { - "barRadius": 0, - "barWidth": 0.6, - "groupWidth": 0.7, - "orientation": "auto", - "showValue": "auto", - "stacking": "normal", - "xField": "tax_year", - "xTickLabelRotation": 0, - "legend": { - "calcs": ["sum"], - "displayMode": "table", - "placement": "bottom" - }, + "legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "bottom"}, "tooltip": {"mode": "multi", "sort": "desc"} }, "targets": [ @@ -2620,8 +2582,8 @@ }, "rawQuery": true, "editorMode": "code", - "format": "table", - "rawSql": "SELECT tax_year, SUM(salary) AS salary, SUM(bonus) AS bonus, SUM(rsu_vest) AS rsu, SUM(GREATEST(gross_pay - salary - bonus - rsu_vest, 0)) AS other FROM payslip_ingest.payslip GROUP BY tax_year ORDER BY tax_year" + "format": "time_series", + "rawSql": "SELECT (DATE '1970-04-06' + (pay_date - MAKE_DATE(SUBSTRING(tax_year, 1, 4)::int, 4, 6)))::timestamp AS \"time\", tax_year AS metric, SUM(gross_pay) OVER (PARTITION BY tax_year ORDER BY pay_date) AS ytd_gross FROM payslip_ingest.payslip ORDER BY tax_year, pay_date" } ] } diff --git a/stacks/monitoring/modules/monitoring/dashboards/wealth.json b/stacks/monitoring/modules/monitoring/dashboards/wealth.json index e68b3143..fd969702 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/wealth.json +++ b/stacks/monitoring/modules/monitoring/dashboards/wealth.json @@ -439,8 +439,8 @@ }, { "id": 11, - "title": "Trailing 12-month growth %", - "description": "% change in net worth over the trailing 12 months. Captures market momentum + new contributions combined.", + "title": "Trailing 12-month investment return %", + "description": "Modified-Dietz return over the trailing 12 months: market_gain / (nw_12mo_ago + 0.5 × contributions_12mo). Excludes new money in — answers 'how did my investments perform' rather than 'how much did my net worth change'.", "type": "stat", "datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"}, "gridPos": {"h": 4, "w": 24, "x": 0, "y": 59}, @@ -475,14 +475,14 @@ "rawQuery": true, "editorMode": "code", "format": "table", - "rawSql": "WITH t12 AS (SELECT (SELECT SUM(total_value) FROM daily_account_valuation WHERE valuation_date = (SELECT MAX(valuation_date) FROM daily_account_valuation)) AS now_nw, (SELECT SUM(total_value) FROM daily_account_valuation WHERE valuation_date = (SELECT MIN(valuation_date) FROM daily_account_valuation WHERE valuation_date >= (SELECT MAX(valuation_date) - INTERVAL '12 months' FROM daily_account_valuation))) AS yr_ago_nw) SELECT ROUND(((now_nw - yr_ago_nw) / NULLIF(yr_ago_nw, 0) * 100)::numeric, 2) AS pct_12mo FROM t12" + "rawSql": "WITH bounds AS (SELECT (SELECT MAX(valuation_date) FROM daily_account_valuation) AS d_now, (SELECT MIN(valuation_date) FROM daily_account_valuation WHERE valuation_date >= (SELECT MAX(valuation_date) - INTERVAL '12 months' FROM daily_account_valuation)) AS d_ago), agg AS (SELECT (SELECT SUM(total_value) FROM daily_account_valuation WHERE valuation_date = b.d_now) AS nw_now, (SELECT SUM(net_contribution) FROM daily_account_valuation WHERE valuation_date = b.d_now) AS contrib_now, (SELECT SUM(total_value) FROM daily_account_valuation WHERE valuation_date = b.d_ago) AS nw_ago, (SELECT SUM(net_contribution) FROM daily_account_valuation WHERE valuation_date = b.d_ago) AS contrib_ago FROM bounds b) SELECT ROUND((((nw_now - nw_ago - (contrib_now - contrib_ago)) / NULLIF(nw_ago + 0.5 * (contrib_now - contrib_ago), 0)) * 100)::numeric, 2) AS pct_12mo FROM agg" } ] }, { "id": 12, - "title": "Yearly growth %", - "description": "% change in net worth from the first to last valuation in each calendar year. Includes both market gains and new contributions — see Panel 13 for the decomposition.", + "title": "Yearly investment return %", + "description": "Modified-Dietz return per calendar year: market_gain / (nw_start + 0.5 × contributions). Pure investment performance — excludes new contributions, so a £100k vest doesn't show as 100% growth. Negative bars = market losses (e.g., 2022 bear market).", "type": "barchart", "datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"}, "gridPos": {"h": 11, "w": 24, "x": 0, "y": 63}, @@ -528,7 +528,7 @@ "rawQuery": true, "editorMode": "code", "format": "table", - "rawSql": "WITH yearly AS (SELECT EXTRACT(YEAR FROM valuation_date)::int AS yr, valuation_date, SUM(total_value) AS nw FROM daily_account_valuation GROUP BY valuation_date), endpoints AS (SELECT yr, (array_agg(nw ORDER BY valuation_date ASC))[1] AS nw_start, (array_agg(nw ORDER BY valuation_date DESC))[1] AS nw_end FROM yearly GROUP BY yr) SELECT yr::text AS year, ROUND(((nw_end - nw_start) / NULLIF(nw_start, 0) * 100)::numeric, 2) AS growth_pct FROM endpoints WHERE nw_start > 0 ORDER BY yr" + "rawSql": "WITH yearly AS (SELECT EXTRACT(YEAR FROM valuation_date)::int AS yr, valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM daily_account_valuation GROUP BY valuation_date), endpoints AS (SELECT yr, (array_agg(nw ORDER BY valuation_date ASC))[1] AS nw_start, (array_agg(nw ORDER BY valuation_date DESC))[1] AS nw_end, (array_agg(contrib ORDER BY valuation_date ASC))[1] AS contrib_start, (array_agg(contrib ORDER BY valuation_date DESC))[1] AS contrib_end FROM yearly GROUP BY yr) SELECT yr::text AS year, ROUND((((nw_end - nw_start - (contrib_end - contrib_start)) / NULLIF(nw_start + 0.5 * (contrib_end - contrib_start), 0)) * 100)::numeric, 2) AS return_pct FROM endpoints WHERE (nw_start + 0.5 * (contrib_end - contrib_start)) > 0 ORDER BY yr" } ] },