Dashboards¶
A strategy's dashboard is what its owner looks at once it runs: a page of numbers,
charts and tables drawn from the deployment's own retained evidence. You write it in
dashboard.py beside strategy.py:
from almanak import dashboard as ui
from almanak.dashboard import build as b
def dashboard(context: ui.Context) -> ui.Dashboard:
return b.dashboard("WETH/USDC liquidity", ..., description="Range and value against holding.")
The deployment page draws it from the strategy's latest state, and the platform can draw it on request from the last fork test, so its author sees it before deploying. The hook is read-only and runs in a sandbox: no network, no files, no effect on execution. A missing reading stays unavailable; never show it as zero.
What a good dashboard shows¶
Write it for the person who deployed the strategy, not for its code.
- Lead with the answers. A handful of numbers that say whether the strategy is doing what they asked, whether it is safe, and whether it is making or losing money against just holding. For an LP: price, range edges, distance to each edge, value against holding. For a loan: health, LTV against the limit, debt, rates. For a hedge: the net exposure against its tolerance.
- Chart what moves. Where a reading over time explains the strategy (price against the range, health against the liquidation line, value against holding, an indicator against its levels), draw it.
- Keep the mechanics out of the way. Reserves, budgets, request escrow, retry flags
and other things the strategy needs but its owner does not belong in a collapsed group
(
b.group(..., collapsed=True)) or nowhere. - Plain, short words. Labels like "Value", "If held", "Health factor". One short text block says what is measured and what is not (fees not read, values in USDC not USD, gas shown separately). Do not claim fiat PnL the data does not carry.
Start from the closest reference's dashboard.py (below) and adapt it to the
allocations and what the user asked to see.
What context carries¶
| Field | What it is |
|---|---|
config |
The strategy's configuration. Parameters are read by name: a plain mapping (parameters["period"]), or the typed model when the workspace declares parameters.py (parameters.period); the references' param(config, "period") reads both. |
memory |
The strategy's own memory (context.memory.values). |
positions |
Each allocation's position: its target and inventory values (token amounts with asset, decimals and symbol). |
values, datasets |
Values and tables the connector projects from the retained state (exchange_inventory, payer_costs, transactions, lending_positions, ...). Every connector's transactions lists each committed transaction the strategy submitted (newest 200) with what it did in the connector's terms ("Swap USDC → WETH", "Supply USDC", "Open long ETH"), its receipt gas and time; payer_costs (exchange, composed), fund_costs (lending) or submitters (GMX) is the gas total. Tables name the fund account only when the strategy has more than one. Show a dataset with ui.Table(id=..., title=..., data=context.datasets["..."]). |
history |
Readings taken at each decision, oldest first, for charts. b.history(context, "price") gives (time, value) pairs. |
prices |
The price series the strategy declared, as its latest decision read it (b.prices(context)): the whole window from the first decision, so an indicator can be recomputed from the same closes the strategy used. |
trades |
Committed transactions that moved tokens (b.trades(context, "position")): time, intent (swap_exact_input, exit_exchange, open_liquidity, ...) and signed amounts, for markers on a chart. |
positions, values, datasets and history are separate reads: inventory is the retained state's
latest, a history row is one decision's reading, and their blocks need not match. Combine them as
retained; never mark a value unavailable only because its block differs from the latest row's.
History fields by connector¶
- Uniswap V3, PancakeSwap V3, Aerodrome Slipstream:
price(currency1 per currency0),wallet0/wallet1; with a positionrange_lower/range_upperandposition0/position1(principal at that price);value(wallet plus position, in currency1) andhold(the starting wallet at that price). Uncollected fees are not read. - Uniswap V4: the same, plus uncollected
fees0/fees1, counted invalue. - Aerodrome:
price,wallet0/wallet1; with a positionshares,position0/position1and claimablefees0/fees1;valueandholdin currency1. - Curve:
wallet0..; with a positionsharesandposition0..;valueandholdin coin0 (StableSwap at the pool's peg rates, Crypto pools at the price oracle, which also readprice1..). - Lending (Aave V3, SparkLend, Compound V3, Morpho Blue, Aave V4):
wallet,supplied,collateral,debtin the asset's units;health(with debt),ltvandmax_ltv;supply_apy/borrow_apy(0.05 is 5%);price_usd, orpricewhere the market does not price in USD (a Compound WETH market prices in WETH; a Morpho collateral leg in the loan asset);valueandholdin that same unit. Aave V4 has no prices or rates. - GMX:
size_usd,size_tokens,collateral,entry_price,funding_long/funding_short,wallet0..; a GM allocation readsgm_shares. Mark price, PnL and liquidation price are not observed. - Composed strategies prefix each part's fields with its allocation
(
liquidity:price,hedge:size_usd,loan:debt); lending legs read together. Their total isvalueandholdwhen every part lends, andlending_valueandlending_holdwhen other parts sit beside the loans (it does not include those parts).
Building blocks¶
almanak.dashboard.build turns plain numbers into blocks:
- Values:
b.token(a token amount),b.price,b.usd,b.percent,b.ratio,b.index(an indicator reading),b.count. Each takes a reason; a value you compute isevidence="author_derived". - Metrics:
b.metric(id, label, value), gathered inb.group(id, title, ...). - Charts:
b.line_chart(id, title, [b.series(...)], caption=...)withbands(a range),references(a level such as health 1.0),markers(trades) and alowerpanel for an indicator under a price (lower_marksnames its levels). - Text:
b.calloutfor a status line (tone="success","warning","danger"),ui.Textfor the scope sentence,ui.Tablefor a dataset.
A time series keeps at most 150 points (b.series thins a longer one, keeping each stretch's
lowest and highest reading, every gap and the latest point): a dashboard response is capped at
256 KiB in the fork test and preview, and each point repeats its value's metadata. The draw
applies the same bound to a Series built by hand, and keeps a table's newest 200 rows (marked
truncated), so a dashboard does not outgrow its limit as history accumulates. A failed draw says
why: the exception type dashboard.py raised, its time or CPU limit, or its size against the
1 MiB limit.
A series needs at least one point: on the first decision there is no history yet, so draw the chart only when its points exist.
Testing it¶
run_testreports whether the dashboard drew from the fork test (ready,absent, orunavailablewith the reason). Fix anunavailabledashboard before deploying.almanak report --run <fork run> --dashboard --workspace .draws the workspace's currentdashboard.pyover a finished fork test, so an edit is checked without running the fork again.- A dashboard is checked again in the browser. Integers larger than 2^53 cannot be read there exactly; keep raw protocol integers out of values you build yourself.
References¶
| Strategy type | Reference |
|---|---|
| Concentrated LP (fixed range) | uniswap_v3_arbitrum_lp, aerodrome_slipstream_base_lp |
| Concentrated LP with fees | uniswap_v4_arbitrum_lp |
| Classic AMM LP | aerodrome_base_volatile_lp |
| Stable pool LP | curve_arbitrum_stable_lp |
| Indicator trading | uniswap_v3_base_rsi |
| Lending supply and borrow | aave_v3_arbitrum_lending, compound_v3_base_lending, morpho_blue_base_lending, aave_v4_weth_supply |
| Leverage loop | arbitrum_aave_leverage_loop |
| Perpetual position | gmx_v2_arbitrum_position |
| GM liquidity | gmx_v2_arbitrum_liquidity |
| Composed (borrow and LP) | arbitrum_aave_borrow_lp |
almanak support --references --connector <id> --json lists them with their files.