1use std::{cell::RefCell, collections::VecDeque, fmt::Debug, rc::Rc};
19
20use ahash::{AHashMap, AHashSet};
21use indexmap::{IndexMap, IndexSet};
22use nautilus_analysis::{analyzer::PortfolioAnalyzer, snapshot::PortfolioStatistics};
23use nautilus_common::{
24 cache::{AccountLookupError, Cache},
25 clock::Clock,
26 enums::LogColor,
27 msgbus::{self, MessagingSwitchboard, TypedHandler, TypedIntoHandler},
28 timer::{TimeEvent, TimeEventCallback},
29};
30use nautilus_core::{UUID4, UnixNanos, WeakCell, datetime::NANOSECONDS_IN_MILLISECOND};
31use nautilus_model::{
32 accounts::{Account, AccountAny},
33 data::{Bar, MarkPriceUpdate, QuoteTick},
34 enums::{OmsType, OrderType, PositionSide, PriceType},
35 events::{AccountState, OrderEventAny, PortfolioSnapshot, position::PositionEvent},
36 identifiers::{AccountId, InstrumentId, PositionId, Venue},
37 instruments::{Instrument, InstrumentAny},
38 orders::{Order, OrderAny},
39 position::Position,
40 types::{AccountBalance, Currency, MarginBalance, Money, Price},
41};
42use rust_decimal::Decimal;
43
44use crate::{config::PortfolioConfig, manager::AccountsManager};
45
46const SNAPSHOT_BUFFER_CAP: usize = 1_000_000;
50
51struct PortfolioState {
52 accounts: AccountsManager,
53 analyzer: PortfolioAnalyzer,
54 unrealized_pnls: IndexMap<InstrumentId, Money>,
55 realized_pnls: IndexMap<InstrumentId, Money>,
56 recorded_closed_position_cycles: AHashSet<(PositionId, UnixNanos)>,
57 snapshot_sum_per_position: AHashMap<PositionId, Money>,
58 snapshot_last_per_position: AHashMap<PositionId, Money>,
59 snapshot_processed_counts: AHashMap<PositionId, usize>,
60 snapshot_account_ids: AHashMap<PositionId, AccountId>,
61 net_positions: IndexMap<InstrumentId, Decimal>,
62 pending_calcs: AHashSet<InstrumentId>,
63 bar_close_prices: AHashMap<InstrumentId, Price>,
64 initialized: bool,
65 last_account_state_log_ts: AHashMap<AccountId, u64>,
66 min_account_state_logging_interval_ns: u64,
67 venues_missing_price: AHashMap<Venue, AHashSet<InstrumentId>>,
68 account_open_positions: AHashMap<AccountId, usize>,
69 portfolio_snapshots: AHashMap<AccountId, VecDeque<PortfolioSnapshot>>,
70 pre_position_fill_events: AHashSet<UUID4>,
71}
72
73#[derive(Clone, Copy)]
74enum OrderUpdateSource {
75 Endpoint,
76 Topic,
77}
78
79impl PortfolioState {
80 fn new(
81 clock: Rc<RefCell<dyn Clock>>,
82 cache: Rc<RefCell<Cache>>,
83 config: &PortfolioConfig,
84 ) -> Self {
85 let min_account_state_logging_interval_ns = config
86 .min_account_state_logging_interval_ms
87 .map_or(0, |ms| ms * NANOSECONDS_IN_MILLISECOND);
88
89 Self {
90 accounts: AccountsManager::new(clock, cache),
91 analyzer: PortfolioAnalyzer::default(),
92 unrealized_pnls: IndexMap::new(),
93 realized_pnls: IndexMap::new(),
94 recorded_closed_position_cycles: AHashSet::new(),
95 snapshot_sum_per_position: AHashMap::new(),
96 snapshot_last_per_position: AHashMap::new(),
97 snapshot_processed_counts: AHashMap::new(),
98 snapshot_account_ids: AHashMap::new(),
99 net_positions: IndexMap::new(),
100 pending_calcs: AHashSet::new(),
101 bar_close_prices: AHashMap::new(),
102 initialized: false,
103 last_account_state_log_ts: AHashMap::new(),
104 min_account_state_logging_interval_ns,
105 venues_missing_price: AHashMap::new(),
106 account_open_positions: AHashMap::new(),
107 portfolio_snapshots: AHashMap::new(),
108 pre_position_fill_events: AHashSet::new(),
109 }
110 }
111
112 fn reset(&mut self) {
113 log::debug!("RESETTING");
114 self.net_positions.clear();
115 self.unrealized_pnls.clear();
116 self.realized_pnls.clear();
117 self.recorded_closed_position_cycles.clear();
118 self.snapshot_sum_per_position.clear();
119 self.snapshot_last_per_position.clear();
120 self.snapshot_processed_counts.clear();
121 self.snapshot_account_ids.clear();
122 self.pending_calcs.clear();
123 self.bar_close_prices.clear();
124 self.last_account_state_log_ts.clear();
125 self.venues_missing_price.clear();
126 self.account_open_positions.clear();
127 self.portfolio_snapshots.clear();
128 self.pre_position_fill_events.clear();
129 self.analyzer.reset();
130 self.initialized = false;
131 log::debug!("READY");
132 }
133}
134
135pub struct Portfolio {
136 pub(crate) clock: Rc<RefCell<dyn Clock>>,
137 pub(crate) cache: Rc<RefCell<Cache>>,
138 inner: Rc<RefCell<PortfolioState>>,
139 config: PortfolioConfig,
140}
141
142impl Debug for Portfolio {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 f.debug_struct(stringify!(Portfolio)).finish()
145 }
146}
147
148impl Portfolio {
149 pub fn new(
150 clock: Rc<RefCell<dyn Clock>>,
151 cache: Rc<RefCell<Cache>>,
152 config: Option<PortfolioConfig>,
153 ) -> Self {
154 let config = config.unwrap_or_default();
155 let inner = Rc::new(RefCell::new(PortfolioState::new(
156 clock.clone(),
157 cache.clone(),
158 &config,
159 )));
160
161 Self::register_message_handlers(&cache, &clock, &inner, config);
162
163 Self {
164 clock,
165 cache,
166 inner,
167 config,
168 }
169 }
170
171 #[must_use]
176 pub fn clone_shallow(&self) -> Self {
177 Self {
178 clock: self.clock.clone(),
179 cache: self.cache.clone(),
180 inner: self.inner.clone(),
181 config: self.config,
182 }
183 }
184
185 fn register_message_handlers(
186 cache: &Rc<RefCell<Cache>>,
187 clock: &Rc<RefCell<dyn Clock>>,
188 inner: &Rc<RefCell<PortfolioState>>,
189 config: PortfolioConfig,
190 ) {
191 let inner_weak = WeakCell::from(Rc::downgrade(inner));
192
193 let update_account_handler = {
195 let cache = cache.clone();
196 let inner = inner_weak.clone();
197 TypedHandler::from(move |event: &AccountState| {
198 if let Some(inner_rc) = inner.upgrade() {
199 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
200 update_account(&cache, &inner_rc, event);
201 }
202 })
203 };
204
205 let update_position_handler = {
206 let cache = cache.clone();
207 let clock = clock.clone();
208 let inner = inner_weak.clone();
209 TypedHandler::from(move |event: &PositionEvent| {
210 if let Some(inner_rc) = inner.upgrade() {
211 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
212 update_position(&cache, &clock, &inner_rc, config, event);
213 }
214 })
215 };
216
217 let update_quote_handler = {
218 let cache = cache.clone();
219 let clock = clock.clone();
220 let inner = inner_weak.clone();
221 TypedHandler::from(move |quote: &QuoteTick| {
222 if let Some(inner_rc) = inner.upgrade() {
223 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
224 update_quote_tick(&cache, &clock, &inner_rc, config, quote);
225 }
226 })
227 };
228
229 let update_bar_handler = {
230 let cache = cache.clone();
231 let clock = clock.clone();
232 let inner = inner_weak.clone();
233 TypedHandler::from(move |bar: &Bar| {
234 if let Some(inner_rc) = inner.upgrade() {
235 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
236 update_bar(&cache, &clock, &inner_rc, config, bar);
237 }
238 })
239 };
240
241 let update_mark_price_handler = {
242 let cache = cache.clone();
243 let clock = clock.clone();
244 let inner = inner_weak.clone();
245 TypedHandler::from(move |mark_price: &MarkPriceUpdate| {
246 if let Some(inner_rc) = inner.upgrade() {
247 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
248 update_instrument_id(
249 &cache,
250 &clock,
251 &inner_rc,
252 config,
253 &mark_price.instrument_id,
254 );
255 }
256 })
257 };
258
259 let update_order_handler = {
260 let cache = cache.clone();
261 let inner = inner_weak.clone();
262 TypedHandler::from(move |event: &OrderEventAny| {
263 if let Some(inner_rc) = inner.upgrade() {
264 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
265 on_order_event(&cache, &inner_rc, event);
266 }
267 })
268 };
269
270 let endpoint = MessagingSwitchboard::portfolio_update_account();
271 msgbus::register_account_state_endpoint(endpoint, update_account_handler.clone());
272
273 let update_order_endpoint_handler = {
274 let cache = cache.clone();
275 let clock = clock.clone();
276 let inner = inner_weak;
277 TypedIntoHandler::from(move |event: OrderEventAny| {
278 if let Some(inner_rc) = inner.upgrade() {
279 let inner_rc: Rc<RefCell<PortfolioState>> = inner_rc.into();
280 update_order(
281 &cache,
282 &clock,
283 &inner_rc,
284 config,
285 &event,
286 OrderUpdateSource::Endpoint,
287 );
288 }
289 })
290 };
291 msgbus::register_order_event_endpoint(
292 MessagingSwitchboard::portfolio_update_order(),
293 update_order_endpoint_handler,
294 );
295
296 msgbus::subscribe_quotes("data.quotes.*".into(), update_quote_handler, Some(10));
297
298 if config.bar_updates {
299 msgbus::subscribe_bars("data.bars.*EXTERNAL".into(), update_bar_handler, Some(10));
300 }
301
302 if config.use_mark_prices {
303 msgbus::subscribe_mark_prices(
304 "data.mark_prices.*".into(),
305 update_mark_price_handler,
306 Some(10),
307 );
308 }
309 msgbus::subscribe_order_events("events.order.*".into(), update_order_handler, Some(10));
310 msgbus::subscribe_position_events(
311 "events.position.*".into(),
312 update_position_handler,
313 Some(10),
314 );
315 msgbus::subscribe_account_state(
316 "events.account.*".into(),
317 update_account_handler,
318 Some(10),
319 );
320 }
321
322 pub fn reset(&mut self) {
323 log::debug!("RESETTING");
324 let account_ids: Vec<AccountId> = self
325 .inner
326 .borrow()
327 .account_open_positions
328 .keys()
329 .copied()
330 .collect();
331
332 for account_id in account_ids {
333 self.clock
334 .borrow_mut()
335 .cancel_timer(&snapshot_timer_name(account_id));
336 }
337 self.inner.borrow_mut().reset();
338 log::debug!("READY");
339 }
340
341 #[must_use]
343 pub fn cache(&self) -> &Rc<RefCell<Cache>> {
344 &self.cache
345 }
346
347 #[must_use]
349 pub fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
350 &self.clock
351 }
352
353 #[must_use]
355 pub fn is_initialized(&self) -> bool {
356 self.inner.borrow().initialized
357 }
358
359 #[must_use]
363 pub fn balances_locked(&self, venue: &Venue) -> IndexMap<Currency, Money> {
364 self.cache.borrow().account_for_venue(venue).map_or_else(
365 || {
366 log::error!("Cannot get balances locked: no account generated for {venue}");
367 IndexMap::new()
368 },
369 |account| account.balances_locked(),
370 )
371 }
372
373 #[must_use]
377 pub fn margins_init(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
378 self.cache.borrow().account_for_venue(venue).map_or_else(
379 || {
380 log::error!(
381 "Cannot get initial (order) margins: no account registered for {venue}"
382 );
383 IndexMap::new()
384 },
385 |account| match &*account {
386 AccountAny::Margin(margin_account) => margin_account.initial_margins(),
387 AccountAny::Cash(_) | AccountAny::Betting(_) => {
388 log::warn!("Initial margins not applicable for cash account");
389 IndexMap::new()
390 }
391 },
392 )
393 }
394
395 #[must_use]
399 pub fn margins_maint(&self, venue: &Venue) -> IndexMap<InstrumentId, Money> {
400 self.cache.borrow().account_for_venue(venue).map_or_else(
401 || {
402 log::error!(
403 "Cannot get maintenance (position) margins: no account registered for {venue}"
404 );
405 IndexMap::new()
406 },
407 |account| match &*account {
408 AccountAny::Margin(margin_account) => margin_account.maintenance_margins(),
409 AccountAny::Cash(_) | AccountAny::Betting(_) => {
410 log::warn!("Maintenance margins not applicable for cash account");
411 IndexMap::new()
412 }
413 },
414 )
415 }
416
417 #[must_use]
421 pub fn unrealized_pnls(
422 &mut self,
423 venue: &Venue,
424 account_id: Option<&AccountId>,
425 ) -> IndexMap<Currency, Money> {
426 let instrument_ids = {
427 let cache = self.cache.borrow();
428 let positions = cache.positions(Some(venue), None, None, account_id, None);
429
430 if positions.is_empty() {
431 return IndexMap::new(); }
433
434 let instrument_ids: IndexSet<InstrumentId> =
438 positions.iter().map(|p| p.instrument_id).collect();
439
440 instrument_ids
441 };
442
443 let mut unrealized_pnls: IndexMap<Currency, Money> = IndexMap::new();
444
445 for instrument_id in instrument_ids {
446 if account_id.is_none()
449 && let Some(&pnl) = self.inner.borrow_mut().unrealized_pnls.get(&instrument_id)
450 {
451 unrealized_pnls
452 .entry(pnl.currency)
453 .and_modify(|total| *total = *total + pnl)
454 .or_insert(pnl);
455 continue;
456 }
457
458 if let Some(pnl) = self.calculate_unrealized_pnl(&instrument_id, account_id) {
459 unrealized_pnls
460 .entry(pnl.currency)
461 .and_modify(|total| *total = *total + pnl)
462 .or_insert(pnl);
463 }
464 }
465
466 unrealized_pnls
467 }
468
469 #[must_use]
473 pub fn realized_pnls(
474 &mut self,
475 venue: &Venue,
476 account_id: Option<&AccountId>,
477 ) -> IndexMap<Currency, Money> {
478 let instrument_ids = {
479 let cache = self.cache.borrow();
480 let positions = cache.positions(Some(venue), None, None, account_id, None);
481
482 if positions.is_empty() {
483 return IndexMap::new(); }
485
486 let instrument_ids: IndexSet<InstrumentId> =
487 positions.iter().map(|p| p.instrument_id).collect();
488
489 instrument_ids
490 };
491
492 let mut realized_pnls: IndexMap<Currency, Money> = IndexMap::new();
493
494 for instrument_id in instrument_ids {
495 if account_id.is_none()
498 && let Some(&pnl) = self.inner.borrow_mut().realized_pnls.get(&instrument_id)
499 {
500 realized_pnls
501 .entry(pnl.currency)
502 .and_modify(|total| *total = *total + pnl)
503 .or_insert(pnl);
504 continue;
505 }
506
507 if let Some(pnl) = self.calculate_realized_pnl(&instrument_id, account_id) {
508 realized_pnls
509 .entry(pnl.currency)
510 .and_modify(|total| *total = *total + pnl)
511 .or_insert(pnl);
512 }
513 }
514
515 realized_pnls
516 }
517
518 #[must_use]
519 pub fn net_exposures(
520 &self,
521 venue: &Venue,
522 account_id: Option<&AccountId>,
523 ) -> Option<IndexMap<Currency, Money>> {
524 let cache = self.cache.borrow();
525 let account = if let Some(id) = account_id {
526 if let Some(account) = cache.account(id) {
527 account
528 } else {
529 log::error!("Cannot calculate net exposures: no account for {id}");
530 return None;
531 }
532 } else if let Some(account) = cache.account_for_venue(venue) {
533 account
534 } else {
535 log::error!("Cannot calculate net exposures: no account registered for {venue}");
536 return None;
537 };
538
539 let positions_open = cache.positions_open(Some(venue), None, None, account_id, None);
540 if positions_open.is_empty() {
541 return Some(IndexMap::new()); }
543
544 let mut net_exposures: IndexMap<Currency, Money> = IndexMap::new();
545
546 for position in positions_open {
547 let instrument = if let Some(instrument) = cache.instrument(&position.instrument_id) {
548 instrument
549 } else {
550 log::error!(
551 "Cannot calculate net exposures: no instrument for {}",
552 position.instrument_id
553 );
554 return None; };
556
557 if position.side == PositionSide::Flat {
558 log::error!(
559 "Cannot calculate net exposures: position is flat for {}",
560 position.instrument_id
561 );
562 continue; }
564
565 let price = self.get_price(&position)?;
566 let xrate = if let Some(xrate) = self.calculate_xrate_to_base(instrument, &account) {
567 xrate
568 } else {
569 log::error!(
570 "Cannot calculate net exposures: insufficient data for {}/{:?}",
572 instrument.settlement_currency(),
573 account.base_currency()
574 );
575 return None; };
577
578 let settlement_currency = account
579 .base_currency()
580 .unwrap_or_else(|| instrument.settlement_currency());
581
582 let net_exposure = match Money::from_decimal(
583 instrument
584 .calculate_notional_value(position.quantity, price, None)
585 .as_decimal()
586 * xrate,
587 settlement_currency,
588 ) {
589 Ok(money) => money,
590 Err(e) => {
591 log::error!("Cannot calculate net exposures: {e}");
592 return None;
593 }
594 };
595
596 net_exposures
597 .entry(settlement_currency)
598 .and_modify(|total| *total = *total + net_exposure)
599 .or_insert(net_exposure);
600 }
601
602 Some(net_exposures)
603 }
604
605 #[must_use]
606 pub fn unrealized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
607 self.unrealized_pnl_for_account(instrument_id, None)
608 }
609
610 #[must_use]
611 pub fn unrealized_pnl_for_account(
612 &mut self,
613 instrument_id: &InstrumentId,
614 account_id: Option<&AccountId>,
615 ) -> Option<Money> {
616 if account_id.is_some() {
617 return self.calculate_unrealized_pnl(instrument_id, account_id);
618 }
619
620 if let Some(pnl) = self
621 .inner
622 .borrow()
623 .unrealized_pnls
624 .get(instrument_id)
625 .copied()
626 {
627 return Some(pnl);
628 }
629
630 let pnl = self.calculate_unrealized_pnl(instrument_id, None)?;
631 self.inner
632 .borrow_mut()
633 .unrealized_pnls
634 .insert(*instrument_id, pnl);
635 Some(pnl)
636 }
637
638 #[must_use]
639 pub fn realized_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
640 self.realized_pnl_for_account(instrument_id, None)
641 }
642
643 #[must_use]
644 pub fn realized_pnl_for_account(
645 &mut self,
646 instrument_id: &InstrumentId,
647 account_id: Option<&AccountId>,
648 ) -> Option<Money> {
649 if account_id.is_some() {
650 return self.calculate_realized_pnl(instrument_id, account_id);
651 }
652
653 if let Some(pnl) = self
654 .inner
655 .borrow()
656 .realized_pnls
657 .get(instrument_id)
658 .copied()
659 {
660 return Some(pnl);
661 }
662
663 let pnl = self.calculate_realized_pnl(instrument_id, None)?;
664 self.inner
665 .borrow_mut()
666 .realized_pnls
667 .insert(*instrument_id, pnl);
668 Some(pnl)
669 }
670
671 #[must_use]
675 pub fn total_pnl(&mut self, instrument_id: &InstrumentId) -> Option<Money> {
676 self.total_pnl_for_account(instrument_id, None)
677 }
678
679 #[must_use]
680 pub fn total_pnl_for_account(
681 &mut self,
682 instrument_id: &InstrumentId,
683 account_id: Option<&AccountId>,
684 ) -> Option<Money> {
685 let realized = self.realized_pnl_for_account(instrument_id, account_id)?;
686 let unrealized = self.unrealized_pnl_for_account(instrument_id, account_id)?;
687
688 if realized.currency != unrealized.currency {
689 log::error!(
690 "Cannot calculate total PnL: currency mismatch {} vs {}",
691 realized.currency,
692 unrealized.currency
693 );
694 return None;
695 }
696
697 Some(realized + unrealized)
698 }
699
700 #[must_use]
705 pub fn total_pnls(
706 &mut self,
707 venue: &Venue,
708 account_id: Option<&AccountId>,
709 ) -> IndexMap<Currency, Money> {
710 let realized_pnls = self.realized_pnls(venue, account_id);
711 let unrealized_pnls = self.unrealized_pnls(venue, account_id);
712
713 let mut total_pnls: IndexMap<Currency, Money> = IndexMap::new();
714
715 for (currency, realized) in realized_pnls {
717 total_pnls.insert(currency, realized);
718 }
719
720 for (currency, unrealized) in unrealized_pnls {
722 match total_pnls.get_mut(¤cy) {
723 Some(total) => {
724 *total = *total + unrealized;
725 }
726 None => {
727 total_pnls.insert(currency, unrealized);
728 }
729 }
730 }
731
732 total_pnls
733 }
734
735 #[must_use]
744 pub fn mark_values(
745 &mut self,
746 venue: &Venue,
747 account_id: Option<&AccountId>,
748 ) -> IndexMap<Currency, Money> {
749 let mut values: IndexMap<Currency, Decimal> = IndexMap::new();
750 let mut unpriced: AHashSet<InstrumentId> = AHashSet::new();
751
752 if self.accumulate_mark_values(*venue, account_id, &mut values, &mut unpriced) {
753 self.update_missing_price_state(*venue, &unpriced);
754 } else if account_id.is_none() {
755 self.inner.borrow_mut().venues_missing_price.remove(venue);
758 }
759
760 decimal_map_to_money(values)
761 }
762
763 #[must_use]
773 pub fn equity(
774 &mut self,
775 venue: &Venue,
776 account_id: Option<&AccountId>,
777 ) -> IndexMap<Currency, Money> {
778 let (mut equity, is_margin) = {
779 let cache = self.cache.borrow();
780 let account = match account_id {
781 Some(id) => cache.account(id),
782 None => cache.account_for_venue(venue),
783 };
784
785 match account {
786 Some(account) => {
787 let equity: IndexMap<Currency, Decimal> = account
788 .balances_total()
789 .into_iter()
790 .map(|(c, m)| (c, m.as_decimal()))
791 .collect();
792 (equity, matches!(&*account, AccountAny::Margin(_)))
793 }
794 None => return IndexMap::new(),
795 }
796 };
797
798 let mut unpriced: AHashSet<InstrumentId> = AHashSet::new();
799
800 if is_margin {
801 let instrument_ids: IndexSet<InstrumentId> = {
803 let cache = self.cache.borrow();
804 cache
805 .positions_open(Some(venue), None, None, account_id, None)
806 .iter()
807 .map(|p| p.instrument_id)
808 .collect()
809 };
810
811 if instrument_ids.is_empty() {
812 if account_id.is_none() {
813 self.inner.borrow_mut().venues_missing_price.remove(venue);
814 }
815 } else {
816 for instrument_id in instrument_ids {
817 let cached = if account_id.is_none() {
821 self.inner
822 .borrow()
823 .unrealized_pnls
824 .get(&instrument_id)
825 .copied()
826 } else {
827 None
828 };
829 let pnl = match cached {
830 Some(pnl) => Some(pnl),
831 None => self.calculate_unrealized_pnl(&instrument_id, account_id),
832 };
833
834 match pnl {
835 Some(pnl) => {
836 *equity.entry(pnl.currency).or_insert(Decimal::ZERO) +=
837 pnl.as_decimal();
838 }
839 None => {
840 unpriced.insert(instrument_id);
841 }
842 }
843 }
844 self.update_missing_price_state(*venue, &unpriced);
845 }
846 } else if self.accumulate_mark_values(*venue, account_id, &mut equity, &mut unpriced) {
847 self.update_missing_price_state(*venue, &unpriced);
848 } else if account_id.is_none() {
849 self.inner.borrow_mut().venues_missing_price.remove(venue);
850 }
851
852 decimal_map_to_money(equity)
853 }
854
855 #[must_use]
863 pub fn build_snapshot(&mut self, account_id: &AccountId) -> Option<PortfolioSnapshot> {
864 let account = self.cache.borrow().account_owned(account_id)?;
865
866 let balances: Vec<AccountBalance> = account.balances().into_values().collect();
867 let margins: Vec<MarginBalance> = match &account {
868 AccountAny::Margin(m) => m
869 .margins
870 .values()
871 .copied()
872 .chain(m.account_margins.values().copied())
873 .collect(),
874 AccountAny::Cash(_) | AccountAny::Betting(_) => Vec::new(),
875 };
876
877 let open_venues: AHashSet<Venue> = self
882 .cache
883 .borrow()
884 .positions_open(None, None, None, Some(account_id), None)
885 .iter()
886 .map(|p| p.instrument_id.venue)
887 .collect();
888 let all_venues: AHashSet<Venue> = self
889 .cache
890 .borrow()
891 .positions(None, None, None, Some(account_id), None)
892 .iter()
893 .map(|p| p.instrument_id.venue)
894 .collect();
895
896 let mut unrealized: IndexMap<Currency, Money> = IndexMap::new();
897 let mut realized: IndexMap<Currency, Money> = IndexMap::new();
898 let mut equity: IndexMap<Currency, Money> = account.balances_total().into_iter().collect();
899
900 for venue in &open_venues {
901 for (currency, money) in self.unrealized_pnls(venue, Some(account_id)) {
902 unrealized
903 .entry(currency)
904 .and_modify(|total| *total = *total + money)
905 .or_insert(money);
906 }
907 }
908
909 for venue in &all_venues {
910 for (currency, money) in self.realized_pnls(venue, Some(account_id)) {
911 realized
912 .entry(currency)
913 .and_modify(|total| *total = *total + money)
914 .or_insert(money);
915 }
916 }
917
918 match &account {
919 AccountAny::Margin(_) => {
920 for (currency, value) in &unrealized {
921 equity
922 .entry(*currency)
923 .and_modify(|total| *total = *total + *value)
924 .or_insert(*value);
925 }
926 }
927 AccountAny::Cash(_) | AccountAny::Betting(_) => {
928 for venue in &open_venues {
929 for (currency, money) in self.mark_values(venue, Some(account_id)) {
930 equity
931 .entry(currency)
932 .and_modify(|total| *total = *total + money)
933 .or_insert(money);
934 }
935 }
936 }
937 }
938
939 let unrealized_pnls: Vec<Money> = unrealized.into_values().collect();
940 let realized_pnls: Vec<Money> = realized.into_values().collect();
941 let total_equity: Vec<Money> = equity.into_values().collect();
942
943 let ts_now = self.clock.borrow().timestamp_ns();
944
945 Some(PortfolioSnapshot::new(
946 account.id(),
947 account.account_type(),
948 account.base_currency(),
949 balances,
950 margins,
951 unrealized_pnls,
952 realized_pnls,
953 total_equity,
954 UUID4::new(),
955 ts_now,
956 ts_now,
957 ))
958 }
959
960 #[must_use]
967 pub fn snapshots(&self, account_id: &AccountId) -> Vec<PortfolioSnapshot> {
968 self.inner
969 .borrow()
970 .portfolio_snapshots
971 .get(account_id)
972 .map(|ring| ring.iter().cloned().collect())
973 .unwrap_or_default()
974 }
975
976 #[must_use]
982 pub fn missing_price_instruments(&self, venue: &Venue) -> Vec<InstrumentId> {
983 let mut ids: Vec<InstrumentId> = self
984 .inner
985 .borrow()
986 .venues_missing_price
987 .get(venue)
988 .map(|set| set.iter().copied().collect())
989 .unwrap_or_default();
990 ids.sort();
993 ids
994 }
995
996 fn update_missing_price_state(&self, venue: Venue, unpriced: &AHashSet<InstrumentId>) {
997 let mut inner = self.inner.borrow_mut();
998 let tracked = inner.venues_missing_price.entry(venue).or_default();
999
1000 let mut ids: Vec<InstrumentId> = unpriced.iter().copied().collect();
1002 ids.sort();
1003 for instrument_id in ids {
1004 if tracked.insert(instrument_id) {
1005 log::warn!(
1006 "No price available for open position {instrument_id}; \
1007 subscribe to quotes, trades or bars for continuous mark-to-market equity"
1008 );
1009 }
1010 }
1011
1012 tracked.retain(|id| unpriced.contains(id));
1014 }
1015
1016 fn accumulate_mark_values(
1020 &self,
1021 venue: Venue,
1022 account_id: Option<&AccountId>,
1023 values: &mut IndexMap<Currency, Decimal>,
1024 unpriced: &mut AHashSet<InstrumentId>,
1025 ) -> bool {
1026 let cache = self.cache.borrow();
1027 let positions = cache.positions_open(Some(&venue), None, None, account_id, None);
1028
1029 if positions.is_empty() {
1030 return false;
1031 }
1032
1033 let account = match account_id {
1034 Some(id) => cache.account(id),
1035 None => cache.account_for_venue(&venue),
1036 };
1037 let mut xrate_cache: AHashMap<Currency, Option<Decimal>> = AHashMap::new();
1038
1039 for position in positions {
1040 let sign = match position.side {
1041 PositionSide::Long => Decimal::ONE,
1042 PositionSide::Short => Decimal::NEGATIVE_ONE,
1043 PositionSide::Flat | PositionSide::NoPositionSide => continue,
1044 };
1045
1046 let instrument = match cache.instrument(&position.instrument_id) {
1047 Some(i) => i,
1048 None => {
1049 unpriced.insert(position.instrument_id);
1050 continue;
1051 }
1052 };
1053
1054 let price = match self.get_price(&position) {
1055 Some(p) => p,
1056 None => {
1057 unpriced.insert(position.instrument_id);
1058 continue;
1059 }
1060 };
1061
1062 let settlement = instrument.settlement_currency();
1063 let (xrate, currency) = if self.config.convert_to_account_base_currency
1064 && let Some(account) = account.as_ref()
1065 && let Some(base_currency) = account.base_currency()
1066 {
1067 let xrate_opt = *xrate_cache
1068 .entry(settlement)
1069 .or_insert_with(|| self.calculate_xrate_to_base(instrument, account));
1070 let xrate = match xrate_opt {
1071 Some(x) => x,
1072 None => {
1073 unpriced.insert(position.instrument_id);
1074 continue;
1075 }
1076 };
1077 (xrate, base_currency)
1078 } else {
1079 (Decimal::ONE, settlement)
1080 };
1081
1082 let notional = position.notional_value(price).as_decimal() * xrate * sign;
1084 *values.entry(currency).or_insert(Decimal::ZERO) += notional;
1085 }
1086
1087 true
1088 }
1089
1090 #[must_use]
1091 pub fn net_exposure(
1092 &self,
1093 instrument_id: &InstrumentId,
1094 account_id: Option<&AccountId>,
1095 ) -> Option<Money> {
1096 let cache = self.cache.borrow();
1097
1098 let instrument = if let Some(instrument) = cache.instrument(instrument_id) {
1099 instrument
1100 } else {
1101 log::error!("Cannot calculate net exposure: no instrument for {instrument_id}");
1102 return None;
1103 };
1104
1105 let positions_open =
1106 cache.positions_open(None, Some(instrument_id), None, account_id, None);
1107
1108 if positions_open.is_empty() {
1109 return Some(Money::zero(instrument.settlement_currency()));
1110 }
1111
1112 let mut net_exposure = Decimal::ZERO;
1113 let mut first_base_currency: Option<Currency> = None;
1114
1115 for position in &positions_open {
1116 let account = match cache.try_account(&position.account_id) {
1118 Ok(account) => account,
1119 Err(e) => {
1120 log::error!("Cannot calculate net exposure: {e}");
1121 return None;
1122 }
1123 };
1124
1125 if let Some(base) = account.base_currency() {
1127 match first_base_currency {
1128 None => {
1129 first_base_currency = Some(base);
1130 }
1131 Some(first) if first != base => {
1132 log::error!(
1133 "Cannot calculate net exposure: accounts have different base \
1134 currencies ({first} vs {base}); multi-account aggregation requires \
1135 consistent base currencies"
1136 );
1137 return None;
1138 }
1139 _ => {}
1140 }
1141 }
1142
1143 let price = self.get_price(position)?;
1144 let xrate = if let Some(xrate) = self.calculate_xrate_to_base(instrument, &account) {
1145 xrate
1146 } else {
1147 log::error!(
1148 "Cannot calculate net exposures: insufficient data for {}/{:?}",
1149 instrument.settlement_currency(),
1150 account.base_currency()
1151 );
1152 return None;
1153 };
1154
1155 let notional_value =
1156 instrument.calculate_notional_value(position.quantity, price, None);
1157 net_exposure += notional_value.as_decimal() * xrate;
1158 }
1159
1160 let settlement_currency =
1161 first_base_currency.unwrap_or_else(|| instrument.settlement_currency());
1162
1163 match Money::from_decimal(net_exposure, settlement_currency) {
1164 Ok(money) => Some(money),
1165 Err(e) => {
1166 log::error!("Cannot calculate net exposure: {e}");
1167 None
1168 }
1169 }
1170 }
1171
1172 #[must_use]
1173 pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal {
1174 self.inner
1175 .borrow()
1176 .net_positions
1177 .get(instrument_id)
1178 .copied()
1179 .unwrap_or(Decimal::ZERO)
1180 }
1181
1182 #[must_use]
1183 pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool {
1184 self.inner
1185 .borrow()
1186 .net_positions
1187 .get(instrument_id)
1188 .copied()
1189 .map_or_else(|| false, |net_position| net_position > Decimal::ZERO)
1190 }
1191
1192 #[must_use]
1193 pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool {
1194 self.inner
1195 .borrow()
1196 .net_positions
1197 .get(instrument_id)
1198 .copied()
1199 .map_or_else(|| false, |net_position| net_position < Decimal::ZERO)
1200 }
1201
1202 #[must_use]
1203 pub fn is_flat(&self, instrument_id: &InstrumentId) -> bool {
1204 self.inner
1205 .borrow()
1206 .net_positions
1207 .get(instrument_id)
1208 .copied()
1209 .map_or_else(|| true, |net_position| net_position == Decimal::ZERO)
1210 }
1211
1212 #[must_use]
1213 pub fn is_completely_flat(&self) -> bool {
1214 for net_position in self.inner.borrow().net_positions.values() {
1215 if *net_position != Decimal::ZERO {
1216 return false;
1217 }
1218 }
1219 true
1220 }
1221
1222 pub fn initialize_orders(&mut self) {
1228 let mut initialized = true;
1229 let orders_and_instruments = {
1230 let cache = self.cache.borrow();
1231
1232 let mut instruments_with_orders = Vec::new();
1233 let mut instruments = AHashSet::new();
1234
1235 for client_order_id in cache.iter_client_order_ids_open(None, None, None, None) {
1236 if let Some(order) = cache.order(&client_order_id) {
1237 instruments.insert(order.instrument_id());
1238 }
1239 }
1240
1241 for instrument_id in instruments {
1242 if let Some(instrument) = cache.instrument(&instrument_id) {
1243 let orders = cache
1244 .orders_open(None, Some(&instrument_id), None, None, None)
1245 .into_iter()
1246 .map(|order| order.clone())
1247 .collect::<Vec<OrderAny>>();
1248 instruments_with_orders.push((instrument.clone(), orders));
1249 } else {
1250 log::error!(
1251 "Cannot update initial (order) margin: no instrument found for {instrument_id}"
1252 );
1253 initialized = false;
1254 break;
1255 }
1256 }
1257 instruments_with_orders
1258 };
1259
1260 for (instrument, orders_open) in &orders_and_instruments {
1261 let account = {
1262 let cache = self.cache.borrow();
1263 if let Some(account) = cache.account_for_venue(&instrument.id().venue) {
1264 account.clone()
1265 } else {
1266 log::error!(
1267 "Cannot update initial (order) margin: no account registered for {}",
1268 instrument.id().venue
1269 );
1270 initialized = false;
1271 break;
1272 }
1273 };
1274
1275 let orders_open_refs: Vec<&OrderAny> = orders_open.iter().collect();
1276 let result = self.inner.borrow_mut().accounts.update_orders(
1277 &account,
1278 instrument,
1279 &orders_open_refs,
1280 self.clock.borrow().timestamp_ns(),
1281 );
1282
1283 match result {
1284 Some((updated_account, _)) => {
1285 self.cache
1286 .borrow_mut()
1287 .update_account(&updated_account)
1288 .unwrap();
1289 }
1290 None => {
1291 initialized = false;
1292 }
1293 }
1294 }
1295
1296 let total_orders = orders_and_instruments
1297 .into_iter()
1298 .map(|(_, orders)| orders.len())
1299 .sum::<usize>();
1300
1301 log::info!(
1302 color = if total_orders > 0 { LogColor::Blue as u8 } else { LogColor::Normal as u8 };
1303 "Initialized {} open order{}",
1304 total_orders,
1305 if total_orders == 1 { "" } else { "s" }
1306 );
1307
1308 self.inner.borrow_mut().initialized = initialized;
1309 }
1310
1311 pub fn initialize_positions(&mut self) {
1317 self.inner.borrow_mut().unrealized_pnls.clear();
1318 self.inner.borrow_mut().realized_pnls.clear();
1319 let all_positions_open: Vec<Position>;
1320 let mut instruments = AHashSet::new();
1321 {
1322 let cache = self.cache.borrow();
1323 all_positions_open = cache
1324 .positions_open(None, None, None, None, None)
1325 .into_iter()
1326 .map(|p| p.cloned())
1327 .collect();
1328
1329 for position in &all_positions_open {
1330 instruments.insert(position.instrument_id);
1331 }
1332 }
1333
1334 let mut initialized = true;
1335
1336 for instrument_id in instruments {
1337 let positions_open: Vec<Position> = {
1338 let cache = self.cache.borrow();
1339 cache
1340 .positions_open(None, Some(&instrument_id), None, None, None)
1341 .into_iter()
1342 .map(|p| p.cloned())
1343 .collect()
1344 };
1345
1346 let position_refs: Vec<&Position> = positions_open.iter().collect();
1347 self.update_net_position(&instrument_id, &position_refs);
1348
1349 if let Some(calculated_unrealized_pnl) =
1350 self.calculate_unrealized_pnl(&instrument_id, None)
1351 {
1352 self.inner
1353 .borrow_mut()
1354 .unrealized_pnls
1355 .insert(instrument_id, calculated_unrealized_pnl);
1356 } else {
1357 log::debug!(
1358 "Failed to calculate unrealized PnL for {instrument_id}, marking as pending"
1359 );
1360 self.inner.borrow_mut().pending_calcs.insert(instrument_id);
1361 }
1362
1363 if let Some(calculated_realized_pnl) = self.calculate_realized_pnl(&instrument_id, None)
1364 {
1365 self.inner
1366 .borrow_mut()
1367 .realized_pnls
1368 .insert(instrument_id, calculated_realized_pnl);
1369 } else {
1370 log::warn!(
1371 "Failed to calculate realized PnL for {instrument_id}, marking as pending"
1372 );
1373 self.inner.borrow_mut().pending_calcs.insert(instrument_id);
1374 }
1375
1376 let cache = self.cache.borrow();
1377 let Some(account) = cache.account_for_venue_owned(&instrument_id.venue) else {
1378 log::error!(
1379 "Cannot update maintenance (position) margin: no account registered for {}",
1380 instrument_id.venue
1381 );
1382 initialized = false;
1383 break;
1384 };
1385
1386 let account = match account {
1387 AccountAny::Cash(_) | AccountAny::Betting(_) => continue,
1388 AccountAny::Margin(margin_account) => margin_account,
1389 };
1390
1391 let Some(instrument) = cache.instrument(&instrument_id).cloned() else {
1392 log::error!(
1393 "Cannot update maintenance (position) margin: no instrument found for {instrument_id}"
1394 );
1395 initialized = false;
1396 break;
1397 };
1398 let positions: Vec<Position> = cache
1399 .positions_open(None, Some(&instrument_id), None, None, None)
1400 .into_iter()
1401 .map(|p| p.cloned())
1402 .collect();
1403 drop(cache);
1404
1405 let result = self.inner.borrow_mut().accounts.update_positions(
1406 &account,
1407 &instrument,
1408 positions.iter().collect(),
1409 self.clock.borrow().timestamp_ns(),
1410 );
1411
1412 match result {
1413 Some((updated_account, _)) => {
1414 self.cache
1415 .borrow_mut()
1416 .update_account(&AccountAny::Margin(updated_account))
1417 .unwrap();
1418 }
1419 None => {
1420 initialized = false;
1421 }
1422 }
1423 }
1424
1425 let open_count = all_positions_open.len();
1426 self.inner.borrow_mut().initialized = initialized;
1427 log::info!(
1428 color = if open_count > 0 { LogColor::Blue as u8 } else { LogColor::Normal as u8 };
1429 "Initialized {} open position{}",
1430 open_count,
1431 if open_count == 1 { "" } else { "s" }
1432 );
1433
1434 if self.config.snapshot_interval_ms.is_some() {
1435 let account_ids: AHashSet<AccountId> =
1436 all_positions_open.iter().map(|p| p.account_id).collect();
1437
1438 for account_id in account_ids {
1439 update_snapshot_timer_state(
1440 &self.cache,
1441 &self.clock,
1442 &self.inner,
1443 self.config,
1444 account_id,
1445 );
1446 }
1447 }
1448 }
1449
1450 pub fn update_quote_tick(&mut self, quote: &QuoteTick) {
1454 update_quote_tick(&self.cache, &self.clock, &self.inner, self.config, quote);
1455 }
1456
1457 pub fn update_bar(&mut self, bar: &Bar) {
1461 update_bar(&self.cache, &self.clock, &self.inner, self.config, bar);
1462 }
1463
1464 pub fn update_account(&mut self, event: &AccountState) {
1466 update_account(&self.cache, &self.inner, event);
1467 }
1468
1469 pub fn update_order(&mut self, event: &OrderEventAny) {
1473 update_order(
1474 &self.cache,
1475 &self.clock,
1476 &self.inner,
1477 self.config,
1478 event,
1479 OrderUpdateSource::Topic,
1480 );
1481 }
1482
1483 #[must_use]
1487 pub fn recorded_realized_pnls(&self) -> AHashMap<Currency, Vec<(PositionId, UnixNanos, f64)>> {
1488 self.inner.borrow().analyzer.recorded_realized_pnls.clone()
1489 }
1490
1491 #[must_use]
1497 pub fn statistics(&self) -> PortfolioStatistics {
1498 let cache = self.cache.borrow();
1499 let accounts = cache.accounts_all_owned();
1500 let positions: Vec<Position> = cache
1501 .positions(None, None, None, None, None)
1502 .into_iter()
1503 .map(|p| p.cloned())
1504 .collect();
1505 let mut snapshots = Vec::new();
1506 for position in &positions {
1507 snapshots.extend(cache.position_snapshots(Some(&position.id), None));
1508 }
1509 let recorded = self.inner.borrow().analyzer.recorded_realized_pnls.clone();
1510 PortfolioAnalyzer::from_accounts(&accounts, &positions, &snapshots, recorded).statistics()
1511 }
1512
1513 pub fn update_position(&mut self, event: &PositionEvent) {
1517 update_position(&self.cache, &self.clock, &self.inner, self.config, event);
1518 }
1519
1520 fn update_net_position(&self, instrument_id: &InstrumentId, positions: &[&Position]) {
1521 let mut net_position = Decimal::ZERO;
1522
1523 for open_position in positions {
1524 log::debug!("open_position: {}", *open_position);
1525 net_position += open_position.signed_decimal_qty();
1526 }
1527
1528 let existing_position = self.net_position(instrument_id);
1529 if existing_position != net_position {
1530 self.inner
1531 .borrow_mut()
1532 .net_positions
1533 .insert(*instrument_id, net_position);
1534 log::info!("{instrument_id} net_position={net_position}");
1535 }
1536 }
1537
1538 fn calculate_unrealized_pnl(
1539 &self,
1540 instrument_id: &InstrumentId,
1541 account_id: Option<&AccountId>,
1542 ) -> Option<Money> {
1543 let cache = self.cache.borrow();
1544 let account = match account_id {
1545 Some(id) => cache.account(id),
1546 None => cache.account_for_venue(&instrument_id.venue),
1547 };
1548 let account = if let Some(account) = account {
1549 account
1550 } else {
1551 log::error!(
1552 "Cannot calculate unrealized PnL: no account for {} / {account_id:?}",
1553 instrument_id.venue,
1554 );
1555 return None;
1556 };
1557
1558 let instrument = if let Some(instrument) = cache.instrument(instrument_id) {
1559 instrument
1560 } else {
1561 log::error!("Cannot calculate unrealized PnL: no instrument for {instrument_id}");
1562 return None;
1563 };
1564
1565 let currency = account
1566 .base_currency()
1567 .unwrap_or_else(|| instrument.settlement_currency());
1568
1569 let positions_open =
1570 cache.positions_open(None, Some(instrument_id), None, account_id, None);
1571
1572 if positions_open.is_empty() {
1573 return Some(Money::zero(currency));
1574 }
1575
1576 let mut total_pnl = Decimal::ZERO;
1577
1578 for position in positions_open {
1579 if position.instrument_id != *instrument_id {
1580 continue; }
1582
1583 if position.side == PositionSide::Flat {
1584 continue; }
1586
1587 let price = if let Some(price) = self.get_price(&position) {
1588 price
1589 } else {
1590 log::debug!("Cannot calculate unrealized PnL: no prices for {instrument_id}");
1591 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1592 return None; };
1594
1595 let mut pnl = position.unrealized_pnl(price).as_decimal();
1596
1597 if let Some(base_currency) = account.base_currency() {
1598 let xrate = if let Some(xrate) = self.calculate_xrate_to_base(instrument, &account)
1599 {
1600 xrate
1601 } else {
1602 log::warn!(
1603 "Cannot calculate unrealized PnL: insufficient data for {}/{}",
1605 instrument.settlement_currency(),
1606 base_currency
1607 );
1608 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1609 return None; };
1611
1612 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
1613 }
1614
1615 total_pnl += pnl;
1616 }
1617
1618 match Money::from_decimal(total_pnl, currency) {
1619 Ok(money) => Some(money),
1620 Err(e) => {
1621 log::error!("Cannot calculate unrealized PnL: {e}");
1622 None
1623 }
1624 }
1625 }
1626
1627 fn ensure_snapshot_pnls_cached_for(&self, instrument_id: &InstrumentId) {
1628 let snapshot_position_ids = self.cache.borrow().position_snapshot_ids(instrument_id);
1634
1635 if snapshot_position_ids.is_empty() {
1636 return; }
1638
1639 let mut rebuild = false;
1640
1641 for position_id in &snapshot_position_ids {
1643 let curr_count = self.cache.borrow().position_snapshot_count(position_id);
1644 let prev_count = self
1645 .inner
1646 .borrow()
1647 .snapshot_processed_counts
1648 .get(position_id)
1649 .copied()
1650 .unwrap_or(0);
1651
1652 if prev_count > curr_count {
1653 rebuild = true;
1654 break;
1655 }
1656 }
1657
1658 if rebuild {
1659 for position_id in &snapshot_position_ids {
1661 let snapshot_count = self.cache.borrow().position_snapshot_count(position_id);
1665 let snapshots = self
1666 .cache
1667 .borrow()
1668 .position_snapshots(Some(position_id), None);
1669
1670 let mut sum_pnl: Option<Money> = None;
1671 let mut last_pnl: Option<Money> = None;
1672 let mut snapshot_account_id: Option<AccountId> = None;
1673
1674 for snapshot in snapshots {
1675 snapshot_account_id.get_or_insert(snapshot.account_id);
1676 if let Some(realized_pnl) = snapshot.realized_pnl {
1677 if let Some(sum) = sum_pnl {
1678 if sum.currency == realized_pnl.currency {
1679 sum_pnl = Some(sum + realized_pnl);
1680 }
1681 } else {
1682 sum_pnl = Some(realized_pnl);
1683 }
1684 last_pnl = Some(realized_pnl);
1685 }
1686 }
1687
1688 let mut inner = self.inner.borrow_mut();
1689
1690 if let Some(sum) = sum_pnl {
1691 inner.snapshot_sum_per_position.insert(*position_id, sum);
1692
1693 if let Some(last) = last_pnl {
1694 inner.snapshot_last_per_position.insert(*position_id, last);
1695 }
1696 } else {
1697 inner.snapshot_sum_per_position.remove(position_id);
1698 inner.snapshot_last_per_position.remove(position_id);
1699 }
1700
1701 if let Some(account_id) = snapshot_account_id {
1702 inner.snapshot_account_ids.insert(*position_id, account_id);
1703 } else {
1704 inner.snapshot_account_ids.remove(position_id);
1705 }
1706
1707 inner
1708 .snapshot_processed_counts
1709 .insert(*position_id, snapshot_count);
1710 }
1711 } else {
1712 for position_id in &snapshot_position_ids {
1714 let curr_count = self.cache.borrow().position_snapshot_count(position_id);
1717 let prev_count = self
1718 .inner
1719 .borrow()
1720 .snapshot_processed_counts
1721 .get(position_id)
1722 .copied()
1723 .unwrap_or(0);
1724
1725 if prev_count >= curr_count {
1726 continue;
1727 }
1728
1729 let mut sum_pnl = self
1730 .inner
1731 .borrow()
1732 .snapshot_sum_per_position
1733 .get(position_id)
1734 .copied();
1735 let mut last_pnl = self
1736 .inner
1737 .borrow()
1738 .snapshot_last_per_position
1739 .get(position_id)
1740 .copied();
1741 let mut snapshot_account_id: Option<AccountId> = None;
1742
1743 let new_snapshots = self
1744 .cache
1745 .borrow()
1746 .position_snapshots_from(position_id, prev_count);
1747
1748 for snapshot in new_snapshots {
1749 snapshot_account_id.get_or_insert(snapshot.account_id);
1750 if let Some(realized_pnl) = snapshot.realized_pnl {
1751 if let Some(sum) = sum_pnl {
1752 if sum.currency == realized_pnl.currency {
1753 sum_pnl = Some(sum + realized_pnl);
1754 }
1755 } else {
1756 sum_pnl = Some(realized_pnl);
1757 }
1758 last_pnl = Some(realized_pnl);
1759 }
1760 }
1761
1762 let mut inner = self.inner.borrow_mut();
1763
1764 if let Some(sum) = sum_pnl {
1765 inner.snapshot_sum_per_position.insert(*position_id, sum);
1766
1767 if let Some(last) = last_pnl {
1768 inner.snapshot_last_per_position.insert(*position_id, last);
1769 }
1770 }
1771
1772 if let Some(account_id) = snapshot_account_id
1773 && !inner.snapshot_account_ids.contains_key(position_id)
1774 {
1775 inner.snapshot_account_ids.insert(*position_id, account_id);
1776 }
1777
1778 inner
1779 .snapshot_processed_counts
1780 .insert(*position_id, curr_count);
1781 }
1782 }
1783 }
1784
1785 fn calculate_realized_pnl(
1786 &self,
1787 instrument_id: &InstrumentId,
1788 account_id: Option<&AccountId>,
1789 ) -> Option<Money> {
1790 self.ensure_snapshot_pnls_cached_for(instrument_id);
1792
1793 let cache = self.cache.borrow();
1794 let account = match account_id {
1795 Some(id) => cache.account(id),
1796 None => cache.account_for_venue(&instrument_id.venue),
1797 };
1798 let account = if let Some(account) = account {
1799 account
1800 } else {
1801 log::error!(
1802 "Cannot calculate realized PnL: no account for {} / {account_id:?}",
1803 instrument_id.venue,
1804 );
1805 return None;
1806 };
1807
1808 let instrument = if let Some(instrument) = cache.instrument(instrument_id) {
1809 instrument
1810 } else {
1811 log::error!("Cannot calculate realized PnL: no instrument for {instrument_id}");
1812 return None;
1813 };
1814
1815 let currency = account
1816 .base_currency()
1817 .unwrap_or_else(|| instrument.settlement_currency());
1818
1819 let positions = cache.positions(None, Some(instrument_id), None, account_id, None);
1820
1821 let mut snapshot_position_ids: Vec<PositionId> = if let Some(filter_id) = account_id {
1826 let inner = self.inner.borrow();
1827 cache
1828 .position_snapshot_ids(instrument_id)
1829 .into_iter()
1830 .filter(|pid| {
1831 inner
1832 .snapshot_account_ids
1833 .get(pid)
1834 .is_some_and(|id| id == filter_id)
1835 })
1836 .collect()
1837 } else {
1838 cache
1839 .position_snapshot_ids(instrument_id)
1840 .into_iter()
1841 .collect()
1842 };
1843 snapshot_position_ids.sort();
1844
1845 let is_netting = positions
1847 .iter()
1848 .any(|p| cache.oms_type(&p.id) == Some(OmsType::Netting));
1849
1850 let mut total_pnl = Decimal::ZERO;
1851
1852 if is_netting && !snapshot_position_ids.is_empty() {
1853 for position_id in &snapshot_position_ids {
1856 let is_active = positions.iter().any(|p| p.id == *position_id);
1857
1858 if is_active {
1859 let last_pnl = self
1861 .inner
1862 .borrow()
1863 .snapshot_last_per_position
1864 .get(position_id)
1865 .copied();
1866
1867 if let Some(last_pnl) = last_pnl {
1868 let mut pnl = last_pnl.as_decimal();
1869
1870 if let Some(base_currency) = account.base_currency()
1871 && positions.iter().any(|p| p.id == *position_id)
1872 {
1873 let xrate = if let Some(xrate) =
1874 self.calculate_xrate_to_base(instrument, &account)
1875 {
1876 xrate
1877 } else {
1878 log::warn!(
1879 "Cannot calculate realized PnL: insufficient exchange rate data for {}/{}, marking as pending calculation",
1880 instrument.settlement_currency(),
1881 base_currency
1882 );
1883 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1884 return Some(Money::zero(currency));
1885 };
1886
1887 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
1888 }
1889
1890 total_pnl += pnl;
1891 }
1892 } else {
1893 let sum_pnl = self
1895 .inner
1896 .borrow()
1897 .snapshot_sum_per_position
1898 .get(position_id)
1899 .copied();
1900
1901 if let Some(sum_pnl) = sum_pnl {
1902 let mut pnl = sum_pnl.as_decimal();
1903
1904 if let Some(base_currency) = account.base_currency() {
1905 let xrate = cache.get_xrate(
1907 instrument_id.venue,
1908 instrument.settlement_currency(),
1909 base_currency,
1910 PriceType::Mid,
1911 );
1912
1913 if let Some(xrate) = xrate {
1914 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
1915 } else {
1916 log::warn!(
1917 "Cannot calculate realized PnL: insufficient exchange rate data for {}/{}, marking as pending calculation",
1918 instrument.settlement_currency(),
1919 base_currency
1920 );
1921 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1922 return Some(Money::zero(currency));
1923 }
1924 }
1925
1926 total_pnl += pnl;
1927 }
1928 }
1929 }
1930
1931 for position in positions {
1933 if position.instrument_id != *instrument_id {
1934 continue;
1935 }
1936
1937 if let Some(realized_pnl) = position.realized_pnl {
1938 let mut pnl = realized_pnl.as_decimal();
1939
1940 if let Some(base_currency) = account.base_currency() {
1941 let xrate = if let Some(xrate) =
1942 self.calculate_xrate_to_base(instrument, &account)
1943 {
1944 xrate
1945 } else {
1946 log::warn!(
1947 "Cannot calculate realized PnL: insufficient exchange rate data for {}/{}, marking as pending calculation",
1948 instrument.settlement_currency(),
1949 base_currency
1950 );
1951 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1952 return Some(Money::zero(currency));
1953 };
1954
1955 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
1956 }
1957
1958 total_pnl += pnl;
1959 }
1960 }
1961 } else {
1962 for position_id in &snapshot_position_ids {
1965 let sum_pnl = self
1966 .inner
1967 .borrow()
1968 .snapshot_sum_per_position
1969 .get(position_id)
1970 .copied();
1971
1972 if let Some(sum_pnl) = sum_pnl {
1973 let mut pnl = sum_pnl.as_decimal();
1974
1975 if let Some(base_currency) = account.base_currency() {
1976 let xrate = cache.get_xrate(
1977 instrument_id.venue,
1978 instrument.settlement_currency(),
1979 base_currency,
1980 PriceType::Mid,
1981 );
1982
1983 if let Some(xrate) = xrate {
1984 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
1985 } else {
1986 log::warn!(
1987 "Cannot calculate realized PnL: insufficient exchange rate data for {}/{}, marking as pending calculation",
1988 instrument.settlement_currency(),
1989 base_currency
1990 );
1991 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
1992 return Some(Money::zero(currency));
1993 }
1994 }
1995
1996 total_pnl += pnl;
1997 }
1998 }
1999
2000 for position in positions {
2002 if position.instrument_id != *instrument_id {
2003 continue;
2004 }
2005
2006 if let Some(realized_pnl) = position.realized_pnl {
2007 let mut pnl = realized_pnl.as_decimal();
2008
2009 if let Some(base_currency) = account.base_currency() {
2010 let xrate = if let Some(xrate) =
2011 self.calculate_xrate_to_base(instrument, &account)
2012 {
2013 xrate
2014 } else {
2015 log::warn!(
2016 "Cannot calculate realized PnL: insufficient exchange rate data for {}/{}, marking as pending calculation",
2017 instrument.settlement_currency(),
2018 base_currency
2019 );
2020 self.inner.borrow_mut().pending_calcs.insert(*instrument_id);
2021 return Some(Money::zero(currency));
2022 };
2023
2024 pnl = (pnl * xrate).round_dp(u32::from(currency.precision));
2025 }
2026
2027 total_pnl += pnl;
2028 }
2029 }
2030 }
2031
2032 match Money::from_decimal(total_pnl, currency) {
2033 Ok(money) => Some(money),
2034 Err(e) => {
2035 log::error!("Cannot calculate realized PnL: {e}");
2036 Some(Money::zero(currency))
2037 }
2038 }
2039 }
2040
2041 fn get_price(&self, position: &Position) -> Option<Price> {
2042 let cache = self.cache.borrow();
2043 let instrument_id = &position.instrument_id;
2044
2045 if self.config.use_mark_prices
2047 && let Some(mark_price) = cache.mark_price(instrument_id)
2048 {
2049 return Some(mark_price.value);
2050 }
2051
2052 let price_type = match position.side {
2054 PositionSide::Long => PriceType::Bid,
2055 PositionSide::Short => PriceType::Ask,
2056 _ => panic!("invalid `PositionSide`, was {}", position.side),
2057 };
2058
2059 cache
2060 .price(instrument_id, price_type)
2061 .or_else(|| cache.price(instrument_id, PriceType::Last))
2062 .or_else(|| {
2063 self.inner
2064 .borrow()
2065 .bar_close_prices
2066 .get(instrument_id)
2067 .copied()
2068 })
2069 }
2070
2071 fn calculate_xrate_to_base(
2072 &self,
2073 instrument: &InstrumentAny,
2074 account: &AccountAny,
2075 ) -> Option<Decimal> {
2076 if !self.config.convert_to_account_base_currency {
2077 return Some(Decimal::ONE); }
2079
2080 let base_currency = match account.base_currency() {
2081 Some(base_currency) => base_currency,
2082 None => return Some(Decimal::ONE),
2083 };
2084
2085 let settlement = instrument.settlement_currency();
2086 let cache = self.cache.borrow();
2087
2088 if self.config.use_mark_xrates
2089 && let Some(xrate) = cache.get_mark_xrate(settlement, base_currency)
2090 {
2091 return Decimal::try_from(xrate).ok();
2093 }
2094
2095 cache.get_xrate(
2096 instrument.id().venue,
2097 settlement,
2098 base_currency,
2099 PriceType::Mid,
2100 )
2101 }
2102}
2103
2104fn decimal_map_to_money(map: IndexMap<Currency, Decimal>) -> IndexMap<Currency, Money> {
2107 map.into_iter()
2108 .filter_map(
2109 |(currency, amount)| match Money::from_decimal(amount, currency) {
2110 Ok(money) => Some((currency, money)),
2111 Err(e) => {
2112 log::error!("Cannot convert {currency} amount to Money: {e}");
2113 None
2114 }
2115 },
2116 )
2117 .collect()
2118}
2119
2120fn update_quote_tick(
2121 cache: &Rc<RefCell<Cache>>,
2122 clock: &Rc<RefCell<dyn Clock>>,
2123 inner: &Rc<RefCell<PortfolioState>>,
2124 config: PortfolioConfig,
2125 quote: &QuoteTick,
2126) {
2127 update_instrument_id(cache, clock, inner, config, "e.instrument_id);
2128}
2129
2130fn update_bar(
2131 cache: &Rc<RefCell<Cache>>,
2132 clock: &Rc<RefCell<dyn Clock>>,
2133 inner: &Rc<RefCell<PortfolioState>>,
2134 config: PortfolioConfig,
2135 bar: &Bar,
2136) {
2137 let instrument_id = bar.bar_type.instrument_id();
2138 inner
2139 .borrow_mut()
2140 .bar_close_prices
2141 .insert(instrument_id, bar.close);
2142 update_instrument_id(cache, clock, inner, config, &instrument_id);
2143}
2144
2145fn update_instrument_id(
2146 cache: &Rc<RefCell<Cache>>,
2147 clock: &Rc<RefCell<dyn Clock>>,
2148 inner: &Rc<RefCell<PortfolioState>>,
2149 config: PortfolioConfig,
2150 instrument_id: &InstrumentId,
2151) {
2152 inner
2153 .borrow_mut()
2154 .unrealized_pnls
2155 .shift_remove(instrument_id);
2156
2157 if inner.borrow().initialized || !inner.borrow().pending_calcs.contains(instrument_id) {
2158 return;
2159 }
2160
2161 let mut result_maint = None;
2162
2163 let (account, instrument, orders_open, positions_open) = {
2165 let cache_ref = cache.borrow();
2166 let account = if let Some(account) = cache_ref.account_for_venue(&instrument_id.venue) {
2167 account.clone()
2168 } else {
2169 log::error!(
2170 "Cannot update tick: no account registered for {}",
2171 instrument_id.venue
2172 );
2173 return;
2174 };
2175 let instrument = if let Some(instrument) = cache_ref.instrument(instrument_id) {
2176 instrument.clone()
2177 } else {
2178 log::error!("Cannot update tick: no instrument found for {instrument_id}");
2179 return;
2180 };
2181 let orders_open: Vec<OrderAny> = cache_ref
2182 .orders_open(None, Some(instrument_id), None, None, None)
2183 .iter()
2184 .map(|o| (*o).clone())
2185 .collect();
2186 let positions_open: Vec<Position> = cache_ref
2187 .positions_open(None, Some(instrument_id), None, None, None)
2188 .iter()
2189 .map(|p| (*p).clone())
2190 .collect();
2191 (account, instrument, orders_open, positions_open)
2192 };
2193
2194 let orders_open_refs: Vec<&OrderAny> = orders_open.iter().collect();
2196 let result_init = inner.borrow().accounts.update_orders(
2197 &account,
2198 &instrument,
2199 &orders_open_refs,
2200 clock.borrow().timestamp_ns(),
2201 );
2202
2203 if let AccountAny::Margin(ref margin_account) = account {
2204 result_maint = inner.borrow().accounts.update_positions(
2205 margin_account,
2206 &instrument,
2207 positions_open.iter().collect(),
2208 clock.borrow().timestamp_ns(),
2209 );
2210 }
2211
2212 if let Some((ref updated_account, _)) = result_init {
2213 cache.borrow_mut().update_account(updated_account).unwrap();
2214 }
2215
2216 let portfolio_clone = Portfolio {
2217 clock: clock.clone(),
2218 cache: cache.clone(),
2219 inner: inner.clone(),
2220 config,
2221 };
2222
2223 let result_unrealized_pnl: Option<Money> =
2224 portfolio_clone.calculate_unrealized_pnl(instrument_id, None);
2225
2226 if result_init.is_some()
2227 && (matches!(account, AccountAny::Cash(_) | AccountAny::Betting(_))
2228 || (result_maint.is_some() && result_unrealized_pnl.is_some()))
2229 {
2230 inner.borrow_mut().pending_calcs.remove(instrument_id);
2231 if inner.borrow().pending_calcs.is_empty() {
2232 inner.borrow_mut().initialized = true;
2233 }
2234 }
2235}
2236
2237fn update_order(
2238 cache: &Rc<RefCell<Cache>>,
2239 clock: &Rc<RefCell<dyn Clock>>,
2240 inner: &Rc<RefCell<PortfolioState>>,
2241 config: PortfolioConfig,
2242 event: &OrderEventAny,
2243 source: OrderUpdateSource,
2244) {
2245 let mut mark_pre_position_fill_event = None;
2246
2247 if let OrderEventAny::Filled(order_filled) = event {
2248 match source {
2249 OrderUpdateSource::Endpoint => {
2250 mark_pre_position_fill_event = Some(order_filled.event_id);
2251 }
2252 OrderUpdateSource::Topic => {
2253 if inner
2254 .borrow_mut()
2255 .pre_position_fill_events
2256 .remove(&order_filled.event_id)
2257 {
2258 return;
2259 }
2260 }
2261 }
2262 }
2263
2264 let account_id = match event.account_id() {
2265 Some(account_id) => account_id,
2266 None => {
2267 return; }
2269 };
2270
2271 let (instrument, orders_open) = {
2273 let cache_ref = cache.borrow();
2274
2275 let account = match cache_ref.try_account(&account_id) {
2276 Ok(account) => account,
2277 Err(e) => {
2278 log::error!("Cannot update order: {e}");
2279 return;
2280 }
2281 };
2282
2283 match &*account {
2284 AccountAny::Margin(margin_account) => {
2285 if !margin_account.base.calculate_account_state {
2286 return;
2287 }
2288 }
2289 AccountAny::Cash(cash_account) => {
2290 if !cash_account.base.calculate_account_state {
2291 return;
2292 }
2293 }
2294 AccountAny::Betting(betting_account) => {
2295 if !betting_account.base.calculate_account_state {
2296 return;
2297 }
2298 }
2299 }
2300
2301 match event {
2302 OrderEventAny::Accepted(_)
2303 | OrderEventAny::Canceled(_)
2304 | OrderEventAny::Expired(_)
2305 | OrderEventAny::Rejected(_)
2306 | OrderEventAny::Updated(_)
2307 | OrderEventAny::Filled(_) => {}
2308 _ => {
2309 return;
2310 }
2311 }
2312
2313 let order = cache_ref.order(&event.client_order_id());
2314 if order.is_none() && !matches!(event, OrderEventAny::Filled(_)) {
2315 log::error!(
2316 "Cannot update order: {} not found in the cache",
2317 event.client_order_id()
2318 );
2319 return; }
2321
2322 if matches!(event, OrderEventAny::Rejected(_))
2323 && order.is_some_and(|order| order.order_type() != OrderType::StopLimit)
2324 {
2325 return; }
2327
2328 let instrument = if let Some(instrument) = cache_ref.instrument(&event.instrument_id()) {
2329 instrument.clone()
2330 } else {
2331 log::error!(
2332 "Cannot update order: no instrument found for {}",
2333 event.instrument_id()
2334 );
2335 return;
2336 };
2337
2338 let orders_open: Vec<OrderAny> = cache_ref
2339 .orders_open(
2340 None,
2341 Some(&event.instrument_id()),
2342 None,
2343 Some(&account_id),
2344 None,
2345 )
2346 .iter()
2347 .map(|o| (*o).clone())
2348 .collect();
2349
2350 (instrument, orders_open)
2351 };
2352
2353 let mut working_account = match cache.borrow_mut().take_account(&account_id) {
2355 Some(account) => account,
2356 None => {
2357 log::error!(
2358 "Cannot update order: {}",
2359 AccountLookupError::not_found(account_id)
2360 );
2361 return;
2362 }
2363 };
2364
2365 if let OrderEventAny::Filled(order_filled) = event {
2366 if !instrument.is_spread() {
2367 let (post_balance, _state) = inner.borrow().accounts.update_balances(
2368 working_account,
2369 &instrument,
2370 *order_filled,
2371 );
2372 working_account = post_balance;
2373 }
2374
2375 cache.borrow_mut().cache_account_owned(working_account);
2376
2377 let portfolio_clone = Portfolio {
2378 clock: clock.clone(),
2379 cache: cache.clone(),
2380 inner: inner.clone(),
2381 config,
2382 };
2383
2384 match portfolio_clone.calculate_unrealized_pnl(&order_filled.instrument_id, None) {
2385 Some(unrealized_pnl) => {
2386 inner
2387 .borrow_mut()
2388 .unrealized_pnls
2389 .insert(event.instrument_id(), unrealized_pnl);
2390 }
2391 None => {
2392 log::error!(
2393 "Failed to calculate unrealized PnL for instrument {}",
2394 event.instrument_id()
2395 );
2396 }
2397 }
2398
2399 working_account = cache
2400 .borrow_mut()
2401 .take_account(&account_id)
2402 .expect("account restored before unrealized PnL calculation");
2403 }
2404
2405 let orders_open_refs: Vec<&OrderAny> = orders_open.iter().collect();
2406 let account_state = inner.borrow().accounts.update_orders_in_place(
2407 &mut working_account,
2408 &instrument,
2409 &orders_open_refs,
2410 clock.borrow().timestamp_ns(),
2411 );
2412
2413 let is_fill = matches!(event, OrderEventAny::Filled(_));
2414 let suppress_margin_fill_account_state =
2415 is_fill && matches!(working_account, AccountAny::Margin(_));
2416 let publish_account_state = !matches!(source, OrderUpdateSource::Endpoint) && !is_fill;
2417
2418 if !publish_account_state
2419 && !suppress_margin_fill_account_state
2420 && let Some(account_state) = account_state.as_ref()
2421 && let Err(e) = working_account.apply(account_state.clone())
2422 {
2423 log::error!("Cannot apply generated account state: {e}");
2424 }
2425
2426 let updated_account_id = working_account.id();
2427
2428 if account_state.is_some() || matches!(event, OrderEventAny::Filled(_)) {
2429 cache
2430 .borrow_mut()
2431 .update_account_owned(working_account)
2432 .unwrap();
2433 } else {
2434 cache.borrow_mut().cache_account_owned(working_account);
2435 }
2436
2437 if let Some(event_id) = mark_pre_position_fill_event {
2439 inner.borrow_mut().pre_position_fill_events.insert(event_id);
2440 }
2441
2442 if let Some(account_state) = account_state {
2443 if publish_account_state {
2444 msgbus::publish_account_state(
2445 format!("events.account.{updated_account_id}").into(),
2446 &account_state,
2447 );
2448 }
2449 } else {
2450 log::debug!("Added pending calculation for {}", instrument.id());
2451 inner.borrow_mut().pending_calcs.insert(instrument.id());
2452 }
2453
2454 log::debug!("Updated {event}");
2455}
2456
2457fn on_order_event(
2458 cache: &Rc<RefCell<Cache>>,
2459 inner: &Rc<RefCell<PortfolioState>>,
2460 event: &OrderEventAny,
2461) {
2462 if let OrderEventAny::Filled(order_filled) = event {
2463 inner
2464 .borrow_mut()
2465 .pre_position_fill_events
2466 .remove(&order_filled.event_id);
2467 return;
2468 }
2469
2470 let account_id = match event.account_id() {
2471 Some(account_id) => account_id,
2472 None => return,
2473 };
2474
2475 match event {
2476 OrderEventAny::Accepted(_)
2477 | OrderEventAny::Canceled(_)
2478 | OrderEventAny::Expired(_)
2479 | OrderEventAny::Rejected(_)
2480 | OrderEventAny::Updated(_) => {}
2481 _ => return,
2482 }
2483
2484 let account_state = cache
2485 .borrow()
2486 .account(&account_id)
2487 .and_then(|account| account.last_event());
2488
2489 if let Some(account_state) = account_state {
2490 msgbus::publish_account_state(
2491 format!("events.account.{account_id}").into(),
2492 &account_state,
2493 );
2494 }
2495}
2496
2497fn update_position(
2498 cache: &Rc<RefCell<Cache>>,
2499 clock: &Rc<RefCell<dyn Clock>>,
2500 inner: &Rc<RefCell<PortfolioState>>,
2501 config: PortfolioConfig,
2502 event: &PositionEvent,
2503) {
2504 let instrument_id = event.instrument_id();
2505 let account_id = event.account_id();
2506
2507 update_snapshot_timer_state(cache, clock, inner, config, account_id);
2508
2509 let portfolio_clone = Portfolio {
2510 clock: clock.clone(),
2511 cache: cache.clone(),
2512 inner: inner.clone(),
2513 config,
2514 };
2515
2516 {
2517 let cache_ref = cache.borrow();
2518 let refs = cache_ref.positions_open(None, Some(&instrument_id), None, None, None);
2519 log::debug!("position fresh from cache -> {refs:?}");
2520 let positions: Vec<&Position> = refs.iter().map(|r| &**r).collect();
2521 portfolio_clone.update_net_position(&instrument_id, &positions);
2522 }
2523
2524 record_closed_position_pnl(cache, inner, config, event);
2525
2526 if let Some(calculated_unrealized_pnl) =
2527 portfolio_clone.calculate_unrealized_pnl(&instrument_id, None)
2528 {
2529 inner
2530 .borrow_mut()
2531 .unrealized_pnls
2532 .insert(event.instrument_id(), calculated_unrealized_pnl);
2533 } else {
2534 log::debug!(
2535 "Failed to calculate unrealized PnL for {}, marking as pending",
2536 event.instrument_id()
2537 );
2538 inner
2539 .borrow_mut()
2540 .pending_calcs
2541 .insert(event.instrument_id());
2542 }
2543
2544 if let Some(calculated_realized_pnl) =
2545 portfolio_clone.calculate_realized_pnl(&instrument_id, None)
2546 {
2547 inner
2548 .borrow_mut()
2549 .realized_pnls
2550 .insert(event.instrument_id(), calculated_realized_pnl);
2551 } else {
2552 log::warn!(
2553 "Failed to calculate realized PnL for {}, marking as pending",
2554 event.instrument_id()
2555 );
2556 inner
2557 .borrow_mut()
2558 .pending_calcs
2559 .insert(event.instrument_id());
2560 }
2561
2562 let account = { cache.borrow().account_owned(&account_id) };
2563 let account_state_to_publish = match account {
2564 Some(AccountAny::Margin(margin_account)) => {
2565 if margin_account.calculate_account_state {
2566 let instrument = { cache.borrow().instrument(&instrument_id).cloned() };
2567 if let Some(instrument) = instrument {
2568 let result = {
2569 let cache_ref = cache.borrow();
2570 let refs = cache_ref.positions_open(
2571 None,
2572 Some(&instrument_id),
2573 None,
2574 Some(&account_id),
2575 None,
2576 );
2577 let positions: Vec<&Position> = refs.iter().map(|r| &**r).collect();
2578 inner.borrow_mut().accounts.update_positions(
2579 &margin_account,
2580 &instrument,
2581 positions,
2582 clock.borrow().timestamp_ns(),
2583 )
2584 };
2585
2586 if let Some((margin_account, account_state)) = result {
2587 cache
2588 .borrow_mut()
2589 .update_account(&AccountAny::Margin(margin_account))
2590 .unwrap();
2591 Some(account_state)
2592 } else {
2593 margin_account.last_event()
2594 }
2595 } else {
2596 log::error!("Cannot update position: no instrument found for {instrument_id}");
2597 margin_account.last_event()
2598 }
2599 } else {
2600 margin_account.last_event()
2601 }
2602 }
2603 Some(account) => account.last_event(),
2604 None => {
2605 log::error!(
2606 "Cannot update position: no account registered for {}",
2607 event.account_id()
2608 );
2609 None
2610 }
2611 };
2612
2613 if let Some(account_state) = account_state_to_publish {
2614 msgbus::publish_account_state(
2615 format!("events.account.{account_id}").into(),
2616 &account_state,
2617 );
2618 }
2619}
2620
2621fn record_closed_position_pnl(
2622 cache: &Rc<RefCell<Cache>>,
2623 inner: &Rc<RefCell<PortfolioState>>,
2624 config: PortfolioConfig,
2625 event: &PositionEvent,
2626) {
2627 let position_id = match event {
2628 PositionEvent::PositionOpened(event) => event.position_id,
2629 PositionEvent::PositionChanged(event) => event.position_id,
2630 PositionEvent::PositionClosed(event) => event.position_id,
2631 PositionEvent::PositionAdjusted(event) => event.position_id,
2632 };
2633
2634 let cache_ref = cache.borrow();
2635 let Some(position) = cache_ref.position(&position_id) else {
2636 return;
2637 };
2638
2639 if !position.is_closed() {
2640 return;
2641 }
2642
2643 let Some(realized_pnl) = position.realized_pnl else {
2644 return;
2645 };
2646
2647 let mut inner_ref = inner.borrow_mut();
2648
2649 if !inner_ref
2650 .recorded_closed_position_cycles
2651 .insert((position.id, position.ts_opened))
2652 {
2653 return;
2654 }
2655
2656 let converted_pnl =
2657 converted_realized_pnl(&cache_ref, config, event, position_id, realized_pnl);
2658
2659 let ts_event = position.ts_last;
2660 inner_ref
2661 .analyzer
2662 .record_trade(&position.id, ts_event, &realized_pnl);
2663
2664 if let Some(converted_pnl) = converted_pnl {
2665 inner_ref
2666 .analyzer
2667 .record_trade(&position.id, ts_event, &converted_pnl);
2668 }
2669}
2670
2671fn converted_realized_pnl(
2672 cache_ref: &Cache,
2673 config: PortfolioConfig,
2674 event: &PositionEvent,
2675 position_id: PositionId,
2676 realized_pnl: Money,
2677) -> Option<Money> {
2678 let account = cache_ref.account(&event.account_id())?;
2679 let base_currency = account.base_currency()?;
2680
2681 if realized_pnl.currency == base_currency {
2682 return None;
2683 }
2684
2685 let xrate = if config.use_mark_xrates {
2686 cache_ref
2687 .get_mark_xrate(realized_pnl.currency, base_currency)
2688 .and_then(|xrate| Decimal::try_from(xrate).ok())
2689 } else {
2690 cache_ref.get_xrate(
2691 event.instrument_id().venue,
2692 realized_pnl.currency,
2693 base_currency,
2694 PriceType::Mid,
2695 )
2696 };
2697
2698 let Some(xrate) = xrate else {
2699 log::warn!(
2700 "Cannot record account-currency realized PnL for {position_id}: conversion failed from {} to {base_currency}",
2701 realized_pnl.currency
2702 );
2703 return None;
2704 };
2705
2706 let amount = (realized_pnl.as_decimal() * xrate).round_dp(u32::from(base_currency.precision));
2707 match Money::from_decimal(amount, base_currency) {
2708 Ok(amount) => Some(amount),
2709 Err(e) => {
2710 log::warn!("Cannot record account-currency realized PnL for {position_id}: {e}");
2711 None
2712 }
2713 }
2714}
2715
2716fn update_account(
2717 cache: &Rc<RefCell<Cache>>,
2718 inner: &Rc<RefCell<PortfolioState>>,
2719 event: &AccountState,
2720) {
2721 let already_applied = {
2722 cache
2723 .borrow()
2724 .account(&event.account_id)
2725 .and_then(|account| account.last_event())
2726 .is_some_and(|last_event| last_event.event_id == event.event_id)
2727 };
2728
2729 if !already_applied && let Err(e) = cache.borrow_mut().update_account_state(event) {
2730 log::error!("Failed to update account state: {e}");
2731 return;
2732 }
2733
2734 let mut inner_ref = inner.borrow_mut();
2736 let should_log = if inner_ref.min_account_state_logging_interval_ns > 0 {
2737 let current_ts = event.ts_init.as_u64();
2738 let last_ts = inner_ref
2739 .last_account_state_log_ts
2740 .get(&event.account_id)
2741 .copied()
2742 .unwrap_or(0);
2743
2744 if last_ts == 0 || (current_ts - last_ts) >= inner_ref.min_account_state_logging_interval_ns
2745 {
2746 inner_ref
2747 .last_account_state_log_ts
2748 .insert(event.account_id, current_ts);
2749 true
2750 } else {
2751 false
2752 }
2753 } else {
2754 true };
2756
2757 if should_log {
2758 log::info!("Updated {event}");
2759 }
2760}
2761
2762fn snapshot_timer_name(account_id: AccountId) -> String {
2763 format!("portfolio_snapshot.{account_id}")
2764}
2765
2766fn update_snapshot_timer_state(
2767 cache: &Rc<RefCell<Cache>>,
2768 clock: &Rc<RefCell<dyn Clock>>,
2769 inner: &Rc<RefCell<PortfolioState>>,
2770 config: PortfolioConfig,
2771 account_id: AccountId,
2772) {
2773 if config.snapshot_interval_ms.is_none() {
2774 return;
2775 }
2776
2777 let current_count = cache
2778 .borrow()
2779 .positions_open(None, None, None, Some(&account_id), None)
2780 .len();
2781
2782 let prev_count = inner
2783 .borrow()
2784 .account_open_positions
2785 .get(&account_id)
2786 .copied()
2787 .unwrap_or(0);
2788
2789 inner
2790 .borrow_mut()
2791 .account_open_positions
2792 .insert(account_id, current_count);
2793
2794 if prev_count == 0 && current_count > 0 {
2795 arm_snapshot_timer(cache, clock, inner, config, account_id);
2796 } else if prev_count > 0 && current_count == 0 {
2797 clock
2798 .borrow_mut()
2799 .cancel_timer(&snapshot_timer_name(account_id));
2800 }
2801}
2802
2803fn arm_snapshot_timer(
2804 cache: &Rc<RefCell<Cache>>,
2805 clock: &Rc<RefCell<dyn Clock>>,
2806 inner: &Rc<RefCell<PortfolioState>>,
2807 config: PortfolioConfig,
2808 account_id: AccountId,
2809) {
2810 let interval_ms = match config.snapshot_interval_ms {
2811 Some(ms) if ms > 0 => ms,
2812 _ => return,
2813 };
2814 let interval_ns = interval_ms * NANOSECONDS_IN_MILLISECOND;
2815 let timer_name = snapshot_timer_name(account_id);
2816
2817 let cache_weak = Rc::downgrade(cache);
2818 let clock_weak = Rc::downgrade(clock);
2819 let inner_weak = Rc::downgrade(inner);
2820
2821 let callback: Rc<dyn Fn(TimeEvent)> = Rc::new(move |event| {
2822 let cache = match cache_weak.upgrade() {
2823 Some(c) => c,
2824 None => return,
2825 };
2826 let clock = match clock_weak.upgrade() {
2827 Some(c) => c,
2828 None => return,
2829 };
2830 let inner = match inner_weak.upgrade() {
2831 Some(i) => i,
2832 None => return,
2833 };
2834 emit_snapshot(&cache, &clock, &inner, config, account_id, event.ts_event);
2835 });
2836
2837 if let Err(e) = clock.borrow_mut().set_timer_ns(
2838 &timer_name,
2839 interval_ns,
2840 None,
2841 None,
2842 Some(TimeEventCallback::from(callback)),
2843 Some(true),
2844 Some(false),
2845 ) {
2846 log::error!("Failed to arm portfolio snapshot timer for {account_id}: {e}");
2847 }
2848}
2849
2850fn emit_snapshot(
2851 cache: &Rc<RefCell<Cache>>,
2852 clock: &Rc<RefCell<dyn Clock>>,
2853 inner: &Rc<RefCell<PortfolioState>>,
2854 config: PortfolioConfig,
2855 account_id: AccountId,
2856 ts_event: nautilus_core::UnixNanos,
2857) {
2858 let mut portfolio = Portfolio {
2859 cache: cache.clone(),
2860 clock: clock.clone(),
2861 inner: inner.clone(),
2862 config,
2863 };
2864
2865 let mut snapshot = match portfolio.build_snapshot(&account_id) {
2866 Some(snapshot) => snapshot,
2867 None => return,
2868 };
2869 snapshot.ts_event = ts_event;
2873
2874 msgbus::publish_portfolio_snapshot(format!("events.portfolio.{account_id}").into(), &snapshot);
2875
2876 let mut inner_mut = inner.borrow_mut();
2877 push_bounded(
2878 &mut inner_mut.portfolio_snapshots,
2879 account_id,
2880 snapshot,
2881 SNAPSHOT_BUFFER_CAP,
2882 );
2883}
2884
2885fn push_bounded(
2887 snapshots: &mut AHashMap<AccountId, VecDeque<PortfolioSnapshot>>,
2888 account_id: AccountId,
2889 snapshot: PortfolioSnapshot,
2890 cap: usize,
2891) {
2892 let ring = snapshots.entry(account_id).or_default();
2893 if ring.len() == cap {
2894 ring.pop_front();
2895 }
2896 ring.push_back(snapshot);
2897}
2898
2899#[cfg(test)]
2900mod tests {
2901 use nautilus_core::{UUID4, UnixNanos};
2902 use nautilus_model::{enums::AccountType, identifiers::AccountId};
2903 use rstest::rstest;
2904
2905 use super::*;
2906
2907 fn mk_snapshot(seq: u64) -> PortfolioSnapshot {
2908 PortfolioSnapshot::new(
2909 AccountId::new("SIM-001"),
2910 AccountType::Cash,
2911 None,
2912 Vec::new(),
2913 Vec::new(),
2914 Vec::new(),
2915 Vec::new(),
2916 Vec::new(),
2917 UUID4::new(),
2918 UnixNanos::from(seq),
2919 UnixNanos::from(seq),
2920 )
2921 }
2922
2923 #[rstest]
2924 fn push_bounded_drops_oldest_when_at_cap() {
2925 let account_id = AccountId::new("SIM-001");
2926 let mut snapshots: AHashMap<AccountId, VecDeque<PortfolioSnapshot>> = AHashMap::new();
2927
2928 for seq in 0..5 {
2929 push_bounded(&mut snapshots, account_id, mk_snapshot(seq), 3);
2930 }
2931
2932 let ring = snapshots.get(&account_id).expect("ring exists");
2933 assert_eq!(ring.len(), 3);
2934 assert_eq!(ring.front().unwrap().ts_event, UnixNanos::from(2));
2935 assert_eq!(ring.back().unwrap().ts_event, UnixNanos::from(4));
2936 }
2937}