
Backtest Agent
Fetch.ai uAgents & The Graph drive Uniswap backtester: DataAgent fetches events, Backtester tests
Demo
About
This project implements a fully decentralized, agent-based backtesting system for Uniswap V4 strategies, leveraging Fetch.ai’s uAgents framework for orchestration and The Graph for efficient historical data access . **Architecture & Components** At its core, the system comprises two autonomous uAgents running locally: * **DataAgent**: Listens for `FetchEvents` messages, then queries Uniswap’s V4 subgraph on The Graph to retrieve mint, burn, and swap events for a given pool and time range . * **BacktestAgent**: Receives user-facing `BacktestRequest` messages (specifying pool address, start/end UNIX timestamps, and optional strategy parameters), validates inputs, invokes the DataAgent to fetch events, writes a byte-for-byte compatible `events.json` file, and calls a Foundry-based Solidity backtester to simulate on-chain behavior . **DataAgent Workflow** 1. **Pagination & Resilience**: Paginates GraphQL queries against `https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3`, with exponential back-off and up to three retries using `aiohttp` . 2. **Optional Enrichment**: (Future) Augments events with 1inch swap quotes to model realistic slippage. 3. **Normalization**: Transforms raw GraphQL output into a standardized `PoolEvent` schema and sends an `Events` model payload back to the BacktestAgent . 4. **Caching**: Uses a `BacktestDataManager.cache_graph_data()` layer to avoid redundant downloads when re-testing overlapping time windows . **BacktestAgent Orchestration** 1. **Receive & Validate**: On incoming `BacktestRequest`, shorthand pool symbols (e.g. “USDC-ETH”) are resolved to on-chain addresses. 2. **Fetch Events**: Sends `FetchEvents` to the DataAgent and awaits an `Events` response. 3. **Simulation Invocation**: Runs a Python wrapper that writes `src/data/pool-events.json` and executes the Foundry script `src/UniV4Backtester.s.sol` via `forge script … --json`, forking a live RPC at `UNI_RPC_URL` to ensure determinism . 4. **Persistence & Reply**: Saves the returned JSON metrics (PnL, Sharpe, fees, impermanent loss, gas costs) via `BacktestDataManager.save_backtest_result()`, then replies with a typed `BacktestResponse` model back to the original requester . **Message & Data Models** * `BacktestRequest`: `{ pool: str; start: int; end: int; strategy_params?: dict }` * `Events`: `{ events: List<PoolEvent> }` * `BacktestResponse`: `{ kind: "backtest_result"; pnl: float; sharpe: float; total_fees: float; impermanent_loss: float; gas_costs: float; success: bool; error_message?: str }` All models extend `uagents.Model` (Pydantic v1) for validation and serialization . **End-to-End Message Flow** ```mermaid flowchart TD A[BacktestRequest] --> B[validate] B --> C{cache?} C -->|miss| D[DataAgent.fetch] C -->|hit| E[load cache] D --> F[save cache] E & F --> G[run_backtest] G --> H[store result] H --> I[BacktestResponse] ``` Everything runs locally except for calls to The Graph and (optionally) 1inch . **Setup & Usage** * **Dependencies**: `pip install -r requirements.txt` with `uagents==0.22.5`, Foundry (`forge`) for Solidity ≥0.8.26. * **Env Vars**: `export UNI_RPC_URL="your_rpc_url_here"` * **Run**: `python main.py` to start DataAgent (port 8001) and BacktestAgent (port 8002); or launch individually via `python data_agent.py` / `python backtest_agent.py` . * **Client Example**: A third “client” agent can programmatically send a `BacktestRequest` and await `BacktestResponse` . **Key Benefits** * **Efficient Data Access**: Subgraph queries eliminate the need for costly archive node RPC calls. * **Deterministic Results**: On-chain state is forked and replayed in a controlled environment for reproducibility. * **Decoupled, Async Architecture**: Clear separation of data fetching and simulation, with non-blocking message handling. **Extensibility & Roadmap** * **Additional Data Sources**: Extend `TheGraphClient` to other DeFi subgraphs. * **Custom Strategies**: Plug in new Solidity hooks for bespoke liquidity-provision ranges. * **Batch Testing**: Enable multi-pool or multi-period backtests in one request. * **Dashboard & CI**: Expose results via REST/React dashboard and integrate smoke tests in GitHub Actions .
How it's made
Here’s a deep dive into how we built the Uniswap V4 backtester—everything from the low-level tech choices to the glue code that makes it all hum: --- ## 1. Agent Framework & Messaging We leveraged **Fetch.ai’s uAgents** (v0.22.5) as our underlying RPC/message bus. Each agent is a standalone Python process: * **DataAgent** (`data_agent.py`) and **BacktestAgent** (`backtest_agent.py`) are both subclasses of `uagents.Agent`, exposing simple function-call endpoints over HTTP (ports 8001 and 8002). * All message schemas (`BacktestRequest`, `Events`, `BacktestResponse`) extend `uagents.Model` (Pydantic v1) for strict validation/serialization . * Agents communicate asynchronously: the BacktestAgent sends a `FetchEvents` to DataAgent, which replies with an `Events` payload. --- ## 2. Historical Data Collection **DataAgent** is responsible for fetching all mint/burn/swap events for a given Uniswap V4 pool: 1. **GraphQL Queries** to The Graph’s Uniswap V3/V4 subgraph at `https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3` 2. **Async HTTP** via `aiohttp` with an **exponential back-off** and up to **3 retries** on failures . 3. **Pagination** under-the-hood to handle large time ranges. 4. **Normalization** into a `PoolEvent` schema and return as an `Events` model. 5. **Caching**: a `BacktestDataManager.cache_graph_data()` layer stores previously-fetched windows to avoid redundant downloads on overlapping backtests . --- ## 3. Deterministic Backtesting via Foundry The **BacktestAgent** drives an on-chain fork simulation using **Foundry**: * A Python wrapper (`run_backtest`) writes the JSON bundle to `src/data/pool-events.json` and shells out to: ```bash forge script src/UniV4Backtester.s.sol \ --fork-url $UNI_RPC_URL \ --json ``` Foundry (Solidity ≥0.8.26) uses `stdJson` to ingest the events file byte-for-byte, replaying each mint/swap/burn on a forked RPC for exact reproducibility . * The wrapper captures stdout JSON (PnL, Sharpe, fees, impermanent loss, gas costs) and parses it back into our `BacktestResponse` model. --- ## 4. Glue & Orchestration * **Ports & Endpoints** * DataAgent: `http://127.0.0.1:8001/submit` * BacktestAgent: `http://127.0.0.1:8002/submit`  * **Flow** 1. Receive `BacktestRequest` (pool address or shorthand like “USDC-ETH”, start/end UNIX timestamps, optional strategy params). 2. Resolve symbols to on-chain addresses. 3. Request events from DataAgent (or load from cache). 4. Invoke Foundry script via subprocess. 5. Persist results with `BacktestDataManager.save_backtest_result()`. 6. Reply with typed metrics to the caller . ```mermaid flowchart TD A[BacktestRequest] --> B{Cache?} B -->|miss| C[DataAgent.fetchEvents] B -->|hit| D[Load cached events] C & D --> E[run_backtest (Foundry)] E --> F[Save & reply BacktestResponse] ``` --- ## 5. Partner Technologies & “Hacks” * **The Graph** Replaces costly archive-node RPCs with subgraph queries for sub-second historical lookups . * **Foundry** (Paradigm) Its `stdJson` library let us avoid writing a custom Solidity parser—just dump the same JSON the test harness expects, and replay it natively in solidity . * **Future 1inch Integration** We’ve stubbed in an enrichment step for fetching slippage quotes from 1inch, to more accurately model real trading costs . * **Byte-for-Byte Compatibility** Ensuring our Python-generated JSON matched exactly the format the Foundry script reads was surprisingly finicky—tweaking numeric types and ordering fields became a “hacky” but crucial step in getting deterministic simulation. * **Asynchronous, Non-Blocking Design** By decoupling data fetching from backtesting, we can pipeline multiple backtests or extend to batch-mode with minimal changes. --- ### In Summary This project stitches together: 1. **uAgents** for lightweight RPC/message passing. 2. **The Graph** for lightning-fast historical DeFi data. 3. **Foundry** for rock-solid, reproducible on-chain simulation. 4. **Python async & caching** to glue it all with resilience and performance. The result is a fully local, extensible, and deterministic Uniswap V4 backtester—powered by agent-oriented design and on-chain tooling.