1use 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 #[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(); }
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 *unrealized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64();
448 continue;
449 }
450
451 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(); }
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 *realized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64();
486 continue;
487 }
488
489 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; };
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()); }
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; };
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; }
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; }
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, 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 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(); }
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)) .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 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, 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; }
1026
1027 if position.side == PositionSide::Flat {
1028 continue; }
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; };
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, 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; }
1112
1113 if position.realized_pnl.is_none() {
1114 continue; }
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; }
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, }
1199 }
1200}
1201
1202fn 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, "e.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 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(); }
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; }
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; };
1383
1384 if matches!(event, OrderEventAny::Rejected(_)) && order.order_type() != OrderType::StopLimit {
1385 return; }
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; }
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)) .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#[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 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 #[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 #[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 #[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 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_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 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 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 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 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 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 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_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 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 portfolio
2279 .cache
2280 .borrow_mut()
2281 .add_order(order.clone(), None, None, true)
2282 .unwrap();
2283
2284 portfolio.initialize_orders();
2286
2287 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 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 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 let last = get_quote_tick(&instrument_audusd, 250001.0, 250002.0, 1.0, 1.0);
2362
2363 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!(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 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 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 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_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 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 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 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_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 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 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 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_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 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 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_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 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 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 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_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 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 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_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 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 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 portfolio.update_position(&PositionEvent::PositionChanged(position1_changed));
3116
3117 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 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 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 let position1_closed = get_close_position(&position1);
3274 portfolio.update_position(&PositionEvent::PositionClosed(position1_closed));
3275
3276 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 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_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 assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new());
3494 }
3495}