Skip to content

Commit 02c8061

Browse files
committed
feat(portfolio): show and persist untracked positions
Split tracked vs. untracked positions while loading portfolio data. Render untracked holdings in a Not tracked section after tracked rows. Include untracked positions in position snapshot persistence without changing trading filters.
1 parent 7cca0df commit 02c8061

1 file changed

Lines changed: 66 additions & 24 deletions

File tree

thetagang/portfolio_manager.py

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def __init__(
103103
self.qualified_contracts: Dict[int, Contract] = {}
104104
self.dry_run = dry_run
105105
self.regime_rebalance_order_ref_prefix = "tg:regime-rebalance"
106+
self.last_untracked_positions: Dict[str, List[PortfolioItem]] = {}
106107

107108
def get_short_calls(
108109
self, portfolio_positions: Dict[str, List[PortfolioItem]]
@@ -361,22 +362,32 @@ def get_symbols(self) -> List[str]:
361362
def filter_positions(
362363
self, portfolio_positions: List[PortfolioItem]
363364
) -> List[PortfolioItem]:
365+
filtered_positions, _ = self.partition_positions(portfolio_positions)
366+
return filtered_positions
367+
368+
def partition_positions(
369+
self, portfolio_positions: List[PortfolioItem]
370+
) -> Tuple[List[PortfolioItem], List[PortfolioItem]]:
364371
symbols = self.get_symbols()
365-
return [
366-
item
367-
for item in portfolio_positions
368-
if item.account == self.account_number
369-
and (
372+
tracked_positions: List[PortfolioItem] = []
373+
untracked_positions: List[PortfolioItem] = []
374+
for item in portfolio_positions:
375+
if item.account != self.account_number or item.position == 0:
376+
continue
377+
if (
370378
item.contract.symbol in symbols
371379
or item.contract.symbol == "VIX"
372380
or item.contract.symbol == self.config.cash_management.cash_fund
373-
)
374-
and item.position != 0
375-
]
381+
):
382+
tracked_positions.append(item)
383+
else:
384+
untracked_positions.append(item)
385+
return (tracked_positions, untracked_positions)
376386

377387
async def get_portfolio_positions(self) -> Dict[str, List[PortfolioItem]]:
378388
attempts = 3
379389
symbols = set(self.get_symbols())
390+
self.last_untracked_positions = {}
380391

381392
for attempt in range(1, attempts + 1):
382393
try:
@@ -397,8 +408,13 @@ async def get_portfolio_positions(self) -> Dict[str, List[PortfolioItem]]:
397408
continue
398409

399410
portfolio_positions = self.ibkr.portfolio(account=self.account_number)
400-
filtered_positions = self.filter_positions(portfolio_positions)
411+
filtered_positions, untracked_positions = self.partition_positions(
412+
portfolio_positions
413+
)
401414
portfolio_by_symbol = portfolio_positions_to_dict(filtered_positions)
415+
self.last_untracked_positions = portfolio_positions_to_dict(
416+
untracked_positions
417+
)
402418
filtered_conids = {item.contract.conId for item in filtered_positions}
403419

404420
if portfolio_by_symbol:
@@ -549,9 +565,18 @@ async def summarize_account(
549565
log.print(Panel(table))
550566

551567
portfolio_positions = await self.get_portfolio_positions()
568+
untracked_positions = self.last_untracked_positions
552569
if self.data_store:
553570
self.data_store.record_account_snapshot(account_summary)
554-
self.data_store.record_positions_snapshot(portfolio_positions)
571+
combined_positions: Dict[str, List[PortfolioItem]] = dict(
572+
portfolio_positions
573+
)
574+
for symbol, positions in untracked_positions.items():
575+
if symbol in combined_positions:
576+
combined_positions[symbol].extend(positions)
577+
else:
578+
combined_positions[symbol] = positions
579+
self.data_store.record_positions_snapshot(combined_positions)
555580

556581
position_values: Dict[int, Dict[str, str]] = {}
557582

@@ -597,11 +622,13 @@ async def load_position_task(pos: PortfolioItem) -> None:
597622
pos.contract.lastTradeDateOrContractMonth
598623
)
599624

600-
tasks: List[Coroutine[Any, Any, None]] = [
601-
load_position_task(position)
602-
for _, positions in portfolio_positions.items()
603-
for position in positions
604-
]
625+
tasks: List[Coroutine[Any, Any, None]] = []
626+
for _, positions in portfolio_positions.items():
627+
for position in positions:
628+
tasks.append(load_position_task(position))
629+
for _, positions in untracked_positions.items():
630+
for position in positions:
631+
tasks.append(load_position_task(position))
605632
await log.track_async(tasks, "Loading portfolio positions...")
606633

607634
table = Table(
@@ -621,24 +648,21 @@ async def load_position_task(pos: PortfolioItem) -> None:
621648
table.add_column("Exp", justify="right")
622649
table.add_column("DTE", justify="right")
623650
table.add_column("ITM?")
624-
first = True
625-
for symbol, position in portfolio_positions.items():
626-
if not first:
627-
table.add_section()
628-
first = False
651+
652+
def getval(col: str, conId: int) -> str:
653+
return position_values[conId][col]
654+
655+
def add_symbol_positions(symbol: str, positions: List[PortfolioItem]) -> None:
629656
table.add_row(symbol)
630657
sorted_positions = sorted(
631-
position,
658+
positions,
632659
key=lambda p: (
633660
option_dte(p.contract.lastTradeDateOrContractMonth)
634661
if isinstance(p.contract, Option)
635662
else -1
636663
), # Keep stonks on top
637664
)
638665

639-
def getval(col: str, conId: int) -> str:
640-
return position_values[conId][col]
641-
642666
for pos in sorted_positions:
643667
conId = pos.contract.conId
644668
if isinstance(pos.contract, Stock):
@@ -670,6 +694,24 @@ def getval(col: str, conId: int) -> str:
670694
getval("itm?", conId),
671695
)
672696

697+
first = True
698+
for symbol, position in portfolio_positions.items():
699+
if not first:
700+
table.add_section()
701+
first = False
702+
add_symbol_positions(symbol, position)
703+
704+
if untracked_positions:
705+
table.add_section()
706+
table.add_row("Not tracked")
707+
table.add_section()
708+
first_untracked = True
709+
for symbol, position in untracked_positions.items():
710+
if not first_untracked:
711+
table.add_section()
712+
first_untracked = False
713+
add_symbol_positions(symbol, position)
714+
673715
log.print(table)
674716

675717
return (account_summary, portfolio_positions)

0 commit comments

Comments
 (0)