1use nautilus_core::{UUID4, UnixNanos};
17use serde::{Deserialize, Serialize};
18
19use crate::{
20 enums::{OrderSide, PositionSide},
21 events::OrderFilled,
22 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
23 position::Position,
24 types::{Currency, Money, Price, Quantity},
25};
26
27#[repr(C)]
29#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
30#[cfg_attr(
31 feature = "python",
32 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
33)]
34#[cfg_attr(
35 feature = "python",
36 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
37)]
38pub struct PositionChanged {
39 pub trader_id: TraderId,
41 pub strategy_id: StrategyId,
43 pub instrument_id: InstrumentId,
45 pub position_id: PositionId,
47 pub account_id: AccountId,
49 pub opening_order_id: ClientOrderId,
51 pub entry: OrderSide,
53 pub side: PositionSide,
55 pub signed_qty: f64,
57 pub quantity: Quantity,
59 pub peak_quantity: Quantity,
61 pub last_qty: Quantity,
63 pub last_px: Price,
65 pub currency: Currency,
67 pub avg_px_open: f64,
69 pub avg_px_close: Option<f64>,
71 pub realized_return: f64,
73 pub realized_pnl: Option<Money>,
75 pub unrealized_pnl: Money,
77 pub event_id: UUID4,
79 pub ts_opened: UnixNanos,
81 pub ts_event: UnixNanos,
83 pub ts_init: UnixNanos,
85}
86
87impl PositionChanged {
88 #[must_use]
89 pub fn create(
90 position: &Position,
91 fill: &OrderFilled,
92 event_id: UUID4,
93 ts_init: UnixNanos,
94 ) -> Self {
95 Self {
96 trader_id: position.trader_id,
97 strategy_id: position.strategy_id,
98 instrument_id: position.instrument_id,
99 position_id: position.id,
100 account_id: position.account_id,
101 opening_order_id: position.opening_order_id,
102 entry: position.entry,
103 side: position.side,
104 signed_qty: position.signed_qty,
105 quantity: position.quantity,
106 peak_quantity: position.peak_qty,
107 last_qty: fill.last_qty,
108 last_px: fill.last_px,
109 currency: position.quote_currency,
110 avg_px_open: position.avg_px_open,
111 avg_px_close: position.avg_px_close,
112 realized_return: position.realized_return,
113 realized_pnl: position.realized_pnl,
114 unrealized_pnl: Money::new(0.0, position.quote_currency),
115 event_id,
116 ts_opened: position.ts_opened,
117 ts_event: fill.ts_event,
118 ts_init,
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use nautilus_core::UnixNanos;
126 use rstest::*;
127
128 use super::*;
129 use crate::{
130 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
131 events::OrderFilled,
132 identifiers::{
133 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
134 VenueOrderId,
135 },
136 instruments::{InstrumentAny, stubs::audusd_sim},
137 position::Position,
138 types::{Currency, Money, Price, Quantity},
139 };
140
141 fn create_test_position_changed() -> PositionChanged {
142 PositionChanged {
143 trader_id: TraderId::from("TRADER-001"),
144 strategy_id: StrategyId::from("EMA-CROSS"),
145 instrument_id: InstrumentId::from("EURUSD.SIM"),
146 position_id: PositionId::from("P-001"),
147 account_id: AccountId::from("SIM-001"),
148 opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
149 entry: OrderSide::Buy,
150 side: PositionSide::Long,
151 signed_qty: 150.0,
152 quantity: Quantity::from("150"),
153 peak_quantity: Quantity::from("150"),
154 last_qty: Quantity::from("50"),
155 last_px: Price::from("1.0550"),
156 currency: Currency::USD(),
157 avg_px_open: 1.0525,
158 avg_px_close: None,
159 realized_return: 0.0,
160 realized_pnl: None,
161 unrealized_pnl: Money::new(75.0, Currency::USD()),
162 event_id: UUID4::default(),
163 ts_opened: UnixNanos::from(1_000_000_000),
164 ts_event: UnixNanos::from(1_500_000_000),
165 ts_init: UnixNanos::from(2_500_000_000),
166 }
167 }
168
169 fn create_test_order_filled() -> OrderFilled {
170 OrderFilled::new(
171 TraderId::from("TRADER-001"),
172 StrategyId::from("EMA-CROSS"),
173 InstrumentId::from("AUD/USD.SIM"),
174 ClientOrderId::from("O-19700101-000000-001-001-2"),
175 VenueOrderId::from("2"),
176 AccountId::from("SIM-001"),
177 TradeId::from("T-002"),
178 OrderSide::Buy,
179 OrderType::Market,
180 Quantity::from("50"),
181 Price::from("0.8050"),
182 Currency::USD(),
183 LiquiditySide::Taker,
184 UUID4::default(),
185 UnixNanos::from(1_500_000_000),
186 UnixNanos::from(2_500_000_000),
187 false,
188 Some(PositionId::from("P-001")),
189 Some(Money::new(1.0, Currency::USD())),
190 )
191 }
192
193 #[rstest]
194 fn test_position_changed_create() {
195 let instrument = audusd_sim();
196 let initial_fill = OrderFilled::new(
197 TraderId::from("TRADER-001"),
198 StrategyId::from("EMA-CROSS"),
199 InstrumentId::from("AUD/USD.SIM"),
200 ClientOrderId::from("O-19700101-000000-001-001-1"),
201 VenueOrderId::from("1"),
202 AccountId::from("SIM-001"),
203 TradeId::from("T-001"),
204 OrderSide::Buy,
205 OrderType::Market,
206 Quantity::from("100"),
207 Price::from("0.8000"),
208 Currency::USD(),
209 LiquiditySide::Taker,
210 UUID4::default(),
211 UnixNanos::from(1_000_000_000),
212 UnixNanos::from(2_000_000_000),
213 false,
214 Some(PositionId::from("P-001")),
215 Some(Money::new(2.0, Currency::USD())),
216 );
217
218 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
219 let change_fill = create_test_order_filled();
220 let event_id = UUID4::default();
221 let ts_init = UnixNanos::from(3_000_000_000);
222
223 let position_changed = PositionChanged::create(&position, &change_fill, event_id, ts_init);
224
225 assert_eq!(position_changed.trader_id, position.trader_id);
226 assert_eq!(position_changed.strategy_id, position.strategy_id);
227 assert_eq!(position_changed.instrument_id, position.instrument_id);
228 assert_eq!(position_changed.position_id, position.id);
229 assert_eq!(position_changed.account_id, position.account_id);
230 assert_eq!(position_changed.opening_order_id, position.opening_order_id);
231 assert_eq!(position_changed.entry, position.entry);
232 assert_eq!(position_changed.side, position.side);
233 assert_eq!(position_changed.signed_qty, position.signed_qty);
234 assert_eq!(position_changed.quantity, position.quantity);
235 assert_eq!(position_changed.peak_quantity, position.peak_qty);
236 assert_eq!(position_changed.last_qty, change_fill.last_qty);
237 assert_eq!(position_changed.last_px, change_fill.last_px);
238 assert_eq!(position_changed.currency, position.quote_currency);
239 assert_eq!(position_changed.avg_px_open, position.avg_px_open);
240 assert_eq!(position_changed.avg_px_close, position.avg_px_close);
241 assert_eq!(position_changed.realized_return, position.realized_return);
242 assert_eq!(position_changed.realized_pnl, position.realized_pnl);
243 assert_eq!(
244 position_changed.unrealized_pnl,
245 Money::new(0.0, position.quote_currency)
246 );
247 assert_eq!(position_changed.event_id, event_id);
248 assert_eq!(position_changed.ts_opened, position.ts_opened);
249 assert_eq!(position_changed.ts_event, change_fill.ts_event);
250 assert_eq!(position_changed.ts_init, ts_init);
251 }
252
253 #[rstest]
254 fn test_position_changed_different_sides() {
255 let mut long_position = create_test_position_changed();
256 long_position.side = PositionSide::Long;
257 long_position.signed_qty = 150.0;
258
259 let mut short_position = create_test_position_changed();
260 short_position.side = PositionSide::Short;
261 short_position.signed_qty = -150.0;
262
263 assert_eq!(long_position.side, PositionSide::Long);
264 assert_eq!(long_position.signed_qty, 150.0);
265
266 assert_eq!(short_position.side, PositionSide::Short);
267 assert_eq!(short_position.signed_qty, -150.0);
268 }
269}