Backtest Agent
Fetch.ai uAgents & The Graph drive Uniswap backtester: DataAgent fetches events, Backtester tests
Problem Statement
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 & ComponentsAt its core, the system comprises two autonomous uAgents running locally:DataAgent: Listens forFetchEventsmessages, 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-facingBacktestRequestmessages (specifying pool address, start/end UNIX timestamps, and optional strategy parameters), validates inputs, invokes the DataAgent to fetch events, writes a byte-for-byte compatibleevents.jsonfile, and calls a Foundry-based Solidity backtester to simulate on-chain behavior .DataAgent WorkflowPagination & Resilience: Paginates GraphQL queries againsthttps://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3, with exponential back-off and up to three retries usingaiohttp.Optional Enrichment: (Future) Augments events with 1inch swap quotes to model realistic slippage.Normalization: Transforms raw GraphQL output into a standardizedPoolEventschema and sends anEventsmodel payload back to the BacktestAgent .Caching: Uses aBacktestDataManager.cache_graph_data()layer to avoid redundant downloads when re-testing overlapping time windows .BacktestAgent OrchestrationReceive & Validate: On incomingBacktestRequest, shorthand pool symbols (e.g. “USDC-ETH”) are resolved to on-chain addresses.Fetch Events: SendsFetchEventsto the DataAgent and awaits anEventsresponse.Simulation Invocation: Runs a Python wrapper that writessrc/data/pool-events.jsonand executes the Foundry scriptsrc/UniV4Backtester.s.solviaforge script … --json, forking a live RPC atUNI_RPC_URLto ensure determinism .Persistence & Reply: Saves the returned JSON metrics (PnL, Sharpe, fees, impermanent loss, gas costs) viaBacktestDataManager.save_backtest_result(), then replies with a typedBacktestResponsemodel back to the original requester .Message & Data ModelsBacktestRequest:{ 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 extenduagents.Model(Pydantic v1) for validation and serialization .End-to-End Message Flowflowchart 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 & UsageDependencies:pip install -r requirements.txtwithuagents==0.22.5, Foundry (forge) for Solidity ≥0.8.26.Env Vars:export UNI_RPC_URL="your_rpc_url_here"Run:python main.pyto start DataAgent (port 8001) and BacktestAgent (port 8002); or launch individually viapython data_agent.py/python backtest_agent.py.Client Example: A third “client” agent can programmatically send aBacktestRequestand awaitBacktestResponse.Key BenefitsEfficient 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 & RoadmapAdditional Data Sources: ExtendTheGraphClientto 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 .
Solution
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 & MessagingWe leveragedFetch.ai’s uAgents(v0.22.5) as our underlying RPC/message bus. Each agent is a standalone Python process:DataAgent(data_agent.py) andBacktestAgent(backtest_agent.py) are both subclasses ofuagents.Agent, exposing simple function-call endpoints over HTTP (ports 8001 and 8002).All message schemas (BacktestRequest,Events,BacktestResponse) extenduagents.Model(Pydantic v1) for strict validation/serialization .Agents communicate asynchronously: the BacktestAgent sends aFetchEventsto DataAgent, which replies with anEventspayload.2. Historical Data CollectionDataAgentis responsible for fetching all mint/burn/swap events for a given Uniswap V4 pool:GraphQL Queriesto The Graph’s Uniswap V3/V4 subgraph athttps://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3Async HTTPviaaiohttpwith anexponential back-offand up to3 retrieson failures .Paginationunder-the-hood to handle large time ranges.Normalizationinto aPoolEventschema and return as anEventsmodel.Caching: aBacktestDataManager.cache_graph_data()layer stores previously-fetched windows to avoid redundant downloads on overlapping backtests .3. Deterministic Backtesting via FoundryTheBacktestAgentdrives an on-chain fork simulation usingFoundry:A Python wrapper (run_backtest) writes the JSON bundle tosrc/data/pool-events.jsonand shells out to:forge script src/UniV4Backtester.s.sol \ --fork-url $UNI_RPC_URL \ --jsonFoundry (Solidity ≥0.8.26) usesstdJsonto 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 ourBacktestResponsemodel.4. Glue & OrchestrationPorts & EndpointsDataAgent:http://127.0.0.1:8001/submitBacktestAgent:http://127.0.0.1:8002/submitFlowReceiveBacktestRequest(pool address or shorthand like “USDC-ETH”, start/end UNIX timestamps, optional strategy params).Resolve symbols to on-chain addresses.Request events from DataAgent (or load from cache).Invoke Foundry script via subprocess.Persist results withBacktestDataManager.save_backtest_result().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 GraphReplaces costly archive-node RPCs with subgraph queries for sub-second historical lookups .Foundry(Paradigm) ItsstdJsonlibrary 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 IntegrationWe’ve stubbed in an enrichment step for fetching slippage quotes from 1inch, to more accurately model real trading costs .Byte-for-Byte CompatibilityEnsuring 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 DesignBy decoupling data fetching from backtesting, we can pipeline multiple backtests or extend to batch-mode with minimal changes.In SummaryThis project stitches together:uAgentsfor lightweight RPC/message passing.The Graphfor lightning-fast historical DeFi data.Foundryfor rock-solid, reproducible on-chain simulation.Python async & cachingto 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.
Hackathon
ETHGlobal Cannes
2025