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>
193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# Trex Core Data Model
|
|
|
|
- Baseline Mode: mixed
|
|
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
|
- Status: draft
|
|
- Owner: project maintainers
|
|
- Last Updated (KST): 2026-06-08 (BAT-019)
|
|
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery)
|
|
|
|
## Project Summary
|
|
- trex 제품군의 공용 베이스로, 첫 도메인으로 trex-orbit 의 일일 거래 슬롯 생명주기(slot) 순수 도메인을 storage-agnostic 하게 이식한 코어다.
|
|
|
|
## Primary Domain
|
|
- 자동매매 일일 거래 슬롯 생명주기 도메인 (equity trading slot lifecycle)
|
|
|
|
## Primary Persistence Backend
|
|
- PostgreSQL (BAT-002, REQ-006; BAT-006 확장) — 코어 데몬의 durable 단일 writer. 도메인은 여전히 storage-agnostic(SlotRepository Protocol); in-memory 어댑터는 테스트용으로 잔존. 슬라이스 1 테이블: daily_slot / daily_symbol_entries / slot_transition_log / intent_log / ledger_outbox (daily_slot_archive·prune 은 범위 밖).
|
|
|
|
## Purpose
|
|
- This document captures the approved cross-cutting data model baseline for the project.
|
|
- It is a shared reference for REQ drafting, batch design, implementation, verification, migration planning, and handoff.
|
|
|
|
## Scope
|
|
- Covers core entities, relationships, states, persistence mappings, and shared consistency rules.
|
|
- Does not duplicate full functional acceptance criteria already owned by REQ documents.
|
|
- Does not replace ORM definitions, migration files, or generated schema artifacts.
|
|
|
|
## Model Summary
|
|
|
|
- Slot :: 하나의 일일 거래 슬롯의 현재 생명주기 상태/점유 정보(source of truth)
|
|
- SlotTransitionRecord :: 슬롯 상태 전이의 append-only 감사 기록
|
|
- daily_symbol_entries (카운터) :: (trading_date, symbol) 별 재진입 횟수(결정 입력)
|
|
- SlotCapacity (계산 스냅샷) :: idle/holding/pending + scarcity_ratio (영속 엔티티 아님)
|
|
|
|
## Entity: `Slot`
|
|
|
|
- Purpose: 하나의 일일 거래 슬롯의 현재 생명주기 상태와 점유 정보를 담는 source of truth.
|
|
- Source of Truth: daily_slot (향후 테이블) / 현재는 in-memory dict
|
|
- Lifecycle: idle -> pending_buy -> holding -> pending_sell -> closed -> (recovery) idle ; closed -> pending_buy (직접 재할당, dcy-019) ; 모든 비-error 상태 -> error ; error -> {idle, holding}
|
|
|
|
### Key Fields
|
|
|
|
- slot_id :: UUID :: 슬롯 식별자(PK)
|
|
- trading_date :: date :: 거래일 — 설정 타임존(`trading_timezone`, 기본 `Asia/Seoul`) 기준 날짜로 계산(dcy-014/REQ-005). 시스템 UTC 날짜가 아니라 거래 캘린더 일자와 일치. 일일 preallocation 이 TZ 일자 경계마다 멱등 생성.
|
|
- status :: SlotState :: 6 상태(idle/pending_buy/holding/pending_sell/closed/error) 중 현재 상태
|
|
- symbol :: str? :: 점유 종목
|
|
- qty / avg_price :: numeric? :: 수량 / 평단
|
|
- limit_price :: numeric? :: 현재 pending 주문 지정가(side 무관, dcy-012/REQ-002 — 과거 `entry_price`에서 rename). pending 진입(매수/매도 지정가) 시 set, pending 이탈(→holding/→closed) 시 None 으로 clear → 매수 지정가가 매도로 누수되지 않음(issue #10). None 이면 dispatch 시 시장가.
|
|
- allocated_capital :: numeric? :: 배정 자본
|
|
- order_id / broker_order_id :: str? :: 주문 식별자(자유 주석 / 브로커 핸들)
|
|
- strategy_id / sell_strategy_id :: str? :: 진입/청산 전략 식별
|
|
- portfolio_id :: str? :: 포트폴리오 식별
|
|
- forced_close_at :: datetime? :: 강제 청산 마감선
|
|
- entry_count :: int :: 슬롯 단위 누적 진입 횟수(dcy-019/REQ-002). 매 `→pending_buy`(idle→ / closed→ 모두) 시 +1, 그 외 전이·closed→idle 복구는 보존. buy admission selector 의 공정 정렬 키(최저 진입 우선). 종목 단위 `daily_symbol_entries.entry_count`(재진입 캡)와 별개.
|
|
- created_at / updated_at :: datetime? :: 생성/갱신 시각 — **UTC 저장 유지**(dcy-014/REQ-005 NFR-1: `trading_date` 만 TZ 기준, timestamp 는 UTC)
|
|
|
|
### Relationships
|
|
|
|
- daily_symbol_entries :: closed->idle 복구 시 (trading_date, symbol) 카운트 +1
|
|
- slot_transition_log :: 매 전이마다 append-only 기록 1행
|
|
|
|
### State Rules
|
|
|
|
- 전이 패밀리(8 base[dcy-019 closed→pending_buy 포함] + *→error + error→{idle,holding})에 없는 전이는 거부한다.
|
|
- 전이 전 current status == from_state 여야 한다(레이스 감지).
|
|
- closed->idle / pending_buy->idle 복구 시 점유 필드(symbol/qty/.../forced_close_at)를 초기화한다.
|
|
- closed->pending_buy 직접 재할당 시에도 이전 사이클의 점유 필드를 clean-slate 로 초기화한 뒤 새 진입 필드를 적용한다. entry_count 만 누적 보존(dcy-019/REQ-002).
|
|
|
|
## Entity: `SlotTransitionRecord`
|
|
|
|
- Purpose: 슬롯 상태 전이의 append-only 감사 기록.
|
|
- Source of Truth: slot_transition_log (향후 테이블) / 현재는 in-memory list
|
|
- Lifecycle: append-only (UPDATE/DELETE 금지)
|
|
|
|
### Key Fields
|
|
|
|
- slot_id :: UUID :: 대상 슬롯
|
|
- from_state / to_state :: SlotState :: 전이 전후 상태
|
|
- ts :: datetime :: 전이 시각(UTC)
|
|
- reason :: str :: 전이 사유
|
|
- actor :: str :: 전이 주체(시스템 워커 / 운영자)
|
|
- extra :: dict :: 부가 컨텍스트(예: symbol)
|
|
|
|
### Relationships
|
|
|
|
- Slot :: slot_id 로 연결
|
|
|
|
### State Rules
|
|
|
|
- 오직 INSERT 만 허용한다(append-only).
|
|
|
|
## Entity: `intent_log` (BAT-002)
|
|
|
|
- Purpose: HTTP 로 제출된 buy/sell intent 의 멱등 dedup 게이트 + audit. `intent_id` UNIQUE 가 at-least-once 재전송의 슬롯 중복 전이를 차단한다(dcy-002 OQ-1).
|
|
- Source of Truth: intent_log (PostgreSQL) / 테스트는 in-memory IntentStore
|
|
- Lifecycle: processing -> admitted | rejected
|
|
|
|
### Key Fields
|
|
- intent_id :: TEXT PK :: 멱등키(전략 데몬 발급)
|
|
- intent_type :: TEXT :: buy | sell
|
|
- payload :: JSONB :: 제출 본문 스냅샷
|
|
- status :: TEXT :: processing/admitted/rejected
|
|
- slot_id :: UUID? :: 수락 시 전이된 슬롯
|
|
- reason :: TEXT? :: 거부 사유
|
|
- created_at / updated_at :: TIMESTAMPTZ
|
|
|
|
### State Rules
|
|
- `try_insert` 는 INSERT ... ON CONFLICT DO NOTHING(dedup 게이트). 중복 intent_id 는 재전이 없이 기존 decision 반환.
|
|
- 완전 비동기 outbox(상태머신 + 소비 루프)로의 승격은 브로커 집행 Discovery 로 Deferred(이 테이블이 전구체).
|
|
|
|
## Entity: `LedgerTrade` (BAT-006)
|
|
|
|
- Purpose: portfolio-hub 원장에 기록되는 단일 체결 거래 데이터. application 경계 DTO(영속 엔티티 아님 — 영속 표현은 `ledger_outbox` 에서 운반).
|
|
- Source of Truth: 어댑터 호출 시 in-memory; 영속 표현은 `ledger_outbox` 의 컬럼들.
|
|
|
|
### Key Fields
|
|
|
|
- asset_id :: str :: 외부 원장 path param (= 슬롯 `portfolio_id`, dcy-004 OQ-1)
|
|
- order_id :: str :: 로컬 주문 식별자 (`broker_order_id`)
|
|
- execution_id :: str :: 결정적 합성 (paper / KIS 규칙). 멱등 키.
|
|
- side :: 'buy' | 'sell'
|
|
- qty :: float
|
|
- price :: float
|
|
- fee :: float :: BrokerPort `calculate_fee(side, qty*price)`
|
|
- tax :: float :: 기본 0(정밀 모델 후속)
|
|
- executed_at :: datetime?
|
|
- correlation_id (derived) :: `{asset_id}:{order_id}:{execution_id}`
|
|
|
|
### State Rules
|
|
|
|
- 동일 `execution_id` 재기록 시 ledger 어댑터/서버가 dedup 한다(`LedgerWriteResult.DUPLICATE_OK` 또는 server-side 멱등). 로컬에서는 outbox PK 가 dedup gate.
|
|
|
|
## Entity: `ledger_outbox` (BAT-006)
|
|
|
|
- Purpose: 체결 → portfolio-hub 원장 기록을 슬롯 전이와 분리해 무유실 + 멱등으로 처리하기 위한 durable work-item store(dcy-004 OQ-2, orbit `LedgerWriter` 패턴).
|
|
- Source of Truth: `ledger_outbox` (PostgreSQL) / 테스트는 in-memory `InMemoryLedgerOutbox`
|
|
- Lifecycle: pending → completed | conflict_blocked | **delivery_failed** ; retryable_failure 는 status `pending` 유지(attempts++ + `next_retry_at` 백오프). delivery_failed = permanent(404/400/422 즉시) 또는 dead-letter(max_attempts 소진), 사유는 last_outcome 으로 구분(dcy-015/REQ-019).
|
|
|
|
### Key Fields
|
|
|
|
- correlation_id :: TEXT PK :: `{asset_id}:{order_id}:{execution_id}` — 로컬 dedup 게이트
|
|
- asset_id / order_id / execution_id :: TEXT
|
|
- side :: TEXT CHECK ('buy','sell')
|
|
- qty / price / fee / tax :: NUMERIC(18,4)
|
|
- executed_at :: TIMESTAMPTZ?
|
|
- status :: TEXT CHECK ('pending','completed','conflict_blocked','delivery_failed') — `delivery_failed` 추가(dcy-015, 마이그레이션 007)
|
|
- attempts :: INTEGER :: 재시도 누적
|
|
- last_outcome :: TEXT? CHECK ('completed','duplicate_completed','retryable_failure','conflict_blocked','permanent_failure','dead_lettered')
|
|
- last_error :: TEXT?
|
|
- next_retry_at :: TIMESTAMPTZ? :: 백오프 중 재시도 가능 최소 시각(dcy-015/REQ-019). `list_pending` 은 `next_retry_at > now` 행을 건너뜀(head-of-line 보호)
|
|
- created_at / updated_at :: TIMESTAMPTZ
|
|
|
|
### State Rules
|
|
|
|
- `enqueue` 는 `INSERT ... ON CONFLICT (correlation_id) DO NOTHING`(dedup 게이트). 동일 fill 이 다시 enqueue 되어도 새 행을 만들지 않는다.
|
|
- writer 결과 매핑:
|
|
- NEW → outcome `completed`, status `completed`.
|
|
- DUPLICATE_OK 또는 로컬 dedup hit → outcome `duplicate_completed`, status `completed`.
|
|
- 409 → outcome `conflict_blocked`, status `conflict_blocked` (자동 재시도 중단, 운영자 해소).
|
|
- 5xx/network/timeout → outcome `retryable_failure`, status `pending` (다음 tick 재시도).
|
|
- slot 전이는 outbox 적재와 분리되어 critical path 유지.
|
|
|
|
## Shared Consistency Rules
|
|
|
|
- (trading_date, symbol) 재진입 카운트는 closed->idle 복구와 원자적으로 증가한다.
|
|
- 모든 상태 변경과 전이 로그 append 는 동일 advisory-locked 트랜잭션 안에서 일어난다.
|
|
- 재진입 정책 결정 입력은 daily_symbol_entries 카운터에서 읽고, slot_transition_log(감사 전용)를 join 하지 않는다.
|
|
|
|
## Persistence / Integration Notes
|
|
|
|
- 현재는 InMemorySlotRepository(threading.Lock 으로 advisory lock 모사)만 제공한다.
|
|
- 향후 PostgreSQL 구현 시 plan.md 부록 A 의 컬럼 계약(타입 포함)을 만족해야 한다.
|
|
- daily_symbol_entries PK = (trading_date, symbol); slot_transition_log 는 append-only 시퀀스.
|
|
|
|
## Current Gaps / Planned Changes
|
|
|
|
- daily_slot / daily_symbol_entries / slot_transition_log 의 물리 스키마(DDL)는 미이식 — 영속화 단계에서 작성.
|
|
- daily_slot_archive(EOD 아카이브)와 retention prune 은 trex-orbit 인프라 기능으로, 이번 순수 도메인 범위 밖.
|
|
- SlotState/필드 계약을 코드와 문서 사이에서 드리프트 없이 유지할 방법은 첫 Discovery 에서 정의(plan.md OQ-4).
|
|
|
|
## Update Triggers
|
|
|
|
- Update this document when a core entity, key field, or relationship changes.
|
|
- Update this document when lifecycle or persistence behavior changes.
|
|
- Update this document when a batch introduces a migration, backfill, or breaking data assumption.
|
|
|
|
## Change Log
|
|
|
|
- 2026-06-03 :: bootstrap-baseline 으로 초기 데이터 모델 baseline 생성(Slot + SlotTransitionRecord + 재진입 카운터).
|
|
- 2026-06-03 :: BAT-002 — PostgreSQL 영속화 확정 + `intent_log` 엔티티 추가(REQ-006/007/009).
|
|
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
|
- 2026-06-04 :: BAT-006 — ledger 통합 데이터 모델 반영(LedgerTrade DTO + `ledger_outbox` 테이블/`correlation_id` 멱등/outcome 어휘, FillStatus.execution_id/fee/tax 운반) (REQ-017~020).
|