diff --git a/observability/README.md b/observability/README.md
index d11689d35..e1685db32 100644
--- a/observability/README.md
+++ b/observability/README.md
@@ -47,6 +47,28 @@ for the environment the board shows. `dashboard/src/lib/transfer-board.test.ts`
fails when the copy drifts, so change the SPA and the copy together. It runs in
the dashboard build of CI.
+The Dashboard tab's inventory is three Grafana tables, for their column filters:
+USD at Alpaca, USD on each chain, and Equities. Per-chain rows (USD · Onchain)
+and columns (Equities) come from the bot's `chain`-labelled series
+(`liq_usdc_chain_*`, `liq_usdc_corridor_*`, `liq_equity_chain_available`). The
+exporter does not publish them, so with `Source` on `exporter` the tables show
+the single Base row and column from the unlabelled series. The fallback is per
+source: while the bot publishes per-chain series, a chain without a labelled
+value shows none. The Equities Ratio is coloured only where it is the bot's own
+band verdict (a symbol rebalanced on Base, with a balance, with no vault on
+another chain, while the default Base target is set), so it is grey on the
+`exporter` source, which cannot see the other chains.
+
+Keep the Dashboard tab's panel heights (`TRADES_H`, `TRANSFERS_H` and the
+`native_inventory` heights in the generator) unless you check a new set on a
+preview board: reload it at a few window heights and confirm both columns end
+level and every table shows its rows. The tab link opens the board with
+`autofitpanels`, which scales and rounds every height, keeps the result, and
+fits it again on the next render. Whenever one column rounds a row taller than
+the window, the next pass shrinks everything again and the other column loses a
+row per pass. Most height sets do that on some window size; the current ones do
+not, for windows of 18 to 45 rows.
+
`alerting/liquidity.rules.yml` is the alert rules, in the `Alerts` folder. Each
rule's `uid` is permanent and must be unique across every repo's file. Removing
a rule from the file does not remove it from Grafana: add its `uid` to
diff --git a/observability/dashboards/liquidity/t0-liquidity.json b/observability/dashboards/liquidity/t0-liquidity.json
index e2438a864..473b0295c 100644
--- a/observability/dashboards/liquidity/t0-liquidity.json
+++ b/observability/dashboards/liquidity/t0-liquidity.json
@@ -212,8 +212,9 @@
{
"id": 2,
"type": "table",
- "title": "Inventory",
- "description": "The SPA's USDC row: Raindex + Alpaca Total + Inflight = Total; Ratio = Raindex/(Raindex+Alpaca Total); \u0394 = deviation from the target ratio in band widths, colored outside +/-1, where the SPA colors its bar.",
+ "pluginVersion": "13.1.0",
+ "title": "USD \u00b7 Alpaca",
+ "description": "Alpaca's cash: no rebalance target, so it shows its share of the USD total. The USD total is Alpaca, the primary chain's vault and the cash in flight; other chains' vaults are not in it. Rebalanceable = max(0, withdrawable - reserve).",
"datasource": {
"type": "stackdriver",
"uid": "cloudmon"
@@ -228,7 +229,7 @@
"queryType": "promQL",
"promQLQuery": {
"projectName": "$env",
- "expr": "sum by (row, col) (label_replace(label_replace(liq_usdc_onchain_available{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Raindex\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_inflight_total{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Inflight\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_alpaca_total{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Alpaca Total\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_total{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Total\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_ratio{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Ratio\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_ratio_deviation{job=~\"$source\"} and liq_usdc_total{job=~\"$source\"} > 0, \"row\", \"USDC\", \"\", \"\"), \"col\", \"\u0394\", \"\", \"\"))",
+ "expr": "sum by (venue, col) (label_replace(label_replace(liq_usdc_alpaca_total{job=~\"$source\"}, \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"cash\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace(liq_usdc_alpaca_usdc{job=~\"$source\"}, \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"usdc\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace((liq_settings_cash_reserved{job=~\"$source\"} or (liq_usdc_offchain_gross{job=~\"$source\"} - liq_usdc_offchain_available{job=~\"$source\"}) or (liq_usdc_alpaca_total{job=~\"$source\"} - liq_usdc_offchain_available{job=~\"$source\"})), \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"reserve\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace(liq_usdc_rebalanceable{job=~\"$source\"}, \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"rebalanceable\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace(liq_usdc_offchain_inflight{job=~\"$source\"}, \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"inflight\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace(round(100 * (liq_usdc_alpaca_total{job=~\"$source\"} / liq_usdc_total{job=~\"$source\"}), 0.1), \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"share\", \"\", \"\")) or sum by (venue, col) (label_replace(label_replace(liq_usdc_total{job=~\"$source\"}, \"venue\", \"Alpaca\", \"\", \"\"), \"col\", \"total\", \"\", \"\"))",
"step": "60s"
},
"timeSeriesList": {
@@ -241,8 +242,8 @@
}
],
"gridPos": {
- "h": 4,
- "w": 7,
+ "h": 8,
+ "w": 13,
"x": 0,
"y": 1
},
@@ -259,7 +260,7 @@
"id": "groupBy",
"options": {
"fields": {
- "row": {
+ "venue": {
"operation": "groupby",
"aggregations": []
},
@@ -280,7 +281,7 @@
"id": "groupingToMatrix",
"options": {
"columnField": "col",
- "rowField": "row",
+ "rowField": "venue",
"valueField": "Value (lastNotNull)",
"emptyValue": "null"
}
@@ -289,38 +290,56 @@
"id": "organize",
"options": {
"excludeByName": {},
- "renameByName": {
- "row\\col": "Asset"
- },
"indexByName": {
- "row\\col": 0,
- "Raindex": 1,
- "Inflight": 2,
- "Alpaca Total": 3,
- "Total": 4,
- "Ratio": 5,
- "\u0394": 6
+ "venue\\col": 0,
+ "cash": 1,
+ "usdc": 2,
+ "reserve": 3,
+ "rebalanceable": 4,
+ "inflight": 5,
+ "share": 6,
+ "total": 7
+ },
+ "renameByName": {
+ "venue\\col": "Venue",
+ "cash": "Cash",
+ "usdc": "USDC",
+ "reserve": "Reserve",
+ "rebalanceable": "Rebalanceable",
+ "inflight": "In flight",
+ "share": "Share of total",
+ "total": "USD total"
}
}
+ },
+ {
+ "id": "convertFieldType",
+ "options": {
+ "conversions": [
+ {
+ "targetField": "share",
+ "destinationType": "string"
+ }
+ ]
+ }
}
],
"fieldConfig": {
"defaults": {
- "unit": "short",
+ "unit": "locale",
"decimals": 2,
"custom": {
- "align": "auto",
- "filterable": false,
+ "align": "right",
+ "filterable": true,
"cellOptions": {
"type": "auto"
- },
- "minWidth": 40
+ }
},
"thresholds": {
"mode": "absolute",
"steps": [
{
- "color": "text",
+ "color": "red",
"value": null
}
]
@@ -331,186 +350,147 @@
{
"matcher": {
"id": "byName",
- "options": "Asset"
+ "options": "Venue"
},
"properties": [
+ {
+ "id": "custom.align",
+ "value": "left"
+ },
{
"id": "custom.width",
- "value": 50
+ "value": 80
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Inflight"
+ "options": "Cash"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 80
+ "value": 110
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Raindex"
+ "options": "USDC"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 90
+ "value": 140
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Alpaca Total"
+ "options": "Reserve"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 90
+ "value": 95
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Total"
+ "options": "Rebalanceable"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 90
+ "value": 140
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Ratio"
+ "options": "In flight"
},
"properties": [
{
- "id": "unit",
- "value": "percentunit"
- },
+ "id": "custom.width",
+ "value": 95
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Share of total"
+ },
+ "properties": [
{
- "id": "decimals",
- "value": 1
+ "id": "mappings",
+ "value": [
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(null|NaN)?$",
+ "result": {
+ "text": " ",
+ "index": 0
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(.*)$",
+ "result": {
+ "text": "
$1%
",
+ "index": 1
+ }
+ }
+ }
+ ]
},
{
"id": "custom.cellOptions",
"value": {
- "type": "gauge",
- "mode": "basic",
- "valueDisplayMode": "text"
+ "type": "markdown"
}
},
{
- "id": "min",
- "value": 0
- },
- {
- "id": "max",
- "value": 1
- },
- {
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "blue",
- "value": null
- }
- ]
- }
+ "id": "custom.align",
+ "value": "center"
},
{
- "id": "custom.width",
- "value": 75
+ "id": "custom.filterable",
+ "value": false
}
]
},
{
"matcher": {
"id": "byName",
- "options": "\u0394"
+ "options": "USD total"
},
"properties": [
- {
- "id": "unit",
- "value": "short"
- },
- {
- "id": "decimals",
- "value": 1
- },
- {
- "id": "custom.cellOptions",
- "value": {
- "type": "color-text"
- }
- },
{
"id": "custom.width",
- "value": 45
- },
+ "value": 120
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Share of total"
+ },
+ "properties": [
{
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "red",
- "value": null
- },
- {
- "color": "text",
- "value": -1
- },
- {
- "color": "green",
- "value": 1
- }
- ]
- }
+ "id": "custom.minWidth",
+ "value": 60
}
]
}
@@ -518,7 +498,7 @@
},
"options": {
"showHeader": true,
- "cellHeight": "sm"
+ "cellHeight": "md"
},
"timeFrom": "5m",
"hideTimeOverride": false
@@ -526,8 +506,9 @@
{
"id": 3,
"type": "table",
- "title": "Alpaca",
- "description": "Rebalanceable = max(0, withdrawable \u2212 reserve). Counter-tradeable deliberately equals Alpaca Total (reserve NOT subtracted, same as the SPA).",
+ "pluginVersion": "13.1.0",
+ "title": "USD \u00b7 Onchain",
+ "description": "Each chain's vault and the ratio the bot rebalances on, vault / (vault + Alpaca cash), against the chain's band. It is not a share of all USD: other chains are not in it. Ethereum is the hub wallet Alpaca deposits to and withdraws from, with no band.",
"datasource": {
"type": "stackdriver",
"uid": "cloudmon"
@@ -542,7 +523,7 @@
"queryType": "promQL",
"promQLQuery": {
"projectName": "$env",
- "expr": "sum by (row, col) (label_replace(label_replace(liq_usdc_alpaca_usdc{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"USDC\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_rebalanceable{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Rebalanceable\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_alpaca_total{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Counter-tradeable\", \"\", \"\"))",
+ "expr": "sum by (venue, col) (label_replace(max by (venue) (label_replace(liq_usdc_chain_available{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_usdc_onchain_available{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})), \"col\", \"vault\", \"\", \"\")) or sum by (venue, col) (label_replace(max by (venue) (label_replace(liq_usdc_chain_inflight{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_usdc_onchain_inflight{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})), \"col\", \"inflight\", \"\", \"\")) or sum by (venue, col) (label_replace(max by (venue) (label_replace(liq_usdc_inflight_base_wallet{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) or max by (venue) (label_replace(liq_usdc_inflight_ethereum_wallet{job=~\"$source\"}, \"venue\", \"ethereum\", \"\", \"\")), \"col\", \"wallet\", \"\", \"\")) or sum by (venue, col) (label_replace((round(100 * (max by (venue) (label_replace(liq_usdc_chain_ratio{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_usdc_ratio{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"}))), 0.1) + 0.0001) * (2 * (abs((max by (venue) (label_replace(liq_usdc_chain_ratio{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_usdc_ratio{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"}))) - (max by (venue) (label_replace(liq_usdc_corridor_target{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_settings_usdc_target{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})))) <= bool (max by (venue) (label_replace(liq_usdc_corridor_deviation{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_settings_usdc_deviation{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})))) - 1), \"col\", \"ratio\", \"\", \"\")) or sum by (venue, col) (label_replace(max by (venue) (label_replace(liq_usdc_corridor_target{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_settings_usdc_target{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})), \"col\", \"target\", \"\", \"\")) or sum by (venue, col) (label_replace(max by (venue) (label_replace(liq_usdc_corridor_deviation{job=~\"$source\"}, \"venue\", \"$1\", \"chain\", \"(.*)\")) or (max by (venue) (label_replace(liq_settings_usdc_deviation{job=~\"$source\"}, \"venue\", \"base\", \"\", \"\")) unless on () count(liq_usdc_chain_available{job=~\"$source\"})), \"col\", \"deviation\", \"\", \"\"))",
"step": "60s"
},
"timeSeriesList": {
@@ -555,10 +536,10 @@
}
],
"gridPos": {
- "h": 4,
- "w": 4,
- "x": 7,
- "y": 1
+ "h": 9,
+ "w": 13,
+ "x": 0,
+ "y": 9
},
"transformations": [
{
@@ -573,7 +554,7 @@
"id": "groupBy",
"options": {
"fields": {
- "row": {
+ "venue": {
"operation": "groupby",
"aggregations": []
},
@@ -594,7 +575,7 @@
"id": "groupingToMatrix",
"options": {
"columnField": "col",
- "rowField": "row",
+ "rowField": "venue",
"valueField": "Value (lastNotNull)",
"emptyValue": "null"
}
@@ -602,38 +583,55 @@
{
"id": "organize",
"options": {
- "excludeByName": {
- "row\\col": true
+ "excludeByName": {},
+ "indexByName": {
+ "venue\\col": 0,
+ "vault": 1,
+ "inflight": 2,
+ "wallet": 3,
+ "ratio": 4,
+ "target": 5,
+ "deviation": 6
},
"renameByName": {
- "row\\col": "Asset"
- },
- "indexByName": {
- "row\\col": 0,
- "USDC": 1,
- "Rebalanceable": 2,
- "Counter-tradeable": 3
+ "venue\\col": "Chain",
+ "vault": "Vault",
+ "inflight": "In flight",
+ "wallet": "Wallet",
+ "ratio": "Ratio",
+ "target": "Target",
+ "deviation": "\u00b1"
}
}
+ },
+ {
+ "id": "convertFieldType",
+ "options": {
+ "conversions": [
+ {
+ "targetField": "ratio",
+ "destinationType": "string"
+ }
+ ]
+ }
}
],
"fieldConfig": {
"defaults": {
- "unit": "short",
+ "unit": "locale",
"decimals": 2,
"custom": {
- "align": "auto",
- "filterable": false,
+ "align": "right",
+ "filterable": true,
"cellOptions": {
"type": "auto"
- },
- "minWidth": 40
+ }
},
"thresholds": {
"mode": "absolute",
"steps": [
{
- "color": "text",
+ "color": "red",
"value": null
}
]
@@ -644,218 +642,187 @@
{
"matcher": {
"id": "byName",
- "options": "USDC"
+ "options": "Chain"
},
"properties": [
{
- "id": "unit",
- "value": "locale"
+ "id": "custom.align",
+ "value": "left"
},
{
- "id": "decimals",
- "value": 2
+ "id": "custom.width",
+ "value": 125
},
{
- "id": "custom.width",
- "value": 90
+ "id": "mappings",
+ "value": [
+ {
+ "type": "value",
+ "options": {
+ "base": {
+ "text": "Base",
+ "index": 0
+ },
+ "robinhood": {
+ "text": "Robinhood",
+ "index": 1
+ },
+ "hyperevm": {
+ "text": "HyperEVM",
+ "index": 2
+ },
+ "ethereum": {
+ "text": "Ethereum (hub)",
+ "index": 3
+ }
+ }
+ }
+ ]
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Rebalanceable"
+ "options": "Vault"
},
"properties": [
{
- "id": "unit",
- "value": "locale"
- },
+ "id": "custom.width",
+ "value": 110
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "In flight"
+ },
+ "properties": [
{
- "id": "decimals",
- "value": 2
- },
+ "id": "custom.width",
+ "value": 95
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Wallet"
+ },
+ "properties": [
{
"id": "custom.width",
- "value": 105
+ "value": 85
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Counter-tradeable"
+ "options": "Ratio"
},
"properties": [
{
- "id": "unit",
- "value": "locale"
+ "id": "mappings",
+ "value": [
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(null|NaN)?$",
+ "result": {
+ "text": " ",
+ "index": 0
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^-(\\d+(?:\\.[1-9])?).*$",
+ "result": {
+ "text": "$1%
",
+ "index": 1
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(\\d+(?:\\.[1-9])?).*$",
+ "result": {
+ "text": "$1%
",
+ "index": 2
+ }
+ }
+ }
+ ]
},
{
- "id": "decimals",
- "value": 2
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "markdown"
+ }
+ },
+ {
+ "id": "custom.align",
+ "value": "center"
+ },
+ {
+ "id": "custom.filterable",
+ "value": false
}
]
- }
- ]
- },
- "options": {
- "showHeader": true,
- "cellHeight": "sm"
- },
- "timeFrom": "5m",
- "hideTimeOverride": false
- },
- {
- "id": 4,
- "type": "table",
- "title": "Wallets",
- "description": "USDC seen in the bot's Ethereum and Base wallets: sanity checks, not part of Total.",
- "datasource": {
- "type": "stackdriver",
- "uid": "cloudmon"
- },
- "targets": [
- {
- "refId": "A",
- "datasource": {
- "type": "stackdriver",
- "uid": "cloudmon"
- },
- "queryType": "promQL",
- "promQLQuery": {
- "projectName": "$env",
- "expr": "sum by (row, col) (label_replace(label_replace(liq_usdc_inflight_ethereum_wallet{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Eth\", \"\", \"\")) or sum by (row, col) (label_replace(label_replace(liq_usdc_inflight_base_wallet{job=~\"$source\"}, \"row\", \"USDC\", \"\", \"\"), \"col\", \"Base\", \"\", \"\"))",
- "step": "60s"
},
- "timeSeriesList": {
- "projectName": "",
- "filters": [],
- "view": "FULL",
- "groupBys": []
- },
- "instant": true
- }
- ],
- "gridPos": {
- "h": 4,
- "w": 3,
- "x": 11,
- "y": 1
- },
- "transformations": [
- {
- "id": "labelsToFields",
- "options": {}
- },
- {
- "id": "merge",
- "options": {}
- },
- {
- "id": "groupBy",
- "options": {
- "fields": {
- "row": {
- "operation": "groupby",
- "aggregations": []
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Target"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "percentunit"
},
- "col": {
- "operation": "groupby",
- "aggregations": []
+ {
+ "id": "decimals",
+ "value": 0
},
- "Value": {
- "operation": "aggregate",
- "aggregations": [
- "lastNotNull"
- ]
- }
- }
- }
- },
- {
- "id": "groupingToMatrix",
- "options": {
- "columnField": "col",
- "rowField": "row",
- "valueField": "Value (lastNotNull)",
- "emptyValue": "null"
- }
- },
- {
- "id": "organize",
- "options": {
- "excludeByName": {
- "row\\col": true
- },
- "renameByName": {
- "row\\col": "Asset"
- },
- "indexByName": {
- "row\\col": 0,
- "Eth": 1,
- "Base": 2
- }
- }
- }
- ],
- "fieldConfig": {
- "defaults": {
- "unit": "short",
- "decimals": 2,
- "custom": {
- "align": "auto",
- "filterable": false,
- "cellOptions": {
- "type": "auto"
- },
- "minWidth": 40
- },
- "thresholds": {
- "mode": "absolute",
- "steps": [
{
- "color": "text",
- "value": null
+ "id": "custom.width",
+ "value": 85
}
]
},
- "mappings": []
- },
- "overrides": [
{
"matcher": {
"id": "byName",
- "options": "Eth"
+ "options": "\u00b1"
},
"properties": [
{
"id": "unit",
- "value": "locale"
+ "value": "percentunit"
},
{
"id": "decimals",
- "value": 2
+ "value": 0
},
{
"id": "custom.width",
- "value": 80
+ "value": 60
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Base"
+ "options": "Ratio"
},
"properties": [
{
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
+ "id": "custom.minWidth",
+ "value": 60
}
]
}
@@ -863,16 +830,17 @@
},
"options": {
"showHeader": true,
- "cellHeight": "sm"
+ "cellHeight": "md"
},
"timeFrom": "5m",
- "hideTimeOverride": true
+ "hideTimeOverride": false
},
{
- "id": 5,
+ "id": 4,
"type": "table",
- "title": "",
- "description": "The SPA inventory equity table: CT / Rebal / Ext status lights from settings; Raindex / Inflight / Alpaca / Total share balances; Ratio = onchain/(onchain+offchain); \u0394 = deviation from the target ratio in band widths, colored outside +/-1; Exposure = net \u00d7 last price. Unwrapped/Wrapped are wallet-observed and not part of Total. Sorted CT-first (desc) so counter-tradeable assets float to the top, like the SPA.",
+ "pluginVersion": "13.1.0",
+ "title": "Equities",
+ "description": "Flags: counter trading, rebalancing and extended hours, green when on, red when off, hollow grey when not set. Share balances per venue, one column per chain. Total and Total USD count only the primary chain (Base) with Alpaca and the shares in flight: a wrapped share on another chain is worth that chain's underlying, so the chains are not added together. Ratio = Base vault available / (Base vault available + Alpaca available), green inside the default Base band and red outside it. It is grey, with no verdict, where that is not the bot's own band: a symbol not rebalanced on Base, with no balance, with a vault on another chain, or with no default target, and every symbol on the exporter source, which cannot see the other chains. A click on Ratio sorts by its encoded text, not by percent. Exposure = net x live price, empty without a price.",
"datasource": {
"type": "stackdriver",
"uid": "cloudmon"
@@ -887,7 +855,7 @@
"queryType": "promQL",
"promQLQuery": {
"projectName": "$env",
- "expr": "sum by (symbol, col) (label_replace(liq_asset_counter_trading{job=~\"$source\"}, \"col\", \"CT\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_asset_rebalancing{job=~\"$source\"}, \"col\", \"Rebal\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_asset_extended_hours{job=~\"$source\"}, \"col\", \"Ext\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_onchain_available{job=~\"$source\"}, \"col\", \"Raindex\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_inflight_total{job=~\"$source\"}, \"col\", \"Inflight\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_offchain_available{job=~\"$source\"}, \"col\", \"Alpaca\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_total{job=~\"$source\"}, \"col\", \"Total\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_ratio{job=~\"$source\"}, \"col\", \"Ratio\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_ratio_deviation{job=~\"$source\"} and liq_equity_total{job=~\"$source\"} > 0, \"col\", \"\u0394\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_exposure_usd{job=~\"$source\"} or (liq_equity_total{job=~\"$source\"} * NaN), \"col\", \"Exposure\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_unwrapped{job=~\"$source\"}, \"col\", \"Unwrapped\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_wrapped{job=~\"$source\"}, \"col\", \"Wrapped\", \"\", \"\"))",
+ "expr": "sum by (symbol, col) (label_replace((max by (symbol) (liq_asset_counter_trading{job=~\"$source\"}) + 1 or (max by (symbol) (liq_equity_total{job=~\"$source\"}) * 0 + 3)) * 100 + (max by (symbol) (liq_asset_rebalancing{job=~\"$source\"}) + 1 or (max by (symbol) (liq_equity_total{job=~\"$source\"}) * 0 + 3)) * 10 + (max by (symbol) (liq_asset_extended_hours{job=~\"$source\"}) + 1 or (max by (symbol) (liq_equity_total{job=~\"$source\"}) * 0 + 3)), \"col\", \"flags\", \"\", \"\")) or max by (symbol, col) (label_replace(liq_equity_chain_available{job=~\"$source\"}, \"col\", \"onchain_$1\", \"chain\", \"(.*)\")) or (max by (symbol, col) (label_replace(liq_equity_onchain_available{job=~\"$source\"}, \"col\", \"onchain_base\", \"\", \"\")) unless on () count(liq_equity_chain_available{job=~\"$source\"})) or sum by (symbol, col) (label_replace(liq_equity_inflight_total{job=~\"$source\"}, \"col\", \"inflight\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_offchain_available{job=~\"$source\"}, \"col\", \"alpaca\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_total{job=~\"$source\"}, \"col\", \"total\", \"\", \"\")) or sum by (symbol, col) (label_replace((liq_equity_total{job=~\"$source\"} * on (symbol) group_left max by (symbol) (liq_position_last_price_usd{job=~\"$source\"})) or (liq_equity_total{job=~\"$source\"} * NaN), \"col\", \"total_usd\", \"\", \"\")) or sum by (symbol, col) (label_replace(((round(100 * (liq_equity_ratio{job=~\"$source\"}), 0.1) + 0.0001) * (2 * (abs((liq_equity_ratio{job=~\"$source\"}) - scalar(max(liq_settings_equity_target{job=~\"$source\"}))) <= bool scalar(max(liq_settings_equity_deviation{job=~\"$source\"}))) - 1) and on (symbol) ((liq_equity_total{job=~\"$source\"} > 0) and on (symbol) (max by (symbol) (liq_asset_rebalancing{job=~\"$source\"}) == 1) and on () count(liq_settings_equity_target{job=~\"$source\"}) and on (symbol) max by (symbol) (liq_equity_chain_available{job=~\"$source\",chain=\"base\"}) unless on (symbol) max by (symbol) (liq_equity_chain_available{job=~\"$source\",chain!=\"base\"}))) or (round(100 * (liq_equity_ratio{job=~\"$source\"}), 0.1) + 1000 + 0.0001 unless on (symbol) ((liq_equity_total{job=~\"$source\"} > 0) and on (symbol) (max by (symbol) (liq_asset_rebalancing{job=~\"$source\"}) == 1) and on () count(liq_settings_equity_target{job=~\"$source\"}) and on (symbol) max by (symbol) (liq_equity_chain_available{job=~\"$source\",chain=\"base\"}) unless on (symbol) max by (symbol) (liq_equity_chain_available{job=~\"$source\",chain!=\"base\"}))), \"col\", \"ratio\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_exposure_usd{job=~\"$source\"} or (liq_equity_total{job=~\"$source\"} * NaN), \"col\", \"exposure\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_unwrapped{job=~\"$source\"}, \"col\", \"unwrapped\", \"\", \"\")) or sum by (symbol, col) (label_replace(liq_equity_wrapped{job=~\"$source\"}, \"col\", \"wrapped\", \"\", \"\"))",
"step": "60s"
},
"timeSeriesList": {
@@ -900,10 +868,10 @@
}
],
"gridPos": {
- "h": 24,
- "w": 14,
+ "h": 35,
+ "w": 13,
"x": 0,
- "y": 5
+ "y": 18
},
"transformations": [
{
@@ -948,34 +916,61 @@
"id": "organize",
"options": {
"excludeByName": {},
- "renameByName": {
- "symbol\\col": "Asset"
- },
"indexByName": {
"symbol\\col": 0,
- "CT": 1,
- "Rebal": 2,
- "Ext": 3,
- "Raindex": 4,
- "Inflight": 5,
- "Alpaca": 6,
- "Total": 7,
- "Ratio": 8,
- "\u0394": 9,
- "Exposure": 10,
- "Unwrapped": 11,
- "Wrapped": 12
+ "flags": 1,
+ "onchain_base": 2,
+ "onchain_robinhood": 3,
+ "onchain_hyperevm": 4,
+ "inflight": 5,
+ "alpaca": 6,
+ "total": 7,
+ "total_usd": 8,
+ "ratio": 9,
+ "exposure": 10,
+ "unwrapped": 11,
+ "wrapped": 12
+ },
+ "renameByName": {
+ "symbol\\col": "Asset",
+ "flags": "Flags",
+ "onchain_base": "Base",
+ "onchain_robinhood": "Robinhood",
+ "onchain_hyperevm": "HyperEVM",
+ "inflight": "Inflight",
+ "alpaca": "Alpaca",
+ "total": "Total",
+ "total_usd": "Total USD",
+ "ratio": "Ratio",
+ "exposure": "Exposure",
+ "unwrapped": "Unwrapped",
+ "wrapped": "Wrapped"
}
}
+ },
+ {
+ "id": "convertFieldType",
+ "options": {
+ "conversions": [
+ {
+ "targetField": "ratio",
+ "destinationType": "string"
+ },
+ {
+ "targetField": "flags",
+ "destinationType": "string"
+ }
+ ]
+ }
}
],
"fieldConfig": {
"defaults": {
- "unit": "short",
+ "unit": "locale",
"decimals": 2,
"custom": {
- "align": "auto",
- "filterable": false,
+ "align": "left",
+ "filterable": true,
"cellOptions": {
"type": "auto"
}
@@ -984,7 +979,7 @@
"mode": "absolute",
"steps": [
{
- "color": "text",
+ "color": "red",
"value": null
}
]
@@ -998,150 +993,292 @@
"options": "Asset"
},
"properties": [
+ {
+ "id": "custom.align",
+ "value": "left"
+ },
{
"id": "custom.width",
- "value": 60
+ "value": 96
}
]
},
{
"matcher": {
"id": "byName",
- "options": "CT"
+ "options": "Flags"
},
"properties": [
{
"id": "mappings",
"value": [
{
- "type": "value",
+ "type": "regex",
"options": {
- "1": {
- "text": "\u25cf",
- "color": "green",
+ "pattern": "^111$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
"index": 0
- },
- "0": {
- "text": "\u25cf",
- "color": "red",
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^112$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
"index": 1
}
}
},
{
- "type": "special",
+ "type": "regex",
"options": {
- "match": "null+nan",
+ "pattern": "^113$",
"result": {
- "text": "\u25cb",
- "color": "text",
+ "text": "\u25cf \u25cf \u25cb",
"index": 2
}
}
- }
- ]
- },
- {
- "id": "custom.cellOptions",
- "value": {
- "type": "color-text"
- }
- },
- {
- "id": "custom.align",
- "value": "center"
- },
- {
- "id": "custom.width",
- "value": 40
- }
- ]
- },
- {
- "matcher": {
- "id": "byName",
- "options": "Rebal"
- },
- "properties": [
- {
- "id": "mappings",
- "value": [
+ },
{
- "type": "value",
+ "type": "regex",
"options": {
- "1": {
- "text": "\u25cf",
- "color": "green",
- "index": 0
- },
- "0": {
- "text": "\u25cf",
- "color": "red",
- "index": 1
+ "pattern": "^121$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 3
}
}
},
{
- "type": "special",
+ "type": "regex",
"options": {
- "match": "null+nan",
+ "pattern": "^122$",
"result": {
- "text": "\u25cb",
- "color": "text",
- "index": 2
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 4
}
}
- }
- ]
- },
- {
- "id": "custom.cellOptions",
- "value": {
- "type": "color-text"
- }
- },
- {
- "id": "custom.align",
- "value": "center"
- },
- {
- "id": "custom.width",
- "value": 40
- }
- ]
- },
- {
- "matcher": {
- "id": "byName",
- "options": "Ext"
- },
- "properties": [
- {
- "id": "mappings",
- "value": [
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^123$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cb",
+ "index": 5
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^131$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cf",
+ "index": 6
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^132$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cf",
+ "index": 7
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^133$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cb",
+ "index": 8
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^211$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 9
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^212$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 10
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^213$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cb",
+ "index": 11
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^221$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 12
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^222$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cf",
+ "index": 13
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^223$",
+ "result": {
+ "text": "\u25cf \u25cf \u25cb",
+ "index": 14
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^231$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cf",
+ "index": 15
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^232$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cf",
+ "index": 16
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^233$",
+ "result": {
+ "text": "\u25cf \u25cb \u25cb",
+ "index": 17
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^311$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cf",
+ "index": 18
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^312$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cf",
+ "index": 19
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^313$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cb",
+ "index": 20
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^321$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cf",
+ "index": 21
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^322$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cf",
+ "index": 22
+ }
+ }
+ },
{
- "type": "value",
+ "type": "regex",
"options": {
- "1": {
- "text": "\u25cf",
- "color": "green",
- "index": 0
- },
- "0": {
- "text": "\u25cf",
- "color": "red",
- "index": 1
+ "pattern": "^323$",
+ "result": {
+ "text": "\u25cb \u25cf \u25cb",
+ "index": 23
}
}
},
{
- "type": "special",
+ "type": "regex",
"options": {
- "match": "null+nan",
+ "pattern": "^331$",
"result": {
- "text": "\u25cb",
- "color": "text",
- "index": 2
+ "text": "\u25cb \u25cb \u25cf",
+ "index": 24
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^332$",
+ "result": {
+ "text": "\u25cb \u25cb \u25cf",
+ "index": 25
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^333$",
+ "result": {
+ "text": "\u25cb \u25cb \u25cb",
+ "index": 26
}
}
}
@@ -1150,230 +1287,195 @@
{
"id": "custom.cellOptions",
"value": {
- "type": "color-text"
+ "type": "markdown"
}
},
{
"id": "custom.align",
"value": "center"
},
+ {
+ "id": "custom.filterable",
+ "value": false
+ },
{
"id": "custom.width",
- "value": 40
+ "value": 64
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Raindex"
+ "options": "Base"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 70
+ "value": 92
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Inflight"
+ "options": "Robinhood"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 70
+ "value": 92
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Alpaca"
+ "options": "HyperEVM"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 70
+ "value": 92
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Total"
+ "options": "Inflight"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 70
+ "value": 86
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Wrapped"
+ "options": "Alpaca"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 70
+ "value": 86
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Unwrapped"
+ "options": "Total"
},
"properties": [
- {
- "id": "unit",
- "value": "locale"
- },
- {
- "id": "decimals",
- "value": 2
- },
{
"id": "custom.width",
- "value": 80
+ "value": 74
}
]
},
{
"matcher": {
"id": "byName",
- "options": "Ratio"
+ "options": "Total USD"
},
"properties": [
{
"id": "unit",
- "value": "percentunit"
+ "value": "currencyUSD"
},
{
"id": "decimals",
- "value": 1
- },
- {
- "id": "custom.cellOptions",
- "value": {
- "type": "gauge",
- "mode": "basic",
- "valueDisplayMode": "text"
- }
- },
- {
- "id": "min",
"value": 0
},
{
- "id": "max",
- "value": 1
- },
- {
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "blue",
- "value": null
+ "id": "mappings",
+ "value": [
+ {
+ "type": "special",
+ "options": {
+ "match": "null+nan",
+ "result": {
+ "text": "\u2014",
+ "color": "#6b7280",
+ "index": 0
+ }
}
- ]
- }
+ }
+ ]
},
{
"id": "custom.width",
- "value": 140
+ "value": 100
}
]
},
{
"matcher": {
"id": "byName",
- "options": "\u0394"
+ "options": "Ratio"
},
"properties": [
{
- "id": "unit",
- "value": "short"
- },
- {
- "id": "decimals",
- "value": 1
+ "id": "mappings",
+ "value": [
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(null|NaN)?$",
+ "result": {
+ "text": " ",
+ "index": 0
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(?=\\d{4})10*(\\d+(?:\\.[1-9])?).*$",
+ "result": {
+ "text": "$1%
",
+ "index": 1
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^-(\\d+(?:\\.[1-9])?).*$",
+ "result": {
+ "text": "$1%
",
+ "index": 2
+ }
+ }
+ },
+ {
+ "type": "regex",
+ "options": {
+ "pattern": "^(\\d+(?:\\.[1-9])?).*$",
+ "result": {
+ "text": "$1%
",
+ "index": 3
+ }
+ }
+ }
+ ]
},
{
"id": "custom.cellOptions",
"value": {
- "type": "color-text"
+ "type": "markdown"
}
},
{
- "id": "custom.width",
- "value": 45
+ "id": "custom.align",
+ "value": "center"
},
{
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "red",
- "value": null
- },
- {
- "color": "text",
- "value": -1
- },
- {
- "color": "green",
- "value": 1
- }
- ]
- }
+ "id": "custom.filterable",
+ "value": false
}
]
},
@@ -1388,14 +1490,20 @@
"value": "currencyUSD"
},
{
- "id": "decimals",
- "value": 2
- },
- {
- "id": "custom.cellOptions",
- "value": {
- "type": "color-text"
- }
+ "id": "mappings",
+ "value": [
+ {
+ "type": "special",
+ "options": {
+ "match": "null+nan",
+ "result": {
+ "text": "\u2014",
+ "color": "#6b7280",
+ "index": 0
+ }
+ }
+ }
+ ]
},
{
"id": "thresholds",
@@ -1408,34 +1516,68 @@
},
{
"color": "text",
- "value": -0.01
+ "value": -0.005
},
{
"color": "green",
- "value": 0.01
+ "value": 0.005
}
]
}
},
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "color-text"
+ }
+ },
{
"id": "custom.width",
- "value": 80
+ "value": 95
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Unwrapped"
+ },
+ "properties": [
+ {
+ "id": "custom.align",
+ "value": "right"
},
{
- "id": "mappings",
- "value": [
- {
- "type": "special",
- "options": {
- "match": "null+nan",
- "result": {
- "text": "\u2014",
- "color": "text",
- "index": 0
- }
- }
- }
- ]
+ "id": "custom.width",
+ "value": 118
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Wrapped"
+ },
+ "properties": [
+ {
+ "id": "custom.align",
+ "value": "right"
+ },
+ {
+ "id": "custom.width",
+ "value": 102
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Ratio"
+ },
+ "properties": [
+ {
+ "id": "custom.minWidth",
+ "value": 60
}
]
}
@@ -1443,11 +1585,11 @@
},
"options": {
"showHeader": true,
- "cellHeight": "sm",
+ "cellHeight": "md",
"sortBy": [
{
- "displayName": "CT",
- "desc": true
+ "displayName": "Asset",
+ "desc": false
}
]
},
@@ -1455,7 +1597,7 @@
"hideTimeOverride": false
},
{
- "id": 6,
+ "id": 5,
"type": "table",
"title": "Trade History",
"description": "Direct Raindex fills, fills routed through supported adapters such as Bebop, and the corresponding Alpaca hedge trades placed to offset exposure. One row per trade at its latest status. Built from the newest 500 status entries, so a busy period can push older rows out of the 30-day window.",
@@ -1478,9 +1620,9 @@
}
],
"gridPos": {
- "h": 14,
- "w": 10,
- "x": 14,
+ "h": 25,
+ "w": 11,
+ "x": 13,
"y": 1
},
"transformations": [
@@ -1890,7 +2032,7 @@
}
},
{
- "id": 7,
+ "id": 6,
"type": "table",
"title": "Cross-venue Transfers",
"description": "Asset movements between venues to rebalance inventory: equity mints (Alpaca to onchain), redemptions (onchain to Alpaca), and USDC bridges (Base/Ethereum via CCTP). One row per transfer at its latest status. A USDC bridge can show the previous status for a while: the exporter stamps each status with the transfer's updatedAt, which is not always later than the one before (bridging starts from initiated_at). Built from the newest 500 status entries, so a busy period can push older rows out of the 30-day window.",
@@ -1913,10 +2055,10 @@
}
],
"gridPos": {
- "h": 14,
- "w": 10,
- "x": 14,
- "y": 15
+ "h": 26,
+ "w": 11,
+ "x": 13,
+ "y": 26
},
"transformations": [
{
diff --git a/observability/dashboards/liquidity/tabs/t0-liquidity-logs.json b/observability/dashboards/liquidity/tabs/t0-liquidity-logs.json
index 428d76e7e..239bcf02d 100644
--- a/observability/dashboards/liquidity/tabs/t0-liquidity-logs.json
+++ b/observability/dashboards/liquidity/tabs/t0-liquidity-logs.json
@@ -308,7 +308,7 @@
],
"panels": [
{
- "id": 84,
+ "id": 83,
"type": "marcusolsson-dynamictext-panel",
"title": "",
"description": "The SPA header: settings pills, Config, the CLI recovery guide, the deployed commit, uptime, and whether the bot is up: the exporter's liq_up, or the bot scrape's up on the bot source.",
@@ -372,7 +372,7 @@
"hideTimeOverride": true
},
{
- "id": 85,
+ "id": 84,
"type": "table",
"title": "Log History",
"description": "The bot's structured log lines, re-emitted by the exporter with real severity and target (the raw container stream is all-INFO). Filter with the level/category/target/search variables above. \u26a0\ufe0f Only the newest matching entries, about the panel's pixel width of them. Narrow the filters, or use the SPA for deep digs. DEBUG/TRACE are not shipped.",
diff --git a/observability/dashboards/liquidity/tabs/t0-liquidity-orders.json b/observability/dashboards/liquidity/tabs/t0-liquidity-orders.json
index d1982327d..77d949033 100644
--- a/observability/dashboards/liquidity/tabs/t0-liquidity-orders.json
+++ b/observability/dashboards/liquidity/tabs/t0-liquidity-orders.json
@@ -146,7 +146,7 @@
],
"panels": [
{
- "id": 8,
+ "id": 7,
"type": "marcusolsson-dynamictext-panel",
"title": "",
"description": "The SPA header: settings pills, Config, the CLI recovery guide, the deployed commit, uptime, and whether the bot is up: the exporter's liq_up, or the bot scrape's up on the bot source.",
@@ -210,7 +210,7 @@
"hideTimeOverride": true
},
{
- "id": 9,
+ "id": 8,
"type": "table",
"title": "Raindex Orders",
"description": "The SPA's Orders tab, columns in SPA order (Output | Input | Vault Balance | IO Ratio | Order Hash | Created). Shipped by the exporter's collect_orders() as one Cloud Logging row per order per poll cycle (logName liquidity-orders); insertId buckets by poll-hour so a still-active order dedupes to ~1 stored row/hour rather than one per 60s poll \u2014 with a 2h window that means a couple of rows per order is expected, not a bug. Created is the order's own on-chain creation time (jsonPayload.created_at), not the row's ingestion time. The datasource returns at most about the panel's pixel width in entries.",
@@ -495,7 +495,7 @@
}
},
{
- "id": 10,
+ "id": 9,
"type": "stat",
"title": "",
"description": "In-flight broker orders right now (/orders/pending).",
@@ -577,7 +577,7 @@
}
},
{
- "id": 11,
+ "id": 10,
"type": "stat",
"title": "",
"description": "Raindex orders owned by the bot (upstream REST API total).",
@@ -659,7 +659,7 @@
}
},
{
- "id": 12,
+ "id": 11,
"type": "stat",
"title": "",
"description": "1 when the bot's upstream REST API for Raindex orders is unavailable.",
diff --git a/observability/dashboards/liquidity/tabs/t0-liquidity-performance.json b/observability/dashboards/liquidity/tabs/t0-liquidity-performance.json
index 18c2fbc1d..23ec2e20b 100644
--- a/observability/dashboards/liquidity/tabs/t0-liquidity-performance.json
+++ b/observability/dashboards/liquidity/tabs/t0-liquidity-performance.json
@@ -187,7 +187,7 @@
],
"panels": [
{
- "id": 34,
+ "id": 33,
"type": "marcusolsson-dynamictext-panel",
"title": "",
"description": "The SPA header: settings pills, Config, the CLI recovery guide, the deployed commit, uptime, and whether the bot is up: the exporter's liq_up, or the bot scrape's up on the bot source.",
@@ -251,7 +251,7 @@
"hideTimeOverride": true
},
{
- "id": 35,
+ "id": 34,
"type": "stat",
"title": "Detection latency",
"description": "p95, 24h. Fill on-chain \u2192 bot noticed. good \u226430s, warn \u2264120s.",
@@ -340,7 +340,7 @@
}
},
{
- "id": 36,
+ "id": 35,
"type": "stat",
"title": "Exposure window",
"description": "p95, 24h. Fill on-chain \u2192 hedge filled: how long capital stays exposed. good \u226460s, warn \u2264300s.",
@@ -429,7 +429,7 @@
}
},
{
- "id": 37,
+ "id": 36,
"type": "stat",
"title": "Errors (24h)",
"description": "ERROR-level log lines in 24h. good \u22640, warn \u226410. Money-at-risk lifecycle failures are broken out per event type in the 'Errors & warnings by module' table below.",
@@ -518,7 +518,7 @@
}
},
{
- "id": 38,
+ "id": 37,
"type": "stat",
"title": "Oldest unhedged fill",
"description": "Age of the oldest on-chain fill not yet hedged. good \u22645m, warn \u226430m; '\u2014' = nothing unhedged (the SPA shows 'none').",
@@ -607,7 +607,7 @@
}
},
{
- "id": 39,
+ "id": 38,
"type": "stat",
"title": "Block lag",
"description": "Chain blocks the fill-detection checkpoint trails by. good \u226430, warn \u2264300.",
@@ -696,7 +696,7 @@
}
},
{
- "id": 40,
+ "id": 39,
"type": "stat",
"title": "",
"description": "SPA secondary line: p50 detection latency.",
@@ -782,7 +782,7 @@
}
},
{
- "id": 41,
+ "id": 40,
"type": "stat",
"title": "",
"description": "SPA secondary line: fill sample count backing the detection latency percentiles.",
@@ -868,7 +868,7 @@
}
},
{
- "id": 42,
+ "id": 41,
"type": "stat",
"title": "",
"description": "SPA secondary line: p50 exposure-window latency.",
@@ -954,7 +954,7 @@
}
},
{
- "id": 43,
+ "id": 42,
"type": "stat",
"title": "",
"description": "SPA secondary line: hedge sample count backing the exposure-window percentiles.",
@@ -1040,7 +1040,7 @@
}
},
{
- "id": 44,
+ "id": 43,
"type": "stat",
"title": "",
"description": "SPA secondary line: WARN-level log lines in 24h.",
@@ -1126,7 +1126,7 @@
}
},
{
- "id": 45,
+ "id": 44,
"type": "stat",
"title": "",
"description": "SPA secondary line: money-at-risk lifecycle failure events in 24h (the 'Errors & warnings by module' table breaks these out by event type).",
@@ -1212,7 +1212,7 @@
}
},
{
- "id": 46,
+ "id": 45,
"type": "stat",
"title": "",
"description": "SPA secondary line: apalis job queues with a failed or killed job right now (instantaneous, not windowed) \u2014 the SPA folds killed into the same 'queue(s) failed' phrase.",
@@ -1302,7 +1302,7 @@
}
},
{
- "id": 47,
+ "id": 46,
"type": "stat",
"title": "",
"description": "SPA secondary line: on-chain fills still uncovered by a hedge, summed across symbols.",
@@ -1388,7 +1388,7 @@
}
},
{
- "id": 48,
+ "id": 47,
"type": "stat",
"title": "",
"description": "SPA secondary line: symbols with an open (unhedged) exposure \u2014 0 is the SPA's 'all fills hedged'.",
@@ -1484,7 +1484,7 @@
}
},
{
- "id": 49,
+ "id": 48,
"type": "stat",
"title": "",
"description": "SPA secondary line: age of the current block-lag sample.",
@@ -1570,7 +1570,7 @@
}
},
{
- "id": 50,
+ "id": 49,
"type": "stat",
"title": "",
"description": "SPA secondary line: order-fill monitor poll ticks skipped (overrun) in 24h.",
@@ -1656,7 +1656,7 @@
}
},
{
- "id": 51,
+ "id": 50,
"type": "bargauge",
"title": "Hedge cycle stages (p95, 24h)",
"description": "The SPA waterfall's aggregate form: p95 per stage. Per-cycle waterfalls live in the SPA.",
@@ -1734,7 +1734,7 @@
}
},
{
- "id": 52,
+ "id": 51,
"type": "timeseries",
"title": "Latency percentiles over time \u2014 $stage",
"description": "p50/p90/p99 of the selected hedge stage, from the exporter's rolling 24h summary scraped once a minute.",
@@ -1890,7 +1890,7 @@
}
},
{
- "id": 53,
+ "id": 52,
"type": "table",
"title": "Errors & warnings by module (24h)",
"description": "The SPA's per-target error/warning counts. The Logs tab has the lines themselves. Click a module to jump to the Logs board pre-filtered on it (the SPA's 'logs \u2192' button); sparkline cells are skipped as low-value/fiddly here.",
@@ -2095,7 +2095,7 @@
}
},
{
- "id": 54,
+ "id": 53,
"type": "bargauge",
"title": "USDC rebalance stages (p95, 30d)",
"description": "Stage timings for USDC rebalances. From the server's stageSummary; per-operation waterfalls live in the SPA.",
@@ -2165,7 +2165,7 @@
}
},
{
- "id": 55,
+ "id": 54,
"type": "bargauge",
"title": "Equity rebalance stages (p95, 30d)",
"description": "Stage timings for equity mints/redemptions.",
@@ -2235,7 +2235,7 @@
}
},
{
- "id": 56,
+ "id": 55,
"type": "table",
"title": "Dependency health (24h)",
"description": "Every RPC / broker operation: calls, errors, latency percentiles.",
@@ -2395,7 +2395,7 @@
}
},
{
- "id": 57,
+ "id": 56,
"type": "timeseries",
"title": "CCTP attestation time",
"description": "Duration of the most recent CCTP attestation per rebalance kind, sampled once per scrape. The SPA can chart the full attestationTrend history for the selected range; this gauge only has 'last value at scrape time', so the line only accrues from whenever this panel started being scraped forward \u2014 a short/flat history here does not mean attestations were fast, it means the metric is young.",
@@ -2516,7 +2516,7 @@
}
},
{
- "id": 58,
+ "id": 57,
"type": "timeseries",
"title": "Block lag over time",
"description": "The ingestion-health chart: how far fill detection trails the chain.",
@@ -2587,7 +2587,7 @@
}
},
{
- "id": 59,
+ "id": 58,
"type": "stat",
"title": "Poll cycles (24h)",
"description": "Order-fill monitor poll cycles.",
@@ -2668,7 +2668,7 @@
}
},
{
- "id": 60,
+ "id": 59,
"type": "stat",
"title": "Poll errors (24h)",
"description": "Failed poll cycles.",
@@ -2753,7 +2753,7 @@
}
},
{
- "id": 61,
+ "id": 60,
"type": "stat",
"title": "Skipped ticks (24h)",
"description": "Poll ticks skipped (overrun).",
@@ -2838,7 +2838,7 @@
}
},
{
- "id": 62,
+ "id": 61,
"type": "bargauge",
"title": "Poll duration percentiles",
"description": "The monitor's own cycle duration.",
@@ -2908,7 +2908,7 @@
}
},
{
- "id": 63,
+ "id": 62,
"type": "table",
"title": "Job queues",
"description": "The bot's apalis job queues (instantaneous whole-table counts, not windowed). killed or failed above zero is the SPA's critical Errors state.",
@@ -3094,7 +3094,7 @@
}
},
{
- "id": 64,
+ "id": 63,
"type": "row",
"title": "Hedge execution quality (native bot metrics)",
"collapsed": true,
@@ -3106,7 +3106,7 @@
},
"panels": [
{
- "id": 65,
+ "id": 64,
"type": "timeseries",
"title": "Hedge fill latency percentiles",
"description": "Wall-clock seconds from hedge order placement to confirmed fill. The app builds these as a metrics-rs summary (no explicit buckets), so the quantiles come straight off the {quantile=...} label rather than from histogram_quantile. This is the metrics-side view of the SPA's 'Latency percentiles over time' chart \u2014 that chart's per-stage waterfall (detection / decision / submission / execution) comes from the telemetry tables in SQLite and has no metric behind it.",
@@ -3218,7 +3218,7 @@
}
},
{
- "id": 66,
+ "id": 65,
"type": "timeseries",
"title": "Hedge fill latency by symbol (1h average)",
"description": "Average seconds between placing a hedge and it filling, per symbol, derived from the summary's _sum / _count. Spikes mean capital sits exposed longer on that name.",
@@ -3293,7 +3293,7 @@
}
},
{
- "id": 67,
+ "id": 66,
"type": "timeseries",
"title": "Broker errors per hour, by kind",
"description": "Alpaca API failures, split by kind (e.g. place_order_failed) and symbol. Empty is the healthy state.",
@@ -3363,7 +3363,7 @@
}
},
{
- "id": 68,
+ "id": 67,
"type": "timeseries",
"title": "Hedge scans skipped, by reason",
"description": "Extended-hours buys the position scan dropped before enqueueing a hedge job, because reference-price resolution or crossing failed. Each one is an on-chain fill that did not get hedged on that pass.",
@@ -3433,7 +3433,7 @@
}
},
{
- "id": 69,
+ "id": 68,
"type": "timeseries",
"title": "Hedges dead-lettered, by reason",
"description": "Hedge attempts the process gave up on: a permanent or retry-budget-exhausted pricing failure, or broker rate-limiting that outlived the reschedule budget. Anything here is an unhedged fill that needs a human.",
@@ -3503,7 +3503,7 @@
}
},
{
- "id": 70,
+ "id": 69,
"type": "timeseries",
"title": "Which reference priced each extended-hours limit",
"description": "hedge_price_source_total by source (primary_quote / mark / delayed_sip_quote) and path (ordinary_extended / close_flatten). Shows which fallback legs are load-bearing \u2014 a rise in delayed_sip_quote means we are pricing off stale data.",
@@ -3577,7 +3577,7 @@
]
},
{
- "id": 71,
+ "id": 70,
"type": "row",
"title": "Close-flatten (end-of-session flattening) (native bot metrics)",
"collapsed": true,
@@ -3589,7 +3589,7 @@
},
"panels": [
{
- "id": 72,
+ "id": 71,
"type": "timeseries",
"title": "Attempts, by post-close gap reason",
"description": "Close-flatten hedge pricing attempts, by the post-close gap reason that triggered them.",
@@ -3659,7 +3659,7 @@
}
},
{
- "id": 73,
+ "id": 72,
"type": "timeseries",
"title": "Blocked before submission, by reason",
"description": "Close-flatten attempts stopped before they reached the broker. close_time_unknown carries no symbol label; the rest are per symbol.",
@@ -3729,7 +3729,7 @@
}
},
{
- "id": 74,
+ "id": 73,
"type": "timeseries",
"title": "Placements, by cross applied",
"description": "Limit orders priced and cleared for submission, bucketed to whole percent of cross. Counts attempts, not orders: a retried or re-driven placement of the same hedge counts again, and an attempt the broker later rejects still counts.",
@@ -3799,7 +3799,7 @@
}
},
{
- "id": 75,
+ "id": 74,
"type": "timeseries",
"title": "Outcomes",
"description": "Terminal broker-state dispatches for close-flatten placements (filled / cancelled / failed). A placement can be observed again until its follow-up job commits the terminal aggregate transition, so treat these as dispatches rather than a strict order count.",
@@ -3871,7 +3871,7 @@
]
},
{
- "id": 76,
+ "id": 75,
"type": "row",
"title": "Data integrity & ingestion health (native bot metrics)",
"collapsed": true,
@@ -3883,7 +3883,7 @@
},
"panels": [
{
- "id": 77,
+ "id": 76,
"type": "stat",
"title": "Ambiguous settlements (24h)",
"description": "Inventory settlements quarantined because one transaction emitted multiple OperatorDeposits or multiple OperatorWithdraws and the legs could not be safely paired. Each one is a held-back settlement.",
@@ -3973,7 +3973,7 @@
}
},
{
- "id": 78,
+ "id": 77,
"type": "stat",
"title": "Unpaired settlement legs (24h)",
"description": "OperatorDeposit / OperatorWithdraw legs with no same-transaction counterpart in the batch.",
@@ -4063,7 +4063,7 @@
}
},
{
- "id": 79,
+ "id": 78,
"type": "stat",
"title": "Unusable snapshot marks (24h)",
"description": "Nonzero equity balances captured with a missing or stale USD mark. These make the portfolio snapshot \u2014 and therefore the PnL replay in the SPA \u2014 incomplete for that interval.",
@@ -4153,7 +4153,7 @@
}
},
{
- "id": 80,
+ "id": 79,
"type": "stat",
"title": "PnL ledger checkpoint lag",
"description": "How many events the PnL ledger's checkpoint trails the event store by. The PnL numbers themselves are computed by the backend from SQLite and are not exported \u2014 this gauge is the one PnL signal that reaches Grafana, and it answers 'is the PnL panel's input keeping up?'.",
@@ -4247,7 +4247,7 @@
}
},
{
- "id": 81,
+ "id": 80,
"type": "timeseries",
"title": "Scrape health",
"description": "The Ops Agent's own view of the bot's /metrics endpoint: 1 = scraped successfully, 0 = the endpoint did not answer. Every other panel on this dashboard goes blank while this is 0, so check it first before concluding the bot is idle.",
@@ -4320,7 +4320,7 @@
}
},
{
- "id": 82,
+ "id": 81,
"type": "timeseries",
"title": "Scrape duration and series count",
"description": "How long the scrape takes and how many series come back. A series count that collapses to near zero while up=1 means the bot restarted and its counters reset.",
@@ -4411,7 +4411,7 @@
}
},
{
- "id": 83,
+ "id": 82,
"type": "timeseries",
"title": "Bot-gas redrives and PnL catch-up failures",
"description": "bot_gas_redrive_total: receipt-cost enqueue failures redriven instead of failing the triggering job. pnl_ledger_catch_up_failures_total: the PnL ledger failing to catch up to the event store. Both are self-healing retries \u2014 a steady rate means something underneath is not healing.",
diff --git a/observability/dashboards/liquidity/tabs/t0-liquidity-pnl.json b/observability/dashboards/liquidity/tabs/t0-liquidity-pnl.json
index eb5277c17..627fa4def 100644
--- a/observability/dashboards/liquidity/tabs/t0-liquidity-pnl.json
+++ b/observability/dashboards/liquidity/tabs/t0-liquidity-pnl.json
@@ -193,7 +193,7 @@
],
"panels": [
{
- "id": 13,
+ "id": 12,
"type": "marcusolsson-dynamictext-panel",
"title": "",
"description": "The SPA header: settings pills, Config, the CLI recovery guide, the deployed commit, uptime, and whether the bot is up: the exporter's liq_up, or the bot scrape's up on the bot source.",
@@ -257,7 +257,7 @@
"hideTimeOverride": true
},
{
- "id": 14,
+ "id": 13,
"type": "stat",
"title": "",
"description": "Gross minus costs plus revenues. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -351,7 +351,7 @@
}
},
{
- "id": 15,
+ "id": 14,
"type": "stat",
"title": "",
"description": "Closed-lot fill replay before costs. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -445,7 +445,7 @@
}
},
{
- "id": 16,
+ "id": 15,
"type": "stat",
"title": "",
"description": "Cost/revenue ledger entries. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -531,7 +531,7 @@
}
},
{
- "id": 17,
+ "id": 16,
"type": "stat",
"title": "",
"description": "Dividends and broker ledger credits. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -625,7 +625,7 @@
}
},
{
- "id": 18,
+ "id": 17,
"type": "stat",
"title": "",
"description": "Broker hedge stream. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -719,7 +719,7 @@
}
},
{
- "id": 19,
+ "id": 18,
"type": "stat",
"title": "",
"description": "Users passively crossed open inventory. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -813,7 +813,7 @@
}
},
{
- "id": 20,
+ "id": 19,
"type": "stat",
"title": "",
"description": "Closed delayed or offchain-origin exposure. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -907,7 +907,7 @@
}
},
{
- "id": 21,
+ "id": 20,
"type": "stat",
"title": "",
"description": "Requires historical snapshots. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -1001,7 +1001,7 @@
}
},
{
- "id": 22,
+ "id": 21,
"type": "stat",
"title": "",
"description": "All directional streams together. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -1095,7 +1095,7 @@
}
},
{
- "id": 23,
+ "id": 22,
"type": "stat",
"title": "",
"description": "Every stream summed. (SPA PnL tile, computed by the bot's replay \u2014 5-min refresh cadence.)",
@@ -1189,7 +1189,7 @@
}
},
{
- "id": 24,
+ "id": 23,
"type": "stat",
"title": "",
"description": "Open replay inventory in shares (the SPA's muted tile).",
@@ -1271,7 +1271,7 @@
}
},
{
- "id": 25,
+ "id": 24,
"type": "stat",
"title": "",
"description": "Total fills in the PnL database sample.",
@@ -1353,7 +1353,7 @@
}
},
{
- "id": 26,
+ "id": 25,
"type": "stat",
"title": "",
"description": "Average deployed capital from daily snapshots \u2014 computed by the bot, never rendered by the SPA.",
@@ -1435,7 +1435,7 @@
}
},
{
- "id": 27,
+ "id": 26,
"type": "stat",
"title": "",
"description": "Annualized return on that capital \u2014 computed by the bot, never rendered by the SPA.",
@@ -1525,7 +1525,7 @@
}
},
{
- "id": 28,
+ "id": 27,
"type": "stat",
"title": "",
"description": "Days of snapshot coverage behind the capital figures.",
@@ -1607,7 +1607,7 @@
}
},
{
- "id": 29,
+ "id": 28,
"type": "table",
"title": "Per Asset PnL",
"description": "The SPA's Per Asset PnL table (closed-lot replay per symbol; Lots = matched lot count; Volume = both legs of the matched shares, without the SPA's USD sub-line).",
@@ -2137,7 +2137,7 @@
}
},
{
- "id": 30,
+ "id": 29,
"type": "barchart",
"title": "PnL by Asset per Time Unit",
"description": "One bar per calendar day, split by asset and stacked, straight from the bot's daily PnL buckets. A day with no closed lot for an asset is a zero, as in the SPA.",
@@ -2284,7 +2284,7 @@
}
},
{
- "id": 31,
+ "id": 30,
"type": "barchart",
"title": "PnL by PnL-Stream per Time Unit",
"description": "The same days split by PnL stream, the SPA's four component streams only, since directional_exposure and total are tile-only aggregates there and stacking them beside their own components double-counts.",
@@ -2492,7 +2492,7 @@
}
},
{
- "id": 32,
+ "id": 31,
"type": "barchart",
"title": "Cumulative PnL by Asset",
"description": "Running total per asset through the selected range. Unstacked: the SPA draws one independent line per asset here, not a stack. The last day's bars sum to the Total PnL tile above.",
@@ -2639,7 +2639,7 @@
}
},
{
- "id": 33,
+ "id": 32,
"type": "barchart",
"title": "Cumulative PnL by PnL-Stream",
"description": "Running total per stream, unstacked like the SPA's lines. Grafana has no categorical-axis line chart, so these are bars over the same day buckets: the numbers are the SPA's, the shape is not.",
diff --git a/observability/gen-t0-liquidity.py b/observability/gen-t0-liquidity.py
index 0bded1191..a9b3a1073 100644
--- a/observability/gen-t0-liquidity.py
+++ b/observability/gen-t0-liquidity.py
@@ -19,10 +19,9 @@
liquidity-botlogs via Cloud Logging entries.write
Layout mirrors the SPA's tabs as rows, in tab order: header strip,
-Dashboard (inventory | trades | transfers, 14/10 split: the SPA's 11fr/9fr
-is 13/11, but Grafana's panel padding needs the extra unit to fit the USDC
-row's three panels; checked with no sideways scroll on a 1934px-wide
-viewport, and the fixed column widths do overflow near 1600px), Orders, PnL, Performance, Logs — then the pre-exporter
+Dashboard (inventory tables | trades over rebalances, a 13/11 split like
+the SPA's 11fr/9fr; checked with no sideways scroll on a 1934px-wide
+viewport), Orders, PnL, Performance, Logs — then the pre-exporter
native-metric rows (t0-liquidity-native-rows.json), collapsed.
Two Grafana-side patterns worth knowing before editing:
@@ -266,6 +265,9 @@ def or_chain(cols, row_label="symbol"):
(instance, job, cluster...) so the table transformations see clean frames.
"""
return " or ".join(
+ # name None: the expression already carries its own col label (a
+ # column per chain, see native_inventory).
+ expr if name is None else
f'sum by ({row_label}, col) '
f'(label_replace({expr}, "col", "{name}", "", ""))'
for expr, name in cols
@@ -548,39 +550,7 @@ def cloudlog_table(title, desc, query, columns, w=12, h=9, x=0, y=0,
"sell": {"text": "sell", "color": "red", "index": 1},
}},
]
-LIGHT_MAPPINGS = [
- {"type": "value", "options": {
- "1": {"text": "●", "color": "green", "index": 0},
- "0": {"text": "●", "color": "red", "index": 1},
- }},
- {"type": "special", "options": {"match": "null+nan",
- "result": {"text": "○", "color": "text",
- "index": 2}}},
-]
-USD = [{"id": "unit", "value": "currencyUSD"}, {"id": "decimals", "value": 2}]
-USD_SIGNED = USD + [
- {"id": "custom.cellOptions", "value": {"type": "color-text"}},
- {"id": "thresholds", "value": {"mode": "absolute", "steps": [
- {"color": "red", "value": None}, {"color": "text", "value": -0.01},
- {"color": "green", "value": 0.01}]}},
-]
-PCT = [
- {"id": "unit", "value": "percentunit"}, {"id": "decimals", "value": 1},
- # Inline bar plus percent, like the SPA's Ratio progress bar. The bar
- # shares the cell with the percent text, so the column needs the width.
- {"id": "custom.cellOptions", "value": {"type": "gauge", "mode": "basic",
- "valueDisplayMode": "text"}},
- {"id": "min", "value": 0}, {"id": "max", "value": 1},
- {"id": "thresholds", "value": {"mode": "absolute", "steps": [
- {"color": "blue", "value": None}]}},
-]
-LIGHT = [
- {"id": "mappings", "value": LIGHT_MAPPINGS},
- {"id": "custom.cellOptions", "value": {"type": "color-text"}},
- {"id": "custom.align", "value": "center"},
- {"id": "custom.width", "value": 40},
-]
# ==========================================================================
# Value mappings shared by the row tables — the SPA capitalizes and
@@ -635,10 +605,6 @@ def cloudlog_table(title, desc, query, columns, w=12, h=9, x=0, y=0,
}},
]
-# The SPA renders plain locale numbers (37,934.83) — no $ prefix, no SI
-# abbreviation — in the inventory tables. "locale" is Grafana's
-# toLocaleString unit; it never abbreviates.
-NUM = [{"id": "unit", "value": "locale"}, {"id": "decimals", "value": 2}]
# The stackdriver plugin runs a range query even for an `instant` target, so
# a latest-value panel on the 24h dashboard pulled every 60s point of the day
# (27,092 points for the equity table) just to show the newest one. A 5m
@@ -651,26 +617,8 @@ def cloudlog_table(title, desc, query, columns, w=12, h=9, x=0, y=0,
LATEST_ONLY_HIDDEN = {"timeFrom": "5m", "hideTimeOverride": True}
-# The SPA packs the inventory columns tight (64px bars, short numbers), so the
-# whole table fits beside the trade list. Grafana's auto widths are about
-# twice that and push Ratio and Exposure off the right edge.
-def width(px):
- return [{"id": "custom.width", "value": px}]
-
-
# The SPA's timestamp style: "Jul 31, 13:31:50 UTC" (D7).
TIME_FMT = [{"id": "unit", "value": "time:MMM D, HH:mm:ss [UTC]"}]
-# D5: narrow ratio-deviation column after "Ratio", in band widths
-# ((ratio - target) / deviation): outside +/-1 is colored, inside the band
-# renders in plain text.
-DELTA = [
- {"id": "unit", "value": "short"}, {"id": "decimals", "value": 1},
- {"id": "custom.cellOptions", "value": {"type": "color-text"}},
- {"id": "custom.width", "value": 45},
- {"id": "thresholds", "value": {"mode": "absolute", "steps": [
- {"color": "red", "value": None}, {"color": "text", "value": -1},
- {"color": "green", "value": 1}]}},
-]
# ==========================================================================
# The tab suite: five dashboards, one per SPA tab, cross-linked with a
@@ -870,124 +818,409 @@ def make_dashboard(uid, title, description, panels, links, variables):
dashboards = []
# ==========================================================================
-# Tab 1: Dashboard — the SPA's Inventory card (cash row + equity table)
-# on the left, Trade History / Cross-venue Transfers stacked right.
+# Tab 1: Dashboard: the SPA's Inventory card as Grafana tables (USD at
+# Alpaca, USD on each chain, equities) on the left, Trades / Rebalances
+# stacked right.
# ==========================================================================
panels = []
panels += pills(0)
-def usdc(metric):
- return f'label_replace({metric}, "row", "USDC", "", "")'
-
-
-def usdc_group(title, desc, cols, w, x, overrides, show_asset=False,
- latest=LATEST_ONLY):
- """One slice of the SPA's USDC row. The SPA groups its right-hand
- columns under "Alpaca" and "Wallets" header cells; a Grafana table has
- no grouped headers, so each group is its own panel and the panel title
- is the group header."""
- panel = matrix_table(
- title, desc, or_chain([(usdc(metric), name) for metric, name in cols],
- row_label="row"),
- "row", w=w, h=4, x=x, y=1, first_col="Asset",
- column_order=[name for _, name in cols], unit_overrides=overrides,
- decimals=2)
- if not show_asset:
- panel["transformations"][-1]["options"]["excludeByName"]["row\\col"] = True
- # Grafana gives a column without a width a 150px minimum, which made
- # these narrow panels scroll; 40px lets an auto column take what is left.
- panel["fieldConfig"]["defaults"]["custom"]["minWidth"] = 40
- return {**panel, **latest}
-
-
-panels.append(usdc_group(
- "Inventory",
- "The SPA's USDC row: Raindex + Alpaca Total + Inflight = Total; Ratio "
- "= Raindex/(Raindex+Alpaca Total); Δ = deviation from the target ratio "
- "in band widths, colored outside +/-1, where the SPA colors its bar.",
- [("liq_usdc_onchain_available", "Raindex"),
- ("liq_usdc_inflight_total", "Inflight"),
- ("liq_usdc_alpaca_total", "Alpaca Total"),
- ("liq_usdc_total", "Total"),
- ("liq_usdc_ratio", "Ratio"),
- ("liq_usdc_ratio_deviation and liq_usdc_total > 0", "Δ")],
- w=7, x=0, show_asset=True,
- # 90px fits a six-figure balance ("104,302.08"); Ratio gives up the
- # width, since its bar is redrawn in part 2 anyway.
- overrides={"Asset": width(50), "Inflight": NUM + width(80),
- **{c: NUM + width(90) for c in
- ["Raindex", "Alpaca Total", "Total"]},
- "Ratio": PCT + width(75), "Δ": DELTA}))
-panels.append(usdc_group(
- "Alpaca",
- "Rebalanceable = max(0, withdrawable − reserve). Counter-tradeable "
- "deliberately equals Alpaca Total (reserve NOT subtracted, same as the "
- "SPA).",
- [("liq_usdc_alpaca_usdc", "USDC"),
- ("liq_usdc_rebalanceable", "Rebalanceable"),
- ("liq_usdc_alpaca_total", "Counter-tradeable")],
- w=4, x=7, overrides={"USDC": NUM + width(90),
- "Rebalanceable": NUM + width(105),
- # No width: it takes the rest of the panel.
- "Counter-tradeable": NUM}))
-panels.append(usdc_group(
- "Wallets",
- "USDC seen in the bot's Ethereum and Base wallets: sanity checks, not "
- "part of Total.",
- [("liq_usdc_inflight_ethereum_wallet", "Eth"),
- ("liq_usdc_inflight_base_wallet", "Base")],
- w=3, x=11, overrides={"Eth": NUM + width(80), "Base": NUM},
- # Three units are too narrow for the title and the badge: Grafana drops
- # the title, so this panel hides the badge like the header pills.
- latest=LATEST_ONLY_HIDDEN))
-
-equity_expr = or_chain([
- ("liq_asset_counter_trading", "CT"),
- ("liq_asset_rebalancing", "Rebal"),
- ("liq_asset_extended_hours", "Ext"),
- ("liq_equity_onchain_available", "Raindex"),
- ("liq_equity_inflight_total", "Inflight"),
- ("liq_equity_offchain_available", "Alpaca"),
- ("liq_equity_total", "Total"),
- ("liq_equity_ratio", "Ratio"),
- ("liq_equity_ratio_deviation and liq_equity_total > 0", "Δ"),
- # The exporter emits Exposure only for a priced symbol. NaN for the rest
- # keeps the column in the matrix when no symbol has a price, and NaN hits
- # the "—" mapping like the SPA's dash.
- ("liq_equity_exposure_usd or (liq_equity_total * NaN)", "Exposure"),
- ("liq_equity_unwrapped", "Unwrapped"),
- ("liq_equity_wrapped", "Wrapped"),
-])
-panels.append({**matrix_table(
- "",
- "The SPA inventory equity table: CT / Rebal / Ext status lights from "
- "settings; Raindex / Inflight / Alpaca / Total share balances; Ratio = "
- "onchain/(onchain+offchain); Δ = deviation from the target ratio in "
- "band widths, colored outside +/-1; Exposure = net × last price. "
- "Unwrapped/Wrapped are wallet-observed and not part of Total. Sorted "
- "CT-first (desc) so counter-tradeable assets float to the top, like "
- "the SPA.",
- equity_expr, "symbol", w=14, h=24, x=0, y=5,
- first_col="Asset",
- column_order=["CT", "Rebal", "Ext", "Raindex", "Inflight", "Alpaca",
- "Total", "Ratio", "Δ", "Exposure", "Unwrapped", "Wrapped"],
- unit_overrides={
- "Asset": width(60),
- "CT": LIGHT, "Rebal": LIGHT, "Ext": LIGHT,
- **{c: NUM + width(70) for c in ["Raindex", "Inflight", "Alpaca",
- "Total", "Wrapped"]},
- "Unwrapped": NUM + width(80),
- # Wider than the USDC row's: the gauge draws its bar in what the
- # percent text leaves, and the SPA's bar is 64px.
- "Ratio": PCT + width(140), "Δ": DELTA,
- # The SPA prints "—" when the pricing service has no live price.
- "Exposure": USD_SIGNED + width(80) + [{"id": "mappings", "value": [
- {"type": "special", "options": {"match": "null+nan", "result": {
- "text": "—", "color": "text", "index": 0}}}]}],
- },
- sort_by={"displayName": "CT", "desc": True},
- decimals=2,
-), **LATEST_ONLY})
+def native_table(title, desc, expr, row_label, columns, w, h, x, y,
+ first_col, sort_by=None, widths=None, bars=(),
+ align="right", stretch=None, first_col_mappings=None):
+ """A Grafana table from one or-chain query pivoted on (row_label, col),
+ like matrix_table. columns: [(col, display name, [field override
+ properties])]; bars: columns drawn as HTML bars (see bar_cell)."""
+ matrix_key = f"{row_label}\\col"
+ names = {col: name for col, name, _ in columns}
+ # Fixed widths, except one `stretch` column that takes the rest of the
+ # card, so the table ends at the card's edge.
+ widths = {col: px for col, px in (widths or {}).items() if col != stretch}
+ # A stretching column otherwise never goes under Grafana's 150px
+ # default, which overflows a card whose fixed columns leave less.
+ stretch_props = ([{"matcher": {"id": "byName", "options": names[stretch]},
+ "properties": [{"id": "custom.minWidth", "value": 60}]}]
+ if stretch else [])
+ overrides = [{"matcher": {"id": "byName", "options": names[col]},
+ "properties": [*(props or []),
+ *([{"id": "custom.width", "value": widths[col]}]
+ if col in widths else [])]}
+ for col, _, props in columns if props or col in widths]
+ return {
+ "id": nid(), "type": "table", "pluginVersion": "13.1.0",
+ "title": title, "description": desc,
+ "datasource": CM,
+ "targets": [promql(expr, instant=True)],
+ "gridPos": {"h": h, "w": w, "x": x, "y": y},
+ "transformations": [
+ {"id": "labelsToFields", "options": {}},
+ {"id": "merge", "options": {}},
+ {"id": "groupBy", "options": {"fields": {
+ row_label: {"operation": "groupby", "aggregations": []},
+ "col": {"operation": "groupby", "aggregations": []},
+ "Value": {"operation": "aggregate",
+ "aggregations": ["lastNotNull"]},
+ }}},
+ {"id": "groupingToMatrix",
+ "options": {"columnField": "col", "rowField": row_label,
+ "valueField": "Value (lastNotNull)",
+ "emptyValue": "null"}},
+ {"id": "organize", "options": {
+ "excludeByName": {},
+ "indexByName": {name: index for index, name in enumerate(
+ [matrix_key] + [col for col, _, _ in columns])},
+ "renameByName": {matrix_key: first_col, **names}}},
+ # Regex value mappings only read strings (see bar_cell).
+ *([{"id": "convertFieldType", "options": {"conversions": [
+ {"targetField": col, "destinationType": "string"}
+ for col in bars]}}] if bars else []),
+ ],
+ "fieldConfig": {
+ "defaults": {"unit": "locale", "decimals": 2,
+ "custom": {"align": align, "filterable": True,
+ "cellOptions": {"type": "auto"}},
+ "thresholds": {"mode": "absolute", "steps": [
+ {"color": "red", "value": None}]},
+ "mappings": []},
+ "overrides": [
+ {"matcher": {"id": "byName", "options": first_col},
+ "properties": [{"id": "custom.align", "value": "left"},
+ {"id": "custom.width",
+ "value": widths.get(row_label, 90)},
+ *([{"id": "mappings",
+ "value": first_col_mappings}]
+ if first_col_mappings else [])]},
+ *overrides,
+ *stretch_props,
+ ],
+ },
+ "options": {"showHeader": True, "cellHeight": "md",
+ **({"sortBy": [sort_by]} if sort_by else {})},
+ **LATEST_ONLY,
+ }
+
+
+
+# Added to a banded percent before its sign, so an out-of-band 0% is -0.0001
+# and not -0, which Grafana writes as "0" (in band). The bar cell's regex
+# keeps one decimal, so the offset never shows.
+BAND_SIGN_OFFSET = 0.0001
+
+
+def banded_pct(expr, inside):
+ """pct_bar's sign encoding: the percent rounded to 0.1, negative when
+ `inside` (a 0/1 PromQL bool) says it is outside the band."""
+ return (f"(round(100 * ({expr}), 0.1) + {BAND_SIGN_OFFSET}) "
+ f"* (2 * {inside} - 1)")
+
+
+# Added to a percent the board cannot judge against a band, so the bar cell
+# draws it grey. A percent is at most 100, so a judged value never has four
+# integer digits and an unjudged one always does.
+UNJUDGED_OFFSET = 1000
+
+
+def unjudged_pct(expr):
+ """A percent with no band verdict: grey in a banded bar cell."""
+ return (f"round(100 * ({expr}), 0.1) + {UNJUDGED_OFFSET} "
+ f"+ {BAND_SIGN_OFFSET}")
+
+
+def pct_bar(expr, band=None):
+ """A percent for an HTML bar cell (see BAR_CELL): the value as a percent
+ rounded to 0.1, negative when it is outside the rebalance band. A table
+ cell can only style its own value, so the band verdict travels in the
+ sign. band: (target metric, deviation metric), or None for a neutral
+ share."""
+ if not band:
+ return f"round(100 * ({expr}), 0.1)"
+ target, deviation = band
+ inside = (f"(abs(({expr}) - scalar(max({target}))) "
+ f"<= bool scalar(max({deviation})))")
+ return banded_pct(expr, inside)
+
+
+def bar_html(fill, text):
+ # One div: Grafana's sanitizer drops position, top, left and
+ # line-height, so the fill is a hard-stop gradient behind the text.
+ return ('{text}%
')
+
+
+# A percent drawn as the custom card's bar: the fill as wide as the percent
+# and the percent written inside it. Grafana's gauge cell puts the number
+# beside its bar, so this is a Markdown + HTML cell: the value turns into a
+# string and a regex mapping writes the HTML, its capture group the percent.
+# Negative means outside the band (see pct_bar): red, else green. With
+# `unjudged`, a value with four integer digits is a percent plus
+# UNJUDGED_OFFSET (see unjudged_pct): grey, its percent read after the offset.
+GREY_FILL = "rgba(148,163,184,0.45)"
+UNJUDGED_PATTERN = r"^(?=\d{4})10*(\d+(?:\.[1-9])?).*$"
+
+
+def bar_cell(neutral=False, unjudged=False):
+ # No value (a chain without a vault) shows nothing.
+ mappings = [{"type": "regex", "options": {"pattern": "^(null|NaN)?$",
+ "result": {"text": " ", "index": 0}}}]
+ mappings += ([] if not unjudged else [
+ {"type": "regex", "options": {"pattern": UNJUDGED_PATTERN, "result": {
+ "text": bar_html(GREY_FILL, "$1"), "index": len(mappings)}}}])
+ # The percent keeps at most one decimal, which drops pct_bar's offset
+ # and any float noise from the rounding.
+ mappings += ([] if neutral else [
+ {"type": "regex", "options": {"pattern": r"^-(\d+(?:\.[1-9])?).*$", "result": {
+ "text": bar_html("rgba(239,68,68,0.6)", "$1"), "index": len(mappings)}}}])
+ mappings.append({"type": "regex", "options": {
+ "pattern": r"^(\d+(?:\.[1-9])?).*$" if not neutral else "^(.*)$", "result": {
+ "text": bar_html(GREY_FILL if neutral else "rgba(34,197,94,0.6)", "$1"),
+ "index": len(mappings)}}})
+ # The cell is an encoded string, so sorting or filtering it would order
+ # the encodings, not the percents.
+ return [{"id": "mappings", "value": mappings},
+ {"id": "custom.cellOptions", "value": {"type": "markdown"}},
+ {"id": "custom.align", "value": "center"},
+ {"id": "custom.filterable", "value": False}]
+
+# CT, Rebal and Ext as one column of three dots. Grafana renders no column
+# under 50px, so three one-dot columns took 150px. The query packs the flags
+# into one number, CT x 100 + Rebal x 10 + Ext, with each digit 1 off, 2 on
+# or 3 not set (never 0, so the number never loses a leading digit), and one
+# regex mapping per combination draws the dots as HTML.
+def flag_digit(metric):
+ return (f"(max by (symbol) ({metric}) + 1 "
+ "or (max by (symbol) (liq_equity_total) * 0 + 3))")
+
+
+FLAGS_EXPR = (f"{flag_digit('liq_asset_counter_trading')} * 100 + "
+ f"{flag_digit('liq_asset_rebalancing')} * 10 + "
+ f"{flag_digit('liq_asset_extended_hours')}")
+FLAG_DOT = {"2": '●',
+ "1": '●',
+ "3": '○'}
+FLAGS_CELL = [
+ {"id": "mappings", "value": [
+ {"type": "regex", "options": {"pattern": f"^{a}{b}{c}$", "result": {
+ "text": " ".join(FLAG_DOT[d] for d in (a, b, c)),
+ "index": index}}}
+ for index, (a, b, c) in enumerate(
+ (a, b, c) for a in "123" for b in "123" for c in "123")]},
+ {"id": "custom.cellOptions", "value": {"type": "markdown"}},
+ {"id": "custom.align", "value": "center"},
+ {"id": "custom.filterable", "value": False},
+]
+
+# Unpriced symbols arrive as NaN (see native_inventory) and read as a dash.
+NO_PRICE = {"id": "mappings", "value": [{"type": "special", "options": {
+ "match": "null+nan", "result": {"text": "—", "color": "#6b7280",
+ "index": 0}}}]}
+
+# Display names for the chains the bot knows (config keys). A chain not
+# listed here still gets its row and column, under its raw key.
+CHAIN_NAMES = {"base": "Base", "robinhood": "Robinhood", "hyperevm": "HyperEVM",
+ "ethereum": "Ethereum"}
+
+
+# The Equities Ratio is Base vault / (Base vault + Alpaca), both available,
+# against the default Base target. The planner instead sizes each chain's
+# share of the total over the chains whose listing rebalances, in underlying
+# shares, against that listing's own target. The board can see the other
+# chains only on the bot source, so it judges a symbol only there: rebalanced
+# on Base, with a balance, the default target set, its Base vault read, and no
+# vault row on any other chain, empty or not (a multi-chain listing such as
+# DNUT sets its own target_share). Every other symbol is grey, no verdict.
+# The verdict can still differ from the bot's for a Base-only listing with
+# its own target_share, while a wrapper ratio is not 1, or while a transfer is
+# in flight.
+EQUITY_JUDGED = (
+ '(liq_equity_total > 0) '
+ 'and on (symbol) (max by (symbol) (liq_asset_rebalancing) == 1) '
+ 'and on () count(liq_settings_equity_target) '
+ 'and on (symbol) max by (symbol) '
+ '(liq_equity_chain_available{chain="base"}) '
+ 'unless on (symbol) max by (symbol) '
+ '(liq_equity_chain_available{chain!="base"})')
+EQUITY_RATIO = (
+ f'({pct_bar("liq_equity_ratio", ("liq_settings_equity_target", "liq_settings_equity_deviation"))}'
+ f' and on (symbol) ({EQUITY_JUDGED})) '
+ f'or ({unjudged_pct("liq_equity_ratio")} unless on (symbol) ({EQUITY_JUDGED}))')
+
+
+def native_inventory(w, x, y, heights):
+ """The Inventory card as three Grafana tables: USD at Alpaca, USD on each
+ chain, and the equities."""
+ alpaca_h, chains_h, equity_h = heights
+ usd = lambda metric, row: (f'label_replace({metric}, "venue", "{row}", '
+ '"", "")')
+ alpaca = or_chain([
+ (usd("liq_usdc_alpaca_total", "Alpaca"), "cash"),
+ (usd("liq_usdc_alpaca_usdc", "Alpaca"), "usdc"),
+ # The reserve setting, else gross minus available cash, else Alpaca
+ # total minus available (0 when the bot ships no gross figure), as
+ # the custom card computed it.
+ (usd("(liq_settings_cash_reserved or (liq_usdc_offchain_gross "
+ "- liq_usdc_offchain_available) or (liq_usdc_alpaca_total "
+ "- liq_usdc_offchain_available))", "Alpaca"), "reserve"),
+ (usd("liq_usdc_rebalanceable", "Alpaca"), "rebalanceable"),
+ (usd("liq_usdc_offchain_inflight", "Alpaca"), "inflight"),
+ (usd(pct_bar("liq_usdc_alpaca_total / liq_usdc_total"), "Alpaca"),
+ "share"),
+ (usd("liq_usdc_total", "Alpaca"), "total"),
+ ], row_label="venue")
+ # One row per chain from the bot's per-chain series, else (the exporter
+ # source, which has none) the single Base row from the unlabelled ones.
+ # The fallback is per source, not per row: while the bot publishes
+ # liq_usdc_chain_available, a chain without a labelled value shows none,
+ # so a chain without a corridor has no target and no ratio, and a ratio
+ # the bot leaves out is not filled from the unlabelled one. Each side is
+ # reduced to just the venue label so `or` can tell they are the same row.
+ def per_chain(labelled, base):
+ return (f'max by (venue) (label_replace({labelled}, "venue", "$1", '
+ f'"chain", "(.*)")) or (max by (venue) (label_replace({base}, '
+ '"venue", "base", "", "")) unless on () '
+ 'count(liq_usdc_chain_available))')
+ ratio = per_chain("liq_usdc_chain_ratio", "liq_usdc_ratio")
+ target = per_chain("liq_usdc_corridor_target", "liq_settings_usdc_target")
+ deviation = per_chain("liq_usdc_corridor_deviation",
+ "liq_settings_usdc_deviation")
+ inside = (f"(abs(({ratio}) - ({target})) <= bool ({deviation}))")
+ chains = or_chain([
+ (per_chain("liq_usdc_chain_available", "liq_usdc_onchain_available"),
+ "vault"),
+ (per_chain("liq_usdc_chain_inflight", "liq_usdc_onchain_inflight"),
+ "inflight"),
+ ('max by (venue) (label_replace(liq_usdc_inflight_base_wallet, '
+ '"venue", "base", "", "")) or max by (venue) (label_replace('
+ 'liq_usdc_inflight_ethereum_wallet, "venue", "ethereum", "", ""))',
+ "wallet"),
+ # pct_bar's sign encoding, with each chain's own band.
+ (banded_pct(ratio, inside), "ratio"),
+ (target, "target"),
+ (deviation, "deviation"),
+ ], row_label="venue")
+ equity = or_chain([
+ (FLAGS_EXPR, "flags"),
+ # A column per chain (col onchain_) from the per-chain metric,
+ # else (the exporter source) the onchain total as the Base column.
+ ('max by (symbol, col) (label_replace(liq_equity_chain_available, '
+ '"col", "onchain_$1", "chain", "(.*)")) or (max by (symbol, col) '
+ '(label_replace(liq_equity_onchain_available, "col", "onchain_base", '
+ '"", "")) unless on () count(liq_equity_chain_available))', None),
+ ("liq_equity_inflight_total", "inflight"),
+ ("liq_equity_offchain_available", "alpaca"),
+ ("liq_equity_total", "total"),
+ # Priced symbols only: the last price from the pricing feed. The NaN
+ # fallback keeps the column on the board for unpriced symbols (a
+ # dash, see NO_PRICE), where Grafana would drop a column that has no
+ # values at all.
+ ("(liq_equity_total * on (symbol) group_left "
+ "max by (symbol) (liq_position_last_price_usd)) "
+ "or (liq_equity_total * NaN)", "total_usd"),
+ (EQUITY_RATIO, "ratio"),
+ ("liq_equity_exposure_usd or (liq_equity_total * NaN)", "exposure"),
+ ("liq_equity_unwrapped", "unwrapped"),
+ ("liq_equity_wrapped", "wrapped"),
+ ])
+ pct = [{"id": "unit", "value": "percentunit"}, {"id": "decimals", "value": 0}]
+ return [
+ native_table(
+ "USD · Alpaca",
+ "Alpaca's cash: no rebalance target, so it shows its share of "
+ "the USD total. The USD total is Alpaca, the primary chain's "
+ "vault and the cash in flight; other chains' vaults are not in "
+ "it. Rebalanceable = max(0, withdrawable - reserve).",
+ alpaca, "venue",
+ [("cash", "Cash", None), ("usdc", "USDC", None),
+ ("reserve", "Reserve", None),
+ ("rebalanceable", "Rebalanceable", None),
+ ("inflight", "In flight", None),
+ ("share", "Share of total", bar_cell(neutral=True)),
+ ("total", "USD total", None)],
+ w, alpaca_h, x, y, first_col="Venue", bars=("share",),
+ stretch="share",
+ widths={"venue": 80, "cash": 110, "usdc": 140, "reserve": 95,
+ "rebalanceable": 140, "inflight": 95, "share": 180,
+ "total": 120}),
+ native_table(
+ "USD · Onchain",
+ "Each chain's vault and the ratio the bot rebalances on, vault "
+ "/ (vault + Alpaca cash), against the chain's band. It is not a "
+ "share of all USD: other chains are not in it. Ethereum is the "
+ "hub wallet Alpaca deposits to and withdraws from, with no band.", chains, "venue",
+ [("vault", "Vault", None), ("inflight", "In flight", None),
+ ("wallet", "Wallet", None),
+ ("ratio", "Ratio", bar_cell()),
+ ("target", "Target", pct), ("deviation", "±", pct)],
+ w, chains_h, x, y + alpaca_h, first_col="Chain",
+ first_col_mappings=[{"type": "value", "options": {
+ key: {"text": name if key != "ethereum" else "Ethereum (hub)",
+ "index": index}
+ for index, (key, name) in enumerate(CHAIN_NAMES.items())}}],
+ bars=("ratio",), stretch="ratio",
+ widths={"venue": 125, "vault": 110, "inflight": 95, "wallet": 85,
+ "target": 85, "deviation": 60},
+ ),
+ native_table(
+ "Equities",
+ "Flags: counter trading, rebalancing and extended hours, green "
+ "when on, red when off, hollow grey when not set. "
+ "Share balances per venue, one column per chain. Total and "
+ "Total USD count only the primary chain (Base) with Alpaca and "
+ "the shares in flight: a wrapped share on another chain is "
+ "worth that chain's underlying, so the chains are not added "
+ "together. Ratio = Base vault available / (Base vault available "
+ "+ Alpaca available), green inside the default Base band and "
+ "red outside it. It is grey, with no verdict, where that is not "
+ "the bot's own band: a symbol not rebalanced on Base, with no "
+ "balance, with a vault on another chain, or with no default "
+ "target, and "
+ "every symbol on the exporter source, which cannot see the "
+ "other chains. A click on Ratio sorts by its encoded text, not by "
+ "percent. Exposure = net x live price, empty without a "
+ "price.", equity, "symbol",
+ [("flags", "Flags", FLAGS_CELL),
+ *[(f"onchain_{key}", name, None)
+ for key, name in CHAIN_NAMES.items() if key != "ethereum"],
+ ("inflight", "Inflight", None),
+ ("alpaca", "Alpaca", None), ("total", "Total", None),
+ ("total_usd", "Total USD", [
+ {"id": "unit", "value": "currencyUSD"},
+ {"id": "decimals", "value": 0}, NO_PRICE]),
+ ("ratio", "Ratio", bar_cell(unjudged=True)),
+ ("exposure", "Exposure", [
+ {"id": "unit", "value": "currencyUSD"}, NO_PRICE,
+ {"id": "thresholds", "value": {"mode": "absolute", "steps": [
+ {"color": "red", "value": None},
+ {"color": "text", "value": -0.005},
+ {"color": "green", "value": 0.005}]}},
+ {"id": "custom.cellOptions", "value": {"type": "color-text"}}]),
+ # Left-aligned like the SPA's table, except the wrap columns.
+ ("unwrapped", "Unwrapped", [{"id": "custom.align", "value": "right"}]),
+ ("wrapped", "Wrapped", [{"id": "custom.align", "value": "right"}])],
+ w, equity_h, x, y + alpaca_h + chains_h, first_col="Asset",
+ # Each width fits its header's text plus the filter icon
+ # (measured). A column under Grafana's 50px minimum renders at the
+ # minimum and would leave the stretching Ratio column too wide by
+ # the difference. With Total USD and Exposure both
+ # showing, the fixed columns no longer leave Ratio its 50px.
+ widths={"symbol": 96, "flags": 64,
+ # One per chain; only chains with balances show up.
+ **{f"onchain_{key}": 92 for key in CHAIN_NAMES},
+ "inflight": 86, "alpaca": 86, "total": 74,
+ "total_usd": 100, "ratio": 158, "exposure": 95,
+ "unwrapped": 118, "wrapped": 102},
+ bars=("ratio", "flags"), align="left", stretch="ratio",
+ sort_by={"displayName": "Asset", "desc": False}),
+ ]
+
+
+# The inventory tables (8 + 9 + 35 rows) beside Trades stacked on
+# Rebalances (25 + 26). Change these heights only after checking a new set on
+# a preview board (observability/README.md says why and how).
+TRADES_H, TRANSFERS_H = 25, 26
+# USD · Alpaca gets 8 so its one row shows under the column headers: at 6 the
+# fit left it a header and a scrollbar on windows under ~1600px tall.
+panels += native_inventory(w=13, x=0, y=1, heights=(8, 9, 35))
def latest_status_table(title, desc, log, fields, shown, w, h, x, y,
body_regex=None, number_columns=None, time_columns=(),
@@ -1106,7 +1339,7 @@ def column(name, px, mappings=None, color_text=False):
fields={"timestamp": "Time", "symbol": "Asset", "venue": "Venue",
"direction": "Side", "shares": "Size", "status": "Status"},
shown=["Time", "Asset", "Venue", "Side", "Size", "Status"],
- w=10, h=14, x=14, y=1,
+ w=11, h=TRADES_H, x=13, y=1,
number_columns={"Size": 3},
overrides=[
# No width on the last column: it takes what is left, so the
@@ -1139,7 +1372,7 @@ def column(name, px, mappings=None, color_text=False):
# match only one alternative, whatever the body's key order.
body_regex='"(?:direction|kind)":"(?alpaca_to_base|base_to_alpaca'
'|equity_mint|equity_redemption)"',
- w=10, h=14, x=14, y=15,
+ w=11, h=TRANSFERS_H, x=13, y=1 + TRADES_H,
number_columns={"Amount": 3},
time_columns=["Started"],
overrides=[
@@ -1238,8 +1471,7 @@ def column(name, px, mappings=None, color_text=False):
# audit's fallback ("keep currencyUSD on tiles, full locale in tables") was
# only needed because no unit did both — this one does, so it's used on
# both the tiles below and the Per Asset PnL table's dollar columns
-# (PNL_TABLE_USD_SIGNED further down); do NOT touch the shared USD_SIGNED
-# constant (line ~344) — Tab 1 owns it (its Exposure column).
+# (PNL_TABLE_USD_SIGNED further down).
PNL_TILE_USD = "currency:financial:$"
PNL_TABLE_USD_SIGNED = [
{"id": "unit", "value": PNL_TILE_USD}, {"id": "decimals", "value": 2},
@@ -1311,7 +1543,7 @@ def column(name, px, mappings=None, color_text=False):
("Total", "total"),
]
# P5: the SPA's Volume column, in shares rather than USD, so it gets its own
-# override rather than joining unit_overrides' USD_SIGNED sweep below. The
+# override rather than joining unit_overrides' PNL_TABLE_USD_SIGNED sweep below. The
# SPA places it right after Net; decimals=4 matches its "20.0723 sh"
# precision. It counts a matched lot on both legs, so the exporter serves
# 2 x matchedShares (verified against the entries page: their shares sum
@@ -1926,6 +2158,48 @@ def check_committed(boards):
print(f"{len(boards)} boards match the generator")
+def bar_cell_shows(cell, value):
+ """(fill, percent) that a bar cell's first matching regex mapping draws
+ for `value` as Grafana passes it, the number's string form."""
+ mappings = next(o["value"] for o in cell if o["id"] == "mappings")
+ for mapping in mappings:
+ match = re.fullmatch(mapping["options"]["pattern"], value)
+ if match:
+ html = match.expand(mapping["options"]["result"]["text"]
+ .replace("$1", r"\1"))
+ fill = re.search(r"90deg, (rgba\([^)]*\))", html)
+ text = re.search(r">([^<]*)%", html)
+ return (fill.group(1) if fill else None,
+ text.group(1) if text else None)
+ return None
+
+
+def check_bar_cells():
+ """The banded and unjudged encodings draw the colour and percent they
+ mean, for values with and without float noise. CI runs this on --check."""
+ green, red = "rgba(34,197,94,0.6)", "rgba(239,68,68,0.6)"
+ cell = bar_cell(unjudged=True)
+ cases = {
+ "45.5001": (green, "45.5"), "100.0001": (green, "100"),
+ "0.0001": (green, "0"), "45.5000999999999": (green, "45.5"),
+ "-45.5001": (red, "45.5"), "-0.0001": (red, "0"),
+ "-100.0001": (red, "100"),
+ "1045.5001": (GREY_FILL, "45.5"), "1000.0001": (GREY_FILL, "0"),
+ "1100.0001": (GREY_FILL, "100"), "1005.0001": (GREY_FILL, "5"),
+ "1010.0001": (GREY_FILL, "10"), "1000.5001": (GREY_FILL, "0.5"),
+ "1045.5000999999999": (GREY_FILL, "45.5"),
+ }
+ wrong = {value: (bar_cell_shows(cell, value), want)
+ for value, want in cases.items()
+ if bar_cell_shows(cell, value) != want}
+ # Cells without the unjudged mapping keep their two colours.
+ if bar_cell_shows(bar_cell(), "100.0001") != (green, "100"):
+ wrong["banded 100.0001"] = bar_cell_shows(bar_cell(), "100.0001")
+ if wrong:
+ raise SystemExit(f"bar cell mappings draw the wrong bar: {wrong}")
+
+
+check_bar_cells()
check_liq_pinned(dashboards)
if sys.argv[1:] == ["--check"]:
check_committed(dashboards)