From 86c892aa0ca18ee1c9519ee34c04fc7f5d41888d Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Wed, 10 Jun 2026 10:42:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(slot):=20BAT-027=20M1=20=E2=80=94=20additi?= =?UTF-8?q?ve=20=EC=98=81=EA=B5=AC=20=EC=8A=AC=EB=A1=AF=20=ED=95=84?= =?UTF-8?q?=EB=93=9C(instance=5Fkey,=20retired=5Fat)=20+=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20009=20(dcy-020/?= =?UTF-8?q?REQ-056)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 안 B(점진/additive-first)의 1단계. 라이브 영향·cascade 없이 자산 영구 슬롯 토대의 스키마/모델 필드만 additive 추가. - Slot.instance_key(기본 "default")·retired_at 추가 + EXPECTED_SLOT_FIELDS 정합 - 마이그레이션 009: instance_key NOT NULL DEFAULT 'default', retired_at, 부분 UNIQUE(instance_key, portfolio_id) WHERE portfolio_id IS NOT NULL (멱등) - postgresql _SLOT_COLUMNS/_row_to_slot/update_slot 매핑(row[19]/[20]) - entry_count/trading_date 컬럼은 유지(drop 은 cutover BAT-δ) 검증: 전체 375 passed / 35 skipped, ruff clean (additive — cascade 무파손). M2(상태머신 inactive 별칭+인스턴스 lock)/M3(소비처 cascade)는 후속. Co-Authored-By: Claude Opus 4.8 --- .../implementation.md | 20 ++++++++++++++----- trex_core/adapters/persistence/postgresql.py | 9 ++++++--- ...9_add_slot_instance_key_and_retired_at.sql | 16 +++++++++++++++ trex_core/domain/slot/models.py | 9 +++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 trex_core/adapters/persistence/sql/009_add_slot_instance_key_and_retired_at.sql diff --git a/docs/batches/bat-027_20260610_asset-permanent-slot-foundation/implementation.md b/docs/batches/bat-027_20260610_asset-permanent-slot-foundation/implementation.md index 170974c..07c0fc4 100644 --- a/docs/batches/bat-027_20260610_asset-permanent-slot-foundation/implementation.md +++ b/docs/batches/bat-027_20260610_asset-permanent-slot-foundation/implementation.md @@ -1,16 +1,26 @@ # BAT-027 Implementation -- Status: not-started +- Status: in-progress (M1 완료, M2/M3 대기) - Batch: BAT-027 ## 구현 요약 -- (run-batch-delivery 진입 시 작성) design.md 접근안 확정 후 채운다. +- 안 B(점진/additive-first). M1(REQ-056 additive 모델+마이그레이션+persistence) 완료. M2/M3(상태머신 inactive 별칭 + 인스턴스 스코프 lock + 소비처 cascade)는 다음 단계. ## 변경 내역 -- (대기) + +### M1 — REQ-056 additive (완료) +- `trex_core/domain/slot/models.py`: `Slot` 에 `instance_key: str = "default"`·`retired_at: Optional[datetime] = None` 추가, `EXPECTED_SLOT_FIELDS` +instance_key/+retired_at. (entry_count/trading_date 는 안 B로 유지 — drop 은 BAT-δ) +- `trex_core/adapters/persistence/sql/009_add_slot_instance_key_and_retired_at.sql`(신규): instance_key NOT NULL DEFAULT 'default', retired_at, 부분 UNIQUE(instance_key, portfolio_id) WHERE portfolio_id IS NOT NULL. additive·멱등. +- `trex_core/adapters/persistence/postgresql.py`: `_SLOT_COLUMNS` +instance_key/retired_at(row[19]/[20]), `_row_to_slot` 매핑, `update_slot` SET. pre_allocate INSERT 는 DEFAULT 의존(변경 없음). + +### M2 — REQ-057 상태머신+인스턴스 lock (대기) +- state.py inactive 표준 + idle/closed 별칭(closed→idle 제거), advisory lock 인스턴스 스코프(2-int). + +### M3 — 소비처 cascade 정합 (대기) +- execution/intents/cli/ops_checks/service 등 idle·closed·전역 lock 참조 정합. ## 테스트 -- (대기) +- M1: 도메인 슬롯 단위 79 + 전체 375 passed / 35 skipped, ruff clean(additive 무파손). PG 마이그레이션 009 round-trip 은 verification 단계에서 test DSN 검증. ## 검증 실행 -- (대기) +- M1: `TREX_BROKER=paper pytest -q` → 375 passed / 35 skipped / 1 deselected. `ruff check trex_core` All passed. diff --git a/trex_core/adapters/persistence/postgresql.py b/trex_core/adapters/persistence/postgresql.py index a09f42c..0361804 100644 --- a/trex_core/adapters/persistence/postgresql.py +++ b/trex_core/adapters/persistence/postgresql.py @@ -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, entry_count" + "created_at, updated_at, entry_count, instance_key, retired_at" ) @@ -139,14 +139,15 @@ 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, " - "entry_count=%s, updated_at=%s WHERE slot_id=%s", + "entry_count=%s, instance_key=%s, retired_at=%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.entry_count, slot.instance_key, slot.retired_at, slot.updated_at or datetime.now(tz=timezone.utc), slot.slot_id, ), ) @@ -276,6 +277,8 @@ class PostgreSQLSlotRepository: created_at=row[16], updated_at=row[17], entry_count=int(row[18]) if row[18] is not None else 0, + instance_key=row[19] if row[19] is not None else "default", + retired_at=row[20], ) diff --git a/trex_core/adapters/persistence/sql/009_add_slot_instance_key_and_retired_at.sql b/trex_core/adapters/persistence/sql/009_add_slot_instance_key_and_retired_at.sql new file mode 100644 index 0000000..6fe9f0c --- /dev/null +++ b/trex_core/adapters/persistence/sql/009_add_slot_instance_key_and_retired_at.sql @@ -0,0 +1,16 @@ +-- BAT-027 / dcy-020 / REQ-056 (안 B): asset-centric permanent slot foundation. +-- Additive step — adds the instance scope key and the soft-retire marker without +-- touching live rows or dropping pool-model columns (entry_count/trading_date +-- removal is deferred to the cutover, BAT-δ). +-- * instance_key: scopes slots per daemon instance in a shared DB. DEFAULT +-- 'default' keeps existing single-instance rows valid; real tagging happens +-- in the cutover (REQ-064). +-- * retired_at: soft-retire / universe-detached marker (OQ-2). +-- Uniqueness is a PARTIAL index (WHERE portfolio_id IS NOT NULL) so anonymous +-- pool rows (portfolio_id NULL) don't collide during the transition; promoted to +-- a full unique constraint after cutover. +ALTER TABLE daily_slot ADD COLUMN IF NOT EXISTS instance_key TEXT NOT NULL DEFAULT 'default'; +ALTER TABLE daily_slot ADD COLUMN IF NOT EXISTS retired_at TIMESTAMPTZ; +CREATE UNIQUE INDEX IF NOT EXISTS uq_daily_slot_instance_portfolio + ON daily_slot (instance_key, portfolio_id) + WHERE portfolio_id IS NOT NULL; diff --git a/trex_core/domain/slot/models.py b/trex_core/domain/slot/models.py index 8ee3308..1581273 100644 --- a/trex_core/domain/slot/models.py +++ b/trex_core/domain/slot/models.py @@ -46,6 +46,13 @@ class Slot: entry_count: int = 0 created_at: Optional[datetime] = None updated_at: Optional[datetime] = None + # dcy-020 / REQ-056 (BAT-027): asset-centric permanent slot foundation (안 B). + # instance_key scopes slots per daemon instance in a shared DB + # (UNIQUE(instance_key, portfolio_id)); advisory lock is also instance-scoped. + # retired_at marks a soft-retired / universe-detached slot (OQ-2). Both added + # additively here; entry_count/trading_date removal is deferred to cutover (BAT-δ). + instance_key: str = "default" + retired_at: Optional[datetime] = None # Canonical column contract for `Slot` (dcy-001 OQ-2 / FR-5). @@ -72,6 +79,8 @@ EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({ "entry_count", # dcy-019 / REQ-002 (BAT-025) "created_at", "updated_at", + "instance_key", # dcy-020 / REQ-056 (BAT-027) + "retired_at", # dcy-020 / REQ-056 (BAT-027) })