nautilus_model/events/position/
opened.rs1use 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.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 PositionOpened {
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 last_qty: Quantity,
61 pub last_px: Price,
63 pub currency: Currency,
65 pub avg_px_open: f64,
67 pub realized_pnl: Option<Money>,
69 pub event_id: UUID4,
71 pub ts_event: UnixNanos,
73 pub ts_init: UnixNanos,
75}
76
77impl PositionOpened {
78 #[must_use]
79 pub fn create(
80 position: &Position,
81 fill: &OrderFilled,
82 event_id: UUID4,
83 ts_init: UnixNanos,
84 ) -> Self {
85 Self {
86 trader_id: position.trader_id,
87 strategy_id: position.strategy_id,
88 instrument_id: position.instrument_id,
89 position_id: position.id,
90 account_id: position.account_id,
91 opening_order_id: position.opening_order_id,
92 entry: position.entry,
93 side: position.side,
94 signed_qty: position.signed_qty,
95 quantity: position.quantity,
96 last_qty: fill.last_qty,
97 last_px: fill.last_px,
98 currency: position.quote_currency,
99 avg_px_open: position.avg_px_open,
100 realized_pnl: position.realized_pnl,
101 event_id,
102 ts_event: fill.ts_event,
103 ts_init,
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use nautilus_core::UnixNanos;
111 use rstest::*;
112
113 use super::*;
114 use crate::{
115 enums::{OrderSide, PositionSide},
116 events::{OrderFilled, order::spec::OrderFilledSpec},
117 identifiers::{
118 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
119 VenueOrderId,
120 },
121 instruments::{
122 Instrument, InstrumentAny,
123 stubs::{audusd_sim, xbtusd_bitmex},
124 },
125 position::Position,
126 types::{Currency, Money, Price, Quantity},
127 };
128
129 fn create_test_position_opened() -> PositionOpened {
130 PositionOpened {
131 trader_id: TraderId::from("TRADER-001"),
132 strategy_id: StrategyId::from("EMA-CROSS"),
133 instrument_id: InstrumentId::from("EURUSD.SIM"),
134 position_id: PositionId::from("P-001"),
135 account_id: AccountId::from("SIM-001"),
136 opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
137 entry: OrderSide::Buy,
138 side: PositionSide::Long,
139 signed_qty: 100.0,
140 quantity: Quantity::from("100"),
141 last_qty: Quantity::from("100"),
142 last_px: Price::from("1.0500"),
143 currency: Currency::USD(),
144 avg_px_open: 1.0500,
145 realized_pnl: Some(Money::new(-2.0, Currency::USD())),
146 event_id: UUID4::default(),
147 ts_event: UnixNanos::from(1_000_000_000),
148 ts_init: UnixNanos::from(2_000_000_000),
149 }
150 }
151
152 fn create_test_order_filled() -> OrderFilled {
153 OrderFilledSpec::builder()
154 .strategy_id(StrategyId::from("EMA-CROSS"))
155 .instrument_id(InstrumentId::from("AUD/USD.SIM"))
156 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
157 .venue_order_id(VenueOrderId::from("1"))
158 .trade_id(TradeId::from("T-001"))
159 .last_qty(Quantity::from("100"))
160 .last_px(Price::from("0.8000"))
161 .ts_event(UnixNanos::from(1_000_000_000))
162 .ts_init(UnixNanos::from(2_000_000_000))
163 .position_id(PositionId::from("P-001"))
164 .commission(Money::new(2.0, Currency::USD()))
165 .build()
166 }
167
168 #[rstest]
169 fn test_position_opened_create() {
170 let instrument = audusd_sim();
171 let fill = create_test_order_filled();
172 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), fill.clone());
173 let event_id = UUID4::default();
174 let ts_init = UnixNanos::from(3_000_000_000);
175
176 let position_opened = PositionOpened::create(&position, &fill, event_id, ts_init);
177
178 assert_eq!(position_opened.trader_id, position.trader_id);
179 assert_eq!(position_opened.strategy_id, position.strategy_id);
180 assert_eq!(position_opened.instrument_id, position.instrument_id);
181 assert_eq!(position_opened.position_id, position.id);
182 assert_eq!(position_opened.account_id, position.account_id);
183 assert_eq!(position_opened.opening_order_id, position.opening_order_id);
184 assert_eq!(position_opened.entry, position.entry);
185 assert_eq!(position_opened.side, position.side);
186 assert_eq!(position_opened.signed_qty, position.signed_qty);
187 assert_eq!(position_opened.quantity, position.quantity);
188 assert_eq!(position_opened.last_qty, fill.last_qty);
189 assert_eq!(position_opened.last_px, fill.last_px);
190 assert_eq!(position_opened.currency, position.quote_currency);
191 assert_eq!(position_opened.avg_px_open, position.avg_px_open);
192 assert_eq!(position_opened.realized_pnl, position.realized_pnl);
193 assert_eq!(position_opened.event_id, event_id);
194 assert_eq!(position_opened.ts_event, fill.ts_event);
195 assert_eq!(position_opened.ts_init, ts_init);
196 }
197
198 #[rstest]
199 fn test_position_opened_with_different_sides() {
200 let mut long_position = create_test_position_opened();
201 long_position.side = PositionSide::Long;
202 long_position.entry = OrderSide::Buy;
203 long_position.signed_qty = 100.0;
204
205 let mut short_position = create_test_position_opened();
206 short_position.side = PositionSide::Short;
207 short_position.entry = OrderSide::Sell;
208 short_position.signed_qty = -100.0;
209
210 assert_eq!(long_position.side, PositionSide::Long);
211 assert_eq!(long_position.entry, OrderSide::Buy);
212 assert_eq!(long_position.signed_qty, 100.0);
213
214 assert_eq!(short_position.side, PositionSide::Short);
215 assert_eq!(short_position.entry, OrderSide::Sell);
216 assert_eq!(short_position.signed_qty, -100.0);
217 }
218
219 #[rstest]
220 fn test_position_opened_realized_pnl_uses_settlement_currency() {
221 let instrument = InstrumentAny::CryptoPerpetual(xbtusd_bitmex());
222 let fill = OrderFilledSpec::builder()
223 .instrument_id(instrument.id())
224 .position_id(PositionId::from("P-001"))
225 .last_qty(Quantity::from("100000"))
226 .last_px(Price::from("10500.0"))
227 .currency(Currency::USD())
228 .commission(Money::from("0.01 BTC"))
229 .build();
230 let position = Position::new(&instrument, fill.clone());
231
232 let event =
233 PositionOpened::create(&position, &fill, UUID4::default(), UnixNanos::default());
234
235 assert_eq!(event.currency, Currency::USD());
236 assert_eq!(event.realized_pnl, Some(Money::from("-0.01 BTC")));
237 }
238}