diff --git a/docker/grafana/provisioning/dashboards/routing.json b/docker/grafana/provisioning/dashboards/routing.json index b3965d5c..1bfd7050 100644 --- a/docker/grafana/provisioning/dashboards/routing.json +++ b/docker/grafana/provisioning/dashboards/routing.json @@ -3887,7 +3887,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield by directional channel pair (incoming -> outgoing) on NodeGuard-opened liquidity. Numerator is settled forwarding fees for the pair; swap costs are NOT subtracted at this granularity because swaps rebalance node-level liquidity, not specific channel pairs (per-pair allocation would be an accounting convention, not a causal attribution). Denominator is each qualifying channel's SatsAmount prorated by the fraction of the window it was open (CreationDatetime to ClosedAt, or UpdateDatetime fallback when Status = Closed). Channels not opened by NodeGuard contribute revenue but never liquidity. APR is null when the pair has no qualifying liquidity. For APR net of swap costs, see the 'NodeGuard-Liquidity APR by Node' or 'Combined NodeGuard-Liquidity APR' panels.", + "description": "Annualized yield by directional channel pair (incoming -> outgoing) on NodeGuard-opened liquidity. Numerator is settled forwarding fees for the pair, net of any enabled liquidity-management costs allocated across channel pairs within the same node in proportion to pair fee revenue. Apply Swap Costs controls completed swap-out fees; Apply Rebalance Costs controls succeeded rebalance fees. Denominator is each qualifying channel's SatsAmount prorated by the fraction of the window it was open (CreationDatetime to ClosedAt, or UpdateDatetime fallback when Status = Closed). Channels not opened by NodeGuard contribute revenue but never liquidity. APR is null when the pair has no qualifying liquidity.", "fieldConfig": { "defaults": { "color": { @@ -4019,7 +4019,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), pair_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingChannelId\", \"OutgoingChannelId\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), pair_with_liquidity AS (SELECT pf.\"ManagedNodePubKey\", pf.managed_node_name, pf.pair_label, pf.\"IncomingChannelId\", pf.\"OutgoingChannelId\", pf.settled_count, pf.fee_sats, COALESCE(p_in.prorated_sats, 0) + COALESCE(p_out.prorated_sats, 0) - CASE WHEN pf.\"IncomingChannelId\" = pf.\"OutgoingChannelId\" THEN COALESCE(p_in.prorated_sats, 0) ELSE 0 END AS qualifying_liquidity_sats, COALESCE(cs_in.status, '?') AS incoming_status, COALESCE(cs_out.status, '?') AS outgoing_status FROM pair_fees pf LEFT JOIN prorated p_in ON p_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN prorated p_out ON p_out.\"ChanId\" = pf.\"OutgoingChannelId\" LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\") SELECT managed_node_name AS \"Node\", pair_label AS \"Channel Pair\", incoming_status AS \"Incoming Status\", outgoing_status AS \"Outgoing Status\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM pair_with_liquidity ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), pair_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingChannelId\", \"OutgoingChannelId\"), node_pair_fee_totals AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS total_fee_sats FROM pair_fees GROUP BY \"ManagedNodePubKey\"), node_yield_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, COALESCE(SUM(yc.cost_sats), 0)::numeric AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), pair_with_liquidity AS (SELECT pf.\"ManagedNodePubKey\", pf.managed_node_name, pf.pair_label, pf.\"IncomingChannelId\", pf.\"OutgoingChannelId\", pf.settled_count, pf.fee_sats, COALESCE(p_in.prorated_sats, 0) + COALESCE(p_out.prorated_sats, 0) - CASE WHEN pf.\"IncomingChannelId\" = pf.\"OutgoingChannelId\" THEN COALESCE(p_in.prorated_sats, 0) ELSE 0 END AS qualifying_liquidity_sats, COALESCE(cs_in.status, '?') AS incoming_status, COALESCE(cs_out.status, '?') AS outgoing_status FROM pair_fees pf LEFT JOIN prorated p_in ON p_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN prorated p_out ON p_out.\"ChanId\" = pf.\"OutgoingChannelId\" LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\"), pair_with_costs AS (SELECT pwl.*, CASE WHEN COALESCE(npft.total_fee_sats, 0) > 0 THEN pwl.fee_sats::numeric * COALESCE(nyc.yield_cost_sats, 0) / npft.total_fee_sats ELSE 0 END AS allocated_yield_cost_sats FROM pair_with_liquidity pwl LEFT JOIN node_pair_fee_totals npft ON npft.\"ManagedNodePubKey\" = pwl.\"ManagedNodePubKey\" LEFT JOIN node_yield_costs nyc ON nyc.managed_node_pubkey = pwl.\"ManagedNodePubKey\") SELECT managed_node_name AS \"Node\", pair_label AS \"Channel Pair\", incoming_status AS \"Incoming Status\", outgoing_status AS \"Outgoing Status\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN ((fee_sats::numeric - allocated_yield_cost_sats) / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM pair_with_costs ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4031,7 +4031,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield by directional peer pair (incoming alias -> outgoing alias) on NodeGuard-opened liquidity. Numerator is settled forwarding fees; swap costs are NOT subtracted at peer-pair granularity because swaps rebalance node-level liquidity, not peer-specific flow. Denominator is the union of NodeGuard-opened channels that routed inside the pair during the window, each prorated by its open fraction and counted once even if a single channel appears on both sides. For APR net of swap costs, see the node-level panels.", + "description": "Annualized yield by directional peer pair (incoming alias -> outgoing alias) on NodeGuard-opened liquidity. Numerator is settled forwarding fees for the pair, net of any enabled liquidity-management costs allocated across peer pairs within the same node in proportion to peer-pair fee revenue. Apply Swap Costs controls completed swap-out fees; Apply Rebalance Costs controls succeeded rebalance fees. Denominator is the union of NodeGuard-opened channels that routed inside the pair during the window, each prorated by its open fraction and counted once even if a single channel appears on both sides. APR is null when the peer pair has no qualifying liquidity.", "fieldConfig": { "defaults": { "color": { @@ -4163,7 +4163,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_pair_events AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingPeerAlias\" AS incoming_peer, \"OutgoingPeerAlias\" AS outgoing_peer, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\"), peer_pair_channels AS (SELECT DISTINCT f.\"ManagedNodePubKey\", f.\"IncomingPeerAlias\" AS incoming_peer, f.\"OutgoingPeerAlias\" AS outgoing_peer, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_pair_liquidity AS (SELECT ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_pair_channels ppc JOIN prorated p ON p.\"ChanId\" = ppc.chan_id GROUP BY ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer), peer_pair_with_alloc AS (SELECT ppe.\"ManagedNodePubKey\", ppe.managed_node_name, ppe.incoming_peer, ppe.outgoing_peer, ppe.settled_count, ppe.fee_sats, COALESCE(ppl.qualifying_liquidity_sats, 0) AS qualifying_liquidity_sats FROM peer_pair_events ppe LEFT JOIN peer_pair_liquidity ppl ON ppl.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\" AND ppl.incoming_peer IS NOT DISTINCT FROM ppe.incoming_peer AND ppl.outgoing_peer IS NOT DISTINCT FROM ppe.outgoing_peer) SELECT managed_node_name AS \"Node\", (COALESCE(incoming_peer, '?') || ' -> ' || COALESCE(outgoing_peer, '?')) AS \"Peer Pair\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_pair_with_alloc ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_pair_events AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingPeerAlias\" AS incoming_peer, \"OutgoingPeerAlias\" AS outgoing_peer, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\"), node_peer_pair_fee_totals AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS total_fee_sats FROM peer_pair_events GROUP BY \"ManagedNodePubKey\"), node_yield_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, COALESCE(SUM(yc.cost_sats), 0)::numeric AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), peer_pair_channels AS (SELECT DISTINCT f.\"ManagedNodePubKey\", f.\"IncomingPeerAlias\" AS incoming_peer, f.\"OutgoingPeerAlias\" AS outgoing_peer, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_pair_liquidity AS (SELECT ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_pair_channels ppc JOIN prorated p ON p.\"ChanId\" = ppc.chan_id GROUP BY ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer), peer_pair_with_alloc AS (SELECT ppe.\"ManagedNodePubKey\", ppe.managed_node_name, ppe.incoming_peer, ppe.outgoing_peer, ppe.settled_count, ppe.fee_sats, COALESCE(ppl.qualifying_liquidity_sats, 0) AS qualifying_liquidity_sats FROM peer_pair_events ppe LEFT JOIN peer_pair_liquidity ppl ON ppl.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\" AND ppl.incoming_peer IS NOT DISTINCT FROM ppe.incoming_peer AND ppl.outgoing_peer IS NOT DISTINCT FROM ppe.outgoing_peer), peer_pair_with_costs AS (SELECT ppwa.*, CASE WHEN COALESCE(nppft.total_fee_sats, 0) > 0 THEN ppwa.fee_sats::numeric * COALESCE(nyc.yield_cost_sats, 0) / nppft.total_fee_sats ELSE 0 END AS allocated_yield_cost_sats FROM peer_pair_with_alloc ppwa LEFT JOIN node_peer_pair_fee_totals nppft ON nppft.\"ManagedNodePubKey\" = ppwa.\"ManagedNodePubKey\" LEFT JOIN node_yield_costs nyc ON nyc.managed_node_pubkey = ppwa.\"ManagedNodePubKey\") SELECT managed_node_name AS \"Node\", (COALESCE(incoming_peer, '?') || ' -> ' || COALESCE(outgoing_peer, '?')) AS \"Peer Pair\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN ((fee_sats::numeric - allocated_yield_cost_sats) / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_pair_with_costs ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4175,7 +4175,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Combined annualized yield across all settled forwards in the window, net of swap costs. Numerator is total fee_sats minus the sum of completed swap-out costs (Service + Lightning + OnChain fees) on the selected nodes in the window. Denominator is the sum of prorated SatsAmount across every distinct NodeGuard-opened channel that routed at least one settled forward in the window (each counted once total). APR can be negative.", + "description": "Combined annualized yield across all settled forwards in the window, net of liquidity-management costs. Numerator is total fee_sats minus completed swap-out costs when Apply Swap Costs is enabled and minus succeeded rebalance fees when Apply Rebalance Costs is enabled. Denominator is the sum of prorated SatsAmount across every distinct NodeGuard-opened channel that routed at least one settled forward in the window (each counted once total). APR can be negative.", "fieldConfig": { "defaults": { "color": { @@ -4242,7 +4242,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_swap_costs AS (SELECT $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tsc.swap_cost_sats)::numeric / ql.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM total_fees tf, total_swap_costs tsc, qualifying_liquidity ql, bounds b", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_yield_costs AS (SELECT COALESCE(SUM(yc.cost_sats), 0) AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tyc.yield_cost_sats)::numeric / ql.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM total_fees tf, total_yield_costs tyc, qualifying_liquidity ql, bounds b", "refId": "A" } ], @@ -4254,7 +4254,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Period yield as a percentage — the actual return on NodeGuard liquidity for the selected window, NOT annualized. Equal to (settled fee revenue − completed swap-out costs) / qualifying_liquidity. The Combined APR panel takes this same value and multiplies by 365/window_days to annualize. Respects the Apply Swap Costs toggle. Can be negative.", + "description": "Period yield as a percentage — the actual return on NodeGuard liquidity for the selected window, NOT annualized. Equal to (settled fee revenue − optional swap costs − optional rebalance costs) / qualifying_liquidity. The Combined APR panel takes this same value and multiplies by 365/window_days to annualize. Respects the Apply Swap Costs and Apply Rebalance Costs toggles. Can be negative.", "fieldConfig": { "defaults": { "color": { @@ -4313,7 +4313,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_swap_costs AS (SELECT $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / GREATEST(EXTRACT(EPOCH FROM (b.window_end - b.window_start)) / 86400.0, 0.000001)), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tsc.swap_cost_sats)::numeric / ql.liquidity_sats) ELSE NULL END AS \"Raw Yield\" FROM total_fees tf, total_swap_costs tsc, qualifying_liquidity ql", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_yield_costs AS (SELECT COALESCE(SUM(yc.cost_sats), 0) AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / GREATEST(EXTRACT(EPOCH FROM (b.window_end - b.window_start)) / 86400.0, 0.000001)), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tyc.yield_cost_sats)::numeric / ql.liquidity_sats) ELSE NULL END AS \"Raw Yield\" FROM total_fees tf, total_yield_costs tyc, qualifying_liquidity ql", "refId": "A" } ], @@ -4325,7 +4325,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Per-node annualized yield, net of that node's completed swap-out costs. APR = ((node_fee_sats - node_swap_cost_sats) / node_qualifying_liquidity_sats) * (365 / window_days). A node's qualifying liquidity is the sum of prorated SatsAmount of NodeGuard-opened channels that routed at least one settled forward for that node in the window, each counted once. APR can be negative.", + "description": "Per-node annualized yield, net of that node's liquidity-management costs. APR = ((node_fee_sats - node_yield_cost_sats) / node_qualifying_liquidity_sats) * (365 / window_days). A node's yield costs are the sum of completed swap-out costs when Apply Swap Costs is enabled and succeeded rebalance fees when Apply Rebalance Costs is enabled. A node's qualifying liquidity is the sum of prorated SatsAmount of NodeGuard-opened channels that routed at least one settled forward for that node in the window, each counted once. APR can be negative.", "fieldConfig": { "defaults": { "color": { @@ -4392,7 +4392,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), node_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\"), node_swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_participating_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), node_liquidity AS (SELECT npc.\"ManagedNodePubKey\", SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days) AS liquidity_sats FROM node_participating_chans npc JOIN \"Channels\" c ON c.\"ChanId\" = npc.chan_id CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY npc.\"ManagedNodePubKey\") SELECT nf.managed_node_name AS \"Node\", CASE WHEN nl.liquidity_sats > 0 THEN ((nf.fee_sats - COALESCE(nsc.swap_cost_sats, 0))::numeric / nl.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM node_fees nf LEFT JOIN node_liquidity nl ON nl.\"ManagedNodePubKey\" = nf.\"ManagedNodePubKey\" LEFT JOIN node_swap_costs nsc ON nsc.managed_node_pubkey = nf.\"ManagedNodePubKey\" CROSS JOIN bounds b ORDER BY \"APR\" DESC NULLS LAST", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), node_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\"), node_yield_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, COALESCE(SUM(yc.cost_sats), 0) AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_participating_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), node_liquidity AS (SELECT npc.\"ManagedNodePubKey\", SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days) AS liquidity_sats FROM node_participating_chans npc JOIN \"Channels\" c ON c.\"ChanId\" = npc.chan_id CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY npc.\"ManagedNodePubKey\") SELECT nf.managed_node_name AS \"Node\", CASE WHEN nl.liquidity_sats > 0 THEN ((nf.fee_sats - COALESCE(nyc.yield_cost_sats, 0))::numeric / nl.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM node_fees nf LEFT JOIN node_liquidity nl ON nl.\"ManagedNodePubKey\" = nf.\"ManagedNodePubKey\" LEFT JOIN node_yield_costs nyc ON nyc.managed_node_pubkey = nf.\"ManagedNodePubKey\" CROSS JOIN bounds b ORDER BY \"APR\" DESC NULLS LAST", "refId": "A" } ], @@ -4547,7 +4547,186 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield attributed to each individual NodeGuard channel. Each settled forward credits BOTH its incoming and outgoing channel with the full fee (full-attribution, same convention as 'Routed fee distribution From/To'). Numerator is settled forwarding fees; swap costs are NOT subtracted at per-channel granularity (swaps rebalance the whole node, not a single channel). Denominator is this channel's prorated SatsAmount alone. APR is null when the channel has no qualifying liquidity (i.e. not opened by NodeGuard). For APR net of swap costs, see the node-level panels.", + "description": "Diagnostic view of recent rebalance attempts on the selected nodes. Shows source and target routing hints, requested vs actual amount, fee paid, effective ppm, and current status for operator troubleshooting. Succeeded rebalances with recorded fees are also part of the yield-cost subtraction in the APR panels above when Apply Rebalance Costs is enabled.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "Succeeded": { + "color": "green", + "index": 0 + }, + "Pending": { + "color": "yellow", + "index": 1 + }, + "Probing": { + "color": "orange", + "index": 2 + }, + "In Flight": { + "color": "blue", + "index": 3 + }, + "Failed": { + "color": "red", + "index": 4 + }, + "No Route": { + "color": "orange", + "index": 5 + }, + "Timeout": { + "color": "red", + "index": 6 + }, + "Insufficient Balance": { + "color": "red", + "index": 7 + }, + "Exceeded Fee Limit": { + "color": "red", + "index": 8 + } + } + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Fee Paid (BTC)" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Effective PPM" + }, + "properties": [ + { + "id": "decimals", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 179 + }, + "id": 112, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum", + "count" + ], + "fields": [ + "Actual Amount (BTC)", + "Fee Paid (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT r.\"CreationDatetime\" AS \"Date\", COALESCE(n.\"Name\", '(no node)') AS \"Node\", CASE r.\"Status\" WHEN 0 THEN 'Pending' WHEN 1 THEN 'Probing' WHEN 2 THEN 'In Flight' WHEN 3 THEN 'Succeeded' WHEN 4 THEN 'Failed' WHEN 5 THEN 'No Route' WHEN 6 THEN 'Timeout' WHEN 7 THEN 'Insufficient Balance' WHEN 8 THEN 'Exceeded Fee Limit' ELSE '?' END AS \"Status\", CASE WHEN r.\"IsManual\" THEN 'Manual' ELSE 'Auto' END AS \"Type\", COALESCE(r.\"SourceChanIdLnd\"::text, 'Any') AS \"Source ChanId\", CASE WHEN sc.\"Id\" IS NULL THEN '-' WHEN sn.\"PubKey\" = r.\"SourceNodePubKey\" THEN COALESCE(dn.\"Name\", LEFT(dn.\"PubKey\", 12) || '...', '-') ELSE COALESCE(sn.\"Name\", LEFT(sn.\"PubKey\", 12) || '...', '-') END AS \"Source Peer\", CASE WHEN r.\"TargetPubkey\" IS NULL THEN 'Any' ELSE COALESCE(tn.\"Name\", LEFT(r.\"TargetPubkey\", 12) || '...') END AS \"Target Peer\", (r.\"RequestedAmountSats\" / 100000000.0)::numeric(20,8) AS \"Requested Amount (BTC)\", (r.\"SatsAmount\" / 100000000.0)::numeric(20,8) AS \"Actual Amount (BTC)\", (COALESCE(r.\"FeePaidSats\", 0) / 100000000.0)::numeric(20,8) AS \"Fee Paid (BTC)\", CASE WHEN r.\"FeePaidMsat\" IS NOT NULL AND r.\"SatsAmount\" > 0 THEN ROUND((r.\"FeePaidMsat\"::numeric * 1000.0) / r.\"SatsAmount\") ELSE NULL END AS \"Effective PPM\", r.\"AttemptNumber\" AS \"Attempt\" FROM \"Rebalances\" r LEFT JOIN \"Nodes\" n ON n.\"Id\" = r.\"NodeId\" LEFT JOIN \"Channels\" sc ON sc.\"Id\" = r.\"SourceChannelId\" LEFT JOIN \"Nodes\" sn ON sn.\"Id\" = sc.\"SourceNodeId\" LEFT JOIN \"Nodes\" dn ON dn.\"Id\" = sc.\"DestinationNodeId\" LEFT JOIN \"Nodes\" tn ON tn.\"PubKey\" = r.\"TargetPubkey\" WHERE $__timeFilter(r.\"CreationDatetime\") AND n.\"PubKey\" IN ($node) ORDER BY r.\"CreationDatetime\" DESC", + "refId": "A" + } + ], + "title": "Recent Rebalances (Diagnostic)", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Annualized yield attributed to each individual NodeGuard channel. Each settled forward credits BOTH its incoming and outgoing channel with the full fee (full-attribution, same convention as 'Routed fee distribution From/To'). When Apply Swap Costs or Apply Rebalance Costs is enabled, the selected node's yield costs are allocated across channels in proportion to that same full-attribution fee share before APR is calculated. Denominator is this channel's prorated SatsAmount alone. APR is null when the channel has no qualifying liquidity (i.e. not opened by NodeGuard).", "fieldConfig": { "defaults": { "color": { @@ -4679,7 +4858,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), channel_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.chan_id, MAX(u.peer_alias) AS peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\", f.\"IncomingPeerAlias\"), (f.\"OutgoingChannelId\", f.\"OutgoingPeerAlias\")) AS u(chan_id, peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.chan_id), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), all_channel_sizes AS (SELECT \"ChanId\", MAX(\"SatsAmount\") AS sats_amount FROM \"Channels\" GROUP BY \"ChanId\"), channels_with_meta AS (SELECT ce.\"ManagedNodePubKey\", ce.managed_node_name, ce.chan_id, ce.peer_alias, ce.settled_count, ce.fee_sats, COALESCE(acs.sats_amount, 0) AS channel_size_sats, COALESCE(p.prorated_sats, 0) AS qualifying_liquidity_sats, COALESCE(cs.status, '?') AS status FROM channel_events ce LEFT JOIN prorated p ON p.\"ChanId\" = ce.chan_id LEFT JOIN all_channel_sizes acs ON acs.\"ChanId\" = ce.chan_id LEFT JOIN channel_status cs ON cs.\"ChanId\" = ce.chan_id) SELECT managed_node_name AS \"Node\", COALESCE(peer_alias, '?') AS \"Channel (peer)\", chan_id::text AS \"Chan ID\", status AS \"Status\", (channel_size_sats / 100000000.0)::numeric(20,8) AS \"Channel Size (BTC)\", settled_count AS \"HTLC Settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM channels_with_meta ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), channel_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.chan_id, MAX(u.peer_alias) AS peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\", f.\"IncomingPeerAlias\"), (f.\"OutgoingChannelId\", f.\"OutgoingPeerAlias\")) AS u(chan_id, peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.chan_id), node_channel_fee_totals AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS total_fee_sats FROM channel_events GROUP BY \"ManagedNodePubKey\"), node_yield_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, COALESCE(SUM(yc.cost_sats), 0)::numeric AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), all_channel_sizes AS (SELECT \"ChanId\", MAX(\"SatsAmount\") AS sats_amount FROM \"Channels\" GROUP BY \"ChanId\"), channels_with_meta AS (SELECT ce.\"ManagedNodePubKey\", ce.managed_node_name, ce.chan_id, ce.peer_alias, ce.settled_count, ce.fee_sats, COALESCE(acs.sats_amount, 0) AS channel_size_sats, COALESCE(p.prorated_sats, 0) AS qualifying_liquidity_sats, COALESCE(cs.status, '?') AS status FROM channel_events ce LEFT JOIN prorated p ON p.\"ChanId\" = ce.chan_id LEFT JOIN all_channel_sizes acs ON acs.\"ChanId\" = ce.chan_id LEFT JOIN channel_status cs ON cs.\"ChanId\" = ce.chan_id), channels_with_costs AS (SELECT cwm.*, CASE WHEN COALESCE(ncft.total_fee_sats, 0) > 0 THEN cwm.fee_sats::numeric * COALESCE(nyc.yield_cost_sats, 0) / ncft.total_fee_sats ELSE 0 END AS allocated_yield_cost_sats FROM channels_with_meta cwm LEFT JOIN node_channel_fee_totals ncft ON ncft.\"ManagedNodePubKey\" = cwm.\"ManagedNodePubKey\" LEFT JOIN node_yield_costs nyc ON nyc.managed_node_pubkey = cwm.\"ManagedNodePubKey\") SELECT managed_node_name AS \"Node\", COALESCE(peer_alias, '?') AS \"Channel (peer)\", chan_id::text AS \"Chan ID\", status AS \"Status\", (channel_size_sats / 100000000.0)::numeric(20,8) AS \"Channel Size (BTC)\", settled_count AS \"HTLC Settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN ((fee_sats::numeric - allocated_yield_cost_sats) / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM channels_with_costs ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4691,7 +4870,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield attributed to each individual peer (by alias). Each settled forward credits BOTH the incoming and outgoing peer with the full fee (full-attribution). Numerator is settled forwarding fees; swap costs are NOT subtracted at per-peer granularity. Denominator is the union of NodeGuard-opened channels associated with the peer, each prorated by its open fraction in the window and counted once. APR is null when the peer has no qualifying NodeGuard liquidity. For APR net of swap costs, see the node-level panels.", + "description": "Annualized yield attributed to each individual peer (by alias). Each settled forward credits BOTH the incoming and outgoing peer with the full fee (full-attribution). When Apply Swap Costs or Apply Rebalance Costs is enabled, the selected node's yield costs are allocated across peers in proportion to that same full-attribution fee share before APR is calculated. Denominator is the union of NodeGuard-opened channels associated with the peer, each prorated by its open fraction in the window and counted once. APR is null when the peer has no qualifying NodeGuard liquidity.", "fieldConfig": { "defaults": { "color": { @@ -4823,7 +5002,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\"), (f.\"OutgoingPeerAlias\")) AS u(peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.peer_alias), peer_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.peer_alias, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\", f.\"IncomingChannelId\"), (f.\"OutgoingPeerAlias\", f.\"OutgoingChannelId\")) AS u(peer_alias, chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_liquidity AS (SELECT pc.\"ManagedNodePubKey\", pc.peer_alias, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_chans pc JOIN prorated p ON p.\"ChanId\" = pc.chan_id GROUP BY pc.\"ManagedNodePubKey\", pc.peer_alias) SELECT pe.managed_node_name AS \"Node\", COALESCE(pe.peer_alias, '?') AS \"Peer\", pe.settled_count AS \"HTLC Settled\", (pe.fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (COALESCE(pl.qualifying_liquidity_sats, 0) / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN pl.qualifying_liquidity_sats >= 1 THEN (pe.fee_sats::numeric / pl.qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_events pe LEFT JOIN peer_liquidity pl ON pl.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" AND pl.peer_alias IS NOT DISTINCT FROM pe.peer_alias ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\"), (f.\"OutgoingPeerAlias\")) AS u(peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.peer_alias), node_peer_fee_totals AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS total_fee_sats FROM peer_events GROUP BY \"ManagedNodePubKey\"), node_yield_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, COALESCE(SUM(yc.cost_sats), 0)::numeric AS yield_cost_sats FROM (SELECT s.\"NodeId\", $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc JOIN \"Nodes\" n ON n.\"Id\" = yc.\"NodeId\" WHERE n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), peer_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.peer_alias, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\", f.\"IncomingChannelId\"), (f.\"OutgoingPeerAlias\", f.\"OutgoingChannelId\")) AS u(peer_alias, chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_liquidity AS (SELECT pc.\"ManagedNodePubKey\", pc.peer_alias, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_chans pc JOIN prorated p ON p.\"ChanId\" = pc.chan_id GROUP BY pc.\"ManagedNodePubKey\", pc.peer_alias), peers_with_costs AS (SELECT pe.*, CASE WHEN COALESCE(npft.total_fee_sats, 0) > 0 THEN pe.fee_sats::numeric * COALESCE(nyc.yield_cost_sats, 0) / npft.total_fee_sats ELSE 0 END AS allocated_yield_cost_sats FROM peer_events pe LEFT JOIN node_peer_fee_totals npft ON npft.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" LEFT JOIN node_yield_costs nyc ON nyc.managed_node_pubkey = pe.\"ManagedNodePubKey\") SELECT pwc.managed_node_name AS \"Node\", COALESCE(pwc.peer_alias, '?') AS \"Peer\", pwc.settled_count AS \"HTLC Settled\", (pwc.fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (COALESCE(pl.qualifying_liquidity_sats, 0) / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN pl.qualifying_liquidity_sats >= 1 THEN ((pwc.fee_sats::numeric - pwc.allocated_yield_cost_sats) / pl.qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peers_with_costs pwc LEFT JOIN peer_liquidity pl ON pl.\"ManagedNodePubKey\" = pwc.\"ManagedNodePubKey\" AND pl.peer_alias IS NOT DISTINCT FROM pwc.peer_alias ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4890,6 +5069,19 @@ "x": 0, "y": 178 }, + "id": 205, + "panels": [], + "title": "Yield — Rebalance Diagnostics", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 189 + }, "id": 204, "panels": [], "title": "Yield — APR Trends", @@ -4900,7 +5092,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Rolling APR (left axis, percent) and rolling-average Qualifying Liquidity (right axis, BTC) per managed node, in the style of \"Profit over time\". Hourly buckets; each plotted point is the rolling APR over the last $aprWindow ending that hour, paired with the average qualifying liquidity deployed over the same rolling window. Designed for self-comparison within a node: visually spot whether adding/removing liquidity tracks with APR moving up or down. Honors $applySwapCosts and $liquidityScope. APR can be negative.", + "description": "Rolling APR (left axis, percent) and rolling-average Qualifying Liquidity (right axis, BTC) per managed node, in the style of \"Profit over time\". Hourly buckets; each plotted point is the rolling APR over the last $aprWindow ending that hour, paired with the average qualifying liquidity deployed over the same rolling window. Designed for self-comparison within a node: visually spot whether adding/removing liquidity tracks with APR moving up or down. Honors the Apply Swap Costs, Apply Rebalance Costs, and Liquidity Scope controls. APR can be negative.", "fieldConfig": { "defaults": { "color": { @@ -5005,7 +5197,7 @@ "h": 9, "w": 24, "x": 0, - "y": 179 + "y": 190 }, "id": 113, "options": { @@ -5033,7 +5225,7 @@ "editorMode": "code", "format": "time_series", "rawQuery": true, - "rawSql": "WITH days AS (SELECT generate_series(date_trunc('hour', $__timeFrom()::timestamptz), $__timeTo()::timestamptz, INTERVAL '1 hour') AS bucket_start), day_buckets AS (SELECT bucket_start, bucket_start + INTERVAL '1 hour' AS bucket_end FROM days), nodes AS (SELECT \"ManagedNodePubKey\" AS pubkey, MAX(\"ManagedNodeName\") AS node_name FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node) GROUP BY 1), participating_node_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\" AS pubkey, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" IN ($node)), day_node_grid AS (SELECT db.bucket_start, db.bucket_end, n.pubkey, n.node_name FROM day_buckets db CROSS JOIN nodes n), day_fees AS (SELECT g.bucket_start, g.pubkey, g.node_name, COALESCE(ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM day_node_grid g LEFT JOIN \"ForwardingHtlcEvents\" f ON f.\"EventTimestamp\" >= g.bucket_start AND f.\"EventTimestamp\" < g.bucket_end AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" = g.pubkey GROUP BY g.bucket_start, g.pubkey, g.node_name), day_swap_costs AS (SELECT g.bucket_start, g.pubkey, $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM day_node_grid g LEFT JOIN \"Nodes\" n ON n.\"PubKey\" = g.pubkey LEFT JOIN \"SwapOuts\" s ON s.\"NodeId\" = n.\"Id\" AND s.\"Status\" = 1 AND s.\"CreationDatetime\" >= g.bucket_start AND s.\"CreationDatetime\" < g.bucket_end GROUP BY g.bucket_start, g.pubkey), day_liquidity AS (SELECT g.bucket_start, g.pubkey, COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE g.bucket_end END, g.bucket_end) - GREATEST(c.\"CreationDatetime\", g.bucket_start))) / 86400.0, 0.0)), 0) AS sat_days FROM day_node_grid g LEFT JOIN participating_node_chans pc ON pc.pubkey = g.pubkey LEFT JOIN \"Channels\" c ON c.\"ChanId\" = pc.chan_id AND (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY g.bucket_start, g.pubkey), rolling AS (SELECT df.bucket_start, df.pubkey, df.node_name, SUM(df.fee_sats) OVER w AS r_fee, SUM(COALESCE(dsc.swap_cost_sats, 0)) OVER w AS r_swap, SUM(COALESCE(dl.sat_days, 0)) OVER w AS r_satdays, COUNT(*) OVER w AS r_count FROM day_fees df LEFT JOIN day_swap_costs dsc ON dsc.bucket_start = df.bucket_start AND dsc.pubkey = df.pubkey LEFT JOIN day_liquidity dl ON dl.bucket_start = df.bucket_start AND dl.pubkey = df.pubkey WINDOW w AS (PARTITION BY df.pubkey ORDER BY df.bucket_start ROWS BETWEEN (CASE '$aprWindow' WHEN 'day' THEN 23 WHEN 'week' THEN 167 ELSE 719 END) PRECEDING AND CURRENT ROW)) SELECT t AS \"time\", m AS metric, v AS value FROM (SELECT bucket_start AS t, node_name || ' APR' AS m, CASE WHEN r_satdays > 0 THEN (r_fee - r_swap)::numeric * 365.0 / r_satdays ELSE NULL END AS v FROM rolling UNION ALL SELECT bucket_start AS t, node_name || ' Liquidity (BTC)' AS m, CASE WHEN r_count > 0 THEN (r_satdays * 24.0 / r_count / 100000000.0)::numeric(20,8) ELSE NULL END AS v FROM rolling) sub ORDER BY t, m", + "rawSql": "WITH days AS (SELECT generate_series(date_trunc('hour', $__timeFrom()::timestamptz), $__timeTo()::timestamptz, INTERVAL '1 hour') AS bucket_start), day_buckets AS (SELECT bucket_start, bucket_start + INTERVAL '1 hour' AS bucket_end FROM days), nodes AS (SELECT \"ManagedNodePubKey\" AS pubkey, MAX(\"ManagedNodeName\") AS node_name FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node) GROUP BY 1), participating_node_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\" AS pubkey, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" IN ($node)), day_node_grid AS (SELECT db.bucket_start, db.bucket_end, n.pubkey, n.node_name FROM day_buckets db CROSS JOIN nodes n), day_fees AS (SELECT g.bucket_start, g.pubkey, g.node_name, COALESCE(ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM day_node_grid g LEFT JOIN \"ForwardingHtlcEvents\" f ON f.\"EventTimestamp\" >= g.bucket_start AND f.\"EventTimestamp\" < g.bucket_end AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" = g.pubkey GROUP BY g.bucket_start, g.pubkey, g.node_name), day_yield_costs AS (SELECT g.bucket_start, g.pubkey, COALESCE(SUM(yc.cost_sats), 0) AS yield_cost_sats FROM day_node_grid g LEFT JOIN \"Nodes\" n ON n.\"PubKey\" = g.pubkey LEFT JOIN (SELECT s.\"NodeId\", s.\"CreationDatetime\" AS cost_time, $applySwapCosts * (COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS cost_sats FROM \"SwapOuts\" s WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") UNION ALL SELECT r.\"NodeId\", r.\"CreationDatetime\" AS cost_time, $applyRebalanceCosts * COALESCE(r.\"FeePaidSats\", 0) AS cost_sats FROM \"Rebalances\" r WHERE r.\"Status\" = 3 AND r.\"FeePaidSats\" IS NOT NULL AND $__timeFilter(r.\"CreationDatetime\")) yc ON yc.\"NodeId\" = n.\"Id\" AND yc.cost_time >= g.bucket_start AND yc.cost_time < g.bucket_end GROUP BY g.bucket_start, g.pubkey), day_liquidity AS (SELECT g.bucket_start, g.pubkey, COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE g.bucket_end END, g.bucket_end) - GREATEST(c.\"CreationDatetime\", g.bucket_start))) / 86400.0, 0.0)), 0) AS sat_days FROM day_node_grid g LEFT JOIN participating_node_chans pc ON pc.pubkey = g.pubkey LEFT JOIN \"Channels\" c ON c.\"ChanId\" = pc.chan_id AND (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY g.bucket_start, g.pubkey), rolling AS (SELECT df.bucket_start, df.pubkey, df.node_name, SUM(df.fee_sats) OVER w AS r_fee, SUM(COALESCE(dyc.yield_cost_sats, 0)) OVER w AS r_cost, SUM(COALESCE(dl.sat_days, 0)) OVER w AS r_satdays, COUNT(*) OVER w AS r_count FROM day_fees df LEFT JOIN day_yield_costs dyc ON dyc.bucket_start = df.bucket_start AND dyc.pubkey = df.pubkey LEFT JOIN day_liquidity dl ON dl.bucket_start = df.bucket_start AND dl.pubkey = df.pubkey WINDOW w AS (PARTITION BY df.pubkey ORDER BY df.bucket_start ROWS BETWEEN (CASE '$aprWindow' WHEN 'day' THEN 23 WHEN 'week' THEN 167 ELSE 719 END) PRECEDING AND CURRENT ROW)) SELECT t AS \"time\", m AS metric, v AS value FROM (SELECT bucket_start AS t, node_name || ' APR' AS m, CASE WHEN r_satdays > 0 THEN (r_fee - r_cost)::numeric * 365.0 / r_satdays ELSE NULL END AS v FROM rolling UNION ALL SELECT bucket_start AS t, node_name || ' Liquidity (BTC)' AS m, CASE WHEN r_count > 0 THEN (r_satdays * 24.0 / r_count / 100000000.0)::numeric(20,8) ELSE NULL END AS v FROM rolling) sub ORDER BY t, m", "refId": "A" } ], @@ -5156,7 +5348,7 @@ "text": "Yes", "value": "1" }, - "description": "When set to Yes, APR panels subtract completed swap-out costs (Service + Lightning + OnChain fees) from the routing-revenue numerator. When No, APR is gross routing yield only.", + "description": "When set to Yes, APR panels subtract completed swap-out costs (Service + Lightning + OnChain fees) from the routing-revenue numerator. When No, swap costs are ignored.", "hide": 0, "includeAll": false, "label": "Apply Swap Costs", @@ -5179,6 +5371,35 @@ "skipUrlSync": false, "type": "custom" }, + { + "current": { + "selected": true, + "text": "Yes", + "value": "1" + }, + "description": "When set to Yes, APR panels subtract succeeded rebalance fees from the routing-revenue numerator. When No, rebalance costs are ignored.", + "hide": 0, + "includeAll": false, + "label": "Apply Rebalance Costs", + "multi": false, + "name": "applyRebalanceCosts", + "options": [ + { + "selected": true, + "text": "Yes", + "value": "1" + }, + { + "selected": false, + "text": "No", + "value": "0" + } + ], + "query": "Yes : 1, No : 0", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, { "current": { "selected": true,