751 lines
34 KiB
JSON
751 lines
34 KiB
JSON
{
|
||
"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
|
||
}
|