Skip to main content

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)

prices.csv Format

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
ColumnTypeDescription
tsu64Unix timestamp in seconds
tokenu64Token ID (1=ETH, 2=USDC, 3=wstETH, etc.)
price_e18stringPrice 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

1

Engine queries oracle

At each tick, the engine looks up the latest price at or before the current ts for each token.
2

Oracle returns closest price

let oracle = load_prices_csv("prices.csv")?;
let (ts, price_e18) = oracle.latest_at("1", 1725000150)?;
// Returns: (1725000100, 1600000000000000000000)
3

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.00 initially, then 1.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

1

Use Python for complex scenarios

Generate CSV with NumPy/Pandas for scenarios like mean reversion, GARCH volatility, or historical replays.
2

Align timestamps with your sim config

Ensure start_ts in your YAML matches the first entry in prices.csv.
3

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): 20002000 → 1950 → $1900
  • USDC (2): 1.001.00 → 1.00 → $0.999
  • wstETH (3): 21002100 → 2050 → $2000

Next Steps

Build docs developers (and LLMs) love