monitoring: investment-only returns + YoY YTD gross line chart

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.
This commit is contained in:
Viktor Barzin 2026-04-25 23:25:42 +00:00
parent 55d1da41f6
commit 77bed10a51
2 changed files with 20 additions and 58 deletions

View file

@ -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"
}
]
}

View file

@ -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"
}
]
},