Skip to main content

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 + funding`. The "side" is the side of the *closing* fill (so a
16/// long 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    /// Net perp funding cashflow attributed to the closed quantity over
34    /// its hold, in quote currency. Positive = the position collected
35    /// funding, negative = it paid. Always `0.0` when
36    /// [`FundingModel::None`](crate::FundingModel::None) (the default)
37    /// is configured. `#[serde(default)]` so results serialized before
38    /// funding modelling shipped still deserialize.
39    #[serde(default)]
40    pub funding: f64,
41    /// When the close fill occurred.
42    pub closed_at: DateTime<Utc>,
43}
44
45impl TradeOutcome {
46    /// Net of fees and funding: `gross_pnl - fee + funding`. With
47    /// funding modelling off this is `gross_pnl - fee` exactly, as
48    /// before.
49    pub fn net_pnl(&self) -> f64 {
50        self.gross_pnl - self.fee + self.funding
51    }
52
53    /// Win (`true`) / loss / breakeven on net PnL.
54    pub fn outcome(&self) -> Outcome {
55        let n = self.net_pnl();
56        if n > 0.0 {
57            Outcome::Win
58        } else if n < 0.0 {
59            Outcome::Loss
60        } else {
61            Outcome::Breakeven
62        }
63    }
64}
65
66/// Trade classification on net PnL — used by metric aggregation.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Outcome {
69    /// Net PnL > 0.
70    Win,
71    /// Net PnL < 0.
72    Loss,
73    /// Net PnL == 0.
74    Breakeven,
75}