feat(slot): slot entry_count + closed-slot reallocation (dcy-019, BAT-025) + REL-005
dcy-019/issue #18: slot 자원을 진입 횟수 기준으로 고르게 재사용하고 idle 소진 시 closed 슬롯을 직접 재할당한다. 거버넌스는 전부 change-req(신규 REQ 없음). - 도메인(REQ-002): Slot.entry_count(기본 0) + EXPECTED_SLOT_FIELDS, 상태 머신 closed→pending_buy 신규 전이(base 8→9, ALLOWED_TRANSITIONS 14→15), _apply_transition_fields closed→pending_buy clean-slate + →pending_buy 시 +1 - 영속화(REQ-006): 멱등 마이그레이션 008(entry_count), flat 매핑, find_allocatable_slot(idle∪closed, entry_count→updated_at→slot_id) + in-memory 동등 - admission(REQ-007): _admit_buy find_idle_slot→find_allocatable_slot + from_state=slot.status - 테스트(REQ-003): 전이 카운트/계약 가드 정합 + test_slot_entry_count.py / test_buy_admission_reallocation.py - 문서(REQ-009): data-model.md / runtime-flows.md 정합 - 기존 daily_symbol_entries(symbol-scoped) 분리·미변경 BAT-025(release-candidate) delivery + change-req REQ-002/003/006/007/009(CHG-20260609-01). REL-005(confirmed, app-service): BAT-024+BAT-025 묶음. operator-facing run-the-app 스모크로 enter --forced-close-at(BAT-024)·closed 재할당(BAT-025) 실 데몬 검증 (실 PG 마이그레이션 008 round-trip 포함). 검증: 단위 366 passed / 35 skipped(KIS sandbox 1건 deselect — live .env 환경 의존), ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ _SLOT_COLUMNS = (
|
||||
"slot_id, trading_date, status, symbol, qty, avg_price, limit_price, "
|
||||
"allocated_capital, order_id, strategy_id, broker_order_id, portfolio_id, "
|
||||
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
|
||||
"created_at, updated_at"
|
||||
"created_at, updated_at, entry_count"
|
||||
)
|
||||
|
||||
|
||||
@@ -116,6 +116,21 @@ class PostgreSQLSlotRepository:
|
||||
row = cur.fetchone()
|
||||
return self._row_to_slot(row) if row else None
|
||||
|
||||
def find_allocatable_slot(self, trading_date: date) -> Optional[Slot]:
|
||||
# dcy-019 / REQ-006: idle ∪ closed candidates, ordered by entry_count asc
|
||||
# → updated_at asc → slot_id (deterministic, OQ-2). Concurrency on the
|
||||
# last candidate handled by the caller (advisory lock + retry, NFR-1).
|
||||
with self._psycopg.connect(self._dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
|
||||
"WHERE trading_date = %s AND status IN ('idle','closed') "
|
||||
"ORDER BY entry_count ASC, updated_at ASC, slot_id ASC LIMIT 1",
|
||||
(trading_date,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return self._row_to_slot(row) if row else None
|
||||
|
||||
def update_slot(self, slot: Slot) -> None:
|
||||
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
|
||||
with conn.cursor() as cur:
|
||||
@@ -124,13 +139,14 @@ class PostgreSQLSlotRepository:
|
||||
"limit_price=%s, allocated_capital=%s, order_id=%s, strategy_id=%s, "
|
||||
"broker_order_id=%s, portfolio_id=%s, sell_strategy_id=%s, "
|
||||
"forced_close_at=%s, stop_loss_price=%s, take_profit_price=%s, "
|
||||
"updated_at=%s WHERE slot_id=%s",
|
||||
"entry_count=%s, updated_at=%s WHERE slot_id=%s",
|
||||
(
|
||||
slot.status.value, slot.symbol, slot.qty, slot.avg_price,
|
||||
slot.limit_price, slot.allocated_capital, slot.order_id,
|
||||
slot.strategy_id, slot.broker_order_id, slot.portfolio_id,
|
||||
slot.sell_strategy_id, slot.forced_close_at,
|
||||
slot.stop_loss_price, slot.take_profit_price,
|
||||
slot.entry_count,
|
||||
slot.updated_at or datetime.now(tz=timezone.utc), slot.slot_id,
|
||||
),
|
||||
)
|
||||
@@ -239,7 +255,7 @@ class PostgreSQLSlotRepository:
|
||||
# avg_price(5) limit_price(6) allocated_capital(7) order_id(8)
|
||||
# strategy_id(9) broker_order_id(10) portfolio_id(11) sell_strategy_id(12)
|
||||
# forced_close_at(13) stop_loss_price(14) take_profit_price(15)
|
||||
# created_at(16) updated_at(17)
|
||||
# created_at(16) updated_at(17) entry_count(18)
|
||||
return Slot(
|
||||
slot_id=row[0] if isinstance(row[0], UUID) else UUID(str(row[0])),
|
||||
trading_date=row[1],
|
||||
@@ -259,6 +275,7 @@ class PostgreSQLSlotRepository:
|
||||
take_profit_price=d(row[15]),
|
||||
created_at=row[16],
|
||||
updated_at=row[17],
|
||||
entry_count=int(row[18]) if row[18] is not None else 0,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user