Skip to main content

Overview

ANK provides a dedicated risk layer that tracks per-tick metrics (wallet value, LTV, health factor, drawdown) and computes summary statistics (Sharpe ratio, volatility, max drawdown).

CSV Output Format

Enable CSV logging by setting risk_out_csv in your config:
risk_out_csv: "apps/cli/examples/risk_out.csv"

Per-Tick Columns

Each tick writes a row with these columns:
ts, wallet_value_e18, deposit_value_e18, debt_value_e18, net_value_e18, ltv_bps, hf_bps, deposits_units, debt_units, drawdown_e18
ColumnDescription
tsUnix timestamp (seconds)
wallet_value_e18Total wallet assets in e18 units
deposit_value_e18Collateral value deposited (e18)
debt_value_e18Total debt value (e18)
net_value_e18Equity = deposits + wallet - debt (e18)
ltv_bpsLoan-to-value in basis points (7000 = 70%)
hf_bpsHealth factor in basis points (10000 = 1.0, higher is safer)
deposits_unitsRaw deposit units (protocol-specific)
debt_unitsRaw debt units (protocol-specific)
drawdown_e18Current drawdown from running peak (e18)
All _e18 values use 1e18 scaling (1 unit = 1,000,000,000,000,000,000). Divide by 1e18 to get human-readable amounts.

Summary Rows

At the end of the run, two special rows are appended:
  1. Header row: SUMMARY, minHF_bps, maxHF_bps, max_drawdown_e18, vol_e18, ticks
  2. Data row: Contains the computed summary metrics
SUMMARY, minHF_bps, maxHF_bps, max_drawdown_e18, vol_e18, ticks
SUMMARY, 8543, 15234, 123456789012345678, 45678901234567890, 100
MetricDescription
minHF_bpsMinimum observed health factor
maxHF_bpsMaximum observed health factor
max_drawdown_e18Maximum drawdown from peak equity (e18)
vol_e18Per-tick volatility estimate (e18)
ticksNumber of ticks observed

RiskAccumulator API

Programmatically track risk metrics using RiskAccumulator:
1

Initialize the accumulator

use ank_risk::RiskAccumulator;

let mut risk = RiskAccumulator::default();
2

Update each tick

let ts = 1725000000u64;
let net_value_e18 = 10_000u128 * 1_000_000_000_000_000_000; // 10k units
let hf_bps = 12_000u64; // HF = 1.2

let drawdown = risk.update(ts, net_value_e18, hf_bps);
println!("Current drawdown: {} e18", drawdown);
3

Finalize and get summary

let summary = risk.finalize();

println!("Ticks: {}", summary.ticks.0);
println!("Min HF: {} bps", summary.min_hf_bps.0);
println!("Max DD: {} e18", summary.max_drawdown_e18.0);
println!("Sharpe: {:.2}", summary.sharpe);
println!("Sortino: {:.2}", summary.sortino);
println!("Vol (annual): {:.4}", summary.vol_annual);
println!("APR: {:.2}%", summary.apr * 100.0);
println!("APY: {:.2}%", summary.apy * 100.0);
println!("Total Return: {:.2}%", summary.total_return * 100.0);

RiskSummary Fields

The RiskSummary struct contains:
pub struct RiskSummary {
    pub ticks: U64S,
    pub min_hf_bps: U64S,
    pub max_hf_bps: U64S,
    pub max_drawdown_e18: U128S,

    pub vol_annual: f64,       // Annualized volatility
    pub sharpe: f64,           // Sharpe ratio (rf=0)
    pub sortino: f64,          // Sortino ratio (rf=0)

    pub terminal_net_e18: U128S,
    pub initial_net_e18: U128S,
    pub total_pnl_e18: I128S,

    pub total_return: f64,     // (terminal / initial) - 1
    pub apr: f64,              // Annualized return (linear)
    pub apy: f64,              // Annualized yield (compounded)

    pub wins: U64S,            // Ticks with positive returns
    pub losses: U64S,          // Ticks with negative returns
    pub win_rate: f64,         // wins / (wins + losses)
}
Health Factor (HF) is in basis points: HF < 10000 (1.0) triggers liquidation risk. Always monitor min_hf_bps to ensure your strategy stays safe.

Example Config with Risk CSV

steps: 100
start_ts: 1725000000
user: 1
log_level: INFO
risk_out_csv: "risk_out.csv"

leverage:
  token: 1
  initial_deposit_units: 10000
  target_ltv_bps: 7000
  band_bps: 250

Computing Aave Values

Use compute_aave_values_from_views to derive risk metrics from protocol state:
use ank_risk::compute_aave_values_from_views;

let aave_user = prots["aave-v3"].view_user(UserId(1));
let aave_mkt = prots["aave-v3"].view_market();

let (deposit_value_e18, debt_value_e18, ltv_bps, hf_bps) =
  compute_aave_values_from_views(&aave_user, &aave_mkt);

println!("Deposits: {} e18", deposit_value_e18);
println!("Debt: {} e18", debt_value_e18);
println!("LTV: {}% ({}bps)", ltv_bps / 100, ltv_bps);
println!("HF: {:.2} ({}bps)", hf_bps as f64 / 10_000.0, hf_bps);

Expected View Shapes

Aave Market View:
{
  "reserves": {
    "ETH": {
      "price_e18": "2000000000000000000000",
      "liq_threshold_bps": 8000
    },
    "wstETH": {
      "price_e18": "2200000000000000000000",
      "liq_threshold_bps": 7500
    }
  }
}
Aave User View:
{
  "deposits": {
    "wstETH": "5000000000000000000000"
  },
  "debts": {
    "ETH": "3000000000000000000000"
  }
}

Plotting Risk Metrics

The CSV format is designed for easy plotting:
import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('risk_out.csv')

# Filter out summary rows
df = df[df['ts'].notna()]
df['ts'] = pd.to_numeric(df['ts'])
df['net_value_e18'] = pd.to_numeric(df['net_value_e18'])
df['ltv_bps'] = pd.to_numeric(df['ltv_bps'])
df['hf_bps'] = pd.to_numeric(df['hf_bps'])

# Plot net value over time
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
plt.plot(df['ts'], df['net_value_e18'] / 1e18)
plt.title('Net Equity')
plt.ylabel('Units')

# Plot LTV
plt.subplot(1, 3, 2)
plt.plot(df['ts'], df['ltv_bps'] / 100)
plt.axhline(y=70, color='r', linestyle='--', label='Target')
plt.title('LTV %')
plt.ylabel('%')

# Plot Health Factor
plt.subplot(1, 3, 3)
plt.plot(df['ts'], df['hf_bps'] / 10000)
plt.axhline(y=1.0, color='r', linestyle='--', label='Liquidation')
plt.title('Health Factor')
plt.ylabel('HF')

plt.tight_layout()
plt.show()

Next Steps

Build docs developers (and LLMs) love