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 `account_id`, or for the account issued under `venue`
632    /// when no account ID is given.
633    ///
634    /// # Panics
635    ///
636    /// Panics if the portfolio is already mutably borrowed.
637    #[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    /// Returns the initial margin requirements for the `account_id`, or for the account issued
647    /// under `venue` when no account ID is given.
648    ///
649    /// # Panics
650    ///
651    /// Panics if the portfolio is already mutably borrowed.
652    #[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    /// Returns the maintenance margin requirements for the `account_id`, or for the account
664    /// issued under `venue` when no account ID is given.
665    ///
666    /// # Panics
667    ///
668    /// Panics if the portfolio is already mutably borrowed.
669    #[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    /// Returns the unrealized PnLs for all positions at the given venue.
681    ///
682    /// # Panics
683    ///
684    /// Panics if the portfolio is already borrowed.
685    #[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    /// Returns the realized PnLs for all positions at the given venue.
697    ///
698    /// # Panics
699    ///
700    /// Panics if the portfolio is already borrowed.
701    #[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    /// Returns net exposures by currency for the given venue.
713    ///
714    /// # Panics
715    ///
716    /// Panics if the portfolio is already mutably borrowed.
717    #[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    /// Returns the unrealized PnL for the given instrument ID.
729    ///
730    /// # Panics
731    ///
732    /// Panics if the portfolio is already borrowed.
733    #[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    /// Returns the unrealized PnL for the given instrument ID and account filter.
739    ///
740    /// # Panics
741    ///
742    /// Panics if the portfolio is already borrowed.
743    #[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    /// Returns the realized PnL for the given instrument ID.
758    ///
759    /// # Panics
760    ///
761    /// Panics if the portfolio is already borrowed.
762    #[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    /// Returns the realized 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 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    /// Returns the total PnL for the given instrument ID.
784    ///
785    /// # Panics
786    ///
787    /// Panics if the portfolio is already borrowed.
788    #[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    /// Returns the total PnL for the given instrument ID and account filter.
794    ///
795    /// # Panics
796    ///
797    /// Panics if the portfolio is already borrowed.
798    #[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    /// Returns the total PnLs for the given venue.
810    ///
811    /// # Panics
812    ///
813    /// Panics if the portfolio is already borrowed.
814    #[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    /// Returns the per-currency mark-to-market value of open positions at the given venue.
826    ///
827    /// # Panics
828    ///
829    /// Panics if the portfolio is already borrowed.
830    #[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    /// Returns the per-currency total equity for the given venue.
840    ///
841    /// # Panics
842    ///
843    /// Panics if the portfolio is already borrowed.
844    #[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    /// Builds a portfolio snapshot for the given account.
854    ///
855    /// # Panics
856    ///
857    /// Panics if the portfolio is already borrowed.
858    #[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    /// Returns an owned snapshot of computed portfolio performance statistics.
864    ///
865    /// # Panics
866    ///
867    /// Panics if the portfolio is already mutably borrowed.
868    #[must_use]
869    pub fn statistics(&self) -> PortfolioStatistics {
870        self.portfolio.borrow().statistics()
871    }
872
873    /// Returns the recorded portfolio snapshots for the given account.
874    ///
875    /// # Panics
876    ///
877    /// Panics if the portfolio is already mutably borrowed.
878    #[must_use]
879    pub fn snapshots(&self, account_id: &AccountId) -> Vec<PortfolioSnapshot> {
880        self.portfolio.borrow().snapshots(account_id)
881    }
882
883    /// Returns the instruments currently flagged as unpriced for the given venue.
884    ///
885    /// # Panics
886    ///
887    /// Panics if the portfolio is already mutably borrowed.
888    #[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    /// Returns the net exposure for the given instrument ID.
896    ///
897    /// # Panics
898    ///
899    /// Panics if the portfolio is already mutably borrowed.
900    #[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    /// Returns the net position for the given instrument ID.
912    ///
913    /// # Panics
914    ///
915    /// Panics if the portfolio is already mutably borrowed.
916    #[must_use]
917    pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
918        self.portfolio.borrow().net_position(instrument_id)
919    }
920
921    /// Returns whether the net position is long for the given instrument ID.
922    ///
923    /// # Panics
924    ///
925    /// Panics if the portfolio is already mutably borrowed.
926    #[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    /// Returns whether the net position is short for the given instrument ID.
932    ///
933    /// # Panics
934    ///
935    /// Panics if the portfolio is already mutably borrowed.
936    #[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    /// Returns whether the net position is flat for the given instrument ID.
942    ///
943    /// # Panics
944    ///
945    /// Panics if the portfolio is already mutably borrowed.
946    #[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    /// Returns whether every net position is flat.
952    ///
953    /// # Panics
954    ///
955    /// Panics if the portfolio is already mutably borrowed.
956    #[must_use]
957    pub fn is_completely_net_flat(&self) -> bool {
958        self.portfolio.borrow().is_completely_net_flat()
959    }
960
961    /// Returns realized PnLs recorded during portfolio event processing.
962    ///
963    /// Each record is `(position_id, ts_event, realized_pnl)`.
964    ///
965    /// # Panics
966    ///
967    /// Panics if the portfolio is already mutably borrowed.
968    #[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}