Skip to content

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():

rust
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:

FieldTypeDescription
initial_capitalf64Starting account equity
commission_typeCommissionTypePerContract, PerTrade, or PercentOfValue
commission_valuef64Commission amount per the commission type
margin_longf64Long position margin requirement (%)
margin_shortf64Short position margin requirement (%)
slippagef64Order slippage in ticks
pyramidingu32Max simultaneous entries in the same direction
risk_free_ratef64Annual risk-free rate for Sharpe/Sortino (%)
process_orders_on_closeboolFill orders on bar close instead of next bar open
calc_on_order_fillsboolRe-execute script on each order fill
close_entries_ruleCloseEntriesRuleFifo 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_percent
  • max_runup / max_runup_percent
  • buy_hold_return / buy_hold_return_percent
  • sharpe_ratio / sortino_ratio
  • num_even_trades

Profit & Loss

FieldDescription
net_profitNet profit in account currency
net_profit_percentNet profit as % of initial capital
gross_profitSum of profits from winning trades
gross_profit_percentGross profit %
gross_lossSum of losses from losing trades (positive number)
gross_loss_percentGross loss %
profit_factorGross profit ÷ gross loss (None if no losing trades)
buy_hold_returnBuy-and-hold return in account currency (performance_all only)
buy_hold_return_percentBuy-and-hold return % (performance_all only)

Drawdown & Runup

Only populated in performance_all. Always 0.0 in performance_long / performance_short.

FieldDescription
max_drawdownMaximum equity drawdown in account currency
max_drawdown_percentMax drawdown %
max_runupMaximum equity runup in account currency
max_runup_percentMax runup %

Risk-Adjusted Returns

Only populated in performance_all. Always None in performance_long / performance_short.

FieldDescription
sharpe_ratioAnnualized Sharpe ratio (None if insufficient data)
sortino_ratioAnnualized Sortino ratio (None if insufficient data)

Trade Statistics

FieldDescription
total_closed_tradesTotal number of completed trades
total_open_tradesUnrealized trades at end of backtest
num_winning_tradesTrades with profit > 0
num_losing_tradesTrades with profit < 0
num_even_tradesBreak-even trades (profit == 0) (performance_all only, always 0 otherwise)
percent_profitableWinning trades ÷ total × 100 (None if no closed trades)

Average Trade

FieldDescription
avg_tradeAverage P&L per trade
avg_trade_percentAverage P&L % per trade
avg_winning_tradeAverage profit of winning trades
avg_winning_trade_percentAverage winning trade profit %
avg_losing_tradeAverage loss of losing trades
avg_losing_trade_percentAverage losing trade loss %
ratio_avg_win_lossAvg winning trade ÷ avg losing trade (None if no losers)
largest_winning_tradeSingle largest profit
largest_winning_trade_percentSingle largest profit %
largest_losing_tradeSingle largest loss
largest_losing_trade_percentSingle largest loss %

Holding Period

FieldDescription
avg_bars_in_tradesAverage bars held across all trades
avg_bars_in_winning_tradesAverage bars held for winning trades
avg_bars_in_losing_tradesAverage bars held for losing trades

Other

FieldDescription
commission_paidTotal commissions paid
max_contracts_heldPeak simultaneous contracts/shares held
margin_callsNumber of margin calls triggered

Trade History

Closed Trades

Each entry in report.closed_trades is a ClosedTradeReport:

FieldTypeDescription
trade_numusizeTrade number (0-based)
entry_idStringEntry order ID (e.g. "Long Entry")
entry_commentStringEntry order comment
entry_sideDirectionLong or Short
entry_pricef64Entry fill price
entry_barusizeBar index of entry fill
entry_timei64Entry timestamp (Unix milliseconds)
exit_idStringExit order ID
exit_commentStringExit order comment
exit_pricef64Exit fill price
exit_barusizeBar index of exit fill
exit_timei64Exit timestamp (Unix milliseconds)
quantityf64Contracts/shares traded
profitf64Realized P&L (after commission)
profit_percentf64Realized P&L % relative to entry value
cumulative_profitf64Running total P&L including this trade
cumulative_profit_percentf64Running total P&L % vs. initial capital
max_runupf64Max favorable excursion in account currency
max_runup_percentf64Max favorable excursion %
max_drawdownf64Max adverse excursion in account currency
max_drawdown_percentf64Max adverse excursion %
commissionf64Total commission for this trade (entry + exit)

Open Trades

Each entry in report.open_trades is an OpenTradeReport for an unrealized position:

FieldTypeDescription
trade_numusizeTrade number (0-based)
entry_idStringEntry order ID
entry_commentStringEntry order comment
entry_sideDirectionLong or Short
entry_pricef64Entry fill price
entry_barusizeBar index of entry fill
entry_timei64Entry timestamp (Unix milliseconds)
quantityf64Contracts/shares held
profitf64Current unrealized P&L (after entry commission)
profit_percentf64Current unrealized P&L %
max_runupf64Peak unrealized profit so far
max_runup_percentf64Peak unrealized profit %
max_drawdownf64Peak unrealized loss so far
max_drawdown_percentf64Peak unrealized loss %
commissionf64Entry commission paid so far

Equity Curves

Three Vec<f64> slices are indexed by bar (index 0 = first bar):

FieldDescription
equity_curveAccount equity at the close of each bar
drawdown_curveDrawdown from peak equity (peak - equity, always ≥ 0)
buy_hold_curveHypothetical buy-and-hold equity at each bar
rust
// 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:

rust
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:

rust
pub struct TradingRangeReport {
    pub start_time: i64,  // Unix milliseconds
    pub end_time: i64,    // Unix milliseconds
}

Complete Example

rust
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

Released under the MIT License.