Skip to main content

nautilus_trading/strategy/
api.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! User-facing strategy APIs.
17
18use 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/// User-facing order creation API.
39#[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    /// Generates a new client order ID.
51    ///
52    /// # Panics
53    ///
54    /// Panics if the order factory is already mutably borrowed.
55    #[must_use]
56    pub fn generate_client_order_id(&self) -> ClientOrderId {
57        self.order_factory.borrow_mut().generate_client_order_id()
58    }
59
60    /// Generates a new order list ID.
61    ///
62    /// # Panics
63    ///
64    /// Panics if the order factory is already mutably borrowed.
65    #[must_use]
66    pub fn generate_order_list_id(&self) -> OrderListId {
67        self.order_factory.borrow_mut().generate_order_list_id()
68    }
69
70    /// Creates a new market order.
71    ///
72    /// # Panics
73    ///
74    /// Panics if the order parameters fail validation or the order factory is already mutably
75    /// borrowed.
76    #[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    /// Creates a new limit order.
106    ///
107    /// # Panics
108    ///
109    /// Panics if the order parameters fail validation or the order factory is already mutably
110    /// borrowed.
111    #[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    /// Creates a new stop-market order.
153    ///
154    /// # Panics
155    ///
156    /// Panics if the order parameters fail validation or the order factory is already mutably
157    /// borrowed.
158    #[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    /// Creates a new stop-limit order.
200    ///
201    /// # Panics
202    ///
203    /// Panics if the order parameters fail validation or the order factory is already mutably
204    /// borrowed.
205    #[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    /// Creates a new market-to-limit order.
251    ///
252    /// # Panics
253    ///
254    /// Panics if the order parameters fail validation or the order factory is already mutably
255    /// borrowed.
256    #[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    /// Creates a new market-if-touched order.
290    ///
291    /// # Panics
292    ///
293    /// Panics if the order parameters fail validation or the order factory is already mutably
294    /// borrowed.
295    #[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    /// Creates a new limit-if-touched order.
335    ///
336    /// # Panics
337    ///
338    /// Panics if the order parameters fail validation or the order factory is already mutably
339    /// borrowed.
340    #[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    /// Creates a new trailing-stop-market order.
386    ///
387    /// # Panics
388    ///
389    /// Panics if the order parameters fail validation or the order factory is already mutably
390    /// borrowed.
391    #[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    /// Creates a new trailing-stop-limit order.
439    ///
440    /// # Panics
441    ///
442    /// Panics if the order parameters fail validation or the order factory is already mutably
443    /// borrowed.
444    #[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    /// Creates a new order list from the given orders.
498    ///
499    /// # Panics
500    ///
501    /// Panics if the list parameters fail validation or the order factory is already mutably
502    /// borrowed.
503    #[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    /// Creates a bracket order with an entry order and attached take-profit and stop-loss legs.
509    ///
510    /// # Panics
511    ///
512    /// Panics if the bracket parameters fail validation or the order factory is already mutably
513    /// borrowed.
514    #[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/// User-facing portfolio read API.
611#[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    /// Returns `true` if the portfolio has been initialized.
622    ///
623    /// # Panics
624    ///
625    /// Panics if the portfolio is already mutably borrowed.
626    #[must_use]
627    pub fn is_initialized(&self) -> bool {
628        self.portfolio.borrow().is_initialized()
629    }
630
631    /// Returns the locked balances for the given venue.
632    ///
633    /// # Panics
634    ///
635    /// Panics if the portfolio is already mutably borrowed.
636    #[must_use]
637    pub fn balances_locked(&self, venue: &Venue) -> IndexMap<Currency, Money> {
638        self.portfolio.borrow().balances_locked(venue)
639    }
640
641    /// Returns the initial margin requirements for the given venue.
642    ///
643    /// # Panics
644    ///
645    /// Panics if the portfolio is already mutably borrowed.
646    #[must_use]
647    pub fn instrument_initial_margins(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
648        self.portfolio.borrow().instrument_initial_margins(venue)
649    }
650
651    /// Returns the maintenance margin requirements for the given venue.
652    ///
653    /// # Panics
654    ///
655    /// Panics if the portfolio is already mutably borrowed.
656    #[must_use]
657    pub fn instrument_maintenance_margins(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
658        self.portfolio
659            .borrow()
660            .instrument_maintenance_margins(venue)
661    }
662
663    /// Returns the unrealized PnLs for all positions at the given venue.
664    ///
665    /// # Panics
666    ///
667    /// Panics if the portfolio is already borrowed.
668    #[must_use]
669    pub fn unrealized_pnls(
670        &self,
671        venue: &Venue,
672        account_id: Option<&AccountId>,
673    ) -> Option<IndexMap<Currency, Money>> {
674        self.portfolio
675            .borrow_mut()
676            .unrealized_pnls(venue, account_id, None)
677    }
678
679    /// Returns the realized PnLs for all positions at the given venue.
680    ///
681    /// # Panics
682    ///
683    /// Panics if the portfolio is already borrowed.
684    #[must_use]
685    pub fn realized_pnls(
686        &self,
687        venue: &Venue,
688        account_id: Option<&AccountId>,
689    ) -> Option<IndexMap<Currency, Money>> {
690        self.portfolio
691            .borrow_mut()
692            .realized_pnls(venue, account_id, None)
693    }
694
695    /// Returns net exposures by currency for the given venue.
696    ///
697    /// # Panics
698    ///
699    /// Panics if the portfolio is already mutably borrowed.
700    #[must_use]
701    pub fn net_exposures(
702        &self,
703        venue: &Venue,
704        account_id: Option<&AccountId>,
705    ) -> Option<IndexMap<Currency, Money>> {
706        self.portfolio
707            .borrow()
708            .net_exposures(venue, account_id, None)
709    }
710
711    /// Returns the unrealized PnL for the given instrument ID.
712    ///
713    /// # Panics
714    ///
715    /// Panics if the portfolio is already borrowed.
716    #[must_use]
717    pub fn unrealized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
718        self.portfolio.borrow_mut().unrealized_pnl(instrument_id)
719    }
720
721    /// Returns the unrealized PnL for the given instrument ID and account filter.
722    ///
723    /// # Panics
724    ///
725    /// Panics if the portfolio is already borrowed.
726    #[must_use]
727    pub fn unrealized_pnl_for_account(
728        &self,
729        instrument_id: &InstrumentId,
730        account_id: Option<&AccountId>,
731    ) -> Option<Money> {
732        self.portfolio.borrow_mut().unrealized_pnl_for_account(
733            instrument_id,
734            None,
735            account_id,
736            None,
737        )
738    }
739
740    /// Returns the realized PnL for the given instrument ID.
741    ///
742    /// # Panics
743    ///
744    /// Panics if the portfolio is already borrowed.
745    #[must_use]
746    pub fn realized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
747        self.portfolio.borrow_mut().realized_pnl(instrument_id)
748    }
749
750    /// Returns the realized PnL for the given instrument ID and account filter.
751    ///
752    /// # Panics
753    ///
754    /// Panics if the portfolio is already borrowed.
755    #[must_use]
756    pub fn realized_pnl_for_account(
757        &self,
758        instrument_id: &InstrumentId,
759        account_id: Option<&AccountId>,
760    ) -> Option<Money> {
761        self.portfolio
762            .borrow_mut()
763            .realized_pnl_for_account(instrument_id, account_id, None)
764    }
765
766    /// Returns the total PnL for the given instrument ID.
767    ///
768    /// # Panics
769    ///
770    /// Panics if the portfolio is already borrowed.
771    #[must_use]
772    pub fn total_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
773        self.portfolio.borrow_mut().total_pnl(instrument_id)
774    }
775
776    /// Returns the total PnL for the given instrument ID and account filter.
777    ///
778    /// # Panics
779    ///
780    /// Panics if the portfolio is already borrowed.
781    #[must_use]
782    pub fn total_pnl_for_account(
783        &self,
784        instrument_id: &InstrumentId,
785        account_id: Option<&AccountId>,
786    ) -> Option<Money> {
787        self.portfolio
788            .borrow_mut()
789            .total_pnl_for_account(instrument_id, None, account_id, None)
790    }
791
792    /// Returns the total PnLs for the given venue.
793    ///
794    /// # Panics
795    ///
796    /// Panics if the portfolio is already borrowed.
797    #[must_use]
798    pub fn total_pnls(
799        &self,
800        venue: &Venue,
801        account_id: Option<&AccountId>,
802    ) -> Option<IndexMap<Currency, Money>> {
803        self.portfolio
804            .borrow_mut()
805            .total_pnls(venue, account_id, None)
806    }
807
808    /// Returns the per-currency mark-to-market value of open positions at the given venue.
809    ///
810    /// # Panics
811    ///
812    /// Panics if the portfolio is already borrowed.
813    #[must_use]
814    pub fn mark_values(
815        &self,
816        venue: &Venue,
817        account_id: Option<&AccountId>,
818    ) -> IndexMap<Currency, Money> {
819        self.portfolio.borrow_mut().mark_values(venue, account_id)
820    }
821
822    /// Returns the per-currency total equity for the given venue.
823    ///
824    /// # Panics
825    ///
826    /// Panics if the portfolio is already borrowed.
827    #[must_use]
828    pub fn equity(
829        &self,
830        venue: &Venue,
831        account_id: Option<&AccountId>,
832    ) -> IndexMap<Currency, Money> {
833        self.portfolio.borrow_mut().equity(venue, account_id)
834    }
835
836    /// Builds a portfolio snapshot for the given account.
837    ///
838    /// # Panics
839    ///
840    /// Panics if the portfolio is already borrowed.
841    #[must_use]
842    pub fn build_snapshot(&self, account_id: &AccountId) -> Option<PortfolioSnapshot> {
843        self.portfolio.borrow_mut().build_snapshot(account_id)
844    }
845
846    /// Returns an owned snapshot of computed portfolio performance statistics.
847    ///
848    /// # Panics
849    ///
850    /// Panics if the portfolio is already mutably borrowed.
851    #[must_use]
852    pub fn statistics(&self) -> PortfolioStatistics {
853        self.portfolio.borrow().statistics()
854    }
855
856    /// Returns the recorded portfolio snapshots for the given account.
857    ///
858    /// # Panics
859    ///
860    /// Panics if the portfolio is already mutably borrowed.
861    #[must_use]
862    pub fn snapshots(&self, account_id: &AccountId) -> Vec<PortfolioSnapshot> {
863        self.portfolio.borrow().snapshots(account_id)
864    }
865
866    /// Returns the instruments currently flagged as unpriced for the given venue.
867    ///
868    /// # Panics
869    ///
870    /// Panics if the portfolio is already mutably borrowed.
871    #[must_use]
872    pub fn missing_price_instruments(&self, venue: &Venue) -> Vec<InstrumentId> {
873        self.portfolio
874            .borrow()
875            .missing_price_instruments(venue, None)
876    }
877
878    /// Returns the net exposure for the given instrument ID.
879    ///
880    /// # Panics
881    ///
882    /// Panics if the portfolio is already mutably borrowed.
883    #[must_use]
884    pub fn net_exposure(
885        &self,
886        instrument_id: &InstrumentId,
887        account_id: Option<&AccountId>,
888    ) -> Option<Money> {
889        self.portfolio
890            .borrow()
891            .net_exposure(instrument_id, None, account_id, None)
892    }
893
894    /// Returns the net position for the given instrument ID.
895    ///
896    /// # Panics
897    ///
898    /// Panics if the portfolio is already mutably borrowed.
899    #[must_use]
900    pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
901        self.portfolio.borrow().net_position(instrument_id)
902    }
903
904    /// Returns whether the net position is long for the given instrument ID.
905    ///
906    /// # Panics
907    ///
908    /// Panics if the portfolio is already mutably borrowed.
909    #[must_use]
910    pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool {
911        self.portfolio.borrow().is_net_long(instrument_id)
912    }
913
914    /// Returns whether the net position is short for the given instrument ID.
915    ///
916    /// # Panics
917    ///
918    /// Panics if the portfolio is already mutably borrowed.
919    #[must_use]
920    pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool {
921        self.portfolio.borrow().is_net_short(instrument_id)
922    }
923
924    /// Returns whether the net position is flat for the given instrument ID.
925    ///
926    /// # Panics
927    ///
928    /// Panics if the portfolio is already mutably borrowed.
929    #[must_use]
930    pub fn is_net_flat(&self, instrument_id: &InstrumentId) -> bool {
931        self.portfolio.borrow().is_net_flat(instrument_id)
932    }
933
934    /// Returns whether every net position is flat.
935    ///
936    /// # Panics
937    ///
938    /// Panics if the portfolio is already mutably borrowed.
939    #[must_use]
940    pub fn is_completely_net_flat(&self) -> bool {
941        self.portfolio.borrow().is_completely_net_flat()
942    }
943
944    /// Returns realized PnLs recorded during portfolio event processing.
945    ///
946    /// Each record is `(position_id, ts_event, realized_pnl)`.
947    ///
948    /// # Panics
949    ///
950    /// Panics if the portfolio is already mutably borrowed.
951    #[must_use]
952    pub fn recorded_realized_pnls(&self) -> AHashMap<Currency, Vec<(PositionId, UnixNanos, f64)>> {
953        self.portfolio.borrow().recorded_realized_pnls()
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use std::{cell::RefCell, rc::Rc};
960
961    use nautilus_common::{cache::Cache, clock::TestClock, factories::OrderFactory};
962    use nautilus_model::{
963        enums::{OrderSide, OrderType},
964        identifiers::{AccountId, InstrumentId, StrategyId, TraderId, Venue},
965        orders::Order,
966    };
967    use rstest::rstest;
968
969    use super::*;
970
971    #[rstest]
972    fn test_order_api_creates_market_order() {
973        let trader_id = TraderId::from("TRADER-001");
974        let strategy_id = StrategyId::from("S-001");
975        let clock = Rc::new(RefCell::new(TestClock::new()));
976        let order_factory = RefCell::new(OrderFactory::new(
977            trader_id,
978            strategy_id,
979            None,
980            None,
981            clock,
982            false,
983            true,
984        ));
985        let api = OrderApi::new(&order_factory);
986        let instrument_id = InstrumentId::from("AUD/USD.SIM");
987
988        let order = api.market(
989            instrument_id,
990            OrderSide::Buy,
991            Quantity::from("100000"),
992            None,
993            None,
994            None,
995            None,
996            None,
997            None,
998            None,
999        );
1000
1001        assert_eq!(order.order_type(), OrderType::Market);
1002        assert_eq!(order.instrument_id(), instrument_id);
1003        assert_eq!(order.order_side(), OrderSide::Buy);
1004        assert_eq!(order.quantity(), Quantity::from("100000"));
1005        assert_eq!(order.trader_id(), trader_id);
1006        assert_eq!(order.strategy_id(), strategy_id);
1007    }
1008
1009    #[rstest]
1010    fn test_order_api_creates_bracket_orders() {
1011        let trader_id = TraderId::from("TRADER-001");
1012        let strategy_id = StrategyId::from("S-001");
1013        let clock = Rc::new(RefCell::new(TestClock::new()));
1014        let order_factory = RefCell::new(OrderFactory::new(
1015            trader_id,
1016            strategy_id,
1017            None,
1018            None,
1019            clock,
1020            false,
1021            true,
1022        ));
1023        let api = OrderApi::new(&order_factory);
1024        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1025
1026        let orders = api
1027            .bracket()
1028            .instrument_id(instrument_id)
1029            .order_side(OrderSide::Buy)
1030            .quantity(Quantity::from("100000"))
1031            .tp_price(Price::from("1.10000"))
1032            .sl_trigger_price(Price::from("0.90000"))
1033            .call();
1034
1035        assert_eq!(orders.len(), 3);
1036        assert!(
1037            orders
1038                .iter()
1039                .all(|order| order.instrument_id() == instrument_id)
1040        );
1041        assert!(orders.iter().all(|order| order.trader_id() == trader_id));
1042        assert!(
1043            orders
1044                .iter()
1045                .all(|order| order.strategy_id() == strategy_id)
1046        );
1047        assert_eq!(orders[0].order_type(), OrderType::Market);
1048        assert_eq!(orders[0].order_side(), OrderSide::Buy);
1049        assert_eq!(orders[0].quantity(), Quantity::from("100000"));
1050        assert_eq!(orders[1].order_type(), OrderType::StopMarket);
1051        assert_eq!(orders[1].order_side(), OrderSide::Sell);
1052        assert_eq!(orders[1].trigger_price(), Some(Price::from("0.90000")));
1053        assert_eq!(orders[2].order_type(), OrderType::Limit);
1054        assert_eq!(orders[2].order_side(), OrderSide::Sell);
1055        assert_eq!(orders[2].price(), Some(Price::from("1.10000")));
1056    }
1057
1058    #[rstest]
1059    fn test_portfolio_api_empty_reads_return_empty_values() {
1060        let cache = Rc::new(RefCell::new(Cache::default()));
1061        let clock = Rc::new(RefCell::new(TestClock::new()));
1062        let portfolio = RefCell::new(Portfolio::new(clock, cache, None));
1063        let api = PortfolioApi::new(&portfolio);
1064        let venue = Venue::from("SIM");
1065        let account_id = AccountId::from("SIM-001");
1066        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1067
1068        assert!(!api.is_initialized());
1069        assert!(api.balances_locked(&venue).is_empty());
1070        assert!(api.instrument_initial_margins(&venue).is_empty());
1071        assert!(api.instrument_maintenance_margins(&venue).is_empty());
1072        assert_eq!(api.unrealized_pnls(&venue, None), Some(IndexMap::new()));
1073        assert_eq!(api.realized_pnls(&venue, None), Some(IndexMap::new()));
1074        assert_eq!(api.net_exposures(&venue, None), None);
1075        assert_eq!(api.unrealized_pnl(&instrument_id), None);
1076        assert_eq!(
1077            api.unrealized_pnl_for_account(&instrument_id, Some(&account_id)),
1078            None
1079        );
1080        assert_eq!(api.realized_pnl(&instrument_id), None);
1081        assert_eq!(
1082            api.realized_pnl_for_account(&instrument_id, Some(&account_id)),
1083            None
1084        );
1085        assert_eq!(api.total_pnl(&instrument_id), None);
1086        assert_eq!(
1087            api.total_pnl_for_account(&instrument_id, Some(&account_id)),
1088            None
1089        );
1090        assert_eq!(api.total_pnls(&venue, None), Some(IndexMap::new()));
1091        assert!(api.mark_values(&venue, None).is_empty());
1092        assert!(api.equity(&venue, None).is_empty());
1093        assert_eq!(api.build_snapshot(&account_id), None);
1094        assert!(api.snapshots(&account_id).is_empty());
1095        assert!(api.missing_price_instruments(&venue).is_empty());
1096        assert_eq!(api.net_exposure(&instrument_id, None), None);
1097        assert_eq!(api.net_position(&instrument_id), Decimal::ZERO);
1098        assert!(!api.is_net_long(&instrument_id));
1099        assert!(!api.is_net_short(&instrument_id));
1100        assert!(api.is_net_flat(&instrument_id));
1101        assert!(api.is_completely_net_flat());
1102        assert!(api.recorded_realized_pnls().is_empty());
1103
1104        let _statistics = api.statistics();
1105    }
1106}