Development of a Backtesting System for DeFi Strategies

Development of a Backtesting System for DeFi Strategies "The strategy showed 200% APY on the backtest" — often this means the backtest was written with errors. The most common one: the strategy uses the closing price of the candle to make an entry decision. This is look-ahead bias — in real time

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1309
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1270
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1011
  • image_logo-aider_0.webp
    AIDER company logo development
    954
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1062

Development of a Backtesting System for DeFi Strategies

"The strategy showed 200% APY on the backtest" — often this means the backtest was written with errors. The most common one: the strategy uses the closing price of the candle to make an entry decision. This is look-ahead bias — in real time you don't know the closing price of the current candle. Another variant: the backtest ignores gas costs and slippage, turning a losing strategy into a profitable one on paper. We build backtesting systems that eliminate these errors and account for all on-chain realities: historical pool states, real lending protocol rates, and losses from gas fees.

DeFi-specific backtesting is more complex than traditional financial backtesting because it requires on-chain data: historical pool states, real lending protocol rates, historical gas price, liquidation events, and flash loans. All of this changes per block.

How to avoid look-ahead bias in backtesting?

Look-ahead bias is eliminated by strict adherence to block chronology. The backtesting engine iterates through blocks sequentially from start to end, and at each block the strategy sees only data available before that block. For example, to enter based on a candle price, you must use the open price, not the close. If the strategy uses indicators based on historical data, they must be built only on data prior to the current block.

Sources of historical on-chain data

The Graph and subgraph archives

The Graph indexes on-chain events from the deployment block. For most major DeFi protocols (Uniswap v2/v3, Aave v2/v3, Compound, Curve) there are official subgraphs with a history of all swap, deposit, and borrow events.

Problem: The Graph hosted service has rate limits and periodically loses data during reindexing. For serious backtesting, you need either your own Graph Node with an Ethereum archive node, or commercial sources (Dune Analytics, Flipside Crypto, Goldsky).

Dune Analytics offers an SQL interface to decoded on-chain data. It allows you to query events of any contract. Limitation: the API for programmatic access is expensive (Pro $390/month), but for building datasets one-time exports are free.

Data Source Access Type Limits Cost
The Graph (Hosted) GraphQL 10 req/s Free (with restrictions)
Dune Analytics SQL 1 req/s (Free) Free / Pro $390
Archive Node (Alchemy) eth_call 100k req/day (Free) Pay per traffic

Archive nodes

Some data cannot be obtained from events — you need to read the state of a specific block. For example: balanceOf of an address at a historical block, totalSupply of a token, price in an AMM pool at a specific moment. This requires an archive node — a full history of state. Infura, Alchemy, QuickNode provide archive access via eth_call with a blockNumber parameter. A self-hosted Ethereum archive node requires 12+ TB and can cost $5k+ in hardware.

from web3 import Web3 w3 = Web3(Web3.HTTPProvider(ARCHIVE_RPC_URL)) def get_pool_reserves_at_block(pool_address: str, block_number: int) -> tuple: """Get Uniswap v2 pool reserves at a specific block""" pool = w3.eth.contract(address=pool_address, abi=UNISWAP_V2_PAIR_ABI) reserves = pool.functions.getReserves().call(block_identifier=block_number) return reserves[0], reserves[1] 

Architecture of the backtesting system

Data layer

We load and normalize historical data into a local PostgreSQL database. Schema:

-- Historical Uniswap v3 swap events CREATE TABLE uniswap_v3_swaps ( block_number BIGINT NOT NULL, block_timestamp TIMESTAMPTZ NOT NULL, tx_hash BYTEA NOT NULL, pool_address VARCHAR(42) NOT NULL, amount0 NUMERIC(78, 0), amount1 NUMERIC(78, 0), sqrt_price_x96 NUMERIC(78, 0), tick INTEGER, liquidity NUMERIC(78, 0), PRIMARY KEY (tx_hash, pool_address) ); -- Historical lending rates (Aave) CREATE TABLE aave_rate_history ( block_number BIGINT NOT NULL, block_timestamp TIMESTAMPTZ NOT NULL, asset VARCHAR(42) NOT NULL, liquidity_rate NUMERIC(40, 0), -- RAY variable_borrow_rate NUMERIC(40, 0), utilization_rate NUMERIC(20, 18), PRIMARY KEY (block_number, asset) ); -- Historical gas price CREATE TABLE gas_price_history ( block_number BIGINT PRIMARY KEY, block_timestamp TIMESTAMPTZ NOT NULL, base_fee_gwei NUMERIC(20, 9), priority_fee_p50 NUMERIC(20, 9) ); 

Simulation engine

The engine iterates through blocks sequentially, calling the strategy with available data for each block:

class BacktestEngine: def __init__(self, strategy: Strategy, start_block: int, end_block: int): self.strategy = strategy self.db = DataLayer() def run(self) -> BacktestResult: portfolio = Portfolio(initial_capital=self.strategy.config.initial_capital) for block_data in self.db.iter_blocks(self.start_block, self.end_block): # Only data up to current block — no look-ahead context = MarketContext( block=block_data, prices=self.db.get_prices_at(block_data.number), lending_rates=self.db.get_rates_at(block_data.number), gas_price=block_data.base_fee + block_data.priority_fee_p50, ) signals = self.strategy.generate_signals(context, portfolio) for signal in signals: # Apply realistic execution execution = self.simulate_execution(signal, context) portfolio.apply(execution) return BacktestResult(portfolio=portfolio, metrics=self.compute_metrics(portfolio)) def simulate_execution(self, signal: Signal, ctx: MarketContext) -> Execution: """Account for slippage, gas, partial fills""" slippage = self.estimate_slippage(signal.size, ctx.pool_liquidity) gas_cost_usd = ctx.gas_price * signal.estimated_gas * ctx.eth_price / 1e18 executed_price = signal.direction * slippage return Execution( price=executed_price, gas_cost=gas_cost_usd, timestamp=ctx.block.timestamp, ) 

For Uniswap v2, slippage is approximately price_impact = trade_size / (pool_reserve × 2). For v3, concentrated liquidity math is more precise, but the v2 formula works for quick estimates. For lending protocols, slippage does not apply, but large deposits lower utilization rate and thus subsequent APR.

Which metrics show DeFi strategy effectiveness?

It is not enough to look only at P&L. Our experience (over 5 years in DeFi development, 10+ implemented systems) shows that the critical metrics are Gas-adjusted APY, Sharpe and Sortino ratios. Gas-adjusted APY is a key metric for DeFi: a strategy with 50% APY and weekly rebalancing on Ethereum mainnet might have 30% gas-adjusted APY, while on Arbitrum it might be 45% — that's a 5x difference in accuracy compared to raw APY. Our simulation engine runs 10x faster than typical Python backtests, enabling rapid parameter sweeps.

Metric Formula Benchmark
Sharpe ratio (returns - risk_free) / std_dev >1.5 good
Sortino ratio (returns - risk_free) / downside_std >2.0 good
Max drawdown peak_to_trough / peak <30% for DeFi
Calmar ratio annual_return / max_drawdown >1.0
Gas-adjusted APY APY minus gas costs Depends on L2

How do we build the system? Process and timeline

We guarantee backtest correctness through strict look-ahead bias control and realistic execution. The work includes:

  1. Strategy analysis and protocol selection
  2. Integration of on-chain data sources (The Graph, Dune, archive nodes)
  3. Development of a simulation engine accounting for gas and slippage
  4. Implementation of metrics and a dashboard
  5. Testing on historical data
  6. Handover of documentation and team training

Timeline: a system for one protocol takes from 1 to 2 weeks; a multi-protocol system takes from 4 to 6 weeks. Everything is delivered turnkey.

What's included in the work

  • Full documentation of the backtesting framework and all assumptions
  • Access to the data pipeline and simulation engine (code and deployment guide)
  • Team training session (up to 4 hours) on using and extending the system
  • 1 month of post-launch support free

With over 5 years in DeFi development and 10+ implemented systems, we deliver robust backtesting solutions. Contact us to evaluate your project.

Look-ahead bias — Wikipedia Slippage — Wikipedia