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