# Almanak SDK

> Python SDK for writing, checking, running and backtesting autonomous DeFi strategies

# Overview

# Almanak SDK

Almanak is a Python SDK for writing, checking, backtesting and operating autonomous DeFi strategies. A strategy consists of two author-maintained files: `strategy.py`, which makes decisions, and `config.yaml`, which states the economic choices the strategy may use.

The same strategy definition runs in historical evaluation, identified forks and supported production environments. The SDK keeps network access, secrets, signing, durable execution and protocol mechanics outside strategy code.

## Learn how it works

- [How the SDK works](https://sdk.docs.almanak.co/how-it-works.html) explains the decision loop, trusted components, durable state and the differences between backtest, fork and production runs.
- [Getting started](https://sdk.docs.almanak.co/getting-started.html) walks through a complete Uniswap V4 trading reference, validates it offline, and then runs a backtest or optional fork rehearsal.

Start here if you are evaluating the SDK or need to understand why its author, operator and execution boundaries are separate.

## Build a strategy

- [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) is the contract for hooks, decision context, typed memory, outcomes and teardown.
- [Configuration](https://sdk.docs.almanak.co/configuration.html) describes author YAML, exact quantities, named accounts and markets, and trusted resolution.
- [Backtesting](https://sdk.docs.almanak.co/backtesting.html) covers explicit models, prepared data and offline reproduction.
- [Example strategies](https://sdk.docs.almanak.co/examples.html) provides a curated route through the executable references by concept and complexity.
- [Supported connectors](https://sdk.docs.almanak.co/support.html) is generated from the declarations used by admission. Use it to verify a connector, chain, action and run mode before choosing an example.

These pages are the preferred path for both human authors and coding agents. Complete executable behavior remains in the linked reference strategies.

## Operate and inspect

- [Operating](https://sdk.docs.almanak.co/operating.html) covers private operator configuration, retained workspaces, pause, resume, teardown, shutdown and production prerequisites.
- [CLI reference](https://sdk.docs.almanak.co/cli/almanak.html) lists every command and option from the current parser.
- [API reference](https://sdk.docs.almanak.co/api/authoring.html) documents the public Python contracts used by strategy code.
- [JSON schemas](https://sdk.docs.almanak.co/api/schemas.html) are the machine-readable author and backtest configuration contracts.
- [Daily QA](https://sdk.docs.almanak.co/qa.html) describes the evidence and coverage used to qualify declared support.

## Core guarantees

Safe funds own protocol positions and proceeds. Operators submit and pay gas; EOA execution is also supported. Strategies cannot access secrets, sign or contact providers. The gateway independently checks authority, while the runtime retains pending identities, accounting evidence and outcome delivery.

Every action keeps its named fund, CAIP-2 chain and CAIP-19 asset scope. Missing measurements remain missing rather than becoming zero or an unrequested fallback.

The package is published on PyPI as `almanak` from version 3.0.0. The import module is also `almanak`.

# How the SDK works

An Almanak strategy proposes economic actions. It does not talk directly to a chain, hold credentials, sign transactions or decide whether an operation was completed. Trusted SDK components resolve those responsibilities and retain the evidence needed to recover safely.

## The complete path

```
strategy.py + config.yaml
          |
          v
sandboxed strategy hook  -->  decision + proposed memory
          |                            |
          v                            v
trusted runtime  <-->  connector  <-->  authenticated gateway  <-->  provider / chain
          |
          v
durable state, outcomes, accounting and reports
```

Backtests use the same strategy hooks and economic intents, but replace live execution with an explicit historical model. Fork and production runs use the shared execution, reconciliation and accounting path with different operator authority.

## What each part owns

| Part                   | Owns                                                                              | Does not own                                                            |
| ---------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Strategy               | Economic decisions and proposed memory changes                                    | Secrets, network access, signing, persistence or authoritative holdings |
| Author configuration   | Markets, accounts, allocations, quantities, timing and selected financial sources | Providers, wallet addresses, signing material or fork fixtures          |
| Runtime                | Scheduling, durable work, recovery, outcome delivery and teardown                 | Protocol-specific transaction construction or credentials               |
| Connector              | Protocol observations, actions, transaction plans, position semantics and cleanup | Strategy scheduling or independent durable state                        |
| Gateway                | Controlled reads, authorization, credentials, signing and submission              | Strategy decisions or lifecycle progression                             |
| Operator configuration | Providers, account bindings, fork fixtures and production authority               | Economic strategy choices                                               |
| Durable record         | Captured inputs, pending work, outcomes, positions and evidence                   | A second scheduler or an alternative view of completion                 |

This separation lets the gateway reject an unauthorized request even if strategy code proposes it, and lets the runtime recover admitted work without asking the strategy to guess what happened.

## The decision cycle

1. The runtime checks whether a strategy evaluation is eligible.
1. Connectors acquire the explicitly requested observations through the gateway, or read retained historical inputs in a backtest.
1. The sandbox receives resolved configuration, strategy memory, owned positions, observations, chain availability and the current trigger.
1. `decide(ctx)` returns at most one typed economic intent and an optional memory update.
1. The runtime records the admitted decision before external execution.
1. The connector expands the economic intent into the protocol steps it owns, such as approvals, a swap, repayment or allowance cleanup.
1. The gateway independently checks the exact operation and its execution authority before a live or fork submission.
1. The runtime reconciles the result, updates connector-owned positions and delivers the retained outcome to `on_outcome(ctx)`.

An approval or another setup step can produce an `intermediate` outcome. The runtime continues that already admitted economic action without calling `decide()` again. Only economic completion produces `completed`; a refused or unsuccessful operation produces `failed`.

## State, positions and recovery

Strategy memory records decision state such as a phase or start time. It is not an authoritative account balance or proof that a position exists. Connectors and the durable runtime record own economic positions, pending transactions, allowances, debt and other obligations.

Before resuming, the runtime reconciles recorded external work. An unknown outcome cannot authorize a replacement action. Captured strategy source and configuration remain fixed for the run. Persisted strategy memory evolves through accepted updates and remains available for recovery, so editing the original files does not change an open deployment.

Teardown is also a durable lifecycle state. It bypasses the ordinary decision interval and asks the strategy and connectors to reduce risk and close their known obligations. A successful transaction receipt alone is not proof that all debt, positions or spending authority are clear.

## Three environments

| Environment | What it proves                                                           | Authority and data                                                                                      |
| ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Backtest    | Strategy behavior under a named historical model and frozen assumptions  | Hypothetical inventory; preparation may use read-only providers, execution and reproduction are offline |
| Fork        | Real contract behavior at an identified chain state with simulated funds | Generated fixture accounts and authority on retained local fork processes                               |
| Production  | Supported execution against a live chain                                 | Explicit operator account bindings, payer ceilings and signing authority                                |

Results from one environment do not imply guarantees from another. A passing backtest does not prove contract execution, and a fork lifecycle does not authorize real spending.

## Identity and scope

Chains use strict CAIP-2 identifiers such as `eip155:1`. Assets use CAIP-19 identifiers such as `eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`. Symbols such as USDC and WETH are display metadata, not identity.

Every action remains scoped to a named fund account, chain, connector target and allocation. The gas payer can be different from the account that owns the position. Likewise, two allocation names do not create separate ownership of the same account and protocol balance.

## Continue from here

- Follow [Getting started](https://sdk.docs.almanak.co/getting-started.html) for a complete reference journey.
- Use [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) while writing `strategy.py`.
- Use [Configuration](https://sdk.docs.almanak.co/configuration.html) while writing `config.yaml`.
- Check [Supported connectors](https://sdk.docs.almanak.co/support.html) before selecting a connector, chain, action or environment.
- Browse [Example strategies](https://sdk.docs.almanak.co/examples.html) to find the nearest executable starting point.
# Learn

# Getting started

This tutorial follows the [Uniswap V4 USDC trading reference](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v4_trading). It is small enough to read in one sitting but includes the full strategy lifecycle: enter, hold, recover after restart and exit.

The tutorial uses a repository checkout because reference strategies and sample operator configurations are not installed with the Python package.

## Requirements

Install Python 3.12+ and [uv](https://docs.astral.sh/uv/). Local strategy hook execution is currently verified on Linux. On Debian or Ubuntu, install the required sandbox with:

```
sudo apt-get install bubblewrap libseccomp2
```

Fork execution also requires Foundry's `anvil`. macOS packages and CLI commands are available, but macOS currently rejects the author-worker memory limit, so strategy hook execution fails closed there until that policy is redesigned.

Clone the repository and install its locked dependencies:

```
git clone https://github.com/almanak-co/sdk.git
cd sdk
uv sync --locked
```

For a separate project, install the SDK with `uv add almanak`. To install only the command-line tool, use `uv tool install almanak`. Commands in this tutorial run from the checkout and therefore use `uv run --locked`.

## Read the two author files

The reference has two inputs maintained by the strategy author:

- [`strategy.py`](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v4_trading/strategy.py) contains the decision hooks and typed memory.
- [`config.yaml`](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v4_trading/config.yaml) selects the market, fund, capital, risk bounds and timing.

The configuration joins three names. This abbreviated excerpt shows the binding; use the complete reference file as the executable input:

```
strategy:
  allocations:
    trading:
      market: trading_market
      account: trading
      capital:
        asset: eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
        quantity: '100'

markets:
  trading_market:
    connector: uniswap_v4
    chain: eip155:1
    # Complete pool identity follows in the reference.

accounts:
  trading:
    chain: eip155:1
```

`trading` is a local allocation and account name. `trading_market` is a local market name. On-chain identity comes from the connector target, CAIP-2 chain and CAIP-19 assets, never from these labels or token symbols.

The strategy keeps only the time at which each purchase completed:

```
from pydantic import Field

from almanak.api import StrategyState, declared


class State(StrategyState):
    bought_at: dict[str, int] = Field(default_factory=dict)


def capabilities(config):
    return declared(config)
```

At each evaluation, `decide()` reads the connector observation for an allocation. It asks the connector to enter if the position is not complete, and to perform a full exit when the hold expires or teardown begins. `hold_seconds` is optional and read only by this code; without it the allocation holds until teardown:

```
def decide_allocation(context, allocation, observed):
    started = state(context).bought_at.get(allocation.name)
    due = (
        started is not None
        and allocation.hold_seconds is not None
        and int(observed.effective_at.timestamp()) >= started + allocation.hold_seconds
    )
    if context.teardown or due:
        return context.actions.exit(allocation.name)
    if not observed.owned.entry_complete:
        return context.actions.swap(allocation.name)
    return None
```

The connector owns approvals, swap construction, residual conversion and allowance cleanup. `on_outcome()` records the purchase time only after the economic swap is complete. An intermediate approval does not advance strategy memory.

## Check it offline

Run the source and author-configuration checks without provider access:

```
uv run --locked almanak check strategies/uniswap_v4_trading --format text
```

The result should report `valid: True`, with `configuration` and `sandbox` passed. It reports `ready: False` because market resolution, hooks and online admission deliberately remain `not_checked` without an explicit provider and run mode. A skipped online stage is not a passing readiness claim.

Inspect the exact connector declaration used during admission:

```
uv run --locked almanak support \
  --connector uniswap_v4 --chain eip155:1 --format text
```

Use this command before adapting the strategy to another connector, chain, action or mode. The generated [support catalog](https://sdk.docs.almanak.co/support.html) presents the same data.

## Run the historical evaluation

The reference's `backtest-economic.yaml` adds a historical window, hypothetical inventory, execution latency, sampling bounds, costs and end treatment. Provider access is needed only to prepare or find matching historical inputs. Create an ignored `.env` containing the operator's Alchemy key:

```
ALCHEMY_API_KEY=your-key
```

Check the author and backtest inputs together, then run:

```
uv run --locked almanak check strategies/uniswap_v4_trading \
  --backtest-config strategies/uniswap_v4_trading/backtest-economic.yaml \
  --format text
uv run --locked almanak backtest run strategies/uniswap_v4_trading \
  --backtest-config strategies/uniswap_v4_trading/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --env-file .env --output runs/uniswap-v4-tutorial
```

Preparation resolves the configured market at the historical boundary and collects the explicit model inputs. Economic execution then runs offline. The workspace retains the resolved facts, data identity, decisions, hypothetical positions, costs and progress.

Reproduce the run without provider access and render a portable report:

```
uv run --locked almanak backtest reproduce --run runs/uniswap-v4-tutorial
uv run --locked almanak report --run runs/uniswap-v4-tutorial \
  --format html --output uniswap-v4-tutorial.html
```

## Optional fork rehearsal

A fork exercises the real deployed contracts at the exact anchor in the operator fixture. It uses simulated funds and generated authority; it does not authorize real spending.

First resolve and validate the complete fork inputs through a read-only gateway:

```
uv run --locked almanak check strategies/uniswap_v4_trading --fork --online \
  --gateway-config examples/operators/uniswap_v4_trading.fork.yaml \
  --env-file .env --format text
```

Then start the supervised run:

```
uv run --locked almanak run strategies/uniswap_v4_trading --fork \
  --gateway-config examples/operators/uniswap_v4_trading.fork.yaml \
  --env-file .env --output runs/uniswap-v4-fork
```

Ctrl-C pauses the coordinator while retaining the gateway, fork process, positions and pending work. Inspect and close the deployment explicitly:

```
uv run --locked almanak status --run runs/uniswap-v4-fork
uv run --locked almanak resume --run runs/uniswap-v4-fork
uv run --locked almanak teardown --run runs/uniswap-v4-fork
uv run --locked almanak status --run runs/uniswap-v4-fork
uv run --locked almanak shutdown --run runs/uniswap-v4-fork
```

Wait for recorded closure before shutdown. A stopped fork cannot be recreated as the same deployment, and resume never invents replacement history.

## Build your own

Use this order when adapting a reference:

1. Find the nearest supported lifecycle in [Example strategies](https://sdk.docs.almanak.co/examples.html).
1. Confirm its connector, chain, actions and run modes in [Supported connectors](https://sdk.docs.almanak.co/support.html).
1. Change author choices in `config.yaml` while preserving strict chain and asset identities.
1. Change `strategy.py` through the public hook and action contracts in [Strategy authoring](https://sdk.docs.almanak.co/authoring.html).
1. Run the offline check after every small change.
1. Add an explicit backtest configuration and inspect the retained assumptions.
1. Use a fork only when the strategy passes its earlier checks.

Read [Configuration](https://sdk.docs.almanak.co/configuration.html) for the author schema, [Backtesting](https://sdk.docs.almanak.co/backtesting.html) for data and model limits, and [Operating](https://sdk.docs.almanak.co/operating.html) before working with retained fork or production workspaces.
# Build

# Strategy authoring

A strategy directory contains `strategy.py` and `config.yaml`. There is no strategy base class. The SDK captures the validated parameters and Python source for each run, so editing the original files does not change an open or resumed deployment.

Use [Getting started](https://sdk.docs.almanak.co/getting-started.html) for an end-to-end walkthrough and [Example strategies](https://sdk.docs.almanak.co/examples.html) to choose an executable starting point. This page is the authoring contract to keep beside the code.

## Public imports

Import shared author types from `almanak.api` and connector decision types from the selected connector's public API:

```
from pydantic import Field

from almanak.api import Memory, StrategyState, declared
from almanak.connectors.uniswap_v4.api import Decision, DecisionInput, OutcomeInput
```

Do not import runtime, gateway, execution or signer implementations into a strategy. Connector-specific parameters and observation types are resolved from the author configuration before hooks run.

## Hook contract

| Hook                            | Required | Contract                                                                                                                                                                                                       |
| ------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `capabilities(parameters)`      | Yes      | Return the result of public `declared(parameters)`. It declares the economic actions the resolved configuration allows.                                                                                        |
| `decide(ctx)`                   | Yes      | Return the connector's typed decision with at most one economic intent and the proposed memory.                                                                                                                |
| `validate_config(parameters)`   | No       | Return `None` or raise `InvalidConfig` with an actionable message. It runs in the sandbox after trusted resolution.                                                                                            |
| `data_dependencies(parameters)` | No       | Return explicit historical or financial observation dependencies, such as the [price series](#price-series-and-technical-analysis) decisions read. An omitted hook means no additional dependencies.           |
| `on_outcome(ctx)`               | No       | Return memory after an admitted action stage. An omitted hook leaves memory unchanged.                                                                                                                         |
| `on_event(ctx)`                 | No       | Return memory after a connector-owned external event such as a liquidation. An omitted hook leaves memory unchanged.                                                                                           |
| `dashboard(ctx)`                | No       | Return a typed read-only dashboard over retained evidence, in `dashboard.py` ([Dashboards](https://sdk.docs.almanak.co/dashboards.html)). Failure cannot change execution state or suppress the shared report. |

Hooks are plain module-level functions. Expected neutral market conditions should return a decision without an intent. Programming errors should raise and remain visible rather than being converted into healthy no-action results.

## Declaring capabilities

Every strategy must declare the bounded actions implied by its resolved author configuration:

```
def capabilities(config):
    return declared(config)
```

The trusted connector derives exact assets, protocol calls, recipients and cleanup permissions from this declaration. Strategy code cannot add authority, install permissions or provide a handwritten permission override.

`validate_config()` can enforce policy that is specific to the strategy rather than the connector schema:

```
from almanak.api import InvalidConfig


def validate_config(config):
    if any(allocation.range is not None for allocation in config.allocations):
        raise InvalidConfig("trading allocations must not request liquidity permissions")
```

A refusal is reported as `invalid_config` with its message and line, and is final: `check --online` fails the `strategy` stage with it, and `test run` or `run` stops setup without a retry. `InvalidConfig` counts as a refusal from any function in strategy.py. A plain `raise ValueError(...)` counts only when a `raise` statement in `validate_config` itself raises it. Any other exception, including a ValueError from unpacking, a conversion or a pydantic model, is a crash, reported as a traceback starting at the strategy's own frame.

Schema validation and trusted gateway checks still apply. Strategy validation does not replace either boundary.

Author knobs live in [`strategy.parameters`](https://sdk.docs.almanak.co/configuration.html#strategy-parameters) and arrive as `config.parameters`, a read-only mapping in every hook and as `ctx.config.parameters` in `decide`, `on_outcome` and `on_event`. It is `{}` when the section is omitted. A list arrives as a tuple, and an unquoted decimal as a `float`: quote a decimal and convert it with `Decimal` when it must be exact. `almanak check` validates only their shape, so validate the values here:

```
from decimal import Decimal


def validate_config(config):
    if not 1 <= config.parameters["loops"] <= 5:
        raise ValueError("parameters.loops must be between 1 and 5")
    Decimal(config.parameters["target_ltv"])
```

### Typed parameters

Or declare them as a typed model, and the SDK validates them for you. Subclass `TypedParameters` once in strategy.py, as a top-level class by convention named `StrategyParams` (every reference and template uses that name). The SDK finds it by its base class. It may extend a shared base model; two unrelated models are refused:

```
from decimal import Decimal

from pydantic import Field, field_validator

from almanak.api import PositiveDecimal, TypedParameters, UnitFraction, parameter_base_units


class StrategyParams(TypedParameters):
    borrow_quantity: PositiveDecimal
    target_ltv: UnitFraction
    loops: int = Field(default=3, ge=1, le=5)

    @field_validator("loops")
    @classmethod
    def single_above_80(cls, loops, info):
        if loops > 1 and info.data.get("target_ltv", 0) > Decimal("0.8"):
            raise ValueError("target_ltv above 0.8 allows a single loop only")
        return loops


def borrow_amount(config):
    decimals = config.asset_metadata[config.allocation("loan").reserve.asset].decimals
    return parameter_base_units(config.parameters, "borrow_quantity", decimals)
```

With a model, `config.parameters` is its validated, frozen instance in every hook: `validate_config`, `capabilities`, `data_dependencies`, `decide`, `on_outcome`, `on_event`, teardown decisions and `dashboard`, in forks, production and backtests. The SDK validates it before any hook runs, wherever `validate_config` runs. Each YAML scalar converts to its field's type (`"1"` or `1.0` to an `int`, `0.2` or `"0.2"` to a `Decimal`, `3` to a `str`); `1.5` for an `int`, infinities and NaN are refused. As without a model, a `bool` field takes only `true` or `false`, and a number field refuses a boolean or a `_` in its digits; a tuple's elements convert as pydantic's lax mode does. A key the model does not declare is refused, and a missing key takes the field's default or is refused when it has none. A refusal is the strategy's final `invalid_config`, naming the field, such as `strategy.parameters.borrow_quantity: Input should be greater than 0`, never a traceback. `PositiveDecimal` is a `Decimal` above 0 and `UnitFraction` one strictly between 0 and 1. A field takes any pydantic scalar type or constraint, or a `tuple[...]` of scalars for a flat list; a nested mapping or list is refused at input, and a mutable `list`, `set` or `dict` field or a field alias is refused as `invalid_config` naming the field.

Put a rule between knobs in a `field_validator` on the later-declared knob, reading the earlier ones from `info.data` (a knob that failed its own check is absent there), so the refusal names a field: `strategy.parameters.loops: target_ltv above 0.8 allows a single loop only`. A `model_validator` refusal names only `strategy.parameters`. `parameter_base_units` converts a quantity knob to the asset's base units and refuses one finer than its decimals, naming the field, rather than truncating it; call it from `validate_config` so the refusal comes before any action. Rules that also read the rest of the configuration, such as a knob above an allocation's `minimum_health_factor`, stay in `validate_config`. `--param` and `test_parameters` may set any field the model declares, including one config.yaml leaves to its default: `--param loops=1` works for `loops: int = Field(default=2, ge=1)` with no `loops` in config.yaml, only in the fork workspace's copy. A key the model doesn't declare is refused with the model's fields listed, and the model checks every value it is given, so `--param loops=0` is an `invalid_config` naming `strategy.parameters.loops`. The SDK reads the field names from strategy.py's source without running it, and loads the strategy in the sandbox when the source alone can't tell them: a model under `if` or `try`, built with `type(...)`, with a base that isn't a model (a plain mixin's annotations are fields too), or extending a base imported from outside the SDK. Without a model, `config.parameters` stays the read-only mapping described above.

Declare the fork-test values next to them in [`strategy.test_parameters`](https://sdk.docs.almanak.co/configuration.html#strategy-parameters): the fewest-decision values that still exercise the strategy's paths, such as `loops: 1`. `almanak test run` applies them by default and its verdict names what ran. With a typed model, those values keep their YAML form and the model converts and checks them, so a test value it refuses is an `invalid_config` before the fork starts. Strategy code never sees `test_parameters` outside that fork test. Fork prices don't move, so a price trigger can't be made to fire there by tightening it; leave such knobs at their production values.

## Reading decision context

`decide(ctx)` receives the current resolved view. The important fields and helpers are:

| API                                                | Meaning                                                                                                                                                                                                                                                 |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.config`                                       | One authoritative resolved parameter instance, fixed for the run.                                                                                                                                                                                       |
| `ctx.config.parameters`                            | The read-only author [`strategy.parameters`](https://sdk.docs.almanak.co/configuration.html#strategy-parameters), `{}` when omitted, or the strategy's validated `TypedParameters` instance.                                                            |
| `ctx.memory`                                       | The strategy's last committed memory.                                                                                                                                                                                                                   |
| `ctx.owned`                                        | Connector-owned position records, separate from strategy memory. In a composed strategy each also reports the fund's [owned inventory](#wallet-balance-and-owned-inventory) of its assets.                                                              |
| `ctx.observations`                                 | Current connector observations made available for this evaluation.                                                                                                                                                                                      |
| `ctx.availability`                                 | Per-chain readiness or retained fault state.                                                                                                                                                                                                            |
| `ctx.teardown`                                     | `True` when the durable deployment is closing.                                                                                                                                                                                                          |
| `ctx.prices`                                       | The declared [price series](#price-series-and-technical-analysis), as available at this decision.                                                                                                                                                       |
| `ctx.price(asset)`                                 | The one declared series for an asset, or raise.                                                                                                                                                                                                         |
| `ctx.config.allocation(name)`                      | Resolve a named validated allocation.                                                                                                                                                                                                                   |
| `ctx.observation(name)`                            | Return an observation or `None`, while tracking its scope.                                                                                                                                                                                              |
| `ctx.require_observation(name)`                    | Return an observation or raise a scoped missing-data error.                                                                                                                                                                                             |
| `ctx.position(name)`                               | Return a retained owned position or raise. It never invents zero holdings.                                                                                                                                                                              |
| `ctx.actions`                                      | Connector-owned typed economic actions bound to this context.                                                                                                                                                                                           |
| `ctx.decision(intent=..., memory=..., reason=...)` | Build the typed decision and retain every observation scope accessed by the intent. `reason` optionally says why the strategy holds or takes the intent; only a test run's coordinator logs it (`decision_evaluated`), and nothing acts on or keeps it. |

A decision must not treat missing observations as zero. For a multi-chain strategy, unavailable work on one chain can be skipped only when the intended action and its evidence are independent of that chain.

A compact decision loop looks like this:

```
def decide(context: DecisionInput) -> Decision:
    holds = []
    for item in context.observations:
        observed = context.require_observation(item.allocation)
        allocation = context.config.allocation(observed.allocation)
        intent = decide_allocation(context, allocation, observed)
        if intent is not None:
            return context.decision(intent=intent)
        holds.append(hold_reason(context, allocation, observed))
    return context.decision(reason="; ".join(dict.fromkeys(holds)) or None)
```

A test run shows the reason on its live card, so write it for the person watching: short, in plain words, with the numbers that decided it, such as `reason=f"RSI {rsi:.0f}; buying below {knobs.oversold:g}"` or `"Price left the range, recentring"`. Give a hold a reason whenever its cause is known; give an intent one only when it says something the intent does not, such as why a stop or an unwind fired.

The complete implementation is in the [Uniswap V4 trading reference](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v4_trading/strategy.py).

### Health factors are wad integers

Strategy code reads `minimum_health_factor` as an integer at wad precision (`10**18` is 1.0), on every lending connector. The author writes `'2'`; code reading `allocation.minimum_health_factor` receives `2 * 10**18`. Compare it with other wad values, or divide by `10**18` before using it as a ratio:

```
WAD = 10**18
floor = context.config.allocation("collateral").minimum_health_factor  # 2 * WAD
# collateral value that keeps the floor, with the threshold in basis points
keep = -(-debt_value * floor * 10000 // (liquidation_threshold_bps * WAD))
```

Using the wad integer as if it were the ratio itself (leaving out the division by `WAD`) makes every bound derived from it about `10**18` times too large, so a withdrawal sized from it is never safe and the strategy holds forever. Writing the literal `2` in place of `floor` in the formula above makes `keep` about `10**18` times too small and can permit an unsafe withdrawal.

### Wallet balance and owned inventory

A composed strategy may spend only what it owns: its configured `inventory` plus what its own actions produced (borrowed, withdrawn or swapped into the fund). An exchange allocation's `owned.inventory0` and `owned.inventory1` report that amount for the pair's assets; a lending allocation's `owned.inventory` reports it for the market's asset. Each reflects the last committed outcome, including interest a withdrawal returned. Allocations of one account and asset report the same single amount, which one spend consumes, so a rotation withdraws from one market and then supplies `ctx.position("next").inventory.amount` to the other. Single-connector lending strategies have no separate owned cash; their `inventory` is `None`. A lending observation's `wallet_balance` is the raw on-chain balance of the fund, including anything else sent to it. The two can differ on a fork: a generated fork fixture funds each asset 1% (at least one base unit) above the requirement, while a hand-written fixture funds the amounts it declares. Size supplies, swaps and partial repays from owned inventory, never from `wallet_balance`: an action that spends more than the fund retains is refused as `inventory_unavailable`.

A full `repay` is the exception. It needs the whole debt, accrued interest included, in the fund wallet, and it may spend loan asset sent to the fund beyond owned inventory. Short of that it is refused with `repayment_funding`, naming the shortfall; a transfer to the fund resolves it. Use `repay_amount`, where the connector has one, to repay part of a debt from owned inventory.

### A loan-asset reserve for interest

Interest is not prefunded: a teardown's full repay needs the debt plus the interest accrued since the borrow, so the borrowed amount alone is always a little short. A strategy can accept `capital` on the loan allocation (in the loan asset's units, for example `'1'`) as a loan-asset reserve it never supplies or spends, so the full repay uses it for the interest; the lending references that link here do. Without it, the repay refuses with `repayment_funding` until loan asset is sent to the fund. Size it to the expected interest over the holding time; the fork funding must cover it.

## Price series and technical analysis

A strategy that needs price history, for an indicator such as RSI or a moving average, declares each series in `data_dependencies`. Every decision then receives the latest points of that series in `ctx.prices`. The SDK reads the prices from a pool's own on-chain state through the operator's RPC provider, with no other data service, and computes no indicator; the strategy computes one itself, typically with TA-Lib.

```
import numpy as np
import talib

from almanak.api import AssetType, DataDependencies, PoolPrice, PriceBinding

WETH = AssetType("eip155:8453/erc20:0x4200000000000000000000000000000000000006")


def data_dependencies(config):
    return DataDependencies(
        prices=(
            PriceBinding(
                purpose="strategy",
                chain=config.target("position").fund.chain,
                asset=WETH,  # priced in the pool's other token, here USDC
                window=120,  # hourly closes each decision sees
                source=PoolPrice(
                    kind="pool_state",
                    connector="uniswap_v3",
                    pool="0xd0b53d9277642d899df5c87a3966a349a798f224",
                    timeframe="1h",
                    max_age_seconds=7200,
                    availability_lag_seconds=300,
                ),
            ),
        )
    )


def decide(ctx):
    series = ctx.price(WETH)
    if not series.complete:
        return ctx.decision()  # a short or stale window decides nothing
    rsi = float(talib.RSI(np.array([float(v) for v in series.values]), 14)[-1])
    ...
```

**Declaration.**

- **Purpose:** `purpose` is `strategy`.
- **One pool per series**, on the binding's chain, named for its connector: `connector` is `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome` (classic volatile or stable pools) or `curve` (StableSwap NG, TwoCrypto NG and TriCrypto NG pools) with the pool contract address as `pool`, or `uniswap_v4` with the pool id. The connector's own pool resolution verifies the pool, so a pool the connector refuses (a Curve metapool, a pool no reviewed factory registers, a hooked V4 pool) is refused as a price source too. The declared `asset` must be one of the pool's coins. The pool need not be one the strategy trades: a strategy on any connector may name a supported pool on the same chain. `ax pool` checks a V3-style pool and its tokens.
- **Quote:** `quote` names the coin the asset is priced in. It defaults to the other coin of a two-coin pool and is required for a Curve pool of three or more coins (TriCrypto, multi-coin StableSwap), where it must be another coin of the pool; the SDK never picks one.
- **Two sources:**
- `PoolPrice` (`kind: pool_state`): one close per candle, the pool's price at the end of the last block at or before the candle boundary. A boundary with no trades still has a close, the pool's actual state, so the series has no gaps. The close is the pool's spot price: the V3/V4 pool price; for Aerodrome the reserves' ratio (volatile) or the marginal price of the x³y + y³x invariant on decimal-normalized reserves (stable); for Curve StableSwap the invariant's marginal price from balances, coin decimals and the current A; for Curve TwoCrypto and TriCrypto the pool's own `last_prices`, its spot price after its last trade. All exclude fees.
- `PoolSwapCandles` (`kind: pool_swaps`): OHLCV candles. A candle opens at the previous boundary's close and closes at this boundary's; its high and low also cover every trade price in between, and its volume is the sum of the trades' absolute amounts of the quote coin, in whole tokens (USDC above). A trade's price is the pool's resulting spot price for V3/V4 Swap events and Aerodrome `Sync` events (Aerodrome volume is the larger of each `Swap`'s quote amounts in and out). For Curve it is the execution price of each `TokenExchange` between the asset and the quote, the bought amount over the sold amount, so it includes the pool fee: Curve highs and lows can lie outside the spot closes by about the fee, and trades between other coins of the pool are not counted. A candle without trades is flat with zero volume. It reads every trade log of the pool, so it costs more provider reads than closes.
- **Price unit:** the declared asset in its quote coin, decimal-adjusted (WETH in USDC above), rounded to 30 significant digits. `series.quote` names the quote. The SDK converts to no other unit. Ratio and relative indicators (RSI, MACD, EMA/SMA crosses, Bollinger bands, Stochastic, percentage moves) do not depend on the unit. A strategy that needs absolute USD levels, or USD comparisons across assets, names a stablecoin-quoted pool on the same chain as its price source; any supported pool may be named, not only the traded one.
- **Timeframe:** `1m`, `5m`, `15m`, `1h`, `4h`, `12h` or `1d`.
- **Window:** `window` is how many points each decision sees, at most 8760 (`MAX_WINDOW` in `almanak.api`).
- **Age and lag:** `availability_lag_seconds` is how long after its boundary a point counts as available: block-finality and confirmation slack. `max_age_seconds` must cover the timeframe plus the lag; it is how old the latest point may be for the series to count as `complete`.
- **One series per asset.**
- **Single prices (Aave V4 economic backtests only):** a strategy price without a window may instead read a CoinGecko coin price (`CoinGeckoPrice`) or a Spoke oracle (`ProtocolPrice`). It arrives in `ctx.prices` as one `PriceObservation` (`value`, `source_time`, `available_at`), may also be listed in the backtest file (it must then equal the hook's declaration), and is not delivered on forks or live.

A backtest configuration never repeats a strategy's series; the backtest reads them from this hook.

**What a decision sees.** `ctx.prices` holds one `PriceSeries` per declaration: the latest `window` points, oldest first, that were available at the decision.

- **Cutoff:** a point is available once its boundary has passed and its availability lag has elapsed. Nothing later is visible.
- **No substitution:** a failed read is never replaced by another source.
- **Members:**
- `values`: the closes as `Decimal`.
- `opens`, `highs`, `lows`, `volumes` (swap candles only; a closes series raises): the other candle members as `Decimal`.
- `points`: `source_time` (the boundary), `value` (the close) and, for swap candles, `opened_at`, `open`, `high`, `low` and `volume`.
- `quote`: the token the points are priced in.
- `latest`: the newest point.
- `gaps`: consecutive points further apart than the timeframe.
- `complete`: true when the window is full, evenly spaced and its latest point is within `max_age_seconds`.

Decide nothing on an incomplete series, as above. `ctx.price(asset)` raises for an asset without exactly one declared series.

**Every run mode sees the same points.**

- **Backtests:** preparation reads each series through the operator's archive provider from `window` points before the start, so the first decision already has a full window.
- **Fork and live runs:** the trusted gateway reads the series from its chain before each decision, cut off at the earlier of the decision time and the observed block time, and retains what it served. A fork therefore sees the points as of its fork block. The gateway refuses to start when a declared pool does not resolve on its chain or does not hold the declared asset. If a read fails at a decision, that decision holds (no action) and the next one tries again. Teardown decisions receive no series in any run mode, backtests included, so a failed read never blocks an unwind: exit before reading `ctx.price` when `ctx.teardown` is true, as the reference does.

**Indicators.**

- **Library:** hosted runtimes and backtests include numpy and TA-Lib; elsewhere install `almanak[ta]`. Convert `values` (and `highs`, `lows` or `volumes` for indicators such as ATR, stochastics or OBV, which need `PoolSwapCandles`) to float arrays.
- **Window size:** smoothed indicators settle only after several periods, so declare a window several times the indicator's period (`window=120` for RSI-14 on hourly closes).
- **Crossings:** keep the previous indicator value in memory and act when it crosses a level, not on every decision past the level, which would repeat the trade. A previous value or an entry price is a decision record, which memory may hold. Memory is committed with the decision, before its action executes: to retry an entry whose action failed, record it as pending and confirm it in `on_outcome`, as the reference does. Decisions more frequent than the timeframe see the same latest point until the next boundary.
- **Levels or crossings:** a crossing needs the series to move between two decisions. On a fork the series stands still at the fork block, so a strategy that acts only on crossings never acts there. A rule on a level (RSI inside a band, a close above its average) gives the same answer for the same series and can be exercised on a fork anchored where the level holds. Keep memory unchanged while holding: derive values such as the highest close since entry from the series instead of writing them every decision.
- **The strategy bounds its own trades.** An exchange allocation is offered an entry quote whenever it holds funding inventory, and authority carries no amount bound, so a signal that fires again can enter again. `entry_complete` records first exposure; it does not stop re-entry. A strategy that should trade one cycle records that in memory, as the reference does.
- **Timed exits are the strategy's.** `allocation.hold_seconds` is optional and only strategy code reads it; no connector closes a position when it elapses. When it is `None` there is no scheduled exit and the allocation holds until teardown, so a stop-only strategy omits it.

The [Uniswap V3 Base RSI reference](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v3_base_rsi/strategy.py) is a complete strategy: it declares the hourly WETH closes of its own pool, computes RSI-14 with TA-Lib, buys on a cross below 30 and sells on a cross above 70. Three more Base references use levels: an [RSI-gated LP](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v3_base_rsi_lp/strategy.py), a [loan held above a moving average](https://github.com/almanak-co/sdk/blob/main/strategies/aave_v3_base_sma_borrow/strategy.py) and an [ATR breakout on full candles](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v3_base_atr_breakout/strategy.py).

## Requesting economic actions

Use `ctx.actions` instead of constructing protocol transactions. Actions remain bound to the configured allocation and the observations accessed by the decision.

Lending connectors expose actions such as `supply`, `borrow`, `repay` and `withdraw`. Exchange connectors expose actions such as `swap`, `open_liquidity`, `collect`, `close_liquidity` and `exit`. The exact supported set is connector-specific; check [Supported connectors](https://sdk.docs.almanak.co/support.html) and the nearest reference before using one. The generated [strategy actions](https://sdk.docs.almanak.co/support.html#strategy-actions) list gives each connector's exact `ctx.actions` signatures. In the bundled docs it is the "Strategy actions" section of Supported connectors and chains, with one heading per connector such as "uniswap_v3 actions" or "gmx_v2 actions" listing each method and what it does, for example what `exit` teardown closes, converts and revokes, and `repay` versus `repay_amount`.

Concentrated-liquidity allocations configured with a `band` instead of a fixed `range` recentre from strategy code. `open_liquidity` selects the position centred on the observed tick, the retained `owned.position.range` says where it sits, and `swap(name, rebalance=True)` follows the `rebalance_quote` that brings inventory to the ratio the next centred position consumes:

```
position = observed.owned.position
if position is not None:
    if position.range.lower <= observed.pool.tick < position.range.upper:
        return None
    return ctx.actions.close_liquidity(name)
if observed.rebalance_quote is not None and not already_rebalanced:
    return ctx.actions.swap(name, rebalance=True)
return ctx.actions.open_liquidity(name)
```

Every selection is checked against the band by the planner and the gateway; a strategy cannot move liquidity outside it.

An action is economic, not a promise of one transaction. For example, an exit can close an owned LP position, collect fees, convert residual strategy inventory to the configured funding asset and revoke remaining spending authority. The connector owns that sequence and its completion test.

Some counter inventory is too small to swap. Fees left behind by an out-of-range close are a common example. When its exact-size exit quote returns nothing, the observation carries it as `exit_residual` (`exit_residuals` for Curve) instead of an `exit_quote`. No swap is offered for it, `actions.exit` returns `None` once it is the only remaining obligation, and the dust stays in owned inventory and reports. A zero-output entry or rebalance quote is simply absent.

An entry slice the market cannot fill at the observed block, because the Quoter reverts, the pool has no liquidity for it or a backtest swap would leave the prepared ticks, offers no `entry_quote`. The observation records why as `entry_quote_error`, and its exit and rebalance quotes are unaffected. Exit observations and teardown in backtests measure no entry quote.

## Typed memory

Define `State` as a subclass of `StrategyState` when a strategy needs durable decision state:

```
from typing import cast


class State(StrategyState):
    version: int = 1
    bought_at: dict[str, int] = Field(default_factory=dict)


def state(context) -> State:
    return cast(State, context.memory)
```

The worker hydrates `State` inside the sandbox. Return an immutable update with `state.updated(...)`:

```
return context.decision(memory=state(context).updated(bought_at=new_times))
```

The durable store retains a JSON envelope containing the version, schema identity and values. Incoming and returned state are validated. The fields `values` and `schema_id` are reserved for that envelope. A strategy without a `State` class uses generic `Memory`.

JSON has no infinity or NaN, so memory returned from any hook, `on_outcome` and `on_event` included, is refused with an error naming the field when it holds `inf`, `-inf` or `nan`. Store `None` or a finite sentinel deliberately instead. A refusal in `on_outcome` leaves that outcome undelivered, and the runtime delivers pending outcomes before any decision, so no later decision runs, teardown included, while the refusal repeats.

Memory records decisions, phases and timers. It must not duplicate authoritative balances, protocol positions, debt, transaction identities or execution progress. Those belong to connectors and the runtime.

Resume uses the captured source and its memory schema. Existing memory cannot silently become another schema, reset to defaults or migrate automatically.

## Outcomes and intermediate work

`on_outcome(ctx)` receives the retained result of the admitted economic action:

```
def on_outcome(context: OutcomeInput) -> Memory:
    if context.result.status != "completed":
        return context.memory
    return state(context).updated(...)
```

`ctx.result.status` is:

- `completed` when the connector's economic completion condition is satisfied.
- `intermediate` when an admitted action still has steps to perform.
- `failed` when the operation did not succeed.

Do not advance economic state after an approval or another intermediate step. The runtime continues intermediate work without another call to `decide()`. A crash between external completion and outcome delivery replays the retained outcome rather than the economic action.

Connector obligation helpers report `clear`, `remaining` or `unknown`. Unknown is not clear and cannot authorize more risk or shutdown without an explicit operator decision.

## Teardown

When `ctx.teardown` is true, the strategy should stop opening risk and request the appropriate exit for any known position. Teardown bypasses the ordinary decision interval but uses the same actions, authorization, durable work and outcome handling as normal execution.

Prefer the connector's full-exit action when it exists. It can own protocol cleanup that a strategy cannot safely infer from a single balance. What each connector's `exit` closes, converts and revokes, and whether it must be called again until it returns None, is listed under that connector's heading in [Strategy actions](https://sdk.docs.almanak.co/support.html#strategy-actions). If required observations are missing, preserve the obligation rather than returning a false closure claim.

## Isolation and external data

All author hooks run without network access, credentials, signing or filesystem writes. They share bounded resources and output limits. Missing or unusable OS isolation refuses execution; there is no unsandboxed fallback.

External information must be declared and delivered as typed observations through the trusted gateway or retained historical inputs. A requested venue, pool, oracle or vendor source cannot silently become another price source.

## Author checklist

- Import only shared and connector public author contracts.
- Declare capabilities from the same resolved parameters used by decisions.
- Access observations before returning an intent so their scopes are retained.
- Keep CAIP chain and asset identity intact throughout configuration and state.
- Keep economic positions and transaction progress out of strategy memory.
- Declare price history in `data_dependencies` and decide nothing on an incomplete series.
- Advance memory only on the outcome status that actually proves the transition.
- Handle teardown and missing evidence explicitly.
- Run `almanak check` after every small change, then add backtest and fork evidence in that order.

# 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 position `range_lower`/`range_upper` and `position0`/`position1` (principal at that price); `value` (wallet plus position, in currency1) and `hold` (the starting wallet at that price). Uncollected fees are not read.
- **Uniswap V4:** the same, plus uncollected `fees0`/`fees1`, counted in `value`.
- **Aerodrome:** `price`, `wallet0`/`wallet1`; with a position `shares`, `position0`/`position1` and claimable `fees0`/`fees1`; `value` and `hold` in currency1.
- **Curve:** `wallet0`..; with a position `shares` and `position0`..; `value` and `hold` in coin0 (StableSwap at the pool's peg rates, Crypto pools at the price oracle, which also read `price1`..).
- **Lending (Aave V3, SparkLend, Compound V3, Morpho Blue, Aave V4):** `wallet`, `supplied`, `collateral`, `debt` in the asset's units; `health` (with debt), `ltv` and `max_ltv`; `supply_apy`/`borrow_apy` (0.05 is 5%); `price_usd`, or `price` where the market does not price in USD (a Compound WETH market prices in WETH; a Morpho collateral leg in the loan asset); `value` and `hold` in 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 reads `gm_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 is `value` and `hold` when every part lends, and `lending_value` and `lending_hold` when 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 is `evidence="author_derived"`.
- **Metrics:** `b.metric(id, label, value)`, gathered in `b.group(id, title, ...)`.
- **Charts:** `b.line_chart(id, title, [b.series(...)], caption=...)` with `bands` (a range), `references` (a level such as health 1.0), `markers` (trades) and a `lower` panel for an indicator under a price (`lower_marks` names its levels).
- **Text:** `b.callout` for a status line (`tone="success"`, `"warning"`, `"danger"`), `ui.Text` for the scope sentence, `ui.Table` for 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_test` reports whether the dashboard drew from the fork test (`ready`, `absent`, or `unavailable` with the reason). Fix an `unavailable` dashboard before deploying.
- `almanak report --run <fork run> --dashboard --workspace .` draws the workspace's current `dashboard.py` over 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.

# Author configuration

`config.yaml` is the directory convention. `--config PATH` selects another YAML file explicitly. Backtests always require `--backtest-config PATH`. Runtime manifests, resolved facts, effective parameters and persistence remain JSON.

## Files and ownership

The SDK keeps economic choices separate from operational authority:

| Input                    | Maintained by                | Contains                                                                                              |
| ------------------------ | ---------------------------- | ----------------------------------------------------------------------------------------------------- |
| `strategy.py`            | Strategy author              | Decision hooks, capability declaration and optional typed memory                                      |
| `config.yaml`            | Strategy author              | Markets, named funds, allocations, quantities, timing and explicit financial sources                  |
| Backtest YAML            | Backtest author or evaluator | Historical window, hypothetical inventory, model, data bounds, costs and end treatment                |
| Operator configuration   | Deployment operator          | Provider bindings, account addresses, fork fixtures, payer ceilings and production signing references |
| Private environment file | Deployment operator          | Provider and vendor credentials referenced by name from operator configuration                        |

Provider URLs, keys, wallet addresses and signing files do not belong in author configuration. Conversely, the operator file cannot silently change the strategy's market, quantity, risk or financial-source choices.

See the [Uniswap V4 trading configuration](https://github.com/almanak-co/sdk/blob/main/strategies/uniswap_v4_trading/config.yaml) for a compact exchange example and the [Aave V4 configuration](https://github.com/almanak-co/sdk/blob/main/strategies/aave_v4_weth_supply/config.yaml) for multiple named funds, supply positions and debt.

## Author sections

| Section     | Meaning                                                                                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strategy`  | Decision interval, named allocations, optional debts, optional gas policy, optional [parameters](#strategy-parameters) and their optional fork-test values. |
| `markets`   | Named market selections: the connector, CAIP-2 chain and the connector's exact target (for Aave V4 the Spoke and reserve ID).                               |
| `accounts`  | Named fund accounts with their CAIP-2 chain.                                                                                                                |
| `assets`    | Optional assertions: a CAIP-19 identity mapped to expected decimals or a display symbol. An ERC-20 asset without one is named by its own `symbol()`.        |
| `inventory` | Explicit account-scoped owned cash for composed strategies, with verified asset precision.                                                                  |

The author file holds choices only. Everything a run must verify is resolved from the chain through a read-only gateway and retained beside the run: the reserve's Hub, the underlying asset and its precision, implementation identities and the anchor block. Providers, account bindings, fork fixtures and production authority live in the operator configuration described in the [operating guide](https://sdk.docs.almanak.co/operating.html#operator-configuration). All reference variants use this author root, including composed strategies with multiple connectors.

Allocations refer to local market names. Names and symbols are never on-chain asset identity. Markets and accounts must agree on chain. Multiple allocations can share a named fund, but names do not split ownership of one protocol balance. Each DEX connector (Uniswap V3 and V4, PancakeSwap V3, Aerodrome, Aerodrome Slipstream, Curve and GMX V2) takes one allocation per fund, so a composed strategy that needs a second pool on the same fund uses another DEX connector or another fund. `strategy.gas` names payer accounts, the chain, the quote source and the exact fee bounds. Production runs require it. Only `accounts`, `chain`, `max_transaction_fee` and `exit_reserve` are required, plus `total_budget` for a lifetime cap: the amounts are spending authority and never default. The fields that tune how execution bids default as below, and a check lists every default it applied under `gas_defaults`.

| Field                     | Default                                                                                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `source`                  | `network_rpc_fee_history`                                                                                  |
| `base_fee_basis`          | `next_block`; `observed_block` on Avalanche, whose fee history reports no next block                       |
| `reward_percentile`       | `50`                                                                                                       |
| `base_fee_multiplier_bps` | `20000`; `12500` on Arbitrum and Robinhood Chain, whose Nitro sequencer refunds a bid down to the base fee |
| `max_gas_limit`           | `1200000`, every action measured through Safe/Roles with the default buffer except GMX order creation      |
| `max_gas_price_wei`       | `max_transaction_fee` in wei divided by `max_gas_limit`, rounded down                                      |
| `gas_limit_buffer_bps`    | `2500`                                                                                                     |

The derived `max_gas_price_wei` is the highest price the fee ceiling admits at `max_gas_limit`, so the defaults never raise `gas_fee_ceiling`. Above it every transaction, exits included, waits for gas to fall; nothing is lost. It is also the strictest price that fee admits: a 200k-gas transaction could afford six times that price within the same fee, but waits. On L2s the market price sits far below it; on Ethereum, state `max_gas_price_wei` and a `max_gas_limit` near your largest action if smaller transactions should not wait. Set any of these fields to override its default. Fork runs with an authored gas policy require matching operator ceilings under `fork.gas`; the policy is retained and enforced against the generated fixture payers. Production runs on Base and Optimism support authored gas policies: the gateway bounds the L1 data and operator fees at quote time inside the same payer ceilings, and reconciles the exact charges from each receipt. Only fork runs on Base and Optimism refuse an authored gas policy, because the fork fixtures have no verified bound for those fees. Gasless forks on those chains remain supported.

Gas amounts are chain-specific: native gas prices differ by orders of magnitude between chains, so never reuse another chain's reference values, and size even a same-chain reference's amounts for the market you deploy into. Size `max_transaction_fee` to pay for `max_gas_limit` at a few times the highest bid of the last 100 blocks (five times leaves room for spikes), plus on Base and Optimism the L1 data and operator fee; the defaulted price ceiling follows from it. The payer's entry minimum is entry transactions x `max_transaction_fee` + `exit_reserve`, so a fee far above what a transaction costs inflates the funding requirement by the same factor. `check --online` reports a ceiling below the current quote under `gas_price` or `gas_fee_quote`, and a `max_transaction_fee` more than 20 times the executable fee (`max_gas_limit` x the highest bid of the last 100 blocks, plus the L1 data and operator fee bound on Base and Optimism) under `gas_ceiling_oversized`, with a suggested value. A stated `max_gas_price_wei` must keep `max_gas_limit` x `max_gas_price_wei` \<= `max_transaction_fee`. On Base and Optimism leave room in `max_transaction_fee` for the L1 data fee as well. When the live quote rises above `max_gas_price_wei`, or a transaction's quoted fee above `max_transaction_fee`, every transaction, exits included, waits until gas falls; nothing is lost, but nothing moves either. The Uniswap V3 LP references for Base, Arbitrum and Optimism carry L2 gas policies; the Optimism one is sized from October 2026 quotes, and Base and Arbitrum keep earlier values, so run `check --online` before reusing any of them. The Avalanche and multichain Aave V4 and Uniswap V4 references keep generous legacy values that their fixtures pin; never size a policy from them (`check --online` can warn `gas_ceiling_oversized` on their Ethereum and Avalanche policies). Most other published references carry an example policy sized from October 2026 quotes at the default gas limit (2.3M on GMX V2), with an `exit_reserve` for the exit transactions `check --production` counts; these are starting points, so run `check --online` before deploying a copy.

### Strategy parameters

`strategy.parameters` holds author knobs the strategy code reads, such as a loop count or a target ratio:

```
strategy:
  parameters:
    loops: 3
    target_ltv: '0.65'
    rebalance_hours: [8, 20]
```

Keys match `^[a-z][a-z0-9_]*$`. A value is a scalar (integer, decimal, boolean, string or null) or a flat list of scalars. Parameters are never resolved, priced or part of capabilities, and authorize nothing. `almanak check` validates only their shape; the strategy validates the values in `validate_config`, or declares a typed model the SDK validates them against before any hook runs (see [typed parameters](https://sdk.docs.almanak.co/authoring.html#typed-parameters)). An omitted section is `{}` and leaves every digest unchanged.

A fork lifecycle can run the same code with other values. `strategy.test_parameters` declares them next to the production values:

```
strategy:
  parameters:
    loops: 3
    target_ltv: '0.65'
  test_parameters:
    loops: 1
```

Each key must be declared in `strategy.parameters`, and its value takes the declared type (`'1'` for an integer knob becomes `1`). When strategy.py declares a [typed parameter model](https://sdk.docs.almanak.co/authoring.html#typed-parameters), a key may instead be any field of that model, including one `strategy.parameters` leaves to the model's default; the value stays as written and the model's field type converts and checks it. Declare the fewest-decision values that still exercise the strategy's paths: one loop round instead of three still supplies, borrows, swaps, resupplies and unwinds. Don't add a value the fork can't use: fork prices don't move, so a closer price stop still never fires, and an interval shorter than the fork's advance adds decisions without testing anything new. `almanak check` validates the shape and the keys. Only `almanak test run` applies the section: strategy code never sees it in production or in backtests, and it leaves every digest unchanged.

`almanak test run` applies `test_parameters` by default, in the fork workspace's copy of the inputs, never in `config.yaml`. `--param loops=2` overrides one key on top of them, and `--production-parameters` runs with the production values plus any `--param`. The verdict records the applied values under `parameter_overrides` and where each came from (`test_parameters` or `--param`) under `parameter_sources`, so it says what ran. An undeclared key is refused: one config.yaml doesn't list, or, with a typed model, one the model doesn't declare. Either way only the fork copy changes, so `config.yaml` and its digest stay as written.

## Exact values

```
assets:
  eip155:1/erc20:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2:
    decimals: 18
    symbol: WETH
```

A token quantity is a quoted decimal such as `'0.01'`, or an explicit `{base_units: 10000000000000000}`. An asset amount contains `asset` and `quantity`. Conversion rejects excess precision. Trusted live or historical metadata must match declared decimals before economic activity. Display symbols cannot alter identity or precision. Resolution reads each ERC-20 market asset's `symbol()` at the anchor block and uses it where the author declares none; a token that answers no short plain symbol stays unnamed. The symbol is contract-controlled text for display: it is kept outside the verified facts and never selects an asset. Financial and strategy parameters do not expand environment variables.

Durations accept whole seconds or strings such as `5s`, `10m`, `2h`, `1d`. Dates use whole-second ISO timestamps with a timezone, such as `'2026-08-20T00:00:00Z'`. YAML rejects duplicate keys, aliases, merges, executable tags, non-finite numbers and unknown schema fields.

Trading allocations select `kind: trading` and omit LP range and collection time. LP allocations select `kind: liquidity` with an explicit tick range or band. `collect_seconds` is optional on a Uniswap V4 liquidity allocation: only strategy code reads it, to time its own fee collection, and connectors never collect on a schedule. Omit it when the strategy collects no fees before exit; closing the position takes the fees it accrued. Native new-risk balance thresholds are allocation policy; payer budgets are shared execution policy and remain distinct.

### Liquidity bands

Uniswap V3, PancakeSwap V3, Aerodrome Slipstream and Uniswap V4 LP allocations declare either a fixed `range` or a `band`:

```
band:
  width_bps: 500
```

Each position opens centred on the observed tick, either `width` ticks wide or, with `width_bps`, that many basis points either side of the current price rounded up to the tick spacing. A strategy recentres by closing the position, swapping with `swap(name, rebalance=True)` and opening again, at any price level. The gateway admits only an aligned range of exactly the band width centred on the tick the decision observed.

An author who wants to provide liquidity only within a price range adds an optional envelope, `lower` and `upper` ticks in multiples of the pool tick spacing:

```
band:
  lower: -200120
  upper: -196120
  width_bps: 500
```

Positions are then clamped inside the envelope, and the gateway refuses any mint outside it before submission. Once the price leaves the envelope, positions open at its nearest edge and stay out of range until the price returns or the envelope changes; whether the strategy holds or reopens them is its own rule. The envelope is a gateway price policy, not on-chain authority, so changing it needs no new permissions. Economic backtests of a band select `liquidity_model: bracketed_historical_fee_growth` and a tick coverage that contains the whole envelope, or for a band without one, the price range the run should measure; see [Backtesting](https://sdk.docs.almanak.co/backtesting.html#liquidity-bands).

## Validation and normalization

```
uv run --locked almanak check strategies/aave_v4_weth_supply --normalized --json
uv run --locked almanak check strategies/aave_v4_weth_supply --fork \
  --gateway-config examples/operators/aave_v4_weth_supply.fork.yaml --env-file .env
uv run --locked almanak check strategies/aave_v4_weth_supply --fork --online \
  --gateway-config examples/operators/aave_v4_weth_supply.fork.yaml --env-file .env
uv run --locked almanak support --json
```

Offline checking parses the author file with source locations, checks every market, account and collateral reference, refuses a second allocation of one DEX connector on a fund and a composed `inventory` missing an asset the file names (a lending market's `asset` or an exchange `capital` asset; pool coins are checked online), and confirms the strategy sandbox. It makes no provider calls and runs no strategy hooks, `validate_config` included, because the hooks need the resolved parameters. `--production` also refuses a file without `strategy.gas` in the configuration stage, since production runs require it. A check without `--fork`, `--production` or `--backtest-config` cannot refuse that rule, so its result carries a `strategy.gas` entry under `warnings` instead. With `--gateway-config` and `--fork` or `--production` it also binds the authored choices to the operator file. `--online` starts a read-only gateway with the operator's providers, resolves the named markets, runs validation, capability and dependency hooks once on the resolved parameters, and adds the mode stage: the compiled fork fixture, or production gas-policy compatibility with the operator ceilings. `--backtest-config` checks the backtest inputs offline and, online, the historical model. `--normalized` adds the parsed inputs and, online, the resolved parameters. Every check whose file omits a defaulted gas field reports a `gas_defaults` list of `{field, value}` entries with the values the SDK supplied. Online `--fork` and `--production` checks also report a `funding` block: for each bound account, the CAIP-19 assets and base units it must hold before its first entry, and for each gas payer, the native entry minimum `max(entry_transactions x max_transaction_fee + exit_reserve, largest minimum_native_balance)` with any gas policy amounts. `entry_transactions` is an upper bound on the transactions entering every allocation the payer pays for submits, through the last one that adds exposure (an Aave supply-and-borrow allocation pair counts its approvals, supplies, collateral enablement and borrow; a trade or liquidity allocation with `entry_swap_bps` below 10000 counts each entry slice of its `capital` as a swap with its approvals), because each pays its fee and each that adds exposure must leave `exit_reserve` unspent. In a composed strategy a trade is also offered slices of shared or borrowed funding inventory beyond its `capital`; those swaps are not counted and draw on what entry leaves. It states requirements only; compare it with your own balances. For Aave V3, SparkLend, Compound V3, Morpho Blue and Aave V4 supply allocations the token amount is each allocation's `capital`, in the market asset: what the allocation starts with. Production admission, the fork fixture funding and the economic backtest inventory check use the same amount. Composed strategies state their `inventory`. Interest on borrowing is not prefunded: repaying the fund's own debt takes no configured amount, and a repayment the fund cannot cover refuses with the shortfall, which a transfer to the fund resolves without a restart.

Allocations state no spending budgets. Generated Safe permissions pin contracts, functions, assets, markets, pools and the Safe as recipient or beneficiary, with no amount limits ([scope-only authority](https://sdk.docs.almanak.co/internal/blueprints/27-scope-only-authority.html)). A lending allocation sets `borrow: true` to borrow and repay; the amounts it supplies and borrows are the strategy's decisions. On Aave V3, SparkLend, Morpho and Compound V3, `minimum_health_factor` is required when `borrow: true`, and every allocation sharing that fund and Pool, market or Comet states the same value. A lend- or supply-only allocation in a scope where none borrows may omit it: there is no debt for it to bound, and while the fund carries any debt there an action the connector risk-checks is refused. Aave V4 already asks for it only on debts. The author writes the ratio (`'2'`); strategy code reads it as a wad integer (`2 * 10**18`), see [Health factors are wad integers](https://sdk.docs.almanak.co/authoring.html#health-factors-are-wad-integers). An exchange allocation is offered an entry quote whenever it holds funding inventory, one `capital x entry_swap_bps / 10000` slice at a time; a strategy that should enter once checks `entry_complete`. On the Uniswap V3, PancakeSwap V3 and Aerodrome Slipstream pools the slice is the swap, and a liquidity mint offers all inventory held, so 5000 swaps about half and invests about all. On Uniswap V4 the slice sizes the whole entry quote: a trading entry swaps it, while a liquidity entry quote swaps only the part of it the mint pairs with and is offered only while no position is held. The mint still uses all inventory held, so with the usual single entry swap then `open_liquidity` a value below 10000 (the V4 default) leaves about the rest of capital idle; a banded allocation that recentres through `rebalance_quote`, or a strategy that swaps repeatedly before minting, is not limited this way. `entry_swap_bps: 0` offers no entry quote, for a liquidity allocation that deposits without an entry swap (a single-sided Curve deposit, or a band that enters only through `rebalance_quote`, which sizes itself from the centred position and ignores `entry_swap_bps`); a trading allocation still requires at least 1. `hold_seconds` is optional on every allocation that offers it: only strategy code reads it, for its own timed exit, and connectors never act on it. Omit it to hold until teardown. `strategy.gas.total_budget` is optional; without it there is no lifetime gas cap, while `max_transaction_fee` and `exit_reserve` still apply. If the operator policy or fixture stage is refused, the error output still carries it. Backtest checks omit it. Online admission of funding and installed authority is never checked here; each result identifies passed, failed or not-checked stages.

Every check also reports findings under `warnings`, each with `code`, `field` and `message`. They are advisory:

| Code                    | Finding                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gas_limit`             | A `strategy.gas` `max_gas_limit` is below the largest measured gas of any action the policy's connectors plan through Safe/Roles, raised by its own `gas_limit_buffer_bps`. The measurements ship as reviewed fork receipts with their provenance.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `gas_amount_units`      | A gas fee, budget or reserve exceeds 10 whole native units (10,000 POL on Polygon, whose native coin is cheap), which usually means base units were written as a quoted decimal.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `gas_fee_ceiling`       | A `max_transaction_fee` is below `max_gas_limit` x `max_gas_price_wei`, so the fee ceiling refuses transactions both other ceilings admit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `gas_budget`            | A `total_budget`, when set, is below one `max_transaction_fee` per entry transaction plus `exit_reserve`. Execution keeps `exit_reserve` unspent while it enters, so at the fee ceiling the deployment stops partway through entry. The entry count is a lower bound per allocation the policy pays for: 2 for a V3 pool trade (approve, swap), 4 for a V3 pool liquidity allocation (approve, swap, approve, mint), 1 for anything else.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `gas_exit_reserve`      | An `exit_reserve` is below one `max_transaction_fee` per exit transaction: each allocation's `gas_lifecycle` exit from `almanak support` (V3-style pools: trade 2 (approve, swap), liquidity 5 (close, approve, swap, and a revoke for each of the two position-manager allowances the mint can leave); Uniswap V4: trade 4 (approve, Permit2 approve, swap, Permit2 revoke), liquidity 5 (burn first); Aerodrome: trade 2, liquidity 5 (approve shares, remove, approve, swap, revoke); Curve: trade 2, liquidity 3 (remove, approve, swap); Aave V3, SparkLend, Compound V3 and Morpho Blue: a supply 1 (withdraw), a `borrow` allocation 3 (approve, repay, revoke); Aave V4: a supply 1, a debt 3 plus 1 per collateral it disables; GMX: a position 3 (approve the order fee, create the close order, claim accrued funding)), and 1 for anything else. These count one repayment; a debt unwound in several cycles (a leverage loop) needs more, see `exit_per_unwind_cycle` below, which the configuration cannot state, so the finding is a floor and its message names the cycle term when the policy pays a borrow.                                                                                       |
| `gas_price`             | Online only: a `max_gas_price_wei` is below the gas price execution would quote now with the policy's own `reward_percentile` and `base_fee_multiplier_bps`, so every transaction is refused until the market falls. A failed quote is reported under this code too.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `gas_fee_quote`         | Online only: at the current quote, `max_transaction_fee` pays for less gas than `max_gas_limit`, so a larger transaction waits until gas falls.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `gas_ceiling_oversized` | Online only: `max_transaction_fee` is more than 20 times the executable fee, `max_gas_limit` x W plus, on Base and Optimism, the SDK's L1 data and operator fee bound L1 for a transaction of `max_gas_limit` gas and 2000 calldata bytes. W is the highest bid over the last 100 blocks ending at the checked block: per block, the base fee (the next block's, or the block's own for an `observed_block` basis) x `base_fee_multiplier_bps` / 10000, rounded up, plus the `reward_percentile` tip, and on BNB at least `eth_gasPrice`. The payer's entry minimum is entry transactions x `max_transaction_fee` + `exit_reserve`, so the oversized fee inflates the funding requirement by the stated factor. The message suggests `max_transaction_fee` = `max_gas_limit` x W x 5 + 5 x L1, rounded up to two significant digits: a spike beyond 5x makes transactions, exits included, wait, and nothing is lost. `exit_reserve` is then exit transactions x that fee; an omitted `max_gas_price_wei` follows the fee, a stated one must come down with it. When the node refuses the 100-block window the finding uses the single-block quote and says so. Never reported with `gas_price` or `gas_fee_quote`. |
| `market_read`           | Online only: an unexpected failure while reading the current market for the gas price findings. The findings made before it are kept; it never fails a check.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `gas_entry_slices`      | An allocation's `entry_swap_bps` splits capital into more than 10 entry slices (`ceil(10000 / entry_swap_bps)`, below 1000 bps). Each slice is its own swap with its approvals, so the payer's entry minimum includes at least that many `max_transaction_fee`s for the allocation; the message states the amount.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `entry_budget_partial`  | A Uniswap V4 liquidity allocation without a band sets `entry_swap_bps` below 10000. Its entry quote is sized from that share of capital, so with the usual single entry swap then `open_liquidity` about `capital - capital x entry_swap_bps / 10000` (rounded down) is not swapped into the pair and stays idle; a strategy that swaps repeatedly before minting is not limited this way. Banded allocations are not flagged: they recentre through `rebalance_quote` and mint all inventory.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `production_gas`        | A check without a mode names the production requirement for `strategy.gas`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `decision_interval`     | Production and mode-agnostic checks: `strategy.decision_interval` is shorter than 30 seconds. Every live decision starts a fresh sandboxed worker, whose startup alone took 8 to 11 seconds on a CPU-limited hosted container, so a shorter interval can starve a hosted deployment of CPU until its evaluations time out. Fork and backtest checks do not report it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

## Support

`almanak support` reads the same immutable connector/chain declarations used at admission. These distinguish execution modes, economic intents, account profiles and historical starting bounds. A supported combination does not imply that the selected provider is currently available or that every market has history.

Filter declarations with exact public connector and CAIP-2 chain identities:

```
uv run --locked almanak support --connector uniswap_v3 --chain eip155:8453 --json
```

Every chain row also carries the connector's `gas_lifecycle`: for each allocation kind it trades, pools, lends, borrows or holds (`trade`, `liquidity`, `lending`, `debt`, `position`, `swap`), the transactions one entry and one exit submit. `entry` is the count for a single entry slice; `per_entry_slice` is added for each further `entry_swap_bps` slice of `capital`, `per_item` for each counted item (Curve pool `coins`, Aave V4 debt `collateral`, and GMX deposit `input_assets`: the distinct long, short and wrapped-native assets of its market) and `when_granted` for each named permission the allocation enables (`collateral`, `borrow`). `exit` is the transactions one exit submits: closing the position, the swap back with its approvals, repaying a debt (approve the held balance, repay, revoke what the repayment leaves) and revoking the allowance left behind; `exit_per_item` and `exit_when_granted` add to it the same way (an Aave V4 debt disables each `collateral`; a lending allocation granted `borrow` repays: 1 + 2). `almanak check` sizes `exit_reserve` with these. Summed over the allocations a payer pays for, the entries are the `entry_transactions` the `funding` block of `almanak check --json` reports, read from the same connector tables, so gas can be sized before a configuration exists:

```
{"trade": {"entry": 2, "exit": 2, "per_entry_slice": 2, "per_item": {}, "when_granted": {},
           "exit_per_item": {}, "exit_when_granted": {}, "exit_per_unwind_cycle": 0},
 "liquidity": {"entry": 5, "exit": 5, "per_entry_slice": 2, "per_item": {}, "when_granted": {},
               "exit_per_item": {}, "exit_when_granted": {}, "exit_per_unwind_cycle": 0}}
```

The exits count one repayment: the trading allocation swaps what it holds back to the debt asset, the debt is repaid and the collateral withdrawn. A strategy that resupplies what it bought as collateral (a leverage loop: supply, borrow, swap, supply, repeated) holds nothing to swap until it withdraws, and while a minimum health factor refuses withdrawing everything at once it unwinds in cycles: withdraw part of the collateral, swap it, repay part of the debt. `exit_per_unwind_cycle` is what one such cycle submits on the lending connector (Aave V3, SparkLend, Compound V3, Morpho Blue: a partial withdrawal, the repayment's approval and the partial repayment, 3; Aave V4 plans no partial steps, 0). A loop unwound in C cycles, the last ending in the full repayment, exits in the summed exits + 1 (the first cycle's withdrawal) + (C - 1) x (`exit_per_unwind_cycle` + the swapping allocation's `exit`). On Aave V3 with a V3 pool trade: 1 + 3 + 2 = 6 for one repayment, 7 for C = 1, 12 for C = 2. C is set by the strategy's unwind and its parameters (entry rounds, borrow fraction, health-factor floor), not by the configuration: count the partial withdrawals its unwind submits at production parameters (a fork run's `teardown_sequence` lists them for the parameters it ran; more rounds or a higher floor take more cycles, and the rig saw two cycles after one round). `almanak check` counts no cycle, so add the cycles to `exit_reserve` yourself.

An empty result means there is no declared support matching the filters. Unknown connector IDs are errors with supported candidates and a spelling suggestion when available. Suggestions never change the selected protocol. Configuration errors identify the author file, connector field and line, including named markets in composed strategies.

The [reference directories](https://github.com/almanak-co/sdk/tree/main/strategies) contain explicit market examples. Their choices are author inputs, not connector defaults. `almanak support --references` lists the copies the installed SDK carries, with the same `--connector` and `--chain` filters; see [Example strategies](https://sdk.docs.almanak.co/examples.html). Editor schemas live in `docs/schemas/`; `check --normalized` shows the resolved configuration used by the SDK. `almanak support --schema strategy` (or `backtest`) prints the bundled schema set of the installed version as one JSON document, `root` naming the entry schema and `files` holding every referenced schema by file name. `--schema dashboard` prints the schema of the dashboard response the evaluator emits, for hosts that validate and render retained dashboards.

# Backtesting

Backtests execute economic intents in the shared simulator, with durable hypothetical inventory and connector-owned positions. They do not use a Foundry fork. Strategy hooks retain sandbox isolation.

The required backtest YAML contains `mode`, `window`, `inventory`, `execution`, `costs` and, for economic models, the connector `data` policy. Named funds use the author's account names and explicit asset balances; their simulation addresses are deterministic artifacts and cannot grant production authority. A modeled payer and its inventory are required when the selected cost model needs them. Historical provider bindings come from the operator configuration, one per chain. A backtest that requests vendor data names the vendor and kind in its data policy, while the operator binds that exact name and kind to an access tier and credential environment variable. Author and backtest files never contain access settings.

A strategy's [price series](https://sdk.docs.almanak.co/authoring.html#price-series-and-technical-analysis) come from its `data_dependencies` hook, never from the backtest file. Resolution runs the hook and adds each declared series to the data policy. A backtest file that lists a series is refused; only an Aave V4 economic backtest file may list a single strategy price, which must equal the hook's. Preparation reads each series from the pool's state through the operator's archive provider for the series' chain, from `window` boundaries before the window start, so the first decision already has a full window. Its historical gateway resolves the declared pool with the connector's resolution and admits reads of that pool only. The dataset keeps compact evidence: the resolved pool, each boundary's block, its successor and the raw state reads, and for swap candles each log window's trade events with a digest of the provider's answer; opening a dataset rebuilds every point from it. The series and their warm-up are part of the dataset identity, and a strategy that declares no series prepares exactly as before.

## Run and reproduce

```
uv run --locked almanak backtest prepare strategies/aave_v4_weth_supply \
  --backtest-config strategies/aave_v4_weth_supply/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --env-file .env --output datasets/aave
uv run --locked almanak backtest run strategies/aave_v4_weth_supply \
  --backtest-config strategies/aave_v4_weth_supply/backtest-economic.yaml \
  --gateway-config examples/operators/backtest/eip155-1.yaml \
  --dataset datasets/aave --output runs/aave
uv run --locked almanak backtest resume --run runs/aave
uv run --locked almanak backtest reproduce --run runs/aave
```

Preparation and a fresh run first resolve the named markets at the last block at or before the window start through a read-only gateway, retain the verified facts as `resolution.json`, and construct the parameters from them. A dataset records the facts digest it was prepared with; a run with `--dataset` reuses that retained resolution and needs no provider credentials. Without `--dataset`, economic runs prepare or reuse a matching dataset before execution. Preparation uses the operator's archive providers and the explicitly requested vendors through the read-only gateway. Execution makes no data network calls. Missing operator bindings, samples, tick coverage or price sources cannot trigger online quotes, select another vendor or substitute models. Dataset identity includes effective settings, the resolved facts and input evidence. Every fresh run requires the selected operator file, including when a retained dataset supplies the resolved facts without provider access. Every shipped reference uses the public author and backtest inputs. Archived legacy YAML is read only by private component and baseline tooling; the runtime consumes retained JSON and has no legacy author fallback.

A `resolution.json` left in the output or dataset directory by an earlier or interrupted preparation with other inputs is refused, and the error names the file: prepare into a fresh output directory, or remove that directory and prepare again.

Reproduction requires the captured implementation, frozen settings and the retained resolution whose facts digest the manifest names; it replays the retained reads offline and refuses missing or changed evidence. Resume uses retained state, pending actions and outcome delivery. Author edits cannot change that run. Do not replace datasets or silently change sampling on resume.

## Models and assumptions

- `historical_path` retains detailed operation and account-profile assumptions. Uniswap uses historical Quoter calls; Aave detailed lending currently supports Ethereum supply/withdraw only. Remove optional debts from the author configuration when selecting that Aave model.
- `economic` uses prepared Aave lending samples, including borrowing, collateral, repayment and liquidation, or bounded integer Uniswap pool state with price impact and tick crossings. LP fees use explicit fixed historical fee growth.

Hypothetical trades and liquidity do not change later external market history. LP fees exclude hypothetical dilution and fees from the strategy's own trades. Explicit vendor prices may approximate economic risk but never replace a requested oracle or venue observation. Missing measurements and model exclusions remain reported. No model here establishes production execution authority.

The strategy interval is inherited by default. A coarser override is explicit, fixed and reported as reduced cadence fidelity. Unchanged observations do not skip decisions. Keep sampling interval, maximum source age, collection limits, tick coverage, latency, costs and end treatment explicit. Collection-limit failures require an author decision; the SDK does not coarsen resolution automatically.

Progress appears on stderr; redirected stdout remains JSON. `--progress json` produces versioned snapshots, also retained beside the run directory. ETA is a heuristic from comparable timings and current throughput; unsupported estimates remain absent. Observer failures do not affect financial state.

The [historical integration evidence](https://sdk.docs.almanak.co/internal/reports/2026-09-06-backtest-integration.html) separates contract comparisons, sampled-path fidelity and measured preparation and execution timings. Component replay and fork execution are different evidence.

### Liquidity bands

Uniswap V3, PancakeSwap V3, Aerodrome Slipstream and Uniswap V4 `band` allocations run in `economic` mode only, with `liquidity_model: bracketed_historical_fee_growth`. That value requires a band allocation; fixed ranges in the same run keep exact historical range growth. Each allocation's `data.coverage` must contain the whole band envelope. A band without an envelope follows the price, so its coverage is the explicit tick range the run measures: it must hold at least one aligned band width and the position centred on the first sample's tick, and a full-range coverage measures every position the strategy can open. A run that would open a position outside the coverage stops with an error instead of crediting unmeasured fees. Preparation samples every initialized tick of the coverage with its fee growth plus every liquidity event of the pool, within the usual sampling, age and collection limits.

A position the strategy opens can sit on ticks that no historical position initialized, so its fee growth is not directly observable. Between two samples, the SDK measures the growth accrued below every tick that stayed initialized for the whole interval and brackets each position boundary between its neighbours. The lower bracket is credited; it equals the exact growth when both boundaries are continuously initialized ticks, which is common on pools with dense liquidity. The report labels band positions `fee_attribution: bracketed_lower_bound` and retains `uncollected_fee_upper_bound` and `collected_fee_upper_bound_excess`, the fees the sampled state cannot rule out. On sparse pools the credited fees can be far below the upper bound; nothing is interpolated between samples. Fees accrue between the latest samples at or before each liquidity action. Rebalance swaps use the same prepared integer pool calculator as other swaps. Recentring is bounded by the allocation's inventory, as in live execution. `historical_path` backtests and composed strategies refuse band allocations.

### Results in the funding asset

An economic Uniswap V3, PancakeSwap V3 or Aerodrome Slipstream run whose allocations share one funding asset is valued in that asset. The other pool asset is priced by the prepared pool sample in force at each mark, so the valuation reads nothing outside the run's own dataset. The report then carries:

| Field                                             | Meaning                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `valuation`, `valuation_quote`, `valuation_asset` | `pool_price`, the funding asset's symbol (or its CAIP-19 identity when it has none) and its identity.                                                                                                                                                                                                                                                              |
| `equity_curve`                                    | Wallet balances of the pool assets plus each open position's principal and credited pending fees, at every action and on an even grid of at most 480 marks.                                                                                                                                                                                                        |
| `price_series`                                    | The other pool asset's price in the funding asset at the same marks.                                                                                                                                                                                                                                                                                               |
| `metrics`                                         | `total_return_pct`, `max_drawdown_pct` and `sharpe_ratio` (daily marks, annualized; absent below three days).                                                                                                                                                                                                                                                      |
| `net_quote_return`                                | The net result and return in the funding asset.                                                                                                                                                                                                                                                                                                                    |
| `performance`                                     | What produced the result: `fees_value` (fees at the pool price they were collected at), `swap_cost_value` and `swap_volume_value` (each swap against the pool price it executed on), `price_effect_value` (the rest: the price change of the inventory held, impermanent loss included), time in market and in range, holding benchmarks and one row per position. |
| `valuation_excludes`                              | Assets with a balance that the pools do not price, such as the gas asset. They stay in asset units.                                                                                                                                                                                                                                                                |

`fees_value - swap_cost_value + price_effect_value` equals the net result. Gas is not converted: an `excluded` cost policy leaves it out and a `fixed_native` policy reports it in the native asset under `gas_payers`. Allocations with different funding assets keep the asset-unit report, as do Uniswap V4, Curve and Aerodrome runs.

`boundary_evidence` names each pool's continuity proof by its dataset digest with the block interval and the number of liquidity events and inputs it covers. The proof itself stays in the prepared dataset.

### Recent windows

`almanak backtest recent` rewrites an economic backtest file so that it covers the latest `--days` days (30 by default) and charges measured gas. The hosted platform uses it to show a current result next to each example strategy.

```
uv run --locked almanak backtest recent strategies/uniswap_v3_base_trading \
  --backtest-config strategies/uniswap_v3_base_trading/backtest-economic.yaml \
  --env-file .env --output runs/uniswap_v3_base_trading.recent.yaml
```

The window ends on the hour at least two hours before every chain's finalized head, or at an explicit `--end`. Everything sized for the authored window is sized again:

| Setting                                       | Recent window                                                                                                                                                                                                                                                 |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `execution.decision_interval`                 | An hour, when the strategy decides more often; the run reports the reduced cadence.                                                                                                                                                                           |
| Pool `sample_seconds`, `max_age_seconds`      | Hourly sampling with the authored age margin.                                                                                                                                                                                                                 |
| `max_samples` and event or log-request limits | Grown with the window.                                                                                                                                                                                                                                        |
| Tick `coverage`                               | The authored bounds widened to 4,000 ticks either side of the pool tick at the window start.                                                                                                                                                                  |
| `costs`                                       | `fixed_native`: each reachable operation's measured gas (`connectors/gas_references.json`) at the window's median base fee plus the median tip before its end, paid by a hypothetical payer per chain. Operations a swap-only allocation cannot reach cost 0. |

Every read is pinned to a block of the window, so the same `--end` writes the same file. The command prints what it changed, including each chain's gas price and the pool ticks it used. It refuses, with `{"error": "recent_unsupported", "reason": ...}`, an example whose connector reads every block of the window (`per_block_history`), whose run is not valued in its funding asset (`unvalued`) or whose reachable operations have no measured gas (`unmeasured_gas`). Gas is execution gas: rollup L1 data fees are not part of a measured receipt and are not modeled.

# Example strategies

The repository contains executable references used for development and qualification. Only the allowlisted references are published: `almanak support --references` lists them and the wheel ships them; other strategy directories are test fixtures and QA proofs. The existence of a directory does not establish SDK support. Always confirm the connector, chain, action and run mode in [Supported connectors](https://sdk.docs.almanak.co/support.html) or with `almanak support`.

This page is a curated starting point, not a catalog of every strategy directory. An installed SDK also carries a read-only copy of every published reference's author files, so the full catalog is available without a source checkout:

```
almanak support --references --connector uniswap_v3 --chain eip155:4663 --json
```

Each entry names the reference, its connector and CAIP-2 chain markets, the installed directory and the files in it: `strategy.py`, `config.yaml`, the backtest YAML files and the README where present, plus the README's title and one-line summary of what the reference teaches. Without filters the command lists every published reference. Copy a reference into your own directory before changing it; the listing never loads or runs a strategy and does not establish support.

## Recommended starting points

| Example                                                                                                                            | Chain           | Concepts demonstrated                                                                                                                                                                              | Relative complexity |
| ---------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| [Uniswap V4 trading](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v4_arbitrum_trading)                           | Arbitrum        | Exact-input entry, timed hold, complete exit, typed memory and intermediate approvals                                                                                                              | Basic               |
| [Uniswap V4 full-range LP](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v4_arbitrum_lp)                          | Arbitrum        | Acquire both currencies, open an NFT position, collect, close, convert residuals and revoke authority                                                                                              | Intermediate        |
| [Aave V4 WETH supply and borrowing](https://github.com/almanak-co/sdk/tree/main/strategies/aave_v4_weth_supply)                    | Ethereum        | Multiple named funds and positions, collateral, debt, repayment and independent withdrawal                                                                                                         | Intermediate        |
| [Aave V4 Avalanche borrowing](https://github.com/almanak-co/sdk/tree/main/strategies/aave_v4_avalanche_borrowing)                  | Avalanche       | Collateralized borrowing, native gas scope, repayment and collateral release                                                                                                                       | Intermediate        |
| [Uniswap V3 Robinhood banded LP](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v3_robinhood_lp_band)              | Robinhood Chain | Recentring one position inside an owner-approved tick band; waiting while the price is outside it                                                                                                  | Advanced            |
| [Uniswap V3 Base RSI](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v3_base_rsi)                                  | Base            | Declared price series, RSI with TA-Lib, repeating crossing detection in memory, holding on an incomplete series                                                                                    | Intermediate        |
| [Uniswap V3 Base RSI-gated LP](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v3_base_rsi_lp)                      | Base            | An indicator gating a banded LP position: open while RSI ranges, close when it trends                                                                                                              | Intermediate        |
| [Aave V3 Base SMA-gated borrowing](https://github.com/almanak-co/sdk/tree/main/strategies/aave_v3_base_sma_borrow)                 | Base            | A lending strategy reading a pool series: borrow above a moving average, repay below it with a margin                                                                                              | Intermediate        |
| [Uniswap V3 Base ATR breakout](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v3_base_atr_breakout)                | Base            | Full OHLCV candles (`PoolSwapCandles`): channel midline entry, ATR trailing stop derived from the series                                                                                           | Intermediate        |
| [Aave V3 borrow and Uniswap V3 trade](https://github.com/almanak-co/sdk/tree/main/strategies/arbitrum_aave_borrow_and_trade)       | Arbitrum        | Composed borrow-and-trade in one fund: supply, borrow, swap part, reverse, repay, withdraw, with an unsupplied loan-asset reserve for accrued interest                                             | Intermediate        |
| [Morpho Blue borrow and Uniswap V3 trade](https://github.com/almanak-co/sdk/tree/main/strategies/arbitrum_morpho_borrow_and_trade) | Arbitrum        | The same composed shape on one explicit Morpho market, with a USDC reserve in inventory for the full repay                                                                                         | Intermediate        |
| [Aave V4 borrow and Uniswap V4 trade](https://github.com/almanak-co/sdk/tree/main/strategies/avalanche_aave_v4_borrow_and_trade)   | Avalanche       | Composed Aave V4 (full withdraw and full repay only) with a hookless V4 swap; the unsupplied loan-asset buffer is how a V4 strategy covers interest and buy-backs                                  | Intermediate        |
| [Uniswap V3 trading](https://github.com/almanak-co/sdk/tree/main/strategies/uniswap_v3_arbitrum_trading)                           | Arbitrum        | One Uniswap V3 exact-input buy, a timed hold measured from the fill, then the exit and revoke                                                                                                      | Basic               |
| [Aave V3 leverage loop](https://github.com/almanak-co/sdk/tree/main/strategies/arbitrum_aave_leverage_loop)                        | Arbitrum        | Composed lending and swap: knobs in `strategy.parameters`, steps derived from owned inventory, trade capital as a slice size, a repeated withdraw-swap-repay unwind with a trigger above the floor | Advanced            |
| [Morpho Blue leverage loop](https://github.com/almanak-co/sdk/tree/main/strategies/base_morpho_leverage_loop)                      | Base            | The same loop on one Morpho market, with debt and health computed from the market's shares and oracle                                                                                              | Advanced            |
| [Aave V3 borrow, then Uniswap V3 liquidity](https://github.com/almanak-co/sdk/tree/main/strategies/arbitrum_aave_borrow_lp)        | Arbitrum        | A band position funded in the borrowed asset so its exit yields the repay asset; a loan-asset reserve (or, optionally, collateral) covers a shortfall                                              | Advanced            |
| [Aave V3 lending short](https://github.com/almanak-co/sdk/tree/main/strategies/arbitrum_aave_lending_short)                        | Arbitrum        | Borrow and sell the loan asset; buy back and repay at an oracle-price stop or take-profit                                                                                                          | Advanced            |
| [Aave V3 health-factor-managed borrow](https://github.com/almanak-co/sdk/tree/main/strategies/aave_v3_arbitrum_managed_borrow)     | Arbitrum        | Repay or borrow to a target health factor inside a band whose repay trigger sits above the gateway floor                                                                                           | Intermediate        |
| [GMX V2 price stop](https://github.com/almanak-co/sdk/tree/main/strategies/gmx_v2_arbitrum_price_stop)                             | Arbitrum        | One perpetual position closed at a stop-loss or take-profit from one-minute pool closes                                                                                                            | Intermediate        |

Start with the Uniswap trading reference when learning the hook contract. It has one account, market and allocation, and its durable state is a single purchase timestamp. Use the Aave WETH reference for named lending positions and the banded LP reference only after the fixed-range LP lifecycle is clear.

## What an example contains

A complete reference normally contains:

| File                     | Purpose                                                               |
| ------------------------ | --------------------------------------------------------------------- |
| `strategy.py`            | Sandboxed decision, capability, outcome and optional dashboard hooks  |
| `config.yaml`            | Author-owned economic choices and named chain scope                   |
| `backtest-economic.yaml` | Prepared economic model, historical window and hypothetical inventory |
| `backtest.yaml`          | Optional detailed historical-path model where supported               |
| `README.md`              | The lifecycle, commands, assumptions and important authority limits   |

Operator provider bindings, account addresses and fork fixtures live under [`examples/operators/`](https://github.com/almanak-co/sdk/tree/main/examples/operators), not in the strategy directory. Generated deployments and resolved facts are runtime artifacts.

## Choosing an example

Choose by economic lifecycle before choosing by protocol name:

1. Decide whether the strategy lends, borrows, trades, provides liquidity or composes several of those actions.
1. Filter the support declaration by exact connector and CAIP-2 chain.
1. Confirm every required action, account profile and backtest model is declared.
1. Select the closest published reference and preserve its connector-owned action sequence while changing author policy.
1. Run the offline check before adding provider or execution inputs.

For example:

```
uv run --locked almanak support \
  --connector uniswap_v4 --chain eip155:42161 --format text
uv run --locked almanak check strategies/uniswap_v4_arbitrum_trading --format text
```

An empty support result means the requested combination is not declared. A directory that `almanak support --references` does not list is a fixture or QA proof, not evidence of support.

## Adapting safely

- Keep `strategy.py` and `config.yaml` as the only author-maintained runtime inputs.
- Preserve strict CAIP-2 and CAIP-19 identities. Do not replace them with chain numbers, token symbols or guessed addresses.
- Keep providers, secrets, wallet bindings and execution authority out of author files.
- Use public `ctx.actions` methods instead of copying protocol transaction construction from SDK internals.
- Treat the example's configuration as explicit sample choices, not connector defaults or recommended financial parameters.
- Retain teardown, failure and intermediate-outcome behavior when simplifying an example.
- Add new actions only when the support declaration and connector public API expose them.

Continue with [Strategy authoring](https://sdk.docs.almanak.co/authoring.html) for the hook contract, [Configuration](https://sdk.docs.almanak.co/configuration.html) for author inputs, and [Backtesting](https://sdk.docs.almanak.co/backtesting.html) for model and reproduction requirements.

# Supported connectors and chains

Connectors advertise immutable per-chain support for execution modes, economic actions, account profiles and historical coverage. A declaration does not establish provider availability, wallet funding or installed Safe authority.

| Connector              | `eip155:1`                 | `eip155:10`                | `eip155:137`               | `eip155:42161`             | `eip155:43114`             | `eip155:4663`              | `eip155:56`                | `eip155:8453`              | `eip155:999`               |
| ---------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- | -------------------------- |
| `aave_v3`              | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |                            | fork, production, backtest | fork, production, backtest |                            |
| `aave_v4`              | fork, production, backtest |                            |                            |                            | fork, production, backtest |                            |                            |                            |                            |
| `aerodrome`            |                            |                            |                            |                            |                            |                            |                            | fork, production, backtest |                            |
| `aerodrome_slipstream` |                            |                            |                            |                            |                            |                            |                            | fork, production, backtest |                            |
| `compound_v3`          | fork, production, backtest | fork, production, backtest |                            | fork, production, backtest |                            |                            |                            | fork, production, backtest |                            |
| `curve`                | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |                            |
| `gmx_v2`               |                            |                            |                            | fork, production, backtest | fork                       |                            |                            |                            |                            |
| `morpho_blue`          | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |
| `pancakeswap_v3`       | fork, production, backtest |                            |                            | fork, production, backtest |                            |                            | fork, production, backtest | fork, production, backtest |                            |
| `sparklend`            | fork, production, backtest |                            |                            |                            |                            |                            |                            |                            |                            |
| `uniswap_v3`           | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |                            |
| `uniswap_v4`           | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest | fork, production, backtest |                            |

## aave_v3

| Chain          | Modes                      | Actions                                                                    | Account profiles | Restrictions                                                                                                                                                                                             | Backtest models                                     | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified Pool and reserve-token revision 11 implementations, ordinary collateral mode and variable-rate debt, configured Pool, reserve assets and oracle feeds; no default markets                       | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 L2Pool and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                      | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 L2Pool, aToken and variable-debt semantics, full runtime fingerprints retain documented proxy metadata differences, ordinary collateral mode and explicit configured oracle sources | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints and explicitly configured oracle sources                                                                | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified revision-11 PoolInstance and reserve-token implementations, complete proxy fingerprints; reserve proxy metadata difference retained                                                             | `aave-v3-revision11-economic-indices-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | verified Pool and reserve-token revision 11 implementations, ordinary collateral mode and variable-rate debt, pinned Jovian L1 and operator fee accounting                                               | `aave-v3-revision11-economic-indices-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments                                             | Exit | Exit increments             | Each unwind cycle |
| --------------- | ----- | ------------------------ | ------------------------------------------------------------ | ---- | --------------------------- | ----------------- |
| `lending`       | 2     | none                     | +1 when `collateral` is granted, +1 when `borrow` is granted | 1    | +2 when `borrow` is granted | 3                 |

The `ctx.actions` methods for `aave_v3` and what each does are listed under "aave_v3 actions" in Strategy actions below.

## aave_v4

| Chain          | Modes                      | Actions                         | Account profiles | Restrictions                                                                    | Backtest models                                                                                 | Variants |
| -------------- | -------------------------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, borrow, repay | eoa, safe_roles  | verified V4 implementation, local fund/Spoke collateral, standard ERC-20 assets | `aave-v4-supply-historical-shares-v1` (historical_path), `aave-v4-economic-rates-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, borrow, repay | eoa, safe_roles  | verified V4 implementation, local fund/Spoke collateral, standard ERC-20 assets | `aave-v4-economic-rates-v1` (economic)                                                          | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments            | Exit | Exit increments             | Each unwind cycle |
| --------------- | ----- | ------------------------ | --------------------------- | ---- | --------------------------- | ----------------- |
| `lending`       | 2     | none                     | none                        | 1    | none                        | none              |
| `debt`          | 1     | none                     | +1 per item of `collateral` | 3    | +1 per item of `collateral` | none              |

The `ctx.actions` methods for `aave_v4` and what each does are listed under "aave_v4 actions" in Strategy actions below.

## aerodrome

| Chain         | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                             | Backtest models                                          | Variants         |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------- |
| `eip155:8453` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Aerodrome factory, clone, router and fee contracts, fungible pool shares and separate account-wide fee claims, one allocation per fund; no unrelated initial shares, fees or pool LP deposit, stable and volatile pools; no staking or reward authority, economic execution assumes access; approval and revocation mechanics excluded, pinned Jovian L1 and operator fee accounting | `aerodrome-economic-integer-fixed-indices-v1` (economic) | stable, volatile |

| Allocation kind | Entry | Each further entry slice | Entry increments | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------- | ---- | --------------- | ----------------- |
| `trade`         | 2     | 2                        | none             | 2    | none            | none              |
| `liquidity`     | 5     | 2                        | none             | 5    | none            | none              |

The `ctx.actions` methods for `aerodrome` and what each does are listed under "aerodrome actions" in Strategy actions below.

## aerodrome_slipstream

| Chain         | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                              | Backtest models                                          | Variants |
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -------- |
| `eip155:8453` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured compiled Slipstream factory, clone, periphery and fee modules, one allocation per fund, unstaked NFTs and undiscounted submitter fee context, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `slipstream-economic-integer-fixed-growth-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------- | ---- | --------------- | ----------------- |
| `trade`         | 2     | 2                        | none             | 2    | none            | none              |
| `liquidity`     | 5     | 2                        | none             | 5    | none            | none              |

The `ctx.actions` methods for `aerodrome_slipstream` and what each does are listed under "aerodrome_slipstream actions" in Strategy actions below.

## compound_v3

| Chain          | Modes                      | Actions                                                        | Account profiles | Restrictions                                                                                                                                                                                                                                                                                           | Backtest models                                       | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks                                               | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks, pinned Jovian L1 and operator fee accounting | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks                                               | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Comet implementation, extension and immutable asset-list revisions, signed base principal and non-interest-bearing collateral, no rewards or manager authority, scope-only multipurpose ABI grants; economic distinctions remain gateway checks, pinned Jovian L1 and operator fee accounting | `compound-v3-exogenous-integer-indices-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments            | Exit | Exit increments             | Each unwind cycle |
| --------------- | ----- | ------------------------ | --------------------------- | ---- | --------------------------- | ----------------- |
| `lending`       | 2     | none                     | +1 when `borrow` is granted | 1    | +2 when `borrow` is granted | 3                 |

The `ctx.actions` methods for `compound_v3` and what each does are listed under "compound_v3 actions" in Strategy actions below.

## curve

| Chain          | Modes                      | Actions                                                                                                                   | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                                      | Backtest models                                                                                                      | Variants                                       |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |
| `eip155:10`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution, pinned Jovian L1 and operator fee accounting | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:137`   | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |
| `eip155:43114` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic)                                                        | stableswap_ng                                  |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng_2024 |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution                                               | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng                    |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | verified StableSwap NG, TwoCrypto NG or TriCrypto NG revision, ordered standard non-rebasing coins; no metapool routing, one allocation per fund, LP fees embedded in share economics; explicit configured single-coin exit, economic simulation assumes execution access and fixed external market history, explicit bounded samples and source age; no missing-input substitution, pinned Jovian L1 and operator fee accounting | `curve-stableswap-fixed-external-unit-balances-v1` (economic), `curve-ng-fixed-external-unit-balances-v1` (economic) | stableswap_ng, twocrypto_ng, tricrypto_ng      |

| Allocation kind | Entry | Each further entry slice | Entry increments       | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------------- | ---- | --------------- | ----------------- |
| `trade`         | 2     | 2                        | none                   | 2    | none            | none              |
| `liquidity`     | 3     | 2                        | +1 per item of `coins` | 3    | none            | none              |

The `ctx.actions` methods for `curve` and what each does are listed under "curve actions" in Strategy actions below.

## gmx_v2

| Chain          | Modes                      | Actions                                                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                               | Backtest models                                         | Variants |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------- |
| `eip155:42161` | fork, production, backtest | increase_position, decrease_position, close_position, revoke_position, swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, revoke_exchange | eoa, safe_roles  | Verified market and compiled deployment closure; wrapped-native collateral., Solvent liquidation reconciles as an external effect; native insolvency is not advertised (16 September 2026 disposition)., Dedicated Safe with accepted broad cancellation grants and retained-key checks., one allocation per fund                                                          | `gmx-v2-published-opportunity-integer-gm-v1` (economic) | none     |
| `eip155:43114` | fork                       | increase_position, decrease_position, close_position, revoke_position, swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, close_liquidity, revoke_exchange | eoa, safe_roles  | Candidate identified-fork request lifecycle; not promoted SDK support., Verified market and compiled deployment closure; wrapped-native collateral., Position and swap candidate implementation; GM liquidity candidate implementation; offline models unproven., Dedicated Safe with accepted broad cancellation grants and retained-key checks., one allocation per fund | none                                                    | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments              | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ----------------------------- | ---- | --------------- | ----------------- |
| `position`      | 2     | none                     | none                          | 3    | none            | none              |
| `swap`          | 3     | none                     | none                          | 1    | none            | none              |
| `liquidity`     | 1     | none                     | +1 per item of `input_assets` | 1    | none            | none              |

The `ctx.actions` methods for `gmx_v2` and what each does are listed under "gmx_v2 actions" in Strategy actions below.

## morpho_blue

| Chain          | Modes                      | Actions                                                        | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                         | Backtest models                                     | Variants |
| -------------- | -------------------------- | -------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks, pinned Jovian L1 and operator fee accounting | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit ERC-4626 conversion oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                     | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks, pinned Jovian L1 and operator fee accounting | `morpho-blue-archive-anchored-market-v1` (economic) | none     |
| `eip155:999`   | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount | eoa, safe_roles  | verified Morpho Blue core, adaptive IRM and explicit non-vault oracle revision, separate supply shares, borrow shares and non-interest-bearing collateral, no rewards, callbacks, bundlers or delegated managers, scope-only supply and borrow grants with no amount bound; health and ownership remain gateway checks                                               | `morpho-blue-archive-anchored-market-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments            | Exit | Exit increments             | Each unwind cycle |
| --------------- | ----- | ------------------------ | --------------------------- | ---- | --------------------------- | ----------------- |
| `lending`       | 2     | none                     | +1 when `borrow` is granted | 1    | +2 when `borrow` is granted | 3                 |

The `ctx.actions` methods for `morpho_blue` and what each does are listed under "morpho_blue actions" in Strategy actions below.

## pancakeswap_v3

| Chain          | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                                             | Backtest models                                              | Variants |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, one allocation per fund, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, one allocation per fund, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, one allocation per fund, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity, exit_exchange, revoke_exchange | eoa, safe_roles  | configured verified Pancake original router, factory, deployer and pool, one allocation per fund, absent or compiled verified liquidity-mining callback; unstaked NFTs, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `pancakeswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------- | ---- | --------------- | ----------------- |
| `trade`         | 2     | 2                        | none             | 2    | none            | none              |
| `liquidity`     | 5     | 2                        | none             | 5    | none            | none              |

The `ctx.actions` methods for `pancakeswap_v3` and what each does are listed under "pancakeswap_v3 actions" in Strategy actions below.

## sparklend

| Chain      | Modes                      | Actions                                                                    | Account profiles | Restrictions                                                                                                                                                                                                  | Backtest models                                      | Variants |
| ---------- | -------------------------- | -------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -------- |
| `eip155:1` | fork, production, backtest | supply, withdraw, withdraw_amount, borrow, repay, repay_amount, collateral | eoa, safe_roles  | Ethereum Pool revision 4 and token revision 1, ordinary collateral mode and variable-rate debt, isolated and siloed reserves remain unsupported, configured Pool, assets and oracle feeds; no default markets | `sparklend-revision4-economic-indices-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments                                             | Exit | Exit increments             | Each unwind cycle |
| --------------- | ----- | ------------------------ | ------------------------------------------------------------ | ---- | --------------------------- | ----------------- |
| `lending`       | 2     | none                     | +1 when `collateral` is granted, +1 when `borrow` is granted | 1    | +2 when `borrow` is granted | 3                 |

The `ctx.actions` methods for `sparklend` and what each does are listed under "sparklend actions" in Strategy actions below.

## uniswap_v3

| Chain          | Modes                      | Actions                                                                                                                                           | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                              | Backtest models                                          | Variants |
| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT                                               | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, exit_exchange, revoke_exchange, open_liquidity, increase_liquidity, decrease_liquidity, collect_liquidity_fees, close_liquidity | eoa, safe_roles  | configured verified factory, pool and periphery revisions, explicit token pair and fee; exact-input swaps only, one allocation per fund, economic execution assumes access; approval and revocation mechanics excluded, broader NFT manager grants can affect compatible owned or approved NFTs; gateway operations require the retained strategy-owned NFT, pinned Jovian L1 and operator fee accounting | `uniswap-v3-economic-integer-fixed-growth-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------- | ---- | --------------- | ----------------- |
| `trade`         | 2     | 2                        | none             | 2    | none            | none              |
| `liquidity`     | 5     | 2                        | none             | 5    | none            | none              |

The `ctx.actions` methods for `uniswap_v3` and what each does are listed under "uniswap_v3 actions" in Strategy actions below.

## uniswap_v4

| Chain          | Modes                      | Actions                                                                                                   | Account profiles | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | Backtest models                                                                                                        | Variants |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- |
| `eip155:1`     | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:10`    | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only), pinned Jovian L1 and operator fee accounting | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:137`   | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:42161` | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:43114` | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |
| `eip155:4663`  | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:56`    | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only)                                               | `uniswap-v4-economic-integer-fixed-growth-v1` (economic)                                                               | none     |
| `eip155:8453`  | fork, production, backtest | swap_exact_input, open_liquidity, collect_liquidity_fees, close_liquidity, revoke_exchange, exit_exchange | eoa, safe_roles  | hookless pools, or reviewed hook profiles (Pons hooks for trading only), two standard ERC-20 currencies, or the native coin held as its wrapped token for trading and liquidity, native-coin liquidity in one token pair per chain, never beside ERC-20 liquidity on that chain; a native mint measures more gas than an ERC-20 mint, and check warns when max_gas_limit is below the measured native mint, measured on another chain where this one has no measurement, single pool, one allocation per fund, fixed LP range or recentring band (bands backtest in economic mode only), pinned Jovian L1 and operator fee accounting | `uniswap-v4-exact-quoter-fixed-path-lp-v1` (historical_path), `uniswap-v4-economic-integer-fixed-growth-v1` (economic) | none     |

| Allocation kind | Entry | Each further entry slice | Entry increments | Exit | Exit increments | Each unwind cycle |
| --------------- | ----- | ------------------------ | ---------------- | ---- | --------------- | ----------------- |
| `trade`         | 3     | 3                        | none             | 4    | none            | none              |
| `liquidity`     | 8     | 3                        | none             | 5    | none            | none              |

The `ctx.actions` methods for `uniswap_v4` and what each does are listed under "uniswap_v4 actions" in Strategy actions below.

## Strategy actions

Methods a strategy calls on `ctx.actions` for each connector, read from the binding the connector installs. The first argument is the configured allocation name. Each entry gives the signature and what the action does: for example that an `exit` teardown closes liquidity, converts residual inventory to the funding asset and revokes allowances, how `swap` picks its quote, and `repay` versus `repay_amount`.

### aave_v3 actions

Strategy actions on `ctx.actions` for the `aave_v3` connector: `borrow`, `repay`, `repay_amount`, `set_collateral`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units of the reserve asset; needs `borrow` authority.
- `repay(name: str)`: Repay the whole debt with accrued interest; the fund must hold all of it.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `set_collateral(name: str, enabled: bool)`: Enable or disable the supplied balance as collateral. Disabling with debt outstanding must keep the minimum health factor.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory. A first supply into a reserve with an LTV enables collateral; needs `collateral: true`.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### aave_v4 actions

Strategy actions on `ctx.actions` for the `aave_v4` connector: `borrow`, `repay`, `supply`, `withdraw`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units on a debt allocation. Borrowing is backed by the debt allocation's named collateral allocations.
- `repay(name: str)`: Repay the whole debt with accrued interest; the fund must hold all of it.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.

### aerodrome actions

Strategy actions on `ctx.actions` for the `aerodrome` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### aerodrome_slipstream actions

Strategy actions on `ctx.actions` for the `aerodrome_slipstream` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### composed actions

Strategy actions on `ctx.actions` for the `composed` connector: `borrow`, `close_liquidity`, `collect`, `component`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `repay`, `repay_amount`, `revoke`, `set_collateral`, `supply`, `swap`, `withdraw`, `withdraw_amount`.

- `borrow(name, amount)`: Route `borrow` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `close_liquidity(name)`: Route `close_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and uniswap_v4. GMX takes minimums: call it through `component(name)`.
- `collect(name)`: Route `collect` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and uniswap_v4.
- `component(name)`: The `ctx.actions` of the component connector that owns allocation `name`. Call it for anything a wrapper here does not forward, e.g. `ctx.actions.component(name).open_liquidity(name)` on uniswap_v4, or GMX `increase`.
- `decrease_liquidity(name, liquidity)`: Route `decrease_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. GMX takes `shares` and minimums: call it through `component(name)`.
- `exit(name)`: Route `exit` (full teardown) to the component that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve, uniswap_v4 and gmx_v2; lending components have none. Returns None when that component has nothing left to exit.
- `increase_liquidity(name, *, fraction_bps=10000)`: Route `increase_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. GMX takes maximums: call it through `component(name)`; uniswap_v4 has none.
- `open_liquidity(name, *, fraction_bps=10000)`: Route `open_liquidity` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream and curve. uniswap_v4 takes no `fraction_bps` and GMX takes maximums: call them through `component(name)`.
- `repay(name)`: Route `repay` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `repay_amount(name, amount)`: Route `repay_amount` to the component connector that owns allocation `name`. Lending components: aave_v3, sparklend, compound_v3, morpho_blue; aave_v4 has none.
- `revoke(name)`: Route `revoke` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and gmx_v2.
- `set_collateral(name, enabled)`: Route `set_collateral` to the component connector that owns allocation `name`. Components: aave_v3 and sparklend.
- `supply(name, amount)`: Route `supply` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `swap(name, *, exit=False)`: Route `swap` to the component connector that owns allocation `name`. Components: uniswap_v3, pancakeswap_v3, aerodrome, aerodrome_slipstream, curve and uniswap_v4, forwarding only `exit`; call `component(name)` for `rebalance=True` and GMX swaps.
- `withdraw(name)`: Route `withdraw` to the component connector that owns allocation `name`. Lending components: aave_v3, aave_v4, sparklend, compound_v3, morpho_blue.
- `withdraw_amount(name, amount)`: Route `withdraw_amount` to the component connector that owns allocation `name`. Lending components: aave_v3, sparklend, compound_v3, morpho_blue; aave_v4 has none.

### compound_v3 actions

Strategy actions on `ctx.actions` for the `compound_v3` connector: `borrow`, `repay`, `repay_amount`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` of the Comet base asset against collateral in the same Comet. Needs `borrow` authority on the base allocation.
- `repay(name: str)`: Repay the whole debt with accrued interest; the fund must hold all of it.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `supply(name: str, amount: int)`: Supply `amount` base units of the allocation's asset to its Comet. The base asset earns interest; a collateral asset backs borrowing of the base asset.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### curve actions

Strategy actions on `ctx.actions` for the `curve` connector: `close_liquidity`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Burn all owned LP shares with slippage-bounded minimums. Withdraws in `single_coin_exit` when configured, otherwise proportionally.
- `decrease_liquidity(name, liquidity)`: Burn `liquidity` LP shares, keeping some owned, with slippage-bounded minimums. Withdraws in `single_coin_exit` when configured, otherwise proportionally.
- `exit(name)`: Full exit/teardown: burn owned LP shares, swap non-funding coins to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Deposit up to `fraction_bps` of owned inventory into the owned LP shares.
- `open_liquidity(name, *, fraction_bps=10000)`: Deposit up to `fraction_bps` of owned inventory of every pool coin for LP shares. Refuses when shares are already owned; use `increase_liquidity` then.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False)`: Swap along `entry_quote`, or the first of `exit_quotes` with `exit=True`. The minimum output is the quote less `slippage_bps`; raises when that quote is absent.

### gmx_v2 actions

Strategy actions on `ctx.actions` for the `gmx_v2` connector: `close`, `close_liquidity`, `decrease`, `decrease_liquidity`, `exit`, `increase`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close(name)`: Close the whole perpetual position through a keeper order.
- `close_liquidity(name, *, minimum0: AssetAmount, minimum1: AssetAmount)`: Redeem all owned GM market tokens for at least `minimum0`/`minimum1`.
- `decrease(name, *, collateral_base_units, size_delta_usd_e30)`: Shrink the perpetual position by `size_delta_usd_e30` and withdraw collateral. `collateral_base_units` of the collateral asset are withdrawn alongside the size change.
- `decrease_liquidity(name, *, shares: AssetAmount, minimum0: AssetAmount, minimum1: AssetAmount)`: Redeem `shares` GM market tokens for at least `minimum0`/`minimum1`.
- `exit(name)`: Next exit/teardown step, or None when nothing remains; call it every decision. Perpetuals close then revoke; swap allocations swap non-funding inventory to the funding asset at `exit_minimum_output`; GM liquidity redeems at `withdrawal_minimums`.
- `increase(name, *, collateral_base_units, size_delta_usd_e30)`: Open or grow the perpetual position by collateral and USD size. `collateral_base_units` is in the collateral asset; `size_delta_usd_e30` has 30 decimals.
- `increase_liquidity(name, *, maximum0: AssetAmount, maximum1: AssetAmount)`: Deposit up to `maximum0`/`maximum1` more into the owned GM tokens.
- `open_liquidity(name, *, maximum0: AssetAmount, maximum1: AssetAmount)`: Deposit up to `maximum0`/`maximum1` of the market currencies for GM tokens.
- `revoke(name)`: Zero the allocation's remaining allowances (exchange or position authority).
- `swap(name, *, amount: AssetAmount, minimum_output: AssetAmount)`: Request an asynchronous keeper swap of `amount` with an explicit `minimum_output`.

### morpho_blue actions

Strategy actions on `ctx.actions` for the `morpho_blue` connector: `borrow`, `repay`, `repay_amount`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` of the loan asset against collateral in the same market. Needs `borrow` authority on the loan allocation.
- `repay(name: str)`: Repay the whole debt with accrued interest; the fund must hold all of it.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `supply(name: str, amount: int)`: Supply `amount` base units of the allocation's asset to its market. A loan allocation lends the loan asset; a collateral allocation posts collateral.
- `withdraw(name: str)`: Withdraw everything: all supplied loan shares or all posted collateral.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### pancakeswap_v3 actions

Strategy actions on `ctx.actions` for the `pancakeswap_v3` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### sparklend actions

Strategy actions on `ctx.actions` for the `sparklend` connector: `borrow`, `repay`, `repay_amount`, `set_collateral`, `supply`, `withdraw`, `withdraw_amount`.

- `borrow(name: str, amount: int)`: Borrow `amount` base units of the reserve asset; needs `borrow` authority.
- `repay(name: str)`: Repay the whole debt with accrued interest; the fund must hold all of it.
- `repay_amount(name: str, amount: int)`: Repay exactly `amount` base units; refuses an amount above the outstanding debt.
- `set_collateral(name: str, enabled: bool)`: Enable or disable the supplied balance as collateral. Disabling with debt outstanding must keep the minimum health factor.
- `supply(name: str, amount: int)`: Supply `amount` base units of the reserve asset from fund inventory. A first supply into a reserve with an LTV enables collateral; needs `collateral: true`.
- `withdraw(name: str)`: Withdraw the whole supplied balance, including accrued interest.
- `withdraw_amount(name: str, amount: int)`: Withdraw exactly `amount` base units, leaving the rest supplied.

### uniswap_v3 actions

Strategy actions on `ctx.actions` for the `uniswap_v3` connector: `close_liquidity`, `collect`, `decrease_liquidity`, `exit`, `increase_liquidity`, `open_liquidity`, `revoke`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory, with principal less `slippage_bps`. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect` (or `close_liquidity` again once no shares remain).
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `decrease_liquidity(name, liquidity)`: Remove `liquidity` units from the owned position, keeping it open. Concentrated pools also collect fees; classic Aerodrome burns shares for principal only, so claim its fees with `collect`. Minimums are observed principal less `slippage_bps`.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `increase_liquidity(name, *, fraction_bps=10000)`: Add up to `fraction_bps` of owned inventory to the owned position.
- `open_liquidity(name, *, fraction_bps=10000)`: Open a position from up to `fraction_bps` of owned inventory in both assets. Concentrated pools: a banded allocation centres the range on the observed tick, a fixed one uses its range. Classic Aerodrome stable/volatile pools have no range.
- `revoke(name)`: Zero the allocation's remaining token allowances (spending authority).
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

### uniswap_v4 actions

Strategy actions on `ctx.actions` for the `uniswap_v4` connector: `close_liquidity`, `collect`, `exit`, `open_liquidity`, `swap`.

- `close_liquidity(name)`: Remove all owned liquidity into owned inventory. Minimums are the observed principal less the allocation's `slippage_bps`.
- `collect(name)`: Collect accrued fees from the owned position without changing its liquidity.
- `exit(name)`: Full exit/teardown: close owned liquidity, swap counter inventory to funding, revoke. Exit converts residual inventory to the configured funding asset and clears remaining allowances in one runtime-driven sequence; returns None when nothing remains to exit. Measured dust whose exact-size exit quote returns zero output may stay owned after it.
- `open_liquidity(name)`: Open a position using all owned inventory of both currencies as maximums. A banded allocation centres the range on the observed tick; a fixed one uses its range.
- `swap(name, *, exit=False, rebalance=False)`: Swap along an observed exact-size quote, with the minimum output less `slippage_bps`. Follows `entry_quote` by default, `exit_quote` with `exit=True` and the banded `rebalance_quote` with `rebalance=True`; raises when that quote is absent at this block.

Generated from `almanak support --json` and the connector action bindings; do not edit by hand.
# Operate

# Operating retained workspaces

Execution requires `run --fork` or `run --production` with an explicit operator file (`--gateway-config`) and the private provider env file (`--env-file`). Credentials never select production implicitly. Each private workspace captures the authored inputs, source, the generated read context, resolved facts, setup stages, endpoint references, process identities and logs. `state.sqlite` contains authoritative execution facts; `workspace.json` contains supervision. Keep credentials outside strategy-readable paths.

## Operator configuration

The operator file is YAML or JSON with `schema_version: operator-gateway-config-v1`. It selects the enabled `modes`, one provider per chain under `networks` (`alchemy` with `api_key_env`, or `rpc_url` with `url_env`; the env file holds the values), named vendor access under `vendors` when a backtest requests one, the named `accounts.bindings` the author file requires, and mode authority: `fork` holds the fixture anchors, funds and operators; `production` holds execution networks, independent payer ceilings and signing file references. When the author requests gas policy, `fork.gas` also supplies explicit operator ceilings, grouped by the fixture's actual payer relationships. A shared operator requires one compatible policy and ceiling for all accounts it submits for. The [fork](https://github.com/almanak-co/sdk/blob/main/examples/operators/aave_v4_weth_supply.fork.yaml), [production](https://github.com/almanak-co/sdk/blob/main/examples/operators/aave_v4_weth_supply.production.yaml) and [backtest](https://github.com/almanak-co/sdk/blob/main/examples/operators/backtest/eip155-1.yaml) examples cover the Aave reference. An operator file never carries provider keys.

A run first binds the authored choices to the operator file and writes the read context, then starts a read-only gateway that resolves the named markets at the selected anchors and retains the verified facts. Fork runs read the anchors from the operator fixture; production runs pin fresh anchors. Hooks run once on the resolved parameters and the results are frozen before any execution authority exists. `check --online` performs the same resolution and hook stages in a temporary directory and reports a preview of the resolved facts without creating a workspace.

| Command                                         | Effect                                                              |
| ----------------------------------------------- | ------------------------------------------------------------------- |
| `status --run PATH`                             | Retained financial state and current local process availability.    |
| `pause --run PATH`                              | Wait for coordinator pause; leave services and positions available. |
| `resume --run PATH`                             | Explicit recovery through captured inputs and retained execution.   |
| `teardown --run PATH`                           | Persist closure intent and start valid cleanup.                     |
| `shutdown --run PATH`                           | Pause then stop services after closure.                             |
| `shutdown --run PATH --force`                   | Stop despite unresolved obligations and preserve evidence.          |
| `report --run PATH --format html --output FILE` | Offline shared report and optional custom hook.                     |

`resume` returns once the supervisor accepts it. Admission, the gateway start and the coordinator start then run in the background: status reports `provisioning: true` until they finish and then the resulting stage, such as `running`, `action_required` or `failed` with its diagnostic. Other commands except `pause` are refused meanwhile.

Production admission reports every gap in one diagnostic. When funding is missing and Safe permissions are not installed, the stage is `action_required` with both reasons, and the owner transactions are already exported under `private/deployment/permissions/`.

Ctrl-C on a foreground run requests pause and waits for acknowledgement. Pending work and open positions remain recorded. A failed coordinator is not restarted automatically. Its diagnostic is the refusal the coordinator reported, such as an `inventory` shortfall or a `gas_limit` or `gas_budget` refusal naming the requested amount, the estimate or quote and the configured limit, and status keeps reporting it after the supervisor exits. A coordinator that stopped without a reported refusal leaves the generic instruction to inspect its log. A market fee quote above `max_gas_price_wei` (`gas_price`) or `max_transaction_fee` (`gas_fee_quote`) does not stop a single-chain coordinator: it signs nothing, keeps its decision and quotes again with a back-off of up to a minute, for as long as the market stays above the ceiling. While it holds, status stays `running` and the diagnostic names the kind and the policy field; the coordinator log has a `gas_ceiling_hold` line per attempt with the quote, then `gas_ceiling_recovered` once a plan is admitted or `gas_ceiling_dropped` when the decision is dropped. On a multi-chain deployment the refusal is the chain's retained fault and the chain is retried on each pass. `almanak test run` stops on such a hold at once. Explicit resume preserves pending identities and performs the existing lease, signer-claim and receipt audits. Never remove a journal to clear a pending transaction.

A strategy that declares [price series](https://sdk.docs.almanak.co/authoring.html#price-series-and-technical-analysis) needs no additional service or credential: the gateway reads each series from the chain reader of the series' chain, which the deployment must route. Provisioning refuses a series on a chain the deployment has no provider for, or whose connector has no reviewed deployment there; at launch the gateway resolves each declared pool and refuses to serve one that does not resolve or does not hold the declared asset. Before each decision the gateway reads the series as of the earlier of the decision time and the observed block time, and returns only the series the frozen preparation declared. Boundary blocks, points and their evidence are retained in `strategy-prices.sqlite` beside the journal, so a decision reads only boundaries it has not seen, and each served series, holds included, is appended there. A fork reads history before its fork block through its node, which forwards those reads to the upstream provider. Single strategy prices are a backtest-only Aave V4 input and are not served. A failed read at a decision records a hold for that decision; teardown decisions read no series.

The gateway holds the journal's write lock only to commit a request. Provider reads and remote signing run against the checkpoint read under the current lease; the commit re-checks the lease and refuses a recording whose checkpoint changed meanwhile, and a broadcast re-checks its envelope, hash and nonce and sends only under the lock. Renewals therefore never wait behind a slow plan or signature. Replacement families keep the lock for their whole request. Each `gateway_request` log line reports provider calls by method (`rpc_calls`, `rpc`), HTTP exchanges with the provider (`rpc_round_trips`, a batch counting once), provider, signer and lock time (`rpc_ms`, `signer_ms`, `lock_wait_ms`, `lock_held_ms`) alongside `duration_ms`.

Fork fixtures fund each declared asset with simulated capital. The chain's wrapped native asset is funded by wrapping and transfer, and nothing else is tried. Every other ERC-20 goes through an ordered ladder: a reviewed token-specific funder, a transfer from a listed holder, a known balance slot or seed recipe, `anvil_dealERC20`, then a probe of common balance slots unless the token has a seed recipe. A token with a reviewed funder or a listed holder never falls through to the generic routes; a holder short of the amount at the fork head (after any earlier fixture transfers) is refused. Funding refuses a token address with no contract code. Each ladder route must leave the exact balance and pass a transfer check: the account sends part of it to an address derived from the account and token, which must start empty and receive exactly that amount, and the fork reverts the check. The storage routes (`known_slot`, `seed_recipe`, `deal_erc20`, `probed_slot`) write the balance without minting, so `totalSupply` is left unchanged; such a route is refused when `totalSupply` is below the funded amount. A failed route is rolled back before the next one runs. When no route passes, the fixture refuses and names each outcome; nothing is partially funded. If `anvil_dealERC20` is still running after 20 s, the node keeps tracing in the background, so funding stops without rolling back and the fork must be recreated. In the compiled fixture plan every non-wrapped ERC-20 has `method: local_storage`, which means "any ERC-20 route", not a storage write. The `simulated_initial_capital` rows in `fixture-installation.json` record the route actually used in `method` (`wrap_native`, `reviewed`, `holder_transfer`, `known_slot`, `seed_recipe`, `deal_erc20` or `probed_slot`) with its `slot`, `seed` or `holder`, so a fork pass that relied on unreviewed funding is visible.

Fork resume verifies the original process and Anvil instance, chain and origin. A stopped fork cannot be recreated as the same deployment. Interrupted fixture funding reverts only through the retained snapshot token of the original node; an uncertain rollback, a lost node, or execution state without its preparation is refused. A completed fixture is reused and never rolled back. Production provisioning writes reviewable artifacts and performs no owner actions. Multi-host supervision is unsupported.

## Fork tests

`almanak test config` writes a fork operator file from the author configuration, with the shape of the shipped `examples/operators/*.fork.yaml`:

```
uv run --locked almanak test config ./my-strategy --env-file /private/providers.env \
  --output /private/operator/my-strategy.fork.yaml [--block N] [--force]
```

It names one Alchemy provider per authored chain through `ALCHEMY_API_KEY`, placeholder account bindings, anchors at the latest block (or `--block`, for a single-chain strategy; a chain with a GMX market anchors at least three hours behind the head, so the authentic keeper reports a fork test replays exist for its requests), Safe/Roles fixture funds sized from an online fork check's `funding` block plus 1%, one fixture operator per chain and gas policy holding at least one native unit and twice its entry minimum plus gas budget, and `fork.gas` ceilings equal to the authored `strategy.gas`. On Base and Optimism, where forks cannot bound the L1 data and operator fees and authored ceilings are refused, the file sets `fork.author_gas: unenforced` and carries a note: the fork runs gasless and the authored policy is retained but not enforced. An operator file without that choice keeps the refusal. An existing `--output` is replaced only with `--force`.

`almanak test run` runs one supervised fork lifecycle through the same workspace `run --fork` creates:

```
uv run --locked almanak test run ./my-strategy \
  --gateway-config /private/operator/my-strategy.fork.yaml \
  --env-file /private/providers.env --output /private/runs/test-1 \
  [--decisions 3] [--advance SECONDS] [--through-exit] [--param KEY=VALUE ...] \
  [--production-parameters] [--json]
```

It waits for admission, then lets up to `--decisions` decisions settle. Between them it advances every fork node's time by the decision interval (or `--advance`, raised to the next due decision when shorter, so it never waits on the wall clock) with `evm_setNextBlockTimestamp` and `evm_mine`, and moves the coordinator's decision clock by the same offset; the author's interval is never edited. It stops early once an intent that increases exposure has executed, unless `--through-exit` keeps it deciding up to `--decisions`, so the strategy's own later intents run: the mint after an LP entry swap, fee collection, a recentre, its own close or sell. Size `--advance` so those fall within the decisions, e.g. past a hold time. After the entry, `--idle` consecutive holds (default 3) end the decisions early: a short run of holds is a strategy waiting on its own timer, a longer one means it has nothing left to do. It then requests teardown, waits until the deployment is closed, shuts the services down and releases the run's fund claims. Interrupts and failures still stop every recorded service.

The config's [`strategy.test_parameters`](https://sdk.docs.almanak.co/configuration.html#strategy-parameters) replace their declared `strategy.parameters` values in the workspace's copy of the inputs before admission, unless `--production-parameters` is given. Each `--param KEY=VALUE` (repeatable; split at the first `=`, the value read as one YAML scalar, so `1` is an integer, `true` a boolean and `'"0.5"'` a string) replaces one on top of them. Each takes the declared type: a string knob keeps the text as written (`1.10` stays `'1.10'`), a float knob takes any finite number (`0.00001`, `1e-5`), an integer refuses `_` and exponents, and `null` is refused unless config.yaml declares `null`. With a [typed model](https://sdk.docs.almanak.co/authoring.html#typed-parameters) the model converts it instead. An undeclared key refuses the run as an input error. `config.yaml` is never modified.

Teardown never admits an intent that increases exposure; the coordinator drops it and logs `exposure_refused_in_teardown`. A strategy that proposes one 3 times with no teardown outcome between them ends the wait as `teardown_refused` (class `strategy`), with the intent kind and allocation in its `details`.

While teardown waits, a fork whose journal has stalled for 30 seconds has its time advanced, except while a decision is retained without a plan, attempt or an unresolved request of its own (a completed GMX request exempts nothing): the gateway prepares that decision's plan again against the chain before it reserves it, and a block mined in between would refuse it as a plan that differs from verified preparation.

A fork runs no protocol keeper, so a GMX order, deposit or withdrawal would stay pending. While the run waits on an accepted GMX request it settles it the way a keeper would, with nothing synthesized: it reads a keeper execution mined on the origin chain after the request was created, impersonates that keeper (which already holds the role at the fork origin) and submits its unchanged signed Chainlink Data Streams payloads to the fork's handler, at the reports' time. GMX's contracts then verify the signatures, report times, acceptable price and triggers. A request with no eligible report within its expiration stops the lifecycle with that reason; the source search is retained under `keepers/` in the workspace.

Keepers publish when somebody's request is pending, so on a quiet market they can be minutes apart, longer than GMX's 300-second request expiration. Because the fork runs behind the chain head, the run knows the coming publications and times the fork by them: `test config` starts a single-market GMX fork where usable publications stay under four minutes apart for fifteen minutes after admission, and each later advance (the `--advance` step, the start of teardown and its stall advances) is lengthened when needed so the decision lands 120 to 280 seconds before a usable publication: its request follows the decision's approvals, and its reports must come after the request and within GMX's 300-second expiration of it. This only chooses when the fork acts; the strategy's interval and every report are unchanged. The verdict is printed and retained as `test-verdict.json` in the new workspace; an existing `--output` is refused and keeps its earlier verdict:

| Field                 | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `schema`              | `almanak-test-verdict-v1`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `workspace`           | The fork workspace directory, or null when the inputs were refused                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `parameter_overrides` | The applied parameter values by key, in the declared type, or with a typed model as written for the model to convert: `strategy.test_parameters` (unless `--production-parameters`) with any `--param` on top; `{}` when none applied or the inputs were refused                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `parameter_sources`   | Per key in `parameter_overrides`, where its value came from: `test_parameters` or `--param`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `admission`           | `ok`, and the `refusal` (an input error document, or the failed stage and its diagnostic, with the supervisor's failure kind as `error` when it recorded one), classified like a failure                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `decisions`           | Per decision: `time`, `result` (`hold` or `intent`) and the `intents` kinds; `settled: false` marks a decision the lifecycle stopped on before its work settled. From the `decision_evaluated` line the test run's coordinator logs (only `almanak test run` opts its coordinator in; production and hosted deployments never log it): `evaluated` (false when an unavailable price series held the decision before the strategy ran, with `price_error`), `observed` allocations, `price_series` (per declared series: `asset`, `complete`, `points`, `window`, `latest_at`, `age_seconds`; pool-sourced series also carry `timeframe`, `gaps` and `max_age_seconds`) and `availability` (chains not ready, with their fault). On a multi-chain deployment `chain` names the chain whose tick evaluated the decision; a row is given facts only when they are unambiguously its own. `observation_errors` lists each `*_error` field an observation recorded instead of a measurement, as `allocation`, `field` (its dotted path in the observation) and `message`; an `entry_quote_error` there means the strategy saw no entry to take, for example on an empty pool. These are facts, not the strategy's reason for a hold |
| `transactions`        | Per executed transaction: `trigger` (`decision`, or `teardown` for teardown's own transactions), `action`, `allocation`, `status`, `gas_used`, `via_roles`, `transaction`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `entered`, `closed`   | Whether an entry executed and the deployment closed. A request the connector settled unsuccessfully, such as a GMX order its keeper cancelled, is not an entry                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `evidence_clean`      | The final journal holds no pending or reserved attempt, undelivered outcome, accounting issue, integrity error or chain fault                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `executed`            | Intent kinds that executed completely and successfully, in first-execution order; teardown's are not included, nor an intent whose request was cancelled                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `after_entry`         | The `executed` kinds that ran after the first entry: what `--through-exit` exercised beyond it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `teardown_executed`   | Intent kinds teardown executed completely, in first-execution order: an exit the strategy never ran itself was still exercised when it is listed here. Each kind is listed once; `teardown_sequence` has every execution                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `teardown_sequence`   | Every intent teardown executed completely, in order and with repeats, as `intent` and `allocation`: two exit swaps around a withdrawal are two rows                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `teardown_decisions`  | Every teardown decision the coordinator evaluated, in order: `time`, `result` (`hold` or `intent`), the `intents` kind and the same facts as `decisions` rows (`evaluated`, `observed`, `observation_errors`, `availability`). In teardown a hold claims that nothing is left to do; when the last row holds, `closure` is `closed`, or `not_closed` with the teardown failure's kind as `refusal` (such as `remaining_obligations`, whose `failures` entry lists what stayed open)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `in_flight`           | After an interrupt, each decision the journal still held unsettled: its `trigger` (`interval`, `teardown`, …), `intent` kind, `allocation` and the broadcast `transaction`, null before broadcast. Empty otherwise. `transactions`, `teardown_executed` and `closed` are read from the journal at the interrupt, so teardown work done before it is reported                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `repayment`           | The runtime's latest repayment coverage per borrowing allocation, as at closure: `allocation`, `asset`, `block`, `debt`, `available`, `shortfall` and the labeled rate `projection`; empty when no debt was measured                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `holdings`            | Per fund account, its `balances` once teardown ends (after an interrupt, at the interrupt: `closed` is false and `in_flight` shows what was pending), read from its fork node: each configured asset on its chain with `asset`, `symbol` when known, `amount` in base units and the decimal `quantity`, zero balances included. These are whole on-chain balances, the fixture's 1% funding buffer included; compare them with the asset the exit should leave the fund in. `error` replaces balances that could not be read                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `keepers`             | Per request a fork keeper settled: its `request` key, `kind`, `chain`, the replayed `keeper` and its `source_transaction` and `source_block` on the origin chain, the `report_times`, the fork `transaction` and its `status` (`mined` or `reverted`, with the `revert` reason), and the fork time it `advanced_seconds`. A mined execution whose receipt carries the request's lifecycle event also names the `request_status` GMX gave the request (`executed`, `cancelled` or `frozen`); a cancellation adds its `reason` (GMX's reason string, else the error name for known price and output bounds, else the raw bytes) and `reason_bytes`. A keeper transaction mines whether GMX executed or cancelled the request                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `failures`            | Stage and message of anything that stopped the lifecycle early, each with its `kind`, `class` and `transient`, and the refusal's structured `details` when it has them: a `remaining_obligations` teardown refusal lists `details.remaining`, one row per allocation that still holds something, with its `allocation`, `asset` and each non-zero amount as a decimal string (`supply_scaled`, `debt_scaled`, `collateral_enabled`, `allowance`, `user_configuration`, `supply` and `debt` on Aave and Spark; `principal`, `collateral`, `allowance`, `supply` and `debt` on Compound; `supply_shares`, `borrow_shares`, `collateral`, `allowance` and `debt` on Morpho)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `failure_class`       | The `class` of the admission refusal, else of the first failure; null when nothing failed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `passed`              | Admission passed, the deployment closed, the evidence is clean and nothing failed                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `status`              | `entered` when `passed` and an entry executed, `not_entered` when `passed` but no entry executed: the strategy only held, or its entry's request was cancelled (admission and teardown alone were exercised), else `failed`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `duration_seconds`    | Wall time of the whole command                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |

The exit code is 0 exactly when `passed` is true.

While it runs, the test run's coordinator log (`logs/coordinator.log` in the fork workspace) is a progress stream of JSON lines; production and hosted coordinators never write the three below. Once, when the coordinator starts, a `run_metadata` line names what the run's amounts and venues read as, from the resolved configuration: `assets` maps each verified CAIP-19 asset to its `symbol` and `decimals` (a chain's native token carries decimals only) and `venues` maps each allocation to its connector. Each `decision_evaluated` line also carries `reason`: the text the strategy passed as `context.decision(..., reason=...)` with a hold or an intent, cut to 300 characters, or null when it stated none. After each transaction outcome is committed to the checkpoint, a `transaction_executed` line states it: `time` (the coordinator clock at commit), `allocation`, `operation` (the connector's operation kind, such as `approve`, `swap`, `supply`, `borrow`, `repay`, `withdraw`, `revoke`, `mint` or `burn`), `success`, `tx_hash`, `gas_paid` (`asset` and base-unit `amount` paid by the submitter), `changes` and `revert_reason` (cut to 300 characters, or null). `changes` lists the fund's nonzero token balance changes as CAIP-19 `asset` and signed base-unit `amount` strings, negative when spent: the connector's receipt-derived execution effects, the amounts the fund inventory is updated with. Approvals, revokes and reverted transactions have none, and a connector whose effects cannot be read logs none rather than an estimate. A coordinator stopped between the commit and the line loses that line; it never logs a transaction it did not commit.

Each refusal and failure carries a structured `kind`, taken from its source rather than its message: the error kind of a refused input, the supervisor's failure kind, the error kind of the refusal a stopped coordinator logged, the kind of a fork keeper's data error, or else the stage or condition that ended the lifecycle (`decision_timeout`, `admission_timeout`, `coordinator_stopped`, `keeper`, …). Its `class` is `infra` for the kinds whose cause lies outside the strategy (`rpc`, `http`, `transport`, `unavailable`, `gateway_unavailable`, `timeout`, `rate_limit`, `fork_unavailable`, `unsupported_on_fork`, `admission_timeout`, the coordinator holds that retry without stopping, `strategy_worker_timeout` and `transient_read_hold`, and the stops a wall clock or the harness caused: `interrupt`, `decision_timeout`, `teardown_timeout`, `worker_timeout` (the preloaded strategy worker did not answer during setup, recorded with the supervisor's retryable `start` failure)) and `strategy` for every other kind, including one not listed, and for a contract revert, which keeps the `rpc` kind but is marked `reverted` by the gateway. `transient` is true only for an `infra` failure its source marked transient, one the same run can pass unchanged; `decision_timeout`, `teardown_timeout`, `worker_timeout`, `strategy_worker_timeout` and `transient_read_hold` are, `interrupt` and `unsupported_on_fork` never are.

A coordinator that keeps a `strategy_worker_timeout` or `transient_read_hold` hold (its strategy worker timing out on a starved host, or a provider read the gateway marked transient) retries without stopping, so the test run reads its log: once the hold has lasted 300 seconds without its recovery event, the decision or teardown wait stops with a failure of that kind, its reason in the message and the hold's `kind`, `attempt` and `elapsed_seconds` as `details`. A briefer hold that recovers does not stop the lifecycle. When a wait does run out, a hold the coordinator is still on, however brief, names the failure instead of `decision_timeout` or `teardown_timeout`; without one the timeout keeps its own kind. After a lifecycle that settled no decision, teardown waits at most 180 seconds instead of 900, so the verdict is written within a 20-minute budget; its `teardown_timeout` message says the wait was bounded. A caller can therefore re-run once only when `failure_class` is `infra` and that failure is `transient`, without reading the message.

A local fork can only execute EVM bytecode. Contracts a chain's node implements natively, such as Base's B20 stock tokens whose code is the single byte `0xef`, resolve on mainnet but fail every call on the fork. `check --fork --online` refuses them in its `fork_compatibility` stage with code `unsupported_on_fork`. Fork setup checks the fork nodes before any funding, and again when it reuses a funded fixture, and stops `failed` with the failure `unsupported_on_fork`, naming every such contract. No retry or resume can pass it, so status offers `shutdown` instead of `resume`, `almanak run` exits 1 and the verdict's refusal carries `"error": "unsupported_on_fork"`. This is a limitation of local fork simulation, not of the strategy, which can still be exported and deployed.

Parameters the strategy's own `validate_config` refuses (see [authoring](https://sdk.docs.almanak.co/authoring.html#declaring-capabilities)) stop setup `failed` with the final failure `invalid_config` and the author's message and line as the diagnostic. Status offers `shutdown`, hosts do not retry it, and the verdict's refusal carries `"error": "invalid_config"` with `class` `strategy`. `check --online` fails its `strategy` stage with `"error": "invalid_config"` and code `strategy_config_refused`.

A fork node starts at the fork block's own base fee without Anvil's 1 gwei minimum tip, so its gas quote follows the forked chain's market and a mainnet-calibrated `strategy.gas` fits. On chains whose fee history reports no tips (BNB), the node starts at the upstream gas price instead.

## Production

```
uv run --locked almanak wallet create --account treasury --output /private/operator
uv run --locked almanak check ./my-strategy --production --online \
  --gateway-config /private/operator/production.yaml --env-file /private/providers.env
uv run --locked almanak run ./my-strategy --production \
  --gateway-config /private/operator/production.yaml \
  --env-file /private/providers.env --output /private/runs/live
```

For Safe/Roles accounts the production online check also reports a `permissions` block: per Safe, whether its authority is installed and, when it is not, the Safe Transaction Builder batch the owner executes. It is generated exactly as admission generates it, so the owner can install permissions before the run exists. Admission still generates and verifies the installed authority.

The author file carries the economic gas policy under `strategy.gas`. Resolution retains this policy in every mode. Identified forks enforce the same economic bounds through explicit operator `fork.gas` ceilings and generated fixture payers. Missing or incompatible ceilings are refused before fixture setup. Fork runs on Base and Optimism refuse authored gas caps because their L1 data and operator fees have no verified bound on a fork; gasless forks are unaffected. Production runs on those chains accept authored caps: the gateway bounds the L1 data and operator fees at quote time within the payer ceilings and reconciles the exact charges from each receipt. Fee-only replacement remains opt-in and limited to its supported Ethereum execution path. The operator file carries the production authority: execution networks, independent payer ceilings that the authored policy must fit, the funded account bindings and the signing references. The online check verifies policy compatibility through the read-only gateway and never opens signing files. EOA funds are their own submitters. Safe funds use delegated Roles operators; owners remain distinct and are never a fallback signer.

Encrypted keystores must match the named submitters. Interactive passwords are passed ephemerally and are not saved in manifests. An explicit private `password_file` in the operator signing section is an operator choice. A gateway restart requests a password when required. Credentials must be operator-owned private files outside sandbox readable paths.

A hosted operator may replace `--key-file` with `--signer-file`, a private JSON binding whose `kind` names an installed signer plugin (entry-point group `almanak.signers`). The gateway then holds no key material: every envelope a remote signer returns is decoded and checked against the requested transaction and the bound submitter before it is broadcast or retained. The two flags are exclusive and a remote signer takes no keystore password.

Missing Safe permissions are exported for owner review and installation. The supervisor reports action required and does not install them. The Safe must be a canonical Safe 1.4.1 (L1 or SafeL2) whose only module is its Roles 2.1.0 proxy, with no guard and either no fallback handler or Safe's CompatibilityFallbackHandler 1.4.1. When the operator's role was assigned in the Roles setup transaction, the exported bundle does not assign it again. After owner action, explicit resume verifies installed authority before admitting execution. Installed authority is read from Roles storage at a pinned block: every grant the strategy needs must be installed exactly, with the operator in the role and no default role. Grants beyond the strategy's own, other members of its role and other roles of the operator are not detected; Safes are dedicated to one strategy. The authority check before every plan, signature and broadcast replays the previous check's block-pinned reads in batches at its own observation block. Production advances on canonical mined receipts and audits recorded canonicality before further work. Invalidated history halts with pending evidence retained.

## Reporting and advanced commands

Text is the default on terminals; redirected output is JSON. Override with `--format text`, `--format json` or `--json`. Offline reports never obtain credentials or contact a provider. They distinguish retained status from a fresh online audit and preserve fund/payer scope, unavailable values and simulation assumptions. Custom dashboard failure remains visible without hiding the report.

Explicit process management remains under `service gateway`, `service fork`, `service start`, `service resume`, `service status` and `service teardown`. `permissions generate|verify` uses an authenticated gateway.

### Market resolution

`almanak markets --out .almanak/markets.json` reads, at one pinned block per chain (`--block`, default `latest`; the record's `blocks` names each chain's number and hash, and every market on a chain resolves at that block), what each market in `config.yaml` resolves to: its contracts, tokens and ids, each under the resolver field that names it (`pool`, `currencies`, `asset`, `oracle`, ...), with every token's symbol and decimals, for the user to check on an explorer before approving the strategy. It reads no prices, so it is quick; names that resolve to the same contracts (two legs of one lending market) share one entry, and a market without a resolver is listed under `skipped`. It takes the session's read gateway like `almanak ax`.

### Protocol observations

`almanak ax` reads one protocol observation at one pinned block. Every subcommand takes one transport and its own request arguments after the subcommand, and every one but the two HyperCore subcommands takes `--block` (default `finalized`):

| Subcommand          | `--connector`                                                                | Request                                                                                                                                 |
| ------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `pool`              | `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve` | `--chain` and `--pool`, or `--config`; optional `--depth-bps` for the first three                                                       |
| `pool-price`        | `uniswap_v4`                                                                 | `--chain`, `--currency0`, `--currency1`, `--fee`, `--tick-spacing` and optional `--base`, or `--config`                                 |
| `lending-reserve`   | `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`              | `--chain` and the connector's selector flags, or `--config`                                                                             |
| `lending-position`  | same as `lending-reserve`                                                    | the same selection, plus `--account`                                                                                                    |
| `perp-position`     | `gmx_v2`                                                                     | `--config`, `--account`                                                                                                                 |
| `perp-markets`      | `gmx_v2`                                                                     | `--chain` and `--query`, or `--config`                                                                                                  |
| `balance`           | none                                                                         | `--asset` (CAIP-19 asset type), `--account` (repeatable)                                                                                |
| `resolve`           | none                                                                         | `--chain`, `--query` (symbol or address); optional `--limit`                                                                            |
| `dex-pools`         | none                                                                         | `--chain`, `--token`; optional `--quote`, `--connector`, `--limit`, `--offset`, `--depth-bps`                                           |
| `lending-markets`   | `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`              | `--chain`; optional `--asset` (required for `morpho_blue`), `--collateral`, `--include-unlisted` (`morpho_blue`), `--limit`, `--offset` |
| `hypercore-markets` | none                                                                         | optional `--query` (a coin), `--limit`, `--offset`, `--include-delisted`; no `--block`                                                  |
| `hypercore-account` | none                                                                         | `--account` (repeatable); no `--block`                                                                                                  |

`--config` names a JSON request file whose fields are the connector's request model; for the lending subcommands it is the market selection an author writes under `markets:`. `--block` accepts a decimal block number, `finalized` or `latest`; any other value is refused before a transport opens. The reader first resolves the selector to one header, and every later read of the observation names that header's hash with `requireCanonical`, so the output's `block` records the concrete number and hash all values came from. A `latest` observation can be reorganized away; a later read against a block that is no longer canonical fails instead of mixing blocks.

Each observation reads through exactly one transport: `--env-file` resolves the provider credentials in the calling process, or `--gateway-url` with `--gateway-auth-file` reads through `service read-gateway`, a read-only observation gateway that keeps the credentials in its own process and serves only whitelisted JSON-RPC reads and the discovery lookups below over authenticated loopback. Hosted sessions that must not receive a provider key use the gateway transport. Without either flag, `ax` reads through the gateway named by `ALMANAK_AX_GATEWAY_URL` and `ALMANAK_AX_GATEWAY_AUTH_FILE`, which the hosted worker sets; a flag always wins, and with neither the command is refused. The inline `pool-price` key names a pool on the reviewed deployment for the chain, hookless unless `--hooks` names a reviewed hook (whose dynamic-fee profiles take `--fee 8388608`). Use each command's `--help` for the complete argument list.

`ax lending-reserve` resolves a lending market exactly as the author loader does and reports the price sources the protocol uses at the pinned block, with a paste-ready `config` block:

```
almanak ax lending-reserve --connector aave_v3 --chain eip155:1 \
  --asset eip155:1/erc20:0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 \
  --block finalized --env-file .env
```

Selector flags use the author field names: `--asset` and optional `--pool` for Aave V3 and SparkLend, `--spoke` and `--reserve-id` for Aave V4, `--comet` and `--asset` for Compound V3, `--market-id`, `--leg` and optional `--core` for Morpho Blue. The result carries `resolution`, `price_sources`, `config` and the reserve `observation`. For Aave V3 and SparkLend, `config.allocation` holds the `oracle` and `price_source` an allocation on that market states. For Aave V4, `config.debt` holds the `oracle` and `debt_price_source` of a debt on that reserve, and `config.collateral_price_source` is the entry for that reserve in a debt's `collateral_price_sources`. Compound V3 and Morpho Blue resolve their feeds and oracle themselves, so their `config.market` adds them only as optional assertions. A deployment the connector has not reviewed is refused. A reserve state read the protocol refuses at that block leaves `observation` null with `observation_error`, and the price sources are still reported.

Beside the raw `observation`, `rates` states the reserve's annual rates from that same read, with no read of its own: `supply_apr`, `supply_apy`, `borrow_apr` and `borrow_apy` as decimal ratios per year (`0.05` is 5%), rounded half-even to 12 places from exact arithmetic, `compounding`, `available_liquidity` in the asset's units and the `raw` protocol values they come from. They are the protocol's rates at the pinned block, not a forecast.

| Connector              | Raw rates                                                                                                                                | APR                                                                   | APY                                                                                                          | `available_liquidity`                                                  |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `aave_v3`, `sparklend` | `liquidity_rate_ray`, `variable_borrow_rate_ray`: per year, in ray                                                                       | rate / 1e27                                                           | (1 + APR / Y)^Y - 1 (`per_second`)                                                                           | Aave V3 `virtual_underlying_balance`; SparkLend `available_underlying` |
| `compound_v3`          | the Comet curves' per-second supply and borrow rates at its recorded utilization, as `getSupplyRate` and `getBorrowRate` answer, in 1e18 | rate x Y / 1e18                                                       | (1 + rate / 1e18)^Y - 1 (`per_second`)                                                                       | `base_available`, the Comet's base balance                             |
| `morpho_blue`          | the IRM's per-second `borrowRateView`, in 1e18; `supply_assets`, `borrow_assets`, `fee`                                                  | borrow: rate x Y / 1e18; supply: borrow APR x utilization x (1 - fee) | e^APR - 1 for each (`continuous`, as Morpho accrues): utilization and fee scale the rate before it compounds | `supply_assets - borrow_assets`                                        |

Y is 31,536,000 seconds and utilization is borrow over supply assets.

These are the rates of the asset the market lends, named as `rates.asset`: the reserve's own asset for Aave V3 and SparkLend, the Comet's base asset and Morpho's loan asset. `rates.selected_asset` states what the selected asset earns: `role` `lent` with the market's `supply_apr` and `supply_apy` when it is that asset, or `role` `collateral` with both zero when it is collateral, which neither a Comet nor a Morpho market lends. Selecting WBTC in a USDC Comet reports USDC's rates under `rates` and zero under `rates.selected_asset`. The Aave V4 reserve observation reads no Hub drawn rate or liquidity, so its `rates` is null with `rates_unavailable`. A refused observation reports no `rates`.

`risk` labels the collateral parameters the health factor and the borrow limit use, in basis points, one `collateral` row per collateral asset: `ltv_bps`, `liquidation_threshold_bps` and `liquidation_bonus_bps`. Beside them it states the formulas, at the protocol's own oracle prices:

- `health_factor`: sum(collateral_value x liquidation_threshold) / debt_value; the position is liquidatable below 1.
- `borrow_limit`: debt_value \<= sum(collateral_value x ltv).
- `projection`: for one collateral and a debt of f times its value, the health factor is liquidation_threshold / f, and the largest f a floor H admits is min(ltv, liquidation_threshold / H).

| Connector              | `ltv_bps`                                                     | `liquidation_threshold_bps`        | `liquidation_bonus_bps`                                                                       |
| ---------------------- | ------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `aave_v3`, `sparklend` | the reserve configuration's LTV                               | its liquidation threshold          | its stored bonus - 10000                                                                      |
| `aave_v4`              | the collateral factor of the reserve's current dynamic config | the same factor                    | `maxLiquidationBonus` - 10000, paid at or below `liquidation.health_factor_for_max_bonus`     |
| `compound_v3`          | `borrowCollateralFactor` / 1e14                               | `liquidateCollateralFactor` / 1e14 | (1e18 - `liquidationFactor`) / 1e14, the share an absorbed account loses                      |
| `morpho_blue`          | `lltv` / 1e14                                                 | the same LLTV                      | `liquidation_incentive_factor` - 1, where the factor is min(1.15, 1 / (1 - 0.3 x (1 - LLTV))) |

Compound V3 and Morpho Blue keep the factors on the market, so one read covers every collateral. On Aave V3, SparkLend and Aave V4 they belong to the collateral's own reserve: call `lending-reserve` on the collateral asset, not the borrowed one. Aave V3 and SparkLend values are the ordinary-mode ones; eMode and isolation are not applied. Aave V4 also reports the reserve's `dynamic_config_key` and the Spoke's `liquidation` config, and states that `collateral_risk_bps` is the risk premium the collateral adds to the debt rate, not a collateral factor. An Aave V4 risk read the Spoke refuses leaves `risk` null with `risk_error`; a refused observation reports no `risk`.

Each row's `max_borrow_fraction.by_ltv` is the LTV. Two optional flags project a borrow against that one collateral:

```
almanak ax lending-reserve --connector aave_v3 --chain eip155:1 \
  --asset eip155:1/erc20:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 \
  --borrow-fraction 0.3 --borrow-fraction 0.5 --health-floor 1.6 \
  --block finalized --env-file .env
```

`--borrow-fraction f` (repeatable, above 0 and at most 1) adds a `projected` row per fraction with `health_factor` = liquidation_threshold / f and `within_borrow_limit` (f \<= LTV). `--health-floor H` (above 1, since 1 is the liquidation line) adds `max_borrow_fraction.at_floor` = min(LTV, liquidation_threshold / H). Values are exact decimals rounded down to 4 places, so neither overstates the margin. They are static ratios at the pinned block: no price move, interest or other collateral is included, and a position with several collaterals combines them as `health_factor` states.

`ax balance` reads every repeated `--account` at the same pinned block.

`ax pool` reads a Uniswap V3, PancakeSwap V3 or Aerodrome Slipstream pool from only its chain and address, given inline or as a `{"chain", "pool"}` file. Both forms validate as the same request:

```
almanak ax pool --connector uniswap_v3 --chain eip155:4663 --pool 0x... \
  --block latest --depth-bps 200 --gateway-url "$URL" --gateway-auth-file "$TOKEN_FILE"
```

The connector's resolver reads the pair, fee and tick spacing from the pool and refuses a pool whose factory is not the connector's reviewed deployment on that chain, naming the factory the pool reports. The result carries `support` (`verified`, or `candidate` for a chain declared only for qualification), the resolved identities, the pinned observation and the spot price of currency0 in currency1, which is `null` when the pool has no active liquidity. With `--depth-bps`, `depth.to_lower` is the currency1 the pool pays out before its price falls by that many basis points and `depth.to_upper` the currency0 it pays out before its price rises by as many; both exclude swap fees and are raw integer units. The walk reads every initialized tick inside the band, and a band wider than 5000 basis points or holding more than 2048 initialized ticks is refused rather than truncated.

For an Aerodrome classic or Curve pool, `ax pool` reports the connector's resolution (the pool's coins, decimals and, for Curve, its family and revision, verified against the reviewed factory or registry) and the pool's prices. An Aerodrome classic report also carries `reserves`: each currency's stored reserve, the pool's depth, with `asset`, `amount` in base units, `decimals` and the decimal `quantity`; a Curve report carries its balances in `pool_state`. `--depth-bps` is refused for them. Every price, here and for the V3-style pools, is whole units of `quote` per whole unit of `base`, fees excluded, and names its `kind`:

| Pool                        | Field                               | `kind`                                                                                                                                                       |
| --------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| V3-style, Aerodrome classic | `price`: currency0 in currency1     | `spot`, the marginal price at the pinned state (Aerodrome from its stored reserves, volatile or stable)                                                      |
| Curve StableSwap            | `prices`: each later coin in coin 0 | `spot`, from the invariant as the pool's `get_p` computes it                                                                                                 |
| Curve TwoCrypto, TriCrypto  | `prices`: each later coin in coin 0 | `last_spot` (`last_prices`, the marginal price after the pool's last swap or liquidity change, not an execution price) and `oracle` (`price_oracle`, an EMA) |

An Aerodrome classic pool without liquidity on a priced side reports `null`.

A Curve pool also reports `pool_state`: `balances` and `total_supply` (raw amounts with each asset and its decimals), the `fee` (Curve's 1e10-scaled integer and its basis points; for TwoCrypto and TriCrypto the `mid_fee`, `out_fee` and `fee_gamma` between which the swap fee moves), `amplification` (A at the pinned block), and `virtual_price` (1e18-scaled; StableSwap computes it from the invariant as `get_virtual_price` does, the crypto pools report their stored value). StableSwap also reports `offpeg_fee_multiplier` and TwoCrypto and TriCrypto `gamma`.

A Curve pool is funded when every coin balance and its LP supply are positive. An unfunded pool reports `"status": "unfunded_pool"`, a `reason` naming each empty balance, `prices: null` and only its balances and supply: no swap into it can fill and a first deposit would set its price. `ax dex-pools` reports such a pool as `refused` with reason `unfunded_pool: …`, `backtest prepare` refuses a window in which it is unfunded, and `check --online` for fork or production refuses it in its `pool_liquidity` stage with code `unfunded_pool`, read through the operator's provider at the latest block.

#### Discovery

`ax resolve` and `ax dex-pools` find candidates an author does not yet have. CoinGecko on-chain names the candidates and the chain verifies them; the vendor's liquidity and volume are reported as its figures, in USD cents, and are absent when it omits them. They need `COINGECKO_API_KEY` (a CoinGecko pro key) in the env file of the calling process or of the read gateway.

```
almanak ax resolve --chain eip155:8453 --query cbBTC
almanak ax dex-pools --chain eip155:8453 \
  --token eip155:8453/erc20:0x4200000000000000000000000000000000000006 \
  --quote eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 --depth-bps 100
```

`ax resolve` takes a symbol or an ERC-20 address. A symbol is matched against the tokens in the pools the vendor's search returns, case-insensitively, by the symbol the vendor lists or the token's own `symbol()`, and the candidates are ranked by vendor liquidity. An address the vendor does not list is still reported, with its vendor fields null. Every reported candidate is read on chain: `decimals()` verifies the contract and `symbol()` is reported as `onchain_symbol`, null when the token does not answer it as a string. `source.complete` is false, with the reason in `source.limitation`, when the vendor has not indexed part of the lookup or the search filled the two pages (40 pools) read, since a candidate may then be missing. `recommended` names the leading candidate only when no lookup went unindexed, it is verified, its `onchain_symbol` matches the symbol the vendor lists, and it is either the only candidate or its liquidity is measured at least ten times the next one's measured liquidity, and its own is nonzero; otherwise it is null and the author chooses. A search cut at 40 pools still recommends: liquidity is each token's total across all its pools, and a token large enough to rival the leader has pools among the first 40. A smaller same-symbol token can still be missed; `source.limitation` reports the cut. A symbol is never identity: the author states the CAIP-19 asset. Native assets are `slip44` identities and are not looked up.

`ax dex-pools` lists the pools the vendor reports for an ERC-20 `--token`, optionally only those pairing it with an ERC-20 `--quote`, in the vendor's order (24-hour volume with liquidity), not re-ranked, so an offset continues the vendor's list. The vendor offers no snapshot or cursor, so its order can shift between calls as volumes move. `--limit` (default 10, at most 20) and `--offset` page through them; `page.matched` counts the matches in the pages read and `page.has_more` says whether a later offset returns more. The vendor's venue label only selects a connector; a label the SDK does not know is matched by the pool's own `factory()` against each connector's reviewed factory, before `--connector` filters, so a factory-matched pool is kept. Every reported pool is resolved by its connector, which refuses a pool from an unreviewed deployment and names why. The vendor's pair is only a filter: a pool whose resolved currencies do not include `--token` (and `--quote`, when given) is refused with `vendor_mismatch`. Each pool reports a `status`:

| Status        | Meaning                                                                                                                                                                    |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verified`    | The connector resolved the pool at the pinned block; `market` is a paste-ready `markets:` entry                                                                            |
| `refused`     | The connector refused the pool, with its reason, the pool it resolved does not trade the requested pair (`vendor_mismatch`), or a Curve pool is unfunded (`unfunded_pool`) |
| `unverified`  | Uniswap V4 pools whose key neither the vendor's pair and fee nor the pool's `Initialize` event states, including a malformed event                                         |
| `unsupported` | No SDK connector for the venue: neither its label nor a reviewed factory names one                                                                                         |

A Uniswap V4 key is first derived from the vendor pair and the fee in its pool name as a hookless pool with the standard tick spacing for that fee; when its hash differs from the vendor's pool ID, the reviewed PoolManager's `Initialize` event for that ID states the exact key. The connector then admits the key exactly as it does for a strategy: a hooked pool is verified only when its hook has a reviewed profile, the profile's permission bits and fee mode, and code equal to its pin, and the row adds a `hook` block with the `profile`, hook `address`, `pin: match` and the current `terms` (a Fables or StablePairHook LP fee per direction, or a Pons hook fee and creator tax in bps). Any other hook is refused with the connector's reason. A pool holding the chain's native coin (ETH, POL, BNB or AVAX) keeps the native coin in its PoolKey and is verified with the fund holding its wrapped token (WETH, WPOL, WBNB or WAVAX): the reviewed Universal Router unwraps the input or wraps the output within the swap, and the reviewed PositionManager does the same for liquidity. The vendor files these pools under the native coin, so a `--token` of the wrapped token also reads the vendor's native-coin list, a page of each list in turn, and a `--quote` of the wrapped token keeps them. The native coin itself (`slip44`) is accepted as `--token` or `--quote` and asks for its wrapped token, so it returns the same wrapped and native pools. If the vendor has not indexed one of the two lists, the other is still read and the source is incomplete. The row's `markets:` entry names the native currency; keep it native, since a selection naming the wrapped token is a different pool. A row the chain did not verify reports the vendor's claimed currencies as `unverified_vendor_currencies` (the native coin as `slip44`, `null` when omitted): a claim that tells a native pool from its wrapped twin, not a verified identity. Native pools on a chain without a reviewed router route, and native/wrapped-native pools, are refused with the connector's reason. Vendor USD figures are cents, and absent when missing, malformed or too large to state. `source.complete` is true only when the vendor's list ended within the pages read. One call reads at most five vendor pages, the token's 100 busiest pools (shared with the native coin's list for a wrapped-native token), and the vendor does not index every pool; an incomplete list says so in `source.limitation`, and a pool missing from it may still exist. A token the vendor has not indexed is reported the same way, never as a complete empty list. A known pool, for example one a reference strategy names, is checked directly with `ax pool` or `ax pool-price`.

`ax lending-markets` lists a lending protocol's markets on a chain from the protocol's own records at the pinned block, or, for Morpho Blue where Morpho's API indexes the chain, from that API. None of them needs a key:

```
almanak ax lending-markets --connector aave_v3 --chain eip155:43114
almanak ax lending-markets --connector morpho_blue --chain eip155:8453 \
  --asset eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
```

| Connector              | Record                                                                                                                                                                                                                                                                           | Order and filters                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `aave_v3`, `sparklend` | the reviewed Pool's `getReservesList()` (`source.record` `pool_reserves`)                                                                                                                                                                                                        | the Pool's order; `--asset` keeps that reserve                                                                                                                                                                                                                                                                                                                                                                                                         |
| `aave_v4`              | the reserves of each Spoke the chain's Hubs serve (`getSpokeAddress`, `getReserve`; `hub_spokes`)                                                                                                                                                                                | Hub, Spoke and reserve order; `--asset` keeps reserves of that asset; a Spoke that reverts `getReserveCount()` holds no reserves and is counted in `source.without_reserves`; one whose count cannot be read for any other reason (transport, throttling, an undecodable answer) is named in `source.unreadable`, as is a Hub whose Spokes cannot be enumerated (`hub`) and a reserve that cannot be read (`spoke`, `reserve_id`); the rest still list |
| `compound_v3`          | the Comets the chain's Configurator recorded in `CometDeployed` events (`comet_deployments`)                                                                                                                                                                                     | deployment order; `--asset` keeps Comets whose base or collateral it is, `--collateral` those accepting it                                                                                                                                                                                                                                                                                                                                             |
| `morpho_blue`          | Morpho's API on Ethereum, Optimism, Polygon, HyperEVM, Robinhood, Base and Arbitrum (`source.record` `morpho_api`, with `source.skipped` counting rows it answered in another shape); elsewhere the markets the reviewed core recorded in `CreateMarket` events (`core_markets`) | `--asset` (the loan asset) is required; supplied markets only, most supplied in the loan asset first; `--collateral` narrows; from the API, only markets Morpho lists unless `--include-unlisted` (`source.listed_only`, `source.curation` `morpho_api`); from the core's events, every supplied market (`listed_only` false, `curation` null)                                                                                                         |

A record only names a candidate. Each reported market is resolved by its connector as `lending-reserve` resolves it, and reports `status` `verified` with the same `config` block `lending-reserve` returns, or `refused` with the resolver's reason. A Compound V3 selection names the asked-for `--asset`, base or collateral, or else the base asset; a Comet whose assets cannot be read is named in `source.unreadable`. A Morpho selection is the market's loan leg; its collateral leg uses the same `market_id`. From the API, a Morpho row reports only its `market_id` and the asked-for `loan_asset` as the market's own; Morpho's view is under `vendor`: `collateral_asset`, `lltv`, `supplied` and `borrowed` in the loan asset, `supplied_usd` and `borrowed_usd`, `supply_apy`, Morpho's `listed` curation and its `warnings` (for example `not_whitelisted` or `unrecognized_collateral_asset`). They come from Morpho's current index, not the pinned block, are absent where it omits them, and are never strategy inputs; a verified row's `config` is the resolver's reading at the pinned block, and a market whose own loan or collateral asset contradicts the row is refused with `vendor_mismatch`; a row naming no collateral claims an idle market, whose collateral is the zero address. Anyone can create a Morpho market, and an unlisted one can show supply looped against worthless collateral that still passes the resolver's revision checks, so the API listing keeps to Morpho's listed markets by default; `--include-unlisted` adds the rest, each with Morpho's warnings. The core's events carry no curation, so a listing from them is not filtered: its `source` reports `listed_only` false and `curation` null, and `--include-unlisted` changes nothing there. From the core's events, `supplied` and `borrowed` are the totals the core last recorded, before interest accrued since; unsupplied markets are counted in `source.without_supply`, and a listing that would read more than 5,000 markets is refused. A provider that caps `eth_getLogs` ranges refuses a core's whole history in one request; ours does on Avalanche, where the listing then fails with that reason and `lending-reserve` still verifies a known market ID. Every reported asset is an object of its CAIP-19 `asset` and its on-chain `symbol()`, null where a token does not answer one as a string; the symbol is a label, the asset is the identity. A resolver checks the deployment's revisions, not whether a reserve takes deposits or loans now, so Aave V3 and SparkLend rows also carry the reserve's own `reserve` flags (`active`, `frozen`, `paused`, `borrowing_enabled`, `ltv_bps`, `liquidation_threshold_bps`, `liquidation_bonus_bps`) and Aave V4 rows its `frozen`, `paused` and `borrowable`; a frozen reserve, or one with no loan-to-value, cannot back a borrow even when `verified`. A flag read that fails reports `reserve` as null. Compound V3 rows add `collateral_factors`, one per entry of `collateral_assets` in the same order, and Morpho rows add `liquidation_incentive_factor` beside `lltv` (under `vendor` when the LLTV is Morpho's), each as `lending-reserve`'s `risk` labels them. A `verified` row also carries the `rates` that `lending-reserve` reports for that market, from the same reserve read; a read the protocol refuses leaves `rates` null with `rates_error` and the row still verified, and an Aave V4 row reads nothing for them (`rates_unavailable`). A row from Morpho's API keeps Morpho's own `vendor.supply_apy` beside the on-chain `rates`. `--limit` (default 10, at most 20) and `--offset` (at most 500) page through the matches, with `page.matched` and `page.has_more`. Only the page's markets are resolved. Without a filter, an Aave V4 listing reads only the page's reserves and a Compound V3 listing only the page's Comets: `matched` counts every one the record names, and a reserve or Comet that cannot be read is named in `source.unreadable` only when it falls on the page. With `--asset` or `--collateral`, each is read to count the matches, and one that cannot be read is named and not matched. Each event history is asked for from block zero in one request; a provider that refuses that range or result size fails the listing.

#### HyperCore

Hyperliquid's HyperCore is a venue of HyperEVM (`eip155:999`): a HyperCore account is the same address on that chain. `ax hypercore-markets` and `ax hypercore-account` read it from Hyperliquid's own `/info` API, which needs no key:

```
almanak ax hypercore-markets --query BTC
almanak ax hypercore-account --account 0x...
```

The API has no as-of query, so these two take no `--block`: an answer is HyperCore's state when the API served it. Each report names every query it made in `source` or `sources`, with the time its answer was received in milliseconds (`received_at`), never a block.

`hypercore-markets` reads the first perpetual dex's `metaAndAssetCtxs`. Each market reports its `target` (`dex` and `coin`), its `asset` index (its position in the dex's universe, the number every order carries), the venue's `meta` entry (`szDecimals`, `maxLeverage`, `marginTableId`, and `isDelisted` or `marginMode` where the venue sets them), the `margin_tiers` of its margin table, null when `meta` lists no table of that ID, and its `context`: mark, oracle and mid prices, `funding`, open interest and volumes, as the venue writes them. `--query` reports the one market `meta` names so, or names so apart from letter case. Without it, tradable markets are listed by the venue's 24-hour notional volume, most traded first; `--include-delisted` adds delisted ones. `--limit` (default 20, at most 50) and `--offset` (at most 500) page through them; `matched` counts them all. HIP-3 dexes are not read.

`hypercore-account` reads each `--account`'s `role` (`missing` until HyperCore first credits the address; an approved agent reports the account it trades for), its account abstraction mode (`abstraction`), its perp margin and positions, and its spot balances, its open orders on the first dex (`open_orders`, spot included, whoever placed them, each with its `cloid` when it has one) and the agents it approved (`agents`, each with `validUntil`, its expiry in milliseconds, or null). Under a unified account or portfolio margin, spot holds every balance and the perp state is not meaningful: `perp.state` is then `not_meaningful`, otherwise `as_reported`. Each position carries its market's `asset` index. Each spot balance names its HyperEVM `asset`: the ERC-20 the token is linked to, the USDC ERC-20 for USDC (which HyperCore links to Circle's CoreDepositWallet), the native asset for HYPE, or null for a token linked to no HyperEVM contract, which has no identity. Amounts are decimal strings in the token's HyperCore units (`wei_decimals`).

Every answer must match the shape recorded from mainnet for its query; an answer with a field added, missing or written differently is refused as `malformed` rather than read, and an answer for another market, account, order or time range than asked is refused as `source_mismatch`. The read gateway serves the same queries as its `info` operation, and `ALMANAK_GATEWAY_HYPERLIQUID_WEIGHT_PER_MINUTE` caps them by the API's own request weight: 2 for an account's perp or spot state, 60 for its role and 20 for the rest, plus one per 20 items a fill, funding or ledger page returns. Before sending, such a page reserves the most it can cost (120 for fills, 45 for funding payments, 120 for ledger entries) and settles to the count it returned. The ledger charge is the SDK's conservative choice: the API documents no per-item weight for ledger entries. These reservation limits are separate from reconciliation's truncation thresholds (2000 fills, 500 funding payments, 500 ledger entries), which mark a page that may be cut short.
# Additional Guides

# Almanak Code

Almanak Code is Almanak's AI coding assistant. Every published wheel carries the binary for its own platform, so it is available as soon as the SDK is installed.

## Running it

Running `almanak` with no command starts the assistant:

```
almanak
```

Any command this CLI does not define is passed to it unchanged, so its own options work directly:

```
almanak code -p "supply WETH to Aave on Ethereum"
```

A command the SDK does define always belongs to the SDK, and so does anything starting with an option:

```
almanak check strategies/aave_v4_weth_supply   # SDK
almanak --help                                 # SDK help
almanak --unknown-option                       # SDK error, exit 2
```

## The Python LSP

The assistant drives `pyright-langserver` from `PATH` and falls back to `npx` when it is absent. To install it alongside the SDK:

```
uv add "almanak[code]"
```

## Platform coverage

Wheels are published for macOS on arm64 and x86_64, Linux on arm64 and x86_64, and Windows on x86_64. Installing from the source distribution, or on any other platform, gives a working SDK with no assistant; `almanak` then reports `almanak_code_unavailable` and exits 1.

The pinned release is recorded in `.almanak-code-version`. Nothing is downloaded at install time or at run time.

## The SDK v3 authoring lane

Forwarding sets `ALMANAK_SDK_GENERATION=v3` and `ALMANAK_SDK_V3_PYTHON` to the running interpreter unless they are already set, so the assistant works in its SDK v3 lane: its prompts, sub-agents and strategy workflow target a `strategy.py` plus one `config.yaml`, validated with `almanak check --json`, and its `sdk-docs`/`sdk-src` references resolve to this installed package. Worked examples are listed with `almanak support --references`.

Inside an assistant session the SDK parser always wins, so a command line this CLI does not define, such as a V2 `almanak strat new`, reports the commands it does define rather than reopening the assistant.

`almanak ax` exists in both generations with different meanings: V2's `ax` runs DeFi actions such as swaps, while this SDK's `ax` is read-only protocol observations (`pool`, `pool-price`, `lending-reserve`, `lending-position`, `perp-position`, `perp-markets`, `balance`), read through `--env-file` or a `service read-gateway` sidecar. V2 `ax` command lines, such as `almanak ax swap`, are rejected by the SDK parser.

# Daily SDK QA

The bench runs the actual reference strategies through the existing fork harness and backtest CLI. It validates coverage against the trusted connector support contracts before starting any provider work. The [design contract](https://sdk.docs.almanak.co/internal/blueprints/18-daily-qa-bench.html) owns its scope.

The accepted [chain and connector expansion](https://sdk.docs.almanak.co/internal/blueprints/19-chain-connector-expansion.html) has a separate [96-cell delivery inventory](https://sdk.docs.almanak.co/internal/blueprints/19-chain-connector-inventory.json). It records candidates and missing rollout evidence, not additional supported capabilities. The current QA plan still derives support from trusted connector declarations. New pairs require actual reference outcomes and prepared offline backtests before support promotion. The [composed borrowing/trading report](https://sdk.docs.almanak.co/internal/reports/2026-09-07-composed-borrowing-and-trading.html) retains the current shared-inventory, process-recovery and historical evidence.

## Run locally

Install the locked environment and hook, then inspect the complete matrix:

```
uv sync --locked
uv run --locked pre-commit install
uv run --locked python -m scripts.qa_bench --plan --output artifacts/qa-plan
```

Run all cases with explicit read-only provider bindings:

```
make fixtures
uv run --locked python -m scripts.qa_bench \
  --env-file .env --output artifacts/qa-daily
```

`make fixtures` places the pinned evidence copies under `tests/fixtures/evidence/` from the local evidence tree. The GMX keeper drivers read those copies, so run it before the bench or any manual `pytest --fork-env` run in a fresh checkout and after the manifest changes. `make qa-daily` runs it first.

Use a new output directory for each invocation. Existing output is never reused as evidence for a new run. Plan mode performs no provider calls. Linux needs Anvil and working Bubblewrap/unprivileged namespaces. macOS uses sandbox-exec; Full QA runs locally with retained evidence. Hosted CI runs lint, types, the offline suite with evidence-dependent modules skipped, the CRAP gate over that reduced coverage scoped to the functions a pull request changed, the 87% statement coverage floor over the whole `src/` directory from that same reduced coverage, and CLI smoke checks on Linux and macOS; it does not run the full QA suite. The `Fork Qualification` workflow (`.github/workflows/fork_qualification.yml`) runs the discovered fork lifecycle and, where the checked-out connectors declare production EOA support, the production rehearsal for named references on hosted runners against the repository's provider key; it is the fork qualification a reference listed in `tests/strategy_fork/rehearsal.py::UNREHEARSED` needs before it joins the default rehearsal selection. It also runs [weekly](#hosted-weekly-fork-qualification) on `main`. Each run retains the case evidence as a workflow artifact and spends no funds. GMX keeper drivers settle requests with authentic signed reports read from the pinned evidence copies in `tests/fixtures/evidence.json`, which the workflow downloads from the staging evidence bucket before pytest. The manual `Cell QA Matrix` workflow runs one connector/chain cell's QA cases on hosted runners in parallel; see [Hosted cell matrix](#hosted-cell-matrix) for what it does and does not establish.

For a focused diagnostic run, select exact case IDs from the plan:

```
uv run --locked python -m scripts.qa_bench \
  --env-file .env --output artifacts/qa-focused \
  --case backtest/pinned/uniswap_v4_trading/economic
```

`--lane` selects offline, pytest, fork, faults, backtest or blocked. Unselected cases remain visible as skipped, so a focused run is never displayed as a passing full bench. The exit code for an explicit selection describes its selected cases. The full run exits nonzero if any required case did not pass. Unexpected pytest skips and empty test collections fail the corresponding case.

## Inputs and coverage

The current plan validates 82 connector/chain/profile/model/action obligations in 93 cases, including eight explicit blockers for unregistered rollout references. The executable cases include seven references through both Safe/Roles operator variants and EOA on pinned and newly resolved forks; twelve backtest reference/model combinations with pinned, recent and short native-cadence variants; the full offline gate; focused fork faults and multi-chain outages; support refusals; and supplemental prepared-model action checks. Counts change with declared support and reviewed proof mappings, not by hiding failed cases.

The catalog in `tests/qa/catalog.py` records exercised actions and model revisions. Backtest mode is an explicit field checked against the declared model, not inferred from its revision label. Required support comes from the existing connector composition. Adding a supported chain, profile, action or model without a proof fails plan validation. Every discovered strategy needs a catalog mapping. Supplemental model tests are labeled component proofs; they are not additional strategy lifecycles.

An unregistered candidate reference has an explicit blocked case and contributes no supported-action claims. The case remains in the full result denominator; omitting it or clearing its blocker fails catalog validation. Candidate blockers prevent a passing full bench even when all executable cases pass. Promoting its chain support requires replacing the blocker with reviewed proof mappings.

Author economic choices and named markets remain in each strategy's `config.yaml`. Backtests use a separate backtest configuration. Every reference keeps providers, account bindings and fork fixtures out of its author files: QA derives one operator file per case from `examples/operators/`, varying the pinned or fresh anchor, the Safe or EOA profile and the shared or distinct operator there, and drives its fork lifecycle through the public `almanak run`, `pause`, `resume`, `teardown` and `shutdown` commands. QA writes and validates full resolved configurations, preserving all changes as evidence:

- Fresh forks resolve one block identity per chain before case execution.
- Recent historical windows preserve the configured duration and cleanup period. The explicit source lag in `tests/qa/policy.json` is currently two hours on each chain, measured after the cleanup endpoint. Missing history is refused; the bench does not shorten the window or choose another provider.
- Native-cadence probes use a ten-minute window and ten-minute cleanup, with the author interval and explicit 120-second hold / 60-second collection parameters where those parameters apply. The ordinary pinned/recent variants retain their authored holding parameters and explicit backtest cadence overrides.
- Detailed Aave history uses an explicit debt-free author variant and an hourly override in the long runs. Its native probe has no interval override.
- The EOA rehearsal harness supplies the explicit Ethereum test gas policy in `tests/strategy_fork/rehearsal-policy.json` when a reference has no production policy. Provider selection still comes from that reference. Generated production configurations run only against identified local forks with Foundry test keys; this does not change author production authority.

Prepared backtests use a new dataset directory for cold preparation, then repeat preparation and run execution/reproduction with OS network isolation. Local IPC and loopback gateway calls remain available. Detailed history captures online reads and reproduces them in the same network isolation. Successful cases must show closure, delivered outcomes and the expected economic actions on each chain. Uniswap prepared runs also compare integer calculations with independent Quoter reads at the same sampled blocks, in both directions and several sizes.

Existing frozen fixture, recovery, integer boundary, vendor-source and model regressions run in the full offline gate. The online fault lane runs `tests/fork` and the multi-chain outage tests, including Aave liquidation/contract comparisons.

## Results and limits

`public/results.json` is the versioned complete matrix. `summary.md`, `index.html` and `junit.xml` present the same outcomes. `private/` holds working files and credentials generated by the harness. Both directories remain local. Evidence export preserves dataset bytes or refuses to export a file containing known credential material. Logs are redacted. A refused required artifact blocks the case. The source archive and hashes retain the tested implementation, including uncommitted changes; source changes during execution invalidate passing results.

`public/proof-ledger.json` links every declared connector, chain, action, account profile, model and protocol variant obligation to its scheduled case IDs. An obligation passes only when every mapped case passes. Failed, skipped, unstarted or missing cases remain outstanding, including separate fresh-input and recovery cases. The ledger preserves the tested source identity. It does not turn an undeclared candidate into supported scope or replace the 96-cell rollout inventory.

Each case has isolated journals, gateways, forks and retained evidence. Provider cases run serially initially; `--workers` controls only the offline pytest worker count and accepts `auto` (the default) or an explicit positive integer. The default runs the combined coverage/CRAP gate with `-n auto`. The fault lane uses one pytest worker. The runner does not automatically retry failures. Existing gateway collection limits retain their own explicit bounded provider retry behavior. This is deliberately conservative until measured complete daily runs establish useful concurrency and timeout settings.

Per-command records measure cold preparation, cached preparation, execution and reproduction wall time independently. Quoter comparisons and source resolution remain separate activities. The SDK's retained dataset and progress reports provide provider activity, model and cadence detail. No throughput or ETA claim is inferred from test counts.

The case timeout defaults to one hour. This is an operational bound, not a measured runtime target. A backtest case's execute and reproduce steps share that bound; dataset preparation keeps its own thirty minute bound. Timeouts, missing providers, unstarted cases and source-resolution failures remain visible. Unknown failures are labeled without guessing that the SDK or provider caused them.

Pass `--previous path/to/results.json` to write `changes.json`, comparing case status/category with the previous run. Unchanged cases are absent from this file. Supply the preceding local result explicitly. The bench sends no Slack/email messages. An absent prior artifact is reported as a fresh comparison baseline.

## Hosted cell matrix

The manual `Cell QA Matrix` workflow (`.github/workflows/cell_qa_matrix.yml`) runs one connector/chain cell's QA matrix on hosted runners, one job per case, through the same `scripts.qa_bench --case` entry point as a local run. It spends no funds and installs no production permissions: fixture accounts are public Foundry test keys on identified Anvil forks of the repository's read-only Alchemy provider. Dispatch it with a connector ID and a CAIP-2 chain, or with explicit case IDs:

```
gh workflow run cell_qa_matrix.yml --ref <branch> \
  -f connector=uniswap_v3 -f chain=eip155:56
```

Selection. `scripts/ci/cell_qa_matrix.py` selects every catalog case whose obligations name the connector on the chain, which are the cases the proof ledger maps to the cell. The `fork` and `backtest` lanes run by default and the `pytest` lane can be added. The `offline`, `faults` and `blocked` lanes are refused: the offline gate needs the retained evidence tree, `fork/faults` runs every connector's fault suite, and a candidate blocker cannot pass. A selected cell case outside the chosen lanes stays listed as not run. The reverse-fault entries are the reversal tests in `tests/fork/test_*reversal*.py` that are parametrized by one of the cell's references, or whose module declares it as `REFERENCE`. Each one runs as its own pytest job. When no such test exists, the run reports that gap and does not pass. The `nodes` input adds explicit `tests/fork` or `tests/strategy_fork` node IDs, for example a cell's recovery-foundation nodes. An unknown case, an empty cell and more than 256 entries are refused.

Execution. Each job runs on `ubuntu-latest-l` with Bubblewrap and Anvil and installs the locked environment on CPython 3.12.13. At most eight jobs run at once. Every job is a separate deployment with its own gateway, so the per-deployment provider caps apply to each job, and the Alchemy plan limit is the only ceiling they share. Nothing is retried.

Source and input identity. Every job checks out the run's commit. Before and after its entry, each job records the commit, the bench's tree hash, the lock hash, the Python build and the tool versions. The aggregate job recomputes the tree hash from a clean checkout of the same commit. Any mismatch, or any change during a job, blocks that entry. The plan job resolves the fresh fork origins and the input time once and passes them to every job through `qa_bench --frozen-inputs`. All fresh forks in one run therefore start from the same block per chain, and all recent historical windows end at the same instant, as in one local run.

Time budgets. Fork and pytest-lane cases keep the bench's one-hour case bound, in a 90-minute job. A backtest case gets its cold-preparation budget from `tests/qa/policy.json` plus 7200 seconds. The policy budget is 1800 seconds by default and 7200 seconds for the five composed references it lists; detailed historical cases prepare nothing. The extra 7200 seconds cover cached preparation, execution, reproduction and model comparisons, and match the bound the local runbook recommends for slow windows. The job timeout adds 30 minutes for setup and upload. The largest case, a 7200-second native preparation, gets 14400 seconds in a 270-minute job, inside the six-hour hosted job limit. The planner refuses any case whose bound cannot fit. The preparation budgets are unchanged. Each job prepares its dataset cold from the provider, exactly as a local case does, so no retained dataset is needed or reused. These are operational bounds, not measured runtimes, and a timeout remains a blocked case.

Evidence. Only the bench's sanitized `public/` tree leaves the runner. The bench's export boundary redacts logs, drops credential files and refuses any other file that contains a known secret. `private/` never leaves the runner. Reversal-node files pass through the same export. Before upload, each job scans file names and bytes, including gzip members, for the provider keys. The key values come from the environment only. On a match, the job uploads nothing but its record, and the entry is blocked as unsafe evidence. The per-case source snapshot is dropped from the job artifacts. The aggregate archives one `source.tar.gz` for the verified commit. All artifacts are retained for 90 days.

Results. The `aggregate` job writes `results.json`, marked `"execution": "hosted_cell_matrix"`, and a job summary. The summary has one row per planned entry, the cell's cases that were not run and a cell-scoped proof ledger. The workflow fails unless every planned entry passed on one source. A skipped, missing, timed-out, unsafe or mixed-identity entry keeps the result `incomplete_or_failed`.

### What a hosted cell run establishes

It can satisfy the first item of the [per-cell package](https://sdk.docs.almanak.co/internal/reports/2026-09-16-scoped-promotion-amendment.html): the cell's fork and backtest cases and its reverse-fault node, passed on one commit. That commit must be the frozen integrated source, or the proof must survive the amendment's path-level renewal check. It is not the local retained-evidence gate and does not establish:

- Pytest-lane component proofs that read retained evidence, such as `model-actions/uniswap_v4/eip155:56`. They cannot pass on hosted runners; run them locally with the evidence tree present.
- The full offline suite with retained evidence, combined coverage, CRAP at most 30, Ruff, formatting and mypy (package item 2). Hosted CI's offline suite skips the evidence modules and is not a substitute.
- The recovery-foundation nodes (item 3), unless they were passed as `nodes` and do not need retained evidence. Otherwise they remain local work.
- Append-only sanitized evidence archival with checksum verification (item 4). Download the artifacts, verify their digests and seal them locally. The workflow writes nothing to the evidence bucket.
- GMX cells. Their keeper drivers settle requests with signed reports read from the pinned evidence copies, which this workflow does not fetch, so those cases fail here. The `Fork Qualification` workflow fetches them.

## Hosted weekly fork qualification

The `Fork Qualification` workflow also runs every Sunday at 03:17 UTC on `main`. The scheduled run has no inputs: `scripts.ci.fork_qualification_matrix --production-cells` selects every converted reference with a market on a connector/chain cell that the checked-out connectors declare for production (`almanak support`), and runs the lifecycle and the rehearsal for each. It refuses an empty selection and a declared cell that no reference exercises. Keeper-settled GMX references are selected like any other and replay their signed reports from the pinned copies. At most 24 references run at once.

The `select` job summary lists each cell with its references. When a scheduled run fails, the `report` job opens the issue "Weekly Fork Qualification failed on main", or comments on it if it is open, naming the jobs that did not pass; the next passing scheduled run closes it. A failed run is not proof of support, and a passing run is not a complete per-cell evidence package: it holds no reverse-fault case, recovery-foundation node, backtest or offline gate.

## Local evidence and scheduling

Generated captures under `docs/internal/reports/evidence/` and bench output under `artifacts/` are ignored by Git and remain local. Full offline QA requires the retained captures at their original paths. A fresh clone needs a local copy of those inputs; missing evidence must not be converted into passing or skipped required cases. See the [evidence policy](https://sdk.docs.almanak.co/internal/reports/index.html).

Hosted daily QA and its GitHub watchdog have been removed. There is no hosted daily coverage or automatic replacement local schedule; the only hosted schedule is the [weekly fork qualification](#hosted-weekly-fork-qualification), which covers the fork lifecycle and the rehearsal only. Operators can invoke the commands above with explicit provider bindings and retained results. Hosted CI skips evidence-dependent tests and reports that scope in its job summary.
# CLI Reference

# almanak ax

read-only protocol observations at a pinned block

```
usage: almanak ax [-h]
                  {pool-price,lending-reserve,lending-position,perp-position,perp-markets,pool,balance,resolve,dex-pools,lending-markets,hypercore-markets,hypercore-account}
                  ...
```

## almanak ax pool-price

Uniswap V4 pool spot price

```
usage: almanak ax pool-price [-h] --connector {uniswap_v4}
                             (--config CONFIG | --chain CHAIN)
                             [--currency0 CURRENCY0] [--currency1 CURRENCY1]
                             [--fee FEE] [--tick-spacing TICK_SPACING]
                             [--hooks HOOKS] [--base BASE] [--block BLOCK]
                             [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                             [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                       | Description                                                                                                  |
| --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `uniswap_v4`; required | canonical connector ID                                                                                       |
| `--config`            | Path                          | JSON request file with the full pool source                                                                  |
| `--chain`             |                               | strict CAIP-2 chain, given with the pool key flags                                                           |
| `--currency0`         |                               | CAIP-19 currency0 of the pool key                                                                            |
| `--currency1`         |                               | CAIP-19 currency1 of the pool key                                                                            |
| `--fee`               | int                           | pool fee in hundredths of a basis point                                                                      |
| `--tick-spacing`      | int                           | pool tick spacing                                                                                            |
| `--hooks`             |                               | reviewed hook address of the pool key; omit for a hookless pool (a dynamic-fee hook takes --fee 8388608)     |
| `--base`              |                               | CAIP-19 currency to price; defaults to currency0                                                             |
| `--block`             | block; default `finalized`    | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                          | private provider credentials read by this process                                                            |
| `--gateway-url`       |                               | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                          | authentication token file for --gateway-url                                                                  |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-reserve

lending reserve state, its supply and borrow APR and APY, its collateral risk parameters, its price sources and a config-ready market entry

```
usage: almanak ax lending-reserve [-h] --connector
                                  {aave_v3,sparklend,aave_v4,compound_v3,morpho_blue}
                                  (--config CONFIG | --chain CHAIN)
                                  [--asset ASSET] [--pool POOL]
                                  [--spoke SPOKE] [--reserve-id RESERVE_ID]
                                  [--comet COMET] [--market-id MARKET_ID]
                                  [--leg {loan,collateral}] [--core CORE]
                                  [--block BLOCK]
                                  [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                  [--gateway-auth-file GATEWAY_AUTH_FILE]
                                  [--borrow-fraction BORROW_FRACTION]
                                  [--health-floor HEALTH_FLOOR]
```

| Option                | Details                                                                          | Description                                                                                                                            |
| --------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `--connector`         | one of `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`; required | canonical connector ID                                                                                                                 |
| `--config`            | Path                                                                             | JSON market selection, as written under `markets:`                                                                                     |
| `--chain`             |                                                                                  | strict CAIP-2 chain, given with the selector flags                                                                                     |
| `--asset`             |                                                                                  | CAIP-19 reserve asset (aave_v3, sparklend, compound_v3)                                                                                |
| `--pool`              |                                                                                  | Aave V3 or SparkLend Pool; omit for the reviewed Pool                                                                                  |
| `--spoke`             |                                                                                  | Aave V4 Spoke address                                                                                                                  |
| `--reserve-id`        | int                                                                              | Aave V4 reserve ID on the Spoke                                                                                                        |
| `--comet`             |                                                                                  | Compound V3 Comet address                                                                                                              |
| `--market-id`         |                                                                                  | Morpho Blue market ID                                                                                                                  |
| `--leg`               | one of `loan`, `collateral`                                                      | Morpho Blue market side                                                                                                                |
| `--core`              |                                                                                  | Morpho Blue core; omit for the reviewed deployment                                                                                     |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash                           |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                                                      |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                                                       |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                                            |
| `--borrow-fraction`   | borrow_fraction                                                                  | project the health factor after borrowing this fraction (0 to 1\] of one collateral's value: liquidation_threshold / f; repeatable     |
| `--health-floor`      | health_floor                                                                     | report the largest borrow fraction keeping the health factor at or above this floor (above 1): min(ltv, liquidation_threshold / floor) |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-position

account position in one lending reserve

```
usage: almanak ax lending-position [-h] --connector
                                   {aave_v3,sparklend,aave_v4,compound_v3,morpho_blue}
                                   (--config CONFIG | --chain CHAIN)
                                   [--asset ASSET] [--pool POOL]
                                   [--spoke SPOKE] [--reserve-id RESERVE_ID]
                                   [--comet COMET] [--market-id MARKET_ID]
                                   [--leg {loan,collateral}] [--core CORE]
                                   [--block BLOCK]
                                   [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                   [--gateway-auth-file GATEWAY_AUTH_FILE]
                                   --account ACCOUNT
```

| Option                | Details                                                                          | Description                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `aave_v3`, `sparklend`, `aave_v4`, `compound_v3`, `morpho_blue`; required | canonical connector ID                                                                                       |
| `--config`            | Path                                                                             | JSON market selection, as written under `markets:`                                                           |
| `--chain`             |                                                                                  | strict CAIP-2 chain, given with the selector flags                                                           |
| `--asset`             |                                                                                  | CAIP-19 reserve asset (aave_v3, sparklend, compound_v3)                                                      |
| `--pool`              |                                                                                  | Aave V3 or SparkLend Pool; omit for the reviewed Pool                                                        |
| `--spoke`             |                                                                                  | Aave V4 Spoke address                                                                                        |
| `--reserve-id`        | int                                                                              | Aave V4 reserve ID on the Spoke                                                                              |
| `--comet`             |                                                                                  | Compound V3 Comet address                                                                                    |
| `--market-id`         |                                                                                  | Morpho Blue market ID                                                                                        |
| `--leg`               | one of `loan`, `collateral`                                                      | Morpho Blue market side                                                                                      |
| `--core`              |                                                                                  | Morpho Blue core; omit for the reviewed deployment                                                           |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                  |
| `--account`           | required                                                                         | EVM account address                                                                                          |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax perp-position

GMX V2 account position

```
usage: almanak ax perp-position [-h] --connector {gmx_v2} --config CONFIG
                                [--block BLOCK]
                                [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                [--gateway-auth-file GATEWAY_AUTH_FILE]
                                --account ACCOUNT
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `gmx_v2`; required  | canonical connector ID                                                                                       |
| `--config`            | Path; required             | JSON request file for the connector                                                                          |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |
| `--account`           | required                   | EVM account address                                                                                          |

At most one of `--env-file`, `--gateway-url`.

## almanak ax perp-markets

GMX V2 market lookup

```
usage: almanak ax perp-markets [-h] --connector {gmx_v2}
                               (--config CONFIG | --chain CHAIN)
                               [--query QUERY] [--block BLOCK]
                               [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                               [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `gmx_v2`; required  | canonical connector ID                                                                                       |
| `--config`            | Path                       | JSON request file:                                                                                           |
| `--chain`             |                            | strict CAIP-2 chain, given with --query                                                                      |
| `--query`             |                            | GM share address or a label such as "BTC/USD"                                                                |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax pool

V3-style pool state and optional liquidity depth, or an Aerodrome or Curve pool's resolution

```
usage: almanak ax pool [-h] --connector
                       {uniswap_v3,pancakeswap_v3,aerodrome_slipstream,aerodrome,curve}
                       (--config CONFIG | --chain CHAIN) [--pool POOL]
                       [--block BLOCK]
                       [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                       [--gateway-auth-file GATEWAY_AUTH_FILE]
                       [--depth-bps DEPTH_BPS]
```

| Option                | Details                                                                                       | Description                                                                                                  |
| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--connector`         | one of `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve`; required | canonical connector ID                                                                                       |
| `--config`            | Path                                                                                          | JSON request file:                                                                                           |
| `--chain`             |                                                                                               | strict CAIP-2 chain of the pool, given with --pool                                                           |
| `--pool`              |                                                                                               | pool contract address, given with --chain                                                                    |
| `--block`             | block; default `finalized`                                                                    | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                                          | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                               | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                                          | authentication token file for --gateway-url                                                                  |
| `--depth-bps`         | depth_bps                                                                                     | also report pool output within this price move, 1 to 5000 basis points                                       |

Exactly one of `--config`, `--chain`.

At most one of `--env-file`, `--gateway-url`.

## almanak ax balance

native or ERC-20 balance

```
usage: almanak ax balance [-h] --asset ASSET --account ACCOUNT [--block BLOCK]
                          [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                          [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--asset`             | required                   | CAIP-19 asset type                                                                                           |
| `--account`           | required                   | EVM account address; repeat to read several accounts at the same block                                       |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax resolve

ERC-20 token candidates for a symbol or address, verified at a pinned block

```
usage: almanak ax resolve [-h] --chain CHAIN --query QUERY [--limit LIMIT]
                          [--block BLOCK]
                          [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                          [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                    | Description                                                                                                  |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--chain`             | required                   | strict CAIP-2 chain                                                                                          |
| `--query`             | required                   | token symbol or contract address                                                                             |
| `--limit`             | parse; default `10`        | candidates to report (1 to 30)                                                                               |
| `--block`             | block; default `finalized` | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                       | private provider credentials read by this process                                                            |
| `--gateway-url`       |                            | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                       | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax dex-pools

pools that trade a token, verified through each connector's resolver

```
usage: almanak ax dex-pools [-h] --chain CHAIN --token TOKEN [--quote QUOTE]
                            [--connector {uniswap_v4,uniswap_v3,pancakeswap_v3,aerodrome_slipstream,aerodrome,curve}]
                            [--limit LIMIT] [--offset OFFSET]
                            [--depth-bps DEPTH_BPS] [--block BLOCK]
                            [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                            [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                                                                                           | Description                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--chain`             | required                                                                                          | strict CAIP-2 chain                                                                                          |
| `--token`             | required                                                                                          | CAIP-19 ERC-20 asset or native coin the pools trade                                                          |
| `--quote`             |                                                                                                   | CAIP-19 ERC-20 asset or native coin; only pools pairing --token with it                                      |
| `--connector`         | one of `uniswap_v4`, `uniswap_v3`, `pancakeswap_v3`, `aerodrome_slipstream`, `aerodrome`, `curve` | only pools of this connector                                                                                 |
| `--limit`             | parse; default `10`                                                                               | pools to report (1 to 20)                                                                                    |
| `--offset`            | parse                                                                                             | matching pools to skip, for paging                                                                           |
| `--depth-bps`         | depth_bps                                                                                         | also report each verified V3-style pool's depth within this price move                                       |
| `--block`             | block; default `finalized`                                                                        | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                                                                              | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                                                                                   | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                                                                              | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

## almanak ax lending-markets

a lending protocol's markets on a chain, verified through its connector's resolver

```
usage: almanak ax lending-markets [-h] --connector
                                  {aave_v4,aave_v3,sparklend,compound_v3,morpho_blue}
                                  --chain CHAIN [--asset ASSET]
                                  [--collateral COLLATERAL] [--limit LIMIT]
                                  [--include-unlisted] [--offset OFFSET]
                                  [--block BLOCK]
                                  [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                  [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                                                                          | Description                                                                                                                                     |
| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `--connector`         | one of `aave_v4`, `aave_v3`, `sparklend`, `compound_v3`, `morpho_blue`; required |                                                                                                                                                 |
| `--chain`             | required                                                                         | strict CAIP-2 chain                                                                                                                             |
| `--asset`             |                                                                                  | CAIP-19 ERC-20 asset: an Aave or SparkLend reserve asset, a Compound base or collateral asset, the Morpho loan asset (required for morpho_blue) |
| `--collateral`        |                                                                                  | CAIP-19 ERC-20 asset; only Compound or Morpho markets accepting it                                                                              |
| `--limit`             | parse; default `10`                                                              | markets to report (1 to 20)                                                                                                                     |
| `--include-unlisted`  |                                                                                  | morpho_blue: also list markets Morpho's API does not mark listed; they keep its warnings                                                        |
| `--offset`            | parse                                                                            | matching markets to skip, for paging                                                                                                            |
| `--block`             | block; default `finalized`                                                       | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash                                    |
| `--env-file`          | Path                                                                             | private provider credentials read by this process                                                                                               |
| `--gateway-url`       |                                                                                  | read-only observation gateway on loopback (service read-gateway)                                                                                |
| `--gateway-auth-file` | Path                                                                             | authentication token file for --gateway-url                                                                                                     |

At most one of `--env-file`, `--gateway-url`.

## almanak ax hypercore-markets

HyperCore perpetual markets from Hyperliquid's meta, with their asset index

```
usage: almanak ax hypercore-markets [-h] [--query QUERY] [--limit LIMIT]
                                    [--offset OFFSET] [--include-delisted]
                                    [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                    [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details             | Description                                                      |
| --------------------- | ------------------- | ---------------------------------------------------------------- |
| `--query`             |                     | report one market: its coin as meta names it, e.g. BTC           |
| `--limit`             | parse; default `20` | markets to report (1 to 50)                                      |
| `--offset`            | parse               | listed markets to skip, for paging                               |
| `--include-delisted`  |                     | also list delisted markets                                       |
| `--env-file`          | Path                | private provider credentials read by this process                |
| `--gateway-url`       |                     | read-only observation gateway on loopback (service read-gateway) |
| `--gateway-auth-file` | Path                | authentication token file for --gateway-url                      |

At most one of `--env-file`, `--gateway-url`.

## almanak ax hypercore-account

a HyperCore account: its role, abstraction mode, perp margin and spot balances

```
usage: almanak ax hypercore-account [-h] --account ACCOUNT
                                    [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                                    [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details  | Description                                                      |
| --------------------- | -------- | ---------------------------------------------------------------- |
| `--account`           | required | EVM address of the account; repeat to read several               |
| `--env-file`          | Path     | private provider credentials read by this process                |
| `--gateway-url`       |          | read-only observation gateway on loopback (service read-gateway) |
| `--gateway-auth-file` | Path     | authentication token file for --gateway-url                      |

At most one of `--env-file`, `--gateway-url`.

# almanak backtest

prepare, run and reproduce historical simulations

```
usage: almanak backtest [-h] {run,prepare,resume,reproduce,recent} ...
```

## almanak backtest run

```
usage: almanak backtest run [-h] [--config CONFIG] [--strategy STRATEGY]
                            --backtest-config BACKTEST_CONFIG
                            [--gateway-config GATEWAY_CONFIG] --output OUTPUT
                            [--env-file ENV_FILE] [--dataset DATASET]
                            [--progress {auto,text,json,none}]
                            [--progress-file PROGRESS_FILE]
                            [--timing-history TIMING_HISTORY]
                            [--timing-scope TIMING_SCOPE]
                            [--format {text,json}] [--json]
                            [directory]
```

| Option              | Details                                               | Description                                                          |
| ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `directory`         | Path; default `.`                                     |                                                                      |
| `--config`          | Path                                                  |                                                                      |
| `--strategy`        | Path                                                  |                                                                      |
| `--backtest-config` | Path; required                                        |                                                                      |
| `--gateway-config`  | Path                                                  | operator provider, account and execution settings                    |
| `--output`          | Path; required                                        |                                                                      |
| `--env-file`        | Path                                                  |                                                                      |
| `--dataset`         | Path                                                  |                                                                      |
| `--progress`        | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`   | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history`  | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`    |                                                       | comparable worker environment; defaults to this host                 |
| `--format`          | one of `text`, `json`                                 |                                                                      |
| `--json`            |                                                       |                                                                      |

## almanak backtest prepare

```
usage: almanak backtest prepare [-h] [--config CONFIG] [--strategy STRATEGY]
                                --backtest-config BACKTEST_CONFIG
                                [--gateway-config GATEWAY_CONFIG] --output
                                OUTPUT [--env-file ENV_FILE]
                                [--progress {auto,text,json,none}]
                                [--progress-file PROGRESS_FILE]
                                [--timing-history TIMING_HISTORY]
                                [--timing-scope TIMING_SCOPE]
                                [--format {text,json}] [--json]
                                [directory]
```

| Option              | Details                                               | Description                                                          |
| ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `directory`         | Path; default `.`                                     |                                                                      |
| `--config`          | Path                                                  |                                                                      |
| `--strategy`        | Path                                                  |                                                                      |
| `--backtest-config` | Path; required                                        |                                                                      |
| `--gateway-config`  | Path                                                  | operator provider, account and execution settings                    |
| `--output`          | Path; required                                        |                                                                      |
| `--env-file`        | Path                                                  |                                                                      |
| `--progress`        | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`   | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history`  | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`    |                                                       | comparable worker environment; defaults to this host                 |
| `--format`          | one of `text`, `json`                                 |                                                                      |
| `--json`            |                                                       |                                                                      |

## almanak backtest resume

```
usage: almanak backtest resume [-h] --run RUN [--env-file ENV_FILE]
                               [--progress {auto,text,json,none}]
                               [--progress-file PROGRESS_FILE]
                               [--timing-history TIMING_HISTORY]
                               [--timing-scope TIMING_SCOPE]
                               [--format {text,json}] [--json]
```

| Option             | Details                                               | Description                                                          |
| ------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `--run`            | Path; required                                        |                                                                      |
| `--env-file`       | Path                                                  |                                                                      |
| `--progress`       | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`  | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history` | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`   |                                                       | comparable worker environment; defaults to this host                 |
| `--format`         | one of `text`, `json`                                 |                                                                      |
| `--json`           |                                                       |                                                                      |

## almanak backtest reproduce

```
usage: almanak backtest reproduce [-h] --run RUN
                                  [--progress {auto,text,json,none}]
                                  [--progress-file PROGRESS_FILE]
                                  [--timing-history TIMING_HISTORY]
                                  [--timing-scope TIMING_SCOPE]
                                  [--format {text,json}] [--json]
```

| Option             | Details                                               | Description                                                          |
| ------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- |
| `--run`            | Path; required                                        |                                                                      |
| `--progress`       | one of `auto`, `text`, `json`, `none`; default `auto` | terminal or JSON progress on stderr; snapshot file is always written |
| `--progress-file`  | Path                                                  | atomic progress snapshot (default: OUTPUT.progress.json)             |
| `--timing-history` | Path                                                  | local completed phase timings for ETA calibration                    |
| `--timing-scope`   |                                                       | comparable worker environment; defaults to this host                 |
| `--format`         | one of `text`, `json`                                 |                                                                      |
| `--json`           |                                                       |                                                                      |

## almanak backtest recent

rewrite an economic backtest file for the most recent window, with measured gas

```
usage: almanak backtest recent [-h] [--config CONFIG] [--strategy STRATEGY]
                               --backtest-config BACKTEST_CONFIG --env-file
                               ENV_FILE [--days DAYS] [--end END] --output
                               OUTPUT
                               [directory]
```

| Option              | Details           | Description                                        |
| ------------------- | ----------------- | -------------------------------------------------- |
| `directory`         | Path; default `.` |                                                    |
| `--config`          | Path              |                                                    |
| `--strategy`        | Path              |                                                    |
| `--backtest-config` | Path; required    |                                                    |
| `--env-file`        | Path; required    |                                                    |
| `--days`            | int; default `30` |                                                    |
| `--end`             |                   | ISO window end; defaults to the latest served hour |
| `--output`          | Path; required    |                                                    |

# almanak check

validate author inputs and selected-mode readiness

```
usage: almanak check [-h] [--config CONFIG] [--strategy STRATEGY]
                     [--fork | --production] [--gateway-config GATEWAY_CONFIG]
                     [--env-file ENV_FILE] [--online] [--normalized]
                     [--backtest-config BACKTEST_CONFIG]
                     [--format {text,json}] [--json]
                     [directory]
```

| Option              | Details               | Description                                                    |
| ------------------- | --------------------- | -------------------------------------------------------------- |
| `directory`         | Path; default `.`     |                                                                |
| `--config`          | Path                  |                                                                |
| `--strategy`        | Path                  |                                                                |
| `--fork`            |                       |                                                                |
| `--production`      |                       |                                                                |
| `--gateway-config`  | Path                  | operator provider, account and execution settings              |
| `--env-file`        | Path                  | private provider credentials read only by the operator gateway |
| `--online`          |                       | resolve current facts through a read-only gateway              |
| `--normalized`      |                       |                                                                |
| `--backtest-config` | Path                  |                                                                |
| `--format`          | one of `text`, `json` |                                                                |
| `--json`            |                       |                                                                |

At most one of `--fork`, `--production`.

# almanak lifecycle commands

Retained-run commands that share the `--run` workspace argument.

## almanak status

show retained run state and service availability

```
usage: almanak status [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak pause

pause the coordinator; positions and services remain

```
usage: almanak pause [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak resume

resume captured execution through its original services

```
usage: almanak resume [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak teardown

request and execute economic closure

```
usage: almanak teardown [-h] --run RUN [--format {text,json}] [--json]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |

## almanak shutdown

stop local services, preserving recorded evidence

```
usage: almanak shutdown [-h] --run RUN [--format {text,json}] [--json]
                        [--force]
```

| Option     | Details               | Description |
| ---------- | --------------------- | ----------- |
| `--run`    | Path; required        |             |
| `--format` | one of `text`, `json` |             |
| `--json`   |                       |             |
| `--force`  |                       |             |

# almanak markets

what each market in config.yaml resolves to: its contracts, tokens and ids, with each token's symbol and decimals, for checking before approval (no prices read)

```
usage: almanak markets [-h] [--config CONFIG] [--out OUT] [--block BLOCK]
                       [--env-file ENV_FILE | --gateway-url GATEWAY_URL]
                       [--gateway-auth-file GATEWAY_AUTH_FILE]
```

| Option                | Details                               | Description                                                                                                  |
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--config`            | Path; default `config.yaml`           |                                                                                                              |
| `--out`               | Path; default `.almanak/markets.json` |                                                                                                              |
| `--block`             | block; default `latest`               | block to pin: a decimal block number, 'finalized' or 'latest'; the output names the resolved number and hash |
| `--env-file`          | Path                                  | private provider credentials read by this process                                                            |
| `--gateway-url`       |                                       | read-only observation gateway on loopback (service read-gateway)                                             |
| `--gateway-auth-file` | Path                                  | authentication token file for --gateway-url                                                                  |

At most one of `--env-file`, `--gateway-url`.

# almanak permissions

generate or verify strategy-derived permissions

```
usage: almanak permissions [-h] {generate,verify} ...
```

## almanak permissions generate

```
usage: almanak permissions generate [-h] --gateway-url GATEWAY_URL
                                    --gateway-auth-file GATEWAY_AUTH_FILE
                                    --strategy STRATEGY --config CONFIG
                                    --output OUTPUT
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |
| `--strategy`          | Path; required |             |
| `--config`            | Path; required |             |
| `--output`            | Path; required |             |

## almanak permissions verify

```
usage: almanak permissions verify [-h] --gateway-url GATEWAY_URL
                                  --gateway-auth-file GATEWAY_AUTH_FILE
                                  --manifest MANIFEST
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |
| `--manifest`          | Path; required |             |

# almanak report

render recorded evidence without network access

```
usage: almanak report [-h] --run RUN [--output OUTPUT] [--dashboard]
                      [--workspace WORKSPACE] [--format {text,json,html}]
                      [--json]
```

| Option        | Details                       | Description                                                                                                                          |
| ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `--run`       | Path; required                |                                                                                                                                      |
| `--output`    | Path                          |                                                                                                                                      |
| `--dashboard` |                               | print the strategy's dashboard response for the run (a fork test: its last running state)                                            |
| `--workspace` | Path                          | with --dashboard: draw this workspace's current strategy and dashboard.py over the run's data instead of the source the run captured |
| `--format`    | one of `text`, `json`, `html` |                                                                                                                                      |
| `--json`      |                               |                                                                                                                                      |

# almanak run

start a supervised fork or production workspace

```
usage: almanak run [-h] [--config CONFIG] [--strategy STRATEGY]
                   (--fork | --production) --gateway-config GATEWAY_CONFIG
                   --env-file ENV_FILE --output OUTPUT [--format {text,json}]
                   [--json]
                   [directory]
```

| Option             | Details               | Description                                                    |
| ------------------ | --------------------- | -------------------------------------------------------------- |
| `directory`        | Path; default `.`     |                                                                |
| `--config`         | Path                  |                                                                |
| `--strategy`       | Path                  |                                                                |
| `--fork`           |                       |                                                                |
| `--production`     |                       |                                                                |
| `--gateway-config` | Path; required        | operator provider, account and execution settings              |
| `--env-file`       | Path; required        | private provider credentials read only by the operator gateway |
| `--output`         | Path; required        |                                                                |
| `--format`         | one of `text`, `json` |                                                                |
| `--json`           |                       |                                                                |

Exactly one of `--fork`, `--production`.

# almanak service

advanced explicit process operation

```
usage: almanak service [-h]
                       {gateway,read-gateway,start,resume,status,teardown} ...
```

## almanak service gateway

```
usage: almanak service gateway [-h]
                               (--config CONFIG | --gateway-config GATEWAY_CONFIG)
                               [--read-context READ_CONTEXT | --preparation-file PREPARATION_FILE]
                               [--database DATABASE]
                               (--fork-url FORK_URL | --fork-routes FORK_ROUTES | --production | --fork | --backtest)
                               [--fork-key-file FORK_KEY_FILE]
                               [--env-file ENV_FILE] [--accounts ACCOUNTS]
                               [--key-file KEY_FILE]
                               [--password-file PASSWORD_FILE]
                               --auth-token-file AUTH_TOKEN_FILE
                               (--port PORT | --listen-fd LISTEN_FD)
                               [--lease-seconds LEASE_SECONDS]
```

| Option               | Details             | Description                                                         |
| -------------------- | ------------------- | ------------------------------------------------------------------- |
| `--config`           | Path                |                                                                     |
| `--gateway-config`   | Path                | operator provider, account and execution settings                   |
| `--read-context`     | Path                | generated provisional read context                                  |
| `--preparation-file` | Path                | generated frozen deployment preparation                             |
| `--database`         | Path                |                                                                     |
| `--fork-url`         |                     |                                                                     |
| `--fork-routes`      | Path                |                                                                     |
| `--production`       |                     |                                                                     |
| `--fork`             |                     | read-only fork origin preparation                                   |
| `--backtest`         |                     | read-only historical preparation                                    |
| `--fork-key-file`    | Path                |                                                                     |
| `--env-file`         | Path                |                                                                     |
| `--accounts`         | Path                | operator-generated account bindings                                 |
| `--key-file`         | Path                |                                                                     |
| `--password-file`    | Path                |                                                                     |
| `--auth-token-file`  | Path; required      |                                                                     |
| `--port`             | int                 |                                                                     |
| `--listen-fd`        | int                 | inherited loopback socket, already bound by the supervising process |
| `--lease-seconds`    | float; default `30` |                                                                     |

Exactly one of `--config`, `--gateway-config`.

At most one of `--read-context`, `--preparation-file`.

Exactly one of `--fork-url`, `--fork-routes`, `--production`, `--fork`, `--backtest`.

Exactly one of `--port`, `--listen-fd`.

## almanak service read-gateway

read-only observation gateway holding the provider credentials

```
usage: almanak service read-gateway [-h] --env-file ENV_FILE --auth-token-file
                                    AUTH_TOKEN_FILE --port PORT
```

| Option              | Details        | Description                                            |
| ------------------- | -------------- | ------------------------------------------------------ |
| `--env-file`        | Path; required | private provider credentials read only by this gateway |
| `--auth-token-file` | Path; required |                                                        |
| `--port`            | int; required  |                                                        |

## almanak service start

```
usage: almanak service start [-h] --gateway-url GATEWAY_URL
                             --gateway-auth-file GATEWAY_AUTH_FILE --config
                             CONFIG [--export EXPORT]
                             [--clock-offset-file CLOCK_OFFSET_FILE]
                             [--decision-facts] --strategy STRATEGY
```

| Option                | Details        | Description                                                           |
| --------------------- | -------------- | --------------------------------------------------------------------- |
| `--gateway-url`       | required       |                                                                       |
| `--gateway-auth-file` | Path; required |                                                                       |
| `--config`            | Path; required |                                                                       |
| `--export`            | Path           | retain read-only dashboard input snapshots                            |
| `--clock-offset-file` | Path           | fork test clock: whole seconds added to wall time for decision timing |
| `--decision-facts`    |                | fork test run: log what each decision saw for the test verdict        |
| `--strategy`          | Path; required |                                                                       |

## almanak service resume

```
usage: almanak service resume [-h] --gateway-url GATEWAY_URL
                              --gateway-auth-file GATEWAY_AUTH_FILE
                              [--config CONFIG] [--export EXPORT]
                              [--clock-offset-file CLOCK_OFFSET_FILE]
                              [--decision-facts]
```

| Option                | Details        | Description                                                           |
| --------------------- | -------------- | --------------------------------------------------------------------- |
| `--gateway-url`       | required       |                                                                       |
| `--gateway-auth-file` | Path; required |                                                                       |
| `--config`            | Path           |                                                                       |
| `--export`            | Path           | retain read-only dashboard input snapshots                            |
| `--clock-offset-file` | Path           | fork test clock: whole seconds added to wall time for decision timing |
| `--decision-facts`    |                | fork test run: log what each decision saw for the test verdict        |

## almanak service status

```
usage: almanak service status [-h] [--gateway-url GATEWAY_URL]
                              [--gateway-auth-file GATEWAY_AUTH_FILE]
                              [--database DATABASE]
```

| Option                | Details | Description |
| --------------------- | ------- | ----------- |
| `--gateway-url`       |         |             |
| `--gateway-auth-file` | Path    |             |
| `--database`          | Path    |             |

## almanak service teardown

```
usage: almanak service teardown [-h] --gateway-url GATEWAY_URL
                                --gateway-auth-file GATEWAY_AUTH_FILE
```

| Option                | Details        | Description |
| --------------------- | -------------- | ----------- |
| `--gateway-url`       | required       |             |
| `--gateway-auth-file` | Path; required |             |

# almanak support

show declared connector and chain support and gas lifecycle counts

```
usage: almanak support [-h] [--connector CONNECTOR] [--chain CHAIN]
                       [--schema {strategy,backtest,dashboard} | --references]
                       [--format {text,json}] [--json]
```

| Option         | Details                                    | Description                                                                                                                 |
| -------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `--connector`  |                                            | filter by an installed canonical connector ID                                                                               |
| `--chain`      |                                            | filter by a strict CAIP-2 chain identity                                                                                    |
| `--schema`     | one of `strategy`, `backtest`, `dashboard` |                                                                                                                             |
| `--references` |                                            | list the installed read-only reference strategies with their connector, chain and path, filtered by --connector and --chain |
| `--format`     | one of `text`, `json`                      |                                                                                                                             |
| `--json`       |                                            |                                                                                                                             |

At most one of `--schema`, `--references`.

# almanak test

fork test tooling: operator file and one lifecycle

```
usage: almanak test [-h] {run,config} ...
```

## almanak test run

one supervised fork lifecycle: admission, decisions, teardown, verdict

```
usage: almanak test run [-h] [--config CONFIG] [--strategy STRATEGY]
                        --gateway-config GATEWAY_CONFIG --env-file ENV_FILE
                        --output OUTPUT [--decisions DECISIONS]
                        [--advance ADVANCE] [--through-exit]
                        [--param KEY=VALUE] [--production-parameters]
                        [--idle IDLE] [--format {text,json}] [--json]
                        [directory]
```

| Option                    | Details               | Description                                                                                                                                                           |
| ------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directory`               | Path; default `.`     |                                                                                                                                                                       |
| `--config`                | Path                  |                                                                                                                                                                       |
| `--strategy`              | Path                  |                                                                                                                                                                       |
| `--gateway-config`        | Path; required        | operator provider, account and execution settings                                                                                                                     |
| `--env-file`              | Path; required        | private provider credentials read only by the operator gateway                                                                                                        |
| `--output`                | Path; required        | new fork workspace directory                                                                                                                                          |
| `--decisions`             | positive; default `3` | most decisions before teardown                                                                                                                                        |
| `--advance`               | positive              | seconds of fork time between decisions (default: the decision interval; a shorter step is raised to reach the next due decision)                                      |
| `--through-exit`          |                       | keep deciding after the first entry, up to --decisions, so the strategy's own later intents and exit execute before teardown                                          |
| `--param`                 |                       | set a strategy.parameters value in the fork workspace's copy of the config (read as a YAML scalar, on top of strategy.test_parameters); repeatable                    |
| `--production-parameters` |                       | do not apply strategy.test_parameters: fork with the production strategy.parameters values plus any --param                                                           |
| `--idle`                  | positive; default `3` | with --through-exit, consecutive holds after the entry that end the decisions early (default 3); raise it, or --advance, when the strategy waits longer between steps |
| `--format`                | one of `text`, `json` |                                                                                                                                                                       |
| `--json`                  |                       |                                                                                                                                                                       |

## almanak test config

generate a fork operator file from the author configuration

```
usage: almanak test config [-h] [--config CONFIG] [--strategy STRATEGY]
                           --env-file ENV_FILE [--block BLOCK]
                           [--output OUTPUT] [--force] [--format {text,json}]
                           [--json]
                           [directory]
```

| Option       | Details               | Description                                    |
| ------------ | --------------------- | ---------------------------------------------- |
| `directory`  | Path; default `.`     |                                                |
| `--config`   | Path                  |                                                |
| `--strategy` | Path                  |                                                |
| `--env-file` | Path; required        | private provider credentials                   |
| `--block`    | whole                 | anchor block (default: latest; one chain only) |
| `--output`   | Path                  | write the operator file here                   |
| `--force`    |                       | replace an existing --output                   |
| `--format`   | one of `text`, `json` |                                                |
| `--json`     |                       |                                                |

# almanak wallet

provision encrypted operator credentials

```
usage: almanak wallet [-h] {create} ...
```

## almanak wallet create

```
usage: almanak wallet create [-h] --output OUTPUT --account ACCOUNT
```

| Option      | Details        | Description |
| ----------- | -------------- | ----------- |
| `--output`  | Path; required |             |
| `--account` | required       |             |

# almanak

Define, check, run and explain a strategy. A bare almanak, and any command this CLI does not define, run the bundled Almanak Code assistant when the installed wheel carries a binary for this platform.

```
usage: almanak [-h]
               {check,run,status,pause,resume,teardown,shutdown,test,report,support,backtest,markets,service,permissions,wallet,ax}
               ...
```

| Command               | Description                                                                                                                                                      | Page                                                                            |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `almanak check`       | validate author inputs and selected-mode readiness                                                                                                               | [almanak-check](https://sdk.docs.almanak.co/cli/almanak-check.html)             |
| `almanak run`         | start a supervised fork or production workspace                                                                                                                  | [almanak-run](https://sdk.docs.almanak.co/cli/almanak-run.html)                 |
| `almanak status`      | show retained run state and service availability                                                                                                                 | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak pause`       | pause the coordinator; positions and services remain                                                                                                             | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak resume`      | resume captured execution through its original services                                                                                                          | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak teardown`    | request and execute economic closure                                                                                                                             | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak shutdown`    | stop local services, preserving recorded evidence                                                                                                                | [almanak-lifecycle](https://sdk.docs.almanak.co/cli/almanak-lifecycle.html)     |
| `almanak test`        | fork test tooling: operator file and one lifecycle                                                                                                               | [almanak-test](https://sdk.docs.almanak.co/cli/almanak-test.html)               |
| `almanak report`      | render recorded evidence without network access                                                                                                                  | [almanak-report](https://sdk.docs.almanak.co/cli/almanak-report.html)           |
| `almanak support`     | show declared connector and chain support and gas lifecycle counts                                                                                               | [almanak-support](https://sdk.docs.almanak.co/cli/almanak-support.html)         |
| `almanak backtest`    | prepare, run and reproduce historical simulations                                                                                                                | [almanak-backtest](https://sdk.docs.almanak.co/cli/almanak-backtest.html)       |
| `almanak markets`     | what each market in config.yaml resolves to: its contracts, tokens and ids, with each token's symbol and decimals, for checking before approval (no prices read) | [almanak-markets](https://sdk.docs.almanak.co/cli/almanak-markets.html)         |
| `almanak service`     | advanced explicit process operation                                                                                                                              | [almanak-service](https://sdk.docs.almanak.co/cli/almanak-service.html)         |
| `almanak permissions` | generate or verify strategy-derived permissions                                                                                                                  | [almanak-permissions](https://sdk.docs.almanak.co/cli/almanak-permissions.html) |
| `almanak wallet`      | provision encrypted operator credentials                                                                                                                         | [almanak-wallet](https://sdk.docs.almanak.co/cli/almanak-wallet.html)           |
| `almanak ax`          | read-only protocol observations at a pinned block                                                                                                                | [almanak-ax](https://sdk.docs.almanak.co/cli/almanak-ax.html)                   |
# API Reference

# Execution API

For author code use the [public authoring guide](https://sdk.docs.almanak.co/authoring.html); for commands use the [operating guide](https://sdk.docs.almanak.co/operating.html). This page describes trusted integration contracts.

## Author configuration and support

A strategy owns `strategy.py` and one configuration file. The current [reference strategy catalog](https://github.com/almanak-co/sdk/blob/main/strategies/README.md) selects explicit markets, assets, capital, timing and gas policies in that file. Operator account bindings, credentials and generated deployment files are runtime artifacts.

`connector_for(config).support_for(ChainId(...))` returns an immutable `ConnectorChainSupport`. `supported_chains(RunMode.FORK)`, `RunMode.PRODUCTION` and `RunMode.BACKTEST` enumerate static support. Aave and Uniswap V4 advertise Ethereum and Avalanche for fork and production execution; only Ethereum has historical models. Aave historical actions exclude borrowing and repayment. Historical start times constrain an existing model; an omitted time grants no additional market or data coverage.

Support records expose modes, typed `EconomicAction` values, account profiles, restrictions and historical model identity. Configuration validation and gateway operation admission independently check them. A declaration does not establish provider availability, wallet funding or installed Safe authority.

Configured Aave reserves identify the Spoke, Hub, reserve ID and CAIP asset. Admission verifies their observed relationship, reserve metadata and supported implementation bytecode. Proxy verification normalizes only the verified immutable administrator operand. Uniswap configuration identifies protocol infrastructure and a full PoolKey. Admission verifies infrastructure revisions, periphery relationships and initialized pool state. Asset and pool selection do not consult sample token or market lists. Historical implementation code reads require a captured proxy pointer from a configured contract at the same canonical block; that evidence grants no calls or storage access to the implementation. A Uniswap V4 hook is supported only with a reviewed profile pinned by code (Fables, Uniswap StablePairHook and PonsV2MemeHook instances; Fables and StablePairHook pools also admit LP, Pons pools only trading); other hooks and protocol revisions fail. A reviewed hook whose code later differs refuses new exposure and still admits exits. A native-coin pool trades with the wrapped native held by the fund, wrapped and unwrapped by the reviewed router within the swap; its liquidity is minted and closed with the wrapped native, which the reviewed PositionManager unwraps and wraps within the same call. Simulation and receipt accounting require standard ERC-20 balance effects; unexplained effects prevent further exposure. Historical admission retains its captured model and revision constraints; compatibility for live execution does not establish backtest coverage.

Uniswap capital may identify either pool currency. `target.funding_currency` and `target.counter_currency` preserve that orientation through quotes, inventory, completion and pool-marked reporting. NFT exits retain the [documented broader Roles grant](https://sdk.docs.almanak.co/internal/blueprints/15-uniswap-v4-lifecycles.html#protocol-and-authority).

## Production binding

Every reference binds through the operator configuration. `almanak run --production --gateway-config` reads the public author file with `configuration.unresolved.read_inputs`, resolves named markets through the read-only gateway and constructs authoritative parameters with the operator's account bindings before admission. The author retains economic gas preferences; production networks, payer ceilings, account profiles and credentials belong to the operator file. The SDK exposes no legacy author-loader or complete-config production fallback. Archived roots under `scripts/legacy_configuration/` serve only baseline and component evidence tooling.

Each binding has a name and an EOA or Safe/Roles profile. Its fund, submitter and policy retain full chain and environment scope. The prepared deployment freezes the author digest, resolved facts, account identities, strategy hooks and compatible author/operator gas policy before the execution gateway can start. The supervised run generates and verifies these deployment artifacts. See the [operating guide](https://sdk.docs.almanak.co/operating.html#production).

Gas policies explicitly select `base_fee_basis`: `observed_block` in the Avalanche samples and `next_block` for Ethereum. The retained quote has the corresponding `observed_base_fee` or `next_base_fee`, plus its pinned block and priority fee. Requested missing data fails; there is no automatic fee-basis or provider fallback. The policy multiplier and hard caps apply to that observation.

The resulting configuration contains production authority only. Fork funding and local signing keys do not transfer into it. The [production operator guide](https://github.com/almanak-co/sdk/blob/main/strategies/aave_v4_weth_supply/PRODUCTION.md) describes encrypted credentials, `production-check`, gateway launch and runtime control. `production-check` reports each network separately. A configured URL provider uses only its named environment variable; there is no fallback provider.

## Chain boundary and gateway routes

`Chain` combines a validated CAIP-2 identity and explicit environment authority. New forks retain the origin CAIP identity and use an `anvil:<instance>` environment. `ForkBinding` retains the pinned origin and separate local signing chain `eip155:31337`. Avalanche native fees use `eip155:43114/slip44:9005`; wrapping uses the verified WAVAX mapping.

`ChainAdapter` owns network guards, checkpoints, submission references, preparation, signing, broadcast, receipts, reconciliation and execution costs. `CheckpointReference` and `TransactionReference` carry a chain and an opaque validated `ChainReference`. EVM references require canonical hashes and EVM checkpoints require block heights. Other families preserve case and delegate semantic validation to their adapters. EVM nonce, ABI, gas and signed-transaction fields stay in the EVM implementation and connector execution types.

One authenticated loopback gateway contains explicitly composed adapter routes. `ChainRouter` creates a route when accessed, so one unavailable network does not prevent another route from operating. Multi-chain gateway requests to `observe`, `audit`, `plan`, `sign`, `receipt`, `broadcast`, `reconcile` and execution `update` must include the complete `chain` object. Omitted or mismatched routes are refused. `GatewayClient` and the runtime's `ScopedGateway` provide those routes.

Use `gateway --fork-routes /private/deployment/fork-routes.json` for a generated fork collection. Its entries contain `chain` and loopback `endpoint`. The `fork` command starts one node per configured target, writes one deployment and routing file, and installs only generated Safe authority on the identified fixtures. Production routes come from `production.networks`. Both environments share protocol, account wrapping, simulation, receipt and accounting code.

## Durable work and strategy observations

A multi-chain checkpoint contains `work`, one record per configured chain, and a round-robin `scheduler_cursor`. Each record retains initialization, decision, plan, pending attempt, fault and closure. One economic intent may be active on each chain. The checkpoint has one configuration, strategy memory, outcome order and delivery cursor. Decisions and outcome delivery remain serialized.

Single-chain checkpoints retain their scalar execution slot, including previously recorded Ethereum fork identities and signed bytes. The runtime never rewrites a pending attempt into another signing domain. Chain-scoped updates are projected and independently validated under the same durable journal lock; updates that alter another chain's execution facts are refused.

`DecisionInput.availability` exposes `unobserved`, `ready`, `unavailable`, `invalidated` or `closed`, with retained fault information. A decision receives fresh observations for its selected chain. It must skip unavailable allocations. `Decision.reason` is an optional string the test run's coordinator logs with the decision and nothing else reads. `Decision.required_observations` declares `(chain, account, allocation)` scopes. The runtime and gateway reject dependencies absent from the decision evidence; the connector additionally verifies its economic dependencies. The new reference strategies declare the observations used by each economic decision. No missing observation is interpreted as zero. Cross-chain dependent decisions have no combined observation acquisition workflow in this increment and fail closed.

Committed receipt canonicality is audited before advancing a route. Unknown attempts reconcile using their retained signed identity before new affected work. A chain-local outage or invalidated history blocks that chain; independent healthy entries and exits continue. A shared database, gateway, coordinator or strategy failure stops the runtime. Creating a Safe deployment requires all its permissions to be generated and verified; independent progress begins after that admission. Gateway replacement uses the same database and credentials.

Teardown persists at deployment scope. Each available chain repays, exits and verifies its remaining obligations independently. Overall `closed` requires all chains closed. Invalidated history remains halted for operator investigation; automatic reorganization recovery is unsupported. Reports retain per-chain work, readiness, balances, debt, permissions and payer gas. They do not sum unlike assets or infer a consolidated valuation source.

# Authoring API

Public contracts imported by `strategy.py`. External I/O remains gateway-owned.

Public strategy authoring contracts. External I/O remains gateway-owned.

## AssetAmount

Bases: `Model`

Nonnegative base-unit amount with explicit asset identity.

## DecisionContext

Bases: `Model`

### price

```
price(asset)
```

The declared series for one asset; a strategy declares at most one per asset.

Source code in `src/almanak/strategy_api.py`

```
def price(self, asset):
    """The declared series for one asset; a strategy declares at most one per asset."""
    matches = [series for series in self.prices if series.binding.asset == asset]
    if len(matches) != 1:
        raise ValueError(f"{asset}: no single declared strategy price series")
    return matches[0]
```

## InvalidConfig

Bases: `ValueError`

Raise from `validate_config` to refuse the configuration with this message.

The run reports it as a final `invalid_config` refusal, never as a crash, and from any function in strategy.py that `validate_config` calls.

## PoolPrice

Bases: `PoolSource`

The pool's price at the last block at or before each candle boundary: closes only.

## PoolSwapCandles

Bases: `PoolSource`

OHLCV candles of the pool's trades between consecutive candle boundaries.

A candle opens at the previous boundary's pool price and closes at this one's; its high and low also cover every trade's price, and its volume sums the absolute quote-token amounts of those trades. A trade's price is the pool's resulting spot price (V3-style and V4 Swap events, Aerodrome Sync events) or, for Curve, its execution price including fees (TokenExchange events between the asset and quote). A candle without trades is flat with zero volume.

## PriceBinding

Bases: `Model`

### lookback_seconds

```
lookback_seconds: int
```

History before a decision that its window needs, beyond the latest point.

## PriceSeries

Bases: `Model`

### complete

```
complete: bool
```

The full window, evenly spaced, with its latest point within the source age.

### gaps

```
gaps: tuple[tuple[UnixTime, UnixTime], ...]
```

Consecutive points further apart than the timeframe.

### values

```
values: tuple[Decimal, ...]
```

The closes, for candles.

## StrategyState

Bases: `Memory`

Subclass as State inside strategy.py; typed fields are persisted by the worker.

## TypedParameters

Bases: `Model`

Subclass once in parameters.py or strategy.py, as `StrategyParams`, to type `strategy.parameters`.

Declare it as a top-level class: the SDK reads its fields from the source, and loads the strategy to find a model defined any other way. Each YAML scalar converts to its field's type the way pydantic's lax JSON mode does: `"1"` or `1.0` to an `int`, `0.2` or `"0.2"` to a `Decimal`, `3` to a `str`; `1.5` for an `int`, infinities and NaN are refused. As without a model, a `bool` field takes only `true` or `false`, and a number field refuses a boolean and a `_` in its digits; a tuple's elements convert laxly. Undeclared keys are refused, and a missing key takes its default or is refused when it has none. A list field is a `tuple[...]`: a mutable one is refused, since a hook could change it for the next.

## parameter_base_units

```
parameter_base_units(
    parameters, name: str, decimals: int
) -> int
```

`parameters.<name>`, a quantity in an asset's units, in its base units.

A quantity finer than the asset's `decimals` is refused naming the field, never truncated: call it from `validate_config` so the refusal comes before any action.

Source code in `src/almanak/typed_parameters.py`

```
def parameter_base_units(parameters, name: str, decimals: int) -> int:
    """`parameters.<name>`, a quantity in an asset's units, in its base units.

    A quantity finer than the asset's `decimals` is refused naming the field, never
    truncated: call it from `validate_config` so the refusal comes before any action.
    """
    units = Decimal(getattr(parameters, name)).scaleb(decimals)
    if units != units.to_integral_value():
        field = f"strategy.parameters.{name}"
        raise ParametersRefused(f"{field}: exceeds the asset's {decimals} decimal places", field)
    return int(units)
```

# Configuration API

Public author inputs parse once into `AuthorInputs`; resolution binds them to an operator configuration and retained market facts before parameters exist.

The public author root, with connector-owned inputs and gateway-resolved facts.

These inputs are never strategy parameters. Supported connector bindings share this loader, which has no legacy-format fallback.

## InventoryInput

Bases: `Model`

Owned cash a composed root declares; never spending authority.

## author_field

```
author_field(location) -> str
```

A discriminated union inserts its connector tag, which the author never wrote.

Source code in `src/almanak/configuration/unresolved.py`

```
def author_field(location) -> str:
    """A discriminated union inserts its connector tag, which the author never wrote."""
    return ".".join(str(part) for part in location if str(part) not in TAGS)
```

## diagnostic

```
diagnostic(
    source: Document, error, prefix=""
) -> ConfigurationError
```

Locate a validation or configuration failure in the author's own file.

Source code in `src/almanak/configuration/unresolved.py`

```
def diagnostic(source: Document, error, prefix="") -> ConfigurationError:
    """Locate a validation or configuration failure in the author's own file."""
    field = error.field if isinstance(error, ConfigurationError) else ""
    message = str(error)
    if isinstance(error, ValidationError):
        detail = error.errors(include_input=False, include_url=False)[0]
        field = author_field(detail["loc"])
        message = detail["msg"]
    field = ".".join(part for part in (prefix, field) if part)
    return located(source, ConfigurationError(message, field=field))
```

## inject_connectors

```
inject_connectors(document: Document) -> dict
```

Positions take the connector tag from their named market; the author never writes it.

Source code in `src/almanak/configuration/unresolved.py`

```
def inject_connectors(document: Document) -> dict:
    """Positions take the connector tag from their named market; the author never writes it."""
    value = json.loads(json.dumps(document.value))
    markets = value.get("markets")
    strategy = value.get("strategy")
    if not isinstance(markets, dict) or not isinstance(strategy, dict):
        return value
    for group in ("allocations", "debts"):
        positions = strategy.get(group)
        for name, position in positions.items() if isinstance(positions, dict) else ():
            if isinstance(position, dict):
                tag = position_connector(position, markets, f"strategy.{group}.{name}")
                if tag is not None:
                    position["connector"] = tag
    return value
```

## known_assets

```
known_assets(
    inputs: AuthorInputs, value
) -> tuple[AssetType, ...]
```

The position assets the author file names; pool coins need resolution.

Source code in `src/almanak/configuration/unresolved.py`

```
def known_assets(inputs: AuthorInputs, value) -> tuple[AssetType, ...]:
    """The position assets the author file names; pool coins need resolution."""
    market = inputs.markets[value.market]
    named = (
        getattr(market, "asset", None),
        getattr(getattr(value, "capital", None), "asset", None),
    )
    return tuple(asset for asset in named if isinstance(asset, AssetType))
```

## public_root

```
public_root(value) -> bool
```

Public roots carry no execution block and identify every market chain by CAIP-2 text.

Source code in `src/almanak/configuration/unresolved.py`

```
def public_root(value) -> bool:
    """Public roots carry no execution block and identify every market chain by CAIP-2 text."""
    if not isinstance(value, dict) or "execution" in value:
        return False
    strategy = value.get("strategy")
    if isinstance(strategy, dict) and "connector" in strategy:
        return False
    markets = value.get("markets")
    return (
        isinstance(markets, dict)
        and bool(markets)
        and all(isinstance(m, dict) and isinstance(m.get("chain"), str) for m in markets.values())
    )
```

## read_frozen_inputs

```
read_frozen_inputs(path: Path) -> InputDocument
```

A workspace's inputs as frozen in JSON; YAML would read `1e-05` back as a string.

Source code in `src/almanak/configuration/unresolved.py`

```
def read_frozen_inputs(path: Path) -> InputDocument:
    """A workspace's inputs as frozen in JSON; YAML would read `1e-05` back as a string."""
    return parse_inputs(Document(json.loads(path.read_text()), {}, str(path)))
```

## read_inputs

```
read_inputs(
    path: Path,
    overrides=None,
    fields: frozenset[str] | None = None,
) -> InputDocument
```

The author inputs; `fields` names the strategy's typed parameter model's fields.

Source code in `src/almanak/configuration/unresolved.py`

```
def read_inputs(path: Path, overrides=None, fields: frozenset[str] | None = None) -> InputDocument:
    """The author inputs; `fields` names the strategy's typed parameter model's fields."""
    document = load(path)
    if overrides:
        try:
            value = apply_overrides(document.value, overrides, fields)
        except ConfigurationError as error:
            raise located(document, error) from error
        document = Document(value, document.locations, document.file)
    return parse_inputs(document, fields)
```

## require_bindings

```
require_bindings(document: Document)
```

Every market names a canonical connector that has a public author binding.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_bindings(document: Document):
    """Every market names a canonical connector that has a public author binding."""
    markets = document.value.get("markets", {})
    if not isinstance(markets, dict):
        raise ConfigurationError("named markets must be a mapping", field="markets")
    for name, market in markets.items():
        if not isinstance(market, dict) or "connector" not in market:
            continue
        try:
            binding_for(require_connector(market["connector"]))
        except ConfigurationError as error:
            raise error.at(
                file=document.file,
                field=f"markets.{name}.connector",
                line=document.locations.get(f"markets.{name}.connector"),
            ) from error
```

## require_chain_support

```
require_chain_support(inputs: AuthorInputs)
```

Offline checks refuse a market chain its connector never declared, in any mode.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_chain_support(inputs: AuthorInputs):
    """Offline checks refuse a market chain its connector never declared, in any mode."""
    from almanak.connectors.composition import connector_from_wire

    for name, market in inputs.markets.items():
        connector = connector_from_wire({"connector": str(market.connector)})
        try:
            connector.support_for(market.chain)
        except ValueError as error:
            raise ConfigurationError(str(error), field=f"markets.{name}.chain") from error
```

## require_composed_inventory

```
require_composed_inventory(inputs: AuthorInputs)
```

Composed inventory states every fund asset; check the ones known before resolution.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_composed_inventory(inputs: AuthorInputs):
    """Composed inventory states every fund asset; check the ones known before resolution."""
    positions = {**inputs.strategy.allocations, **inputs.strategy.debts}
    if len({str(inputs.markets[p.market].connector) for p in positions.values()}) < 2:
        return
    declared = {(item.account, item.balance.asset) for item in inputs.inventory}
    for name, value in positions.items():
        for asset in known_assets(inputs, value):
            if (value.account, asset) not in declared:
                raise ConfigurationError(
                    "composed inventory must explicitly cover every configured fund asset; "
                    f"{name!r} uses {asset} in fund {value.account!r}",
                    field="inventory",
                    code="composed_inventory_incomplete",
                    remedy=(
                        f"Add an inventory entry for {asset} on account {value.account!r}, "
                        "with quantity '0' when the strategy starts without it."
                    ),
                )
```

## require_one_allocation_per_fund

```
require_one_allocation_per_fund(inputs: AuthorInputs)
```

The DEX connectors' own per-fund rule, refused here before any resolution.

Source code in `src/almanak/configuration/unresolved.py`

```
def require_one_allocation_per_fund(inputs: AuthorInputs):
    """The DEX connectors' own per-fund rule, refused here before any resolution."""
    from almanak.connectors.support import ONE_ALLOCATION_PER_FUND_CONNECTORS

    seen: dict[tuple[str, str], str] = {}
    for name, value in inputs.strategy.allocations.items():
        connector = str(inputs.markets[value.market].connector)
        if connector not in ONE_ALLOCATION_PER_FUND_CONNECTORS:
            continue
        first = seen.setdefault((connector, value.account), name)
        if first != name:
            raise ConfigurationError(
                f"{connector} takes one allocation per fund; {first!r} and {name!r} "
                f"both use fund {value.account!r}",
                field=f"strategy.allocations.{name}.account",
                code="one_allocation_per_fund",
                remedy="Use another DEX connector or another fund for the second allocation.",
            )
```

## unknown_reference

```
unknown_reference(kind, value, choices, field)
```

Report local-name typos without selecting a replacement automatically.

Source code in `src/almanak/configuration/unresolved.py`

```
def unknown_reference(kind, value, choices, field):
    """Report local-name typos without selecting a replacement automatically."""
    names = tuple(name for name in choices if isinstance(name, str))
    matches = get_close_matches(value, names, n=1) if isinstance(value, str) else []
    hint = f" Did you mean {matches[0]!r}?" if matches else ""
    available = ", ".join(names)
    return ConfigurationError(
        f"Unknown named {kind} {value!r}.{hint} Configured {kind}s: {available}.",
        field=field,
        code="unknown_named_reference",
        remedy=(
            f"Choose one of the configured {kind} names; suggestions never change the selection."
        ),
    )
```

Construct complete parameters only from matching, replay-checked resolution.

## admit_new_positions

```
admit_new_positions(document: InputDocument, result)
```

Preflight for a new run only: connector refusals that need the resolved reads.

Retained parameters (resume, exit) and backtests never pass through here, so a check added later cannot strand a deployment whose parameters are already frozen.

Source code in `src/almanak/configuration/parameters.py`

```
def admit_new_positions(document: InputDocument, result):
    """Preflight for a new run only: connector refusals that need the resolved reads.

    Retained parameters (resume, exit) and backtests never pass through here, so a check
    added later cannot strand a deployment whose parameters are already frozen.
    """
    for anchor in result.anchors.checkpoints:
        markets = tuple(m for m in result.markets if m.resolved.block.chain == anchor.chain)
        evidence = tuple(row for market in markets for row in market.evidence)
        with RetainedResolutionReader(anchor.chain, evidence) as reader:
            check_resolved_positions(document, reader, {m.name: m.resolved for m in markets})
```

## assemble

```
assemble(
    document, groups, accounts, metadata, gas
) -> Model
```

Build one authoritative parameter instance across all named markets.

Source code in `src/almanak/configuration/parameters.py`

```
def assemble(document, groups, accounts, metadata, gas) -> Model:
    """Build one authoritative parameter instance across all named markets."""
    if len(groups) > 1:
        by_name = {
            item.name: item
            for parts in groups.values()
            for group in GROUPS
            for item in parts[group]
        }
        names = (
            *document.inputs.strategy.allocations,
            *document.inputs.strategy.debts,
        )
        positions = tuple(by_name[name] for name in names)
        composed_values = {
            "connector": None,
            "allocations": positions,
            "inventory": resolved_inventory(document, accounts, metadata),
        }
        return validate_parameters(document, None, composed_values, accounts, metadata, gas)
    if document.inputs.inventory:
        raise document.error(
            ConfigurationError(
                "inventory requires positions from more than one connector", field="inventory"
            )
        )
    ((identity, parts),) = groups.items()
    values: dict[str, object] = {"allocations": tuple(parts["allocations"])}
    if identity == ConnectorId("aave_v4"):
        values["debts"] = tuple(parts["debts"])
    return validate_parameters(document, identity, values, accounts, metadata, gas)
```

## check_resolved_positions

```
check_resolved_positions(document, reader, markets)
```

Connector checks that need the market's resolved reads, not only its frozen facts.

Source code in `src/almanak/configuration/parameters.py`

```
def check_resolved_positions(document, reader, markets):
    """Connector checks that need the market's resolved reads, not only its frozen facts."""
    for group in GROUPS:
        for name, selection in getattr(document.inputs.strategy, group).items():
            check = getattr(selection, "check_resolved", None)
            if check is None or selection.market not in markets:
                continue
            try:
                check(reader, markets[selection.market])
            except ConfigurationError as error:
                raise document.error(error, f"strategy.{group}.{name}") from error
```

## fork_rescope

```
fork_rescope(groups, fixture)
```

Positions move to the generated fixture chains; targets keep their origin identity.

Source code in `src/almanak/configuration/parameters.py`

```
def fork_rescope(groups, fixture):
    """Positions move to the generated fixture chains; targets keep their origin identity."""
    from almanak.connectors.composed.fork import remap_allocation

    chains = {b.origin.chain: b.chain for b in fixture.bindings}
    return {
        identity: {
            group: [remap_allocation(item, chains[item.chain]) for item in items]
            for group, items in parts.items()
        }
        for identity, parts in groups.items()
    }
```

## grouped_positions

```
grouped_positions(
    document, markets
) -> dict[ConnectorId, dict[str, list]]
```

Resolved positions grouped by the connector of their named market.

Source code in `src/almanak/configuration/parameters.py`

```
def grouped_positions(document, markets) -> dict[ConnectorId, dict[str, list]]:
    """Resolved positions grouped by the connector of their named market."""
    inputs = document.inputs
    groups: dict[ConnectorId, dict[str, list]] = {}
    for group in GROUPS:
        for name, selection in getattr(inputs.strategy, group).items():
            identity = ConnectorId(inputs.markets[selection.market].connector)
            resolved = resolve_position(document, name, selection, markets, group)
            groups.setdefault(identity, {g: [] for g in GROUPS})[group].append(resolved)
    return groups
```

## resolved_inventory

```
resolved_inventory(document, accounts, metadata)
```

Resolve shared owned cash once, using verified precision and bound fund scope.

Source code in `src/almanak/configuration/parameters.py`

```
def resolved_inventory(document, accounts, metadata):
    """Resolve shared owned cash once, using verified precision and bound fund scope."""
    result = []
    for index, item in enumerate(document.inputs.inventory):
        try:
            binding = accounts.binding(item.account)
            result.append(
                Balance(
                    key=WalletAsset(
                        account=item.account,
                        chain=binding.profile.fund.chain,
                        asset=item.balance.asset,
                    ),
                    amount=resolve_quantity(
                        item.balance.quantity, metadata[item.balance.asset].decimals
                    ),
                )
            )
        except (ConfigurationError, KeyError, ValidationError) as error:
            raise document.error(error, f"inventory.{index}") from error
    return tuple(result)
```

## resolved_metadata

```
resolved_metadata(document, markets, symbols=None)
```

Verified precision per asset, named by the author or else by the token itself.

Source code in `src/almanak/configuration/parameters.py`

```
def resolved_metadata(document, markets, symbols=None):
    """Verified precision per asset, named by the author or else by the token itself."""
    symbols = symbols or {}
    metadata: dict[AssetType, AssetMetadata] = {}
    for market in markets.values():
        for asset in market.assets:
            previous = metadata.setdefault(
                asset.identifier,
                AssetMetadata(decimals=asset.decimals, symbol=symbols.get(asset.identifier)),
            )
            if previous.decimals != asset.decimals:
                raise document.error(
                    ConfigurationError("resolved asset precision is inconsistent", field="markets")
                )
    for account in document.inputs.accounts.values():
        identity, native = native_metadata(account.chain)
        metadata[identity] = native
    for asset, expected in document.inputs.assets.items():
        field = f"assets.{asset}"
        if asset not in metadata:
            raise document.error(
                ConfigurationError("asset metadata has no verified resolution", field=field)
            )
        if expected.decimals is not None and metadata[asset].decimals != expected.decimals:
            raise document.error(
                ConfigurationError(
                    "asset precision assertion differs from verified metadata",
                    field=field + ".decimals",
                )
            )
        metadata[asset] = AssetMetadata(
            decimals=metadata[asset].decimals, symbol=expected.symbol or metadata[asset].symbol
        )
    return metadata
```

Backtest inputs resolve against those same parameters and explicit operator provider bindings.

Public backtest settings: syntax-checked inputs that resolve with verified parameters.

Historical provider bindings come from the operator configuration, never from the author's backtest file, so retained runs carry no provider identity.

## BacktestInputs

Bases: `Model`

### anchors

```
anchors(
    inputs: AuthorInputs,
) -> tuple[AnchorSelection, ...]
```

One anchor per account chain: the last block at the window start.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def anchors(self, inputs: AuthorInputs) -> tuple[AnchorSelection, ...]:
    """One anchor per account chain: the last block at the window start."""
    chains = dict.fromkeys(account.chain for account in inputs.accounts.values())
    return tuple(AnchorSelection(chain=chain, at=self.window.start) for chain in chains)
```

## aave_wire

```
aave_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

Aave V4 keeps the flat fund identity and models no separate gas payer.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def aave_wire(document: BacktestDocument, parameters, history) -> dict:
    """Aave V4 keeps the flat fund identity and models no separate gas payer."""
    refuse_payers(document)
    funds = document.inputs.inventory.funds
    return single_wire(
        document,
        history,
        [fund_wire(document, name, fund, parameters) for name, fund in funds.items()],
    )
```

## bind_vendor_access

```
bind_vendor_access(
    document: BacktestDocument,
    wire: dict,
    available: tuple[VendorBinding, ...],
)
```

Bind only author-selected vendors to operator-owned access settings.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def bind_vendor_access(
    document: BacktestDocument, wire: dict, available: tuple[VendorBinding, ...]
):
    """Bind only author-selected vendors to operator-owned access settings."""
    selected = selected_vendors(document.inputs.data)
    if not selected:
        return
    bindings = {binding.name: binding for binding in available}
    for field, name, kind in selected:
        binding = bindings.get(name)
        if binding is None:
            raise document.error(
                ConfigurationError(
                    f"operator configuration has no vendor binding named {name}",
                    field=f"{field}.name",
                )
            )
        if binding.provider.kind != kind:
            raise document.error(
                ConfigurationError(
                    f"operator vendor {name} is {binding.provider.kind}, not {kind}",
                    field=f"{field}.kind",
                )
            )
    inject_vendor_access(wire.get("data"), bindings)
    for model in wire.get("models", {}).values():
        inject_vendor_access(model.get("data"), bindings)
```

## composed_wire

```
composed_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

A composed run pairs one data policy, one cost policy and one model per connector.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def composed_wire(document: BacktestDocument, parameters, history) -> dict:
    """A composed run pairs one data policy, one cost policy and one model per connector."""
    funds, payers = profiled_inventory(document, parameters)
    return {
        **head_wire(document, history),
        "funds": funds,
        "payers": payers,
        "models": composed_models(document),
    }
```

## connector_wire

```
connector_wire(
    document: BacktestDocument, parameters, history
) -> dict
```

Every other connector carries a simulation profile and its own gas payers.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def connector_wire(document: BacktestDocument, parameters, history) -> dict:
    """Every other connector carries a simulation profile and its own gas payers."""
    funds, payers = profiled_inventory(document, parameters)
    return single_wire(document, history, funds, payers)
```

## payer_wire

```
payer_wire(document, name, payer, parameters) -> dict
```

A payer keeps the same deterministic identity as a hypothetical fund.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def payer_wire(document, name, payer, parameters) -> dict:
    """A payer keeps the same deterministic identity as a hypothetical fund."""
    field = f"inventory.payers.{name}"
    return {
        "name": name,
        "fund": simulation_binding(name, payer.chain).profile.fund.model_dump(mode="json"),
        "balances": balances_wire(document, field, payer.balances, parameters),
    }
```

## refuse_payers

```
refuse_payers(document: BacktestDocument)
```

Aave V4 simulates one account identity, so its inputs carry no payer.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def refuse_payers(document: BacktestDocument):
    """Aave V4 simulates one account identity, so its inputs carry no payer."""
    inventory = document.inputs.inventory
    located_fields = [f"inventory.payers.{name}.chain" for name in inventory.payers]
    located_fields += [
        f"inventory.funds.{name}.payer" for name, fund in inventory.funds.items() if fund.payer
    ]
    if located_fields:
        raise document.error(
            ConfigurationError(
                "this connector models no separate gas payer", field=located_fields[0]
            )
        )
```

## refuse_strategy_prices

```
refuse_strategy_prices(
    document: Document, value, prefix="data"
)
```

Only the strategy declares a pool price series; a backtest never repeats one.

A single strategy price (an Aave V4 economic input) may still be listed here, and must then equal the strategy's declaration.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def refuse_strategy_prices(document: Document, value, prefix="data"):
    """Only the strategy declares a pool price series; a backtest never repeats one.

    A single strategy price (an Aave V4 economic input) may still be listed here, and
    must then equal the strategy's declaration.
    """
    if not isinstance(value, dict):
        return
    prices = value.get("prices")
    for index, binding in enumerate(prices if isinstance(prices, list) else ()):
        if isinstance(binding, dict) and binding.get("purpose") == "strategy":
            source = binding.get("source")
            kind = source.get("kind") if isinstance(source, dict) else None
            if kind not in ("pool_state", "pool_swaps") and binding.get("window", 1) == 1:
                continue
            raise located(
                document,
                ConfigurationError(
                    "strategy price series are declared by the strategy's data_dependencies "
                    "hook, not the backtest configuration; remove this entry",
                    field=f"{prefix}.prices.{index}.purpose",
                ),
            )
    connectors = value.get("connectors")
    if isinstance(connectors, dict):
        for name, policy in connectors.items():
            refuse_strategy_prices(document, policy, f"{prefix}.connectors.{name}")
```

## resolve_backtest

```
resolve_backtest(
    document: BacktestDocument,
    parameters: Model,
    history: tuple[HistoryBinding, ...],
    vendors: tuple[VendorBinding, ...] = (),
)
```

The settings wire of whichever connector the resolved parameters name.

Source code in `src/almanak/configuration/backtest_inputs.py`

```
def resolve_backtest(
    document: BacktestDocument,
    parameters: Model,
    history: tuple[HistoryBinding, ...],
    vendors: tuple[VendorBinding, ...] = (),
):
    """The settings wire of whichever connector the resolved parameters name."""
    from almanak.connectors.composition import connector_for

    builders = {"aave_v4": aave_wire, COMPOSED: composed_wire}
    builder = builders.get(parameter_tag(parameters), connector_wire)
    wire = builder(document, parameters, history)
    bind_vendor_access(document, wire, vendors)
    settings_type = connector_for(parameters).backtest(document.inputs.mode).settings_type
    try:
        return settings_type.model_validate_json(json.dumps(wire))
    except ValidationError as error:
        raise document.error(error) from error
```

Runtime artifacts use the resolved parameter codecs. Archived author fixtures are private evidence inputs under `scripts/legacy_configuration/`; they are not an SDK configuration API.

# Connector support

Connectors advertise immutable, typed per-chain support for execution modes, intents, account profiles and historical coverage. The runtime and the gateway enforce the same declaration; the generated [support page](https://sdk.docs.almanak.co/support.html) renders it.

Static connector support, independent of credentials and current market state.

## candidate_uniswap_support

```
candidate_uniswap_support()
```

Public V4 entries plus unregistered chains for private qualification tests only.

Source code in `src/almanak/connectors/support.py`

```
def candidate_uniswap_support():
    """Public V4 entries plus unregistered chains for private qualification tests only."""
    supports = uniswap_support()
    template = next(support for support in supports if support.chain == ChainId("eip155:1"))
    return (
        *supports,
        *(
            uniswap_candidate_entry(template, chain, start)
            for chain, start in UNISWAP_CANDIDATE_STARTS
        ),
    )
```

## uniswap_candidate_entry

```
uniswap_candidate_entry(template, chain, start)
```

Build one validated candidate entry from a public V4 entry's models and actions.

Source code in `src/almanak/connectors/support.py`

```
def uniswap_candidate_entry(template, chain, start):
    """Build one validated candidate entry from a public V4 entry's models and actions."""
    return ConnectorChainSupport.model_validate(
        {
            **template.model_dump(),
            "chain": ChainId(chain),
            "modes": (RunMode.FORK, RunMode.BACKTEST),
            "historical": tuple(
                {**historical.model_dump(), "backtest_start_at": start}
                for historical in template.historical
            ),
            "restrictions": (
                "candidate fork and backtest verification only; not promoted",
                *template.restrictions,
            ),
        }
    )
```

# Intents

Economic requests carry explicitly bound connector targets. Lending and exchange intents share the same admission, execution and reconciliation path.

## Lending

Economic lending requests with explicitly bound connector target types.

## LendingTarget

Bases: `Model`, `ABC`

### asset

```
asset: AssetType
```

Underlying asset from the connector's complete market identity.

## Exchange and liquidity

Economic exchange and liquidity intents with concrete connector targets.

## DexTarget

Bases: `Model`, `ABC`

### assets

```
assets: tuple[AssetType, ...]
```

The connector's explicitly selected market currencies.

## ExitExchange

Bases: `DexIntent[Target]`

Close owned liquidity, return to configured funding currency and clear authority.

## PoolLiquidityBudget

Bases: `DexIntent[Target]`

Pool-wide quantities for markets whose coin count is part of the target.

## RevokeExchange

Bases: `DexIntent[Target]`

Request economic closure of spending authority; the connector chooses calls.

## TickBounds

Bases: `Model`

One concrete concentrated-liquidity range, chosen inside an authorized band.

# Reports

Read-only projections for terminal, JSON and portable HTML reports.

Read-only projections for terminal, JSON and portable HTML reports.

## apply_effects

```
apply_effects(wallets, positions, effects)
```

Apply one transition's effects; True when the retained shape drifts.

Source code in `src/almanak/reports.py`

```
def apply_effects(wallets, positions, effects):
    """Apply one transition's effects; True when the retained shape drifts."""
    if not isinstance(effects, dict):
        return True
    drift = apply_asset_deltas(wallets, effects.get("assets", []))
    return apply_position_changes(positions, effects.get("positions", [])) or drift
```

## gas_lifecycle

```
gas_lifecycle(binding) -> dict
```

The connector's per-kind entry and exit transaction counts, from its funding table.

Source code in `src/almanak/reports.py`

```
def gas_lifecycle(binding) -> dict:
    """The connector's per-kind entry and exit transaction counts, from its funding table."""
    return binding.execution.gas_lifecycle.model_dump(mode="json")["kinds"]
```

## initial_portfolio

```
initial_portfolio(manifest)
```

Initial portfolio from the manifest's frozen initial state.

Falls back to the funds inventory (the same mapping `Fund.inventory` applies) when the initial state does not carry a portfolio; positions then start empty. Returns None when neither source is retained.

Source code in `src/almanak/reports.py`

```
def initial_portfolio(manifest):
    """Initial portfolio from the manifest's frozen initial state.

    Falls back to the funds inventory (the same mapping ``Fund.inventory``
    applies) when the initial state does not carry a portfolio; positions then
    start empty. Returns None when neither source is retained.
    """
    if not isinstance(manifest, dict):
        return None
    initial = manifest.get("initial")
    if isinstance(initial, dict) and isinstance(initial.get("portfolio"), dict):
        return initial["portfolio"]
    settings = manifest.get("settings")
    funds = settings.get("funds") if isinstance(settings, dict) else None
    if not isinstance(funds, list):
        return None
    balances = []
    for fund in funds:
        if not isinstance(fund, dict):
            return None
        account = fund.get("fund")
        if not isinstance(account, dict):
            return None
        for amount in fund.get("balances", []):
            balances.append(
                {
                    "key": {
                        "account": fund.get("name"),
                        "chain": account.get("chain"),
                        "asset": amount.get("asset"),
                    },
                    "amount": amount.get("amount"),
                }
            )
    return {"balances": balances, "positions": []}
```

## liquidation_effects

```
liquidation_effects(event)
```

Liquidation facts of a recorded event; input-only events carry none.

Source code in `src/almanak/reports.py`

```
def liquidation_effects(event):
    """Liquidation facts of a recorded event; input-only events carry none."""
    result = event.get("transition", {}).get("result", {})
    if not isinstance(result, dict) or result.get("kind") != "liquidation":
        return ()
    return tuple(result.get("liquidations", ()))
```

## portfolio_differs

```
portfolio_differs(wallets, positions, portfolio)
```

Self-check: replayed state against the checkpoint's final portfolio.

Source code in `src/almanak/reports.py`

```
def portfolio_differs(wallets, positions, portfolio):
    """Self-check: replayed state against the checkpoint's final portfolio."""
    expected_wallets = {
        series_identity(balance["key"]): balance.get("amount")
        for balance in portfolio.get("balances", [])
        if isinstance(balance, dict) and "key" in balance
    }
    if {identity: entry["amount"] for identity, entry in wallets.items()} != expected_wallets:
        return True
    expected_positions = {
        series_identity(position["key"]): position.get("payload")
        for position in portfolio.get("positions", [])
        if isinstance(position, dict) and "key" in position
    }
    return {
        identity: position.get("payload") for identity, position in positions.items()
    } != expected_positions
```

## replay_steps

```
replay_steps(wire)
```

(time, pending, effects) steps in engine application order.

Events rank before completed intents at equal times because the engine applies every event up to an intent's eligibility before executing it.

Source code in `src/almanak/reports.py`

```
def replay_steps(wire):
    """(time, pending, effects) steps in engine application order.

    Events rank before completed intents at equal times because the engine
    applies every event up to an intent's eligibility before executing it.
    """
    steps = [
        (event["time"], 0, None, (event.get("transition") or {}).get("effects"))
        for event in wire.get("events", [])
        if isinstance(event.get("time"), int)
    ]
    steps += [
        (
            entry["pending"]["eligible_at"],
            1,
            entry["pending"],
            (entry.get("transition") or {}).get("effects"),
        )
        for entry in wire.get("completed", [])
        if isinstance(entry.get("pending"), dict)
        and isinstance(entry["pending"].get("eligible_at"), int)
    ]
    steps.sort(key=lambda step: (step[0], step[1]))
    return [(time, pending, effects) for time, _, pending, effects in steps]
```

## run_dashboard

```
run_dashboard(root, workspace=None)
```

The strategy's own dashboard for a run, as its deployment would draw it. A fork test ends by winding everything down, so it is drawn from the last input taken while the strategy ran (`dashboard/running.json`) when the run kept one, else from the checkpoint.

With `workspace`, the workspace's current strategy and dashboard.py draw over the run's data instead of the source the run captured: an edited dashboard is seen without a new run.

Source code in `src/almanak/reports.py`

```
def run_dashboard(root, workspace=None):
    """The strategy's own dashboard for a run, as its deployment would draw it. A fork test ends
    by winding everything down, so it is drawn from the last input taken while the strategy ran
    (`dashboard/running.json`) when the run kept one, else from the checkpoint.

    With `workspace`, the workspace's current strategy and dashboard.py draw over the run's
    data instead of the source the run captured: an edited dashboard is seen without a new run."""
    from almanak.dashboard.evaluate import evaluate, evaluate_input
    from almanak.dashboard.export import RUNNING_INPUT
    from almanak.strategy import StrategyDefinition

    wire = checkpoint(root)
    if wire is None or "config" not in wire:
        raise ValueError("this run has no checkpoint to draw a dashboard from")
    state = checkpoint_from_json(json.dumps(wire))
    # The workspace changes only which dashboard draws; the snapshot still names the run.
    current = None
    if workspace is not None:
        current = StrategyDefinition.read(Path(workspace) / "strategy.py")
    running = root / "dashboard" / RUNNING_INPUT
    if running.is_file():
        return evaluate_input(state, json.loads(running.read_text()), current)
    return evaluate(state, strategy=current)
```

## series_identity

```
series_identity(key)
```

Stable identity for a wallet asset or position key: its full sorted JSON.

Source code in `src/almanak/reports.py`

```
def series_identity(key):
    """Stable identity for a wallet asset or position key: its full sorted JSON."""
    return json.dumps(key, sort_keys=True, separators=(",", ":"))
```

## simulation_activity

```
simulation_activity(wire)
```

Project completed intents and liquidation events into one chronological list.

Rows sort by effective simulation time, ascending: `eligible_at` for completed intents and the recorded event time for liquidations. Completed intents precede events recorded at equal times (stable sort over intents-first input).

Source code in `src/almanak/reports.py`

```
def simulation_activity(wire):
    """Project completed intents and liquidation events into one chronological list.

    Rows sort by effective simulation time, ascending: ``eligible_at`` for completed
    intents and the recorded event time for liquidations. Completed intents precede
    events recorded at equal times (stable sort over intents-first input).
    """
    timed = []
    for entry in wire.get("completed", []):
        pending = entry["pending"]
        timed.append(
            (
                pending["eligible_at"],
                {
                    "action": pending["intent"]["kind"],
                    "scope": pending["intent"].get("target"),
                    "decision_time": pending.get("decision_time"),
                    "eligible_at": pending["eligible_at"],
                    "complete": entry["transition"].get("complete", True),
                    "asset_changes": [
                        string_amount(delta)
                        for delta in entry["transition"].get("effects", {}).get("assets", [])
                    ],
                },
            )
        )
    for event in wire.get("events", []):
        for effect in liquidation_effects(event):
            timed.append((event["time"], liquidation_row(event, effect)))
    timed.sort(key=lambda pair: pair[0])
    return [row for _, row in timed]
```

## simulation_series

```
simulation_series(wire, manifest=None)
```

Replay retained effects into a per-step portfolio series in token units.

The initial point carries the manifest's frozen initial portfolio at the window start. Each completed intent adds a step point after its effects apply; connector event effects (for example liquidations) fold into the running state without their own points, and events precede intents recorded at equal times, matching engine application order. The final point carries the checkpoint portfolio at the checkpoint time. Replay self-checks against that final portfolio: schema drift or unknown shapes mark the series `complete: false` instead of failing. Zero-amount wallet entries are kept, since they mark exited assets. Wallet amounts serialize as decimal strings; JSON numbers cannot hold base units exactly. Position payloads pass through verbatim; no revaluation happens here.

Source code in `src/almanak/reports.py`

```
def simulation_series(wire, manifest=None):
    """Replay retained effects into a per-step portfolio series in token units.

    The initial point carries the manifest's frozen initial portfolio at the
    window start. Each completed intent adds a step point after its effects
    apply; connector event effects (for example liquidations) fold into the
    running state without their own points, and events precede intents recorded
    at equal times, matching engine application order. The final point carries
    the checkpoint portfolio at the checkpoint time. Replay self-checks against
    that final portfolio: schema drift or unknown shapes mark the series
    ``complete: false`` instead of failing. Zero-amount wallet entries are
    kept, since they mark exited assets. Wallet amounts serialize as decimal
    strings; JSON numbers cannot hold base units exactly. Position payloads
    pass through verbatim; no revaluation happens here.
    """
    initial = initial_portfolio(manifest)
    schedule = wire.get("schedule")
    start = schedule.get("start") if isinstance(schedule, dict) else None
    complete = initial is not None and start is not None
    points = []
    wallets: dict[str, dict] = {}
    positions: dict[str, dict] = {}
    if complete:
        seed_state(wallets, positions, initial)
        points.append(series_point(start, "initial", wallets, positions))
        for time, pending, effects in replay_steps(wire):
            if apply_effects(wallets, positions, effects):
                complete = False
            if pending is not None:
                points.append(series_point(time, "step", wallets, positions, pending))
    portfolio = wire.get("portfolio")
    time = wire.get("time")
    if isinstance(portfolio, dict) and time is not None:
        if complete and portfolio_differs(wallets, positions, portfolio):
            complete = False
        points.append(final_point(time, portfolio))
    return {"complete": complete, "points": points}
```

## string_amount

```
string_amount(delta)
```

Base-unit amounts serialize as decimal strings; JSON numbers cannot hold them exactly.

Source code in `src/almanak/reports.py`

```
def string_amount(delta):
    """Base-unit amounts serialize as decimal strings; JSON numbers cannot hold them exactly."""
    if isinstance(delta, dict) and isinstance(delta.get("amount"), int):
        return {**delta, "amount": str(delta["amount"])}
    return delta
```

## support_filters

```
support_filters(connector=None, chain=None)
```

Validate the --connector and --chain filters as canonical identities.

Source code in `src/almanak/reports.py`

```
def support_filters(connector=None, chain=None):
    """Validate the --connector and --chain filters as canonical identities."""
    from almanak.caip import ChainId
    from almanak.configuration.selection import require_connector
    from almanak.configuration.yaml import ConfigurationError

    selected = (
        require_connector(connector, field="--connector", file="<command line>")
        if connector is not None
        else None
    )
    try:
        selected_chain = ChainId(chain) if chain is not None else None
    except ValueError as error:
        raise ConfigurationError(str(error), file="<command line>", field="--chain") from error
    return selected, selected_chain
```

# JSON schemas

The public strategy and backtest roots compose connector-specific market and position definitions. Economic and historical model schemas remain separate. These schemas are published with the site and versioned with the package.

- [aave-backtest.schema.json](https://sdk.docs.almanak.co/schemas/aave-backtest.schema.json)
- [aave-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/aave-v3-economic.schema.json)
- [aerodrome-economic.schema.json](https://sdk.docs.almanak.co/schemas/aerodrome-economic.schema.json)
- [aerodrome-slipstream-economic.schema.json](https://sdk.docs.almanak.co/schemas/aerodrome-slipstream-economic.schema.json)
- [backtest.schema.json](https://sdk.docs.almanak.co/schemas/backtest.schema.json)
- [composed-economic.schema.json](https://sdk.docs.almanak.co/schemas/composed-economic.schema.json)
- [compound-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/compound-v3-economic.schema.json)
- [curve-economic.schema.json](https://sdk.docs.almanak.co/schemas/curve-economic.schema.json)
- [gmx-v2-economic.schema.json](https://sdk.docs.almanak.co/schemas/gmx-v2-economic.schema.json)
- [markets.schema.json](https://sdk.docs.almanak.co/schemas/markets.schema.json)
- [markets/aave_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/aave_v3.schema.json)
- [markets/aave_v4.schema.json](https://sdk.docs.almanak.co/schemas/markets/aave_v4.schema.json)
- [markets/aerodrome.schema.json](https://sdk.docs.almanak.co/schemas/markets/aerodrome.schema.json)
- [markets/aerodrome_slipstream.schema.json](https://sdk.docs.almanak.co/schemas/markets/aerodrome_slipstream.schema.json)
- [markets/compound_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/compound_v3.schema.json)
- [markets/curve.schema.json](https://sdk.docs.almanak.co/schemas/markets/curve.schema.json)
- [markets/gmx_v2.schema.json](https://sdk.docs.almanak.co/schemas/markets/gmx_v2.schema.json)
- [markets/morpho_blue.schema.json](https://sdk.docs.almanak.co/schemas/markets/morpho_blue.schema.json)
- [markets/pancakeswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/pancakeswap_v3.schema.json)
- [markets/sparklend.schema.json](https://sdk.docs.almanak.co/schemas/markets/sparklend.schema.json)
- [markets/uniswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/markets/uniswap_v3.schema.json)
- [markets/uniswap_v4.schema.json](https://sdk.docs.almanak.co/schemas/markets/uniswap_v4.schema.json)
- [morpho-blue-economic.schema.json](https://sdk.docs.almanak.co/schemas/morpho-blue-economic.schema.json)
- [pancakeswap-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/pancakeswap-v3-economic.schema.json)
- [positions.schema.json](https://sdk.docs.almanak.co/schemas/positions.schema.json)
- [positions/aave_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v3.schema.json)
- [positions/aave_v4.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v4.schema.json)
- [positions/aave_v4_debt.schema.json](https://sdk.docs.almanak.co/schemas/positions/aave_v4_debt.schema.json)
- [positions/aerodrome.schema.json](https://sdk.docs.almanak.co/schemas/positions/aerodrome.schema.json)
- [positions/aerodrome_slipstream.schema.json](https://sdk.docs.almanak.co/schemas/positions/aerodrome_slipstream.schema.json)
- [positions/compound_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/compound_v3.schema.json)
- [positions/curve.schema.json](https://sdk.docs.almanak.co/schemas/positions/curve.schema.json)
- [positions/gmx_v2.schema.json](https://sdk.docs.almanak.co/schemas/positions/gmx_v2.schema.json)
- [positions/morpho_blue.schema.json](https://sdk.docs.almanak.co/schemas/positions/morpho_blue.schema.json)
- [positions/pancakeswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/pancakeswap_v3.schema.json)
- [positions/sparklend.schema.json](https://sdk.docs.almanak.co/schemas/positions/sparklend.schema.json)
- [positions/uniswap_v3.schema.json](https://sdk.docs.almanak.co/schemas/positions/uniswap_v3.schema.json)
- [positions/uniswap_v4.schema.json](https://sdk.docs.almanak.co/schemas/positions/uniswap_v4.schema.json)
- [sparklend-economic.schema.json](https://sdk.docs.almanak.co/schemas/sparklend-economic.schema.json)
- [strategy.schema.json](https://sdk.docs.almanak.co/schemas/strategy.schema.json)
- [uniswap-economic.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-economic.schema.json)
- [uniswap-historical.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-historical.schema.json)
- [uniswap-v3-economic.schema.json](https://sdk.docs.almanak.co/schemas/uniswap-v3-economic.schema.json)

# Strategy state

Decision and memory contracts shared by economic families and run modes.

Decision and memory contracts shared by economic families and run modes.

## DecisionContext

Bases: `Model`

### price

```
price(asset)
```

The declared series for one asset; a strategy declares at most one per asset.

Source code in `src/almanak/strategy_api.py`

```
def price(self, asset):
    """The declared series for one asset; a strategy declares at most one per asset."""
    matches = [series for series in self.prices if series.binding.asset == asset]
    if len(matches) != 1:
        raise ValueError(f"{asset}: no single declared strategy price series")
    return matches[0]
```

## InvalidConfig

Bases: `ValueError`

Raise from `validate_config` to refuse the configuration with this message.

The run reports it as a final `invalid_config` refusal, never as a crash, and from any function in strategy.py that `validate_config` calls.

## StrategyState

Bases: `Memory`

Subclass as State inside strategy.py; typed fields are persisted by the worker.
