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            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 margins_init(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
648        self.portfolio.borrow().margins_init(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 margins_maint(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
658        self.portfolio.borrow().margins_maint(venue)
659    }
660
661    /// Returns the unrealized PnLs for all positions at the given venue.
662    ///
663    /// # Panics
664    ///
665    /// Panics if the portfolio is already borrowed.
666    #[must_use]
667    pub fn unrealized_pnls(
668        &self,
669        venue: &Venue,
670        account_id: Option<&AccountId>,
671    ) -> IndexMap<Currency, Money> {
672        self.portfolio
673            .borrow_mut()
674            .unrealized_pnls(venue, account_id)
675    }
676
677    /// Returns the realized PnLs for all positions at the given venue.
678    ///
679    /// # Panics
680    ///
681    /// Panics if the portfolio is already borrowed.
682    #[must_use]
683    pub fn realized_pnls(
684        &self,
685        venue: &Venue,
686        account_id: Option<&AccountId>,
687    ) -> IndexMap<Currency, Money> {
688        self.portfolio.borrow_mut().realized_pnls(venue, account_id)
689    }
690
691    /// Returns net exposures by currency for the given venue.
692    ///
693    /// # Panics
694    ///
695    /// Panics if the portfolio is already mutably borrowed.
696    #[must_use]
697    pub fn net_exposures(
698        &self,
699        venue: &Venue,
700        account_id: Option<&AccountId>,
701    ) -> Option<IndexMap<Currency, Money>> {
702        self.portfolio.borrow().net_exposures(venue, account_id)
703    }
704
705    /// Returns the unrealized PnL for the given instrument ID.
706    ///
707    /// # Panics
708    ///
709    /// Panics if the portfolio is already borrowed.
710    #[must_use]
711    pub fn unrealized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
712        self.portfolio.borrow_mut().unrealized_pnl(instrument_id)
713    }
714
715    /// Returns the unrealized PnL for the given instrument ID and account filter.
716    ///
717    /// # Panics
718    ///
719    /// Panics if the portfolio is already borrowed.
720    #[must_use]
721    pub fn unrealized_pnl_for_account(
722        &self,
723        instrument_id: &InstrumentId,
724        account_id: Option<&AccountId>,
725    ) -> Option<Money> {
726        self.portfolio
727            .borrow_mut()
728            .unrealized_pnl_for_account(instrument_id, account_id)
729    }
730
731    /// Returns the realized PnL for the given instrument ID.
732    ///
733    /// # Panics
734    ///
735    /// Panics if the portfolio is already borrowed.
736    #[must_use]
737    pub fn realized_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
738        self.portfolio.borrow_mut().realized_pnl(instrument_id)
739    }
740
741    /// Returns the realized PnL for the given instrument ID and account filter.
742    ///
743    /// # Panics
744    ///
745    /// Panics if the portfolio is already borrowed.
746    #[must_use]
747    pub fn realized_pnl_for_account(
748        &self,
749        instrument_id: &InstrumentId,
750        account_id: Option<&AccountId>,
751    ) -> Option<Money> {
752        self.portfolio
753            .borrow_mut()
754            .realized_pnl_for_account(instrument_id, account_id)
755    }
756
757    /// Returns the total PnL for the given instrument ID.
758    ///
759    /// # Panics
760    ///
761    /// Panics if the portfolio is already borrowed.
762    #[must_use]
763    pub fn total_pnl(&self, instrument_id: &InstrumentId) -> Option<Money> {
764        self.portfolio.borrow_mut().total_pnl(instrument_id)
765    }
766
767    /// Returns the total PnL for the given instrument ID and account filter.
768    ///
769    /// # Panics
770    ///
771    /// Panics if the portfolio is already borrowed.
772    #[must_use]
773    pub fn total_pnl_for_account(
774        &self,
775        instrument_id: &InstrumentId,
776        account_id: Option<&AccountId>,
777    ) -> Option<Money> {
778        self.portfolio
779            .borrow_mut()
780            .total_pnl_for_account(instrument_id, account_id)
781    }
782
783    /// Returns the total PnLs for the given venue.
784    ///
785    /// # Panics
786    ///
787    /// Panics if the portfolio is already borrowed.
788    #[must_use]
789    pub fn total_pnls(
790        &self,
791        venue: &Venue,
792        account_id: Option<&AccountId>,
793    ) -> IndexMap<Currency, Money> {
794        self.portfolio.borrow_mut().total_pnls(venue, account_id)
795    }
796
797    /// Returns the per-currency mark-to-market value of open positions at the given venue.
798    ///
799    /// # Panics
800    ///
801    /// Panics if the portfolio is already borrowed.
802    #[must_use]
803    pub fn mark_values(
804        &self,
805        venue: &Venue,
806        account_id: Option<&AccountId>,
807    ) -> IndexMap<Currency, Money> {
808        self.portfolio.borrow_mut().mark_values(venue, account_id)
809    }
810
811    /// Returns the per-currency total equity for the given venue.
812    ///
813    /// # Panics
814    ///
815    /// Panics if the portfolio is already borrowed.
816    #[must_use]
817    pub fn equity(
818        &self,
819        venue: &Venue,
820        account_id: Option<&AccountId>,
821    ) -> IndexMap<Currency, Money> {
822        self.portfolio.borrow_mut().equity(venue, account_id)
823    }
824
825    /// Builds a portfolio snapshot for the given account.
826    ///
827    /// # Panics
828    ///
829    /// Panics if the portfolio is already borrowed.
830    #[must_use]
831    pub fn build_snapshot(&self, account_id: &AccountId) -> Option<PortfolioSnapshot> {
832        self.portfolio.borrow_mut().build_snapshot(account_id)
833    }
834
835    /// Returns an owned snapshot of computed portfolio performance statistics.
836    ///
837    /// # Panics
838    ///
839    /// Panics if the portfolio is already mutably borrowed.
840    #[must_use]
841    pub fn statistics(&self) -> PortfolioStatistics {
842        self.portfolio.borrow().statistics()
843    }
844
845    /// Returns the recorded portfolio snapshots for the given account.
846    ///
847    /// # Panics
848    ///
849    /// Panics if the portfolio is already mutably borrowed.
850    #[must_use]
851    pub fn snapshots(&self, account_id: &AccountId) -> Vec<PortfolioSnapshot> {
852        self.portfolio.borrow().snapshots(account_id)
853    }
854
855    /// Returns the instruments currently flagged as unpriced for the given venue.
856    ///
857    /// # Panics
858    ///
859    /// Panics if the portfolio is already mutably borrowed.
860    #[must_use]
861    pub fn missing_price_instruments(&self, venue: &Venue) -> Vec<InstrumentId> {
862        self.portfolio.borrow().missing_price_instruments(venue)
863    }
864
865    /// Returns the net exposure for the given instrument ID.
866    ///
867    /// # Panics
868    ///
869    /// Panics if the portfolio is already mutably borrowed.
870    #[must_use]
871    pub fn net_exposure(
872        &self,
873        instrument_id: &InstrumentId,
874        account_id: Option<&AccountId>,
875    ) -> Option<Money> {
876        self.portfolio
877            .borrow()
878            .net_exposure(instrument_id, account_id)
879    }
880
881    /// Returns the net position for the given instrument ID.
882    ///
883    /// # Panics
884    ///
885    /// Panics if the portfolio is already mutably borrowed.
886    #[must_use]
887    pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
888        self.portfolio.borrow().net_position(instrument_id)
889    }
890
891    /// Returns whether the net position is long for the given instrument ID.
892    ///
893    /// # Panics
894    ///
895    /// Panics if the portfolio is already mutably borrowed.
896    #[must_use]
897    pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool {
898        self.portfolio.borrow().is_net_long(instrument_id)
899    }
900
901    /// Returns whether the net position is short for the given instrument ID.
902    ///
903    /// # Panics
904    ///
905    /// Panics if the portfolio is already mutably borrowed.
906    #[must_use]
907    pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool {
908        self.portfolio.borrow().is_net_short(instrument_id)
909    }
910
911    /// Returns whether the net position is flat for the given instrument ID.
912    ///
913    /// # Panics
914    ///
915    /// Panics if the portfolio is already mutably borrowed.
916    #[must_use]
917    pub fn is_flat(&self, instrument_id: &InstrumentId) -> bool {
918        self.portfolio.borrow().is_flat(instrument_id)
919    }
920
921    /// Returns whether every net position is flat.
922    ///
923    /// # Panics
924    ///
925    /// Panics if the portfolio is already mutably borrowed.
926    #[must_use]
927    pub fn is_completely_flat(&self) -> bool {
928        self.portfolio.borrow().is_completely_flat()
929    }
930
931    /// Returns realized PnLs recorded during portfolio event processing.
932    ///
933    /// Each record is `(position_id, ts_event, realized_pnl)`.
934    ///
935    /// # Panics
936    ///
937    /// Panics if the portfolio is already mutably borrowed.
938    #[must_use]
939    pub fn recorded_realized_pnls(&self) -> AHashMap<Currency, Vec<(PositionId, UnixNanos, f64)>> {
940        self.portfolio.borrow().recorded_realized_pnls()
941    }
942}
943
944#[cfg(test)]
945mod tests {
946    use std::{cell::RefCell, rc::Rc};
947
948    use nautilus_common::{cache::Cache, clock::TestClock, factories::OrderFactory};
949    use nautilus_model::{
950        enums::{OrderSide, OrderType},
951        identifiers::{AccountId, InstrumentId, StrategyId, TraderId, Venue},
952        orders::Order,
953    };
954    use rstest::rstest;
955
956    use super::*;
957
958    #[rstest]
959    fn test_order_api_creates_market_order() {
960        let trader_id = TraderId::from("TRADER-001");
961        let strategy_id = StrategyId::from("S-001");
962        let clock = Rc::new(RefCell::new(TestClock::new()));
963        let order_factory = RefCell::new(OrderFactory::new(
964            trader_id,
965            strategy_id,
966            None,
967            None,
968            clock,
969            false,
970            true,
971        ));
972        let api = OrderApi::new(&order_factory);
973        let instrument_id = InstrumentId::from("AUD/USD.SIM");
974
975        let order = api.market(
976            instrument_id,
977            OrderSide::Buy,
978            Quantity::from("100000"),
979            None,
980            None,
981            None,
982            None,
983            None,
984            None,
985            None,
986        );
987
988        assert_eq!(order.order_type(), OrderType::Market);
989        assert_eq!(order.instrument_id(), instrument_id);
990        assert_eq!(order.order_side(), OrderSide::Buy);
991        assert_eq!(order.quantity(), Quantity::from("100000"));
992        assert_eq!(order.trader_id(), trader_id);
993        assert_eq!(order.strategy_id(), strategy_id);
994    }
995
996    #[rstest]
997    fn test_order_api_creates_bracket_orders() {
998        let trader_id = TraderId::from("TRADER-001");
999        let strategy_id = StrategyId::from("S-001");
1000        let clock = Rc::new(RefCell::new(TestClock::new()));
1001        let order_factory = RefCell::new(OrderFactory::new(
1002            trader_id,
1003            strategy_id,
1004            None,
1005            None,
1006            clock,
1007            false,
1008            true,
1009        ));
1010        let api = OrderApi::new(&order_factory);
1011        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1012
1013        let orders = api
1014            .bracket()
1015            .instrument_id(instrument_id)
1016            .order_side(OrderSide::Buy)
1017            .quantity(Quantity::from("100000"))
1018            .tp_price(Price::from("1.10000"))
1019            .sl_trigger_price(Price::from("0.90000"))
1020            .call();
1021
1022        assert_eq!(orders.len(), 3);
1023        assert!(
1024            orders
1025                .iter()
1026                .all(|order| order.instrument_id() == instrument_id)
1027        );
1028        assert!(orders.iter().all(|order| order.trader_id() == trader_id));
1029        assert!(
1030            orders
1031                .iter()
1032                .all(|order| order.strategy_id() == strategy_id)
1033        );
1034        assert_eq!(orders[0].order_type(), OrderType::Market);
1035        assert_eq!(orders[0].order_side(), OrderSide::Buy);
1036        assert_eq!(orders[0].quantity(), Quantity::from("100000"));
1037        assert_eq!(orders[1].order_type(), OrderType::StopMarket);
1038        assert_eq!(orders[1].order_side(), OrderSide::Sell);
1039        assert_eq!(orders[1].trigger_price(), Some(Price::from("0.90000")));
1040        assert_eq!(orders[2].order_type(), OrderType::Limit);
1041        assert_eq!(orders[2].order_side(), OrderSide::Sell);
1042        assert_eq!(orders[2].price(), Some(Price::from("1.10000")));
1043    }
1044
1045    #[rstest]
1046    fn test_portfolio_api_empty_reads_return_empty_values() {
1047        let cache = Rc::new(RefCell::new(Cache::default()));
1048        let clock = Rc::new(RefCell::new(TestClock::new()));
1049        let portfolio = RefCell::new(Portfolio::new(clock, cache, None));
1050        let api = PortfolioApi::new(&portfolio);
1051        let venue = Venue::from("SIM");
1052        let account_id = AccountId::from("SIM-001");
1053        let instrument_id = InstrumentId::from("AUD/USD.SIM");
1054
1055        assert!(!api.is_initialized());
1056        assert!(api.balances_locked(&venue).is_empty());
1057        assert!(api.margins_init(&venue).is_empty());
1058        assert!(api.margins_maint(&venue).is_empty());
1059        assert!(api.unrealized_pnls(&venue, None).is_empty());
1060        assert!(api.realized_pnls(&venue, None).is_empty());
1061        assert_eq!(api.net_exposures(&venue, None), None);
1062        assert_eq!(api.unrealized_pnl(&instrument_id), None);
1063        assert_eq!(
1064            api.unrealized_pnl_for_account(&instrument_id, Some(&account_id)),
1065            None
1066        );
1067        assert_eq!(api.realized_pnl(&instrument_id), None);
1068        assert_eq!(
1069            api.realized_pnl_for_account(&instrument_id, Some(&account_id)),
1070            None
1071        );
1072        assert_eq!(api.total_pnl(&instrument_id), None);
1073        assert_eq!(
1074            api.total_pnl_for_account(&instrument_id, Some(&account_id)),
1075            None
1076        );
1077        assert!(api.total_pnls(&venue, None).is_empty());
1078        assert!(api.mark_values(&venue, None).is_empty());
1079        assert!(api.equity(&venue, None).is_empty());
1080        assert_eq!(api.build_snapshot(&account_id), None);
1081        assert!(api.snapshots(&account_id).is_empty());
1082        assert!(api.missing_price_instruments(&venue).is_empty());
1083        assert_eq!(api.net_exposure(&instrument_id, None), None);
1084        assert_eq!(api.net_position(&instrument_id), Decimal::ZERO);
1085        assert!(!api.is_net_long(&instrument_id));
1086        assert!(!api.is_net_short(&instrument_id));
1087        assert!(api.is_flat(&instrument_id));
1088        assert!(api.is_completely_flat());
1089        assert!(api.recorded_realized_pnls().is_empty());
1090
1091        let _statistics = api.statistics();
1092    }
1093}