Skip to main content

engine/
portfolio.rs

1use crate::types::{FillEvent, PortfolioSnapshot, Side};
2
3#[derive(Debug, Clone)]
4pub struct Portfolio {
5    pub cash: f64,
6    pub position: f64,
7    pub transaction_cost_rate: f64,
8}
9
10impl Portfolio {
11    pub fn new(cash: f64, transaction_cost_rate: f64) -> Self {
12        Self {
13            cash,
14            position: 0.0,
15            transaction_cost_rate,
16        }
17    }
18
19    pub fn update(&mut self, fill: &FillEvent) {
20        let value = fill.price * fill.quantity;
21        let cost = value * self.transaction_cost_rate;
22
23        match fill.side {
24            Side::Buy => {
25                self.cash -= value + cost;
26                self.position += fill.quantity;
27            }
28            Side::Sell => {
29                self.cash += value - cost;
30                self.position -= fill.quantity;
31            }
32        }
33    }
34
35    pub fn snapshot(&self) -> PortfolioSnapshot {
36        PortfolioSnapshot {
37            cash: self.cash,
38            position: self.position,
39        }
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::types::Side;
47
48    #[test]
49    fn test_snapshot_matches_portfolio() {
50        let mut p = Portfolio::new(10000.0, 0.0);
51        p.position = 3.0;
52        let snap = p.snapshot();
53        assert_eq!(snap.cash, 10000.0);
54        assert_eq!(snap.position, 3.0);
55    }
56
57    #[test]
58    fn test_update_buy() {
59        let mut p = Portfolio::new(10000.0, 0.0);
60        let fill = FillEvent {
61            order_id: 1,
62            timestamp: 0.0,
63            side: Side::Buy,
64            price: 100.0,
65            quantity: 2.0,
66        };
67        p.update(&fill);
68        assert!((p.cash - 9800.0).abs() < 1e-10);
69        assert!((p.position - 2.0).abs() < 1e-10);
70    }
71
72    #[test]
73    fn test_update_sell_with_cost() {
74        let mut p = Portfolio::new(10000.0, 0.001);
75        p.position = 5.0;
76        let fill = FillEvent {
77            order_id: 1,
78            timestamp: 0.0,
79            side: Side::Sell,
80            price: 100.0,
81            quantity: 2.0,
82        };
83        p.update(&fill);
84        // value = 200, cost = 0.2, cash += 200 - 0.2 = 199.8
85        assert!((p.cash - 10199.8).abs() < 1e-10);
86        assert!((p.position - 3.0).abs() < 1e-10);
87    }
88}