@@ -24,6 +24,7 @@ The portfolio can satisfy queries for account information, margin balances,
24
24
total risk exposures and total net positions.
25
25
"""
26
26
27
+ import pickle
27
28
import warnings
28
29
from collections import defaultdict
29
30
from decimal import Decimal
@@ -147,6 +148,7 @@ cdef class Portfolio(PortfolioFacade):
147
148
self._unrealized_pnls: dict[InstrumentId , Money] = {}
148
149
self._realized_pnls: dict[InstrumentId , Money] = {}
149
150
self._snapshot_realized_pnls: dict[InstrumentId , Money] = {}
151
+ self._snapshot_processed_counts: dict[PositionId , int] = {}
150
152
self._net_positions: dict[InstrumentId , Decimal] = {}
151
153
self._bet_positions: dict[InstrumentId , object] = {}
152
154
self._index_bet_positions: dict[InstrumentId , set[PositionId]] = defaultdict(set )
@@ -683,6 +685,7 @@ cdef class Portfolio(PortfolioFacade):
683
685
self._unrealized_pnls.clear()
684
686
self._pending_calcs.clear()
685
687
self._snapshot_realized_pnls.clear()
688
+ self._snapshot_processed_counts.clear()
686
689
self.analyzer.reset()
687
690
688
691
self.initialized = False
@@ -709,44 +712,120 @@ cdef class Portfolio(PortfolioFacade):
709
712
self._reset()
710
713
self._log.info("DISPOSED")
711
714
712
- cdef void _ensure_snapshot_pnls_cached (self ):
713
- if self ._snapshot_realized_pnls:
714
- return # Already cached
715
+ cdef void _ensure_snapshot_pnls_cached_for (self , InstrumentId instrument_id ):
716
+ # Get all positions for this instrument (both open and closed)
717
+ cdef set [PositionId] instrument_position_ids = self ._cache.position_ids( venue = None , instrument_id = instrument_id)
715
718
716
- cdef list [Position] all_snapshots = self ._cache.position_snapshots()
717
- cdef Position snapshot
718
- cdef InstrumentId instrument_id
719
- cdef Money existing_pnl
719
+ # Get snapshot position IDs for this instrument from index
720
+ cdef set [PositionId] snapshot_position_ids = self ._cache.position_snapshot_ids(instrument_id)
720
721
721
- for snapshot in all_snapshots:
722
- if snapshot.realized_pnl is None :
723
- continue
722
+ # Combine all position IDs (active and snapshot)
723
+ cdef set [PositionId] position_ids = set ()
724
724
725
- instrument_id = snapshot.instrument_id
726
- existing_pnl = self ._snapshot_realized_pnls.get(instrument_id )
725
+ if instrument_position_ids:
726
+ position_ids.update(instrument_position_ids )
727
727
728
- if existing_pnl is not None :
729
- if existing_pnl.currency == snapshot.realized_pnl.currency:
730
- # Same currency, add the amounts
731
- self ._snapshot_realized_pnls[instrument_id] = Money(
732
- existing_pnl.as_double() + snapshot.realized_pnl.as_double(),
733
- existing_pnl.currency,
734
- )
728
+ position_ids.update(snapshot_position_ids)
729
+
730
+ if not position_ids:
731
+ # Clear stale cached total when no positions or snapshots exist
732
+ self ._snapshot_realized_pnls.pop(instrument_id, None )
733
+ return # No positions or snapshots for this instrument
734
+
735
+ cdef bint rebuild = False
736
+
737
+ cdef:
738
+ PositionId position_id
739
+ list position_id_snapshots
740
+ int prev_count
741
+ int curr_count
742
+
743
+ # Detect purge/reset (count regression) to trigger full rebuild for this instrument
744
+ for position_id in position_ids:
745
+ position_id_snapshots = self ._cache.position_snapshot_bytes(position_id)
746
+ curr_count = len (position_id_snapshots)
747
+ prev_count = self ._snapshot_processed_counts.get(position_id, 0 )
748
+ if prev_count > curr_count:
749
+ rebuild = True
750
+ break
751
+
752
+ cdef:
753
+ Money existing_pnl
754
+ Position snapshot
755
+
756
+ if rebuild:
757
+ # Rebuild from scratch for this instrument
758
+ self ._snapshot_realized_pnls.pop(instrument_id, None )
759
+
760
+ for position_id in position_ids:
761
+ position_id_snapshots = self ._cache.position_snapshot_bytes(position_id)
762
+ curr_count = len (position_id_snapshots)
763
+ if curr_count:
764
+ for s in position_id_snapshots:
765
+ snapshot = pickle.loads(s)
766
+ if snapshot.realized_pnl is None :
767
+ continue
768
+
769
+ # Aggregate if same currency; otherwise log and keep existing
770
+ existing_pnl = self ._snapshot_realized_pnls.get(instrument_id)
771
+ if existing_pnl is not None :
772
+ if existing_pnl.currency == snapshot.realized_pnl.currency:
773
+ self ._snapshot_realized_pnls[instrument_id] = Money(
774
+ existing_pnl.as_double() + snapshot.realized_pnl.as_double(),
775
+ existing_pnl.currency,
776
+ )
777
+ else :
778
+ self ._log.warning(
779
+ f" Cannot aggregate snapshot PnLs with different currencies for {instrument_id}: "
780
+ f" {existing_pnl.currency} vs {snapshot.realized_pnl.currency}" ,
781
+ )
782
+ else :
783
+ self ._snapshot_realized_pnls[instrument_id] = snapshot.realized_pnl
784
+ self ._snapshot_processed_counts[position_id] = curr_count
785
+ return
786
+
787
+ # Incremental path: only unpickle new entries
788
+ for position_id in position_ids:
789
+ position_id_snapshots = self ._cache.position_snapshot_bytes(position_id)
790
+ curr_count = len (position_id_snapshots)
791
+ if curr_count == 0 :
792
+ continue
793
+ prev_count = self ._snapshot_processed_counts.get(position_id, 0 )
794
+ if prev_count >= curr_count:
795
+ continue
796
+ for idx in range (prev_count, curr_count):
797
+ snapshot = pickle.loads(position_id_snapshots[idx])
798
+ if snapshot.realized_pnl is None :
799
+ continue
800
+ existing_pnl = self ._snapshot_realized_pnls.get(instrument_id)
801
+ if existing_pnl is not None :
802
+ if existing_pnl.currency == snapshot.realized_pnl.currency:
803
+ self ._snapshot_realized_pnls[instrument_id] = Money(
804
+ existing_pnl.as_double() + snapshot.realized_pnl.as_double(),
805
+ existing_pnl.currency,
806
+ )
807
+ else :
808
+ self ._log.warning(
809
+ f" Cannot aggregate snapshot PnLs with different currencies for {instrument_id}: "
810
+ f" {existing_pnl.currency} vs {snapshot.realized_pnl.currency}" ,
811
+ )
735
812
else :
736
- # Different currencies - log warning
737
- self ._log.warning(
738
- f" Cannot aggregate snapshot PnLs with different currencies for {instrument_id}: "
739
- f" {existing_pnl.currency} vs {snapshot.realized_pnl.currency}" ,
740
- )
741
- else :
742
- # First snapshot for this instrument
743
- self ._snapshot_realized_pnls[instrument_id] = Money(
744
- snapshot.realized_pnl.as_double(),
745
- snapshot.realized_pnl.currency,
746
- )
813
+ self ._snapshot_realized_pnls[instrument_id] = snapshot.realized_pnl
814
+ self ._snapshot_processed_counts[position_id] = curr_count
815
+
816
+ # Prune stale entries from processed counts
817
+ cdef PositionId stale_position_id
818
+ cdef list [PositionId] stale_ids = []
747
819
748
- if self ._debug and self ._snapshot_realized_pnls:
749
- self ._log.debug(f" Cached snapshot realized PnLs for {len(self._snapshot_realized_pnls)} instruments" )
820
+ for stale_position_id in self ._snapshot_processed_counts:
821
+ if stale_position_id not in position_ids:
822
+ stale_ids.append(stale_position_id)
823
+
824
+ for stale_position_id in stale_ids:
825
+ self ._snapshot_processed_counts.pop(stale_position_id, None )
826
+
827
+ if self ._debug and self ._snapshot_realized_pnls.get(instrument_id) is not None :
828
+ self ._log.debug(f" Cached snapshot realized PnL for {instrument_id}" )
750
829
751
830
# -- QUERIES --------------------------------------------------------------------------------------
752
831
@@ -1533,8 +1612,8 @@ cdef class Portfolio(PortfolioFacade):
1533
1612
else :
1534
1613
currency = instrument.get_cost_currency()
1535
1614
1536
- # Ensure snapshot PnLs are cached (one-time cost )
1537
- self ._ensure_snapshot_pnls_cached( )
1615
+ # Ensure snapshot PnLs for this instrument are cached (incremental )
1616
+ self ._ensure_snapshot_pnls_cached_for(instrument_id )
1538
1617
1539
1618
cdef:
1540
1619
list [Position] positions
@@ -1551,6 +1630,9 @@ cdef class Portfolio(PortfolioFacade):
1551
1630
instrument_id = instrument_id,
1552
1631
)
1553
1632
1633
+ if self ._debug:
1634
+ self ._log.debug(f" Found {len(positions)} positions for {instrument_id}" )
1635
+
1554
1636
if not positions:
1555
1637
# Check if we have cached snapshot PnL for this instrument
1556
1638
cached_snapshot_pnl = self ._snapshot_realized_pnls.get(instrument_id)
@@ -1578,6 +1660,8 @@ cdef class Portfolio(PortfolioFacade):
1578
1660
if cached_snapshot_pnl_for_instrument is None :
1579
1661
pass # No snapshot PnL to add
1580
1662
elif cached_snapshot_pnl_for_instrument.currency == currency:
1663
+ if self ._debug:
1664
+ self ._log.debug(f" Adding cached snapshot PnL: {cached_snapshot_pnl_for_instrument}" )
1581
1665
total_pnl += cached_snapshot_pnl_for_instrument.as_double()
1582
1666
else :
1583
1667
snapshot_xrate = self ._cache.get_xrate(
0 commit comments