project screenshot 1
project screenshot 2
project screenshot 3
project screenshot 4
project screenshot 5
project screenshot 6

Backtest Agent

Fetch.ai uAgents & The Graph drive Uniswap backtester: DataAgent fetches events, Backtester tests

Backtest Agent

Created At

ETHGlobal Cannes

Project Description

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

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:

    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 .
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.

background image mobile

Join the mailing list

Get the latest news and updates