The Engine is the heart of ANK’s backtesting framework. It orchestrates protocol execution, manages user portfolios, and advances time through discrete ticks.
Engine Struct
The Engine struct is defined in core/engine/src/lib.rs:core/engine/src/lib.rs:101-115:
pub struct Engine {
/// Registered protocol instances (by id)
pub protocols : IndexMap < String , Box < dyn Protocol >>,
/// User wallets (token → amount e18)
pub portfolios : IndexMap < UserId , Balances >,
/// Current timestamp
pub ts : Timestamp ,
/// Current tick index
pub step_idx : u64 ,
/// Metrics counters
pub metrics : EngineMetrics ,
/// Optional fee configuration (requires the `fees` feature)
#[cfg(feature = "fees" )]
fees : Option < FeesConfig >,
}
Key Fields
protocols : Registry of all protocol instances, keyed by protocol ID (e.g., "aave-v3", "lido")
portfolios : Per-user token balances, updated after each transaction
ts : Current simulation timestamp (typically Unix seconds or block number)
step_idx : Monotonic counter incremented each tick
metrics : Execution metrics (steps, bundles, gas rejections)
fees (optional): Gas fee configuration with price policies
Creating an Engine
Initialize an engine with a protocol registry and start timestamp:
use ank_engine :: Engine ;
use ank_protocol :: Protocol ;
use indexmap :: IndexMap ;
let mut protocols : IndexMap < String , Box < dyn Protocol >> = IndexMap :: new ();
protocols . insert ( "aave-v3" . into (), Box :: new ( aave_protocol ));
protocols . insert ( "lido" . into (), Box :: new ( lido_protocol ));
let start_ts = 1725000000 u64 ; // Unix timestamp
let mut engine = Engine :: new ( protocols , start_ts );
The engine starts with empty portfolios. You must credit users’ wallets before executing transactions.
Core Methods
tick()
Execute one planning pass with a single user:
pub fn tick < F >( & mut self , user : UserId , mut plan : F ) -> Result < Vec < Vec < ExecOutcome >>>
where
F : FnMut (
EngineCtx ,
& IndexMap < String , Box < dyn Protocol >>,
& IndexMap < UserId , Balances >,
) -> Vec < TxBundle >,
Lifecycle :
Call on_tick() on all protocols (interest accrual, rate updates)
Construct EngineCtx with current ts and step_idx
Call the planner function plan() to get Vec<TxBundle>
Execute each bundle for the user
Increment step_idx and ts
Return outcomes nested as Vec<Vec<ExecOutcome>> (one inner vec per bundle)
Example :
let user = UserId ( 1 );
let outcomes = engine . tick ( user , | ctx , protocols , portfolios | {
let aave = protocols . get ( "aave-v3" ) . unwrap ();
let user_view = aave . view_user ( user );
// Simple deposit strategy
if ctx . step_idx == 0 {
vec! [ TxBundle {
txs : vec! [ Tx {
to : "aave-v3" . into (),
action : Action :: Custom ( json! ({
"kind" : "deposit" ,
"token" : 3 ,
"amount" : "1000000000000000000000" // 1000e18
})),
gas_limit : None ,
}]
}]
} else {
vec! [] // No action
}
}) ? ;
tick_multi()
Execute bundles for multiple users in one global tick (shared timestamp):
pub fn tick_multi (
& mut self ,
plans : Vec <( UserId , Vec < TxBundle >)>,
) -> Result < Vec < Vec < ExecOutcome >>>
This is useful for simulating multi-user scenarios (e.g., liquidators competing with borrowers).
Example :
let plans = vec! [
( UserId ( 1 ), vec! [ deposit_bundle ]),
( UserId ( 2 ), vec! [ borrow_bundle ]),
];
engine . tick_multi ( plans ) ? ;
tick_with_bundles()
Convenience method to run pre-computed bundles:
pub fn tick_with_bundles (
& mut self ,
user : UserId ,
bundles : Vec < TxBundle >,
) -> Result < Vec < Vec < ExecOutcome >>>
Equivalent to tick() but takes bundles directly instead of a planner closure.
Execution Context
Every tick, the planner receives an EngineCtx:
pub struct EngineCtx {
pub ts : Timestamp , // Current simulation timestamp
pub step_idx : u64 , // Monotonic tick counter (0, 1, 2, ...)
}
Use this for:
Time-based logic : “If ts >= maturity, redeem PT”
Cooldowns : “Only rebalance every 10 ticks”
Debugging : Log step_idx with each action
Balance Management
The engine provides helpers to read and modify user balances:
balances_mut()
Get mutable access to a user’s balances (creates empty wallet if missing):
let user = UserId ( 1 );
let wallet = engine . balances_mut ( user );
wallet . set ( TokenId ( 1 ), 10_000_000_000_000_000_000_000 u128 ); // 10000 ETH
balances()
Get a cloned snapshot of balances:
let wallet = engine . balances ( UserId ( 1 ));
let eth_balance = wallet . get ( TokenId ( 1 ));
balances_ref()
Get an immutable reference (returns Option<&Balances>):
if let Some ( wallet ) = engine . balances_ref ( UserId ( 1 )) {
println! ( "ETH: {}" , wallet . get ( TokenId ( 1 )));
}
Gas Fee Modeling
When the fees feature is enabled, the engine can model gas costs and apply them to user balances.
FeesConfig
pub struct FeesConfig {
pub gas_token : TokenId , // Token used to pay gas (e.g., ETH)
pub price : GasPricePolicy , // How to compute gas price
pub on_shortfall : OnShortfall , // What to do if user can't pay
}
GasPricePolicy
pub enum GasPricePolicy {
FixedGwei ( u64 ), // Static price in gwei (1e9 wei)
Callback ( Arc < dyn Fn ( Timestamp ) -> u64 + Send + Sync >), // Dynamic price
}
Example: Fixed gas price :
use ank_engine :: { FeesConfig , GasPricePolicy , OnShortfall };
use ank_accounting :: TokenId ;
let fees = FeesConfig {
gas_token : TokenId ( 1 ), // ETH
price : GasPricePolicy :: FixedGwei ( 50 ), // 50 gwei
on_shortfall : OnShortfall :: RejectTx ,
};
engine . set_fees ( Some ( fees ));
Example: Dynamic gas price :
use std :: sync :: Arc ;
let fees = FeesConfig {
gas_token : TokenId ( 1 ),
price : GasPricePolicy :: Callback ( Arc :: new ( | ts | {
// Higher gas during "peak hours"
if ts % 86400 < 43200 { 100 } else { 30 }
})),
on_shortfall : OnShortfall :: AllowDebt ,
};
engine . set_fees ( Some ( fees ));
OnShortfall
pub enum OnShortfall {
RejectTx , // Fail tx if user can't pay precharge
AllowDebt , // Allow negative balance on gas token
}
With AllowDebt, users can go negative on the gas token. Use this for research scenarios where you want to isolate strategy logic from gas constraints.
Fee Calculation
The engine computes fees in two phases:
1. Precharge (PreTx)
If Tx.gas_limit is set:
let precharge_e18 = ( gas_limit as u128 ) * ( gas_price_gwei as u128 ) * 1_000_000_000 u128 ;
This amount is debited from the user’s balance before execution.
2. Refund/Post-charge (PostTx)
After execution:
let fee_e18 = ( outcome . gas_used as u128 ) * ( gas_price_gwei as u128 ) * 1_000_000_000 u128 ;
If precharge was applied:
Refund precharge_e18 - fee_e18 if gas_used < gas_limit
Charge extra fee_e18 - precharge_e18 if gas_used > gas_limit
If no precharge (gas_limit was None):
Post-charge exact fee_e18
Execution Metrics
The engine tracks high-level counters:
pub struct EngineMetrics {
pub steps : u64 , // Ticks processed
pub bundles_submitted : u64 , // Total bundles
pub txs_executed : u64 , // Successful txs
pub txs_rejected_gas : u64 , // Txs rejected due to gas shortfall
}
Access after simulation:
let metrics = & engine . metrics;
println! ( "Steps: {}, Txs: {}, Rejected: {}" ,
metrics . steps, metrics . txs_executed, metrics . txs_rejected_gas);
Best Practices
Initialization : Always credit user wallets with initial balances before calling tick().
Immutable reads : Use balances_ref() when you don’t need ownership, to avoid cloning.
Gas modeling : For production-like simulations, use dynamic GasPricePolicy::Callback based on historical data.
Tick order : on_tick() is called before the planner. This means interest/rates update before your strategy sees them.
Example: Full Simulation Loop
use ank_engine :: Engine ;
use ank_accounting :: { UserId , TokenId };
use ank_exec :: { Tx , TxBundle };
use ank_protocol :: Action ;
let mut engine = Engine :: new ( protocols , 1725000000 );
let user = UserId ( 1 );
// Credit initial balance
engine . balances_mut ( user ) . set ( TokenId ( 1 ), 10_000_000_000_000_000_000_000 u128 );
// Run 100 ticks
for _ in 0 .. 100 {
engine . tick ( user , | ctx , protocols , portfolios | {
// Your strategy logic
if ctx . step_idx % 10 == 0 {
vec! [ TxBundle { txs : vec! [ rebalance_tx ] }]
} else {
vec! []
}
}) ? ;
}
println! ( "Final metrics: {:?}" , engine . metrics);
Protocols Learn how protocols integrate with the engine
Strategies Build planners that emit TxBundles
Accounting Understand Balances and delta application