130 lines
9.3 KiB
Markdown
130 lines
9.3 KiB
Markdown
# Trex Core Runtime Flows
|
|
|
|
- Baseline Mode: mixed
|
|
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
|
- Status: draft
|
|
- Owner: project maintainers
|
|
- Last Updated (KST): 2026-06-04 (BAT-006)
|
|
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
|
|
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
|
|
|
## Project Summary
|
|
- trex 제품군의 공용 베이스로, 첫 도메인으로 trex-orbit 의 일일 거래 슬롯 생명주기(slot) 순수 도메인을 storage-agnostic 하게 이식한 코어다.
|
|
|
|
## Purpose
|
|
- This document is the current approved execution-flow baseline for the project.
|
|
- It is a cross-cutting reference document for Discovery, REQ, Batch Design, Batch Verification, and implementation updates.
|
|
|
|
## Scope
|
|
- Covers representative domain-service flows (library 형태이므로 외부 명령이 아니라 in-process API 호출 흐름).
|
|
- Does not duplicate full acceptance criteria or low-level implementation details.
|
|
|
|
## Covered Entry Points
|
|
|
|
- `trex-core-daemon` :: 슬롯 코어 데몬 (lifecycle + HTTP `/v1` 서빙, BAT-002)
|
|
- HTTP `/v1` :: intent 제출/조회 경계 (전략 데몬 클라이언트)
|
|
- SlotService :: 슬롯 생명주기 도메인 서비스 (preallocate / transit / capacity / entry-count)
|
|
- SlotRepository (Protocol) :: 저장소 구현체 주입 경계
|
|
|
|
## Flow: `데몬 lifecycle` (BAT-002)
|
|
|
|
1. `trex-core-daemon` 기동 → `DaemonConfig.from_env()`(TREX_POSTGRESQL_DSN 등).
|
|
2. startup(lifespan): `apply_migrations(dsn)`(멱등) → 거래일 슬롯 `preallocate_daily_slots`(멱등, 공개 엔드포인트 아님).
|
|
3. uvicorn 이 FastAPI `/v1` 앱 서빙(`GET /v1/healthz` 200).
|
|
4. shutdown: 연결-당-호출 어댑터라 풀 정리 없음, graceful 종료.
|
|
|
|
## Flow: `매수 intent` (POST /v1/intents/buy, BAT-002)
|
|
|
|
1. 전략 데몬이 intent(intent_id 포함)를 POST.
|
|
2. IntentService: `intent_log.try_insert`(intent_id) dedup 게이트 — 중복이면 기존 decision 동기 반환(슬롯 무전이).
|
|
3. 신규면 admission: `find_idle_slot` → `SlotService.transit_slot(idle→pending_buy)`(advisory lock 단일 writer). last-idle 경합 시 StateMismatch 재시도.
|
|
4. `intent_log.finalize(admitted/rejected, slot_id)` → admit/reject 동기 응답.
|
|
|
|
## Flow: `매도 intent` (POST /v1/intents/sell, BAT-002)
|
|
|
|
1. 전략 데몬이 holding 슬롯을 `GET /v1/slots?status=holding` 로 조회 후 sell intent POST.
|
|
2. dedup 게이트 → 검증(slot 이 holding 인가) → `transit_slot(holding→pending_sell)` → finalize → 동기 응답.
|
|
|
|
## Flow: `주문 집행 루프` (백그라운드, BAT-004)
|
|
|
|
데몬 lifecycle 에서 시작되는 단일 백그라운드 execution 루프가 주기적으로(설정 간격) 수행:
|
|
1. dispatch: `find_pending_for_dispatch`(status pending_* AND `broker_order_id IS NULL`, `FOR UPDATE SKIP LOCKED`) → `BrokerPort.submit_order` → `mark_dispatched(broker_order_id)`(멱등 가드). 거부 시 `pending_buy→idle` / `pending_sell→holding` 롤백.
|
|
2. poll: `list_dispatched_pending`(broker_order_id NOT NULL) → `BrokerPort.poll_fill` → 체결 시 `pending_buy→holding`(avg_price/qty) / `pending_sell→closed`(+재진입), 거부 시 롤백. 전이 후 `clear_dispatch`.
|
|
3. reconcile(재기동): 서빙 전 `list_dispatched_pending` 재폴링으로 크래시 후 복구(중복 주문 0). 미디스패치(broker_order_id NULL) 슬롯은 다음 dispatch 가 1회만 송신.
|
|
|
|
- 모든 슬롯 쓰기는 SlotService(advisory lock) 단일 경로(불변식 A). 브로커 주문/취소는 BrokerPort 단일 세션만(불변식 B).
|
|
- 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅).
|
|
|
|
## Flow: `체결 → ledger outbox` (BAT-006)
|
|
|
|
집행 루프 fill 전이 직후 `ExecutionService` 가 ledger 기록을 outbox 에 적재해 슬롯 전이 critical path 와 ledger write 를 분리한다(원장 기록 단일 주체 = 집행 경로, NFR-3).
|
|
|
|
1. fill 감지: `BrokerPort.poll_fill` 이 `state="filled"` 와 결정적 `execution_id`(paper: `paper:{broker_order_id}:{filled_qty}` / KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`), 계산된 `fee`(BrokerPort `calculate_fee(side, amount)`), `tax=0` 을 운반.
|
|
2. 슬롯 전이: `pending_buy→holding`(avg_price/qty) 또는 `pending_sell→closed` 를 advisory lock 안에서 먼저 commit. 이 단계가 critical path.
|
|
3. ledger enqueue: 슬롯의 `portfolio_id`(== ledger `asset_id`, OQ-1 결정) 가 있으면 `LedgerTrade(asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at)` 을 `LedgerOutbox.enqueue` 로 적재. 행 식별자는 `correlation_id = {asset_id}:{order_id}:{execution_id}`(로컬 dedup). `portfolio_id` 부재면 ledger 기록 skip 후 슬롯 전이는 정상 유지.
|
|
4. `clear_dispatch` 로 `broker_order_id` 해제.
|
|
|
|
## Flow: `ledger writer` (백그라운드, BAT-006)
|
|
|
|
데몬 lifespan 에서 별도 백그라운드 워커가 ledger outbox 를 drain 한다. 슬롯 전이와 분리되어 eventually-consistent.
|
|
|
|
1. `LedgerOutbox.list_pending(limit)` 으로 pending work item 을 FIFO 로 가져온다.
|
|
2. row 의 현재 상태가 이미 `completed` 면 (e.g. 동시/재처리) `duplicate_completed` 로 마킹하고 어댑터를 호출하지 않는다.
|
|
3. `LedgerPort.record_trade(trade)` 호출.
|
|
- `LedgerWriteResult.NEW` → outcome `completed`, row status `completed`.
|
|
- `LedgerWriteResult.DUPLICATE_OK`(어댑터 server-side dedup) → outcome `duplicate_completed`, row status `completed`, ledger 중복 0.
|
|
- `LedgerConflictError`(portfolio-hub 409) → outcome `conflict_blocked`, row status `conflict_blocked` (자동 재시도 중단, 운영자 해소 대상).
|
|
- `LedgerRetryableError`(timeout/5xx/network) → outcome `retryable_failure`, row status `pending` 유지(다음 tick 재시도).
|
|
4. portfolio-hub 장기 장애에도 outbox 가 적재되어 무유실. 재시작 시 PostgreSQL `ledger_outbox.status='pending'` 행을 그대로 이어 처리한다.
|
|
|
|
## Shared Components
|
|
|
|
- SlotRepository Protocol :: 저장소 추상화. 구현체(InMemorySlotRepository, 향후 PostgreSQLSlotRepository)를 주입.
|
|
- SLOT_ADVISORY_LOCK_ID :: 모든 상태 변경을 직렬화하는 공유 advisory lock 식별자.
|
|
- transactional_advisory_lock :: 단일 트랜잭션 + advisory lock 컨텍스트(외부 I/O 금지).
|
|
|
|
## Flow: `slot 사전 할당 (preallocate_daily_slots)`
|
|
|
|
1. 호출자가 `preallocate_daily_slots(trading_date, count)` 를 호출한다(count > 0 검증).
|
|
2. SlotService 가 `transactional_advisory_lock(SLOT_ADVISORY_LOCK_ID)` 를 획득한다.
|
|
3. 저장소가 해당 trading_date 에 슬롯이 이미 있으면 0 을 반환(멱등), 없으면 `count` 개 idle 슬롯을 삽입한다.
|
|
4. 삽입된 슬롯 수를 반환한다.
|
|
|
|
## Flow: `slot 상태 전이 (transit_slot)`
|
|
|
|
1. 호출자가 `transit_slot(slot_id, from_state, to_state, reason, actor, **fields)` 를 호출한다.
|
|
2. (from_state, to_state) 가 8 전이 패밀리에 속하는지 먼저 검증한다(아니면 SlotTransitionError).
|
|
3. advisory lock 획득 후 슬롯을 fetch 한다(없으면 SlotNotFoundError).
|
|
4. 현재 status == from_state 인지 확인한다(아니면 SlotStateMismatchError — 레이스 감지).
|
|
5. to_state 의미에 맞춰 점유 필드를 계산하고 슬롯 행을 update 한다. (closed/pending_buy -> idle 은 점유 초기화)
|
|
6. closed -> idle 복구면 (trading_date, symbol) 재진입 카운트를 +1 한다.
|
|
7. slot_transition_log 에 전이 기록 1행을 append 하고 갱신된 슬롯을 반환한다.
|
|
8. 외부 I/O(브로커 등)는 lock 밖에서 호출자가 수행한다(lock 보유 중 금지).
|
|
|
|
## Flow Constraints
|
|
|
|
- 모든 상태 변경 API 는 advisory-locked 단일 트랜잭션 안에서 수행한다.
|
|
- lock 보유 중에는 외부 I/O 를 수행하지 않는다.
|
|
- slot_transition_log 는 append-only 다(UPDATE/DELETE 금지).
|
|
- 상태 전이는 read(현재 상태)-검증-write 가 동일 트랜잭션에서 원자적으로 이뤄져 레이스를 차단한다.
|
|
|
|
## Current Gaps / Planned Changes
|
|
|
|
- 실제 영속화(PostgreSQL) 흐름은 미구현 — 현재는 in-memory repository 로만 구동.
|
|
- CLI/데몬 런타임 흐름 없음(library). 상위 애플리케이션 통합 흐름은 영속화 단계 이후 정의.
|
|
- 강제 청산/EOD 같은 상위 운영 흐름은 이번 순수 도메인 범위 밖.
|
|
|
|
## Update Triggers
|
|
|
|
- Update this document when a command entry point changes.
|
|
- Update this document when a runtime orchestration path changes.
|
|
- Update this document when a batch changes the approved flow baseline.
|
|
|
|
## Change Log
|
|
|
|
- 2026-06-03 :: bootstrap-baseline 으로 초기 흐름 baseline 생성(slot 도메인 in-process API 흐름 기준).
|
|
- 2026-06-03 :: BAT-001 이식 완료 반영 — preallocate/transit 흐름이 `trex_core.domain.slot` 실제 코드로 구현됨(REQ-004). 영속화/CLI 흐름은 여전히 미구현(범위 밖).
|
|
- 2026-06-03 :: BAT-002 — 데몬 lifecycle + 매수/매도 intent HTTP 흐름 추가, Primary Runtime=daemon+library(REQ-005~009). 브로커 집행/safety floor 흐름은 후속 Discovery.
|
|
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
|
- 2026-06-04 :: BAT-006 — ledger 통합 반영(체결→ledger outbox enqueue, LedgerWriter retry/dedup/409 흐름, FillStatus.execution_id/fee 운반, slot 전이와 ledger write 분리) (REQ-017~020).
|