策略報告
執行策略腳本後,Navi 會產生一份 StrategyReport,包含績效指標、交易歷史、權益曲線和設定詳情。
報告結構
StrategyReport 是 instance.strategy_report() 回傳的頂層型別:
rust
pub struct StrategyReport {
/// 策略設定快照(初始資金、手續費等)
pub config: StrategyConfigReport,
/// 所有交易的綜合績效指標
pub performance_all: PerformanceMetrics,
/// 僅多頭交易的績效指標
pub performance_long: PerformanceMetrics,
/// 僅空頭交易的績效指標
pub performance_short: PerformanceMetrics,
/// 已平倉交易清單(依平倉時間排序)
pub closed_trades: Vec<ClosedTradeReport>,
/// 目前未平倉(浮動損益)的交易
pub open_trades: Vec<OpenTradeReport>,
/// 每根 K 線的權益值(索引 0 = 第一根 K 線)
pub equity_curve: Vec<f64>,
/// 每根 K 線相對峰值權益的回撤(始終 ≥ 0)
pub drawdown_curve: Vec<f64>,
/// 每根 K 線的買入持有權益(initial_capital × close / first_close)
pub buy_hold_curve: Vec<f64>,
/// 用於計算 Sharpe/Sortino 的日報酬率序列
pub daily_returns: Vec<DailyReturnReport>,
/// 從首次入場到最後一筆平倉的時間範圍(無已平倉交易時為 None)
pub trading_range: Option<TradingRangeReport>,
}策略設定
StrategyConfigReport 記錄了策略在 strategy() 中宣告的參數:
| 欄位 | 型別 | 說明 |
|---|---|---|
initial_capital | f64 | 初始帳戶資金 |
commission_type | CommissionType | PerContract、PerTrade 或 PercentOfValue |
commission_value | f64 | 對應手續費類型的手續費金額 |
margin_long | f64 | 多頭保證金要求(%) |
margin_short | f64 | 空頭保證金要求(%) |
slippage | f64 | 訂單滑價(以 tick 為單位) |
pyramiding | u32 | 同方向最大同時入場次數 |
risk_free_rate | f64 | 用於 Sharpe/Sortino 的年化無風險利率(%) |
process_orders_on_close | bool | 在當根 K 線收盤而非次根開盤時成交訂單 |
calc_on_order_fills | bool | 每筆訂單成交後重新執行腳本 |
close_entries_rule | CloseEntriesRule | Fifo 或 Any |
績效指標
PerformanceMetrics 分三種維度計算:所有交易(performance_all)、僅多頭(performance_long)、僅空頭(performance_short)。
以下欄位僅在 performance_all 中有意義 — 它們基於整體權益曲線,無法按方向拆分。在 performance_long 和 performance_short 中這些欄位固定為 0.0 或 None:
max_drawdown/max_drawdown_percentmax_runup/max_runup_percentbuy_hold_return/buy_hold_return_percentsharpe_ratio/sortino_rationum_even_trades
損益
| 欄位 | 說明 |
|---|---|
net_profit | 以帳戶貨幣計的淨利潤 |
net_profit_percent | 淨利潤佔初始資金的百分比 |
gross_profit | 獲利交易的利潤總和 |
gross_profit_percent | 總利潤百分比 |
gross_loss | 虧損交易的虧損總和(正數) |
gross_loss_percent | 總虧損百分比 |
profit_factor | 總利潤 ÷ 總虧損(無虧損交易時為 None) |
buy_hold_return | 買入持有的收益(帳戶貨幣,僅 performance_all) |
buy_hold_return_percent | 買入持有收益百分比(僅 performance_all) |
回撤與上升
僅在
performance_all中有值,在performance_long/performance_short中始終為0.0。
| 欄位 | 說明 |
|---|---|
max_drawdown | 最大權益回撤(帳戶貨幣) |
max_drawdown_percent | 最大回撤百分比 |
max_runup | 最大權益上升(帳戶貨幣) |
max_runup_percent | 最大上升百分比 |
風險調整收益
僅在
performance_all中有值,在performance_long/performance_short中始終為None。
| 欄位 | 說明 |
|---|---|
sharpe_ratio | 年化 Sharpe 比率(資料不足時為 None) |
sortino_ratio | 年化 Sortino 比率(資料不足時為 None) |
交易統計
| 欄位 | 說明 |
|---|---|
total_closed_trades | 已完成的交易總數 |
total_open_trades | 回測結束時未平倉的交易數 |
num_winning_trades | 獲利(profit > 0)的交易數 |
num_losing_trades | 虧損(profit < 0)的交易數 |
num_even_trades | 平本(profit == 0)的交易數(僅 performance_all,其餘始終為 0) |
percent_profitable | 獲利交易數 ÷ 總交易數 × 100(無已平倉交易時為 None) |
平均交易
| 欄位 | 說明 |
|---|---|
avg_trade | 每筆交易的平均損益 |
avg_trade_percent | 每筆交易的平均損益百分比 |
avg_winning_trade | 獲利交易的平均利潤 |
avg_winning_trade_percent | 獲利交易平均利潤百分比 |
avg_losing_trade | 虧損交易的平均虧損 |
avg_losing_trade_percent | 虧損交易平均虧損百分比 |
ratio_avg_win_loss | 平均獲利 ÷ 平均虧損(無虧損交易時為 None) |
largest_winning_trade | 單筆最大利潤 |
largest_winning_trade_percent | 單筆最大利潤百分比 |
largest_losing_trade | 單筆最大虧損 |
largest_losing_trade_percent | 單筆最大虧損百分比 |
持倉時間
| 欄位 | 說明 |
|---|---|
avg_bars_in_trades | 所有交易的平均持倉 K 線數 |
avg_bars_in_winning_trades | 獲利交易的平均持倉 K 線數 |
avg_bars_in_losing_trades | 虧損交易的平均持倉 K 線數 |
其他
| 欄位 | 說明 |
|---|---|
commission_paid | 已支付的手續費總額 |
max_contracts_held | 同時持倉的最大合約/股數 |
margin_calls | 觸發的追繳保證金次數 |
交易歷史
已平倉交易
report.closed_trades 中每條 ClosedTradeReport 對應一筆已完成的交易:
| 欄位 | 型別 | 說明 |
|---|---|---|
trade_num | usize | 交易編號(從 0 開始) |
entry_id | String | 入場訂單 ID(如 "Long Entry") |
entry_comment | String | 入場訂單備註 |
entry_side | Direction | Long 或 Short |
entry_price | f64 | 入場成交價 |
entry_bar | usize | 入場成交的 K 線索引 |
entry_time | i64 | 入場時間戳(Unix 毫秒) |
exit_id | String | 出場訂單 ID |
exit_comment | String | 出場訂單備註 |
exit_price | f64 | 出場成交價 |
exit_bar | usize | 出場成交的 K 線索引 |
exit_time | i64 | 出場時間戳(Unix 毫秒) |
quantity | f64 | 交易數量(合約/股數) |
profit | f64 | 已實現損益(扣除手續費) |
profit_percent | f64 | 已實現損益百分比(相對入場價值) |
cumulative_profit | f64 | 累計損益(含本筆交易) |
cumulative_profit_percent | f64 | 累計損益百分比(相對初始資金) |
max_runup | f64 | 最大順向波動(帳戶貨幣) |
max_runup_percent | f64 | 最大順向波動百分比 |
max_drawdown | f64 | 最大逆向波動(帳戶貨幣) |
max_drawdown_percent | f64 | 最大逆向波動百分比 |
commission | f64 | 本筆交易的手續費總額(入場 + 出場) |
未平倉交易
report.open_trades 中每條 OpenTradeReport 對應一個未結倉位:
| 欄位 | 型別 | 說明 |
|---|---|---|
trade_num | usize | 交易編號(從 0 開始) |
entry_id | String | 入場訂單 ID |
entry_comment | String | 入場訂單備註 |
entry_side | Direction | Long 或 Short |
entry_price | f64 | 入場成交價 |
entry_bar | usize | 入場成交的 K 線索引 |
entry_time | i64 | 入場時間戳(Unix 毫秒) |
quantity | f64 | 持倉數量(合約/股數) |
profit | f64 | 目前浮動損益(扣除入場手續費) |
profit_percent | f64 | 目前浮動損益百分比 |
max_runup | f64 | 迄今最大浮動獲利 |
max_runup_percent | f64 | 迄今最大浮動獲利百分比 |
max_drawdown | f64 | 迄今最大浮動虧損 |
max_drawdown_percent | f64 | 迄今最大浮動虧損百分比 |
commission | f64 | 已支付的入場手續費 |
權益曲線
三組 Vec<f64> 均以 K 線為索引(索引 0 = 第一根 K 線):
| 欄位 | 說明 |
|---|---|
equity_curve | 每根 K 線收盤時的帳戶權益 |
drawdown_curve | 每根 K 線相對峰值權益的回撤(peak - equity,始終 ≥ 0) |
buy_hold_curve | 每根 K 線的買入持有假設權益 |
rust
// 找到最大回撤發生的 K 線
if let Some((bar, dd)) = report.drawdown_curve
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
{
println!("最大回撤發生在第 {} 根 K 線:{:.2}", bar, dd);
}日報酬率
report.daily_returns 提供用於 Sharpe/Sortino 計算的逐日報酬序列:
rust
pub struct DailyReturnReport {
pub date: String, // "YYYY-MM-DD"
pub return_percent: f64, // 當日淨損益百分比
}交易時間範圍
report.trading_range 在至少有一筆已平倉交易時為 Some,涵蓋從首次入場到最後一筆平倉的時間段:
rust
pub struct TradingRangeReport {
pub start_time: i64, // Unix 毫秒
pub end_time: i64, // Unix 毫秒
}完整範例
rust
use navi_vm::{Candlestick, Instance, TimeFrame};
async fn backtest(candles: Vec<Candlestick>) -> Result<(), navi_vm::Error> {
let source = r#"
strategy("均線交叉策略",
initial_capital = 10000,
commission_type = strategy.commission.percent,
commission_value = 0.1)
fast = ta.sma(close, 14)
slow = ta.sma(close, 28)
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
strategy.entry("Short", strategy.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!("=== 策略報告 ===");
println!("淨利潤: {:.2}({:.2}%)", p.net_profit, p.net_profit_percent);
println!("最大回撤: {:.2}({:.2}%)", p.max_drawdown, p.max_drawdown_percent);
println!("盈虧比: {:.2}", p.profit_factor.unwrap_or(0.0));
println!("Sharpe 比率: {:.2}", p.sharpe_ratio.unwrap_or(0.0));
println!("總交易次數: {}", p.total_closed_trades);
println!("勝率: {:.1}%", p.percent_profitable.unwrap_or(0.0));
println!("總手續費: {:.2}", p.commission_paid);
println!("\n=== 交易明細 ===");
for trade in &report.closed_trades {
println!(
"#{:3} {:5} 入場={:.2} 出場={:.2} 損益={:.2}({:.2}%)",
trade.trade_num,
if trade.entry_side == navi_vm::Direction::Long { "多頭" } else { "空頭" },
trade.entry_price,
trade.exit_price,
trade.profit,
trade.profit_percent,
);
}
Ok(())
}