17 KiB
17 KiB
Trex Core Data Model
- Baseline Mode: mixed
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-10 (BAT-031 — dcy-021 포지션 사이징)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery)
Project Summary
- trex 제품군의 공용 베이스로, 첫 도메인으로 trex-orbit 의 일일 거래 슬롯 생명주기(slot) 순수 도메인을 storage-agnostic 하게 이식한 코어다.
Primary Domain
- 자동매매 자산 중심 영구 슬롯 생명주기 도메인 (dcy-020 — portfolio-hub universe 자산 1:1 영구 슬롯; 일일 풀 모델 폐기, DB 는 BAT-030
trex-core-reset로 재초기화)
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 :: universe 자산 1:1 영구 슬롯의 현재 생명주기 상태/점유 정보(source of truth). 키 = (instance_key, portfolio_id) UNIQUE.
- SlotTransitionRecord :: 슬롯 상태 전이의 append-only 감사 기록 (시간 속성의 정본 — 슬롯 행에는 거래일 없음)
- SlotCapacity (계산 스냅샷) :: inactive/holding/pending/active + available(= max(0, M − active)) + scarcity_ratio (영속 엔티티 아님; dcy-020/REQ-059)
daily_symbol_entries:: 폐기(dcy-020/BAT-030) — closed→idle 복구 제거로 writer 소멸. 테이블은 잔존하나 코드 경로 없음.
Entity: Slot
- Purpose: portfolio-hub universe 자산 1:1 영구 슬롯의 현재 생명주기 상태와 점유 정보를 담는 source of truth (dcy-020/REQ-056). 거래일을 넘어 존속하며, universe reconcile(REQ-058)이 생성/소프트 폐기한다.
- Source of Truth: daily_slot (PostgreSQL; 이름은 역사적 잔존 — 행은 영구)
- Lifecycle (5-state, REQ-057): inactive -> pending_buy -> holding -> pending_sell -> inactive ; pending_buy -> inactive (롤백) ; pending_sell -> holding (롤백) ; 모든 비-error -> error ; error -> {inactive, holding}.
idle/closed는 BAT-030(마이그레이션 010)에서inactive로 폐합.
Key Fields
- slot_id :: UUID :: 슬롯 식별자(PK) — 외부 intent 계약의 정본 키(REQ-061)
- instance_key :: TEXT NOT NULL DEFAULT 'default' :: 공유 DB 에서 데몬 인스턴스 스코프(REQ-056). 부분 UNIQUE(instance_key, portfolio_id) WHERE portfolio_id IS NOT NULL (마이그레이션 009; 빈 DB 에 생성되며 자산 중심에서 (instance_key,portfolio_id)는 설계상 유일)
- retired_at :: TIMESTAMPTZ? :: universe 이탈 자산의 soft-retire 마커(REQ-058; 운영 뷰
universe_detached) - status :: SlotState :: 5 상태(inactive/pending_buy/holding/pending_sell/error) 중 현재 상태 — CHECK 제약(마이그레이션 010)
- 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? :: 주문 식별자(자유 주석 / 브로커 핸들)
- venue :: TEXT NOT NULL DEFAULT 'KRX' :: 현재 주문 거래소 구분(AUTO/KRX/NXT/SOR). intent 단계의 AUTO는 KIS 제출 시 실제 KRX/NXT로 교체되며 주문번호와 원자 저장한다. pending 주문·취소까지 보존하고 HOLDING 진입 시 다음 자율주문의 AUTO, INACTIVE 진입 시 KRX로 초기화한다.
- cancel_state :: TEXT? :: 취소 요청의 즉시 결과(requested/rejected/unknown). 최종 취소 상태가 아니다. requested는 재시도하지 않고, rejected/unknown만 authoritative open-row 확인과 bounded backoff 뒤 재시도할 수 있다.
- cancel_requested_at :: TIMESTAMPTZ? :: 마지막 취소 시도 예약 시각. 주문 종료·dispatch clear 때 함께 제거한다.
- order_submitted_at :: TIMESTAMPTZ? :: timeout deadline의 immutable 주문 제출 기준점. poll/cancel bookkeeping 및 daemon 재시작에서 보존하고 terminal dispatch clear 때 제거한다.
- order_date :: TEXT? :: 원주문 KST 날짜(
YYYYMMDD). 날짜 rollover 뒤 KIS 일별주문체결조회가 현재 날짜가 아닌 원주문 날짜를 사용하도록 보존한다. - cancel_attempts :: INTEGER NOT NULL DEFAULT 0 :: 원자적으로 예약된 취소 시도 횟수. 새 dispatch에서 0, terminal dispatch clear에서 0으로 초기화한다.
- strategy_id / sell_strategy_id :: str? :: 진입/청산 전략 식별
- portfolio_id :: str? :: 포트폴리오 식별
- forced_close_at :: datetime? :: 강제 청산 마감선
- created_at / updated_at :: datetime? :: 생성/갱신 시각 — UTC 저장
- (DROP, BAT-030/마이그레이션 011)
trading_date— 영구 슬롯에 거래일 정체성 없음(시간 속성은 transition_log/원장).entry_count— 풀 공정 정렬 키 폐기.
Relationships
- portfolio-hub universe :: portfolio_id 1:1 — universe reconcile(REQ-058)이 신규 자산 슬롯 생성(inactive)·이탈 자산 soft-retire(retired_at)·보유 중 이탈은 tombstone(유지).
- slot_transition_log :: 매 전이마다 append-only 기록 1행 (매도 체결 검출·ledger-gap 검출의 정본)
State Rules
- 전이 패밀리(6 base + *→error + error→{inactive,holding} = 12)에 없는 전이는 거부한다(REQ-057).
- 전이 전 current status == from_state 여야 한다(레이스 감지).
- INACTIVE 진입 시(매수 롤백 / 매도 체결 / error 해소) 포지션·주문 점유 필드(qty/avg_price/limit_price/allocated_capital/order_id/strategy_id/broker_order_id/sell_strategy_id/forced_close_at/SL·TP)를 초기화하되 자산 정체성(instance_key/portfolio_id/symbol/retired_at)은 보존한다 — 영구 슬롯 재사용의 전제.
- buy admission(REQ-059/061): slot_id 정본(portfolio_id dual-accept) → 대상 자산 슬롯 활성화. FCFS, M(
max_active_slots) 캡, 무치환(over-cap 시 기존 보유 유지·신규만 거부). active = holding+pending_buy+pending_sell. - 청산 수량(REQ-062, D3): 집행 dispatch 의 매도 수량은 브로커 잔고(hldg_qty) 정본 — 전량 매도(slot.qty None) = 브로커 잔고, 부분 매도(dcy-021) =
min(주문 qty, 브로커 잔고). 브로커 0 이면 skip+WARN(divergence 표면화). - 포지션 사이징(dcy-021/REQ-066): 보유 슬롯 증액 =
holding→pending_buy→holding(추가 매수); 체결 후 보유 qty = 브로커 잔고,avg_price= 가중평균(기존qty×기존평단 + 체결qty×체결가)/총qty. 부분 감액 =holding→pending_sell→holding(잔량>0, cost basis 유지) /→inactive(전량). 부분 체결은 실제 체결분만 반영·ledger 기록, 미체결 잔량 미추적(재수렴은 전략). pending 동안slot.qty= 주문 수량(전량 매도는 None sentinel), 체결/롤백 후 = 브로커 잔고. 증액은 active 자산 수(M 캡) 불변.
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_idUNIQUE 가 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?
- venue :: str :: Core가 실제 제출에 사용한 거래소. 외부 Portfolio Hub API에는 아직 전달하지 않는다.
- 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-memoryInMemoryLedgerOutbox - 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?
- venue :: TEXT NOT NULL DEFAULT 'KRX' :: 실제 제출 거래소. 기존 행은 KRX로 보존한다.
- 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, statuscompleted. - DUPLICATE_OK 또는 로컬 dedup hit → outcome
duplicate_completed, statuscompleted. - 409 → outcome
conflict_blocked, statusconflict_blocked(자동 재시도 중단, 운영자 해소). - 5xx/network/timeout → outcome
retryable_failure, statuspending(다음 tick 재시도).
- NEW → outcome
- slot 전이는 outbox 적재와 분리되어 critical path 유지.
Entity: broker_account_snapshot (dcy-041 / GitHub #45)
- Purpose: KIS 계좌조회 전송 장애 동안 신규매수와 위험축소 매도의 허용 범위를 짧게 제한하는 계정별 마지막 정상값.
- Source of Truth: 실시간 KIS 조회가 항상 우선이며 이 테이블은 임시 복구값이다. 재시작 원장대조에는 사용하지 않는다.
Key Fields
- account_hash :: TEXT PK ::
SHA-256(KIS base_url|CANO|ACNT_PRDT_CD|app_key). 실전/모의 및 애플리케이션 자격증명 범위를 분리하며 원문을 저장·로그하지 않는다. - holdings :: JSONB NOT NULL ::
[{symbol,hldg_qty}]. 소유권 래퍼가 켜진 계정은 전략 소유분만 저장한다. - holdings_captured_at :: TIMESTAMPTZ? :: 보유수량 조회가 마지막 성공한 시각.
- available_cash :: BIGINT? :: KIS 주문가능조회
nrcvb_buy_amt현금주문 가능금액. - cash_captured_at :: TIMESTAMPTZ? :: 주문가능금액 조회가 마지막 성공한 시각.
State Rules
- 보유수량과 주문가능금액은 독립적으로 갱신한다. 한 조회의 실패가 다른 정상값과 캡처 시각을 덮지 않는다.
- 계정 해시는 저장 직전 재확인하며 바뀌면 저장하지 않는다.
- PostgreSQL advisory lock으로 계정당 백그라운드 조회 소유자는 하나만 허용한다.
- 기본 최대 나이는 매수 600초, 매도 3600초이고 매도도 실시간 보유조회가
transport_failed일 때만 사용한다.
Shared Consistency Rules
- 모든 상태 변경과 전이 로그 append 는 동일 advisory-locked 트랜잭션 안에서 일어난다. advisory lock 은 인스턴스 스코프(
slot_advisory_lock_id(instance_key), REQ-057 NFR-1). - 집행 dispatch/poll 큐 조회는 instance_key 스코프 — 공유 DB 의 두 데몬이 서로의 주문을 집행하지 않는다(REQ-056).
- holding ↔ 브로커 정합(REQ-062): 브로커 잔고가 reconcile ground truth. core holding/브로커 0 → slot error 격리, 수량 불일치 → WARN(미러 비정본), 브로커 보유/슬롯 없음 → WARN(unmanaged).
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_symbol_entries테이블은 폐기 상태로 잔존(코드 경로 없음) — 후속 정리 시 DROP 가능.- daily_slot_archive(EOD 아카이브)와 retention prune 은 범위 밖 유지.
- 포지션 사이징(issue #1 잔여, dcy-020 OQ-4 분리)은 후속 Discovery.
- DB 재초기화(REQ-064)는 운영자 실행:
trex-core-reset --config <instance> --yes— slot-core 테이블 wipe + 마이그레이션 재적용으로 자산 중심 스키마 확정(데이터 보존 cutover 폐기, CHG-20260610-03). 보유 포지션은 재초기화 후 universe reconcile +reconcile_core_holdings(REQ-062)로 운영자가 재편입. 파괴적·DB 전역 — 백업 선행.
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). - 2026-06-10 :: BAT-030 (dcy-020 마감) — 자산 중심 영구 슬롯 모델 확정: 5-state(inactive), idle/closed 폐합(마이그레이션 010), trading_date/entry_count DROP(011), instance_key/retired_at 정식화, slot_id intent 계약, M 캡 admission, 매도 수량 브로커 정본, daily_symbol_entries·일일 preallocation(dcy-014)·풀 admission(dcy-001/019) 폐기, DB 재초기화 도구(trex-core-reset — 데이터 보존 cutover 대체, REQ-064 CHG-20260610-03) (REQ-056~065).
- 2026-06-10 :: BAT-031 (dcy-021 포지션 사이징) — 보유 슬롯 수량 가변: 증액(holding→pending_buy, avg_price 가중평균)·부분 매도(min(주문,잔고), 잔량 holding/전량 inactive, cost basis 유지)·부분 체결 실제만 반영. 수량 정본=브로커 잔고 재동기화. (REQ-066/057/061/062)