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