perf(slots): BAT-040 — slots RPC DB-level status 필터 (REQ-002/006/073, issue #25)

REQ-002/006 change-req(CHG-20260612-01)→구현→confirm-req-implemented,
REQ-073 change-req(CHG-20260612-02, 관찰 계약 불변 Implemented 유지) 전체 delivery.

- repository.py/in_memory_repository.py: SlotRepository.list_slots(instance_key,
  status_filter=None) — status 값 집합 필터, None=전체(기존 호출자 불변).
- postgresql.py: list_slots DB-level WHERE status = ANY(%s) (full scan + Python
  필터 제거). 스키마/마이그레이션 변경 없음.
- messaging_handlers.py: _FILTER_STATUS 매핑으로 filter→status 집합 위임,
  watchdog 갱신용 holding 조회를 all/holding fetch 결과에서 재사용(2nd 커넥션 생략).
- 관찰 가능한 RPC 계약(REQ-073 AC-1~6) 불변 — 동작 보존 최적화.
- tests: in-memory status_filter 5종 + PG 게이트 1종. 439 passed (+5), ruff clean.

REQ-002·006 Approved→Implemented, REQ-073 Implemented 유지, BAT-040 release-candidate.
issue #25 close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 19:27:51 +09:00
co-authored by Claude Opus 4.8
parent 14da195b1e
commit d737467d70
16 changed files with 479 additions and 27 deletions
@@ -0,0 +1,40 @@
# BAT-040 Design
- Status: confirmed
- Batch: BAT-040
## Architecture Summary
- status 필터링을 핸들러(Python)에서 저장소 경계로 내린다. `SlotRepository.list_slots` 에 optional `status_filter: set[str]` 를 추가하고(REQ-002 포트), PostgreSQL 은 `status = ANY(%s)`(REQ-006), in-memory 는 set 멤버십으로 필터한다.
- 핸들러(REQ-073)는 요청 filter 토큰을 status 값 집합으로 매핑해 `list_slots` 에 위임하고, watchdog 갱신용 holding 슬롯을 이미 fetch 한 결과에서 재사용할 수 있으면(all/holding) 두 번째 쿼리를 생략한다.
- 관찰 가능한 응답 계약은 불변 — 동작 보존 최적화.
## Changed Areas
- Modules:
- `trex_core/domain/slot/repository.py` — `list_slots(self, instance_key, status_filter: Optional[set[str]] = None)`
- `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터
- `trex_core/adapters/persistence/postgresql.py` — `status_filter` → `WHERE instance_key = %s AND status = ANY(%s)`
- `trex_core/application/messaging_handlers.py` — filter→status 집합 매핑 + 위임 + watchdog 재사용
- Tests:
- `tests/domain/`(in-memory status_filter), `tests/application/test_slots_handler.py`(filter/spy/키 회귀), `tests/adapters/persistence/`(PG 필터 게이트)
- Docs: 없음(관찰 계약·흐름 불변)
## Key Decisions
1. **`status_filter: Optional[set[str]]` (status 값 집합)**: 단일 status 가 아니라 집합 — `pending` 토큰이 `{pending_buy, pending_sell}` 두 status 로 매핑되기 때문. `None` = 전체(기존). 핸들러가 토큰→집합 매핑을 소유(저장소는 status 값만 안다).
2. **filter 토큰 매핑(핸들러)**: `inactive→{"inactive"}`, `pending→{"pending_buy","pending_sell"}`, `holding→{"holding"}`, `error→{"error"}`, `all`/미인식→`None`. REQ-073 AC-2(미인식 filter → all 일관 처리) 보존.
3. **PG `status = ANY(%s)`**: psycopg 가 Python list 를 PG array 로 어댑팅. `IN` 대신 `ANY` 로 단일 파라미터 바인딩. None 이면 기존 `WHERE instance_key = %s` 분기 유지.
4. **watchdog 두 번째 쿼리 생략 조건**: `status_filter is None`(all) 또는 filter=="holding" 이면 이미 fetch 한 `slots` 에 holding 이 전부 포함되므로 거기서 추출. 그 외(inactive/pending/error)는 holding 이 응답에 없으므로 기존 `list_holding_slots` 호출. spy=True 면 watchdog 자체를 갱신하지 않아 어느 경우든 두 번째 쿼리 없음.
5. **동작 보존**: 핸들러 출력(slots/available_slots/degraded/reason/message, spy 시맨틱)은 불변. AC-1~6 회귀로 보증.
## Edge Cases
- `list_slots` 무인자 호출자(reconcile/safety-sell/kill-switch): default `None` → 기존 전체 fetch, 동작 불변.
- filter=="all": status_filter=None → 전체 fetch, watchdog 은 fetch 결과에서 holding 추출(두 번째 쿼리 생략).
- filter=="holding": status_filter={"holding"} → holding 만 fetch, watchdog 재사용(두 번째 쿼리 생략).
- filter=="inactive"/"pending"/"error": holding 미포함 → watchdog 위해 `list_holding_slots` 호출(spy=false 시).
- 빈 결과(해당 status 슬롯 0): 빈 리스트 반환(정상).
## Architecture Impact
- none (runtime-flows 흐름·관찰 계약 불변. 내부 쿼리 위치/호출 수 최적화만)
## Reference Doc Update Plan
- project-structure.md: none
- runtime-flows.md: none (체결/슬롯 흐름 불변, 핸들러 내부 쿼리 최적화)
@@ -0,0 +1,33 @@
# BAT-040 Implementation
- Status: completed
- Batch: BAT-040
## Plan Summary
- `list_slots(status_filter)` 포트(REQ-002) + in-memory + PG(REQ-006) 구현, 핸들러(REQ-073) filter→status 집합 위임 + watchdog 조회 재사용, 테스트.
## Changed Files
- `trex_core/domain/slot/repository.py` — `list_slots(instance_key, status_filter=None)` Protocol 시그니처 + docstring
- `trex_core/domain/slot/in_memory_repository.py` — `status_filter` 멤버십 필터
- `trex_core/adapters/persistence/postgresql.py` — `list_slots` `status = ANY(%s)` 분기(None=전체)
- `trex_core/application/messaging_handlers.py` — `_FILTER_STATUS` 매핑 + `list_slots(status_filter)` 위임 + watchdog 조회 재사용(all/holding 시 두 번째 커넥션 생략)
- `tests/domain/slot/test_list_slots_filter.py` (신규, 5)
- `tests/adapters/persistence/test_postgresql_repository.py` — `test_list_slots_status_filter_pg` (PG 게이트)
## Execution Log
1. `repository.py`: Protocol `list_slots` 에 `status_filter: Optional[set[str]] = None` 추가.
2. `in_memory_repository.py`: `status_filter is None or s.status.value in status_filter` 필터.
3. `postgresql.py`: `status_filter` truthy 면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), 아니면 기존 전체 쿼리.
4. `messaging_handlers.py`: 모듈 상수 `_FILTER_STATUS`(inactive/pending/holding/error→status 집합, all/미인식→None). 핸들러가 `list_slots(instance_key, status_filter)` 위임. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출, 아니면 `list_holding_slots`(spy=true 면 둘 다 생략).
5. 테스트: in-memory 5종(None 전체/단일 status/pending 그룹/무매치 빈/instance 스코프), PG 게이트 1종.
## Validation
- `pytest tests/application/test_slots_handler.py` — 16 passed (filter/spy/응답 키/available_slots 회귀, 관찰 계약 불변 확인)
- `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규)
- `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5 in-memory; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트)
- `ruff check trex_core tests` — All checks passed
- 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch 등) 전수 회귀 없음(default None 동작 보존).
## Remaining Risks
- 없음. R-1(무인자 호출자 회귀)=default None 보존+전체 회귀 green, R-2(ANY 어댑팅)=psycopg list→array(PG 게이트 테스트, DSN 가용 시 실행), R-3(watchdog 재사용)=spy/holding 터치 테스트 회귀 green.
- PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 설정 환경에서 게이트 테스트로 수행(CI/로컬 DSN). in-memory parity 가 동작 계약을 보증.
@@ -0,0 +1,34 @@
# BAT-040: slots RPC per-request full scan 개선 — list_slots DB-level status 필터 (CHG-20260612-01)
- Status: release-candidate
- Profile: standard
- Source Discovery: 없음 (issue #25, REQ-002/006/073 change-req)
- Included REQ: REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02)
- Excluded REQ: 없음
## Summary
- `trex.slots.req` 핸들러가 매 요청마다 `list_slots`(전 행 fetch) 후 Python 에서 status 를 거르던 구조(issue #25)를 DB 레벨 필터로 바꾼다. `SlotRepository.list_slots` 에 `status_filter` 옵션을 추가(REQ-002 포트 + REQ-006 PG 구현)하고, 핸들러가 filter 토큰을 status 집합으로 매핑해 위임한다(REQ-073). watchdog 갱신용 holding 조회는 응답 필터가 이미 holding 을 포함한 경우(all/holding) fetch 결과를 재사용해 두 번째 커넥션을 생략한다.
## Scope
- In Scope:
- `trex_core/domain/slot/repository.py` — `SlotRepository.list_slots(instance_key, status_filter=None)` (REQ-002)
- `trex_core/domain/slot/in_memory_repository.py` — status_filter 구현 (REQ-002)
- `trex_core/adapters/persistence/postgresql.py` — `list_slots` DB-level `status = ANY(%s)` (REQ-006)
- `trex_core/application/messaging_handlers.py` — `make_slots_rpc_handler` filter→status 집합 매핑·위임 + watchdog 조회 재사용 (REQ-073)
- 테스트: in-memory status_filter 단위, PG 필터(게이트), 핸들러 filter/spy/응답 키 회귀
- Out of Scope:
- 관찰 가능한 RPC 계약 변경(REQ-073 AC-1~6 불변 — 동작 보존 최적화)
- 스키마/마이그레이션 변경(쿼리 한정)
- overlay(paused/warned) 서버측 필터(REQ-074 후속)
- 인덱스 추가/튜닝(별도 운영 판단)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 동작 보존 내부 최적화 — REQ-073 관찰 계약(filter 결과/키 집합/spy/degraded/available_slots) 불변. REQ-073 은 Implemented 유지(재검증), REQ-002/006 은 신규 AC-6 으로 Approved→Implemented 복귀.
- 도메인 포트 시그니처 변경(인터페이스 영향) + 다중 REQ 횡단 → minor-change fast-path 불가, standard profile.
- `list_holding_slots` 는 이미 status-scoped(full scan 아님). 주 개선은 `list_slots` full scan 제거 + all/holding 필터 시 두 번째 커넥션 생략.
@@ -0,0 +1,37 @@
# BAT-040 Planning
- Status: confirmed
- Batch: BAT-040
- Profile: standard
## Included REQ
- REQ-002 (CHG-20260612-01): `SlotRepository.list_slots(instance_key, status_filter=None)` 포트 + in-memory 구현. AC-6 신규.
- REQ-006 (CHG-20260612-01): PostgreSQL `list_slots` DB-level `status = ANY(%s)`. AC-6 신규.
- REQ-073 (CHG-20260612-02): 핸들러 filter→status 집합 매핑·위임 + watchdog 조회 재사용. 관찰 계약 불변, Implemented 유지.
## Delivery Plan
1. `repository.py` — `list_slots` Protocol 시그니처에 `status_filter: Optional[set[str]] = None` 추가(docstring 갱신).
2. `in_memory_repository.py` — `status_filter` 가 주어지면 `s.status.value in status_filter` 필터, None 이면 전체.
3. `postgresql.py` — `status_filter` 주어지면 `WHERE instance_key = %s AND status = ANY(%s)`(list 파라미터), None 이면 기존 전체 쿼리.
4. `messaging_handlers.py` — filter 토큰→status 집합 매핑(`inactive→{inactive}`, `pending→{pending_buy,pending_sell}`, `holding→{holding}`, `error→{error}`, all/미인식→None) 후 `list_slots(instance_key, status_filter)`. watchdog: `status_filter is None or filter=="holding"` 면 fetch 결과에서 holding 추출(두 번째 쿼리 생략), 아니면 `list_holding_slots`.
5. 테스트 — in-memory status_filter 단위, 핸들러 filter/spy/응답 키 회귀(기존), PG 필터(게이트, DSN 가용 시).
## Dependencies
- Internal:
- `list_slots` 호출자 전수(reconcile/safety-sell/kill-switch 등)는 무인자 → default None 으로 동작 불변(회귀 확인 필요)
- `SlotState` 값 문자열(`inactive/pending_buy/holding/pending_sell/error`)
- External: PostgreSQL(통합 게이트). 단위는 in-memory.
## Design Gate
- Design Required Before Implementation: yes
- Reason: 도메인 포트 시그니처 변경(인터페이스) + status 집합 매핑 규칙 + watchdog 쿼리 재사용 조건 + in-memory/PG parity 를 design 에서 확정.
## Milestones
1. Design confirmed — 포트 시그니처·status 집합 매핑·watchdog 재사용 조건 확정
2. Implementation — repository/in_memory/postgresql/handler + 테스트
3. Verification — REQ-002/006 AC-6 + REQ-073 AC-1~6 회귀 + 기존 list_slots 호출자 회귀 + ruff
## Risks
- R-1 (Medium): `list_slots` 무인자 호출자(reconcile 등)가 status_filter default 로 동작이 바뀌면 회귀 — 완화: default None = 기존 전체, 전수 호출 회귀 테스트.
- R-2 (Low): PG `ANY(%s)` 파라미터 어댑팅(list→array) — 완화: psycopg list→array 어댑팅 사용, PG 게이트 통합 테스트로 확인.
- R-3 (Low): watchdog 조회 재사용 조건 오류 시 holding 워치독 미갱신 — 완화: filter별 spy/watchdog 테스트로 holding 터치 보존 확인(REQ-073 AC-4).
@@ -0,0 +1,53 @@
# BAT-040 Verification
- Status: confirmed
- Batch: BAT-040
## Acceptance Mapping
- REQ-002 AC-6 (`list_slots(instance_key, status_filter)` 포트 + in-memory, None 전체):
- Evidence (`tests/domain/slot/test_list_slots_filter.py`):
- `test_none_returns_all`: None/무인자 → 전체 5 PASSED.
- `test_single_status_filter`: `{"holding"}`→1, `{"inactive"}`→3 PASSED.
- `test_pending_group_filter`: `{"pending_buy","pending_sell"}`→pending 1 PASSED.
- `test_empty_when_no_match`: `{"error"}`→[] PASSED.
- `test_instance_scoped`: 타 instance 0 PASSED.
- REQ-006 AC-6 (PostgreSQL `list_slots` DB-level `status = ANY(%s)`, None 전체):
- Evidence: `tests/adapters/persistence/test_postgresql_repository.py::test_list_slots_status_filter_pg` — None→전체 3, `{"inactive"}`→2, pending 그룹→1, `{"error"}`→[]. `TREX_POSTGRESQL_TEST_DSN` 설정 시 실행(현재 미설정 → skip, 전 PG 테스트와 동일 게이트). in-memory parity(REQ-002 AC-6)가 동작 계약을 보증.
- REQ-073 AC-1 (토폴로지 — `trex.slots.req` 존재):
- 변경 범위 밖(핸들러 내부 위임만). Evidence: 전체 회귀 439 passed.
- REQ-073 AC-2 (filter 동작 — inactive/pending/holding/all):
- Evidence: `test_filter_*` 5개 PASSED (`test_slots_handler.py`) — DB 위임 후에도 filter 결과 동일. 미인식 filter→None(all) 일관 처리 보존.
- REQ-073 AC-3 (응답 키 집합 — slots/available_slots/degraded/reason/message):
- Evidence: `test_response_keys_only_slots_health` PASSED — 키 집합 불변.
- REQ-073 AC-4 (spy=true 워치독 미갱신 / spy=false 갱신):
- Evidence: `test_spy_true_no_watchdog_update`, `test_spy_false_updates_watchdog` PASSED — watchdog 조회 재사용 최적화 후에도 holding 터치 시맨틱 보존.
- REQ-073 AC-5 (degraded 신호 전파):
- Evidence: `test_degraded_signal_propagated` PASSED.
- REQ-073 AC-6 (available_slots 값 규칙):
- Evidence: `test_available_slots_*` 3개 PASSED — capacity hint 불변.
## Evidence
- Test:
- `pytest tests/domain/slot/test_list_slots_filter.py` — 5 passed (신규)
- `pytest tests/application/test_slots_handler.py` — 16 passed (REQ-073 AC-1~6 회귀, 관찰 계약 불변)
- `pytest tests/` — **439 passed, 46 skipped** (BAT-039 434 → +5; PG status_filter 테스트는 DSN 부재로 skip, 동일 게이트)
- `ruff check trex_core tests` — All checks passed
- Manual / Architecture Impact (none):
- design.md Architecture Impact = none — `docs/project-structure.md`, `docs/runtime-flows.md` 동기화 불필요(관찰 계약·흐름 불변, 핸들러 내부 쿼리 위치/호출 수 최적화만).
- 무인자 `list_slots` 호출자(reconcile/safety-sell/kill-switch) default None 동작 보존 — 전체 회귀로 확인.
- 핸들러 watchdog 조회 재사용 조건(all/holding fetch 재사용, 그 외 list_holding_slots) 코드 리뷰 + AC-4 테스트로 확인.
## Result
- Ready for Release: true
- Blocking Issues: 없음
- PG `status = ANY` 경로의 라이브 검증은 `TREX_POSTGRESQL_TEST_DSN` 가용 환경의 게이트 테스트로 수행(REQ-006 기존 PG 테스트 전부 동일 조건). in-memory parity 가 release-ready 를 보증 — 차단 아님.
+1
View File
@@ -47,3 +47,4 @@
| BAT-037 | standard | release-candidate | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) |
| BAT-038 | standard | release-candidate | REQ-018 (CHG-20260612-01) | dcy-026 (issue #24) | [bat-038](./bat-038_20260612_sell-transaction-tax-fee/) |
| BAT-039 | standard | release-candidate | REQ-011 (CHG-20260612-01) | 없음 (issue #4) | [bat-039](./bat-039_20260612_paper-seq-redis-persistence/) |
| BAT-040 | standard | release-candidate | REQ-002 (CHG-20260612-01), REQ-006 (CHG-20260612-01), REQ-073 (CHG-20260612-02) | 없음 (issue #25) | [bat-040](./bat-040_20260612_slots-rpc-db-filter/) |
@@ -17,10 +17,11 @@
3. Given 드리프트 테스트, When `EXPECTED_SLOT_FIELDS` ↔ information_schema 대조, Then 일치하고 컬럼 누락 시 실패한다(dcy-001 OQ-2 DB-side 검사 실현).
4. Given 상태 변경, When transit_slot, Then `pg_advisory_xact_lock` 하 단일 트랜잭션으로 직렬화되고 lock 보유 중 외부 I/O 가 없다.
5. (CHG-20260610-02, dcy-023/issue #22) Given 마이그레이션 013(`DROP COLUMN IF EXISTS allocated_capital`, 멱등) 적용, Then `daily_slot` 에 `allocated_capital` 컬럼이 없고 persistence flat 매핑(SELECT/UPDATE/행 복원)에서 해당 컬럼 참조가 제거되며, `EXPECTED_SLOT_FIELDS` ↔ information_schema 드리프트 테스트가 green 이다. 적용은 코드 배포와 동시(`apply_migrations` 자동, dcy-023 OQ-2), 비가역 drop 이므로 적용 전 백업 절차를 release 문서에 명시한다.
6. (CHG-20260612-01, issue #25) Given PostgreSQL `list_slots(instance_key, status_filter)`, When `status_filter` 가 status 값 집합이면, Then `WHERE instance_key = %s AND status = ANY(%s)` 로 DB 에서 직접 필터링해 해당 행만 fetch 하고(전 행 fetch + Python 필터 제거), `None` 이면 기존대로 `WHERE instance_key = %s` 전체를 반환한다. 반환 결과는 in-memory 구현과 동일하다.
## Impacted Area
- Docs: docs/data-model.md(테이블/계약), dcy-001 부록 A(기준)
- Modules: trex_core/ 하위 persistence 어댑터(psycopg) + 마이그레이션 SQL; 도메인 패키지는 무의존 유지(NFR-1); (CHG-20260610-02) postgresql.py flat 매핑 allocated_capital 제거 + 013_drop_allocated_capital.sql
- Modules: trex_core/ 하위 persistence 어댑터(psycopg) + 마이그레이션 SQL; 도메인 패키지는 무의존 유지(NFR-1); (CHG-20260610-02) postgresql.py flat 매핑 allocated_capital 제거 + 013_drop_allocated_capital.sql; (CHG-20260612-01) postgresql.py `list_slots` DB-level status 필터(`status = ANY(%s)`)
- Tests: PostgreSQL repo 통합테스트 + 드리프트 테스트
## Notes
@@ -321,3 +322,50 @@
- Author: Justin Ko
- Type: status
- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-034(release-candidate) verification — allocated_capital 전 경로 제거(도메인/intent/영속/snapshot/CLI), 마이그레이션 013 테스트 DSN 멱등 round-trip + 컬럼 부재 확인, 전체 428 passed + PG 게이트 21 passed + ruff clean, grep 전수 0건. Blocking Issues 없음.
### CHG-20260612-01
- Date: 2026-06-12
- Author: Justin Ko
#### Change Summary
- (change-req, issue #25) PostgreSQL `list_slots` 에 `status_filter` 를 받아 `WHERE instance_key = %s AND status = ANY(%s)` 로 DB 레벨 필터링한다. slots RPC 의 per-request full scan(전 행 fetch + Python 필터)을 제거. `status_filter=None` 은 기존 전체 fetch. 스키마/마이그레이션 변경 없음(쿼리 한정). 신규 AC-6 → **Implemented → Approved 되돌림**.
#### Intent
- `trex.slots.req` 가 200-slot 인스턴스에서 매 요청마다 전 슬롯을 읽어오던 비용을 줄인다(issue #25 (1) DB 레벨 status 필터링). REQ-002 포트 확장에 대응하는 PostgreSQL 구현.
#### Acceptance Criteria Delta
- Added:
- AC-6: `list_slots(instance_key, status_filter)` DB-level `status = ANY(%s)` 필터 + `None` 전체. in-memory 와 결과 동일.
- Updated/Removed: 없음
#### Impacted Area Delta
- Docs: 없음
- Modules: `trex_core/adapters/persistence/postgresql.py`(`list_slots` 쿼리)
- Tests: PG `list_slots` status 필터 통합 테스트(PG 게이트), in-memory parity
#### Delivery Trace Delta
- Batches: BAT-040 (예정, issue #25)
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-040 에서 AC-6 + 기존 AC 회귀)
- Existing Implementation Invalidated: no (쿼리 확장, default None 기존 동작 보존). 신규 AC-6 미구현으로 Implemented 유지 불가 → Approved.
- Status Recommendation: Implemented → **Approved**
- Follow-up Action: confirm-req → BAT-040 구현·검증 → confirm-req-implemented
#### Validation Plan
- BAT-040: PG `list_slots` status 필터 통합(PG 게이트, DSN 가용 시) + in-memory parity 단위 + 전체 suite + ruff clean. 스키마 드리프트 테스트 회귀(컬럼 불변).
### CHG-20260612-IMPL
- Date: 2026-06-12
- Author: Justin Ko
- Type: status
- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-040(release-candidate) verification — AC-6 PG `list_slots` `status = ANY(%s)` 게이트 테스트(`test_list_slots_status_filter_pg`, DSN 가용 시 실행; 현재 skip) + in-memory parity(REQ-002 AC-6)로 동작 계약 보증, 스키마 변경 없음(쿼리 한정), 전체 439 passed + ruff clean. Blocking Issues 없음.
@@ -17,10 +17,11 @@
3. Given 공개 API, When `__init__.py` 의 `__all__` 을 확인, Then `PostgreSQLSlotRepository`·`check_slot_domain_schema_drift` 등 인프라 심볼이 노출되지 않는다.
4. Given `SlotService`, When 6 상태/9 base 전이(`closed→pending_buy` 포함, dcy-019)/advisory-lock 직렬화/`closed→idle` 재진입 +1/`→pending_buy` 시 slot `entry_count` +1(dcy-019)/`closed→pending_buy` clean-slate 점유 초기화/lock 보유 중 외부 I/O 금지 동작을 확인, Then 보존된다. (NFR-2 — 검증은 REQ-003)
5. (CHG-20260610-02, dcy-023/issue #22) Given allocated_capital 제거 후, Then `Slot` 모델과 `EXPECTED_SLOT_FIELDS` 에 `allocated_capital` 필드가 없고, `SlotService` 의 설정/전이 경로(buy 진입·전이 시 set/clear)에서 해당 파라미터가 제거된다.
6. (CHG-20260612-01, issue #25) Given `SlotRepository.list_slots(instance_key, status_filter=None)`, When `status_filter` 가 status 값 집합(예: `{"inactive"}`, `{"pending_buy","pending_sell"}`)이면, Then 해당 status 슬롯만 반환하고 `None`(미지정)이면 기존대로 전체를 반환한다(in-memory 구현 포함). 기존 무인자 호출자는 동작이 불변이다.
## Impacted Area
- Docs: docs/interface-contract.md, docs/data-model.md (계약 기준 — 큰 변경 없을 수 있음)
- Modules: trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py, (CHG-20260610-02) models.py `Slot`/`EXPECTED_SLOT_FIELDS` + service.py allocated_capital 제거
- Modules: trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py, (CHG-20260610-02) models.py `Slot`/`EXPECTED_SLOT_FIELDS` + service.py allocated_capital 제거, (CHG-20260612-01) repository.py `SlotRepository.list_slots` 시그니처 `status_filter` 옵션 + in_memory_repository.py 구현
- Tests: REQ-003 에서 이식 단위테스트 + 계약 테스트로 검증
## Notes
@@ -321,3 +322,50 @@
- Author: Justin Ko
- Type: status
- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-034(release-candidate) verification — allocated_capital 전 경로 제거(도메인/intent/영속/snapshot/CLI), 마이그레이션 013 테스트 DSN 멱등 round-trip + 컬럼 부재 확인, 전체 428 passed + PG 게이트 21 passed + ruff clean, grep 전수 0건. Blocking Issues 없음.
### CHG-20260612-01
- Date: 2026-06-12
- Author: Justin Ko
#### Change Summary
- (change-req, issue #25) `SlotRepository.list_slots` 에 optional `status_filter` 파라미터를 추가해 status 필터링을 저장소 경계로 내린다. 시그니처 `list_slots(instance_key, status_filter: Optional[set[str]] = None)` — `None` 은 기존(전체), 값 집합이면 해당 status 만 반환. in-memory 구현도 동일 의미로 갱신. slots RPC 핸들러(REQ-073)의 per-request full scan + Python 필터를 제거하기 위한 포트 확장. 신규 AC-6 추가 구현·검증 필요 → **Implemented → Approved 되돌림**.
#### Intent
- `trex.slots.req` 가 매 요청마다 전 슬롯을 fetch 한 뒤 Python 에서 거르던 구조(issue #25)를 DB 레벨 필터로 바꾸기 위해, 도메인 포트(`list_slots`)와 in-memory 더블을 먼저 확장한다. 기존 무인자 호출자(reconcile/safety-sell 등)는 default `None` 으로 동작 불변.
#### Acceptance Criteria Delta
- Added:
- AC-6: `list_slots(instance_key, status_filter)` — status 값 집합 필터 + `None` 전체, in-memory 포함, 기존 호출자 불변.
- Updated/Removed: 없음
#### Impacted Area Delta
- Docs: 없음
- Modules: `trex_core/domain/slot/repository.py`(Protocol 시그니처), `trex_core/domain/slot/in_memory_repository.py`(구현)
- Tests: in-memory `list_slots(status_filter)` 단위 — status 집합 필터 + None 전체 + 기존 무인자 회귀
#### Delivery Trace Delta
- Batches: BAT-040 (예정, issue #25)
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-040 에서 AC-6 + 기존 AC 회귀)
- Existing Implementation Invalidated: no (추가 파라미터는 default None 으로 기존 동작 보존). 단 신규 AC-6 미구현으로 Implemented 유지 불가 → Approved.
- Status Recommendation: Implemented → **Approved**
- Follow-up Action: confirm-req → BAT-040 구현·검증 → confirm-req-implemented
#### Validation Plan
- BAT-040: in-memory `list_slots` status 집합 필터 + None 전체 단위 테스트, 기존 무인자 호출 회귀, 전체 suite + ruff clean.
### CHG-20260612-IMPL
- Date: 2026-06-12
- Author: Justin Ko
- Type: status
- Summary: Approved → Implemented (confirm-req-implemented). 근거: BAT-040(release-candidate) verification — AC-6 `list_slots(status_filter)` in-memory 5종(None 전체/단일 status/pending 그룹/무매치/instance 스코프) PASSED, 무인자 호출자 default None 동작 보존(전체 439 passed), ruff clean. Blocking Issues 없음.
@@ -26,7 +26,7 @@
## Impacted Area
- Docs: docs/interface-contract.md(snapshot→slots 계약 — REQ-075), docs/runtime-flows.md
- Modules: trex_core/adapters/messaging/topology.py(큐명), trex_core/application/messaging_handlers.py(slots 핸들러·snapshot 핸들러 제거), trex_core/application/portfolio.py(`View` enum 폐기)
- Modules: trex_core/adapters/messaging/topology.py(큐명), trex_core/application/messaging_handlers.py(slots 핸들러·snapshot 핸들러 제거; (CHG-20260612-02) status 필터 repo 위임 + watchdog 조회 재사용), trex_core/application/portfolio.py(`View` enum 폐기)
- Tests: tests/application/(slots 핸들러 filter/spy/응답 키 집합), tests/adapters/messaging/(토폴로지)
## Notes
@@ -37,6 +37,45 @@
## Change Log
### CHG-20260612-02
- Date: 2026-06-12
- Author: Justin Ko
#### Change Summary
- (change-req, issue #25) slots RPC 핸들러(`make_slots_rpc_handler`)가 status 필터링을 `SlotRepository.list_slots(instance_key, status_filter=...)`(REQ-002/006) 에 위임해 per-request full scan + Python 필터를 제거한다. 추가로 watchdog 갱신용 holding 조회를, 응답 필터가 `all`/`holding` 이라 이미 holding 을 포함한 경우 fetch 결과를 재사용해 두 번째 커넥션을 생략한다(그 외 필터는 기존대로 `list_holding_slots`). **관찰 가능한 응답 계약(AC-1~6)은 불변** — filter 결과·키 집합·spy·degraded·available_slots 동일. AC delta 없음 → **Implemented 유지**(BAT-040 재검증).
#### Intent
- issue #25 (1) DB 레벨 status 필터 + (2) 두 쿼리 통합. orb-v1 이 매 cycle `{filter:"inactive"}` 로 요청할 때 200-slot 인스턴스에서 전 행 fetch 를 반복하던 비용 제거. 동작 보존 내부 최적화.
#### Acceptance Criteria Delta
- Added/Updated/Removed: 없음 (AC-1~6 관찰 계약 불변, 핸들러 내부 위임만)
#### Impacted Area Delta
- Docs: 없음
- Modules: `trex_core/application/messaging_handlers.py`(`make_slots_rpc_handler` — `list_slots(status_filter)` 위임 + watchdog 조회 재사용)
- Tests: 기존 filter/spy/응답 키 집합 테스트 회귀 + status_filter 위임 확인
#### Delivery Trace Delta
- Batches: BAT-040 (예정, issue #25)
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-040 에서 filter/spy/degraded/available_slots 회귀)
- Existing Implementation Invalidated: no (관찰 응답 동일, 내부 위임만 — AC evidence 유효)
- Status Recommendation: **Implemented 유지**
- Follow-up Action: BAT-040 구현·검증으로 재확인(REQ-002/006 Implemented 복귀와 동일 배치)
#### Validation Plan
- BAT-040: 기존 `test_filter_*`/`test_spy_*`/`test_response_keys_*`/`test_available_slots_*` 회귀 green + status_filter 위임 경로(필터별 list_slots 인자) 확인.
### CHG-20260612-IMPL
- Date: 2026-06-12
- Author: Justin Ko
+2 -2
View File
@@ -159,8 +159,8 @@
## Recent Change Log Summary
- 2026-06-12 :: REQ-002·006 Approved → **Implemented** (confirm-req-implemented, BAT-040, issue #25): slots RPC full scan 개선 구현·검증 완료. `list_slots(status_filter)` DB-level 필터 + 핸들러 위임 + watchdog 조회 재사용. 439 passed (in-memory 필터 5 신규, 핸들러 16 회귀, PG 게이트 1), ruff clean. REQ-073 관찰 계약 불변 Implemented 유지. Blocking Issues 없음. issue #25 close.
- 2026-06-12 :: REQ-002·006·073 change-req (CHG-20260612-01/01/02, issue #25): slots RPC per-request full scan 개선 — `list_slots(status_filter)` DB-level status 필터 + 핸들러 위임 + watchdog 조회 재사용. REQ-002/006 AC-6 신규 → Implemented → **Approved** 되돌림(default None 동작 보존). REQ-073 관찰 계약 불변 → **Implemented 유지**.
- 2026-06-12 :: REQ-011 Approved → **Implemented** (confirm-req-implemented, BAT-039): paper order seq Redis 영속화 구현·검증 완료. 434 passed (seq store 5 신규 포함), 재시작 단조·correlation_id 비충돌·in-memory 폴백 green. Blocking Issues 없음. issue #4 close.
- 2026-06-12 :: REQ-011 change-req (CHG-20260612-01, issue #4): paper 브로커 order seq Redis 영속화 — 재시작 후 `correlation_id` 충돌(매도 원장 누락) 방지. optional seq store(RedisSeqStore INCR + in-memory 폴백) 주입. AC-4 신규. 기존 비동기 계약 불변이나 신규 AC 미구현 → Implemented → **Approved** 되돌림. BAT-039 구현·검증 후 복귀. paper 한정(프로덕션 버그 아님).
- 2026-06-12 :: REQ-018 Approved → **Implemented** (confirm-req-implemented, BAT-038): 매도 증권거래세 정책 계산 + `tax=0.0` 제거 구현·검증 완료. 429 passed (fee·tax 정책 6 신규 포함), issue #24 1,376.9원 회귀 green, `tax=0.0` 하드코딩 grep 0건, runtime-flows 동기화. Blocking Issues 없음.
- 2026-06-12 :: REQ-018 change-req (CHG-20260612-01, dcy-026/issue #24): 매도 증권거래세 누락 정정 — `FillStatus.tax` 매도 정책 세율(0.2%) 계산, `tax=0.0` 하드코딩(paper/kis) 제거, 단일 정책 지점 + env(`TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`). AC-3 갱신·AC-6~9 신규. `tax=0` evidence 무효 → Implemented → **Approved** 되돌림. BAT-038 구현·검증 후 복귀.
- 2026-06-12 :: REQ-073 Approved → **Implemented** (confirm-req-implemented, BAT-037): `available_slots: int|null` capacity hint 추가. AC-3 반전·AC-6 신규. 16 tests passed (AC-6 3개 포함), 423 전체 회귀 없음. Blocking Issues 없음.
@@ -71,6 +71,19 @@ def test_buy_intent_idempotent_on_pg(pg_clean):
assert len(pending) == 1 # no double transition
def test_list_slots_status_filter_pg(pg_clean):
# REQ-006 CHG-20260612-01 (issue #25): DB-level status filter (status = ANY).
repo, svc, intents = _wire(pg_clean)
repo.reconcile_universe(INST, _SYMS) # 3 inactive
intents.submit_buy(BuyIntent(intent_id="pg-f1", qty=10, portfolio_id="PF-1")) # PF-1 -> pending_buy
assert len(repo.list_slots(INST)) == 3 # None -> all
assert len(repo.list_slots(INST, status_filter={"inactive"})) == 2
pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"})
assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY
assert repo.list_slots(INST, status_filter={"error"}) == []
def test_buy_by_slot_id_contract_pg(pg_clean):
# dcy-020/REQ-061: slot_id is the canonical buy key against real PG.
repo, svc, intents = _wire(pg_clean)
@@ -0,0 +1,70 @@
"""list_slots status_filter (REQ-002/006 CHG-20260612-01, issue #25).
Pushes status filtering to the repository boundary so the slots RPC handler no
longer fetches every row and filters in Python. None preserves prior behaviour
(all slots), so existing unfiltered callers are unaffected.
"""
from __future__ import annotations
import pytest
from trex_core.domain.slot import SlotService, SlotState
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
INST = "inst-1"
@pytest.fixture
def repo():
return InMemorySlotRepository()
def _seed(repo):
"""3 inactive + 1 holding + 1 pending_buy for INST."""
svc = SlotService(repo, instance_key=INST)
repo.reconcile_universe(INST, {f"pf-{i}": "SYM" for i in range(5)})
# pf-0 -> holding
s0 = repo.find_slot_by_portfolio(INST, "pf-0")
svc.transit_slot(s0.slot_id, s0.status, SlotState.PENDING_BUY,
reason="r", actor="a", symbol="005930")
svc.transit_slot(s0.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
reason="r", actor="a", avg_price=70_000)
# pf-1 -> pending_buy
s1 = repo.find_slot_by_portfolio(INST, "pf-1")
svc.transit_slot(s1.slot_id, s1.status, SlotState.PENDING_BUY,
reason="r", actor="a", symbol="005930")
# pf-2,3,4 remain inactive
return svc
def test_none_returns_all(repo):
_seed(repo)
assert len(repo.list_slots(INST)) == 5
assert len(repo.list_slots(INST, status_filter=None)) == 5
def test_single_status_filter(repo):
_seed(repo)
holding = repo.list_slots(INST, status_filter={"holding"})
assert len(holding) == 1 and holding[0].status is SlotState.HOLDING
inactive = repo.list_slots(INST, status_filter={"inactive"})
assert len(inactive) == 3
assert all(s.status is SlotState.INACTIVE for s in inactive)
def test_pending_group_filter(repo):
_seed(repo)
pending = repo.list_slots(INST, status_filter={"pending_buy", "pending_sell"})
assert len(pending) == 1 and pending[0].status is SlotState.PENDING_BUY
def test_empty_when_no_match(repo):
_seed(repo)
assert repo.list_slots(INST, status_filter={"error"}) == []
def test_instance_scoped(repo):
_seed(repo)
# other instance has no slots
assert repo.list_slots("other", status_filter={"holding"}) == []
+17 -5
View File
@@ -66,13 +66,25 @@ class PostgreSQLSlotRepository:
conn.rollback()
raise
def list_slots(self, instance_key: str) -> list[Slot]:
def list_slots(
self, instance_key: str, status_filter: Optional[set[str]] = None
) -> list[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",
(instance_key,),
)
if status_filter:
# issue #25: filter by status at the DB instead of fetching
# every row and filtering in Python. ANY(%s) binds the set
# as a single PG array parameter.
cur.execute(
f"SELECT {_SLOT_COLUMNS} FROM daily_slot "
"WHERE instance_key = %s AND status = ANY(%s)",
(instance_key, list(status_filter)),
)
else:
cur.execute(
f"SELECT {_SLOT_COLUMNS} FROM daily_slot WHERE instance_key = %s",
(instance_key,),
)
return [self._row_to_slot(r) for r in cur.fetchall()]
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]:
+24 -13
View File
@@ -168,6 +168,17 @@ def _handle_update_thresholds(
# ---- slots RPC handler (REQ-073/074) ----------------------------------------
# Maps a `trex.slots.req` filter token to the set of slot status values it
# selects (issue #25 — pushed to the repository via list_slots status_filter).
# "all" and unrecognized tokens map to None (no filter — REQ-073 AC-2).
_FILTER_STATUS: dict[str, set[str]] = {
"inactive": {"inactive"},
"pending": {"pending_buy", "pending_sell"},
"holding": {"holding"},
"error": {"error"},
}
def make_slots_rpc_handler(
slot_repo: SlotRepository,
*,
@@ -196,21 +207,21 @@ def make_slots_rpc_handler(
filter_val = str(body.get("filter") or "all").lower()
spy = bool(body.get("spy", False))
# Retrieve and filter slots by status
slots = slot_repo.list_slots(instance_key)
if filter_val == "inactive":
slots = [s for s in slots if s.status.value == "inactive"]
elif filter_val == "pending":
slots = [s for s in slots if s.status.value in ("pending_buy", "pending_sell")]
elif filter_val == "holding":
slots = [s for s in slots if s.status.value == "holding"]
elif filter_val == "error":
slots = [s for s in slots if s.status.value == "error"]
# "all" or unrecognized → no filter (REQ-073 AC-2)
# issue #25: filter by status at the DB (REQ-002/006 list_slots
# status_filter) instead of fetching every row and filtering in Python.
# "all"/unrecognized → None (no filter, REQ-073 AC-2).
status_filter = _FILTER_STATUS.get(filter_val)
slots = slot_repo.list_slots(instance_key, status_filter=status_filter)
# REQ-034 CHG-01: update watchdog for holding slots unless spy=True
# REQ-034 CHG-01: update watchdog for holding slots unless spy=True.
# When the fetched set already contains every holding slot (filter "all"
# or "holding"), reuse it and skip the second connection (issue #25 (2)).
if not spy and watchdog_store is not None:
for s in slot_repo.list_holding_slots(instance_key):
if status_filter is None or filter_val == "holding":
holding_slots = [s for s in slots if s.status.value == "holding"]
else:
holding_slots = slot_repo.list_holding_slots(instance_key)
for s in holding_slots:
watchdog_store.update(str(s.slot_id))
# Build merged slot rows (REQ-074)
@@ -51,8 +51,14 @@ class InMemorySlotRepository:
"""Test seeding helper — insert a slot row directly."""
self._slots[slot.slot_id] = slot
def list_slots(self, instance_key: str) -> list[Slot]:
return [s for s in self._slots.values() if s.instance_key == instance_key]
def list_slots(
self, instance_key: str, status_filter: Optional[set[str]] = None
) -> list[Slot]:
return [
s for s in self._slots.values()
if s.instance_key == instance_key
and (status_filter is None or s.status.value in status_filter)
]
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]:
return self._slots.get(slot_id)
+9 -2
View File
@@ -37,8 +37,15 @@ class SlotRepository(Protocol):
...
# --- daily_slot CRUD ---
def list_slots(self, instance_key: str) -> list[Slot]:
"""Return all slots owned by this instance."""
def list_slots(
self, instance_key: str, status_filter: Optional[set[str]] = None
) -> list[Slot]:
"""Return slots owned by this instance.
status_filter: when given, only slots whose status value is in the set
are returned (filtered at the storage boundary — REQ-006 pushes this to
SQL). None returns all slots (prior behaviour). issue #25.
"""
...
def fetch_slot(self, slot_id: UUID) -> Optional[Slot]: