infra/stacks/monitoring/modules/monitoring/dashboards/wealth.json
2026-05-02 20:20:18 +00:00

751 lines
34 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {"type": "datasource", "uid": "grafana"},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Wealth — net worth, contributions, and growth over time. Backed by the wealthfolio_sync PG mirror of Wealthfolio's SQLite, refreshed hourly by the pg-sync sidecar.",
"editable": true,
"fiscalYearStartMonth": 0,
"id": null,
"links": [],
"panels": [
{
"id": 1,
"title": "Net worth (current)",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "fixed", "fixedColor": "green"},
"decimals": 2
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value, d.net_contribution, d.cash_balance, d.investment_market_value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC) SELECT SUM(total_value) AS net_worth FROM latest"
}
]
},
{
"id": 2,
"title": "Net contribution (cumulative)",
"description": "Total deposits minus withdrawals across all accounts.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 4, "x": 4, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "fixed", "fixedColor": "blue"},
"decimals": 2
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value, d.net_contribution, d.cash_balance, d.investment_market_value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC) SELECT SUM(net_contribution) AS contribution FROM latest"
}
]
},
{
"id": 3,
"title": "Growth (unrealised)",
"description": "Net worth minus net contribution — the gain on everything you've put in.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 4, "x": 8, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "thresholds"},
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "green", "value": 0}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value, d.net_contribution, d.cash_balance, d.investment_market_value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC) SELECT (SUM(total_value) - SUM(net_contribution)) AS growth FROM latest"
}
]
},
{
"id": 4,
"title": "ROI %",
"description": "Growth / net contribution × 100. Excludes accounts with zero/negative contribution (Schwab) to avoid distortion.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 3, "x": 12, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "percent",
"color": {"mode": "thresholds"},
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 0},
{"color": "green", "value": 5}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.total_value, d.net_contribution, d.cash_balance, d.investment_market_value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC) SELECT (SUM(total_value - net_contribution) / NULLIF(SUM(net_contribution), 0) * 100) AS roi_pct FROM latest WHERE net_contribution > 0"
}
]
},
{
"id": 5,
"title": "Net worth — total over time",
"description": "Daily total_value summed across all accounts (base GBP).",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 4},
"fieldConfig": {
"defaults": {
"color": {"mode": "fixed", "fixedColor": "green"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "net_worth"},
"properties": [{"id": "displayName", "value": "Net worth"}]
}
]
},
"options": {
"legend": {"calcs": ["last", "max"], "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": "time_series",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)) SELECT valuation_date::timestamp AS \"time\", SUM(total_value) AS net_worth FROM daily_account_valuation WHERE $__timeFilter(valuation_date) AND valuation_date IN (SELECT valuation_date FROM complete_dates) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 6,
"title": "Net contribution vs market value",
"description": "Net contribution = cumulative deposits withdrawals. Market value = total_value (cash + investments). Gap between the two = unrealised growth.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 12, "x": 0, "y": 14},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 0,
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "market_value"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}},
{"id": "displayName", "value": "Market value"}
]
},
{
"matcher": {"id": "byName", "options": "net_contribution"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}},
{"id": "displayName", "value": "Net contribution"}
]
}
]
},
"options": {
"legend": {"calcs": ["last"], "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": "time_series",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)) SELECT valuation_date::timestamp AS \"time\", SUM(net_contribution) AS net_contribution, SUM(total_value) AS market_value FROM daily_account_valuation WHERE $__timeFilter(valuation_date) AND valuation_date IN (SELECT valuation_date FROM complete_dates) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 7,
"title": "Growth (market value contribution) over time",
"description": "Unrealised gain across all accounts. Filled area to emphasise the wealth created above the contributed capital.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 12, "x": 12, "y": 14},
"fieldConfig": {
"defaults": {
"color": {"mode": "fixed", "fixedColor": "#56A64B"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 50,
"gradientMode": "opacity",
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "growth"},
"properties": [{"id": "displayName", "value": "Growth"}]
}
]
},
"options": {
"legend": {"calcs": ["last", "max"], "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": "time_series",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)) SELECT valuation_date::timestamp AS \"time\", (SUM(total_value) - SUM(net_contribution)) AS growth FROM daily_account_valuation WHERE $__timeFilter(valuation_date) AND valuation_date IN (SELECT valuation_date FROM complete_dates) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 8,
"title": "Per-account stacked — total value",
"description": "Stacked area showing each account's contribution to total net worth over time. Useful for spotting which account drives the trajectory.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 11, "w": 24, "x": 0, "y": 24},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"drawStyle": "line",
"lineWidth": 1,
"fillOpacity": 70,
"pointSize": 3,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "normal"}
}
},
"overrides": []
},
"options": {
"legend": {"calcs": ["last"], "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": "time_series",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)) SELECT d.valuation_date::timestamp AS \"time\", a.name AS metric, d.total_value AS value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id WHERE $__timeFilter(d.valuation_date) AND d.valuation_date IN (SELECT valuation_date FROM complete_dates) ORDER BY d.valuation_date, a.name"
}
]
},
{
"id": 9,
"title": "Cash vs invested (stacked)",
"description": "Daily breakdown of uninvested broker cash vs market value of investments. WORKPLACE_PENSION accounts (Fidelity) are reclassified entirely as invested — Wealthfolio dumps pension wrappers into cash_balance because it doesn't track the underlying fund holdings, but they are not actually cash.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 35},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"drawStyle": "line",
"lineWidth": 1,
"fillOpacity": 70,
"pointSize": 3,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "normal"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "cash"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#FADE2A"}},
{"id": "displayName", "value": "Cash"}
]
},
{
"matcher": {"id": "byName", "options": "invested"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#56A64B"}},
{"id": "displayName", "value": "Invested"}
]
}
]
},
"options": {
"legend": {"calcs": ["last"], "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": "time_series",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)) SELECT d.valuation_date::timestamp AS \"time\", SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN 0 ELSE d.cash_balance END) AS cash, SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN d.cash_balance + d.investment_market_value ELSE d.investment_market_value END) AS invested FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id WHERE $__timeFilter(d.valuation_date) AND d.valuation_date IN (SELECT valuation_date FROM complete_dates) GROUP BY d.valuation_date ORDER BY d.valuation_date"
}
]
},
{
"id": 10,
"title": "Activity log",
"description": "Recent activities (BUY / SELL / DEPOSIT / WITHDRAWAL / DIVIDEND / etc.) across all accounts. Limited to 100 most recent.",
"type": "table",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 77},
"fieldConfig": {
"defaults": {
"custom": {"align": "auto", "displayMode": "auto"}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "amount"},
"properties": [{"id": "unit", "value": "currencyGBP"}]
}
]
},
"options": {
"cellHeight": "sm",
"footer": {"show": false}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "SELECT a.activity_date AS \"date\", acc.name AS \"account\", a.activity_type AS \"type\", a.asset_id AS \"asset\", a.quantity AS \"qty\", a.unit_price AS \"unit_price\", a.amount AS \"amount\", a.currency AS \"ccy\", a.notes AS \"notes\" FROM activities a LEFT JOIN accounts acc ON acc.id = a.account_id WHERE $__timeFilter(a.activity_date) ORDER BY a.activity_date DESC LIMIT 100"
}
]
},
{
"id": 11,
"title": "12mo 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": 3, "x": 15, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "percent",
"color": {"mode": "thresholds"},
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 0},
{"color": "green", "value": 5}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.valuation_date AS d_now, d.total_value AS nw_now, d.net_contribution AS contrib_now FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC), ago AS (SELECT DISTINCT ON (l.account_id) l.account_id, d.total_value AS nw_ago, d.net_contribution AS contrib_ago FROM latest l JOIN daily_account_valuation d ON d.account_id = l.account_id AND d.valuation_date <= l.d_now - INTERVAL '12 months' ORDER BY l.account_id, d.valuation_date DESC), agg AS (SELECT (SELECT SUM(nw_now) FROM latest) AS nw_now, (SELECT SUM(contrib_now) FROM latest) AS contrib_now, (SELECT SUM(nw_ago) FROM ago) AS nw_ago, (SELECT SUM(contrib_ago) FROM ago) AS contrib_ago) 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": 15,
"title": "12mo contrib",
"description": "Net contributions (deposits withdrawals) over the trailing 12 months. How much new money you put in — independent of market movement.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 3, "x": 18, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "fixed", "fixedColor": "blue"},
"decimals": 2
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.valuation_date AS d_now, d.total_value AS nw_now, d.net_contribution AS contrib_now FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC), ago AS (SELECT DISTINCT ON (l.account_id) l.account_id, d.total_value AS nw_ago, d.net_contribution AS contrib_ago FROM latest l JOIN daily_account_valuation d ON d.account_id = l.account_id AND d.valuation_date <= l.d_now - INTERVAL '12 months' ORDER BY l.account_id, d.valuation_date DESC), agg AS (SELECT (SELECT SUM(nw_now) FROM latest) AS nw_now, (SELECT SUM(contrib_now) FROM latest) AS contrib_now, (SELECT SUM(nw_ago) FROM ago) AS nw_ago, (SELECT SUM(contrib_ago) FROM ago) AS contrib_ago) SELECT (contrib_now - contrib_ago) AS contrib_12mo FROM agg"
}
]
},
{
"id": 16,
"title": "12mo gain",
"description": "Trailing 12-month market gain in £ — the change in net worth minus net contributions. What the markets gave you, separate from money you added in.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 3, "x": 21, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "thresholds"},
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "green", "value": 0}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) d.account_id, d.valuation_date AS d_now, d.total_value AS nw_now, d.net_contribution AS contrib_now FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC), ago AS (SELECT DISTINCT ON (l.account_id) l.account_id, d.total_value AS nw_ago, d.net_contribution AS contrib_ago FROM latest l JOIN daily_account_valuation d ON d.account_id = l.account_id AND d.valuation_date <= l.d_now - INTERVAL '12 months' ORDER BY l.account_id, d.valuation_date DESC), agg AS (SELECT (SELECT SUM(nw_now) FROM latest) AS nw_now, (SELECT SUM(contrib_now) FROM latest) AS contrib_now, (SELECT SUM(nw_ago) FROM ago) AS nw_ago, (SELECT SUM(contrib_ago) FROM ago) AS contrib_ago) SELECT ((nw_now - nw_ago) - (contrib_now - contrib_ago)) AS gain_12mo FROM agg"
}
]
},
{
"id": 12,
"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": 45},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"unit": "percent",
"decimals": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 0},
{"color": "green", "value": 5}
]
},
"custom": {
"axisPlacement": "auto",
"axisLabel": "",
"fillOpacity": 80,
"gradientMode": "none",
"lineWidth": 1
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "year"},
"properties": [
{"id": "unit", "value": "string"}
]
}
]
},
"options": {
"barRadius": 0,
"barWidth": 0.6,
"groupWidth": 0.7,
"orientation": "auto",
"showValue": "always",
"stacking": "none",
"xField": "year",
"xTickLabelRotation": 0,
"legend": {"displayMode": "list", "placement": "bottom"},
"tooltip": {"mode": "single", "sort": "none"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH active_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)), 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 WHERE valuation_date IN (SELECT valuation_date FROM complete_dates) 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"
}
]
},
{
"id": 13,
"title": "Annual change decomposition — contributions vs market gain",
"description": "Each calendar year's net worth change split into 'new money in' (contributions withdrawals) and 'market gain' (everything else: price appreciation, dividends, etc.). Shows whether you grew because you saved or because the market did the work. Negative bars = withdrawals or market losses.",
"type": "barchart",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 11, "w": 24, "x": 0, "y": 56},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"decimals": 2,
"custom": {
"axisPlacement": "auto",
"axisLabel": "",
"fillOpacity": 80,
"gradientMode": "none",
"lineWidth": 1
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "year"},
"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.6,
"groupWidth": 0.7,
"orientation": "auto",
"showValue": "auto",
"stacking": "normal",
"xField": "year",
"xTickLabelRotation": 0,
"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_accounts AS (SELECT COUNT(*) AS n FROM accounts), complete_dates AS (SELECT d.valuation_date FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id GROUP BY d.valuation_date HAVING COUNT(*) >= (SELECT n FROM active_accounts)), 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 WHERE valuation_date IN (SELECT valuation_date FROM complete_dates) 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((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 yr"
}
]
},
{
"id": 14,
"title": "Per-account ROI %",
"description": "(market value net contribution) / net contribution × 100, latest snapshot. Excludes accounts with zero/negative net contribution (Schwab — RSU vests sold = negative contribution distorts the ratio). Pension shows 0% because Wealthfolio doesn't track underlying fund holdings, so cost_basis = 0 and 'growth' is just the cash balance reported.",
"type": "barchart",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 67},
"fieldConfig": {
"defaults": {
"color": {"mode": "thresholds"},
"unit": "percent",
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 0},
{"color": "green", "value": 10}
]
},
"custom": {
"axisPlacement": "auto",
"axisLabel": "",
"fillOpacity": 80,
"gradientMode": "none",
"lineWidth": 1
}
},
"overrides": []
},
"options": {
"barRadius": 0,
"barWidth": 0.6,
"groupWidth": 0.7,
"orientation": "horizontal",
"showValue": "always",
"stacking": "none",
"xField": "account",
"legend": {"displayMode": "list", "placement": "bottom"},
"tooltip": {"mode": "single", "sort": "none"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT DISTINCT ON (d.account_id) a.name, d.total_value, d.net_contribution FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id ORDER BY d.account_id, d.valuation_date DESC) SELECT name AS account, ROUND(((total_value - net_contribution) / NULLIF(net_contribution, 0) * 100)::numeric, 2) AS roi_pct FROM latest WHERE net_contribution > 0 ORDER BY roi_pct DESC"
}
]
}
],
"refresh": "5m",
"schemaVersion": 39,
"tags": ["finance", "personal", "wealth"],
"templating": {"list": []},
"time": {"from": "now-180d", "to": "now"},
"timepicker": {},
"timezone": "browser",
"title": "Wealth",
"uid": "wealth",
"version": 1
}