Skip to content
/ ank Public

A modular, deterministic on-chain simulator for building and backtesting execution strategies across protocols.

Notifications You must be signed in to change notification settings

auralshin/ank

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ANK — Backtesting Engine

A modular, deterministic on-chain simulator for building and backtesting execution strategies across protocols. Core ideas:

  • A small Engine that advances time (ticks), routes TxBundles to protocols, and tracks wallet balances.
  • Pluggable Protocols (Aave V3, Lido wstETH, Uniswap V3, Pendle) implementing a common trait.
  • A Strategy is just code that, each tick, inspects view_market/view_user + wallet and emits actions.
  • A focused Risk Layer (CSV logs + summary metrics) and Optimization scaffolding.

Features (current)

  • Protocols

    • Aave V3 (multi-asset): deposit/borrow/repay, withdraw (HF-safe), dynamic set_price, per-tick variable debt accrual, liquidation (fractional).
    • Lido (wstETH): stake/unstake with exchange_rate_ray growth per tick; used as Aave collateral; price auto-synced.
    • Uniswap V3 (toy): fixed-fee pool for simple flows / price impact tests.
    • Pendle (full): SY wrapper, PT/YT mint/claim/redeem, PT↔SY AMM with LP shares & fees, admin rate/maturity.
  • Strategy layer

    • Example: cross-protocol leverage loop (borrow ETH → stake in Lido → wstETH → deposit to Aave; deleverage with withdraw→unstake→repay).
    • Inline planner closure; easy to refactor into a Strategy trait for a strategy library.
  • Risk & analytics (M4)

    • Per-tick CSV (risk_out.csv): wallet/deposits/debt/net, LTV, HF, drawdown, units (for plotting).
    • RiskAccumulator: min/max HF, max drawdown, simple volatility; writes a summary row at end.
    • Oracle CSV: optional price paths (overrides protocol static prices), enabling shocks/stress runs.
  • Replay

    • Apply events_csv to mutate protocol state before sim starts (e.g., index bumps, admin changes).
  • Optimization (scaffold)

    • optimize binary runs simple parameter sweeps (e.g., target_ltv_bps, band_bps, initial_deposit_units) and scores runs.

Repo layout

core/                 # accounting, engine, exec, math, protocol trait, risk, oracle, replay
protocols/
  aave-v3/            # Aave V3 simulation (multi-asset + withdraw + set_price + liquidation)
  lido/               # Lido wstETH (stake/unstake, reward rate via exchange_rate_ray)
  uniswap-v3/         # toy pool for swaps
  pendle/             # full PT/YT + SY + AMM
strategy/
  sdk/                # (WIP) Strategy SDK crate
  examples/           # (WIP) strategy examples
apps/
  cli/                # binaries: ank-cli (sim runner), optimize (sweeps)
    examples/
      sim.yaml        # sample run config
      prices.csv      # optional oracle path (ETH, wstETH, etc.)
      sweep.yaml      # grid of params
      objectives.yaml # weights for optimizer score

Build

rustup default stable
cargo build

If cargo run complains about multiple binaries: always pass --bin:

  • cargo run -p ank-cli --bin ank-cli -- --config apps/cli/examples/sim.yaml
  • cargo run -p ank-cli --bin optimize -- --sweep apps/cli/examples/sweep.yaml --objectives apps/cli/examples/objectives.yaml

Quickstart: Lido ↔ Aave cross-protocol leverage

  1. Tokens (by convention):

    • 1 = ETH, 2 = USDC (toy), 3 = wstETH, Pendle uses 4=SY, 5=PT, 6=YT, 7=LP.
  2. Example config (apps/cli/examples/sim.yaml)

    steps: 24
    start_ts: 1725000000   # unix seconds (no underscores)
    user: 1
    log_level: INFO
    risk_out_csv: "apps/cli/examples/risk_out.csv"
    
    leverage:
      token: 1                   # borrow ETH
      initial_deposit_units: 10000
      target_ltv_bps: 7000       # 70%
      band_bps: 250              # ±2.5%
    # prices_csv: "apps/cli/examples/prices.csv"   # optional: price path
  3. Run

cargo run -p ank-cli --bin ank-cli -- --config apps/cli/examples/sim.yaml

You should see:

  • Step 0: stake ETH → receive wstETH; deposit wstETH to Aave.

  • Each tick: sync Aave wstETH price from Lido ER × ETH; controller:

    • below band → borrow ETH → stake → (deposit next tick)
    • above band → repay, and if wallet ETH is short → withdraw wstETH → unstake → repay (mandatory unwind).

Risk output (CSV)

Per-tick columns

ts, wallet_value_e18, deposit_value_e18, debt_value_e18,
net_value_e18, ltv_bps, hf_bps, deposits_units, debt_units, drawdown_e18

Summary rows

  • two rows: a header SUMMARY,... and the data row with:

    • minHF_bps, maxHF_bps, max_drawdown_e18, vol_e18, ticks

These are suitable for plotting LTV/HF/Net vs time and evaluating stress runs.


Oracle price paths (optional)

apps/cli/examples/prices.csv (header + rows; simplest shape):

ts,token,price_e18
1725000000,1,2000_000000000000000000
1725000000,3,2000_000000000000000000    # if you want to override; else wstETH price is synced from Lido ER
1725000100,1,1600_000000000000000000    # crash ETH at t+100
...
  • Engine uses these if present; otherwise, it falls back to protocol prices.
  • Lido → Aave wstETH price is still set each tick via set_price from Lido ER × ETH.

Optimizer (sweeps)

Example sweep.yaml:

base_config: "apps/cli/examples/sim.yaml"
params:
  target_ltv_bps: [6000, 6500, 7000, 7500]
  band_bps: [50, 100, 250]
  initial_deposit_units: [5000, 10000, 20000]
top_k: 10
out_csv: "apps/cli/examples/opt_results.csv"

Example objectives.yaml:

weights:
  terminal_net_e18:  1.0
  min_hf_bps:        0.2
  max_drawdown_e18: -0.5
  vol_e18:          -0.2

Run:

cargo run -p ank-cli --bin optimize -- --sweep apps/cli/examples/sweep.yaml --objectives apps/cli/examples/objectives.yaml

Produces opt_results.csv with candidate params and scores; prints Top-K to stdout.


Pendle (PT/YT + SY + AMM)

Add to registry (example mapping shown above):

let pendle = ank_protocol_pendle::Pendle::new(ank_protocol_pendle::PendleConfig {
  token_underlying: TokenId(1),  // ETH
  token_sy:  TokenId(4),
  token_pt:  TokenId(5),
  token_yt:  TokenId(6),
  token_lp:  TokenId(7),
  yield_rate_ray_per_tick: 1_000_000_000_000_000_000_000u128,
  maturity_ts: cfg.start_ts + 10_000,
  amm_fee_bps: 30,
});
protocols.insert("pendle".into(), Box::new(pendle));

Common actions (from a planner):

  • {"kind":"wrap_sy","amount_underlying":"1000"}
  • {"kind":"mint","amount_underlying":"1000"} or {"kind":"mint_from_sy","sy_shares":"1000"}
  • {"kind":"claim"}, {"kind":"redeem_pt","shares":"..."} (post-maturity), {"kind":"redeem_yt","shares":"..."}
  • AMM: lp_add, lp_remove, swap_exact_pt_for_sy, swap_exact_sy_for_pt (with min_out)

This lets you explore PT discount curves vs SY (rate views) and compose with Aave/Lido.


Strategy development

Right now strategies are closures:

let mut planner = move |ctx: EngineCtx,
                        prots: &IndexMap<String, Box<dyn Protocol>>,
                        portfolios: &IndexMap<UserId, Balances>| -> Vec<TxBundle> {
  // Look at prots["aave-v3"].view_market(), ... and emit vec![TxBundle { txs }]
};

To scale, introduce:

pub trait Strategy {
  fn on_step(
    &mut self,
    ctx: EngineCtx,
    prots: &IndexMap<String, Box<dyn Protocol>>,
    portfolios: &IndexMap<UserId, Balances>
  ) -> Vec<TxBundle>;
}

Then load strategy type + params from YAML for a strategy registry.


Troubleshooting

  • Multiple binaries error: cargo run could not determine which binary to run → Use --bin ank-cli or --bin optimize.

  • YAML integers start_ts: invalid type: string "1_725_..." → Use plain integers (no underscores) or don’t quote numbers.

  • Serde for IndexMap Ensure indexmap has the serde feature enabled in workspace; otherwise Serialize/Deserialize fails.

  • Borrow checker (Aave resource/position) When mutably borrowing a reserve and immutably querying user pos, split blocks or clone values to avoid E0499/E0502.

  • Name collisions (out variable) Don’t reuse out for both your ExecOutcome and local amounts; rename local to amt_out.

  • HF violations on withdraw The Aave withdraw gate enforces HF≥1 post-action. If capped, the error is intentional—reduce requested amount or repay first.


Roadmap (next milestones)

  • M5 – Strategy library & reporting

    • Formal Strategy trait + registry; multiple strategies selectable via YAML.
    • Rich end-of-run report (Top-K from optimizer, plots for LTV/HF/Net/Drawdown).
    • Scenario packs (shock sets) and batch runner.
  • M6 – Web showcase / notebooks

    • Minimal web UI to load configs, run sims, and visualize metrics.
    • Python bindings or notebooks for advanced analysis & calibration.

License / Notes

This is a research/backtesting tool. It does not simulate all production risks (MEV, oracle latency, gas, slippage nuances, liquidation bonuses across chains). Protocol models are simplified; use cautiously for decision-making.


TS Bindings

TS_RS_EXPORT_DIR="$(pwd)/bindings/types" cargo ts

About

A modular, deterministic on-chain simulator for building and backtesting execution strategies across protocols.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages