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:
- Strategy analysis and protocol selection
- Integration of on-chain data sources (The Graph, Dune, archive nodes)
- Development of a simulation engine accounting for gas and slippage
- Implementation of metrics and a dashboard
- Testing on historical data
- 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.







