Strategy Report
After running a strategy script, Navi generates a StrategyReport containing performance metrics, trade history, equity curves, and configuration details.
Report Structure
StrategyReport is the top-level type returned by instance.strategy_report():
pub struct StrategyReport {
/// Strategy configuration snapshot (initial capital, commission, etc.)
pub config: StrategyConfigReport,
/// Performance metrics computed across all trades
pub performance_all: PerformanceMetrics,
/// Performance metrics for long trades only
pub performance_long: PerformanceMetrics,
/// Performance metrics for short trades only
pub performance_short: PerformanceMetrics,
/// Completed trades, ordered by exit time
pub closed_trades: Vec<ClosedTradeReport>,
/// Currently open (unrealized) trades
pub open_trades: Vec<OpenTradeReport>,
/// Per-bar equity value (index 0 = first bar)
pub equity_curve: Vec<f64>,
/// Per-bar drawdown from peak equity (always ≥ 0)
pub drawdown_curve: Vec<f64>,
/// Per-bar buy-and-hold equity (initial_capital × close / first_close)
pub buy_hold_curve: Vec<f64>,
/// Daily returns for Sharpe/Sortino calculation
pub daily_returns: Vec<DailyReturnReport>,
/// Time range from first entry to last exit (None if no closed trades)
pub trading_range: Option<TradingRangeReport>,
}Strategy Configuration
StrategyConfigReport captures the strategy's declared settings:
| Field | Type | Description |
|---|---|---|
initial_capital | f64 | Starting account equity |
commission_type | CommissionType | PerContract, PerTrade, or PercentOfValue |
commission_value | f64 | Commission amount per the commission type |
margin_long | f64 | Long position margin requirement (%) |
margin_short | f64 | Short position margin requirement (%) |
slippage | f64 | Order slippage in ticks |
pyramiding | u32 | Max simultaneous entries in the same direction |
risk_free_rate | f64 | Annual risk-free rate for Sharpe/Sortino (%) |
process_orders_on_close | bool | Fill orders on bar close instead of next bar open |
calc_on_order_fills | bool | Re-execute script on each order fill |
close_entries_rule | CloseEntriesRule | Fifo or Any |
Performance Metrics
PerformanceMetrics is computed three ways: across all trades (performance_all), long-only (performance_long), and short-only (performance_short).
Some fields are only meaningful in performance_all — they reflect the combined equity curve and therefore cannot be split by direction. In performance_long and performance_short these fields are always 0.0 or None:
max_drawdown/max_drawdown_percentmax_runup/max_runup_percentbuy_hold_return/buy_hold_return_percentsharpe_ratio/sortino_rationum_even_trades
Profit & Loss
| Field | Description |
|---|---|
net_profit | Net profit in account currency |
net_profit_percent | Net profit as % of initial capital |
gross_profit | Sum of profits from winning trades |
gross_profit_percent | Gross profit % |
gross_loss | Sum of losses from losing trades (positive number) |
gross_loss_percent | Gross loss % |
profit_factor | Gross profit ÷ gross loss (None if no losing trades) |
buy_hold_return | Buy-and-hold return in account currency (performance_all only) |
buy_hold_return_percent | Buy-and-hold return % (performance_all only) |
Drawdown & Runup
Only populated in
performance_all. Always0.0inperformance_long/performance_short.
| Field | Description |
|---|---|
max_drawdown | Maximum equity drawdown in account currency |
max_drawdown_percent | Max drawdown % |
max_runup | Maximum equity runup in account currency |
max_runup_percent | Max runup % |
Risk-Adjusted Returns
Only populated in
performance_all. AlwaysNoneinperformance_long/performance_short.
| Field | Description |
|---|---|
sharpe_ratio | Annualized Sharpe ratio (None if insufficient data) |
sortino_ratio | Annualized Sortino ratio (None if insufficient data) |
Trade Statistics
| Field | Description |
|---|---|
total_closed_trades | Total number of completed trades |
total_open_trades | Unrealized trades at end of backtest |
num_winning_trades | Trades with profit > 0 |
num_losing_trades | Trades with profit < 0 |
num_even_trades | Break-even trades (profit == 0) (performance_all only, always 0 otherwise) |
percent_profitable | Winning trades ÷ total × 100 (None if no closed trades) |
Average Trade
| Field | Description |
|---|---|
avg_trade | Average P&L per trade |
avg_trade_percent | Average P&L % per trade |
avg_winning_trade | Average profit of winning trades |
avg_winning_trade_percent | Average winning trade profit % |
avg_losing_trade | Average loss of losing trades |
avg_losing_trade_percent | Average losing trade loss % |
ratio_avg_win_loss | Avg winning trade ÷ avg losing trade (None if no losers) |
largest_winning_trade | Single largest profit |
largest_winning_trade_percent | Single largest profit % |
largest_losing_trade | Single largest loss |
largest_losing_trade_percent | Single largest loss % |
Holding Period
| Field | Description |
|---|---|
avg_bars_in_trades | Average bars held across all trades |
avg_bars_in_winning_trades | Average bars held for winning trades |
avg_bars_in_losing_trades | Average bars held for losing trades |
Other
| Field | Description |
|---|---|
commission_paid | Total commissions paid |
max_contracts_held | Peak simultaneous contracts/shares held |
margin_calls | Number of margin calls triggered |
Trade History
Closed Trades
Each entry in report.closed_trades is a ClosedTradeReport:
| Field | Type | Description |
|---|---|---|
trade_num | usize | Trade number (0-based) |
entry_id | String | Entry order ID (e.g. "Long Entry") |
entry_comment | String | Entry order comment |
entry_side | Direction | Long or Short |
entry_price | f64 | Entry fill price |
entry_bar | usize | Bar index of entry fill |
entry_time | i64 | Entry timestamp (Unix milliseconds) |
exit_id | String | Exit order ID |
exit_comment | String | Exit order comment |
exit_price | f64 | Exit fill price |
exit_bar | usize | Bar index of exit fill |
exit_time | i64 | Exit timestamp (Unix milliseconds) |
quantity | f64 | Contracts/shares traded |
profit | f64 | Realized P&L (after commission) |
profit_percent | f64 | Realized P&L % relative to entry value |
cumulative_profit | f64 | Running total P&L including this trade |
cumulative_profit_percent | f64 | Running total P&L % vs. initial capital |
max_runup | f64 | Max favorable excursion in account currency |
max_runup_percent | f64 | Max favorable excursion % |
max_drawdown | f64 | Max adverse excursion in account currency |
max_drawdown_percent | f64 | Max adverse excursion % |
commission | f64 | Total commission for this trade (entry + exit) |
Open Trades
Each entry in report.open_trades is an OpenTradeReport for an unrealized position:
| Field | Type | Description |
|---|---|---|
trade_num | usize | Trade number (0-based) |
entry_id | String | Entry order ID |
entry_comment | String | Entry order comment |
entry_side | Direction | Long or Short |
entry_price | f64 | Entry fill price |
entry_bar | usize | Bar index of entry fill |
entry_time | i64 | Entry timestamp (Unix milliseconds) |
quantity | f64 | Contracts/shares held |
profit | f64 | Current unrealized P&L (after entry commission) |
profit_percent | f64 | Current unrealized P&L % |
max_runup | f64 | Peak unrealized profit so far |
max_runup_percent | f64 | Peak unrealized profit % |
max_drawdown | f64 | Peak unrealized loss so far |
max_drawdown_percent | f64 | Peak unrealized loss % |
commission | f64 | Entry commission paid so far |
Equity Curves
Three Vec<f64> slices are indexed by bar (index 0 = first bar):
| Field | Description |
|---|---|
equity_curve | Account equity at the close of each bar |
drawdown_curve | Drawdown from peak equity (peak - equity, always ≥ 0) |
buy_hold_curve | Hypothetical buy-and-hold equity at each bar |
// Find the worst drawdown bar
if let Some((bar, dd)) = report.drawdown_curve
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
{
println!("Max drawdown at bar {}: {:.2}", bar, dd);
}Daily Returns
report.daily_returns provides the per-day return series used for Sharpe/Sortino calculations:
pub struct DailyReturnReport {
pub date: String, // "YYYY-MM-DD"
pub return_percent: f64, // net P&L % for that calendar date
}Trading Range
report.trading_range is Some when at least one trade was fully closed. It spans from the first entry fill to the last exit fill:
pub struct TradingRangeReport {
pub start_time: i64, // Unix milliseconds
pub end_time: i64, // Unix milliseconds
}Complete Example
use navi_vm::{Candlestick, Instance, TimeFrame};
async fn backtest(candles: Vec<Candlestick>) -> Result<(), navi_vm::Error> {
let source = r#"
strategy("SMA Crossover",
initial_capital: 10000,
commission_type: CommissionType.Percent,
commission_value: 0.1);
let fast = ta.sma(close, 14);
let slow = ta.sma(close, 28);
if ta.crossover(fast, slow) {
strategy.entry("Long", Direction.Long);
}
if ta.crossunder(fast, slow) {
strategy.entry("Short", Direction.Short);
}
"#;
let tf = TimeFrame::days(1);
let symbol = "NASDAQ:AAPL";
let instance = Instance::builder(candles, source, tf, symbol)
.build().await?
.run_to_end(symbol, tf).await?;
let report = instance.strategy_report();
let p = &report.performance_all;
println!("=== Strategy Report ===");
println!("Net Profit: {:.2} ({:.2}%)", p.net_profit, p.net_profit_percent);
println!("Max Drawdown: {:.2} ({:.2}%)", p.max_drawdown, p.max_drawdown_percent);
println!("Profit Factor: {:.2}", p.profit_factor.unwrap_or(0.0));
println!("Sharpe Ratio: {:.2}", p.sharpe_ratio.unwrap_or(0.0));
println!("Total Trades: {}", p.total_closed_trades);
println!("Win Rate: {:.1}%", p.percent_profitable.unwrap_or(0.0));
println!("Commission: {:.2}", p.commission_paid);
println!("\n=== Trade List ===");
for trade in &report.closed_trades {
println!(
"#{:3} {:5} entry={:.2} exit={:.2} P&L={:.2} ({:.2}%)",
trade.trade_num,
if trade.entry_side == navi_vm::Direction::Long { "Long " } else { "Short" },
trade.entry_price,
trade.exit_price,
trade.profit,
trade.profit_percent,
);
}
Ok(())
}Next Steps
- Error Handling — handling compile and runtime errors
- Security & Limits — loop limits and untrusted script safety