From 84fd752747bef799776d36b252693a269d2a12fe Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 7 May 2026 22:38:31 +0000 Subject: [PATCH] monitoring(wealth): monthly contributions vs market gain bar chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal stated by user: see when monthly market gain starts to exceed monthly contributions, i.e. the inflection point where the market is out-earning savings rather than the other way around. New panel id=25 between the annual decomposition (13) and per-account ROI (14): bar chart with two side-by-side bars per month -- contributions (blue) and market gain (green). Same calculation as panel 13 but month-grain instead of year-grain. Months where the green bar dwarfs the blue one are visible at a glance. SQL: same endpoints CTE pattern as panel 13, with date_trunc('month', valuation_date) as the grouping key. Uses max_complete cutoff so partial-today doesn't skew the latest month. Layout: panels at y >= 75 shifted down by 11 (chart height). New chart at y=75; panel 14 (per-account ROI) -> y=86; panel 10 (activity log) -> y=96. Spot check (recent months from PG): 2025-07: contrib +£5,601 market +£42,295 <- big market month 2025-09: contrib +£1,501 market +£24,206 2026-02: contrib +£35,501 market +£41,382 2026-03: contrib +£5,501 market -£38,483 <- correction 2026-04: contrib +£73,267 market +£21,448 --- .../modules/monitoring/dashboards/wealth.json | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/stacks/monitoring/modules/monitoring/dashboards/wealth.json b/stacks/monitoring/modules/monitoring/dashboards/wealth.json index 54d25c4b..a44f8037 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/wealth.json +++ b/stacks/monitoring/modules/monitoring/dashboards/wealth.json @@ -728,7 +728,7 @@ "h": 14, "w": 24, "x": 0, - "y": 85 + "y": 96 }, "fieldConfig": { "defaults": { @@ -1702,6 +1702,124 @@ } ] }, + { + "id": 25, + "title": "Monthly contributions vs market gain", + "description": "Each month split into 'new money in' (contributions) and 'market gain' (everything else). Side-by-side bars, so months where market gain exceeds contributions are visually obvious — that's the inflection point where your investments out-earn your savings.", + "type": "barchart", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 75 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "axisPlacement": "auto", + "axisLabel": "", + "fillOpacity": 80, + "gradientMode": "none", + "lineWidth": 1 + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "month" + }, + "properties": [ + { + "id": "unit", + "value": "string" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "contributions" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + }, + { + "id": "displayName", + "value": "Net contributions" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "market_gain" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } + }, + { + "id": "displayName", + "value": "Market gain" + } + ] + } + ] + }, + "options": { + "barRadius": 0, + "barWidth": 0.7, + "groupWidth": 0.75, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "xField": "month", + "xTickLabelRotation": -45, + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "table", + "rawSql": "WITH active_count AS (SELECT COUNT(*) AS n FROM accounts), max_complete AS (SELECT MAX(valuation_date) AS d FROM (SELECT d.valuation_date, COUNT(*) AS c FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), monthly AS (SELECT date_trunc('month', valuation_date)::date AS month, valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM daily_account_valuation WHERE valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date), endpoints AS (SELECT month, (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 monthly GROUP BY month) SELECT month::text AS month, ROUND((contrib_end - contrib_start)::numeric, 0) AS contributions, ROUND((nw_end - nw_start - (contrib_end - contrib_start))::numeric, 0) AS market_gain FROM endpoints ORDER BY month" + } + ] + }, { "id": 14, "title": "Per-account ROI %", @@ -1715,7 +1833,7 @@ "h": 10, "w": 24, "x": 0, - "y": 75 + "y": 86 }, "fieldConfig": { "defaults": {