Overview
ANK includes a built-in optimizer that sweeps strategy parameters (like target_ltv_bps, band_bps, initial_deposit_units) and scores runs based on weighted objectives (PnL, Sharpe, drawdown, etc.).
Quick Start
Create sweep.yaml
Define the parameter grid: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"
Create objectives.yaml
Define scoring weights:weights:
terminal_net_e18: 1.0
min_hf_bps: 0.2
max_drawdown_e18: -0.5
vol_e18: -0.2
- Positive weights = maximize (e.g.,
terminal_net_e18)
- Negative weights = minimize (e.g.,
max_drawdown_e18, vol_e18)
Run optimizer
cargo run -p ank-cli --bin optimize -- \
--sweep apps/cli/examples/sweep.yaml \
--objectives apps/cli/examples/objectives.yaml
base_config: "apps/cli/examples/sim.yaml" # Base simulation config
params:
# Each param is a list of values to try
target_ltv_bps: [6000, 6500, 7000, 7500, 8000]
band_bps: [50, 100, 150, 200, 250, 300]
initial_deposit_units: [5000, 7500, 10000, 15000, 20000]
top_k: 10 # Print top-K results to stdout
out_csv: "opt_results.csv" # Save all results to CSV
Parameter Grid
The optimizer runs a Cartesian product of all param values:
target_ltv_bps: 5 values
band_bps: 6 values
initial_deposit_units: 5 values
Total runs: 5 × 6 × 5 = 150 simulations
Grid size grows exponentially. For 4 params with 10 values each, you’ll run 10,000 simulations. Use smaller grids or switch to genetic/DE algorithms for large spaces.
weights:
# Maximize terminal equity
terminal_net_e18: 1.0
# Maximize minimum health factor (safety)
min_hf_bps: 0.2
# Minimize max drawdown (negative weight)
max_drawdown_e18: -0.5
# Minimize volatility (negative weight)
vol_e18: -0.2
# Optional: penalize low Sharpe
sharpe: 0.3
The optimizer computes a weighted sum for each run:
score = Σ (weight_i × metric_i)
Example:
score = 1.0 × terminal_net_e18
+ 0.2 × min_hf_bps
- 0.5 × max_drawdown_e18
- 0.2 × vol_e18
Higher scores are better. The optimizer ranks all candidates and prints the top-K.
Available Metrics
From RiskSummary (see Risk Analytics):
| Metric | Type | Description |
|---|
terminal_net_e18 | u128 | Final equity (e18) |
initial_net_e18 | u128 | Starting equity (e18) |
total_pnl_e18 | i128 | PnL = terminal - initial (e18) |
min_hf_bps | u64 | Minimum health factor (bps) |
max_hf_bps | u64 | Maximum health factor (bps) |
max_drawdown_e18 | u128 | Worst drawdown (e18) |
vol_e18 | f64 | Annualized volatility |
sharpe | f64 | Sharpe ratio (rf=0) |
sortino | f64 | Sortino ratio (rf=0) |
total_return | f64 | Total return (%) |
apr | f64 | Annualized return (linear) |
apy | f64 | Annualized yield (compounded) |
win_rate | f64 | Fraction of winning ticks |
Output: opt_results.csv
The optimizer writes all runs to CSV:
target_ltv_bps,band_bps,initial_deposit_units,score,terminal_net_e18,min_hf_bps,max_drawdown_e18,vol_e18
7000,250,10000,15234.5,12000000000000000000000,8500,500000000000000000000,0.12
7500,200,15000,14987.3,13500000000000000000000,7800,650000000000000000000,0.15
...
You can load this in Python/R for further analysis:
import pandas as pd
df = pd.read_csv('opt_results.csv')
df_sorted = df.sort_values('score', ascending=False)
print(df_sorted.head(10))
Top-K Results (stdout)
The optimizer also prints the top-K to console:
=== Top 10 Results ===
Rank 1: score=15234.5
target_ltv_bps: 7000
band_bps: 250
initial_deposit_units: 10000
terminal_net_e18: 12000000000000000000000
min_hf_bps: 8500
max_drawdown_e18: 500000000000000000000
sharpe: 1.85
Rank 2: score=14987.3
target_ltv_bps: 7500
band_bps: 200
initial_deposit_units: 15000
...
Advanced: Genetic Algorithm
For large parameter spaces, use Genetic Algorithm (GA) or Differential Evolution (DE):
use ank_opt::{Optimizer, Algo, Budget, Space};
let space = Space::new()
.real("target_ltv_bps", 6000.0, 8000.0)
.real("band_bps", 50.0, 300.0)
.int("initial_deposit_units", 5000, 20000).step(1000);
let mut opt = Optimizer::new(
Algo::ga(42).pop_size(30).gens(50),
None // No scheduler
);
let result = opt.run(&mut objective, &space, Budget::trials(500))?;
println!("Best score: {}", result.best.score);
DE (Differential Evolution)
let mut opt = Optimizer::new(
Algo::de(42).pop_size(20).f(0.7).cr(0.9),
None
);
Random Search
let mut opt = Optimizer::new(
Algo::random(42),
None
);
Multi-Objective Optimization
Balancing conflicting goals (e.g., max return vs. min risk):
weights:
# High return
terminal_net_e18: 1.0
# Low risk
max_drawdown_e18: -0.8
vol_e18: -0.3
# Safety constraint
min_hf_bps: 0.5
Tweak weights to explore the Pareto frontier (e.g., run multiple sweeps with different weight ratios).
Example: Find Best Leverage Band
sweep.yaml
base_config: "sim.yaml"
params:
target_ltv_bps: [6500, 7000, 7500]
band_bps: [100, 150, 200, 250, 300]
top_k: 5
out_csv: "band_sweep.csv"
objectives.yaml
weights:
terminal_net_e18: 1.0
min_hf_bps: 0.3 # Prefer safer strategies
max_drawdown_e18: -0.6
Run
cargo run -p ank-cli --bin optimize -- \
--sweep band_sweep.yaml \
--objectives objectives.yaml
Interpret Results
Look for:
- Highest score = best risk-adjusted return
- min_hf_bps ≥ 10500 = HF always above 1.05 (safe)
- Low max_drawdown_e18 = stable equity curve
Tips for Effective Optimization
Start with coarse grids
Use 3-5 values per param initially. Refine around promising regions.
Normalize weights
Large metrics (like terminal_net_e18 ~ 1e22) dominate small ones (like sharpe ~ 2.0). Scale weights accordingly or normalize metrics.
Cross-validate
Test top-K params on different price paths to avoid overfitting.
Parallelization (Future)
ANK’s optimizer currently runs serially. For 1000+ runs, consider:
- Splitting the grid into chunks and running multiple processes
- Using a job scheduler (SLURM, AWS Batch)
- Implementing a parallel backend (Rayon)
Next Steps