From 388a7f60c700ae714c042545f8c4e51045de91a4 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 28 May 2026 22:13:44 +0000 Subject: [PATCH] monitoring: add net-pay-vs-market-gains panels to wealth dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new panels comparing employment income to investment returns over time, via Grafana's -- Mixed -- datasource (salary lives in payslip_ingest, portfolio in wealthfolio_sync — separate DBs, so per-target datasources): - cumulative net take-home pay vs cumulative market gain (line race) - net pay vs market gain per year (grouped bars) - net pay vs market gain per month (grouped bars) Inserted after the "Growth over time" panel; existing panels shifted down, full-width tables remain at the bottom. --- .../modules/monitoring/dashboards/wealth.json | 371 +++++++++++++++++- 1 file changed, 361 insertions(+), 10 deletions(-) diff --git a/stacks/monitoring/modules/monitoring/dashboards/wealth.json b/stacks/monitoring/modules/monitoring/dashboards/wealth.json index 4900ea82..6bcbcdbc 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/wealth.json +++ b/stacks/monitoring/modules/monitoring/dashboards/wealth.json @@ -621,7 +621,7 @@ "h": 11, "w": 24, "x": 0, - "y": 40 + "y": 70 }, "fieldConfig": { "defaults": { @@ -686,7 +686,7 @@ "h": 10, "w": 24, "x": 0, - "y": 51 + "y": 81 }, "fieldConfig": { "defaults": { @@ -790,7 +790,7 @@ "h": 14, "w": 24, "x": 0, - "y": 104 + "y": 134 }, "fieldConfig": { "defaults": { @@ -1565,7 +1565,7 @@ "h": 11, "w": 24, "x": 0, - "y": 61 + "y": 91 }, "fieldConfig": { "defaults": { @@ -1659,7 +1659,7 @@ "h": 11, "w": 24, "x": 0, - "y": 72 + "y": 102 }, "fieldConfig": { "defaults": { @@ -1777,7 +1777,7 @@ "h": 11, "w": 24, "x": 0, - "y": 83 + "y": 113 }, "fieldConfig": { "defaults": { @@ -1882,7 +1882,7 @@ "h": 10, "w": 24, "x": 0, - "y": 94 + "y": 124 }, "fieldConfig": { "defaults": { @@ -1962,7 +1962,7 @@ "h": 8, "w": 12, "x": 0, - "y": 32 + "y": 62 }, "fieldConfig": { "defaults": { @@ -2156,7 +2156,7 @@ "uid": "wealth-pg" }, "gridPos": { - "y": 32, + "y": 62, "x": 12, "w": 12, "h": 8 @@ -2273,7 +2273,7 @@ "uid": "wealth-pg" }, "gridPos": { - "y": 118, + "y": 148, "x": 0, "w": 24, "h": 12 @@ -2485,6 +2485,357 @@ "rawSql": "WITH lots AS (SELECT id, activity_date::date AS vest_date, quantity, unit_price AS vest_price, SUM(quantity) OVER (ORDER BY activity_date, id) AS lot_end, SUM(quantity) OVER (ORDER BY activity_date, id) - quantity AS lot_start FROM activities WHERE asset_id='4f60833d-0bfb-484f-8ee6-f129af72e137' AND activity_type='BUY'), sells AS (SELECT activity_date::date AS sell_date, quantity AS sell_qty, unit_price AS sell_price, SUM(quantity) OVER (ORDER BY activity_date, id) AS sell_end, SUM(quantity) OVER (ORDER BY activity_date, id) - quantity AS sell_start FROM activities WHERE asset_id='4f60833d-0bfb-484f-8ee6-f129af72e137' AND activity_type='SELL'), matched AS (SELECT l.vest_date, l.vest_price, s.sell_date, s.sell_price, GREATEST(LEAST(l.lot_end, s.sell_end) - GREATEST(l.lot_start, s.sell_start), 0::numeric) AS qty FROM lots l CROSS JOIN sells s WHERE LEAST(l.lot_end, s.sell_end) > GREATEST(l.lot_start, s.sell_start)) SELECT vest_date, SUM(qty) AS \"shares sold\", (SUM(qty*vest_price)/NULLIF(SUM(qty),0)) AS \"vest price\", SUM(qty*vest_price) AS \"vest value\", (SUM(qty*sell_price)/NULLIF(SUM(qty),0)) AS \"avg sell price\", SUM(qty*sell_price) AS \"sell value\", SUM(qty*(sell_price-vest_price)) AS \"realized PNL\", (SUM(qty*(sell_price-vest_price))/NULLIF(SUM(qty*vest_price),0)*100) AS \"PNL %\", AVG((sell_date-vest_date)) AS \"days held (avg)\" FROM matched GROUP BY vest_date ORDER BY vest_date" } ] + }, + { + "id": 32, + "title": "Net pay earned vs market gains (cumulative)", + "description": "Active vs passive income race. Blue = total take-home pay earned from work (cumulative net_pay, payslip_ingest). Green = total market gains = portfolio value − contributions (cumulative, wealthfolio_sync dav_corrected — matches the 'Growth over time' panel). Set the time range wide (e.g. 2019 → now) to see the full climb from £0.", + "type": "timeseries", + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 32 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 10, + "gradientMode": "opacity", + "pointSize": 5, + "showPoints": "never", + "spanNulls": true, + "axisPlacement": "auto", + "stacking": { + "group": "A", + "mode": "none" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "net_pay_cum" + }, + "properties": [ + { + "id": "displayName", + "value": "Net pay earned (cumulative)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "market_gain_cum" + }, + "properties": [ + { + "id": "displayName", + "value": "Market gains (cumulative)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } + } + ] + } + ] + }, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "payslips-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "rawSql": "WITH m AS (SELECT pay_date, SUM(net_pay) AS net_pay FROM payslip_ingest.payslip GROUP BY pay_date), cum AS (SELECT pay_date, SUM(net_pay) OVER (ORDER BY pay_date) AS net_pay_cum FROM m) SELECT pay_date::timestamp AS \"time\", net_pay_cum FROM cum WHERE $__timeFilter(pay_date) ORDER BY pay_date" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "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 dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)) SELECT valuation_date::timestamp AS \"time\", (SUM(total_value) - SUM(net_contribution)) AS market_gain_cum FROM dav_corrected WHERE $__timeFilter(valuation_date) AND valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date ORDER BY valuation_date" + } + ] + }, + { + "id": 33, + "title": "Net pay vs market gain — per year", + "description": "Each calendar year: take-home pay earned (blue, SUM net_pay) vs market gain generated that year (green, change in portfolio value − contributions across the year). Shows full history regardless of the time picker.", + "type": "timeseries", + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 42 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "bars", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 70, + "gradientMode": "none", + "pointSize": 5, + "showPoints": "never", + "spanNulls": false, + "axisPlacement": "auto", + "stacking": { + "group": "A", + "mode": "none" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "net_pay_year" + }, + "properties": [ + { + "id": "displayName", + "value": "Net pay (year)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "market_gain_year" + }, + "properties": [ + { + "id": "displayName", + "value": "Market gain (year)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } + } + ] + } + ] + }, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "payslips-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "rawSql": "SELECT date_trunc('year', pay_date)::timestamp AS \"time\", SUM(net_pay) AS net_pay_year FROM payslip_ingest.payslip GROUP BY 1 ORDER BY 1" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "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 dav_corrected d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date) x WHERE c >= (SELECT n FROM active_count)), yearly AS (SELECT date_trunc('year', valuation_date)::date AS yr, valuation_date, SUM(total_value) AS nw, SUM(net_contribution) AS contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM max_complete) 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::timestamp AS \"time\", ROUND((nw_end - nw_start - (contrib_end - contrib_start))::numeric, 0) AS market_gain_year FROM endpoints ORDER BY yr" + } + ] + }, + { + "id": 34, + "title": "Net pay vs market gain — per month", + "description": "Each month: take-home pay (blue, SUM net_pay) vs market gain that month (green, change in portfolio value − contributions). Monthly market swings are volatile — the yearly panel above is the smoother read. Shows full history regardless of the time picker.", + "type": "timeseries", + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 52 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "bars", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 70, + "gradientMode": "none", + "pointSize": 5, + "showPoints": "never", + "spanNulls": false, + "axisPlacement": "auto", + "stacking": { + "group": "A", + "mode": "none" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "net_pay_month" + }, + "properties": [ + { + "id": "displayName", + "value": "Net pay (month)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "market_gain_month" + }, + "properties": [ + { + "id": "displayName", + "value": "Market gain (month)" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } + } + ] + } + ] + }, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "payslips-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "rawSql": "SELECT date_trunc('month', pay_date)::timestamp AS \"time\", SUM(net_pay) AS net_pay_month FROM payslip_ingest.payslip GROUP BY 1 ORDER BY 1" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "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 dav_corrected 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 dav_corrected 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::timestamp AS \"time\", ROUND((nw_end - nw_start - (contrib_end - contrib_start))::numeric, 0) AS market_gain_month FROM endpoints ORDER BY month" + } + ] } ], "refresh": "5m",