Overview
The replay system lets you feed protocols with historical chain events (like index bumps, admin changes, liquidity updates) before or during a simulation. This is essential for:
- Historical backtests using real on-chain data
- Stress testing with protocol parameter changes
- Scenario replays (e.g., simulating a past liquidation cascade)
Create a CSV with columns: ts, target, market_hint, payload_json
ts,target,market_hint,payload_json
1725000000,aave-v3,,"{""kind"":""set_reserve_factor"",""token"":1,""factor_bps"":500}"
1725000100,lido,,"{""kind"":""bump_exchange_rate"",""delta_ray"":1000000000000000000000}"
1725000200,uniswap-v3,WETH/USDC:500,"{""kind"":""admin_set_fee"",""new_fee_bps"":30}"
| Column | Type | Description |
|---|
ts | u64 | Unix timestamp (seconds) when event occurs |
target | string | Protocol ID (must match Protocol::id()) |
market_hint | string | Optional routing hint (e.g., pool pair) |
payload_json | string | JSON-encoded event payload (protocol-specific) |
Ensure payload_json is properly escaped for CSV. Use double-double-quotes ("") inside the JSON string, or use a CSV library to handle escaping.
Loading Events
Load CSV into memory
use ank_replay::load_routed_csv;
let events = load_routed_csv("apps/cli/examples/events.csv")?;
// Vec<(String, ChainEvent)>
Apply to protocol registry
use ank_replay::apply_to_registry;
use indexmap::IndexMap;
use ank_protocol::Protocol;
let mut protocols: IndexMap<String, Box<dyn Protocol>> = /* ... */;
apply_to_registry(&mut protocols, &events)?;
This calls protocol.apply_historical(ts, market_hint, payload) for each event.
apply_historical Method
Protocols implement this trait method to handle historical events:
use anyhow::Result;
use ank_accounting::Timestamp;
fn apply_historical(
&mut self,
ts: Timestamp,
market_hint: Option<String>,
payload: serde_json::Value,
) -> Result<()> {
let kind = payload["kind"].as_str().unwrap_or("");
match kind {
"set_reserve_factor" => {
let token = payload["token"].as_u64().unwrap() as u32;
let factor_bps = payload["factor_bps"].as_u64().unwrap() as u64;
self.reserves.get_mut(&token).unwrap().reserve_factor_bps = factor_bps;
}
"bump_liquidity_index" => {
let token = payload["token"].as_u64().unwrap() as u32;
let delta = payload["delta_ray"].as_str().unwrap().parse::<u128>()?;
self.reserves.get_mut(&token).unwrap().liquidity_index_ray += delta;
}
_ => anyhow::bail!("Unknown event kind: {}", kind),
}
Ok(())
}
Event Payloads by Protocol
Aave V3
Set reserve factor:
{
"kind": "set_reserve_factor",
"token": 1,
"factor_bps": 500
}
Bump liquidity index:
{
"kind": "bump_liquidity_index",
"token": 1,
"delta_ray": "1000000000000000000000000000"
}
Admin liquidation threshold change:
{
"kind": "set_liq_threshold",
"token": 3,
"threshold_bps": 7500
}
Lido
Manually bump exchange rate:
{
"kind": "bump_exchange_rate",
"delta_ray": "10000000000000000000000"
}
Set new APY:
{
"kind": "set_apy",
"apy_bps": 450
}
Uniswap V3
Admin fee change:
{
"kind": "admin_set_fee",
"new_fee_bps": 30
}
Inject liquidity (for testing):
{
"kind": "inject_liquidity",
"tick_lower": -887220,
"tick_upper": 887220,
"liquidity": "100000000000000000000"
}
Per-Tick Replay
Apply events only at specific timestamps during the simulation:
use ank_replay::apply_rows_at_ts;
let events = load_routed_csv("events.csv")?;
// In your tick loop:
for tick in 0..steps {
let ts = start_ts + tick;
// Apply any events scheduled for this ts
apply_rows_at_ts(&mut protocols, &events, ts)?;
// Run strategy as usual
engine.tick(user, |ctx, prots, portfolios| {
// ...
})?;
}
This ensures events are applied before the strategy runs for that tick.
Example: Historical Lido Rate Bump
Suppose Lido’s exchange rate jumped on 2023-09-01 at 12:00 UTC (ts=1693569600):
ts,target,market_hint,payload_json
1693569600,lido,,"{""kind"":""bump_exchange_rate"",""delta_ray"":""5000000000000000000000000""}"
Load and apply:
let events = load_routed_csv("lido_bump.csv")?;
apply_to_registry(&mut protocols, &events)?;
Now when your simulation reaches ts=1693569600, Lido’s ER will reflect the historical bump.
CSV Generation from Chain Data
Use a script to extract events from blockchain logs:
import pandas as pd
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/...'))
# Example: Aave ReserveFactorChanged events
ABI = [ /* event ABI */ ]
contract = w3.eth.contract(address='0x...', abi=ABI)
events = contract.events.ReserveFactorChanged.getLogs(
fromBlock=18000000,
toBlock=18100000
)
rows = []
for e in events:
ts = w3.eth.get_block(e['blockNumber'])['timestamp']
payload = {
"kind": "set_reserve_factor",
"token": e['args']['asset'], # map to your token ID
"factor_bps": e['args']['newFactor']
}
rows.append({
'ts': ts,
'target': 'aave-v3',
'market_hint': '',
'payload_json': json.dumps(payload)
})
df = pd.DataFrame(rows)
df.to_csv('aave_events.csv', index=False)
Combining with Price Oracle
Replay both events and prices:
steps: 1000
start_ts: 1693569600
user: 1
log_level: INFO
risk_out_csv: "risk_out.csv"
prices_csv: "historical_prices.csv" # Price feed
events_csv: "historical_events.csv" # Protocol events
leverage:
token: 1
initial_deposit_units: 10000
target_ltv_bps: 7000
band_bps: 250
This gives you a full historical replay with real market conditions and protocol state changes.
Programmatic Event Injection
Manually create ChainEvent structs:
use ank_replay::{ChainEvent, apply_to_registry};
use serde_json::json;
let events = vec![
(
"aave-v3".to_string(),
ChainEvent {
ts: 1725000000,
market_hint: None,
payload: json!({
"kind": "set_reserve_factor",
"token": 1,
"factor_bps": 600
}),
},
),
(
"lido".to_string(),
ChainEvent {
ts: 1725000100,
market_hint: None,
payload: json!({
"kind": "bump_exchange_rate",
"delta_ray": "2000000000000000000000000"
}),
},
),
];
apply_to_registry(&mut protocols, &events)?;
Troubleshooting
Check protocol ID spelling
Ensure target in CSV matches protocol.id() exactly (case-sensitive).
Validate JSON syntax
Use a JSON validator on your payload_json column. Common errors: missing quotes, unescaped quotes.
Handle missing fields gracefully
Protocols should return descriptive errors if required payload fields are missing:let token = payload["token"].as_u64()
.ok_or_else(|| anyhow::anyhow!("Missing 'token' field in payload"))?;
CSV Escaping Helper (Python)
import csv
import json
events = [
{
'ts': 1725000000,
'target': 'aave-v3',
'market_hint': '',
'payload_json': json.dumps({"kind": "set_reserve_factor", "token": 1, "factor_bps": 500})
},
]
with open('events.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['ts', 'target', 'market_hint', 'payload_json'])
writer.writeheader()
writer.writerows(events)
This auto-escapes JSON quotes correctly.
Next Steps