nautilus_model/events/position/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use crate::{
17    events::{PositionChanged, PositionClosed, PositionOpened},
18    identifiers::{AccountId, InstrumentId},
19};
20pub mod changed;
21pub mod closed;
22pub mod opened;
23pub mod snapshot;
24
25#[derive(Debug)]
26pub enum PositionEvent {
27    PositionOpened(PositionOpened),
28    PositionChanged(PositionChanged),
29    PositionClosed(PositionClosed),
30}
31
32impl PositionEvent {
33    pub fn instrument_id(&self) -> InstrumentId {
34        match self {
35            PositionEvent::PositionOpened(position) => position.instrument_id,
36            PositionEvent::PositionChanged(position) => position.instrument_id,
37            PositionEvent::PositionClosed(position) => position.instrument_id,
38        }
39    }
40
41    pub fn account_id(&self) -> AccountId {
42        match self {
43            PositionEvent::PositionOpened(position) => position.account_id,
44            PositionEvent::PositionChanged(position) => position.account_id,
45            PositionEvent::PositionClosed(position) => position.account_id,
46        }
47    }
48}
49
50////////////////////////////////////////////////////////////////////////////////
51// Tests
52////////////////////////////////////////////////////////////////////////////////
53#[cfg(test)]
54mod tests {
55    use nautilus_core::UnixNanos;
56    use rstest::*;
57
58    use super::*;
59    use crate::{
60        enums::{OrderSide, PositionSide},
61        events::{PositionChanged, PositionClosed, PositionOpened},
62        identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
63        types::{Currency, Money, Price, Quantity},
64    };
65
66    fn create_test_position_opened() -> PositionOpened {
67        PositionOpened {
68            trader_id: TraderId::from("TRADER-001"),
69            strategy_id: StrategyId::from("EMA-CROSS"),
70            instrument_id: InstrumentId::from("EURUSD.SIM"),
71            position_id: PositionId::from("P-001"),
72            account_id: AccountId::from("SIM-001"),
73            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
74            entry: OrderSide::Buy,
75            side: PositionSide::Long,
76            signed_qty: 100.0,
77            quantity: Quantity::from("100"),
78            last_qty: Quantity::from("100"),
79            last_px: Price::from("1.0500"),
80            currency: Currency::USD(),
81            avg_px_open: 1.0500,
82            event_id: Default::default(),
83            ts_event: UnixNanos::from(1_000_000_000),
84            ts_init: UnixNanos::from(2_000_000_000),
85        }
86    }
87
88    fn create_test_position_changed() -> PositionChanged {
89        PositionChanged {
90            trader_id: TraderId::from("TRADER-001"),
91            strategy_id: StrategyId::from("EMA-CROSS"),
92            instrument_id: InstrumentId::from("EURUSD.SIM"),
93            position_id: PositionId::from("P-001"),
94            account_id: AccountId::from("SIM-001"),
95            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
96            entry: OrderSide::Buy,
97            side: PositionSide::Long,
98            signed_qty: 150.0,
99            quantity: Quantity::from("150"),
100            peak_quantity: Quantity::from("150"),
101            last_qty: Quantity::from("50"),
102            last_px: Price::from("1.0550"),
103            currency: Currency::USD(),
104            avg_px_open: 1.0525,
105            avg_px_close: None,
106            realized_return: 0.0,
107            realized_pnl: None,
108            unrealized_pnl: Money::new(75.0, Currency::USD()),
109            event_id: Default::default(),
110            ts_opened: UnixNanos::from(1_000_000_000),
111            ts_event: UnixNanos::from(1_500_000_000),
112            ts_init: UnixNanos::from(2_500_000_000),
113        }
114    }
115
116    fn create_test_position_closed() -> PositionClosed {
117        PositionClosed {
118            trader_id: TraderId::from("TRADER-001"),
119            strategy_id: StrategyId::from("EMA-CROSS"),
120            instrument_id: InstrumentId::from("EURUSD.SIM"),
121            position_id: PositionId::from("P-001"),
122            account_id: AccountId::from("SIM-001"),
123            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
124            closing_order_id: Some(ClientOrderId::from("O-19700101-000000-001-001-2")),
125            entry: OrderSide::Buy,
126            side: PositionSide::Flat,
127            signed_qty: 0.0,
128            quantity: Quantity::from("0"),
129            peak_quantity: Quantity::from("150"),
130            last_qty: Quantity::from("150"),
131            last_px: Price::from("1.0600"),
132            currency: Currency::USD(),
133            avg_px_open: 1.0525,
134            avg_px_close: Some(1.0600),
135            realized_return: 0.0071,
136            realized_pnl: Some(Money::new(112.50, Currency::USD())),
137            unrealized_pnl: Money::new(0.0, Currency::USD()),
138            duration: 3_600_000_000_000, // 1 hour in nanoseconds
139            event_id: Default::default(),
140            ts_opened: UnixNanos::from(1_000_000_000),
141            ts_closed: Some(UnixNanos::from(4_600_000_000)),
142            ts_event: UnixNanos::from(4_600_000_000),
143            ts_init: UnixNanos::from(5_000_000_000),
144        }
145    }
146
147    #[rstest]
148    fn test_position_event_opened_instrument_id() {
149        let opened = create_test_position_opened();
150        let event = PositionEvent::PositionOpened(opened);
151
152        assert_eq!(event.instrument_id(), InstrumentId::from("EURUSD.SIM"));
153    }
154
155    #[rstest]
156    fn test_position_event_changed_instrument_id() {
157        let changed = create_test_position_changed();
158        let event = PositionEvent::PositionChanged(changed);
159
160        assert_eq!(event.instrument_id(), InstrumentId::from("EURUSD.SIM"));
161    }
162
163    #[rstest]
164    fn test_position_event_closed_instrument_id() {
165        let closed = create_test_position_closed();
166        let event = PositionEvent::PositionClosed(closed);
167
168        assert_eq!(event.instrument_id(), InstrumentId::from("EURUSD.SIM"));
169    }
170
171    #[rstest]
172    fn test_position_event_opened_account_id() {
173        let opened = create_test_position_opened();
174        let event = PositionEvent::PositionOpened(opened);
175
176        assert_eq!(event.account_id(), AccountId::from("SIM-001"));
177    }
178
179    #[rstest]
180    fn test_position_event_changed_account_id() {
181        let changed = create_test_position_changed();
182        let event = PositionEvent::PositionChanged(changed);
183
184        assert_eq!(event.account_id(), AccountId::from("SIM-001"));
185    }
186
187    #[rstest]
188    fn test_position_event_closed_account_id() {
189        let closed = create_test_position_closed();
190        let event = PositionEvent::PositionClosed(closed);
191
192        assert_eq!(event.account_id(), AccountId::from("SIM-001"));
193    }
194
195    #[rstest]
196    fn test_position_event_debug_formatting() {
197        let opened = create_test_position_opened();
198        let event = PositionEvent::PositionOpened(opened);
199
200        let debug_str = format!("{event:?}");
201        assert!(debug_str.contains("PositionOpened"));
202        assert!(debug_str.contains("EURUSD.SIM"));
203        assert!(debug_str.contains("SIM-001"));
204    }
205
206    #[rstest]
207    fn test_position_event_enum_variants() {
208        let opened = create_test_position_opened();
209        let changed = create_test_position_changed();
210        let closed = create_test_position_closed();
211
212        let event_opened = PositionEvent::PositionOpened(opened);
213        let event_changed = PositionEvent::PositionChanged(changed);
214        let event_closed = PositionEvent::PositionClosed(closed);
215
216        match event_opened {
217            PositionEvent::PositionOpened(_) => {}
218            _ => panic!("Expected PositionOpened variant"),
219        }
220
221        match event_changed {
222            PositionEvent::PositionChanged(_) => {}
223            _ => panic!("Expected PositionChanged variant"),
224        }
225
226        match event_closed {
227            PositionEvent::PositionClosed(_) => {}
228            _ => panic!("Expected PositionClosed variant"),
229        }
230    }
231}