Overview
ANK’s oracle system lets you override protocol prices with custom time-series data from CSV files. This is essential for:
- Stress testing strategies against price crashes
- Historical backtests using real market data
- Scenario analysis (bull/bear/sideways markets)
Create a CSV file with three columns: ts, token, price_e18
ts,token,price_e18
1725000000,1,2000000000000000000000
1725000000,3,2000000000000000000000
1725000100,1,1600000000000000000000
1725000200,1,1200000000000000000000
1725000300,1,1800000000000000000000
| Column | Type | Description |
|---|
ts | u64 | Unix timestamp in seconds |
token | u64 | Token ID (1=ETH, 2=USDC, 3=wstETH, etc.) |
price_e18 | string | Price in 1e18 units (use underscores for readability) |
Use underscores in large numbers for clarity: 2_000_000000000000000000 = $2000 in e18 units.
Enabling Price Overrides
In your simulation config, add the prices_csv field:
steps: 100
start_ts: 1725000000
user: 1
log_level: INFO
risk_out_csv: "risk_out.csv"
prices_csv: "apps/cli/examples/prices.csv" # <-- Add this line
leverage:
token: 1
initial_deposit_units: 10000
target_ltv_bps: 7000
band_bps: 250
How Price Lookup Works
Engine queries oracle
At each tick, the engine looks up the latest price at or before the current ts for each token.
Oracle returns closest price
let oracle = load_prices_csv("prices.csv")?;
let (ts, price_e18) = oracle.latest_at("1", 1725000150)?;
// Returns: (1725000100, 1600000000000000000000)
Protocols may override internally
Some protocols (like Lido → Aave wstETH sync) compute prices dynamically and call set_price each tick, which overrides oracle values.
If you use prices_csv, ensure your protocol logic doesn’t conflict. For example, the Lido→Aave leverage strategy always syncs wstETH price from exchange_rate_ray × ETH_price, so an oracle entry for token 3 (wstETH) may be ignored.
Example: ETH Price Crash Scenario
Simulate a 40% crash over 200 seconds:
ts,token,price_e18
1725000000,1,2000000000000000000000
1725000050,1,1800000000000000000000
1725000100,1,1600000000000000000000
1725000150,1,1400000000000000000000
1725000200,1,1200000000000000000000
Run your backtest:
cargo run -p ank-cli --bin ank-cli -- --config sim.yaml
Check risk_out.csv to see how your strategy’s HF and LTV responded to the crash.
Shock Scenarios
Flash Crash (10% in 1 tick)
ts,token,price_e18
1725000000,1,2000000000000000000000
1725000001,1,1800000000000000000000
1725000002,1,1800000000000000000000
Gradual Bear Market (50% over 1000 ticks)
import pandas as pd
import numpy as np
start_ts = 1725000000
start_price = 2000e18
end_price = 1000e18
ticks = 1000
ts = np.arange(start_ts, start_ts + ticks)
prices = np.linspace(start_price, end_price, ticks)
df = pd.DataFrame({
'ts': ts,
'token': 1,
'price_e18': prices.astype(int)
})
df.to_csv('bear_market.csv', index=False)
Volatility Spike (random walk)
import random
ts = 1725000000
price = 2000e18
rows = []
for i in range(500):
rows.append(f"{ts},{1},{int(price)}")
ts += 1
# ±5% random walk
price *= random.uniform(0.95, 1.05)
with open('volatile.csv', 'w') as f:
f.write('ts,token,price_e18\n')
f.write('\n'.join(rows))
Protocol-Specific Price Sync
Lido → Aave wstETH
The Lido protocol maintains an exchange_rate_ray that grows each tick. Strategies typically sync this to Aave:
let lido_mkt = prots["lido"].view_market();
let er = lido_mkt["exchange_rate_ray"].as_str().unwrap().parse::<u128>()?;
let eth_price = 2000u128 * 1_000_000_000_000_000_000; // from oracle or config
// wstETH price = (exchange_rate_ray × ETH_price) / 1e27
let wsteth_price_e18 = (er * eth_price) / 1_000_000_000_000_000_000_000_000_000;
txs.push(Tx {
protocol: "aave-v3".into(),
action: Action::Custom(json!({
"kind": "set_price",
"token": 3,
"price_e18": wsteth_price_e18.to_string()
})),
gas_limit: None,
});
Static Overrides
For protocols that don’t auto-update prices, your oracle CSV is authoritative:
ts,token,price_e18
1725000000,2,1000000000000000000
1725000100,2,1005000000000000000
This sets USDC (token 2) to 1.00initially,then1.005 at t+100.
Loading Prices Programmatically
use ank_oracle::{load_prices_csv, Oracle};
let oracle = load_prices_csv("apps/cli/examples/prices.csv")?;
// Query latest price at or before ts
if let Some((last_ts, price_e18)) = oracle.latest_at("1", 1725000150) {
println!("ETH @ ts={}: {} e18", last_ts, price_e18);
}
// Get absolute latest
if let Some((ts, price)) = oracle.latest_e18("1") {
println!("Latest ETH: {} @ ts={}", price, ts);
}
// Convert to f64 (convenience)
if let Some(price_f64) = oracle.get_f64(1725000150, "1") {
println!("ETH price: ${:.2}", price_f64);
}
CSV Generation Tips
Use Python for complex scenarios
Generate CSV with NumPy/Pandas for scenarios like mean reversion, GARCH volatility, or historical replays.
Align timestamps with your sim config
Ensure start_ts in your YAML matches the first entry in prices.csv.
Include all relevant tokens
If your strategy uses ETH, wstETH, and USDC, provide prices for tokens 1, 3, and 2.
Example: Multi-Token Price Path
ts,token,price_e18
1725000000,1,2000000000000000000000
1725000000,2,1000000000000000000
1725000000,3,2100000000000000000000
1725000100,1,1950000000000000000000
1725000100,2,1000000000000000000
1725000100,3,2050000000000000000000
1725000200,1,1900000000000000000000
1725000200,2,999000000000000000
1725000200,3,2000000000000000000000
This sets:
- ETH (1): 2000→1950 → $1900
- USDC (2): 1.00→1.00 → $0.999
- wstETH (3): 2100→2050 → $2000
Next Steps