rustrade_backtest/metrics.rs
1//! Trade-level outcome captured during the replay loop.
2//!
3//! [`BacktestResult`](crate::BacktestResult) aggregates these into
4//! summary statistics. Closing more than one position per trade (e.g.
5//! flipping from long to short) emits multiple [`TradeOutcome`]s — one
6//! per closed quantity.
7
8use chrono::{DateTime, Utc};
9use rustrade_core::Side;
10use serde::{Deserialize, Serialize};
11
12/// A single realised trade — open → close — recorded by the engine.
13///
14/// `gross_pnl` is in quote currency, before `fee`. `net_pnl = gross_pnl
15/// - fee`. The "side" is the side of the *closing* fill (so a long
16/// position is closed with `Side::Sell`).
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct TradeOutcome {
19 /// Symbol the trade was on.
20 pub symbol: String,
21 /// Side of the *closing* fill (`Sell` to close a long, etc.).
22 pub close_side: Side,
23 /// Closed quantity.
24 pub qty: f64,
25 /// Average entry price of the closed quantity.
26 pub entry_price: f64,
27 /// Fill price of the close.
28 pub exit_price: f64,
29 /// Gross PnL on the closed quantity, in quote currency, before fees.
30 pub gross_pnl: f64,
31 /// Fee charged to this close, in quote currency.
32 pub fee: f64,
33 /// When the close fill occurred.
34 pub closed_at: DateTime<Utc>,
35}
36
37impl TradeOutcome {
38 /// Net of fees.
39 pub fn net_pnl(&self) -> f64 {
40 self.gross_pnl - self.fee
41 }
42
43 /// Win (`true`) / loss / breakeven on net PnL.
44 pub fn outcome(&self) -> Outcome {
45 let n = self.net_pnl();
46 if n > 0.0 {
47 Outcome::Win
48 } else if n < 0.0 {
49 Outcome::Loss
50 } else {
51 Outcome::Breakeven
52 }
53 }
54}
55
56/// Trade classification on net PnL — used by metric aggregation.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Outcome {
59 /// Net PnL > 0.
60 Win,
61 /// Net PnL < 0.
62 Loss,
63 /// Net PnL == 0.
64 Breakeven,
65}