fix(reconcile): symbol을 슬롯 생성 시 함께 저장 + NULL 슬롯 backfill
reconcile_universe 시그니처를 set[str] → dict[str, str] (portfolio_id→symbol)로 변경. 기존 symbol=NULL 슬롯은 다음 reconcile 실행 시 자동 backfill. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,7 +118,7 @@ class PostgreSQLSlotRepository:
|
||||
return [self._row_to_slot(r) for r in cur.fetchall()]
|
||||
|
||||
def reconcile_universe(
|
||||
self, instance_key: str, universe_portfolio_ids: set[str],
|
||||
self, instance_key: str, universe: dict[str, str],
|
||||
*, allow_retire: bool = True,
|
||||
) -> dict[str, int]:
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
@@ -131,24 +131,38 @@ class PostgreSQLSlotRepository:
|
||||
(instance_key,),
|
||||
)
|
||||
existing = {r[0]: (r[1], r[2]) for r in cur.fetchall()}
|
||||
# backfill symbol on slots that were created before this field was populated
|
||||
cur.execute(
|
||||
"SELECT portfolio_id FROM daily_slot "
|
||||
"WHERE instance_key = %s AND portfolio_id IS NOT NULL AND symbol IS NULL",
|
||||
(instance_key,),
|
||||
)
|
||||
null_symbol_pids = {r[0] for r in cur.fetchall()}
|
||||
for pid in null_symbol_pids:
|
||||
if pid in universe:
|
||||
cur.execute(
|
||||
"UPDATE daily_slot SET symbol = %s, updated_at = %s "
|
||||
"WHERE instance_key = %s AND portfolio_id = %s AND symbol IS NULL",
|
||||
(universe[pid], now, instance_key, pid),
|
||||
)
|
||||
# create new asset slots (idempotent via partial unique)
|
||||
for pid in universe_portfolio_ids:
|
||||
for pid, symbol in universe.items():
|
||||
if pid in existing:
|
||||
continue
|
||||
cur.execute(
|
||||
"INSERT INTO daily_slot "
|
||||
"(slot_id, status, instance_key, portfolio_id, "
|
||||
"created_at, updated_at) VALUES (%s, 'inactive', %s, %s, %s, %s) "
|
||||
"(slot_id, status, symbol, instance_key, portfolio_id, "
|
||||
"created_at, updated_at) VALUES (%s, 'inactive', %s, %s, %s, %s, %s) "
|
||||
"ON CONFLICT (instance_key, portfolio_id) "
|
||||
"WHERE portfolio_id IS NOT NULL DO NOTHING",
|
||||
(uuid4(), instance_key, pid, now, now),
|
||||
(uuid4(), symbol, instance_key, pid, now, now),
|
||||
)
|
||||
created += cur.rowcount
|
||||
# retire / tombstone assets that left the universe (skipped under
|
||||
# the mass-disappearance guard, dcy-020/OQ-2 D3)
|
||||
if allow_retire:
|
||||
for pid, (status, retired_at) in existing.items():
|
||||
if pid in universe_portfolio_ids:
|
||||
if pid in universe:
|
||||
continue
|
||||
if status in ("holding", "pending_buy", "pending_sell"):
|
||||
tombstoned += 1
|
||||
|
||||
@@ -128,7 +128,11 @@ class SnapshotScheduler:
|
||||
|
||||
# ---- asset-centric slot reconcile (dcy-020/REQ-058, BAT-028) ----------
|
||||
def _reconcile_slots(self, items: list) -> None:
|
||||
universe = {it.portfolio_id for it in items if getattr(it, "portfolio_id", None)}
|
||||
universe = {
|
||||
it.portfolio_id: it.symbol
|
||||
for it in items
|
||||
if getattr(it, "portfolio_id", None)
|
||||
}
|
||||
if not universe:
|
||||
_log.warning("slot reconcile skipped: empty universe (degrade guard)")
|
||||
return
|
||||
|
||||
@@ -84,7 +84,7 @@ class InMemorySlotRepository:
|
||||
]
|
||||
|
||||
def reconcile_universe(
|
||||
self, instance_key: str, universe_portfolio_ids: set[str],
|
||||
self, instance_key: str, universe: dict[str, str],
|
||||
*, allow_retire: bool = True,
|
||||
) -> dict[str, int]:
|
||||
existing = {
|
||||
@@ -93,13 +93,18 @@ class InMemorySlotRepository:
|
||||
}
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
created = retired = tombstoned = 0
|
||||
# backfill symbol on slots that were created before this field was populated
|
||||
for s in self._slots.values():
|
||||
if (s.instance_key == instance_key and s.portfolio_id in universe
|
||||
and s.symbol is None):
|
||||
self._slots[s.slot_id] = replace(s, symbol=universe[s.portfolio_id], updated_at=now)
|
||||
# create asset slots for newly-appeared universe assets (idempotent)
|
||||
for pid in universe_portfolio_ids:
|
||||
for pid, symbol in universe.items():
|
||||
if pid not in existing:
|
||||
sid = uuid4()
|
||||
self._slots[sid] = Slot(
|
||||
slot_id=sid, status=SlotState.INACTIVE,
|
||||
instance_key=instance_key, portfolio_id=pid,
|
||||
instance_key=instance_key, portfolio_id=pid, symbol=symbol,
|
||||
created_at=now, updated_at=now,
|
||||
)
|
||||
created += 1
|
||||
@@ -107,7 +112,7 @@ class InMemorySlotRepository:
|
||||
# mass-disappearance guard, dcy-020/OQ-2 D3 — caller passes allow_retire=False)
|
||||
if allow_retire:
|
||||
for pid, s in existing.items():
|
||||
if pid in universe_portfolio_ids:
|
||||
if pid in universe:
|
||||
continue
|
||||
if s.status in self._ACTIVE:
|
||||
tombstoned += 1 # holding/pending — keep (safety-sell), no retire
|
||||
|
||||
@@ -66,13 +66,14 @@ class SlotRepository(Protocol):
|
||||
...
|
||||
|
||||
def reconcile_universe(
|
||||
self, instance_key: str, universe_portfolio_ids: set[str],
|
||||
self, instance_key: str, universe: dict[str, str],
|
||||
*, allow_retire: bool = True,
|
||||
) -> dict[str, int]:
|
||||
"""Reconcile the slot set to the portfolio-hub universe (dcy-020/REQ-058).
|
||||
|
||||
universe: {portfolio_id -> symbol} mapping from PortfolioItem.
|
||||
- new portfolio_id (no slot for instance) -> create an inactive asset slot
|
||||
(idempotent; keyed by (instance_key, portfolio_id), permanent).
|
||||
with symbol set (idempotent; keyed by (instance_key, portfolio_id), permanent).
|
||||
- slot whose portfolio_id left the universe AND is inactive AND not yet
|
||||
retired -> soft-retire (set retired_at).
|
||||
- slot that left the universe but is holding/pending -> tombstone (left
|
||||
|
||||
Reference in New Issue
Block a user