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).
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
| Column | Description |
|---|
ts | Unix timestamp (seconds) |
wallet_value_e18 | Total wallet assets in e18 units |
deposit_value_e18 | Collateral value deposited (e18) |
debt_value_e18 | Total debt value (e18) |
net_value_e18 | Equity = deposits + wallet - debt (e18) |
ltv_bps | Loan-to-value in basis points (7000 = 70%) |
hf_bps | Health factor in basis points (10000 = 1.0, higher is safer) |
deposits_units | Raw deposit units (protocol-specific) |
debt_units | Raw debt units (protocol-specific) |
drawdown_e18 | Current 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:
- Header row:
SUMMARY, minHF_bps, maxHF_bps, max_drawdown_e18, vol_e18, ticks
- Data row: Contains the computed summary metrics
SUMMARY, minHF_bps, maxHF_bps, max_drawdown_e18, vol_e18, ticks
SUMMARY, 8543, 15234, 123456789012345678, 45678901234567890, 100
| Metric | Description |
|---|
minHF_bps | Minimum observed health factor |
maxHF_bps | Maximum observed health factor |
max_drawdown_e18 | Maximum drawdown from peak equity (e18) |
vol_e18 | Per-tick volatility estimate (e18) |
ticks | Number of ticks observed |
RiskAccumulator API
Programmatically track risk metrics using RiskAccumulator:
Initialize the accumulator
use ank_risk::RiskAccumulator;
let mut risk = RiskAccumulator::default();
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);
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