1use std::cell::RefCell;
19
20use ahash::AHashMap;
21use indexmap::IndexMap;
22use nautilus_analysis::snapshot::PortfolioStatistics;
23use nautilus_common::factories::OrderFactory;
24use nautilus_core::UnixNanos;
25use nautilus_model::{
26 enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
27 events::PortfolioSnapshot,
28 identifiers::{
29 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId, Venue,
30 },
31 orders::{OrderAny, OrderList},
32 types::{Currency, Money, Price, Quantity},
33};
34use nautilus_portfolio::Portfolio;
35use rust_decimal::Decimal;
36use ustr::Ustr;
37
38#[derive(Debug)]
40pub struct OrderApi<'a> {
41 order_factory: &'a RefCell<OrderFactory>,
42}
43
44#[bon::bon]
45impl<'a> OrderApi<'a> {
46 pub(crate) const fn new(order_factory: &'a RefCell<OrderFactory>) -> Self {
47 Self { order_factory }
48 }
49
50 #[must_use]
56 pub fn generate_client_order_id(&self) -> ClientOrderId {
57 self.order_factory.borrow_mut().generate_client_order_id()
58 }
59
60 #[must_use]
66 pub fn generate_order_list_id(&self) -> OrderListId {
67 self.order_factory.borrow_mut().generate_order_list_id()
68 }
69
70 #[must_use]
77 #[expect(clippy::too_many_arguments)]
78 pub fn market(
79 &self,
80 instrument_id: InstrumentId,
81 order_side: OrderSide,
82 quantity: Quantity,
83 time_in_force: Option<TimeInForce>,
84 reduce_only: Option<bool>,
85 quote_quantity: Option<bool>,
86 exec_algorithm_id: Option<ExecAlgorithmId>,
87 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
88 tags: Option<Vec<Ustr>>,
89 client_order_id: Option<ClientOrderId>,
90 ) -> OrderAny {
91 self.order_factory.borrow_mut().market(
92 instrument_id,
93 order_side,
94 quantity,
95 time_in_force,
96 reduce_only,
97 quote_quantity,
98 exec_algorithm_id,
99 exec_algorithm_params,
100 tags,
101 client_order_id,
102 )
103 }
104
105 #[must_use]
112 #[expect(clippy::too_many_arguments)]
113 pub fn limit(
114 &self,
115 instrument_id: InstrumentId,
116 order_side: OrderSide,
117 quantity: Quantity,
118 price: Price,
119 time_in_force: Option<TimeInForce>,
120 expire_time: Option<UnixNanos>,
121 post_only: Option<bool>,
122 reduce_only: Option<bool>,
123 quote_quantity: Option<bool>,
124 display_qty: Option<Quantity>,
125 emulation_trigger: Option<TriggerType>,
126 trigger_instrument_id: Option<InstrumentId>,
127 exec_algorithm_id: Option<ExecAlgorithmId>,
128 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
129 tags: Option<Vec<Ustr>>,
130 client_order_id: Option<ClientOrderId>,
131 ) -> OrderAny {
132 self.order_factory.borrow_mut().limit(
133 instrument_id,
134 order_side,
135 quantity,
136 price,
137 time_in_force,
138 expire_time,
139 post_only,
140 reduce_only,
141 quote_quantity,
142 display_qty,
143 emulation_trigger,
144 trigger_instrument_id,
145 exec_algorithm_id,
146 exec_algorithm_params,
147 tags,
148 client_order_id,
149 )
150 }
151
152 #[must_use]
159 #[expect(clippy::too_many_arguments)]
160 pub fn stop_market(
161 &self,
162 instrument_id: InstrumentId,
163 order_side: OrderSide,
164 quantity: Quantity,
165 trigger_price: Price,
166 trigger_type: Option<TriggerType>,
167 time_in_force: Option<TimeInForce>,
168 expire_time: Option<UnixNanos>,
169 reduce_only: Option<bool>,
170 quote_quantity: Option<bool>,
171 display_qty: Option<Quantity>,
172 emulation_trigger: Option<TriggerType>,
173 trigger_instrument_id: Option<InstrumentId>,
174 exec_algorithm_id: Option<ExecAlgorithmId>,
175 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
176 tags: Option<Vec<Ustr>>,
177 client_order_id: Option<ClientOrderId>,
178 ) -> OrderAny {
179 self.order_factory.borrow_mut().stop_market(
180 instrument_id,
181 order_side,
182 quantity,
183 trigger_price,
184 trigger_type,
185 time_in_force,
186 expire_time,
187 reduce_only,
188 quote_quantity,
189 display_qty,
190 emulation_trigger,
191 trigger_instrument_id,
192 exec_algorithm_id,
193 exec_algorithm_params,
194 tags,
195 client_order_id,
196 )
197 }
198
199 #[must_use]
206 #[expect(clippy::too_many_arguments)]
207 pub fn stop_limit(
208 &self,
209 instrument_id: InstrumentId,
210 order_side: OrderSide,
211 quantity: Quantity,
212 price: Price,
213 trigger_price: Price,
214 trigger_type: Option<TriggerType>,
215 time_in_force: Option<TimeInForce>,
216 expire_time: Option<UnixNanos>,
217 post_only: Option<bool>,
218 reduce_only: Option<bool>,
219 quote_quantity: Option<bool>,
220 display_qty: Option<Quantity>,
221 emulation_trigger: Option<TriggerType>,
222 trigger_instrument_id: Option<InstrumentId>,
223 exec_algorithm_id: Option<ExecAlgorithmId>,
224 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
225 tags: Option<Vec<Ustr>>,
226 client_order_id: Option<ClientOrderId>,
227 ) -> OrderAny {
228 self.order_factory.borrow_mut().stop_limit(
229 instrument_id,
230 order_side,
231 quantity,
232 price,
233 trigger_price,
234 trigger_type,
235 time_in_force,
236 expire_time,
237 post_only,
238 reduce_only,
239 quote_quantity,
240 display_qty,
241 emulation_trigger,
242 trigger_instrument_id,
243 exec_algorithm_id,
244 exec_algorithm_params,
245 tags,
246 client_order_id,
247 )
248 }
249
250 #[must_use]
257 #[expect(clippy::too_many_arguments)]
258 pub fn market_to_limit(
259 &self,
260 instrument_id: InstrumentId,
261 order_side: OrderSide,
262 quantity: Quantity,
263 time_in_force: Option<TimeInForce>,
264 expire_time: Option<UnixNanos>,
265 reduce_only: Option<bool>,
266 quote_quantity: Option<bool>,
267 display_qty: Option<Quantity>,
268 exec_algorithm_id: Option<ExecAlgorithmId>,
269 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
270 tags: Option<Vec<Ustr>>,
271 client_order_id: Option<ClientOrderId>,
272 ) -> OrderAny {
273 self.order_factory.borrow_mut().market_to_limit(
274 instrument_id,
275 order_side,
276 quantity,
277 time_in_force,
278 expire_time,
279 reduce_only,
280 quote_quantity,
281 display_qty,
282 exec_algorithm_id,
283 exec_algorithm_params,
284 tags,
285 client_order_id,
286 )
287 }
288
289 #[must_use]
296 #[expect(clippy::too_many_arguments)]
297 pub fn market_if_touched(
298 &self,
299 instrument_id: InstrumentId,
300 order_side: OrderSide,
301 quantity: Quantity,
302 trigger_price: Price,
303 trigger_type: Option<TriggerType>,
304 time_in_force: Option<TimeInForce>,
305 expire_time: Option<UnixNanos>,
306 reduce_only: Option<bool>,
307 quote_quantity: Option<bool>,
308 emulation_trigger: Option<TriggerType>,
309 trigger_instrument_id: Option<InstrumentId>,
310 exec_algorithm_id: Option<ExecAlgorithmId>,
311 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
312 tags: Option<Vec<Ustr>>,
313 client_order_id: Option<ClientOrderId>,
314 ) -> OrderAny {
315 self.order_factory.borrow_mut().market_if_touched(
316 instrument_id,
317 order_side,
318 quantity,
319 trigger_price,
320 trigger_type,
321 time_in_force,
322 expire_time,
323 reduce_only,
324 quote_quantity,
325 emulation_trigger,
326 trigger_instrument_id,
327 exec_algorithm_id,
328 exec_algorithm_params,
329 tags,
330 client_order_id,
331 )
332 }
333
334 #[must_use]
341 #[expect(clippy::too_many_arguments)]
342 pub fn limit_if_touched(
343 &self,
344 instrument_id: InstrumentId,
345 order_side: OrderSide,
346 quantity: Quantity,
347 price: Price,
348 trigger_price: Price,
349 trigger_type: Option<TriggerType>,
350 time_in_force: Option<TimeInForce>,
351 expire_time: Option<UnixNanos>,
352 post_only: Option<bool>,
353 reduce_only: Option<bool>,
354 quote_quantity: Option<bool>,
355 display_qty: Option<Quantity>,
356 emulation_trigger: Option<TriggerType>,
357 trigger_instrument_id: Option<InstrumentId>,
358 exec_algorithm_id: Option<ExecAlgorithmId>,
359 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
360 tags: Option<Vec<Ustr>>,
361 client_order_id: Option<ClientOrderId>,
362 ) -> OrderAny {
363 self.order_factory.borrow_mut().limit_if_touched(
364 instrument_id,
365 order_side,
366 quantity,
367 price,
368 trigger_price,
369 trigger_type,
370 time_in_force,
371 expire_time,
372 post_only,
373 reduce_only,
374 quote_quantity,
375 display_qty,
376 emulation_trigger,
377 trigger_instrument_id,
378 exec_algorithm_id,
379 exec_algorithm_params,
380 tags,
381 client_order_id,
382 )
383 }
384
385 #[must_use]
392 #[expect(clippy::too_many_arguments)]
393 pub fn trailing_stop_market(
394 &self,
395 instrument_id: InstrumentId,
396 order_side: OrderSide,
397 quantity: Quantity,
398 trailing_offset: Decimal,
399 trailing_offset_type: Option<TrailingOffsetType>,
400 activation_price: Option<Price>,
401 trigger_price: Option<Price>,
402 trigger_type: Option<TriggerType>,
403 time_in_force: Option<TimeInForce>,
404 expire_time: Option<UnixNanos>,
405 reduce_only: Option<bool>,
406 quote_quantity: Option<bool>,
407 display_qty: Option<Quantity>,
408 emulation_trigger: Option<TriggerType>,
409 trigger_instrument_id: Option<InstrumentId>,
410 exec_algorithm_id: Option<ExecAlgorithmId>,
411 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
412 tags: Option<Vec<Ustr>>,
413 client_order_id: Option<ClientOrderId>,
414 ) -> OrderAny {
415 self.order_factory.borrow_mut().trailing_stop_market(
416 instrument_id,
417 order_side,
418 quantity,
419 trailing_offset,
420 trailing_offset_type,
421 activation_price,
422 trigger_price,
423 trigger_type,
424 time_in_force,
425 expire_time,
426 reduce_only,
427 quote_quantity,
428 display_qty,
429 emulation_trigger,
430 trigger_instrument_id,
431 exec_algorithm_id,
432 exec_algorithm_params,
433 tags,
434 client_order_id,
435 )
436 }
437
438 #[must_use]
445 #[expect(clippy::too_many_arguments)]
446 pub fn trailing_stop_limit(
447 &self,
448 instrument_id: InstrumentId,
449 order_side: OrderSide,
450 quantity: Quantity,
451 price: Price,
452 limit_offset: Decimal,
453 trailing_offset: Decimal,
454 trailing_offset_type: Option<TrailingOffsetType>,
455 activation_price: Option<Price>,
456 trigger_price: Option<Price>,
457 trigger_type: Option<TriggerType>,
458 time_in_force: Option<TimeInForce>,
459 expire_time: Option<UnixNanos>,
460 post_only: Option<bool>,
461 reduce_only: Option<bool>,
462 quote_quantity: Option<bool>,
463 display_qty: Option<Quantity>,
464 emulation_trigger: Option<TriggerType>,
465 trigger_instrument_id: Option<InstrumentId>,
466 exec_algorithm_id: Option<ExecAlgorithmId>,
467 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
468 tags: Option<Vec<Ustr>>,
469 client_order_id: Option<ClientOrderId>,
470 ) -> OrderAny {
471 self.order_factory.borrow_mut().trailing_stop_limit(
472 instrument_id,
473 order_side,
474 quantity,
475 Some(price),
476 limit_offset,
477 trailing_offset,
478 trailing_offset_type,
479 activation_price,
480 trigger_price,
481 trigger_type,
482 time_in_force,
483 expire_time,
484 post_only,
485 reduce_only,
486 quote_quantity,
487 display_qty,
488 emulation_trigger,
489 trigger_instrument_id,
490 exec_algorithm_id,
491 exec_algorithm_params,
492 tags,
493 client_order_id,
494 )
495 }
496
497 #[must_use]
504 pub fn create_list(&self, orders: &mut [OrderAny], ts_init: UnixNanos) -> OrderList {
505 self.order_factory.borrow_mut().create_list(orders, ts_init)
506 }
507
508 #[must_use]
515 #[builder]
516 pub fn bracket(
517 &self,
518 instrument_id: InstrumentId,
519 order_side: OrderSide,
520 quantity: Quantity,
521 #[builder(default = false)] quote_quantity: bool,
522 emulation_trigger: Option<TriggerType>,
523 trigger_instrument_id: Option<InstrumentId>,
524 #[builder(default = ContingencyType::Ouo)] contingency_type: ContingencyType,
525 #[builder(default = OrderType::Market)] entry_order_type: OrderType,
526 entry_price: Option<Price>,
527 entry_trigger_price: Option<Price>,
528 expire_time: Option<UnixNanos>,
529 #[builder(default = TimeInForce::Gtc)] time_in_force: TimeInForce,
530 #[builder(default = false)] entry_post_only: bool,
531 entry_exec_algorithm_id: Option<ExecAlgorithmId>,
532 entry_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
533 #[builder(default = vec![Ustr::from("ENTRY")])] entry_tags: Vec<Ustr>,
534 entry_client_order_id: Option<ClientOrderId>,
535 #[builder(default = OrderType::Limit)] tp_order_type: OrderType,
536 tp_price: Option<Price>,
537 tp_trigger_price: Option<Price>,
538 #[builder(default = TriggerType::Default)] tp_trigger_type: TriggerType,
539 tp_activation_price: Option<Price>,
540 tp_trailing_offset: Option<Decimal>,
541 #[builder(default = TrailingOffsetType::Price)] tp_trailing_offset_type: TrailingOffsetType,
542 tp_limit_offset: Option<Decimal>,
543 #[builder(default = TimeInForce::Gtc)] tp_time_in_force: TimeInForce,
544 #[builder(default = true)] tp_post_only: bool,
545 tp_exec_algorithm_id: Option<ExecAlgorithmId>,
546 tp_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
547 #[builder(default = vec![Ustr::from("TAKE_PROFIT")])] tp_tags: Vec<Ustr>,
548 tp_client_order_id: Option<ClientOrderId>,
549 #[builder(default = OrderType::StopMarket)] sl_order_type: OrderType,
550 sl_trigger_price: Option<Price>,
551 #[builder(default = TriggerType::Default)] sl_trigger_type: TriggerType,
552 sl_activation_price: Option<Price>,
553 sl_trailing_offset: Option<Decimal>,
554 #[builder(default = TrailingOffsetType::Price)] sl_trailing_offset_type: TrailingOffsetType,
555 #[builder(default = TimeInForce::Gtc)] sl_time_in_force: TimeInForce,
556 sl_exec_algorithm_id: Option<ExecAlgorithmId>,
557 sl_exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
558 #[builder(default = vec![Ustr::from("STOP_LOSS")])] sl_tags: Vec<Ustr>,
559 sl_client_order_id: Option<ClientOrderId>,
560 ) -> Vec<OrderAny> {
561 let mut order_factory = self.order_factory.borrow_mut();
562 order_factory
563 .bracket()
564 .instrument_id(instrument_id)
565 .order_side(order_side)
566 .quantity(quantity)
567 .quote_quantity(quote_quantity)
568 .maybe_emulation_trigger(emulation_trigger)
569 .maybe_trigger_instrument_id(trigger_instrument_id)
570 .contingency_type(contingency_type)
571 .entry_order_type(entry_order_type)
572 .maybe_entry_price(entry_price)
573 .maybe_entry_trigger_price(entry_trigger_price)
574 .maybe_expire_time(expire_time)
575 .time_in_force(time_in_force)
576 .entry_post_only(entry_post_only)
577 .maybe_entry_exec_algorithm_id(entry_exec_algorithm_id)
578 .maybe_entry_exec_algorithm_params(entry_exec_algorithm_params)
579 .entry_tags(entry_tags)
580 .maybe_entry_client_order_id(entry_client_order_id)
581 .tp_order_type(tp_order_type)
582 .maybe_tp_price(tp_price)
583 .maybe_tp_trigger_price(tp_trigger_price)
584 .tp_trigger_type(tp_trigger_type)
585 .maybe_tp_activation_price(tp_activation_price)
586 .maybe_tp_trailing_offset(tp_trailing_offset)
587 .tp_trailing_offset_type(tp_trailing_offset_type)
588 .maybe_tp_limit_offset(tp_limit_offset)
589 .tp_time_in_force(tp_time_in_force)
590 .tp_post_only(tp_post_only)
591 .maybe_tp_exec_algorithm_id(tp_exec_algorithm_id)
592 .maybe_tp_exec_algorithm_params(tp_exec_algorithm_params)
593 .tp_tags(tp_tags)
594 .maybe_tp_client_order_id(tp_client_order_id)
595 .sl_order_type(sl_order_type)
596 .maybe_sl_trigger_price(sl_trigger_price)
597 .sl_trigger_type(sl_trigger_type)
598 .maybe_sl_activation_price(sl_activation_price)
599 .maybe_sl_trailing_offset(sl_trailing_offset)
600 .sl_trailing_offset_type(sl_trailing_offset_type)
601 .sl_time_in_force(sl_time_in_force)
602 .maybe_sl_exec_algorithm_id(sl_exec_algorithm_id)
603 .maybe_sl_exec_algorithm_params(sl_exec_algorithm_params)
604 .sl_tags(sl_tags)
605 .maybe_sl_client_order_id(sl_client_order_id)
606 .call()
607 }
608}
609
610#[derive(Debug)]
612pub struct PortfolioApi<'a> {
613 portfolio: &'a RefCell<Portfolio>,
614}
615
616impl<'a> PortfolioApi<'a> {
617 pub(crate) const fn new(portfolio: &'a RefCell<Portfolio>) -> Self {
618 Self { portfolio }
619 }
620
621 #[must_use]
627 pub fn is_initialized(&self) -> bool {
628 self.portfolio.borrow().is_initialized()
629 }
630
631 #[must_use]
638 pub fn balances_locked(
639 &self,
640 venue: &Venue,
641 account_id: Option<&AccountId>,
642 ) -> IndexMap<Currency, Money> {
643 self.portfolio.borrow().balances_locked(venue, account_id)
644 }
645
646 #[must_use]
653 pub fn instrument_initial_margins(
654 &self,
655 venue: &Venue,
656 account_id: Option<&AccountId>,
657 ) -> IndexMap<InstrumentId, Money> {
658 self.portfolio
659 .borrow()
660 .instrument_initial_margins(venue, account_id)
661 }
662
663 #[must_use]
670 pub fn instrument_maintenance_margins(
671 &self,
672 venue: &Venue,
673 account_id: Option<&AccountId>,
674 ) -> IndexMap<InstrumentId, Money> {
675 self.portfolio
676 .borrow()
677 .instrument_maintenance_margins(venue, account_id)
678 }
679
680 #[must_use]
686 pub fn unrealized_pnls(
687 &self,
688 venue: &Venue,
689 account_id: Option<&AccountId>,
690 ) -> Option<IndexMap<Currency, Money>> {
691 self.portfolio
692 .borrow_mut()
693 .unrealized_pnls(venue, account_id, None)
694 }
695
696 #[must_use]
702 pub fn realized_pnls(
703 &self,
704 venue: &Venue,
705 account_id: Option<&AccountId>,
706 ) -> Option<IndexMap<Currency, Money>> {
707 self.portfolio
708 .borrow_mut()
709 .realized_pnls(venue, account_id, None)
710 }
711
712 #[must_use]
718 pub fn net_exposures(
719 &self,
720 venue: &Venue,
721 account_id: Option<&AccountId>,
722 ) -> Option<IndexMap<Currency, Money>> {
723 self.portfolio
724 .borrow()
725 .net_exposures(venue, account_id, None)
726 }
727
728 #[must_use]
734 pub fn unrealized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
735 self.portfolio.borrow_mut().unrealized_pnl(instrument_id)
736 }
737
738 #[must_use]
744 pub fn unrealized_pnl_for_account(
745 &self,
746 instrument_id: &InstrumentId,
747 account_id: Option<&AccountId>,
748 ) -> Option<Money> {
749 self.portfolio.borrow_mut().unrealized_pnl_for_account(
750 instrument_id,
751 None,
752 account_id,
753 None,
754 )
755 }
756
757 #[must_use]
763 pub fn realized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
764 self.portfolio.borrow_mut().realized_pnl(instrument_id)
765 }
766
767 #[must_use]
773 pub fn realized_pnl_for_account(
774 &self,
775 instrument_id: &InstrumentId,
776 account_id: Option<&AccountId>,
777 ) -> Option<Money> {
778 self.portfolio
779 .borrow_mut()
780 .realized_pnl_for_account(instrument_id, account_id, None)
781 }
782
783 #[must_use]
789 pub fn total_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
790 self.portfolio.borrow_mut().total_pnl(instrument_id)
791 }
792
793 #[must_use]
799 pub fn total_pnl_for_account(
800 &self,
801 instrument_id: &InstrumentId,
802 account_id: Option<&AccountId>,
803 ) -> Option<Money> {
804 self.portfolio
805 .borrow_mut()
806 .total_pnl_for_account(instrument_id, None, account_id, None)
807 }
808
809 #[must_use]
815 pub fn total_pnls(
816 &self,
817 venue: &Venue,
818 account_id: Option<&AccountId>,
819 ) -> Option<IndexMap<Currency, Money>> {
820 self.portfolio
821 .borrow_mut()
822 .total_pnls(venue, account_id, None)
823 }
824
825 #[must_use]
831 pub fn mark_values(
832 &self,
833 venue: &Venue,
834 account_id: Option<&AccountId>,
835 ) -> IndexMap<Currency, Money> {
836 self.portfolio.borrow_mut().mark_values(venue, account_id)
837 }
838
839 #[must_use]
845 pub fn equity(
846 &self,
847 venue: &Venue,
848 account_id: Option<&AccountId>,
849 ) -> IndexMap<Currency, Money> {
850 self.portfolio.borrow_mut().equity(venue, account_id)
851 }
852
853 #[must_use]
859 pub fn build_snapshot(&self, account_id: &AccountId) -> Option<PortfolioSnapshot> {
860 self.portfolio.borrow_mut().build_snapshot(account_id)
861 }
862
863 #[must_use]
869 pub fn statistics(&self) -> PortfolioStatistics {
870 self.portfolio.borrow().statistics()
871 }
872
873 #[must_use]
879 pub fn snapshots(&self, account_id: &AccountId) -> Vec<PortfolioSnapshot> {
880 self.portfolio.borrow().snapshots(account_id)
881 }
882
883 #[must_use]
889 pub fn missing_price_instruments(&self, venue: &Venue) -> Vec<InstrumentId> {
890 self.portfolio
891 .borrow()
892 .missing_price_instruments(venue, None)
893 }
894
895 #[must_use]
901 pub fn net_exposure(
902 &self,
903 instrument_id: &InstrumentId,
904 account_id: Option<&AccountId>,
905 ) -> Option<Money> {
906 self.portfolio
907 .borrow()
908 .net_exposure(instrument_id, None, account_id, None)
909 }
910
911 #[must_use]
917 pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
918 self.portfolio.borrow().net_position(instrument_id)
919 }
920
921 #[must_use]
927 pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool {
928 self.portfolio.borrow().is_net_long(instrument_id)
929 }
930
931 #[must_use]
937 pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool {
938 self.portfolio.borrow().is_net_short(instrument_id)
939 }
940
941 #[must_use]
947 pub fn is_net_flat(&self, instrument_id: &InstrumentId) -> bool {
948 self.portfolio.borrow().is_net_flat(instrument_id)
949 }
950
951 #[must_use]
957 pub fn is_completely_net_flat(&self) -> bool {
958 self.portfolio.borrow().is_completely_net_flat()
959 }
960
961 #[must_use]
969 pub fn recorded_realized_pnls(&self) -> AHashMap<Currency, Vec<(PositionId, UnixNanos, f64)>> {
970 self.portfolio.borrow().recorded_realized_pnls()
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use std::{cell::RefCell, rc::Rc};
977
978 use nautilus_common::{cache::Cache, clock::VirtualClock, factories::OrderFactory};
979 use nautilus_core::{UUID4, UnixNanos};
980 use nautilus_model::{
981 enums::{AccountType, OrderSide, OrderType},
982 events::AccountState,
983 identifiers::{AccountId, InstrumentId, StrategyId, TraderId, Venue},
984 orders::Order,
985 types::{AccountBalance, MarginBalance},
986 };
987 use rstest::rstest;
988
989 use super::*;
990
991 #[rstest]
992 fn test_order_api_creates_market_order() {
993 let trader_id = TraderId::from("TRADER-001");
994 let strategy_id = StrategyId::from("S-001");
995 let clock = Rc::new(RefCell::new(VirtualClock::new()));
996 let order_factory = RefCell::new(OrderFactory::new(
997 trader_id,
998 strategy_id,
999 None,
1000 None,
1001 clock,
1002 false,
1003 true,
1004 ));
1005 let api = OrderApi::new(&order_factory);
1006 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1007
1008 let order = api.market(
1009 instrument_id,
1010 OrderSide::Buy,
1011 Quantity::from("100000"),
1012 None,
1013 None,
1014 None,
1015 None,
1016 None,
1017 None,
1018 None,
1019 );
1020
1021 assert_eq!(order.order_type(), OrderType::Market);
1022 assert_eq!(order.instrument_id(), instrument_id);
1023 assert_eq!(order.order_side(), OrderSide::Buy);
1024 assert_eq!(order.quantity(), Quantity::from("100000"));
1025 assert_eq!(order.trader_id(), trader_id);
1026 assert_eq!(order.strategy_id(), strategy_id);
1027 }
1028
1029 #[rstest]
1030 fn test_order_api_creates_bracket_orders() {
1031 let trader_id = TraderId::from("TRADER-001");
1032 let strategy_id = StrategyId::from("S-001");
1033 let clock = Rc::new(RefCell::new(VirtualClock::new()));
1034 let order_factory = RefCell::new(OrderFactory::new(
1035 trader_id,
1036 strategy_id,
1037 None,
1038 None,
1039 clock,
1040 false,
1041 true,
1042 ));
1043 let api = OrderApi::new(&order_factory);
1044 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1045
1046 let orders = api
1047 .bracket()
1048 .instrument_id(instrument_id)
1049 .order_side(OrderSide::Buy)
1050 .quantity(Quantity::from("100000"))
1051 .tp_price(Price::from("1.10000"))
1052 .sl_trigger_price(Price::from("0.90000"))
1053 .call();
1054
1055 assert_eq!(orders.len(), 3);
1056 assert!(
1057 orders
1058 .iter()
1059 .all(|order| order.instrument_id() == instrument_id)
1060 );
1061 assert!(orders.iter().all(|order| order.trader_id() == trader_id));
1062 assert!(
1063 orders
1064 .iter()
1065 .all(|order| order.strategy_id() == strategy_id)
1066 );
1067 assert_eq!(orders[0].order_type(), OrderType::Market);
1068 assert_eq!(orders[0].order_side(), OrderSide::Buy);
1069 assert_eq!(orders[0].quantity(), Quantity::from("100000"));
1070 assert_eq!(orders[1].order_type(), OrderType::StopMarket);
1071 assert_eq!(orders[1].order_side(), OrderSide::Sell);
1072 assert_eq!(orders[1].trigger_price(), Some(Price::from("0.90000")));
1073 assert_eq!(orders[2].order_type(), OrderType::Limit);
1074 assert_eq!(orders[2].order_side(), OrderSide::Sell);
1075 assert_eq!(orders[2].price(), Some(Price::from("1.10000")));
1076 }
1077
1078 #[rstest]
1079 fn test_portfolio_api_empty_reads_return_empty_values() {
1080 let cache = Rc::new(RefCell::new(Cache::default()));
1081 let clock = Rc::new(RefCell::new(VirtualClock::new()));
1082 let portfolio = RefCell::new(Portfolio::new(clock, cache, None));
1083 let api = PortfolioApi::new(&portfolio);
1084 let venue = Venue::from("SIM");
1085 let account_id = AccountId::from("SIM-001");
1086 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1087
1088 assert!(!api.is_initialized());
1089 assert!(api.balances_locked(&venue, None).is_empty());
1090 assert!(api.instrument_initial_margins(&venue, None).is_empty());
1091 assert!(api.instrument_maintenance_margins(&venue, None).is_empty());
1092 assert_eq!(api.unrealized_pnls(&venue, None), Some(IndexMap::new()));
1093 assert_eq!(api.realized_pnls(&venue, None), Some(IndexMap::new()));
1094 assert_eq!(api.net_exposures(&venue, None), None);
1095 assert_eq!(api.unrealized_pnl(&instrument_id), None);
1096 assert_eq!(
1097 api.unrealized_pnl_for_account(&instrument_id, Some(&account_id)),
1098 None
1099 );
1100 assert_eq!(api.realized_pnl(&instrument_id), None);
1101 assert_eq!(
1102 api.realized_pnl_for_account(&instrument_id, Some(&account_id)),
1103 None
1104 );
1105 assert_eq!(api.total_pnl(&instrument_id), None);
1106 assert_eq!(
1107 api.total_pnl_for_account(&instrument_id, Some(&account_id)),
1108 None
1109 );
1110 assert_eq!(api.total_pnls(&venue, None), Some(IndexMap::new()));
1111 assert!(api.mark_values(&venue, None).is_empty());
1112 assert!(api.equity(&venue, None).is_empty());
1113 assert_eq!(api.build_snapshot(&account_id), None);
1114 assert!(api.snapshots(&account_id).is_empty());
1115 assert!(api.missing_price_instruments(&venue).is_empty());
1116 assert_eq!(api.net_exposure(&instrument_id, None), None);
1117 assert_eq!(api.net_position(&instrument_id), Decimal::ZERO);
1118 assert!(!api.is_net_long(&instrument_id));
1119 assert!(!api.is_net_short(&instrument_id));
1120 assert!(api.is_net_flat(&instrument_id));
1121 assert!(api.is_completely_net_flat());
1122 assert!(api.recorded_realized_pnls().is_empty());
1123
1124 let _statistics = api.statistics();
1125 }
1126
1127 #[rstest]
1128 fn test_portfolio_api_account_queries_select_account_id() {
1129 let cache = Rc::new(RefCell::new(Cache::default()));
1130 let clock = Rc::new(RefCell::new(VirtualClock::new()));
1131 let portfolio = RefCell::new(Portfolio::new(clock, cache, None));
1132 let venue = Venue::from("SIM");
1133 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1134 let total = Money::from("1000 USD");
1135
1136 for (account_id, locked, initial, maintenance) in [
1137 ("SIM-001", "100 USD", "30 USD", "15 USD"),
1138 ("SIM-002", "250 USD", "70 USD", "35 USD"),
1139 ] {
1140 let account_id = AccountId::from(account_id);
1141 let locked = Money::from(locked);
1142 let initial = Money::from(initial);
1143 let maintenance = Money::from(maintenance);
1144 let balance = AccountBalance::new(total, locked, total - locked);
1145 let margin = MarginBalance::new(initial, maintenance, Some(instrument_id));
1146 let event_id = UUID4::new();
1147
1148 let state = AccountState::new(
1149 account_id,
1150 AccountType::Margin,
1151 vec![balance],
1152 vec![margin],
1153 true,
1154 event_id,
1155 UnixNanos::default(),
1156 UnixNanos::default(),
1157 None,
1158 );
1159 portfolio.borrow_mut().update_account(&state);
1160 }
1161
1162 let api = PortfolioApi::new(&portfolio);
1163 let account_id = AccountId::from("SIM-002");
1164 let expected_locked = Money::from("250 USD");
1165 let expected_initial = Money::from("70 USD");
1166 let expected_maintenance = Money::from("35 USD");
1167
1168 let balances_locked = api.balances_locked(&venue, Some(&account_id));
1169 let initial_margins = api.instrument_initial_margins(&venue, Some(&account_id));
1170 let maintenance_margins = api.instrument_maintenance_margins(&venue, Some(&account_id));
1171
1172 assert_eq!(
1173 balances_locked,
1174 IndexMap::from([(Currency::USD(), expected_locked)])
1175 );
1176 assert_eq!(
1177 initial_margins,
1178 IndexMap::from([(instrument_id, expected_initial)])
1179 );
1180 assert_eq!(
1181 maintenance_margins,
1182 IndexMap::from([(instrument_id, expected_maintenance)])
1183 );
1184 }
1185}