nautilus_portfolio/
portfolio.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Provides a generic `Portfolio` for all environments.
17use std::{
18    any::Any,
19    cell::RefCell,
20    collections::{HashMap, HashSet},
21    rc::Rc,
22    sync::Arc,
23};
24
25use nautilus_analysis::{
26    analyzer::PortfolioAnalyzer,
27    statistics::{
28        expectancy::Expectancy, long_ratio::LongRatio, loser_max::MaxLoser, loser_min::MinLoser,
29        profit_factor::ProfitFactor, returns_avg::ReturnsAverage,
30        returns_avg_loss::ReturnsAverageLoss, returns_avg_win::ReturnsAverageWin,
31        returns_volatility::ReturnsVolatility, risk_return_ratio::RiskReturnRatio,
32        sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio, win_rate::WinRate,
33        winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner,
34    },
35};
36use nautilus_common::{
37    cache::Cache,
38    clock::Clock,
39    messages::data::DataResponse,
40    msgbus::{
41        handler::{MessageHandler, ShareableMessageHandler},
42        MessageBus,
43    },
44};
45use nautilus_model::{
46    accounts::AccountAny,
47    data::{Bar, Data, QuoteTick},
48    enums::{OrderSide, OrderType, PositionSide, PriceType},
49    events::{position::PositionEvent, AccountState, OrderEventAny},
50    identifiers::{InstrumentId, Venue},
51    instruments::InstrumentAny,
52    orders::OrderAny,
53    position::Position,
54    types::{Currency, Money, Price},
55};
56use rust_decimal::{
57    prelude::{FromPrimitive, ToPrimitive},
58    Decimal,
59};
60use ustr::Ustr;
61use uuid::Uuid;
62
63use crate::manager::AccountsManager;
64
65struct UpdateQuoteTickHandler {
66    id: Ustr,
67    callback: Box<dyn Fn(&QuoteTick)>,
68}
69
70impl MessageHandler for UpdateQuoteTickHandler {
71    fn id(&self) -> Ustr {
72        self.id
73    }
74
75    fn handle(&self, msg: &dyn Any) {
76        (self.callback)(msg.downcast_ref::<&QuoteTick>().unwrap());
77    }
78    fn handle_response(&self, _resp: DataResponse) {}
79    fn handle_data(&self, _data: Data) {}
80    fn as_any(&self) -> &dyn Any {
81        self
82    }
83}
84
85struct UpdateBarHandler {
86    id: Ustr,
87    callback: Box<dyn Fn(&Bar)>,
88}
89
90impl MessageHandler for UpdateBarHandler {
91    fn id(&self) -> Ustr {
92        self.id
93    }
94
95    fn handle(&self, msg: &dyn Any) {
96        (self.callback)(msg.downcast_ref::<&Bar>().unwrap());
97    }
98    fn handle_response(&self, _resp: DataResponse) {}
99    fn handle_data(&self, _data: Data) {}
100    fn as_any(&self) -> &dyn Any {
101        self
102    }
103}
104
105struct UpdateOrderHandler {
106    id: Ustr,
107    callback: Box<dyn Fn(&OrderEventAny)>,
108}
109
110impl MessageHandler for UpdateOrderHandler {
111    fn id(&self) -> Ustr {
112        self.id
113    }
114
115    fn handle(&self, msg: &dyn Any) {
116        (self.callback)(msg.downcast_ref::<&OrderEventAny>().unwrap());
117    }
118    fn handle_response(&self, _resp: DataResponse) {}
119    fn handle_data(&self, _data: Data) {}
120    fn as_any(&self) -> &dyn Any {
121        self
122    }
123}
124
125struct UpdatePositionHandler {
126    id: Ustr,
127    callback: Box<dyn Fn(&PositionEvent)>,
128}
129
130impl MessageHandler for UpdatePositionHandler {
131    fn id(&self) -> Ustr {
132        self.id
133    }
134
135    fn handle(&self, msg: &dyn Any) {
136        (self.callback)(msg.downcast_ref::<&PositionEvent>().unwrap());
137    }
138    fn handle_response(&self, _resp: DataResponse) {}
139    fn handle_data(&self, _data: Data) {}
140    fn as_any(&self) -> &dyn Any {
141        self
142    }
143}
144
145struct UpdateAccountHandler {
146    id: Ustr,
147    callback: Box<dyn Fn(&AccountState)>,
148}
149
150impl MessageHandler for UpdateAccountHandler {
151    fn id(&self) -> Ustr {
152        self.id
153    }
154
155    fn handle(&self, msg: &dyn Any) {
156        (self.callback)(msg.downcast_ref::<&AccountState>().unwrap());
157    }
158    fn handle_response(&self, _resp: DataResponse) {}
159    fn handle_data(&self, _data: Data) {}
160    fn as_any(&self) -> &dyn Any {
161        self
162    }
163}
164
165struct PortfolioState {
166    accounts: AccountsManager,
167    analyzer: PortfolioAnalyzer,
168    unrealized_pnls: HashMap<InstrumentId, Money>,
169    realized_pnls: HashMap<InstrumentId, Money>,
170    net_positions: HashMap<InstrumentId, Decimal>,
171    pending_calcs: HashSet<InstrumentId>,
172    bar_close_prices: HashMap<InstrumentId, Price>,
173    initialized: bool,
174}
175
176impl PortfolioState {
177    fn new(clock: Rc<RefCell<dyn Clock>>, cache: Rc<RefCell<Cache>>) -> Self {
178        let mut analyzer = PortfolioAnalyzer::new();
179        analyzer.register_statistic(Arc::new(MaxWinner {}));
180        analyzer.register_statistic(Arc::new(AvgWinner {}));
181        analyzer.register_statistic(Arc::new(MinWinner {}));
182        analyzer.register_statistic(Arc::new(MinLoser {}));
183        analyzer.register_statistic(Arc::new(MaxLoser {}));
184        analyzer.register_statistic(Arc::new(Expectancy {}));
185        analyzer.register_statistic(Arc::new(WinRate {}));
186        analyzer.register_statistic(Arc::new(ReturnsVolatility::new(None)));
187        analyzer.register_statistic(Arc::new(ReturnsAverage {}));
188        analyzer.register_statistic(Arc::new(ReturnsAverageLoss {}));
189        analyzer.register_statistic(Arc::new(ReturnsAverageWin {}));
190        analyzer.register_statistic(Arc::new(SharpeRatio::new(None)));
191        analyzer.register_statistic(Arc::new(SortinoRatio::new(None)));
192        analyzer.register_statistic(Arc::new(ProfitFactor {}));
193        analyzer.register_statistic(Arc::new(RiskReturnRatio {}));
194        analyzer.register_statistic(Arc::new(LongRatio::new(None)));
195
196        Self {
197            accounts: AccountsManager::new(clock, cache),
198            analyzer,
199            unrealized_pnls: HashMap::new(),
200            realized_pnls: HashMap::new(),
201            net_positions: HashMap::new(),
202            pending_calcs: HashSet::new(),
203            bar_close_prices: HashMap::new(),
204            initialized: false,
205        }
206    }
207
208    fn reset(&mut self) {
209        log::debug!("RESETTING");
210        self.net_positions.clear();
211        self.unrealized_pnls.clear();
212        self.realized_pnls.clear();
213        self.pending_calcs.clear();
214        self.analyzer.reset();
215        log::debug!("READY");
216    }
217}
218
219pub struct Portfolio {
220    clock: Rc<RefCell<dyn Clock>>,
221    cache: Rc<RefCell<Cache>>,
222    msgbus: Rc<RefCell<MessageBus>>,
223    inner: Rc<RefCell<PortfolioState>>,
224}
225
226impl Portfolio {
227    pub fn new(
228        msgbus: Rc<RefCell<MessageBus>>,
229        cache: Rc<RefCell<Cache>>,
230        clock: Rc<RefCell<dyn Clock>>,
231        portfolio_bar_updates: bool,
232    ) -> Self {
233        let inner = Rc::new(RefCell::new(PortfolioState::new(
234            clock.clone(),
235            cache.clone(),
236        )));
237
238        Self::register_message_handlers(
239            msgbus.clone(),
240            cache.clone(),
241            clock.clone(),
242            inner.clone(),
243            portfolio_bar_updates,
244        );
245
246        Self {
247            clock,
248            cache,
249            msgbus,
250            inner,
251        }
252    }
253
254    fn register_message_handlers(
255        msgbus: Rc<RefCell<MessageBus>>,
256        cache: Rc<RefCell<Cache>>,
257        clock: Rc<RefCell<dyn Clock>>,
258        inner: Rc<RefCell<PortfolioState>>,
259        portfolio_bar_updates: bool,
260    ) {
261        let update_account_handler = {
262            let cache = cache.clone();
263            ShareableMessageHandler(Rc::new(UpdateAccountHandler {
264                id: Ustr::from(&Uuid::new_v4().to_string()),
265                callback: Box::new(move |event: &AccountState| {
266                    update_account(cache.clone(), event);
267                }),
268            }))
269        };
270
271        let update_position_handler = {
272            let cache = cache.clone();
273            let msgbus = msgbus.clone();
274            let clock = clock.clone();
275            let inner = inner.clone();
276            ShareableMessageHandler(Rc::new(UpdatePositionHandler {
277                id: Ustr::from(&Uuid::new_v4().to_string()),
278                callback: Box::new(move |event: &PositionEvent| {
279                    update_position(
280                        cache.clone(),
281                        msgbus.clone(),
282                        clock.clone(),
283                        inner.clone(),
284                        event,
285                    );
286                }),
287            }))
288        };
289
290        let update_quote_handler = {
291            let cache = cache.clone();
292            let msgbus = msgbus.clone();
293            let clock = clock.clone();
294            let inner = inner.clone();
295            ShareableMessageHandler(Rc::new(UpdateQuoteTickHandler {
296                id: Ustr::from(&Uuid::new_v4().to_string()),
297                callback: Box::new(move |quote: &QuoteTick| {
298                    update_quote_tick(
299                        cache.clone(),
300                        msgbus.clone(),
301                        clock.clone(),
302                        inner.clone(),
303                        quote,
304                    );
305                }),
306            }))
307        };
308
309        let update_bar_handler = {
310            let cache = cache.clone();
311            let msgbus = msgbus.clone();
312            let clock = clock.clone();
313            let inner = inner.clone();
314            ShareableMessageHandler(Rc::new(UpdateBarHandler {
315                id: Ustr::from(&Uuid::new_v4().to_string()),
316                callback: Box::new(move |bar: &Bar| {
317                    update_bar(
318                        cache.clone(),
319                        msgbus.clone(),
320                        clock.clone(),
321                        inner.clone(),
322                        bar,
323                    );
324                }),
325            }))
326        };
327
328        let update_order_handler = {
329            let cache = cache;
330            let msgbus = msgbus.clone();
331            let clock = clock.clone();
332            let inner = inner;
333            ShareableMessageHandler(Rc::new(UpdateOrderHandler {
334                id: Ustr::from(&Uuid::new_v4().to_string()),
335                callback: Box::new(move |event: &OrderEventAny| {
336                    update_order(
337                        cache.clone(),
338                        msgbus.clone(),
339                        clock.clone(),
340                        inner.clone(),
341                        event,
342                    );
343                }),
344            }))
345        };
346
347        let mut borrowed_msgbus = msgbus.borrow_mut();
348        borrowed_msgbus.register("Portfolio.update_account", update_account_handler.clone());
349
350        borrowed_msgbus.subscribe("data.quotes.*", update_quote_handler, Some(10));
351        if portfolio_bar_updates {
352            borrowed_msgbus.subscribe("data.quotes.*EXTERNAL", update_bar_handler, Some(10));
353        }
354        borrowed_msgbus.subscribe("events.order.*", update_order_handler, Some(10));
355        borrowed_msgbus.subscribe("events.position.*", update_position_handler, Some(10));
356        borrowed_msgbus.subscribe("events.account.*", update_account_handler, Some(10));
357    }
358
359    pub fn reset(&mut self) {
360        log::debug!("RESETTING");
361        self.inner.borrow_mut().reset();
362        log::debug!("READY");
363    }
364
365    // -- QUERIES ---------------------------------------------------------------------------------
366
367    #[must_use]
368    pub fn is_initialized(&self) -> bool {
369        self.inner.borrow().initialized
370    }
371
372    #[must_use]
373    pub fn balances_locked(&self, venue: &Venue) -> HashMap<Currency, Money> {
374        self.cache.borrow().account_for_venue(venue).map_or_else(
375            || {
376                log::error!(
377                    "Cannot get balances locked: no account generated for {}",
378                    venue
379                );
380                HashMap::new()
381            },
382            AccountAny::balances_locked,
383        )
384    }
385
386    #[must_use]
387    pub fn margins_init(&self, venue: &Venue) -> HashMap<InstrumentId, Money> {
388        self.cache.borrow().account_for_venue(venue).map_or_else(
389            || {
390                log::error!(
391                    "Cannot get initial (order) margins: no account registered for {}",
392                    venue
393                );
394                HashMap::new()
395            },
396            |account| match account {
397                AccountAny::Margin(margin_account) => margin_account.initial_margins(),
398                AccountAny::Cash(_) => {
399                    log::warn!("Initial margins not applicable for cash account");
400                    HashMap::new()
401                }
402            },
403        )
404    }
405
406    #[must_use]
407    pub fn margins_maint(&self, venue: &Venue) -> HashMap<InstrumentId, Money> {
408        self.cache.borrow().account_for_venue(venue).map_or_else(
409            || {
410                log::error!(
411                    "Cannot get maintenance (position) margins: no account registered for {}",
412                    venue
413                );
414                HashMap::new()
415            },
416            |account| match account {
417                AccountAny::Margin(margin_account) => margin_account.maintenance_margins(),
418                AccountAny::Cash(_) => {
419                    log::warn!("Maintenance margins not applicable for cash account");
420                    HashMap::new()
421                }
422            },
423        )
424    }
425
426    #[must_use]
427    pub fn unrealized_pnls(&mut self, venue: &Venue) -> HashMap<Currency, Money> {
428        let instrument_ids = {
429            let borrowed_cache = self.cache.borrow();
430            let positions = borrowed_cache.positions(Some(venue), None, None, None);
431
432            if positions.is_empty() {
433                return HashMap::new(); // Nothing to calculate
434            }
435
436            let instrument_ids: HashSet<InstrumentId> =
437                positions.iter().map(|p| p.instrument_id).collect();
438
439            instrument_ids
440        };
441
442        let mut unrealized_pnls: HashMap<Currency, f64> = HashMap::new();
443
444        for instrument_id in instrument_ids {
445            if let Some(&pnl) = self.inner.borrow_mut().unrealized_pnls.get(&instrument_id) {
446                // PnL already calculated
447                *unrealized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64();
448                continue;
449            }
450
451            // Calculate PnL
452            match self.calculate_unrealized_pnl(&instrument_id) {
453                Some(pnl) => *unrealized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(),
454                None => continue,
455            }
456        }
457
458        unrealized_pnls
459            .into_iter()
460            .map(|(currency, amount)| (currency, Money::new(amount, currency)))
461            .collect()
462    }
463
464    #[must_use]
465    pub fn realized_pnls(&mut self, venue: &Venue) -> HashMap<Currency, Money> {
466        let instrument_ids = {
467            let borrowed_cache = self.cache.borrow();
468            let positions = borrowed_cache.positions(Some(venue), None, None, None);
469
470            if positions.is_empty() {
471                return HashMap::new(); // Nothing to calculate
472            }
473
474            let instrument_ids: HashSet<InstrumentId> =
475                positions.iter().map(|p| p.instrument_id).collect();
476
477            instrument_ids
478        };
479
480        let mut realized_pnls: HashMap<Currency, f64> = HashMap::new();
481
482        for instrument_id in instrument_ids {
483            if let Some(&pnl) = self.inner.borrow_mut().realized_pnls.get(&instrument_id) {
484                // PnL already calculated
485                *realized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64();
486                continue;
487            }
488
489            // Calculate PnL
490            match self.calculate_realized_pnl(&instrument_id) {
491                Some(pnl) => *realized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(),
492                None => continue,
493            }
494        }
495
496        realized_pnls
497            .into_iter()
498            .map(|(currency, amount)| (currency, Money::new(amount, currency)))
499            .collect()
500    }
501
502    #[must_use]
503    pub fn net_exposures(&self, venue: &Venue) -> Option<HashMap<Currency, Money>> {
504        let borrowed_cache = self.cache.borrow();
505        let account = if let Some(account) = borrowed_cache.account_for_venue(venue) {
506            account
507        } else {
508            log::error!(
509                "Cannot calculate net exposures: no account registered for {}",
510                venue
511            );
512            return None; // Cannot calculate
513        };
514
515        let positions_open = borrowed_cache.positions_open(Some(venue), None, None, None);
516        if positions_open.is_empty() {
517            return Some(HashMap::new()); // Nothing to calculate
518        }
519
520        let mut net_exposures: HashMap<Currency, f64> = HashMap::new();
521
522        for position in positions_open {
523            let instrument =
524                if let Some(instrument) = borrowed_cache.instrument(&position.instrument_id) {
525                    instrument
526                } else {
527                    log::error!(
528                        "Cannot calculate net exposures: no instrument for {}",
529                        position.instrument_id
530                    );
531                    return None; // Cannot calculate
532                };
533
534            if position.side == PositionSide::Flat {
535                log::error!(
536                    "Cannot calculate net exposures: position is flat for {}",
537                    position.instrument_id
538                );
539                continue; // Nothing to calculate
540            }
541
542            let last = self.get_last_price(position)?;
543            let xrate = self.calculate_xrate_to_base(instrument, account, position.entry);
544            if xrate == 0.0 {
545                log::error!(
546                    "Cannot calculate net exposures: insufficient data for {}/{:?}",
547                    instrument.settlement_currency(),
548                    account.base_currency()
549                );
550                return None; // Cannot calculate
551            }
552
553            let settlement_currency = account
554                .base_currency()
555                .unwrap_or_else(|| instrument.settlement_currency());
556
557            let net_exposure = instrument
558                .calculate_notional_value(position.quantity, last, None)
559                .as_f64()
560                * xrate;
561
562            let net_exposure = (net_exposure * 10f64.powi(settlement_currency.precision.into()))
563                .round()
564                / 10f64.powi(settlement_currency.precision.into());
565
566            *net_exposures.entry(settlement_currency).or_insert(0.0) += net_exposure;
567        }
568
569        Some(
570            net_exposures
571                .into_iter()
572                .map(|(currency, amount)| (currency, Money::new(amount, currency)))
573                .collect(),
574        )
575    }
576
577    #[must_use]
578    pub fn unrealized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
579        if let Some(pnl) = self
580            .inner
581            .borrow()
582            .unrealized_pnls
583            .get(instrument_id)
584            .copied()
585        {
586            return Some(pnl);
587        }
588
589        let pnl = self.calculate_unrealized_pnl(instrument_id)?;
590        self.inner
591            .borrow_mut()
592            .unrealized_pnls
593            .insert(*instrument_id, pnl);
594        Some(pnl)
595    }
596
597    #[must_use]
598    pub fn realized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
599        if let Some(pnl) = self
600            .inner
601            .borrow()
602            .realized_pnls
603            .get(instrument_id)
604            .copied()
605        {
606            return Some(pnl);
607        }
608
609        let pnl = self.calculate_realized_pnl(instrument_id)?;
610        self.inner
611            .borrow_mut()
612            .realized_pnls
613            .insert(*instrument_id, pnl);
614        Some(pnl)
615    }
616
617    #[must_use]
618    pub fn net_exposure(&self, instrument_id: &InstrumentId) -> Option<Money> {
619        let borrowed_cache = self.cache.borrow();
620        let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue)
621        {
622            account
623        } else {
624            log::error!(
625                "Cannot calculate net exposure: no account registered for {}",
626                instrument_id.venue
627            );
628            return None;
629        };
630
631        let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) {
632            instrument
633        } else {
634            log::error!(
635                "Cannot calculate net exposure: no instrument for {}",
636                instrument_id
637            );
638            return None;
639        };
640
641        let positions_open = borrowed_cache.positions_open(
642            None, // Faster query filtering
643            Some(instrument_id),
644            None,
645            None,
646        );
647
648        if positions_open.is_empty() {
649            return Some(Money::new(0.0, instrument.settlement_currency()));
650        }
651
652        let mut net_exposure = 0.0;
653
654        for position in positions_open {
655            let last = self.get_last_price(position)?;
656            let xrate = self.calculate_xrate_to_base(instrument, account, position.entry);
657            if xrate == 0.0 {
658                log::error!(
659                    "Cannot calculate net exposure: insufficient data for {}/{:?}",
660                    instrument.settlement_currency(),
661                    account.base_currency()
662                );
663                return None;
664            }
665
666            let notional_value = instrument
667                .calculate_notional_value(position.quantity, last, None)
668                .as_f64();
669
670            net_exposure += notional_value * xrate;
671        }
672
673        let settlement_currency = account
674            .base_currency()
675            .unwrap_or_else(|| instrument.settlement_currency());
676
677        Some(Money::new(net_exposure, settlement_currency))
678    }
679
680    #[must_use]
681    pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
682        self.inner
683            .borrow()
684            .net_positions
685            .get(instrument_id)
686            .copied()
687            .unwrap_or(Decimal::ZERO)
688    }
689
690    #[must_use]
691    pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool {
692        self.inner
693            .borrow()
694            .net_positions
695            .get(instrument_id)
696            .copied()
697            .map_or_else(|| false, |net_position| net_position > Decimal::ZERO)
698    }
699
700    #[must_use]
701    pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool {
702        self.inner
703            .borrow()
704            .net_positions
705            .get(instrument_id)
706            .copied()
707            .map_or_else(|| false, |net_position| net_position < Decimal::ZERO)
708    }
709
710    #[must_use]
711    pub fn is_flat(&self, instrument_id: &InstrumentId) -> bool {
712        self.inner
713            .borrow()
714            .net_positions
715            .get(instrument_id)
716            .copied()
717            .map_or_else(|| true, |net_position| net_position == Decimal::ZERO)
718    }
719
720    #[must_use]
721    pub fn is_completely_flat(&self) -> bool {
722        for net_position in self.inner.borrow().net_positions.values() {
723            if *net_position != Decimal::ZERO {
724                return false;
725            }
726        }
727        true
728    }
729
730    // -- COMMANDS --------------------------------------------------------------------------------
731
732    pub fn initialize_orders(&mut self) {
733        let mut initialized = true;
734        let orders_and_instruments = {
735            let borrowed_cache = self.cache.borrow();
736            let all_orders_open = borrowed_cache.orders_open(None, None, None, None);
737
738            let mut instruments_with_orders = Vec::new();
739            let mut instruments = HashSet::new();
740
741            for order in &all_orders_open {
742                instruments.insert(order.instrument_id());
743            }
744
745            for instrument_id in instruments {
746                if let Some(instrument) = borrowed_cache.instrument(&instrument_id) {
747                    let orders = borrowed_cache
748                        .orders_open(None, Some(&instrument_id), None, None)
749                        .into_iter()
750                        .cloned()
751                        .collect::<Vec<OrderAny>>();
752                    instruments_with_orders.push((instrument.clone(), orders));
753                } else {
754                    log::error!(
755                        "Cannot update initial (order) margin: no instrument found for {}",
756                        instrument_id
757                    );
758                    initialized = false;
759                    break;
760                }
761            }
762            instruments_with_orders
763        };
764
765        for (instrument, orders_open) in &orders_and_instruments {
766            let mut borrowed_cache = self.cache.borrow_mut();
767            let account =
768                if let Some(account) = borrowed_cache.account_for_venue(&instrument.id().venue) {
769                    account
770                } else {
771                    log::error!(
772                        "Cannot update initial (order) margin: no account registered for {}",
773                        instrument.id().venue
774                    );
775                    initialized = false;
776                    break;
777                };
778
779            let result = self.inner.borrow_mut().accounts.update_orders(
780                account,
781                instrument.clone(),
782                orders_open.iter().collect(),
783                self.clock.borrow().timestamp_ns(),
784            );
785
786            match result {
787                Some((updated_account, _)) => {
788                    borrowed_cache.add_account(updated_account).unwrap(); // Temp Fix to update the mutated account
789                }
790                None => {
791                    initialized = false;
792                }
793            }
794        }
795
796        let total_orders = orders_and_instruments
797            .into_iter()
798            .map(|(_, orders)| orders.len())
799            .sum::<usize>();
800
801        log::info!(
802            "Initialized {} open order{}",
803            total_orders,
804            if total_orders == 1 { "" } else { "s" }
805        );
806
807        self.inner.borrow_mut().initialized = initialized;
808    }
809
810    pub fn initialize_positions(&mut self) {
811        self.inner.borrow_mut().unrealized_pnls.clear();
812        self.inner.borrow_mut().realized_pnls.clear();
813        let all_positions_open: Vec<Position>;
814        let mut instruments = HashSet::new();
815        {
816            let borrowed_cache = self.cache.borrow();
817            all_positions_open = borrowed_cache
818                .positions_open(None, None, None, None)
819                .into_iter()
820                .cloned()
821                .collect();
822            for position in &all_positions_open {
823                instruments.insert(position.instrument_id);
824            }
825        }
826
827        let mut initialized = true;
828
829        for instrument_id in instruments {
830            let positions_open: Vec<Position> = {
831                let borrowed_cache = self.cache.borrow();
832                borrowed_cache
833                    .positions_open(None, Some(&instrument_id), None, None)
834                    .into_iter()
835                    .cloned()
836                    .collect()
837            };
838
839            self.update_net_position(&instrument_id, positions_open);
840
841            let calculated_unrealized_pnl = self
842                .calculate_unrealized_pnl(&instrument_id)
843                .expect("Failed to calculate unrealized PnL");
844            let calculated_realized_pnl = self
845                .calculate_realized_pnl(&instrument_id)
846                .expect("Failed to calculate realized PnL");
847
848            self.inner
849                .borrow_mut()
850                .unrealized_pnls
851                .insert(instrument_id, calculated_unrealized_pnl);
852            self.inner
853                .borrow_mut()
854                .realized_pnls
855                .insert(instrument_id, calculated_realized_pnl);
856
857            let borrowed_cache = self.cache.borrow();
858            let account =
859                if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue) {
860                    account
861                } else {
862                    log::error!(
863                        "Cannot update maintenance (position) margin: no account registered for {}",
864                        instrument_id.venue
865                    );
866                    initialized = false;
867                    break;
868                };
869
870            let account = match account {
871                AccountAny::Cash(_) => continue,
872                AccountAny::Margin(margin_account) => margin_account,
873            };
874
875            let mut borrowed_cache = self.cache.borrow_mut();
876            let instrument = if let Some(instrument) = borrowed_cache.instrument(&instrument_id) {
877                instrument
878            } else {
879                log::error!(
880                    "Cannot update maintenance (position) margin: no instrument found for {}",
881                    instrument_id
882                );
883                initialized = false;
884                break;
885            };
886
887            let result = self.inner.borrow_mut().accounts.update_positions(
888                account,
889                instrument.clone(),
890                self.cache
891                    .borrow()
892                    .positions_open(None, Some(&instrument_id), None, None),
893                self.clock.borrow().timestamp_ns(),
894            );
895
896            match result {
897                Some((updated_account, _)) => {
898                    borrowed_cache
899                        .add_account(AccountAny::Margin(updated_account)) // Temp Fix to update the mutated account
900                        .unwrap();
901                }
902                None => {
903                    initialized = false;
904                }
905            }
906        }
907
908        let open_count = all_positions_open.len();
909        self.inner.borrow_mut().initialized = initialized;
910        log::info!(
911            "Initialized {} open position{}",
912            open_count,
913            if open_count == 1 { "" } else { "s" }
914        );
915    }
916
917    pub fn update_quote_tick(&mut self, quote: &QuoteTick) {
918        update_quote_tick(
919            self.cache.clone(),
920            self.msgbus.clone(),
921            self.clock.clone(),
922            self.inner.clone(),
923            quote,
924        );
925    }
926
927    pub fn update_bar(&mut self, bar: &Bar) {
928        update_bar(
929            self.cache.clone(),
930            self.msgbus.clone(),
931            self.clock.clone(),
932            self.inner.clone(),
933            bar,
934        );
935    }
936
937    pub fn update_account(&mut self, event: &AccountState) {
938        update_account(self.cache.clone(), event);
939    }
940
941    pub fn update_order(&mut self, event: &OrderEventAny) {
942        update_order(
943            self.cache.clone(),
944            self.msgbus.clone(),
945            self.clock.clone(),
946            self.inner.clone(),
947            event,
948        );
949    }
950
951    pub fn update_position(&mut self, event: &PositionEvent) {
952        update_position(
953            self.cache.clone(),
954            self.msgbus.clone(),
955            self.clock.clone(),
956            self.inner.clone(),
957            event,
958        );
959    }
960
961    // -- INTERNAL --------------------------------------------------------------------------------
962
963    fn update_net_position(&mut self, instrument_id: &InstrumentId, positions_open: Vec<Position>) {
964        let mut net_position = Decimal::ZERO;
965
966        for open_position in positions_open {
967            log::debug!("open_position: {}", open_position);
968            net_position += Decimal::from_f64(open_position.signed_qty).unwrap_or(Decimal::ZERO);
969        }
970
971        let existing_position = self.net_position(instrument_id);
972        if existing_position != net_position {
973            self.inner
974                .borrow_mut()
975                .net_positions
976                .insert(*instrument_id, net_position);
977            log::info!("{} net_position={}", instrument_id, net_position);
978        }
979    }
980
981    fn calculate_unrealized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
982        let borrowed_cache = self.cache.borrow();
983
984        let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue)
985        {
986            account
987        } else {
988            log::error!(
989                "Cannot calculate unrealized PnL: no account registered for {}",
990                instrument_id.venue
991            );
992            return None;
993        };
994
995        let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) {
996            instrument
997        } else {
998            log::error!(
999                "Cannot calculate unrealized PnL: no instrument for {}",
1000                instrument_id
1001            );
1002            return None;
1003        };
1004
1005        let currency = account
1006            .base_currency()
1007            .unwrap_or_else(|| instrument.settlement_currency());
1008
1009        let positions_open = borrowed_cache.positions_open(
1010            None, // Faster query filtering
1011            Some(instrument_id),
1012            None,
1013            None,
1014        );
1015
1016        if positions_open.is_empty() {
1017            return Some(Money::new(0.0, currency));
1018        }
1019
1020        let mut total_pnl = 0.0;
1021
1022        for position in positions_open {
1023            if position.instrument_id != *instrument_id {
1024                continue; // Nothing to calculate
1025            }
1026
1027            if position.side == PositionSide::Flat {
1028                continue; // Nothing to calculate
1029            }
1030
1031            let last = if let Some(price) = self.get_last_price(position) {
1032                price
1033            } else {
1034                log::debug!(
1035                    "Cannot calculate unrealized PnL: no prices for {}",
1036                    instrument_id
1037                );
1038                self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1039                return None; // Cannot calculate
1040            };
1041
1042            let mut pnl = position.unrealized_pnl(last).as_f64();
1043
1044            if let Some(base_currency) = account.base_currency() {
1045                let xrate = self.calculate_xrate_to_base(instrument, account, position.entry);
1046
1047                if xrate == 0.0 {
1048                    log::debug!(
1049                        "Cannot calculate unrealized PnL: insufficient data for {}/{}",
1050                        instrument.settlement_currency(),
1051                        base_currency
1052                    );
1053                    self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1054                    return None;
1055                }
1056
1057                let scale = 10f64.powi(currency.precision.into());
1058                pnl = ((pnl * xrate) * scale).round() / scale;
1059            }
1060
1061            total_pnl += pnl;
1062        }
1063
1064        Some(Money::new(total_pnl, currency))
1065    }
1066
1067    fn calculate_realized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
1068        let borrowed_cache = self.cache.borrow();
1069
1070        let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue)
1071        {
1072            account
1073        } else {
1074            log::error!(
1075                "Cannot calculate realized PnL: no account registered for {}",
1076                instrument_id.venue
1077            );
1078            return None;
1079        };
1080
1081        let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) {
1082            instrument
1083        } else {
1084            log::error!(
1085                "Cannot calculate realized PnL: no instrument for {}",
1086                instrument_id
1087            );
1088            return None;
1089        };
1090
1091        let currency = account
1092            .base_currency()
1093            .unwrap_or_else(|| instrument.settlement_currency());
1094
1095        let positions = borrowed_cache.positions(
1096            None, // Faster query filtering
1097            Some(instrument_id),
1098            None,
1099            None,
1100        );
1101
1102        if positions.is_empty() {
1103            return Some(Money::new(0.0, currency));
1104        }
1105
1106        let mut total_pnl = 0.0;
1107
1108        for position in positions {
1109            if position.instrument_id != *instrument_id {
1110                continue; // Nothing to calculate
1111            }
1112
1113            if position.realized_pnl.is_none() {
1114                continue; // Nothing to calculate
1115            }
1116
1117            let mut pnl = position.realized_pnl?.as_f64();
1118
1119            if let Some(base_currency) = account.base_currency() {
1120                let xrate = self.calculate_xrate_to_base(instrument, account, position.entry);
1121
1122                if xrate == 0.0 {
1123                    log::debug!(
1124                        "Cannot calculate realized PnL: insufficient data for {}/{}",
1125                        instrument.settlement_currency(),
1126                        base_currency
1127                    );
1128                    self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1129                    return None; // Cannot calculate
1130                }
1131
1132                let scale = 10f64.powi(currency.precision.into());
1133                pnl = ((pnl * xrate) * scale).round() / scale;
1134            }
1135
1136            total_pnl += pnl;
1137        }
1138
1139        Some(Money::new(total_pnl, currency))
1140    }
1141
1142    fn get_last_price(&self, position: &Position) -> Option<Price> {
1143        let price_type = match position.side {
1144            PositionSide::Long => PriceType::Bid,
1145            PositionSide::Short => PriceType::Ask,
1146            _ => panic!("invalid `PositionSide`, was {}", position.side),
1147        };
1148
1149        let borrowed_cache = self.cache.borrow();
1150
1151        let instrument_id = &position.instrument_id;
1152        borrowed_cache
1153            .price(instrument_id, price_type)
1154            .or_else(|| borrowed_cache.price(instrument_id, PriceType::Last))
1155            .or_else(|| {
1156                self.inner
1157                    .borrow()
1158                    .bar_close_prices
1159                    .get(instrument_id)
1160                    .copied()
1161            })
1162    }
1163
1164    fn calculate_xrate_to_base(
1165        &self,
1166        instrument: &InstrumentAny,
1167        account: &AccountAny,
1168        side: OrderSide,
1169    ) -> f64 {
1170        match account.base_currency() {
1171            Some(base_currency) => {
1172                let price_type = if side == OrderSide::Buy {
1173                    PriceType::Bid
1174                } else {
1175                    PriceType::Ask
1176                };
1177
1178                self.cache
1179                    .borrow()
1180                    .get_xrate(
1181                        instrument.id().venue,
1182                        instrument.settlement_currency(),
1183                        base_currency,
1184                        price_type,
1185                    )
1186                    .to_f64()
1187                    .unwrap_or_else(|| {
1188                        log::error!(
1189                            "Failed to get/convert xrate for instrument {} from {} to {}",
1190                            instrument.id(),
1191                            instrument.settlement_currency(),
1192                            base_currency
1193                        );
1194                        1.0
1195                    })
1196            }
1197            None => 1.0, // No conversion needed
1198        }
1199    }
1200}
1201
1202// Helper functions
1203fn update_quote_tick(
1204    cache: Rc<RefCell<Cache>>,
1205    msgbus: Rc<RefCell<MessageBus>>,
1206    clock: Rc<RefCell<dyn Clock>>,
1207    inner: Rc<RefCell<PortfolioState>>,
1208    quote: &QuoteTick,
1209) {
1210    update_instrument_id(cache, msgbus, clock.clone(), inner, &quote.instrument_id);
1211}
1212
1213fn update_bar(
1214    cache: Rc<RefCell<Cache>>,
1215    msgbus: Rc<RefCell<MessageBus>>,
1216    clock: Rc<RefCell<dyn Clock>>,
1217    inner: Rc<RefCell<PortfolioState>>,
1218    bar: &Bar,
1219) {
1220    let instrument_id = bar.bar_type.instrument_id();
1221    inner
1222        .borrow_mut()
1223        .bar_close_prices
1224        .insert(instrument_id, bar.close);
1225    update_instrument_id(cache, msgbus, clock.clone(), inner, &instrument_id);
1226}
1227
1228fn update_instrument_id(
1229    cache: Rc<RefCell<Cache>>,
1230    msgbus: Rc<RefCell<MessageBus>>,
1231    clock: Rc<RefCell<dyn Clock>>,
1232    inner: Rc<RefCell<PortfolioState>>,
1233    instrument_id: &InstrumentId,
1234) {
1235    inner.borrow_mut().unrealized_pnls.remove(instrument_id);
1236
1237    if inner.borrow().initialized || !inner.borrow().pending_calcs.contains(instrument_id) {
1238        return;
1239    }
1240
1241    let result_init;
1242    let mut result_maint = None;
1243
1244    let account = {
1245        let borrowed_cache = cache.borrow();
1246        let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue)
1247        {
1248            account
1249        } else {
1250            log::error!(
1251                "Cannot update tick: no account registered for {}",
1252                instrument_id.venue
1253            );
1254            return;
1255        };
1256
1257        let mut borrowed_cache = cache.borrow_mut();
1258        let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) {
1259            instrument.clone()
1260        } else {
1261            log::error!(
1262                "Cannot update tick: no instrument found for {}",
1263                instrument_id
1264            );
1265            return;
1266        };
1267
1268        // Clone the orders and positions to own the data
1269        let orders_open: Vec<OrderAny> = borrowed_cache
1270            .orders_open(None, Some(instrument_id), None, None)
1271            .iter()
1272            .map(|o| (*o).clone())
1273            .collect();
1274
1275        let positions_open: Vec<Position> = borrowed_cache
1276            .positions_open(None, Some(instrument_id), None, None)
1277            .iter()
1278            .map(|p| (*p).clone())
1279            .collect();
1280
1281        result_init = inner.borrow().accounts.update_orders(
1282            account,
1283            instrument.clone(),
1284            orders_open.iter().collect(),
1285            clock.borrow().timestamp_ns(),
1286        );
1287
1288        if let AccountAny::Margin(margin_account) = account {
1289            result_maint = inner.borrow().accounts.update_positions(
1290                margin_account,
1291                instrument,
1292                positions_open.iter().collect(),
1293                clock.borrow().timestamp_ns(),
1294            );
1295        }
1296
1297        if let Some((ref updated_account, _)) = result_init {
1298            borrowed_cache.add_account(updated_account.clone()).unwrap(); // Temp Fix to update the mutated account
1299        }
1300        account.clone()
1301    };
1302
1303    let mut portfolio_clone = Portfolio {
1304        clock: clock.clone(),
1305        cache,
1306        msgbus,
1307        inner: inner.clone(),
1308    };
1309
1310    let result_unrealized_pnl: Option<Money> =
1311        portfolio_clone.calculate_unrealized_pnl(instrument_id);
1312
1313    if result_init.is_some()
1314        && (matches!(account, AccountAny::Cash(_))
1315            || (result_maint.is_some() && result_unrealized_pnl.is_some()))
1316    {
1317        inner.borrow_mut().pending_calcs.remove(instrument_id);
1318        if inner.borrow().pending_calcs.is_empty() {
1319            inner.borrow_mut().initialized = true;
1320        }
1321    }
1322}
1323
1324fn update_order(
1325    cache: Rc<RefCell<Cache>>,
1326    msgbus: Rc<RefCell<MessageBus>>,
1327    clock: Rc<RefCell<dyn Clock>>,
1328    inner: Rc<RefCell<PortfolioState>>,
1329    event: &OrderEventAny,
1330) {
1331    let borrowed_cache = cache.borrow();
1332    let account_id = match event.account_id() {
1333        Some(account_id) => account_id,
1334        None => {
1335            return; // No Account Assigned
1336        }
1337    };
1338
1339    let account = if let Some(account) = borrowed_cache.account(&account_id) {
1340        account
1341    } else {
1342        log::error!(
1343            "Cannot update order: no account registered for {}",
1344            account_id
1345        );
1346        return;
1347    };
1348
1349    match account {
1350        AccountAny::Cash(cash_account) => {
1351            if !cash_account.base.calculate_account_state {
1352                return;
1353            }
1354        }
1355        AccountAny::Margin(margin_account) => {
1356            if !margin_account.base.calculate_account_state {
1357                return;
1358            }
1359        }
1360    }
1361
1362    match event {
1363        OrderEventAny::Accepted(_)
1364        | OrderEventAny::Canceled(_)
1365        | OrderEventAny::Rejected(_)
1366        | OrderEventAny::Updated(_)
1367        | OrderEventAny::Filled(_) => {}
1368        _ => {
1369            return;
1370        }
1371    }
1372
1373    let borrowed_cache = cache.borrow();
1374    let order = if let Some(order) = borrowed_cache.order(&event.client_order_id()) {
1375        order
1376    } else {
1377        log::error!(
1378            "Cannot update order: {} not found in the cache",
1379            event.client_order_id()
1380        );
1381        return; // No Order Found
1382    };
1383
1384    if matches!(event, OrderEventAny::Rejected(_)) && order.order_type() != OrderType::StopLimit {
1385        return; // No change to account state
1386    }
1387
1388    let instrument = if let Some(instrument_id) = borrowed_cache.instrument(&event.instrument_id())
1389    {
1390        instrument_id
1391    } else {
1392        log::error!(
1393            "Cannot update order: no instrument found for {}",
1394            event.instrument_id()
1395        );
1396        return;
1397    };
1398
1399    if let OrderEventAny::Filled(order_filled) = event {
1400        let _ = inner.borrow().accounts.update_balances(
1401            account.clone(),
1402            instrument.clone(),
1403            *order_filled,
1404        );
1405
1406        let mut portfolio_clone = Portfolio {
1407            clock: clock.clone(),
1408            cache: cache.clone(),
1409            msgbus: msgbus.clone(),
1410            inner: inner.clone(),
1411        };
1412
1413        match portfolio_clone.calculate_unrealized_pnl(&order_filled.instrument_id) {
1414            Some(unrealized_pnl) => {
1415                inner
1416                    .borrow_mut()
1417                    .unrealized_pnls
1418                    .insert(event.instrument_id(), unrealized_pnl);
1419            }
1420            None => {
1421                log::error!(
1422                    "Failed to calculate unrealized PnL for instrument {}",
1423                    event.instrument_id()
1424                );
1425            }
1426        }
1427    }
1428
1429    let orders_open = borrowed_cache.orders_open(None, Some(&event.instrument_id()), None, None);
1430
1431    let account_state = inner.borrow_mut().accounts.update_orders(
1432        account,
1433        instrument.clone(),
1434        orders_open,
1435        clock.borrow().timestamp_ns(),
1436    );
1437
1438    let mut borrowed_cache = cache.borrow_mut();
1439    borrowed_cache.update_account(account.clone()).unwrap();
1440
1441    if let Some(account_state) = account_state {
1442        msgbus.borrow().publish(
1443            &Ustr::from(&format!("events.account.{}", account.id())),
1444            &account_state,
1445        );
1446    } else {
1447        log::debug!("Added pending calculation for {}", instrument.id());
1448        inner.borrow_mut().pending_calcs.insert(instrument.id());
1449    }
1450
1451    log::debug!("Updated {}", event);
1452}
1453
1454fn update_position(
1455    cache: Rc<RefCell<Cache>>,
1456    msgbus: Rc<RefCell<MessageBus>>,
1457    clock: Rc<RefCell<dyn Clock>>,
1458    inner: Rc<RefCell<PortfolioState>>,
1459    event: &PositionEvent,
1460) {
1461    let instrument_id = event.instrument_id();
1462
1463    let positions_open: Vec<Position> = {
1464        let borrowed_cache = cache.borrow();
1465
1466        borrowed_cache
1467            .positions_open(None, Some(&instrument_id), None, None)
1468            .iter()
1469            .map(|o| (*o).clone())
1470            .collect()
1471    };
1472
1473    log::debug!("postion fresh from cache -> {:?}", positions_open);
1474
1475    let mut portfolio_clone = Portfolio {
1476        clock: clock.clone(),
1477        cache: cache.clone(),
1478        msgbus,
1479        inner: inner.clone(),
1480    };
1481
1482    portfolio_clone.update_net_position(&instrument_id, positions_open.clone());
1483
1484    let calculated_unrealized_pnl = portfolio_clone
1485        .calculate_unrealized_pnl(&instrument_id)
1486        .expect("Failed to calculate unrealized PnL");
1487    let calculated_realized_pnl = portfolio_clone
1488        .calculate_realized_pnl(&instrument_id)
1489        .expect("Failed to calculate realized PnL");
1490
1491    inner
1492        .borrow_mut()
1493        .unrealized_pnls
1494        .insert(event.instrument_id(), calculated_unrealized_pnl);
1495    inner
1496        .borrow_mut()
1497        .realized_pnls
1498        .insert(event.instrument_id(), calculated_realized_pnl);
1499
1500    let borrowed_cache = cache.borrow();
1501    let account = borrowed_cache.account(&event.account_id());
1502
1503    if let Some(AccountAny::Margin(margin_account)) = account {
1504        if !margin_account.calculate_account_state {
1505            return; // Nothing to calculate
1506        }
1507
1508        let borrowed_cache = cache.borrow();
1509        let instrument = if let Some(instrument) = borrowed_cache.instrument(&instrument_id) {
1510            instrument
1511        } else {
1512            log::error!(
1513                "Cannot update position: no instrument found for {}",
1514                instrument_id
1515            );
1516            return;
1517        };
1518
1519        let result = inner.borrow_mut().accounts.update_positions(
1520            margin_account,
1521            instrument.clone(),
1522            positions_open.iter().collect(),
1523            clock.borrow().timestamp_ns(),
1524        );
1525        let mut borrowed_cache = cache.borrow_mut();
1526        if let Some((margin_account, _)) = result {
1527            borrowed_cache
1528                .add_account(AccountAny::Margin(margin_account)) // Temp Fix to update the mutated account
1529                .unwrap();
1530        }
1531    } else if account.is_none() {
1532        log::error!(
1533            "Cannot update position: no account registered for {}",
1534            event.account_id()
1535        );
1536    }
1537}
1538
1539pub fn update_account(cache: Rc<RefCell<Cache>>, event: &AccountState) {
1540    let mut borrowed_cache = cache.borrow_mut();
1541
1542    if let Some(existing) = borrowed_cache.account(&event.account_id) {
1543        let mut account = existing.clone();
1544        account.apply(event.clone());
1545
1546        if let Err(e) = borrowed_cache.update_account(account.clone()) {
1547            log::error!("Failed to update account: {}", e);
1548            return;
1549        }
1550    } else {
1551        let account = match AccountAny::from_events(vec![event.clone()]) {
1552            Ok(account) => account,
1553            Err(e) => {
1554                log::error!("Failed to create account: {}", e);
1555                return;
1556            }
1557        };
1558
1559        if let Err(e) = borrowed_cache.add_account(account) {
1560            log::error!("Failed to add account: {}", e);
1561            return;
1562        }
1563    }
1564
1565    log::info!("Updated {}", event);
1566}
1567
1568////////////////////////////////////////////////////////////////////////////////
1569// Tests
1570////////////////////////////////////////////////////////////////////////////////
1571#[cfg(test)]
1572mod tests {
1573    use std::{cell::RefCell, rc::Rc};
1574
1575    use nautilus_common::{cache::Cache, clock::TestClock, msgbus::MessageBus};
1576    use nautilus_core::{UnixNanos, UUID4};
1577    use nautilus_model::{
1578        data::{Bar, BarType, QuoteTick},
1579        enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType},
1580        events::{
1581            account::stubs::cash_account_state,
1582            order::stubs::{order_accepted, order_filled, order_submitted},
1583            AccountState, OrderAccepted, OrderEventAny, OrderFilled, OrderSubmitted,
1584            PositionChanged, PositionClosed, PositionEvent, PositionOpened,
1585        },
1586        identifiers::{
1587            stubs::{account_id, uuid4},
1588            AccountId, ClientOrderId, PositionId, StrategyId, Symbol, TradeId, VenueOrderId,
1589        },
1590        instruments::{
1591            stubs::{audusd_sim, currency_pair_btcusdt, default_fx_ccy, ethusdt_bitmex},
1592            CryptoPerpetual, CurrencyPair, InstrumentAny,
1593        },
1594        orders::{OrderAny, OrderTestBuilder},
1595        position::Position,
1596        types::{AccountBalance, Currency, Money, Price, Quantity},
1597    };
1598    use rstest::{fixture, rstest};
1599    use rust_decimal::{prelude::FromPrimitive, Decimal};
1600
1601    use super::Portfolio;
1602
1603    #[fixture]
1604    fn msgbus() -> MessageBus {
1605        MessageBus::default()
1606    }
1607
1608    #[fixture]
1609    fn simple_cache() -> Cache {
1610        Cache::new(None, None)
1611    }
1612
1613    #[fixture]
1614    fn clock() -> TestClock {
1615        TestClock::new()
1616    }
1617
1618    #[fixture]
1619    fn venue() -> Venue {
1620        Venue::new("SIM")
1621    }
1622
1623    #[fixture]
1624    fn instrument_audusd(audusd_sim: CurrencyPair) -> InstrumentAny {
1625        InstrumentAny::CurrencyPair(audusd_sim)
1626    }
1627
1628    #[fixture]
1629    fn instrument_gbpusd() -> InstrumentAny {
1630        InstrumentAny::CurrencyPair(default_fx_ccy(
1631            Symbol::from("GBP/USD"),
1632            Some(Venue::from("SIM")),
1633        ))
1634    }
1635
1636    #[fixture]
1637    fn instrument_btcusdt(currency_pair_btcusdt: CurrencyPair) -> InstrumentAny {
1638        InstrumentAny::CurrencyPair(currency_pair_btcusdt)
1639    }
1640
1641    #[fixture]
1642    fn instrument_ethusdt(ethusdt_bitmex: CryptoPerpetual) -> InstrumentAny {
1643        InstrumentAny::CryptoPerpetual(ethusdt_bitmex)
1644    }
1645
1646    #[fixture]
1647    fn portfolio(
1648        msgbus: MessageBus,
1649        mut simple_cache: Cache,
1650        clock: TestClock,
1651        instrument_audusd: InstrumentAny,
1652        instrument_gbpusd: InstrumentAny,
1653        instrument_btcusdt: InstrumentAny,
1654        instrument_ethusdt: InstrumentAny,
1655    ) -> Portfolio {
1656        simple_cache.add_instrument(instrument_audusd).unwrap();
1657        simple_cache.add_instrument(instrument_gbpusd).unwrap();
1658        simple_cache.add_instrument(instrument_btcusdt).unwrap();
1659        simple_cache.add_instrument(instrument_ethusdt).unwrap();
1660
1661        Portfolio::new(
1662            Rc::new(RefCell::new(msgbus)),
1663            Rc::new(RefCell::new(simple_cache)),
1664            Rc::new(RefCell::new(clock)),
1665            true,
1666        )
1667    }
1668
1669    use std::collections::HashMap;
1670
1671    use nautilus_model::identifiers::Venue;
1672
1673    // Helpers
1674    fn get_cash_account(accountid: Option<&str>) -> AccountState {
1675        AccountState::new(
1676            match accountid {
1677                Some(account_id_str) => AccountId::new(account_id_str),
1678                None => account_id(),
1679            },
1680            AccountType::Cash,
1681            vec![
1682                AccountBalance::new(
1683                    Money::new(10.00000000, Currency::BTC()),
1684                    Money::new(0.00000000, Currency::BTC()),
1685                    Money::new(10.00000000, Currency::BTC()),
1686                ),
1687                AccountBalance::new(
1688                    Money::new(10.000, Currency::USD()),
1689                    Money::new(0.000, Currency::USD()),
1690                    Money::new(10.000, Currency::USD()),
1691                ),
1692                AccountBalance::new(
1693                    Money::new(100000.000, Currency::USDT()),
1694                    Money::new(0.000, Currency::USDT()),
1695                    Money::new(100000.000, Currency::USDT()),
1696                ),
1697                AccountBalance::new(
1698                    Money::new(20.000, Currency::ETH()),
1699                    Money::new(0.000, Currency::ETH()),
1700                    Money::new(20.000, Currency::ETH()),
1701                ),
1702            ],
1703            vec![],
1704            true,
1705            uuid4(),
1706            0.into(),
1707            0.into(),
1708            None,
1709        )
1710    }
1711
1712    fn get_margin_account(accountid: Option<&str>) -> AccountState {
1713        AccountState::new(
1714            match accountid {
1715                Some(account_id_str) => AccountId::new(account_id_str),
1716                None => account_id(),
1717            },
1718            AccountType::Margin,
1719            vec![
1720                AccountBalance::new(
1721                    Money::new(10.000, Currency::BTC()),
1722                    Money::new(0.000, Currency::BTC()),
1723                    Money::new(10.000, Currency::BTC()),
1724                ),
1725                AccountBalance::new(
1726                    Money::new(20.000, Currency::ETH()),
1727                    Money::new(0.000, Currency::ETH()),
1728                    Money::new(20.000, Currency::ETH()),
1729                ),
1730                AccountBalance::new(
1731                    Money::new(100000.000, Currency::USDT()),
1732                    Money::new(0.000, Currency::USDT()),
1733                    Money::new(100000.000, Currency::USDT()),
1734                ),
1735                AccountBalance::new(
1736                    Money::new(10.000, Currency::USD()),
1737                    Money::new(0.000, Currency::USD()),
1738                    Money::new(10.000, Currency::USD()),
1739                ),
1740                AccountBalance::new(
1741                    Money::new(10.000, Currency::GBP()),
1742                    Money::new(0.000, Currency::GBP()),
1743                    Money::new(10.000, Currency::GBP()),
1744                ),
1745            ],
1746            Vec::new(),
1747            true,
1748            uuid4(),
1749            0.into(),
1750            0.into(),
1751            None,
1752        )
1753    }
1754
1755    fn get_quote_tick(
1756        instrument: &InstrumentAny,
1757        bid: f64,
1758        ask: f64,
1759        bid_size: f64,
1760        ask_size: f64,
1761    ) -> QuoteTick {
1762        QuoteTick::new(
1763            instrument.id(),
1764            Price::new(bid, 0),
1765            Price::new(ask, 0),
1766            Quantity::new(bid_size, 0),
1767            Quantity::new(ask_size, 0),
1768            0.into(),
1769            0.into(),
1770        )
1771    }
1772
1773    fn get_bar(
1774        instrument: &InstrumentAny,
1775        open: f64,
1776        high: f64,
1777        low: f64,
1778        close: f64,
1779        volume: f64,
1780    ) -> Bar {
1781        let bar_type_str = format!("{}-1-MINUTE-LAST-EXTERNAL", instrument.id());
1782        Bar::new(
1783            BarType::from(bar_type_str.as_ref()),
1784            Price::new(open, 0),
1785            Price::new(high, 0),
1786            Price::new(low, 0),
1787            Price::new(close, 0),
1788            Quantity::new(volume, 0),
1789            0.into(),
1790            0.into(),
1791        )
1792    }
1793
1794    fn submit_order(order: &OrderAny) -> OrderSubmitted {
1795        order_submitted(
1796            order.trader_id(),
1797            order.strategy_id(),
1798            order.instrument_id(),
1799            order.client_order_id(),
1800            account_id(),
1801            uuid4(),
1802        )
1803    }
1804
1805    fn accept_order(order: &OrderAny) -> OrderAccepted {
1806        order_accepted(
1807            order.trader_id(),
1808            order.strategy_id(),
1809            order.instrument_id(),
1810            order.client_order_id(),
1811            account_id(),
1812            order.venue_order_id().unwrap_or(VenueOrderId::new("1")),
1813            uuid4(),
1814        )
1815    }
1816
1817    fn fill_order(order: &OrderAny) -> OrderFilled {
1818        order_filled(
1819            order.trader_id(),
1820            order.strategy_id(),
1821            order.instrument_id(),
1822            order.client_order_id(),
1823            uuid4(),
1824        )
1825    }
1826
1827    fn get_open_position(position: &Position) -> PositionOpened {
1828        PositionOpened {
1829            trader_id: position.trader_id,
1830            strategy_id: position.strategy_id,
1831            instrument_id: position.instrument_id,
1832            position_id: position.id,
1833            account_id: position.account_id,
1834            opening_order_id: position.opening_order_id,
1835            entry: position.entry,
1836            side: position.side,
1837            signed_qty: position.signed_qty,
1838            quantity: position.quantity,
1839            last_qty: position.quantity,
1840            last_px: Price::new(position.avg_px_open, 0),
1841            currency: position.settlement_currency,
1842            avg_px_open: position.avg_px_open,
1843            event_id: UUID4::new(),
1844            ts_event: 0.into(),
1845            ts_init: 0.into(),
1846        }
1847    }
1848
1849    fn get_changed_position(position: &Position) -> PositionChanged {
1850        PositionChanged {
1851            trader_id: position.trader_id,
1852            strategy_id: position.strategy_id,
1853            instrument_id: position.instrument_id,
1854            position_id: position.id,
1855            account_id: position.account_id,
1856            opening_order_id: position.opening_order_id,
1857            entry: position.entry,
1858            side: position.side,
1859            signed_qty: position.signed_qty,
1860            quantity: position.quantity,
1861            last_qty: position.quantity,
1862            last_px: Price::new(position.avg_px_open, 0),
1863            currency: position.settlement_currency,
1864            avg_px_open: position.avg_px_open,
1865            ts_event: 0.into(),
1866            ts_init: 0.into(),
1867            peak_quantity: position.quantity,
1868            avg_px_close: Some(position.avg_px_open),
1869            realized_return: position.avg_px_open,
1870            realized_pnl: Some(Money::new(10.0, Currency::USD())),
1871            unrealized_pnl: Money::new(10.0, Currency::USD()),
1872            event_id: UUID4::new(),
1873            ts_opened: 0.into(),
1874        }
1875    }
1876
1877    fn get_close_position(position: &Position) -> PositionClosed {
1878        PositionClosed {
1879            trader_id: position.trader_id,
1880            strategy_id: position.strategy_id,
1881            instrument_id: position.instrument_id,
1882            position_id: position.id,
1883            account_id: position.account_id,
1884            opening_order_id: position.opening_order_id,
1885            entry: position.entry,
1886            side: position.side,
1887            signed_qty: position.signed_qty,
1888            quantity: position.quantity,
1889            last_qty: position.quantity,
1890            last_px: Price::new(position.avg_px_open, 0),
1891            currency: position.settlement_currency,
1892            avg_px_open: position.avg_px_open,
1893            ts_event: 0.into(),
1894            ts_init: 0.into(),
1895            peak_quantity: position.quantity,
1896            avg_px_close: Some(position.avg_px_open),
1897            realized_return: position.avg_px_open,
1898            realized_pnl: Some(Money::new(10.0, Currency::USD())),
1899            unrealized_pnl: Money::new(10.0, Currency::USD()),
1900            closing_order_id: Some(ClientOrderId::new("SSD")),
1901            duration: 0,
1902            event_id: UUID4::new(),
1903            ts_opened: 0.into(),
1904            ts_closed: None,
1905        }
1906    }
1907
1908    // Tests
1909    #[rstest]
1910    fn test_account_when_account_returns_the_account_facade(mut portfolio: Portfolio) {
1911        let account_id = "BINANCE-1513111";
1912        let state = get_cash_account(Some(account_id));
1913
1914        portfolio.update_account(&state);
1915
1916        let borrowed_cache = portfolio.cache.borrow_mut();
1917        let account = borrowed_cache.account(&AccountId::new(account_id)).unwrap();
1918        assert_eq!(account.id().get_issuer(), "BINANCE".into());
1919        assert_eq!(account.id().get_issuers_id(), "1513111");
1920    }
1921
1922    #[rstest]
1923    fn test_balances_locked_when_no_account_for_venue_returns_none(
1924        portfolio: Portfolio,
1925        venue: Venue,
1926    ) {
1927        let result = portfolio.balances_locked(&venue);
1928        assert_eq!(result, HashMap::new());
1929    }
1930
1931    #[rstest]
1932    fn test_margins_init_when_no_account_for_venue_returns_none(
1933        portfolio: Portfolio,
1934        venue: Venue,
1935    ) {
1936        let result = portfolio.margins_init(&venue);
1937        assert_eq!(result, HashMap::new());
1938    }
1939
1940    #[rstest]
1941    fn test_margins_maint_when_no_account_for_venue_returns_none(
1942        portfolio: Portfolio,
1943        venue: Venue,
1944    ) {
1945        let result = portfolio.margins_maint(&venue);
1946        assert_eq!(result, HashMap::new());
1947    }
1948
1949    #[rstest]
1950    fn test_unrealized_pnl_for_instrument_when_no_instrument_returns_none(
1951        mut portfolio: Portfolio,
1952        instrument_audusd: InstrumentAny,
1953    ) {
1954        let result = portfolio.unrealized_pnl(&instrument_audusd.id());
1955        assert!(result.is_none());
1956    }
1957
1958    #[rstest]
1959    fn test_unrealized_pnl_for_venue_when_no_account_returns_empty_dict(
1960        mut portfolio: Portfolio,
1961        venue: Venue,
1962    ) {
1963        let result = portfolio.unrealized_pnls(&venue);
1964        assert_eq!(result, HashMap::new());
1965    }
1966
1967    #[rstest]
1968    fn test_realized_pnl_for_instrument_when_no_instrument_returns_none(
1969        mut portfolio: Portfolio,
1970        instrument_audusd: InstrumentAny,
1971    ) {
1972        let result = portfolio.realized_pnl(&instrument_audusd.id());
1973        assert!(result.is_none());
1974    }
1975
1976    #[rstest]
1977    fn test_realized_pnl_for_venue_when_no_account_returns_empty_dict(
1978        mut portfolio: Portfolio,
1979        venue: Venue,
1980    ) {
1981        let result = portfolio.realized_pnls(&venue);
1982        assert_eq!(result, HashMap::new());
1983    }
1984
1985    #[rstest]
1986    fn test_net_position_when_no_positions_returns_zero(
1987        portfolio: Portfolio,
1988        instrument_audusd: InstrumentAny,
1989    ) {
1990        let result = portfolio.net_position(&instrument_audusd.id());
1991        assert_eq!(result, Decimal::ZERO);
1992    }
1993
1994    #[rstest]
1995    fn test_net_exposures_when_no_positions_returns_none(portfolio: Portfolio, venue: Venue) {
1996        let result = portfolio.net_exposures(&venue);
1997        assert!(result.is_none());
1998    }
1999
2000    #[rstest]
2001    fn test_is_net_long_when_no_positions_returns_false(
2002        portfolio: Portfolio,
2003        instrument_audusd: InstrumentAny,
2004    ) {
2005        let result = portfolio.is_net_long(&instrument_audusd.id());
2006        assert!(!result);
2007    }
2008
2009    #[rstest]
2010    fn test_is_net_short_when_no_positions_returns_false(
2011        portfolio: Portfolio,
2012        instrument_audusd: InstrumentAny,
2013    ) {
2014        let result = portfolio.is_net_short(&instrument_audusd.id());
2015        assert!(!result);
2016    }
2017
2018    #[rstest]
2019    fn test_is_flat_when_no_positions_returns_true(
2020        portfolio: Portfolio,
2021        instrument_audusd: InstrumentAny,
2022    ) {
2023        let result = portfolio.is_flat(&instrument_audusd.id());
2024        assert!(result);
2025    }
2026
2027    #[rstest]
2028    fn test_is_completely_flat_when_no_positions_returns_true(portfolio: Portfolio) {
2029        let result = portfolio.is_completely_flat();
2030        assert!(result);
2031    }
2032
2033    #[rstest]
2034    fn test_open_value_when_no_account_returns_none(portfolio: Portfolio, venue: Venue) {
2035        let result = portfolio.net_exposures(&venue);
2036        assert!(result.is_none());
2037    }
2038
2039    #[rstest]
2040    fn test_update_tick(mut portfolio: Portfolio, instrument_audusd: InstrumentAny) {
2041        let tick = get_quote_tick(&instrument_audusd, 1.25, 1.251, 1.0, 1.0);
2042        portfolio.update_quote_tick(&tick);
2043        assert!(portfolio.unrealized_pnl(&instrument_audusd.id()).is_none());
2044    }
2045
2046    //TODO: FIX: It should return an error
2047    #[rstest]
2048    fn test_exceed_free_balance_single_currency_raises_account_balance_negative_exception(
2049        mut portfolio: Portfolio,
2050        cash_account_state: AccountState,
2051        instrument_audusd: InstrumentAny,
2052    ) {
2053        portfolio.update_account(&cash_account_state);
2054
2055        let mut order = OrderTestBuilder::new(OrderType::Market)
2056            .instrument_id(instrument_audusd.id())
2057            .side(OrderSide::Buy)
2058            .quantity(Quantity::from("1000000"))
2059            .build();
2060
2061        portfolio
2062            .cache
2063            .borrow_mut()
2064            .add_order(order.clone(), None, None, false)
2065            .unwrap();
2066
2067        let order_submitted = submit_order(&order);
2068        order
2069            .apply(OrderEventAny::Submitted(order_submitted))
2070            .unwrap();
2071
2072        portfolio.update_order(&OrderEventAny::Submitted(order_submitted));
2073
2074        let order_filled = fill_order(&order);
2075        order.apply(OrderEventAny::Filled(order_filled)).unwrap();
2076        portfolio.update_order(&OrderEventAny::Filled(order_filled));
2077    }
2078
2079    // TODO: It should return an error
2080    #[rstest]
2081    fn test_exceed_free_balance_multi_currency_raises_account_balance_negative_exception(
2082        mut portfolio: Portfolio,
2083        cash_account_state: AccountState,
2084        instrument_audusd: InstrumentAny,
2085    ) {
2086        portfolio.update_account(&cash_account_state);
2087
2088        let account = portfolio
2089            .cache
2090            .borrow_mut()
2091            .account_for_venue(&Venue::from("SIM"))
2092            .unwrap()
2093            .clone();
2094
2095        // Create Order
2096        let mut order = OrderTestBuilder::new(OrderType::Market)
2097            .instrument_id(instrument_audusd.id())
2098            .side(OrderSide::Buy)
2099            .quantity(Quantity::from("3.0"))
2100            .build();
2101
2102        portfolio
2103            .cache
2104            .borrow_mut()
2105            .add_order(order.clone(), None, None, false)
2106            .unwrap();
2107
2108        let order_submitted = submit_order(&order);
2109        order
2110            .apply(OrderEventAny::Submitted(order_submitted))
2111            .unwrap();
2112        portfolio.update_order(&OrderEventAny::Submitted(order_submitted));
2113
2114        // Assert
2115        assert_eq!(
2116            account.balances().iter().next().unwrap().1.total.as_f64(),
2117            1525000.00
2118        );
2119    }
2120
2121    #[rstest]
2122    fn test_update_orders_open_cash_account(
2123        mut portfolio: Portfolio,
2124        cash_account_state: AccountState,
2125        instrument_audusd: InstrumentAny,
2126    ) {
2127        portfolio.update_account(&cash_account_state);
2128
2129        // Create Order
2130        let mut order = OrderTestBuilder::new(OrderType::Limit)
2131            .instrument_id(instrument_audusd.id())
2132            .side(OrderSide::Buy)
2133            .quantity(Quantity::from("1.0"))
2134            .price(Price::new(50000.0, 0))
2135            .build();
2136
2137        portfolio
2138            .cache
2139            .borrow_mut()
2140            .add_order(order.clone(), None, None, false)
2141            .unwrap();
2142
2143        let order_submitted = submit_order(&order);
2144        order
2145            .apply(OrderEventAny::Submitted(order_submitted))
2146            .unwrap();
2147        portfolio.update_order(&OrderEventAny::Submitted(order_submitted));
2148
2149        // ACCEPTED
2150        let order_accepted = accept_order(&order);
2151        order
2152            .apply(OrderEventAny::Accepted(order_accepted))
2153            .unwrap();
2154        portfolio.update_order(&OrderEventAny::Accepted(order_accepted));
2155
2156        assert_eq!(
2157            portfolio
2158                .balances_locked(&Venue::from("SIM"))
2159                .get(&Currency::USD())
2160                .unwrap()
2161                .as_f64(),
2162            25000.0
2163        );
2164    }
2165
2166    #[rstest]
2167    fn test_update_orders_open_margin_account(
2168        mut portfolio: Portfolio,
2169        instrument_btcusdt: InstrumentAny,
2170    ) {
2171        let account_state = get_margin_account(Some("BINANCE-01234"));
2172        portfolio.update_account(&account_state);
2173
2174        // Create Order
2175        let mut order1 = OrderTestBuilder::new(OrderType::StopMarket)
2176            .instrument_id(instrument_btcusdt.id())
2177            .side(OrderSide::Buy)
2178            .quantity(Quantity::from("100.0"))
2179            .price(Price::new(55.0, 1))
2180            .trigger_price(Price::new(35.0, 1))
2181            .build();
2182
2183        let order2 = OrderTestBuilder::new(OrderType::StopMarket)
2184            .instrument_id(instrument_btcusdt.id())
2185            .side(OrderSide::Buy)
2186            .quantity(Quantity::from("1000.0"))
2187            .price(Price::new(45.0, 1))
2188            .trigger_price(Price::new(30.0, 1))
2189            .build();
2190
2191        portfolio
2192            .cache
2193            .borrow_mut()
2194            .add_order(order1.clone(), None, None, true)
2195            .unwrap();
2196
2197        portfolio
2198            .cache
2199            .borrow_mut()
2200            .add_order(order2, None, None, true)
2201            .unwrap();
2202
2203        let order_submitted = submit_order(&order1);
2204        order1
2205            .apply(OrderEventAny::Submitted(order_submitted))
2206            .unwrap();
2207        portfolio.cache.borrow_mut().update_order(&order1).unwrap();
2208
2209        // Push status to Accepted
2210        let order_accepted = accept_order(&order1);
2211        order1
2212            .apply(OrderEventAny::Accepted(order_accepted))
2213            .unwrap();
2214        portfolio.cache.borrow_mut().update_order(&order1).unwrap();
2215
2216        // TODO: Replace with Execution Engine once implemented.
2217        portfolio
2218            .cache
2219            .borrow_mut()
2220            .add_order(order1.clone(), None, None, true)
2221            .unwrap();
2222
2223        let order_filled1 = fill_order(&order1);
2224        order1.apply(OrderEventAny::Filled(order_filled1)).unwrap();
2225
2226        // Act
2227        let last = get_quote_tick(&instrument_btcusdt, 25001.0, 25002.0, 15.0, 12.0);
2228        portfolio.update_quote_tick(&last);
2229        portfolio.initialize_orders();
2230
2231        // Assert
2232        assert_eq!(
2233            portfolio
2234                .margins_init(&Venue::from("BINANCE"))
2235                .get(&instrument_btcusdt.id())
2236                .unwrap()
2237                .as_f64(),
2238            10.5
2239        );
2240    }
2241
2242    #[rstest]
2243    fn test_order_accept_updates_margin_init(
2244        mut portfolio: Portfolio,
2245        instrument_btcusdt: InstrumentAny,
2246    ) {
2247        let account_state = get_margin_account(Some("BINANCE-01234"));
2248        portfolio.update_account(&account_state);
2249
2250        // Create Order
2251        let mut order = OrderTestBuilder::new(OrderType::Limit)
2252            .client_order_id(ClientOrderId::new("55"))
2253            .instrument_id(instrument_btcusdt.id())
2254            .side(OrderSide::Buy)
2255            .quantity(Quantity::from("100.0"))
2256            .price(Price::new(5.0, 0))
2257            .build();
2258
2259        portfolio
2260            .cache
2261            .borrow_mut()
2262            .add_order(order.clone(), None, None, true)
2263            .unwrap();
2264
2265        let order_submitted = submit_order(&order);
2266        order
2267            .apply(OrderEventAny::Submitted(order_submitted))
2268            .unwrap();
2269        portfolio.cache.borrow_mut().update_order(&order).unwrap();
2270
2271        let order_accepted = accept_order(&order);
2272        order
2273            .apply(OrderEventAny::Accepted(order_accepted))
2274            .unwrap();
2275        portfolio.cache.borrow_mut().update_order(&order).unwrap();
2276
2277        // TODO: Replace with Execution Engine once implemented.
2278        portfolio
2279            .cache
2280            .borrow_mut()
2281            .add_order(order.clone(), None, None, true)
2282            .unwrap();
2283
2284        // Act
2285        portfolio.initialize_orders();
2286
2287        // Assert
2288        assert_eq!(
2289            portfolio
2290                .margins_init(&Venue::from("BINANCE"))
2291                .get(&instrument_btcusdt.id())
2292                .unwrap()
2293                .as_f64(),
2294            1.5
2295        );
2296    }
2297
2298    #[rstest]
2299    fn test_update_positions(mut portfolio: Portfolio, instrument_audusd: InstrumentAny) {
2300        let account_state = get_cash_account(None);
2301        portfolio.update_account(&account_state);
2302
2303        // Create Order
2304        let mut order1 = OrderTestBuilder::new(OrderType::Market)
2305            .instrument_id(instrument_audusd.id())
2306            .side(OrderSide::Buy)
2307            .quantity(Quantity::from("10.50"))
2308            .build();
2309
2310        let order2 = OrderTestBuilder::new(OrderType::Market)
2311            .instrument_id(instrument_audusd.id())
2312            .side(OrderSide::Sell)
2313            .quantity(Quantity::from("10.50"))
2314            .build();
2315
2316        portfolio
2317            .cache
2318            .borrow_mut()
2319            .add_order(order1.clone(), None, None, true)
2320            .unwrap();
2321        portfolio
2322            .cache
2323            .borrow_mut()
2324            .add_order(order2.clone(), None, None, true)
2325            .unwrap();
2326
2327        let order1_submitted = submit_order(&order1);
2328        order1
2329            .apply(OrderEventAny::Submitted(order1_submitted))
2330            .unwrap();
2331        portfolio.update_order(&OrderEventAny::Submitted(order1_submitted));
2332
2333        // ACCEPTED
2334        let order1_accepted = accept_order(&order1);
2335        order1
2336            .apply(OrderEventAny::Accepted(order1_accepted))
2337            .unwrap();
2338        portfolio.update_order(&OrderEventAny::Accepted(order1_accepted));
2339
2340        let mut fill1 = fill_order(&order1);
2341        fill1.position_id = Some(PositionId::new("SSD"));
2342
2343        let mut fill2 = fill_order(&order2);
2344        fill2.trade_id = TradeId::new("2");
2345
2346        let mut position1 = Position::new(&instrument_audusd, fill1);
2347        position1.apply(&fill2);
2348
2349        let order3 = OrderTestBuilder::new(OrderType::Market)
2350            .instrument_id(instrument_audusd.id())
2351            .side(OrderSide::Sell)
2352            .quantity(Quantity::from("10.00"))
2353            .build();
2354
2355        let mut fill3 = fill_order(&order3);
2356        fill3.position_id = Some(PositionId::new("SSsD"));
2357
2358        let position2 = Position::new(&instrument_audusd, fill3);
2359
2360        // Update the last quote
2361        let last = get_quote_tick(&instrument_audusd, 250001.0, 250002.0, 1.0, 1.0);
2362
2363        // Act
2364        portfolio
2365            .cache
2366            .borrow_mut()
2367            .add_position(position1, OmsType::Hedging)
2368            .unwrap();
2369        portfolio
2370            .cache
2371            .borrow_mut()
2372            .add_position(position2, OmsType::Hedging)
2373            .unwrap();
2374        portfolio.cache.borrow_mut().add_quote(last).unwrap();
2375        portfolio.update_quote_tick(&last);
2376        portfolio.initialize_positions();
2377
2378        // Assert
2379        assert!(portfolio.is_net_long(&instrument_audusd.id()));
2380    }
2381
2382    #[rstest]
2383    fn test_opening_one_long_position_updates_portfolio(
2384        mut portfolio: Portfolio,
2385        instrument_audusd: InstrumentAny,
2386    ) {
2387        let account_state = get_margin_account(None);
2388        portfolio.update_account(&account_state);
2389
2390        // Create Order
2391        let order = OrderTestBuilder::new(OrderType::Market)
2392            .instrument_id(instrument_audusd.id())
2393            .side(OrderSide::Buy)
2394            .quantity(Quantity::from("10.00"))
2395            .build();
2396
2397        let mut fill = fill_order(&order);
2398        fill.position_id = Some(PositionId::new("SSD"));
2399
2400        // Update the last quote
2401        let last = get_quote_tick(&instrument_audusd, 10510.0, 10511.0, 1.0, 1.0);
2402        portfolio.cache.borrow_mut().add_quote(last).unwrap();
2403        portfolio.update_quote_tick(&last);
2404
2405        let position = Position::new(&instrument_audusd, fill);
2406
2407        // Act
2408        portfolio
2409            .cache
2410            .borrow_mut()
2411            .add_position(position.clone(), OmsType::Hedging)
2412            .unwrap();
2413
2414        let position_opened = get_open_position(&position);
2415        portfolio.update_position(&PositionEvent::PositionOpened(position_opened));
2416
2417        // Assert
2418        assert_eq!(
2419            portfolio
2420                .net_exposures(&Venue::from("SIM"))
2421                .unwrap()
2422                .get(&Currency::USD())
2423                .unwrap()
2424                .as_f64(),
2425            10510.0
2426        );
2427        assert_eq!(
2428            portfolio
2429                .unrealized_pnls(&Venue::from("SIM"))
2430                .get(&Currency::USD())
2431                .unwrap()
2432                .as_f64(),
2433            -6445.89
2434        );
2435        assert_eq!(
2436            portfolio
2437                .realized_pnls(&Venue::from("SIM"))
2438                .get(&Currency::USD())
2439                .unwrap()
2440                .as_f64(),
2441            0.0
2442        );
2443        assert_eq!(
2444            portfolio
2445                .net_exposure(&instrument_audusd.id())
2446                .unwrap()
2447                .as_f64(),
2448            10510.0
2449        );
2450        assert_eq!(
2451            portfolio
2452                .unrealized_pnl(&instrument_audusd.id())
2453                .unwrap()
2454                .as_f64(),
2455            -6445.89
2456        );
2457        assert_eq!(
2458            portfolio
2459                .realized_pnl(&instrument_audusd.id())
2460                .unwrap()
2461                .as_f64(),
2462            0.0
2463        );
2464        assert_eq!(
2465            portfolio.net_position(&instrument_audusd.id()),
2466            Decimal::new(561, 3)
2467        );
2468        assert!(portfolio.is_net_long(&instrument_audusd.id()));
2469        assert!(!portfolio.is_net_short(&instrument_audusd.id()));
2470        assert!(!portfolio.is_flat(&instrument_audusd.id()));
2471        assert!(!portfolio.is_completely_flat());
2472    }
2473
2474    #[rstest]
2475    fn test_opening_one_long_position_updates_portfolio_with_bar(
2476        mut portfolio: Portfolio,
2477        instrument_audusd: InstrumentAny,
2478    ) {
2479        let account_state = get_margin_account(None);
2480        portfolio.update_account(&account_state);
2481
2482        // Create Order
2483        let order = OrderTestBuilder::new(OrderType::Market)
2484            .instrument_id(instrument_audusd.id())
2485            .side(OrderSide::Buy)
2486            .quantity(Quantity::from("10.00"))
2487            .build();
2488
2489        let mut fill = fill_order(&order);
2490        fill.position_id = Some(PositionId::new("SSD"));
2491
2492        // Update the last quote
2493        let last = get_bar(&instrument_audusd, 10510.0, 10510.0, 10510.0, 10510.0, 0.0);
2494        portfolio.update_bar(&last);
2495
2496        let position = Position::new(&instrument_audusd, fill);
2497
2498        // Act
2499        portfolio
2500            .cache
2501            .borrow_mut()
2502            .add_position(position.clone(), OmsType::Hedging)
2503            .unwrap();
2504
2505        let position_opened = get_open_position(&position);
2506        portfolio.update_position(&PositionEvent::PositionOpened(position_opened));
2507
2508        // Assert
2509        assert_eq!(
2510            portfolio
2511                .net_exposures(&Venue::from("SIM"))
2512                .unwrap()
2513                .get(&Currency::USD())
2514                .unwrap()
2515                .as_f64(),
2516            10510.0
2517        );
2518        assert_eq!(
2519            portfolio
2520                .unrealized_pnls(&Venue::from("SIM"))
2521                .get(&Currency::USD())
2522                .unwrap()
2523                .as_f64(),
2524            -6445.89
2525        );
2526        assert_eq!(
2527            portfolio
2528                .realized_pnls(&Venue::from("SIM"))
2529                .get(&Currency::USD())
2530                .unwrap()
2531                .as_f64(),
2532            0.0
2533        );
2534        assert_eq!(
2535            portfolio
2536                .net_exposure(&instrument_audusd.id())
2537                .unwrap()
2538                .as_f64(),
2539            10510.0
2540        );
2541        assert_eq!(
2542            portfolio
2543                .unrealized_pnl(&instrument_audusd.id())
2544                .unwrap()
2545                .as_f64(),
2546            -6445.89
2547        );
2548        assert_eq!(
2549            portfolio
2550                .realized_pnl(&instrument_audusd.id())
2551                .unwrap()
2552                .as_f64(),
2553            0.0
2554        );
2555        assert_eq!(
2556            portfolio.net_position(&instrument_audusd.id()),
2557            Decimal::new(561, 3)
2558        );
2559        assert!(portfolio.is_net_long(&instrument_audusd.id()));
2560        assert!(!portfolio.is_net_short(&instrument_audusd.id()));
2561        assert!(!portfolio.is_flat(&instrument_audusd.id()));
2562        assert!(!portfolio.is_completely_flat());
2563    }
2564
2565    #[rstest]
2566    fn test_opening_one_short_position_updates_portfolio(
2567        mut portfolio: Portfolio,
2568        instrument_audusd: InstrumentAny,
2569    ) {
2570        let account_state = get_margin_account(None);
2571        portfolio.update_account(&account_state);
2572
2573        // Create Order
2574        let order = OrderTestBuilder::new(OrderType::Market)
2575            .instrument_id(instrument_audusd.id())
2576            .side(OrderSide::Sell)
2577            .quantity(Quantity::from("2"))
2578            .build();
2579
2580        let fill = OrderFilled::new(
2581            order.trader_id(),
2582            order.strategy_id(),
2583            order.instrument_id(),
2584            order.client_order_id(),
2585            VenueOrderId::new("123456"),
2586            AccountId::new("SIM-001"),
2587            TradeId::new("1"),
2588            order.order_side(),
2589            order.order_type(),
2590            order.quantity(),
2591            Price::new(10.0, 0),
2592            Currency::USD(),
2593            LiquiditySide::Taker,
2594            uuid4(),
2595            UnixNanos::default(),
2596            UnixNanos::default(),
2597            false,
2598            Some(PositionId::new("SSD")),
2599            Some(Money::from("12.2 USD")),
2600        );
2601
2602        // Update the last quote
2603        let last = get_quote_tick(&instrument_audusd, 15510.15, 15510.25, 13.0, 4.0);
2604
2605        portfolio.cache.borrow_mut().add_quote(last).unwrap();
2606        portfolio.update_quote_tick(&last);
2607
2608        let position = Position::new(&instrument_audusd, fill);
2609
2610        // Act
2611        portfolio
2612            .cache
2613            .borrow_mut()
2614            .add_position(position.clone(), OmsType::Hedging)
2615            .unwrap();
2616
2617        let position_opened = get_open_position(&position);
2618        portfolio.update_position(&PositionEvent::PositionOpened(position_opened));
2619
2620        // Assert
2621        assert_eq!(
2622            portfolio
2623                .net_exposures(&Venue::from("SIM"))
2624                .unwrap()
2625                .get(&Currency::USD())
2626                .unwrap()
2627                .as_f64(),
2628            31020.0
2629        );
2630        assert_eq!(
2631            portfolio
2632                .unrealized_pnls(&Venue::from("SIM"))
2633                .get(&Currency::USD())
2634                .unwrap()
2635                .as_f64(),
2636            -31000.0
2637        );
2638        assert_eq!(
2639            portfolio
2640                .realized_pnls(&Venue::from("SIM"))
2641                .get(&Currency::USD())
2642                .unwrap()
2643                .as_f64(),
2644            -12.2
2645        );
2646        assert_eq!(
2647            portfolio
2648                .net_exposure(&instrument_audusd.id())
2649                .unwrap()
2650                .as_f64(),
2651            31020.0
2652        );
2653        assert_eq!(
2654            portfolio
2655                .unrealized_pnl(&instrument_audusd.id())
2656                .unwrap()
2657                .as_f64(),
2658            -31000.0
2659        );
2660        assert_eq!(
2661            portfolio
2662                .realized_pnl(&instrument_audusd.id())
2663                .unwrap()
2664                .as_f64(),
2665            -12.2
2666        );
2667        assert_eq!(
2668            portfolio.net_position(&instrument_audusd.id()),
2669            Decimal::new(-2, 0)
2670        );
2671
2672        assert!(!portfolio.is_net_long(&instrument_audusd.id()));
2673        assert!(portfolio.is_net_short(&instrument_audusd.id()));
2674        assert!(!portfolio.is_flat(&instrument_audusd.id()));
2675        assert!(!portfolio.is_completely_flat());
2676    }
2677
2678    #[rstest]
2679    fn test_opening_positions_with_multi_asset_account(
2680        mut portfolio: Portfolio,
2681        instrument_btcusdt: InstrumentAny,
2682        instrument_ethusdt: InstrumentAny,
2683    ) {
2684        let account_state = get_margin_account(Some("BITMEX-01234"));
2685        portfolio.update_account(&account_state);
2686
2687        let last_ethusd = get_quote_tick(&instrument_ethusdt, 376.05, 377.10, 16.0, 25.0);
2688        let last_btcusd = get_quote_tick(&instrument_btcusdt, 10500.05, 10501.51, 2.54, 0.91);
2689
2690        portfolio.cache.borrow_mut().add_quote(last_ethusd).unwrap();
2691        portfolio.cache.borrow_mut().add_quote(last_btcusd).unwrap();
2692        portfolio.update_quote_tick(&last_ethusd);
2693        portfolio.update_quote_tick(&last_btcusd);
2694
2695        // Create Order
2696        let order = OrderTestBuilder::new(OrderType::Market)
2697            .instrument_id(instrument_ethusdt.id())
2698            .side(OrderSide::Buy)
2699            .quantity(Quantity::from("10000"))
2700            .build();
2701
2702        let fill = OrderFilled::new(
2703            order.trader_id(),
2704            order.strategy_id(),
2705            order.instrument_id(),
2706            order.client_order_id(),
2707            VenueOrderId::new("123456"),
2708            AccountId::new("SIM-001"),
2709            TradeId::new("1"),
2710            order.order_side(),
2711            order.order_type(),
2712            order.quantity(),
2713            Price::new(376.0, 0),
2714            Currency::USD(),
2715            LiquiditySide::Taker,
2716            uuid4(),
2717            UnixNanos::default(),
2718            UnixNanos::default(),
2719            false,
2720            Some(PositionId::new("SSD")),
2721            Some(Money::from("12.2 USD")),
2722        );
2723
2724        let position = Position::new(&instrument_ethusdt, fill);
2725
2726        // Act
2727        portfolio
2728            .cache
2729            .borrow_mut()
2730            .add_position(position.clone(), OmsType::Hedging)
2731            .unwrap();
2732
2733        let position_opened = get_open_position(&position);
2734        portfolio.update_position(&PositionEvent::PositionOpened(position_opened));
2735
2736        // Assert
2737        assert_eq!(
2738            portfolio
2739                .net_exposures(&Venue::from("BITMEX"))
2740                .unwrap()
2741                .get(&Currency::ETH())
2742                .unwrap()
2743                .as_f64(),
2744            26.59574468
2745        );
2746        assert_eq!(
2747            portfolio
2748                .unrealized_pnls(&Venue::from("BITMEX"))
2749                .get(&Currency::ETH())
2750                .unwrap()
2751                .as_f64(),
2752            0.0
2753        );
2754        // TODO: fix
2755        // assert_eq!(
2756        //     portfolio
2757        //         .margins_maint(&Venue::from("SIM"))
2758        //         .get(&instrument_audusd.id())
2759        //         .unwrap()
2760        //         .as_f64(),
2761        //     0.0
2762        // );
2763        assert_eq!(
2764            portfolio
2765                .net_exposure(&instrument_ethusdt.id())
2766                .unwrap()
2767                .as_f64(),
2768            26.59574468
2769        );
2770    }
2771
2772    #[rstest]
2773    fn test_market_value_when_insufficient_data_for_xrate_returns_none(
2774        mut portfolio: Portfolio,
2775        instrument_btcusdt: InstrumentAny,
2776        instrument_ethusdt: InstrumentAny,
2777    ) {
2778        let account_state = get_margin_account(Some("BITMEX-01234"));
2779        portfolio.update_account(&account_state);
2780
2781        // Create Order
2782        let order = OrderTestBuilder::new(OrderType::Market)
2783            .instrument_id(instrument_ethusdt.id())
2784            .side(OrderSide::Buy)
2785            .quantity(Quantity::from("100"))
2786            .build();
2787
2788        let fill = OrderFilled::new(
2789            order.trader_id(),
2790            order.strategy_id(),
2791            order.instrument_id(),
2792            order.client_order_id(),
2793            VenueOrderId::new("123456"),
2794            AccountId::new("SIM-001"),
2795            TradeId::new("1"),
2796            order.order_side(),
2797            order.order_type(),
2798            order.quantity(),
2799            Price::new(376.05, 0),
2800            Currency::USD(),
2801            LiquiditySide::Taker,
2802            uuid4(),
2803            UnixNanos::default(),
2804            UnixNanos::default(),
2805            false,
2806            Some(PositionId::new("SSD")),
2807            Some(Money::from("12.2 USD")),
2808        );
2809
2810        let last_ethusd = get_quote_tick(&instrument_ethusdt, 376.05, 377.10, 16.0, 25.0);
2811        let last_xbtusd = get_quote_tick(&instrument_btcusdt, 50000.00, 50000.00, 1.0, 1.0);
2812
2813        let position = Position::new(&instrument_ethusdt, fill);
2814        let position_opened = get_open_position(&position);
2815
2816        // Act
2817        portfolio.update_position(&PositionEvent::PositionOpened(position_opened));
2818        portfolio
2819            .cache
2820            .borrow_mut()
2821            .add_position(position, OmsType::Hedging)
2822            .unwrap();
2823        portfolio.cache.borrow_mut().add_quote(last_ethusd).unwrap();
2824        portfolio.cache.borrow_mut().add_quote(last_xbtusd).unwrap();
2825        portfolio.update_quote_tick(&last_ethusd);
2826        portfolio.update_quote_tick(&last_xbtusd);
2827
2828        // Assert
2829        assert_eq!(
2830            portfolio
2831                .net_exposures(&Venue::from("BITMEX"))
2832                .unwrap()
2833                .get(&Currency::ETH())
2834                .unwrap()
2835                .as_f64(),
2836            0.26595745
2837        );
2838    }
2839
2840    #[rstest]
2841    fn test_opening_several_positions_updates_portfolio(
2842        mut portfolio: Portfolio,
2843        instrument_audusd: InstrumentAny,
2844        instrument_gbpusd: InstrumentAny,
2845    ) {
2846        let account_state = get_margin_account(None);
2847        portfolio.update_account(&account_state);
2848
2849        let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0);
2850        let last_gbpusd = get_quote_tick(&instrument_gbpusd, 1.30315, 1.30317, 1.0, 1.0);
2851
2852        portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap();
2853        portfolio.cache.borrow_mut().add_quote(last_gbpusd).unwrap();
2854        portfolio.update_quote_tick(&last_audusd);
2855        portfolio.update_quote_tick(&last_gbpusd);
2856
2857        // Create Order
2858        let order1 = OrderTestBuilder::new(OrderType::Market)
2859            .instrument_id(instrument_audusd.id())
2860            .side(OrderSide::Buy)
2861            .quantity(Quantity::from("100000"))
2862            .build();
2863
2864        let order2 = OrderTestBuilder::new(OrderType::Market)
2865            .instrument_id(instrument_gbpusd.id())
2866            .side(OrderSide::Buy)
2867            .quantity(Quantity::from("100000"))
2868            .build();
2869
2870        portfolio
2871            .cache
2872            .borrow_mut()
2873            .add_order(order1.clone(), None, None, true)
2874            .unwrap();
2875        portfolio
2876            .cache
2877            .borrow_mut()
2878            .add_order(order2.clone(), None, None, true)
2879            .unwrap();
2880
2881        let fill1 = OrderFilled::new(
2882            order1.trader_id(),
2883            order1.strategy_id(),
2884            order1.instrument_id(),
2885            order1.client_order_id(),
2886            VenueOrderId::new("123456"),
2887            AccountId::new("SIM-001"),
2888            TradeId::new("1"),
2889            order1.order_side(),
2890            order1.order_type(),
2891            order1.quantity(),
2892            Price::new(376.05, 0),
2893            Currency::USD(),
2894            LiquiditySide::Taker,
2895            uuid4(),
2896            UnixNanos::default(),
2897            UnixNanos::default(),
2898            false,
2899            Some(PositionId::new("SSD")),
2900            Some(Money::from("12.2 USD")),
2901        );
2902        let fill2 = OrderFilled::new(
2903            order2.trader_id(),
2904            order2.strategy_id(),
2905            order2.instrument_id(),
2906            order2.client_order_id(),
2907            VenueOrderId::new("123456"),
2908            AccountId::new("SIM-001"),
2909            TradeId::new("1"),
2910            order2.order_side(),
2911            order2.order_type(),
2912            order2.quantity(),
2913            Price::new(376.05, 0),
2914            Currency::USD(),
2915            LiquiditySide::Taker,
2916            uuid4(),
2917            UnixNanos::default(),
2918            UnixNanos::default(),
2919            false,
2920            Some(PositionId::new("SSD")),
2921            Some(Money::from("12.2 USD")),
2922        );
2923
2924        portfolio.cache.borrow_mut().update_order(&order1).unwrap();
2925        portfolio.cache.borrow_mut().update_order(&order2).unwrap();
2926
2927        let position1 = Position::new(&instrument_audusd, fill1);
2928        let position2 = Position::new(&instrument_gbpusd, fill2);
2929
2930        let position_opened1 = get_open_position(&position1);
2931        let position_opened2 = get_open_position(&position2);
2932
2933        // Act
2934        portfolio
2935            .cache
2936            .borrow_mut()
2937            .add_position(position1, OmsType::Hedging)
2938            .unwrap();
2939        portfolio
2940            .cache
2941            .borrow_mut()
2942            .add_position(position2, OmsType::Hedging)
2943            .unwrap();
2944        portfolio.update_position(&PositionEvent::PositionOpened(position_opened1));
2945        portfolio.update_position(&PositionEvent::PositionOpened(position_opened2));
2946
2947        // Assert
2948        assert_eq!(
2949            portfolio
2950                .net_exposures(&Venue::from("SIM"))
2951                .unwrap()
2952                .get(&Currency::USD())
2953                .unwrap()
2954                .as_f64(),
2955            100000.0
2956        );
2957
2958        assert_eq!(
2959            portfolio
2960                .unrealized_pnls(&Venue::from("SIM"))
2961                .get(&Currency::USD())
2962                .unwrap()
2963                .as_f64(),
2964            -37500000.0
2965        );
2966
2967        assert_eq!(
2968            portfolio
2969                .realized_pnls(&Venue::from("SIM"))
2970                .get(&Currency::USD())
2971                .unwrap()
2972                .as_f64(),
2973            -12.2
2974        );
2975        // FIX: TODO: should not be empty
2976        assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new());
2977        assert_eq!(
2978            portfolio
2979                .net_exposure(&instrument_audusd.id())
2980                .unwrap()
2981                .as_f64(),
2982            100000.0
2983        );
2984        assert_eq!(
2985            portfolio
2986                .net_exposure(&instrument_gbpusd.id())
2987                .unwrap()
2988                .as_f64(),
2989            100000.0
2990        );
2991        assert_eq!(
2992            portfolio
2993                .unrealized_pnl(&instrument_audusd.id())
2994                .unwrap()
2995                .as_f64(),
2996            0.0
2997        );
2998        assert_eq!(
2999            portfolio
3000                .unrealized_pnl(&instrument_gbpusd.id())
3001                .unwrap()
3002                .as_f64(),
3003            -37500000.0
3004        );
3005        assert_eq!(
3006            portfolio
3007                .realized_pnl(&instrument_audusd.id())
3008                .unwrap()
3009                .as_f64(),
3010            0.0
3011        );
3012        assert_eq!(
3013            portfolio
3014                .realized_pnl(&instrument_gbpusd.id())
3015                .unwrap()
3016                .as_f64(),
3017            -12.2
3018        );
3019        assert_eq!(
3020            portfolio.net_position(&instrument_audusd.id()),
3021            Decimal::from_f64(100000.0).unwrap()
3022        );
3023        assert_eq!(
3024            portfolio.net_position(&instrument_gbpusd.id()),
3025            Decimal::from_f64(100000.0).unwrap()
3026        );
3027        assert!(portfolio.is_net_long(&instrument_audusd.id()));
3028        assert!(!portfolio.is_net_short(&instrument_audusd.id()));
3029        assert!(!portfolio.is_flat(&instrument_audusd.id()));
3030        assert!(!portfolio.is_completely_flat());
3031    }
3032
3033    #[rstest]
3034    fn test_modifying_position_updates_portfolio(
3035        mut portfolio: Portfolio,
3036        instrument_audusd: InstrumentAny,
3037    ) {
3038        let account_state = get_margin_account(None);
3039        portfolio.update_account(&account_state);
3040
3041        let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0);
3042        portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap();
3043        portfolio.update_quote_tick(&last_audusd);
3044
3045        // Create Order
3046        let order1 = OrderTestBuilder::new(OrderType::Market)
3047            .instrument_id(instrument_audusd.id())
3048            .side(OrderSide::Buy)
3049            .quantity(Quantity::from("100000"))
3050            .build();
3051
3052        let fill1 = OrderFilled::new(
3053            order1.trader_id(),
3054            order1.strategy_id(),
3055            order1.instrument_id(),
3056            order1.client_order_id(),
3057            VenueOrderId::new("123456"),
3058            AccountId::new("SIM-001"),
3059            TradeId::new("1"),
3060            order1.order_side(),
3061            order1.order_type(),
3062            order1.quantity(),
3063            Price::new(376.05, 0),
3064            Currency::USD(),
3065            LiquiditySide::Taker,
3066            uuid4(),
3067            UnixNanos::default(),
3068            UnixNanos::default(),
3069            false,
3070            Some(PositionId::new("SSD")),
3071            Some(Money::from("12.2 USD")),
3072        );
3073
3074        let mut position1 = Position::new(&instrument_audusd, fill1);
3075        portfolio
3076            .cache
3077            .borrow_mut()
3078            .add_position(position1.clone(), OmsType::Hedging)
3079            .unwrap();
3080        let position_opened1 = get_open_position(&position1);
3081        portfolio.update_position(&PositionEvent::PositionOpened(position_opened1));
3082
3083        let order2 = OrderTestBuilder::new(OrderType::Market)
3084            .instrument_id(instrument_audusd.id())
3085            .side(OrderSide::Sell)
3086            .quantity(Quantity::from("50000"))
3087            .build();
3088
3089        let fill2 = OrderFilled::new(
3090            order2.trader_id(),
3091            order2.strategy_id(),
3092            order2.instrument_id(),
3093            order2.client_order_id(),
3094            VenueOrderId::new("123456"),
3095            AccountId::new("SIM-001"),
3096            TradeId::new("2"),
3097            order2.order_side(),
3098            order2.order_type(),
3099            order2.quantity(),
3100            Price::new(1.00, 0),
3101            Currency::USD(),
3102            LiquiditySide::Taker,
3103            uuid4(),
3104            UnixNanos::default(),
3105            UnixNanos::default(),
3106            false,
3107            Some(PositionId::new("SSD")),
3108            Some(Money::from("1.2 USD")),
3109        );
3110
3111        position1.apply(&fill2);
3112        let position1_changed = get_changed_position(&position1);
3113
3114        // Act
3115        portfolio.update_position(&PositionEvent::PositionChanged(position1_changed));
3116
3117        // Assert
3118        assert_eq!(
3119            portfolio
3120                .net_exposures(&Venue::from("SIM"))
3121                .unwrap()
3122                .get(&Currency::USD())
3123                .unwrap()
3124                .as_f64(),
3125            100000.0
3126        );
3127
3128        assert_eq!(
3129            portfolio
3130                .unrealized_pnls(&Venue::from("SIM"))
3131                .get(&Currency::USD())
3132                .unwrap()
3133                .as_f64(),
3134            -37500000.0
3135        );
3136
3137        assert_eq!(
3138            portfolio
3139                .realized_pnls(&Venue::from("SIM"))
3140                .get(&Currency::USD())
3141                .unwrap()
3142                .as_f64(),
3143            -12.2
3144        );
3145        // FIX: TODO: should not be empty
3146        assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new());
3147        assert_eq!(
3148            portfolio
3149                .net_exposure(&instrument_audusd.id())
3150                .unwrap()
3151                .as_f64(),
3152            100000.0
3153        );
3154        assert_eq!(
3155            portfolio
3156                .unrealized_pnl(&instrument_audusd.id())
3157                .unwrap()
3158                .as_f64(),
3159            -37500000.0
3160        );
3161        assert_eq!(
3162            portfolio
3163                .realized_pnl(&instrument_audusd.id())
3164                .unwrap()
3165                .as_f64(),
3166            -12.2
3167        );
3168        assert_eq!(
3169            portfolio.net_position(&instrument_audusd.id()),
3170            Decimal::from_f64(100000.0).unwrap()
3171        );
3172        assert!(portfolio.is_net_long(&instrument_audusd.id()));
3173        assert!(!portfolio.is_net_short(&instrument_audusd.id()));
3174        assert!(!portfolio.is_flat(&instrument_audusd.id()));
3175        assert!(!portfolio.is_completely_flat());
3176        assert_eq!(
3177            portfolio.unrealized_pnls(&Venue::from("BINANCE")),
3178            HashMap::new()
3179        );
3180        assert_eq!(
3181            portfolio.realized_pnls(&Venue::from("BINANCE")),
3182            HashMap::new()
3183        );
3184        assert_eq!(portfolio.net_exposures(&Venue::from("BINANCE")), None);
3185    }
3186
3187    #[rstest]
3188    fn test_closing_position_updates_portfolio(
3189        mut portfolio: Portfolio,
3190        instrument_audusd: InstrumentAny,
3191    ) {
3192        let account_state = get_margin_account(None);
3193        portfolio.update_account(&account_state);
3194
3195        let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0);
3196        portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap();
3197        portfolio.update_quote_tick(&last_audusd);
3198
3199        // Create Order
3200        let order1 = OrderTestBuilder::new(OrderType::Market)
3201            .instrument_id(instrument_audusd.id())
3202            .side(OrderSide::Buy)
3203            .quantity(Quantity::from("100000"))
3204            .build();
3205
3206        let fill1 = OrderFilled::new(
3207            order1.trader_id(),
3208            order1.strategy_id(),
3209            order1.instrument_id(),
3210            order1.client_order_id(),
3211            VenueOrderId::new("123456"),
3212            AccountId::new("SIM-001"),
3213            TradeId::new("1"),
3214            order1.order_side(),
3215            order1.order_type(),
3216            order1.quantity(),
3217            Price::new(376.05, 0),
3218            Currency::USD(),
3219            LiquiditySide::Taker,
3220            uuid4(),
3221            UnixNanos::default(),
3222            UnixNanos::default(),
3223            false,
3224            Some(PositionId::new("SSD")),
3225            Some(Money::from("12.2 USD")),
3226        );
3227
3228        let mut position1 = Position::new(&instrument_audusd, fill1);
3229        portfolio
3230            .cache
3231            .borrow_mut()
3232            .add_position(position1.clone(), OmsType::Hedging)
3233            .unwrap();
3234        let position_opened1 = get_open_position(&position1);
3235        portfolio.update_position(&PositionEvent::PositionOpened(position_opened1));
3236
3237        let order2 = OrderTestBuilder::new(OrderType::Market)
3238            .instrument_id(instrument_audusd.id())
3239            .side(OrderSide::Sell)
3240            .quantity(Quantity::from("50000"))
3241            .build();
3242
3243        let fill2 = OrderFilled::new(
3244            order2.trader_id(),
3245            order2.strategy_id(),
3246            order2.instrument_id(),
3247            order2.client_order_id(),
3248            VenueOrderId::new("123456"),
3249            AccountId::new("SIM-001"),
3250            TradeId::new("2"),
3251            order2.order_side(),
3252            order2.order_type(),
3253            order2.quantity(),
3254            Price::new(1.00, 0),
3255            Currency::USD(),
3256            LiquiditySide::Taker,
3257            uuid4(),
3258            UnixNanos::default(),
3259            UnixNanos::default(),
3260            false,
3261            Some(PositionId::new("SSD")),
3262            Some(Money::from("1.2 USD")),
3263        );
3264
3265        position1.apply(&fill2);
3266        portfolio
3267            .cache
3268            .borrow_mut()
3269            .update_position(&position1)
3270            .unwrap();
3271
3272        // Act
3273        let position1_closed = get_close_position(&position1);
3274        portfolio.update_position(&PositionEvent::PositionClosed(position1_closed));
3275
3276        // Assert
3277        assert_eq!(
3278            portfolio
3279                .net_exposures(&Venue::from("SIM"))
3280                .unwrap()
3281                .get(&Currency::USD())
3282                .unwrap()
3283                .as_f64(),
3284            100000.00
3285        );
3286        assert_eq!(
3287            portfolio
3288                .unrealized_pnls(&Venue::from("SIM"))
3289                .get(&Currency::USD())
3290                .unwrap()
3291                .as_f64(),
3292            -37500000.00
3293        );
3294        assert_eq!(
3295            portfolio
3296                .realized_pnls(&Venue::from("SIM"))
3297                .get(&Currency::USD())
3298                .unwrap()
3299                .as_f64(),
3300            -12.2
3301        );
3302        assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new());
3303    }
3304
3305    #[rstest]
3306    fn test_several_positions_with_different_instruments_updates_portfolio(
3307        mut portfolio: Portfolio,
3308        instrument_audusd: InstrumentAny,
3309        instrument_gbpusd: InstrumentAny,
3310    ) {
3311        let account_state = get_margin_account(None);
3312        portfolio.update_account(&account_state);
3313
3314        // Create Order
3315        let order1 = OrderTestBuilder::new(OrderType::Market)
3316            .instrument_id(instrument_audusd.id())
3317            .side(OrderSide::Buy)
3318            .quantity(Quantity::from("100000"))
3319            .build();
3320        let order2 = OrderTestBuilder::new(OrderType::Market)
3321            .instrument_id(instrument_audusd.id())
3322            .side(OrderSide::Buy)
3323            .quantity(Quantity::from("100000"))
3324            .build();
3325        let order3 = OrderTestBuilder::new(OrderType::Market)
3326            .instrument_id(instrument_gbpusd.id())
3327            .side(OrderSide::Buy)
3328            .quantity(Quantity::from("100000"))
3329            .build();
3330        let order4 = OrderTestBuilder::new(OrderType::Market)
3331            .instrument_id(instrument_gbpusd.id())
3332            .side(OrderSide::Sell)
3333            .quantity(Quantity::from("100000"))
3334            .build();
3335
3336        let fill1 = OrderFilled::new(
3337            order1.trader_id(),
3338            StrategyId::new("S-1"),
3339            order1.instrument_id(),
3340            order1.client_order_id(),
3341            VenueOrderId::new("123456"),
3342            AccountId::new("SIM-001"),
3343            TradeId::new("1"),
3344            order1.order_side(),
3345            order1.order_type(),
3346            order1.quantity(),
3347            Price::new(1.0, 0),
3348            Currency::USD(),
3349            LiquiditySide::Taker,
3350            uuid4(),
3351            UnixNanos::default(),
3352            UnixNanos::default(),
3353            false,
3354            Some(PositionId::new("P-1")),
3355            None,
3356        );
3357        let fill2 = OrderFilled::new(
3358            order2.trader_id(),
3359            StrategyId::new("S-1"),
3360            order2.instrument_id(),
3361            order2.client_order_id(),
3362            VenueOrderId::new("123456"),
3363            AccountId::new("SIM-001"),
3364            TradeId::new("2"),
3365            order2.order_side(),
3366            order2.order_type(),
3367            order2.quantity(),
3368            Price::new(1.0, 0),
3369            Currency::USD(),
3370            LiquiditySide::Taker,
3371            uuid4(),
3372            UnixNanos::default(),
3373            UnixNanos::default(),
3374            false,
3375            Some(PositionId::new("P-2")),
3376            None,
3377        );
3378        let fill3 = OrderFilled::new(
3379            order3.trader_id(),
3380            StrategyId::new("S-1"),
3381            order3.instrument_id(),
3382            order3.client_order_id(),
3383            VenueOrderId::new("123456"),
3384            AccountId::new("SIM-001"),
3385            TradeId::new("3"),
3386            order3.order_side(),
3387            order3.order_type(),
3388            order3.quantity(),
3389            Price::new(1.0, 0),
3390            Currency::USD(),
3391            LiquiditySide::Taker,
3392            uuid4(),
3393            UnixNanos::default(),
3394            UnixNanos::default(),
3395            false,
3396            Some(PositionId::new("P-3")),
3397            None,
3398        );
3399        let fill4 = OrderFilled::new(
3400            order4.trader_id(),
3401            StrategyId::new("S-1"),
3402            order4.instrument_id(),
3403            order4.client_order_id(),
3404            VenueOrderId::new("123456"),
3405            AccountId::new("SIM-001"),
3406            TradeId::new("4"),
3407            order4.order_side(),
3408            order4.order_type(),
3409            order4.quantity(),
3410            Price::new(1.0, 0),
3411            Currency::USD(),
3412            LiquiditySide::Taker,
3413            uuid4(),
3414            UnixNanos::default(),
3415            UnixNanos::default(),
3416            false,
3417            Some(PositionId::new("P-4")),
3418            None,
3419        );
3420
3421        let position1 = Position::new(&instrument_audusd, fill1);
3422        let position2 = Position::new(&instrument_audusd, fill2);
3423        let mut position3 = Position::new(&instrument_gbpusd, fill3);
3424
3425        let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0);
3426        let last_gbpusd = get_quote_tick(&instrument_gbpusd, 1.30315, 1.30317, 1.0, 1.0);
3427
3428        portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap();
3429        portfolio.cache.borrow_mut().add_quote(last_gbpusd).unwrap();
3430        portfolio.update_quote_tick(&last_audusd);
3431        portfolio.update_quote_tick(&last_gbpusd);
3432
3433        portfolio
3434            .cache
3435            .borrow_mut()
3436            .add_position(position1.clone(), OmsType::Hedging)
3437            .unwrap();
3438        portfolio
3439            .cache
3440            .borrow_mut()
3441            .add_position(position2.clone(), OmsType::Hedging)
3442            .unwrap();
3443        portfolio
3444            .cache
3445            .borrow_mut()
3446            .add_position(position3.clone(), OmsType::Hedging)
3447            .unwrap();
3448
3449        let position_opened1 = get_open_position(&position1);
3450        let position_opened2 = get_open_position(&position2);
3451        let position_opened3 = get_open_position(&position3);
3452
3453        portfolio.update_position(&PositionEvent::PositionOpened(position_opened1));
3454        portfolio.update_position(&PositionEvent::PositionOpened(position_opened2));
3455        portfolio.update_position(&PositionEvent::PositionOpened(position_opened3));
3456
3457        let position_closed3 = get_close_position(&position3);
3458        position3.apply(&fill4);
3459        portfolio
3460            .cache
3461            .borrow_mut()
3462            .add_position(position3.clone(), OmsType::Hedging)
3463            .unwrap();
3464        portfolio.update_position(&PositionEvent::PositionClosed(position_closed3));
3465
3466        // Assert
3467        assert_eq!(
3468            portfolio
3469                .net_exposures(&Venue::from("SIM"))
3470                .unwrap()
3471                .get(&Currency::USD())
3472                .unwrap()
3473                .as_f64(),
3474            200000.00
3475        );
3476        assert_eq!(
3477            portfolio
3478                .unrealized_pnls(&Venue::from("SIM"))
3479                .get(&Currency::USD())
3480                .unwrap()
3481                .as_f64(),
3482            0.0
3483        );
3484        assert_eq!(
3485            portfolio
3486                .realized_pnls(&Venue::from("SIM"))
3487                .get(&Currency::USD())
3488                .unwrap()
3489                .as_f64(),
3490            0.0
3491        );
3492        // FIX: TODO: should not be empty
3493        assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new());
3494    }
3495}