feat(slot): BAT-028 M2/M3 — universe reconcile + 자산 중심 admission (dcy-020/REQ-058·059)
config 게이트 이중 경로(max_active_slots 설정 시 자산 중심, 미설정 시 legacy pool). 운영 데몬은 cutover(BAT-δ) 전까지 pool 무중단. - repository: find_slot_by_portfolio·count_active(holding+pending_buy+pending_sell)· reconcile_universe(생성 멱등/soft-retire/tombstone, allow_retire 가드) — protocol+in_memory+PG (PG: ON CONFLICT (instance_key, portfolio_id) WHERE portfolio_id IS NOT NULL) - snapshot_scheduler: run_once swap 직후 _reconcile_slots(빈 universe skip, ≥50%↓ retire 보류) - intents: _admit_buy 모드 분기 + _admit_buy_asset_centric(활성화/already_active/ capacity_full/unknown_portfolio/over-cap, 인스턴스 lock) - daemon: instance_key·max_active_slots·reconcile_enabled·trading_date_fn 배선 - 신규 테스트: test_universe_reconcile(6), test_asset_centric_admission(6), PG reconcile(1) 검증: 단위 387 passed / 35 skipped, PG-gated 17 passed, ruff clean. legacy 무파손. REQ-058/059/060 은 Approved 유지(자산 중심 경로 도입 검증; default 전환·cutover 는 BAT-δ). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,74 @@ class PostgreSQLSlotRepository:
|
||||
row = cur.fetchone()
|
||||
return self._row_to_slot(row) if row else None
|
||||
|
||||
# --- asset-centric (dcy-020 / BAT-028) ---
|
||||
def find_slot_by_portfolio(self, instance_key: str, portfolio_id: str) -> Optional[Slot]:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
|
||||
"WHERE instance_key = %s AND portfolio_id = %s LIMIT 1",
|
||||
(instance_key, portfolio_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return self._row_to_slot(row) if row else None
|
||||
|
||||
def count_active(self, instance_key: str) -> int:
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM daily_slot WHERE instance_key = %s "
|
||||
"AND status IN ('holding','pending_buy','pending_sell')",
|
||||
(instance_key,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
def reconcile_universe(
|
||||
self, instance_key: str, trading_date: date, universe_portfolio_ids: set[str],
|
||||
*, allow_retire: bool = True,
|
||||
) -> dict[str, int]:
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
created = retired = tombstoned = 0
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT portfolio_id, status, retired_at FROM daily_slot "
|
||||
"WHERE instance_key = %s AND portfolio_id IS NOT NULL",
|
||||
(instance_key,),
|
||||
)
|
||||
existing = {r[0]: (r[1], r[2]) for r in cur.fetchall()}
|
||||
# create new asset slots (idempotent via partial unique)
|
||||
for pid in universe_portfolio_ids:
|
||||
if pid in existing:
|
||||
continue
|
||||
cur.execute(
|
||||
"INSERT INTO daily_slot "
|
||||
"(slot_id, trading_date, status, instance_key, portfolio_id, "
|
||||
"created_at, updated_at) VALUES (%s, %s, 'idle', %s, %s, %s, %s) "
|
||||
"ON CONFLICT (instance_key, portfolio_id) "
|
||||
"WHERE portfolio_id IS NOT NULL DO NOTHING",
|
||||
(uuid4(), trading_date, 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:
|
||||
continue
|
||||
if status in ("holding", "pending_buy", "pending_sell"):
|
||||
tombstoned += 1
|
||||
elif retired_at is None:
|
||||
cur.execute(
|
||||
"UPDATE daily_slot SET retired_at = %s, updated_at = %s "
|
||||
"WHERE instance_key = %s AND portfolio_id = %s",
|
||||
(now, now, instance_key, pid),
|
||||
)
|
||||
retired += 1
|
||||
conn.commit()
|
||||
return {"created": created, "retired": retired, "tombstoned": tombstoned}
|
||||
|
||||
def update_slot(self, slot: Slot) -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
|
||||
Reference in New Issue
Block a user