1use nautilus_core::{
17 UUID4,
18 nanos::{DurationNanos, UnixNanos},
19};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 enums::{OrderSide, PositionSide},
24 events::OrderFilled,
25 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
26 position::Position,
27 types::{Currency, Money, Price, Quantity},
28};
29
30#[repr(C)]
32#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
40)]
41pub struct PositionClosed {
42 pub trader_id: TraderId,
44 pub strategy_id: StrategyId,
46 pub instrument_id: InstrumentId,
48 pub position_id: PositionId,
50 pub account_id: AccountId,
52 pub opening_order_id: ClientOrderId,
54 pub closing_order_id: Option<ClientOrderId>,
56 pub entry: OrderSide,
58 pub side: PositionSide,
60 pub signed_qty: f64,
62 pub quantity: Quantity,
64 pub peak_quantity: Quantity,
66 pub last_qty: Quantity,
68 pub last_px: Price,
70 pub currency: Currency,
72 pub avg_px_open: f64,
74 pub avg_px_close: Option<f64>,
76 pub realized_return: f64,
78 pub realized_pnl: Option<Money>,
80 pub unrealized_pnl: Money,
82 pub duration: DurationNanos,
84 pub event_id: UUID4,
86 pub ts_opened: UnixNanos,
88 pub ts_closed: Option<UnixNanos>,
90 pub ts_event: UnixNanos,
92 pub ts_init: UnixNanos,
94}
95
96impl PositionClosed {
97 #[must_use]
98 pub fn create(
99 position: &Position,
100 fill: &OrderFilled,
101 event_id: UUID4,
102 ts_init: UnixNanos,
103 ) -> Self {
104 Self {
105 trader_id: position.trader_id,
106 strategy_id: position.strategy_id,
107 instrument_id: position.instrument_id,
108 position_id: position.id,
109 account_id: position.account_id,
110 opening_order_id: position.opening_order_id,
111 closing_order_id: position.closing_order_id,
112 entry: position.entry,
113 side: position.side,
114 signed_qty: position.signed_qty,
115 quantity: position.quantity,
116 peak_quantity: position.peak_qty,
117 last_qty: fill.last_qty,
118 last_px: fill.last_px,
119 currency: position.quote_currency,
120 avg_px_open: position.avg_px_open,
121 avg_px_close: position.avg_px_close,
122 realized_return: position.realized_return,
123 realized_pnl: position.realized_pnl,
124 unrealized_pnl: Money::new(0.0, position.quote_currency),
125 duration: position.duration_ns,
126 event_id,
127 ts_opened: position.ts_opened,
128 ts_closed: position.ts_closed,
129 ts_event: fill.ts_event,
130 ts_init,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use nautilus_core::UnixNanos;
138 use rstest::*;
139
140 use super::*;
141 use crate::{
142 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
143 events::OrderFilled,
144 identifiers::{
145 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
146 VenueOrderId,
147 },
148 instruments::{InstrumentAny, stubs::audusd_sim},
149 position::Position,
150 types::{Currency, Money, Price, Quantity},
151 };
152
153 fn create_test_position_closed() -> PositionClosed {
154 PositionClosed {
155 trader_id: TraderId::from("TRADER-001"),
156 strategy_id: StrategyId::from("EMA-CROSS"),
157 instrument_id: InstrumentId::from("EURUSD.SIM"),
158 position_id: PositionId::from("P-001"),
159 account_id: AccountId::from("SIM-001"),
160 opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
161 closing_order_id: Some(ClientOrderId::from("O-19700101-000000-001-001-2")),
162 entry: OrderSide::Buy,
163 side: PositionSide::Flat,
164 signed_qty: 0.0,
165 quantity: Quantity::from("0"),
166 peak_quantity: Quantity::from("150"),
167 last_qty: Quantity::from("150"),
168 last_px: Price::from("1.0600"),
169 currency: Currency::USD(),
170 avg_px_open: 1.0525,
171 avg_px_close: Some(1.0600),
172 realized_return: 0.0071,
173 realized_pnl: Some(Money::new(112.50, Currency::USD())),
174 unrealized_pnl: Money::new(0.0, Currency::USD()),
175 duration: 3_600_000_000_000, event_id: UUID4::default(),
177 ts_opened: UnixNanos::from(1_000_000_000),
178 ts_closed: Some(UnixNanos::from(4_600_000_000)),
179 ts_event: UnixNanos::from(4_600_000_000),
180 ts_init: UnixNanos::from(5_000_000_000),
181 }
182 }
183
184 fn create_test_order_filled() -> OrderFilled {
185 OrderFilled::new(
186 TraderId::from("TRADER-001"),
187 StrategyId::from("EMA-CROSS"),
188 InstrumentId::from("EURUSD.SIM"),
189 ClientOrderId::from("O-19700101-000000-001-001-2"),
190 VenueOrderId::from("2"),
191 AccountId::from("SIM-001"),
192 TradeId::from("T-002"),
193 OrderSide::Sell,
194 OrderType::Market,
195 Quantity::from("150"),
196 Price::from("1.0600"),
197 Currency::USD(),
198 LiquiditySide::Taker,
199 UUID4::default(),
200 UnixNanos::from(4_600_000_000),
201 UnixNanos::from(5_000_000_000),
202 false,
203 Some(PositionId::from("P-001")),
204 Some(Money::new(2.5, Currency::USD())),
205 )
206 }
207
208 #[rstest]
209 fn test_position_closed_create() {
210 let instrument = audusd_sim();
211 let initial_fill = OrderFilled::new(
212 TraderId::from("TRADER-001"),
213 StrategyId::from("EMA-CROSS"),
214 InstrumentId::from("AUD/USD.SIM"),
215 ClientOrderId::from("O-19700101-000000-001-001-1"),
216 VenueOrderId::from("1"),
217 AccountId::from("SIM-001"),
218 TradeId::from("T-001"),
219 OrderSide::Buy,
220 OrderType::Market,
221 Quantity::from("100"),
222 Price::from("0.8000"),
223 Currency::USD(),
224 LiquiditySide::Taker,
225 UUID4::default(),
226 UnixNanos::from(1_000_000_000),
227 UnixNanos::from(2_000_000_000),
228 false,
229 Some(PositionId::from("P-001")),
230 Some(Money::new(2.0, Currency::USD())),
231 );
232
233 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
234 let closing_fill = create_test_order_filled();
235 let event_id = UUID4::default();
236 let ts_init = UnixNanos::from(6_000_000_000);
237
238 let position_closed = PositionClosed::create(&position, &closing_fill, event_id, ts_init);
239
240 assert_eq!(position_closed.trader_id, position.trader_id);
241 assert_eq!(position_closed.strategy_id, position.strategy_id);
242 assert_eq!(position_closed.instrument_id, position.instrument_id);
243 assert_eq!(position_closed.position_id, position.id);
244 assert_eq!(position_closed.account_id, position.account_id);
245 assert_eq!(position_closed.opening_order_id, position.opening_order_id);
246 assert_eq!(position_closed.closing_order_id, position.closing_order_id);
247 assert_eq!(position_closed.entry, position.entry);
248 assert_eq!(position_closed.side, position.side);
249 assert_eq!(position_closed.signed_qty, position.signed_qty);
250 assert_eq!(position_closed.quantity, position.quantity);
251 assert_eq!(position_closed.peak_quantity, position.peak_qty);
252 assert_eq!(position_closed.last_qty, closing_fill.last_qty);
253 assert_eq!(position_closed.last_px, closing_fill.last_px);
254 assert_eq!(position_closed.currency, position.quote_currency);
255 assert_eq!(position_closed.avg_px_open, position.avg_px_open);
256 assert_eq!(position_closed.avg_px_close, position.avg_px_close);
257 assert_eq!(position_closed.realized_return, position.realized_return);
258 assert_eq!(position_closed.realized_pnl, position.realized_pnl);
259 assert_eq!(
260 position_closed.unrealized_pnl,
261 Money::new(0.0, position.quote_currency)
262 );
263 assert_eq!(position_closed.duration, position.duration_ns);
264 assert_eq!(position_closed.event_id, event_id);
265 assert_eq!(position_closed.ts_opened, position.ts_opened);
266 assert_eq!(position_closed.ts_closed, position.ts_closed);
267 assert_eq!(position_closed.ts_event, closing_fill.ts_event);
268 assert_eq!(position_closed.ts_init, ts_init);
269 }
270
271 #[rstest]
272 fn test_position_closed_flat_position() {
273 let position_closed = create_test_position_closed();
274
275 assert_eq!(position_closed.side, PositionSide::Flat);
276 assert_eq!(position_closed.signed_qty, 0.0);
277 assert_eq!(position_closed.quantity, Quantity::from("0"));
278 assert_eq!(
279 position_closed.unrealized_pnl,
280 Money::new(0.0, Currency::USD())
281 );
282 }
283
284 #[rstest]
285 fn test_position_closed_loss_scenario() {
286 let mut position_closed = create_test_position_closed();
287 position_closed.avg_px_close = Some(1.0400); position_closed.realized_return = -0.0119;
289 position_closed.realized_pnl = Some(Money::new(-187.50, Currency::USD()));
290
291 assert_eq!(position_closed.avg_px_close, Some(1.0400));
292 assert!(position_closed.realized_return < 0.0);
293 assert_eq!(
294 position_closed.realized_pnl,
295 Some(Money::new(-187.50, Currency::USD()))
296 );
297 }
298}