diff --git a/stacks/monitoring/modules/monitoring/dashboards/wealth.json b/stacks/monitoring/modules/monitoring/dashboards/wealth.json index c64a44ef..6d26a63d 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/wealth.json +++ b/stacks/monitoring/modules/monitoring/dashboards/wealth.json @@ -305,7 +305,7 @@ }, "gridPos": { "h": 4, - "w": 5, + "w": 4, "x": 0, "y": 5 }, @@ -398,8 +398,8 @@ }, "gridPos": { "h": 4, - "w": 5, - "x": 5, + "w": 4, + "x": 4, "y": 5 }, "fieldConfig": { @@ -491,8 +491,8 @@ }, "gridPos": { "h": 4, - "w": 5, - "x": 10, + "w": 4, + "x": 8, "y": 5 }, "fieldConfig": { @@ -584,8 +584,8 @@ }, "gridPos": { "h": 4, - "w": 5, - "x": 15, + "w": 4, + "x": 12, "y": 5 }, "fieldConfig": { @@ -678,7 +678,7 @@ "gridPos": { "h": 4, "w": 4, - "x": 20, + "x": 16, "y": 5 }, "fieldConfig": { @@ -770,9 +770,9 @@ }, "gridPos": { "h": 4, - "w": 6, - "x": 0, - "y": 9 + "w": 4, + "x": 20, + "y": 5 }, "fieldConfig": { "defaults": { @@ -832,1426 +832,1420 @@ { "type": "row", "title": "Net worth over time", - "collapsed": true, + "collapsed": false, "id": 9301, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 13 + "y": 9 }, - "panels": [ + "panels": [] + }, + { + "id": 9202, + "title": "Net worth: contribution vs market value (+ growth)", + "description": "Merged: net contribution + market value (net worth) lines; Growth = the gap between them.", + "type": "timeseries", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 10 + }, + "fieldConfig": { + "defaults": { + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Growth" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 15 + }, + { + "id": "custom.lineWidth", + "value": 1 + } + ] + } + ] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "calcs": [ + "last" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ { - "id": 9202, - "title": "Net worth: contribution vs market value (+ growth)", - "description": "Merged: net contribution + market value (net worth) lines; Growth = the gap between them.", - "type": "timeseries", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 9, - "w": 24, - "x": 0, - "y": 10 + "format": "time_series", + "editorMode": "code", + "rawQuery": true, + "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(net_contribution) AS \"Net contribution\", SUM(total_value) AS \"Market value (net worth)\", (SUM(total_value)-SUM(net_contribution)) AS \"Growth\" FROM dav_corrected WHERE $__timeFilter(valuation_date) AND valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date ORDER BY valuation_date" + } + ] + }, + { + "id": 8, + "title": "Per-account stacked \u2014 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": 9, + "w": 12, + "x": 0, + "y": 19 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "fieldConfig": { - "defaults": { - "unit": "currencyGBP", - "decimals": 0, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 0, - "showPoints": "never" - } + "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_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)) 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 <= (SELECT d FROM max_complete) 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 \u2014 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": 9, + "w": 12, + "x": 12, + "y": 19 + }, + "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" }, - "overrides": [ + "properties": [ { - "matcher": { - "id": "byName", - "options": "Growth" - }, - "properties": [ - { - "id": "custom.fillOpacity", - "value": 15 - }, - { - "id": "custom.lineWidth", - "value": 1 - } - ] + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#FADE2A" + } + }, + { + "id": "displayName", + "value": "Cash" } ] }, - "options": { - "legend": { - "displayMode": "list", - "placement": "bottom", - "calcs": [ - "last" - ] + { + "matcher": { + "id": "byName", + "options": "invested" }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "format": "time_series", - "editorMode": "code", - "rawQuery": true, - "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(net_contribution) AS \"Net contribution\", SUM(total_value) AS \"Market value (net worth)\", (SUM(total_value)-SUM(net_contribution)) AS \"Growth\" FROM dav_corrected WHERE $__timeFilter(valuation_date) AND valuation_date <= (SELECT d FROM max_complete) GROUP BY valuation_date ORDER BY valuation_date" - } - ] - }, - { - "id": 8, - "title": "Per-account stacked \u2014 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": 9, - "w": 12, - "x": 0, - "y": 19 - }, - "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_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)) 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 <= (SELECT d FROM max_complete) 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 \u2014 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": 9, - "w": 12, - "x": 12, - "y": 19 - }, - "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": [ + "properties": [ { - "matcher": { - "id": "byName", - "options": "cash" - }, - "properties": [ - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "#FADE2A" - } - }, - { - "id": "displayName", - "value": "Cash" - } - ] + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } }, { - "matcher": { - "id": "byName", - "options": "invested" - }, - "properties": [ - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "#56A64B" - } - }, - { - "id": "displayName", - "value": "Invested" - } - ] + "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" }, - "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_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)) 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 <= (SELECT d FROM max_complete) GROUP BY d.valuation_date ORDER BY d.valuation_date" - } - ] + "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 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)) 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 <= (SELECT d FROM max_complete) GROUP BY d.valuation_date ORDER BY d.valuation_date" } ] }, { "type": "row", "title": "Returns & contributions", - "collapsed": true, + "collapsed": false, "id": 9302, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 33 + "y": 29 }, - "panels": [ - { - "id": 9203, - "title": "Yearly: contributions / market gain / return %", - "description": "Merged yearly view. Contributions + market gain as bars (left \u00a3 axis); return % as a line (right % axis). timeFrom=10y so it always shows full history.", - "type": "timeseries", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 30 - }, - "fieldConfig": { - "defaults": { - "unit": "currencyGBP", - "decimals": 0, - "custom": { - "drawStyle": "bars", - "fillOpacity": 80, - "lineWidth": 1 - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Return %" - }, - "properties": [ - { - "id": "custom.drawStyle", - "value": "line" - }, - { - "id": "custom.lineWidth", - "value": 2 - }, - { - "id": "custom.fillOpacity", - "value": 0 - }, - { - "id": "unit", - "value": "percent" - }, - { - "id": "custom.axisPlacement", - "value": "right" - }, - { - "id": "custom.axisLabel", - "value": "Return %" - }, - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "yellow" - } - } - ] - } - ] - }, - "options": { - "legend": { - "displayMode": "list", - "placement": "bottom", - "calcs": [ - "last" - ] - }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "format": "time_series", - "editorMode": "code", - "rawQuery": true, - "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c>=(SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date<=(SELECT d FROM mc) GROUP BY valuation_date), ye AS (SELECT DISTINCT ON (date_trunc('year',valuation_date)) date_trunc('year',valuation_date)::date yr, nw, contrib FROM daily ORDER BY date_trunc('year',valuation_date), valuation_date DESC), dd AS (SELECT yr, nw, contrib, lag(nw) OVER (ORDER BY yr) pnw, lag(contrib) OVER (ORDER BY yr) pc FROM ye) SELECT yr::timestamp AS \"time\", round((contrib-pc)::numeric,0) AS \"Contributions\", round(((nw-pnw)-(contrib-pc))::numeric,0) AS \"Market gain\", round((((nw-pnw)-(contrib-pc))/NULLIF(pnw+0.5*(contrib-pc),0)*100)::numeric,2) AS \"Return %\" FROM dd WHERE pnw IS NOT NULL ORDER BY yr" - } - ], - "timeFrom": "10y" + "panels": [] + }, + { + "id": 9203, + "title": "Yearly: contributions / market gain / return %", + "description": "Merged yearly view. Contributions + market gain as bars (left \u00a3 axis); return % as a line (right % axis). timeFrom=10y so it always shows full history.", + "type": "timeseries", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 30 + }, + "fieldConfig": { + "defaults": { + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "bars", + "fillOpacity": 80, + "lineWidth": 1 + } }, - { - "id": 25, - "title": "Monthly contributions vs market gain", - "description": "Each month's net contributions vs market gain as two lines. Where the green (market) line crosses above the blue (contributions) line is when investments out-earn savings for that month. Months below zero on the green line = market drawdowns.", - "type": "timeseries", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 40 - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Return %" + }, + "properties": [ + { + "id": "custom.drawStyle", + "value": "line" }, - "unit": "currencyGBP", - "decimals": 0, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 0, - "pointSize": 5, - "showPoints": "auto", - "spanNulls": true, - "axisPlacement": "auto", - "stacking": { - "group": "A", - "mode": "none" + { + "id": "custom.lineWidth", + "value": 2 + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "unit", + "value": "percent" + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "Return %" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "yellow" } } - }, - "overrides": [ - { - "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": { - "legend": { - "calcs": [ - "last", - "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": "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)), daily AS (SELECT 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), month_end AS (SELECT DISTINCT ON (date_trunc('month', valuation_date)) date_trunc('month', valuation_date)::date AS month, nw, contrib FROM daily ORDER BY date_trunc('month', valuation_date), valuation_date DESC), deltas AS (SELECT month, nw, contrib, lag(nw) OVER (ORDER BY month) AS prev_nw, lag(contrib) OVER (ORDER BY month) AS prev_contrib FROM month_end) SELECT month::timestamp AS time, ROUND((contrib - prev_contrib)::numeric, 0) AS contributions, ROUND(((nw - prev_nw) - (contrib - prev_contrib))::numeric, 0) AS market_gain FROM deltas WHERE prev_nw IS NOT NULL ORDER BY month" - } + } + ] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "calcs": [ + "last" ] }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ { - "id": 14, - "title": "Per-account ROI %", - "description": "(market value \u2212 net contribution) / net contribution \u00d7 100, latest snapshot. Excludes accounts with zero/negative net contribution (Schwab \u2014 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", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 50 + "format": "time_series", + "editorMode": "code", + "rawQuery": true, + "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c>=(SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date<=(SELECT d FROM mc) GROUP BY valuation_date), ye AS (SELECT DISTINCT ON (date_trunc('year',valuation_date)) date_trunc('year',valuation_date)::date yr, nw, contrib FROM daily ORDER BY date_trunc('year',valuation_date), valuation_date DESC), dd AS (SELECT yr, nw, contrib, lag(nw) OVER (ORDER BY yr) pnw, lag(contrib) OVER (ORDER BY yr) pc FROM ye) SELECT yr::timestamp AS \"time\", round((contrib-pc)::numeric,0) AS \"Contributions\", round(((nw-pnw)-(contrib-pc))::numeric,0) AS \"Market gain\", round((((nw-pnw)-(contrib-pc))/NULLIF(pnw+0.5*(contrib-pc),0)*100)::numeric,2) AS \"Return %\" FROM dd WHERE pnw IS NOT NULL ORDER BY yr" + } + ], + "timeFrom": "10y" + }, + { + "id": 25, + "title": "Monthly contributions vs market gain", + "description": "Each month's net contributions vs market gain as two lines. Where the green (market) line crosses above the blue (contributions) line is when investments out-earn savings for that month. Months below zero on the green line = market drawdowns.", + "type": "timeseries", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 40 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "pointSize": 5, + "showPoints": "auto", + "spanNulls": true, + "axisPlacement": "auto", + "stacking": { + "group": "A", + "mode": "none" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "contributions" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } }, - "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 + { + "id": "displayName", + "value": "Net contributions" } - }, - "overrides": [] + ] }, - "options": { - "barRadius": 0, - "barWidth": 0.6, - "groupWidth": 0.7, - "orientation": "horizontal", - "showValue": "always", - "stacking": "none", - "xField": "account", - "legend": { - "displayMode": "list", - "placement": "bottom" + { + "matcher": { + "id": "byName", + "options": "market_gain" }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#56A64B" + } }, - "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 dav_corrected 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" - } - ] + { + "id": "displayName", + "value": "Market gain" + } + ] + } + ] + }, + "options": { + "legend": { + "calcs": [ + "last", + "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": "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)), daily AS (SELECT 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), month_end AS (SELECT DISTINCT ON (date_trunc('month', valuation_date)) date_trunc('month', valuation_date)::date AS month, nw, contrib FROM daily ORDER BY date_trunc('month', valuation_date), valuation_date DESC), deltas AS (SELECT month, nw, contrib, lag(nw) OVER (ORDER BY month) AS prev_nw, lag(contrib) OVER (ORDER BY month) AS prev_contrib FROM month_end) SELECT month::timestamp AS time, ROUND((contrib - prev_contrib)::numeric, 0) AS contributions, ROUND(((nw - prev_nw) - (contrib - prev_contrib))::numeric, 0) AS market_gain FROM deltas WHERE prev_nw IS NOT NULL ORDER BY month" + } + ] + }, + { + "id": 14, + "title": "Per-account ROI %", + "description": "(market value \u2212 net contribution) / net contribution \u00d7 100, latest snapshot. Excludes accounts with zero/negative net contribution (Schwab \u2014 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": 50 + }, + "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 dav_corrected 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" } ] }, { "type": "row", "title": "Income vs market", - "collapsed": true, + "collapsed": false, "id": 9303, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 65 + "y": 61 }, - "panels": [ - { - "id": 32, - "title": "Net pay vs market gain (${grain})", - "description": "Active vs passive income. Net pay (payslips) vs market gain (portfolio). $grain = cumulative / yearly / monthly.", - "type": "timeseries", - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" + "panels": [] + }, + { + "id": 32, + "title": "Net pay vs market gain (${grain})", + "description": "Active vs passive income. Net pay (payslips) vs market gain (portfolio). $grain = cumulative / yearly / monthly.", + "type": "timeseries", + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 62 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 62 - }, - "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" - } - } + "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" }, - "overrides": [ + "properties": [ { - "matcher": { - "id": "byName", - "options": "net_pay_cum" - }, - "properties": [ - { - "id": "displayName", - "value": "Net pay earned (cumulative)" - }, - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "blue" - } - } - ] + "id": "displayName", + "value": "Net pay earned (cumulative)" }, { - "matcher": { - "id": "byName", - "options": "market_gain_cum" - }, - "properties": [ - { - "id": "displayName", - "value": "Market gains (cumulative)" - }, - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "#56A64B" - } - } - ] + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } } ] }, - "options": { - "legend": { - "calcs": [ - "last", - "max" - ], - "displayMode": "table", - "placement": "bottom" + { + "matcher": { + "id": "byName", + "options": "market_gain_cum" }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } + "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" }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "format": "time_series", - "editorMode": "code", - "rawQuery": true, - "rawSql": "WITH p AS (SELECT date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, pay_date) bucket, SUM(net_pay) np FROM payslip_ingest.payslip GROUP BY 1) SELECT bucket::timestamp AS \"time\", CASE WHEN '$grain'='cumulative' THEN SUM(np) OVER (ORDER BY bucket) ELSE np END AS \"Net pay\" FROM p ORDER BY bucket" - }, - { - "refId": "B", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "format": "time_series", - "editorMode": "code", - "rawQuery": true, - "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c>=(SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date<=(SELECT d FROM mc) GROUP BY valuation_date), pe AS (SELECT DISTINCT ON (date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date)) date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date)::date bucket, nw, contrib FROM daily ORDER BY date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date), valuation_date DESC), dd AS (SELECT bucket, (nw-lag(nw) OVER (ORDER BY bucket))-(contrib-lag(contrib) OVER (ORDER BY bucket)) mg FROM pe) SELECT bucket::timestamp AS \"time\", CASE WHEN '$grain'='cumulative' THEN SUM(mg) OVER (ORDER BY bucket) ELSE mg END AS \"Market gain\" FROM dd WHERE mg IS NOT NULL ORDER BY bucket" - } - ] + "format": "time_series", + "editorMode": "code", + "rawQuery": true, + "rawSql": "WITH p AS (SELECT date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, pay_date) bucket, SUM(net_pay) np FROM payslip_ingest.payslip GROUP BY 1) SELECT bucket::timestamp AS \"time\", CASE WHEN '$grain'='cumulative' THEN SUM(np) OVER (ORDER BY bucket) ELSE np END AS \"Net pay\" FROM p ORDER BY bucket" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "format": "time_series", + "editorMode": "code", + "rawQuery": true, + "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c>=(SELECT n FROM active_count)), daily AS (SELECT valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date<=(SELECT d FROM mc) GROUP BY valuation_date), pe AS (SELECT DISTINCT ON (date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date)) date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date)::date bucket, nw, contrib FROM daily ORDER BY date_trunc(CASE '$grain' WHEN 'yearly' THEN 'year' ELSE 'month' END, valuation_date), valuation_date DESC), dd AS (SELECT bucket, (nw-lag(nw) OVER (ORDER BY bucket))-(contrib-lag(contrib) OVER (ORDER BY bucket)) mg FROM pe) SELECT bucket::timestamp AS \"time\", CASE WHEN '$grain'='cumulative' THEN SUM(mg) OVER (ORDER BY bucket) ELSE mg END AS \"Market gain\" FROM dd WHERE mg IS NOT NULL ORDER BY bucket" } ] }, { "type": "row", "title": "Holdings", - "collapsed": true, + "collapsed": false, "id": 9304, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 77 + "y": 73 }, - "panels": [ - { - "id": 26, - "title": "Positions", - "description": "Currently-held positions: shares, cost basis, latest market price, and unrealised return. Latest holdings_snapshots TOTAL aggregate + latest quote per asset.", - "type": "table", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "gridPos": { - "h": 10, - "w": 12, - "x": 0, - "y": 74 - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "displayMode": "auto" - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "shares" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "avg cost" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyGBP" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "last" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyGBP" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "market value" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyGBP" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "cost" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyGBP" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "gain" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyGBP" - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 0 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "return %" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "percent" - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 0 - } - ] - } - } - ] - } - ] - }, - "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.symbol, a.name, p.quantity AS shares, p.average_cost AS \"avg cost\", q.close AS \"last\", (p.quantity * q.close) AS \"market value\", p.total_cost_basis AS cost, ((p.quantity * q.close) - p.total_cost_basis) AS gain, CASE WHEN p.total_cost_basis > 0 THEN ((p.quantity * q.close) / p.total_cost_basis - 1) * 100 END AS \"return %\", p.currency, q.day AS \"as of\" FROM positions_latest p LEFT JOIN assets a ON a.id = p.asset_id LEFT JOIN quote_latest q ON q.asset_id = p.asset_id ORDER BY (p.quantity * q.close) DESC NULLS LAST" - } - ] + "panels": [] + }, + { + "id": 26, + "title": "Positions", + "description": "Currently-held positions: shares, cost basis, latest market price, and unrealised return. Latest holdings_snapshots TOTAL aggregate + latest quote per asset.", + "type": "table", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 74 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto" + } }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "shares" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "avg cost" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyGBP" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "last" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyGBP" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "market value" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyGBP" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cost" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyGBP" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gain" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyGBP" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "return %" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "percent" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + } + } + ] + } + ] + }, + "options": { + "cellHeight": "sm", + "footer": { + "show": false + } + }, + "targets": [ { - "id": 10, - "title": "Activity log", - "description": "Recent activities (BUY / SELL / DEPOSIT / WITHDRAWAL / DIVIDEND / etc.) across all accounts. Limited to 100 most recent.", - "type": "table", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 10, - "w": 12, - "x": 12, - "y": 74 - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "displayMode": "auto" - } + "rawQuery": true, + "editorMode": "code", + "format": "table", + "rawSql": "SELECT a.symbol, a.name, p.quantity AS shares, p.average_cost AS \"avg cost\", q.close AS \"last\", (p.quantity * q.close) AS \"market value\", p.total_cost_basis AS cost, ((p.quantity * q.close) - p.total_cost_basis) AS gain, CASE WHEN p.total_cost_basis > 0 THEN ((p.quantity * q.close) / p.total_cost_basis - 1) * 100 END AS \"return %\", p.currency, q.day AS \"as of\" FROM positions_latest p LEFT JOIN assets a ON a.id = p.asset_id LEFT JOIN quote_latest q ON q.asset_id = p.asset_id ORDER BY (p.quantity * q.close) DESC NULLS LAST" + } + ] + }, + { + "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": 10, + "w": 12, + "x": 12, + "y": 74 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "amount" }, - "overrides": [ + "properties": [ { - "matcher": { - "id": "byName", - "options": "amount" - }, - "properties": [ - { - "id": "unit", - "value": "currencyGBP" - } - ] + "id": "unit", + "value": "currencyGBP" } ] + } + ] + }, + "options": { + "cellHeight": "sm", + "footer": { + "show": false + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" }, - "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" - } - ] + "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" } ] }, { "type": "row", "title": "RSUs (META)", - "collapsed": true, + "collapsed": false, "id": 9305, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 89 + "y": 85 }, - "panels": [ + "panels": [] + }, + { + "id": 30, + "title": "META vest cadence \u2014 value vs share count (per vest event)", + "description": "Per-vest event timeline. Left axis (USD): vest value = shares \u00d7 vest-day META price. Right axis: number of shares vested. Each point is one vest date (sometimes a single date has multiple BUY rows \u2014 aggregated here).", + "type": "timeseries", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 86 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "lineInterpolation": "linear", + "fillOpacity": 10, + "pointSize": 7, + "showPoints": "always", + "spanNulls": true, + "axisPlacement": "auto", + "stacking": { + "group": "A", + "mode": "none" + } + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "vest value" + }, + "properties": [ + { + "id": "unit", + "value": "currencyUSD" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "blue" + } + }, + { + "id": "custom.axisLabel", + "value": "Vest value (USD)" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "shares" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "orange" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.axisLabel", + "value": "Shares vested" + } + ] + } + ] + }, + "options": { + "legend": { + "calcs": [ + "sum", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ { - "id": 30, - "title": "META vest cadence \u2014 value vs share count (per vest event)", - "description": "Per-vest event timeline. Left axis (USD): vest value = shares \u00d7 vest-day META price. Right axis: number of shares vested. Each point is one vest date (sometimes a single date has multiple BUY rows \u2014 aggregated here).", - "type": "timeseries", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 86 + "rawQuery": true, + "editorMode": "code", + "format": "time_series", + "rawSql": "SELECT activity_date::date::timestamp AS \"time\", SUM(quantity*unit_price) AS \"vest value\", SUM(quantity) AS \"shares\" FROM activities WHERE asset_id='4f60833d-0bfb-484f-8ee6-f129af72e137' AND activity_type='BUY' GROUP BY activity_date::date ORDER BY activity_date::date" + } + ] + }, + { + "id": 31, + "title": "META vests \u2014 realized PNL (FIFO-matched against sells)", + "description": "One row per vest with realized P&L computed by FIFO-matching that vest's shares against subsequent sells. Each vest's shares may be spread across multiple sells; the matched sell-price column is the weighted average. 'Avg days held' is the average gap between this vest's date and the sell dates that consumed its shares. Compare against panel 28 to see realized vs hypo-if-held.", + "type": "table", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 86 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto" }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "decimals": 2 + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "shares sold" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vest price" + }, + "properties": [ + { + "id": "decimals", + "value": 2 }, - "decimals": 0, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "lineInterpolation": "linear", - "fillOpacity": 10, - "pointSize": 7, - "showPoints": "always", - "spanNulls": true, - "axisPlacement": "auto", - "stacking": { - "group": "A", - "mode": "none" + { + "id": "unit", + "value": "currencyUSD" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vest value" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyUSD" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "avg sell price" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyUSD" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "sell value" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyUSD" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "realized PNL" + }, + "properties": [ + { + "id": "decimals", + "value": 2 + }, + { + "id": "unit", + "value": "currencyUSD" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] } } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PNL %" }, - "overrides": [ + "properties": [ { - "matcher": { - "id": "byName", - "options": "vest value" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "blue" - } - }, - { - "id": "custom.axisLabel", - "value": "Vest value (USD)" - } - ] + "id": "decimals", + "value": 1 }, { - "matcher": { - "id": "byName", - "options": "shares" - }, - "properties": [ - { - "id": "unit", - "value": "short" - }, - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "orange" + "id": "unit", + "value": "percent" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 } - }, - { - "id": "custom.axisPlacement", - "value": "right" - }, - { - "id": "custom.axisLabel", - "value": "Shares vested" - } - ] + ] + } } ] }, - "options": { - "legend": { - "calcs": [ - "sum", - "max" - ], - "displayMode": "table", - "placement": "bottom" + { + "matcher": { + "id": "byName", + "options": "days held (avg)" }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" + "properties": [ + { + "id": "decimals", + "value": 0 }, - "rawQuery": true, - "editorMode": "code", - "format": "time_series", - "rawSql": "SELECT activity_date::date::timestamp AS \"time\", SUM(quantity*unit_price) AS \"vest value\", SUM(quantity) AS \"shares\" FROM activities WHERE asset_id='4f60833d-0bfb-484f-8ee6-f129af72e137' AND activity_type='BUY' GROUP BY activity_date::date ORDER BY activity_date::date" - } + { + "id": "unit", + "value": "d" + } + ] + } + ] + }, + "options": { + "cellHeight": "sm", + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "shares sold", + "vest value", + "sell value", + "realized PNL" ] - }, + } + }, + "targets": [ { - "id": 31, - "title": "META vests \u2014 realized PNL (FIFO-matched against sells)", - "description": "One row per vest with realized P&L computed by FIFO-matching that vest's shares against subsequent sells. Each vest's shares may be spread across multiple sells; the matched sell-price column is the weighted average. 'Avg days held' is the average gap between this vest's date and the sell dates that consumed its shares. Compare against panel 28 to see realized vs hypo-if-held.", - "type": "table", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 12, - "w": 12, - "x": 12, - "y": 86 - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "auto", - "displayMode": "auto" - }, - "decimals": 2 - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "shares sold" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "vest price" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyUSD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "vest value" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyUSD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "avg sell price" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyUSD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "sell value" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyUSD" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "realized PNL" - }, - "properties": [ - { - "id": "decimals", - "value": 2 - }, - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 0 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "PNL %" - }, - "properties": [ - { - "id": "decimals", - "value": 1 - }, - { - "id": "unit", - "value": "percent" - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-text" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 0 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "days held (avg)" - }, - "properties": [ - { - "id": "decimals", - "value": 0 - }, - { - "id": "unit", - "value": "d" - } - ] - } - ] - }, - "options": { - "cellHeight": "sm", - "footer": { - "show": true, - "reducer": [ - "sum" - ], - "fields": [ - "shares sold", - "vest value", - "sell value", - "realized PNL" - ] - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "rawQuery": true, - "editorMode": "code", - "format": "table", - "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" - } - ] + "rawQuery": true, + "editorMode": "code", + "format": "table", + "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" } ] }, { "type": "row", "title": "Projections", - "collapsed": true, + "collapsed": false, "id": 9306, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 103 + "y": 99 }, - "panels": [ + "panels": [] + }, + { + "id": 9103, + "title": "Net worth \u2014 ${horizon_years}-year projection", + "description": "Projected net worth at Low/Base/High fixed rates plus your trailing-3y historical return, all compounding from today with your auto monthly contribution (set $monthly_contribution=0 for pure compounding; 'Base, no new contributions' shows the same base rate with zero contributions). NOTE: set the time range to include the future (use the links above) to see the projection.", + "type": "trend", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "wealth-pg" + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 100 + }, + "fieldConfig": { + "defaults": { + "unit": "currencyGBP", + "decimals": 0, + "custom": { + "drawStyle": "line", + "lineWidth": 2, + "fillOpacity": 0, + "showPoints": "never", + "lineInterpolation": "smooth" + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "^(Low|Base \\(|High|Historical)" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "custom.lineWidth", + "value": 1 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Base, no new contributions" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dot", + "dash": [ + 2, + 6 + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Years from today" + }, + "properties": [ + { + "id": "unit", + "value": "none" + }, + { + "id": "custom.axisLabel", + "value": "Years from today" + } + ] + } + ] + }, + "options": { + "xField": "Years from today", + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ { - "id": 9103, - "title": "Net worth \u2014 ${horizon_years}-year projection", - "description": "Projected net worth at Low/Base/High fixed rates plus your trailing-3y historical return, all compounding from today with your auto monthly contribution (set $monthly_contribution=0 for pure compounding; 'Base, no new contributions' shows the same base rate with zero contributions). NOTE: set the time range to include the future (use the links above) to see the projection.", - "type": "trend", + "refId": "A", "datasource": { "type": "grafana-postgresql-datasource", "uid": "wealth-pg" }, - "gridPos": { - "h": 12, - "w": 24, - "x": 0, - "y": 100 - }, - "fieldConfig": { - "defaults": { - "unit": "currencyGBP", - "decimals": 0, - "custom": { - "drawStyle": "line", - "lineWidth": 2, - "fillOpacity": 0, - "showPoints": "never", - "lineInterpolation": "smooth" - } - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "^(Low|Base \\(|High|Historical)" - }, - "properties": [ - { - "id": "custom.lineStyle", - "value": { - "fill": "dash", - "dash": [ - 10, - 10 - ] - } - }, - { - "id": "custom.lineWidth", - "value": 1 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Base, no new contributions" - }, - "properties": [ - { - "id": "custom.lineStyle", - "value": { - "fill": "dot", - "dash": [ - 2, - 6 - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Years from today" - }, - "properties": [ - { - "id": "unit", - "value": "none" - }, - { - "id": "custom.axisLabel", - "value": "Years from today" - } - ] - } - ] - }, - "options": { - "xField": "Years from today", - "legend": { - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "multi" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "wealth-pg" - }, - "format": "table", - "editorMode": "code", - "rawQuery": true, - "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c >= (SELECT n FROM active_count)), latest AS (SELECT DISTINCT ON (account_id) account_id, total_value, net_contribution FROM dav_corrected WHERE valuation_date <= (SELECT d FROM mc) ORDER BY account_id, valuation_date DESC), agg AS (SELECT SUM(total_value) nw0, SUM(net_contribution) c_now FROM latest), ago AS (SELECT SUM(x.nc) c_ago FROM latest l LEFT JOIN LATERAL (SELECT net_contribution nc FROM dav_corrected dd WHERE dd.account_id=l.account_id AND dd.valuation_date <= (SELECT d FROM mc) - INTERVAL '12 months' ORDER BY dd.valuation_date DESC LIMIT 1) x ON true), yearly AS (SELECT EXTRACT(YEAR FROM valuation_date)::int yr, valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM mc) GROUP BY valuation_date), ep AS (SELECT yr, (array_agg(nw ORDER BY valuation_date))[1] nw_s, (array_agg(nw ORDER BY valuation_date DESC))[1] nw_e, (array_agg(contrib ORDER BY valuation_date))[1] c_s, (array_agg(contrib ORDER BY valuation_date DESC))[1] c_e, COUNT(*) days FROM yearly GROUP BY yr), r3 AS (SELECT (nw_e-nw_s-(c_e-c_s))/NULLIF(nw_s+0.5*(c_e-c_s),0) ret FROM ep WHERE (nw_s+0.5*(c_e-c_s))>0 AND days>=300 ORDER BY yr DESC LIMIT 3), params AS (SELECT (SELECT nw0 FROM agg) nw0, COALESCE(NULLIF('$monthly_contribution','auto')::numeric, ((SELECT c_now FROM agg)-(SELECT c_ago FROM ago))/12.0) cm, ($rate_low::float)/100 rl, ($rate_base::float)/100 rb, ($rate_high::float)/100 rh, (SELECT exp(avg(ln(1+ret)))-1 FROM r3) rhist), m AS (SELECT generate_series(0, ${horizon_years}*12) n) SELECT round((m.n/12.0)::numeric,2) AS \"Years from today\", round((nw0*power(1+(power(1+rl,1/12.0)-1),m.n) + cm*((power(1+(power(1+rl,1/12.0)-1),m.n)-1)/NULLIF((power(1+rl,1/12.0)-1),0)))::numeric,0) AS \"Low ($rate_low%)\", round((nw0*power(1+(power(1+rb,1/12.0)-1),m.n) + cm*((power(1+(power(1+rb,1/12.0)-1),m.n)-1)/NULLIF((power(1+rb,1/12.0)-1),0)))::numeric,0) AS \"Base ($rate_base%)\", round((nw0*power(1+(power(1+rb,1/12.0)-1),m.n))::numeric,0) AS \"Base, no new contributions\", round((nw0*power(1+(power(1+rh,1/12.0)-1),m.n) + cm*((power(1+(power(1+rh,1/12.0)-1),m.n)-1)/NULLIF((power(1+rh,1/12.0)-1),0)))::numeric,0) AS \"High ($rate_high%)\", round((nw0*power(1+(power(1+rhist,1/12.0)-1),m.n) + cm*((power(1+(power(1+rhist,1/12.0)-1),m.n)-1)/NULLIF((power(1+rhist,1/12.0)-1),0)))::numeric,0) AS \"Historical (trailing 3y)\" FROM m, params" - } - ] + "format": "table", + "editorMode": "code", + "rawQuery": true, + "rawSql": "WITH active_count AS (SELECT COUNT(*) n FROM accounts), mc AS (SELECT MAX(valuation_date) d FROM (SELECT valuation_date, COUNT(*) c FROM dav_corrected GROUP BY valuation_date) x WHERE c >= (SELECT n FROM active_count)), latest AS (SELECT DISTINCT ON (account_id) account_id, total_value, net_contribution FROM dav_corrected WHERE valuation_date <= (SELECT d FROM mc) ORDER BY account_id, valuation_date DESC), agg AS (SELECT SUM(total_value) nw0, SUM(net_contribution) c_now FROM latest), ago AS (SELECT SUM(x.nc) c_ago FROM latest l LEFT JOIN LATERAL (SELECT net_contribution nc FROM dav_corrected dd WHERE dd.account_id=l.account_id AND dd.valuation_date <= (SELECT d FROM mc) - INTERVAL '12 months' ORDER BY dd.valuation_date DESC LIMIT 1) x ON true), yearly AS (SELECT EXTRACT(YEAR FROM valuation_date)::int yr, valuation_date, SUM(total_value) nw, SUM(net_contribution) contrib FROM dav_corrected WHERE valuation_date <= (SELECT d FROM mc) GROUP BY valuation_date), ep AS (SELECT yr, (array_agg(nw ORDER BY valuation_date))[1] nw_s, (array_agg(nw ORDER BY valuation_date DESC))[1] nw_e, (array_agg(contrib ORDER BY valuation_date))[1] c_s, (array_agg(contrib ORDER BY valuation_date DESC))[1] c_e, COUNT(*) days FROM yearly GROUP BY yr), r3 AS (SELECT (nw_e-nw_s-(c_e-c_s))/NULLIF(nw_s+0.5*(c_e-c_s),0) ret FROM ep WHERE (nw_s+0.5*(c_e-c_s))>0 AND days>=300 ORDER BY yr DESC LIMIT 3), params AS (SELECT (SELECT nw0 FROM agg) nw0, COALESCE(NULLIF('$monthly_contribution','auto')::numeric, ((SELECT c_now FROM agg)-(SELECT c_ago FROM ago))/12.0) cm, ($rate_low::float)/100 rl, ($rate_base::float)/100 rb, ($rate_high::float)/100 rh, (SELECT exp(avg(ln(1+ret)))-1 FROM r3) rhist), m AS (SELECT generate_series(0, ${horizon_years}*12) n) SELECT round((m.n/12.0)::numeric,2) AS \"Years from today\", round((nw0*power(1+(power(1+rl,1/12.0)-1),m.n) + cm*((power(1+(power(1+rl,1/12.0)-1),m.n)-1)/NULLIF((power(1+rl,1/12.0)-1),0)))::numeric,0) AS \"Low ($rate_low%)\", round((nw0*power(1+(power(1+rb,1/12.0)-1),m.n) + cm*((power(1+(power(1+rb,1/12.0)-1),m.n)-1)/NULLIF((power(1+rb,1/12.0)-1),0)))::numeric,0) AS \"Base ($rate_base%)\", round((nw0*power(1+(power(1+rb,1/12.0)-1),m.n))::numeric,0) AS \"Base, no new contributions\", round((nw0*power(1+(power(1+rh,1/12.0)-1),m.n) + cm*((power(1+(power(1+rh,1/12.0)-1),m.n)-1)/NULLIF((power(1+rh,1/12.0)-1),0)))::numeric,0) AS \"High ($rate_high%)\", round((nw0*power(1+(power(1+rhist,1/12.0)-1),m.n) + cm*((power(1+(power(1+rhist,1/12.0)-1),m.n)-1)/NULLIF((power(1+rhist,1/12.0)-1),0)))::numeric,0) AS \"Historical (trailing 3y)\" FROM m, params" } ] } @@ -2406,4 +2400,4 @@ "title": "Wealth", "uid": "wealth", "version": 1 -} \ No newline at end of file +}