Files
trex-core/docs/runtime-flows.md
T
genichinandClaude Opus 4.8 cc28e1fba5 fix(execution): dcy-012 매도 가격 버그 정정 — slot entry_price→limit_price rename (BAT-016)
매도(sell) 주문이 현재가가 아닌 매수가(entry_price) 지정가로 나가던 버그(issue #10)
정정. 슬롯의 매수 전용 entry_price 를 side 무관 "현재 pending 주문 지정가"
limit_price 로 내부 rename 하고, pending 이탈(→holding/→closed) 시 clear 해
매수 지정가가 매도로 누수되는 경로를 구조적으로 제거.

거버넌스: 전부 change-req (신규 REQ 없음, dcy-012 OQ-3).
- REQ-002: Slot entry_price→limit_price rename + clear-on-exit 생애주기
- REQ-006: daily_slot 컬럼 rename 마이그레이션 006 (idempotent) + persistence 매핑
- REQ-013: _dispatch 가 slot.limit_price 단일 필드로 가격/주문유형 결정
- REQ-007: SellIntent/sell payload optional limit_price + _admit_sell 운반
- REQ-036: SlotService.sell optional 가격 → 자율 매도(safety-sell/kill-switch) 시장가
- REQ-003: 컬럼 계약/드리프트 가드 limit_price 반영
- REQ-015: runtime-flows/interface-contract/data-model 정합

매도 정책(OQ-1 하이브리드): 가격 지정→지정가("00"), 미지정→시장가("01").
자율 매도는 가격 미전달 → 항상 시장가 (손절 미체결 pending_sell 갇힘 방지, issue #9 연계).

검증: 단위 286 passed/32 skipped, 인프라 게이트(PG+RabbitMQ) 318 passed, ruff clean.
신규 test_sell_pricing(recording broker)로 지정가 매수 슬롯의 매도가 매수가
재사용 안 함을 직접 검증. BAT-016 release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 13:41:46 +09:00

26 KiB
Raw Blame History

Trex Core Runtime Flows

  • Baseline Mode: mixed
  • Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
  • Status: draft
  • Owner: project maintainers
  • Last Updated (KST): 2026-06-08 (BAT-016)
  • 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 + RabbitMQ 통신, BAT-002/BAT-007)
  • RabbitMQ trex.snapshot.req :: 전략 모듈의 스냅샷 RPC (request-reply, BAT-007)
  • RabbitMQ trex.intent.req :: 전략 모듈의 매수/매도 admission RPC (request-reply, BAT-007)
  • RabbitMQ trex.events :: 체결/슬롯 상태 이벤트 topic exchange (pub/sub, BAT-007)
  • SlotService :: 슬롯 생명주기 도메인 서비스 (preallocate / transit / capacity / entry-count)
  • SlotRepository (Protocol) :: 저장소 구현체 주입 경계
  • MessagingTransport (Protocol) :: 메시지 transport 추상화 (RabbitMQ / InMemory stub)

Flow: 데몬 lifecycle (BAT-002, BAT-007)

  1. trex-core-daemon --config <path>.toml 기동 → DaemonConfig.from_config(toml) = from_env()(TREX_POSTGRESQL_DSN/TREX_RABBITMQ_URL 등) + TOML(instance_key, portfolio_hub_token) 병합. --config 필수(없으면 오류 종료, BAT-013/REQ-048). instance_key 로 큐명·라우팅 분리.
  2. setup: apply_migrations(dsn)(멱등) → 거래일 슬롯 preallocate_daily_slots(멱등) → execution.reconcile()(REQ-014) → 이벤트 publisher 의 declare_topology().
  3. start: 7개 백그라운드 스레드 동시 기동
    • trex-execution :: 브로커 dispatch/poll 루프 (REQ-013/014)
    • trex-ledger :: ledger outbox 드레인 (REQ-019)
    • trex-intent-rpc :: trex.intent.req 소비자 (prefetch=1, FIFO, dedup via intent_log; update_thresholds 액션 포함 REQ-040)
    • trex-snapshot-rpc :: trex.snapshot.req 소비자 (응답 holding 슬롯마다 워치독 last_seen_at 갱신 REQ-034)
    • trex-control-rpc :: trex.control.req 소비자 — pause/resume/list + kill_switch/resume_kill_switch (REQ-032/043)
    • trex-snapshot-scheduler :: 일일 portfolio-hub 스냅샷 refresh (REQ-030)
    • trex-safety-sell :: 자율 안전 매도 트리거 루프 (forced_close / stop-loss·take-profit / 장 마감, REQ-037/038/039)
  4. shutdown: SIGINT/SIGTERM → stop.set() → 모든 스레드 join + 모든 transport close(). HTTP 표면 0, 별도 포트 바인딩 없음.

Flow: 스냅샷 RPC (RabbitMQ trex.snapshot.req, BAT-007)

  1. 전략 모듈이 RPC 요청을 trex.snapshot.req 큐로 publish — payload 에 view/strategy_id/trading_date, properties 에 correlation_id + reply_to(direct reply-to).
  2. trex-snapshot-rpc 스레드가 메시지 수신 → make_snapshot_rpc_handler(slot_repo, slot_service) 로 슬롯 목록 + 가용성 계산.
  3. 응답을 reply_to 큐로 publish (correlation_id 일치) → manual ack.
  4. read-only 이므로 dedup 불요; 재요청 시 동일 응답을 다시 생성한다.

Flow: 매수/매도 intent admission RPC (RabbitMQ trex.intent.req, BAT-007)

  1. 전략 모듈이 RPC 요청을 trex.intent.req 큐로 publish — payload {intent_type, intent_id, ...}, properties correlation_id = intent 멱등키, reply_to.
  2. trex-intent-rpc 스레드(단일, prefetch=1)가 메시지 수신 → IntentService.submit_buy/sell 호출.
  3. intent_log.try_insert(intent_id, ...) 가 dedup 게이트 — 중복 메시지는 기존 decision 재반환(슬롯 무전이).
  4. 신규면 admission: portfolio_id 필수 검사(누락 시 portfolio_id_required 거부) → symbol resolve(SnapshotStore.get(portfolio_id).symbol, universe 부재 시 unknown_portfolio 거부, dcy-011/REQ-007) → find_idle_slot → SlotService.transit_slot(idle→pending_buy, symbol=resolved, portfolio_id) → Postgres durable commit. (buy payload 에 client symbol 없음)
  5. admission 응답(status, slot_id?, reason?)을 reply_to 큐로 publish → manual ack.
  6. ack-after-commit 시퀀스: durable commit 이 ack 보다 먼저이므로 ack 전 크래시 시 재전달돼도 (3) 의 dedup 으로 재실행되지 않는다.
  7. 체결/종료는 응답이 아니라 후속 이벤트(아래)로 전달 — admission 동기 + fill 비동기 2단계 시맨틱.

Flow: 슬롯 이벤트 발행/구독 (RabbitMQ trex.events, BAT-007)

  1. 체결: execution 루프가 pending_buy→holding(또는 pending_sell→closed)을 commit + ledger outbox enqueue (REQ-018/019).
  2. 같은 스레드에서 event_publisher.publish_event(rk, payload) 호출.
    • routing key: {event_type}.{strategy_id}.{symbol} (예: slot_filled_buy.A.005930).
    • payload: 현재 상태 + sequence_at(슬롯 updated_at iso) 운반 → 수신자는 last-wins 적용 가능 (순서 보증 불요).
    • publisher confirms 활성, 발행 실패는 로그 + DLQ 라우팅(trex.events.dlq). publish 실패가 슬롯 전이를 롤백하지 않는다.
  3. 전략별 durable queue(trex.events.strategy.{strategy_id})는 *.{strategy_id}.# 패턴으로 바인딩 → broker 단 필터로 자기 자산 이벤트만 수신.
  4. 전략 down 중 적체된 메시지는 catch-up 으로 복구; TTL(1h) / max-length(10k) 초과는 drop-head + 후속 스냅샷 RPC reconcile.

Flow: 일일 스냅샷 refresh (BAT-008 / REQ-030)

데몬 lifespan 의 trex-snapshot-scheduler 스레드가 portfolio-hub universe 를 fetch 해 in-memory 스냅샷을 atomic-swap 으로 교체하고, 같은 패스에서 정합 검증을 수행한다.

  1. boot: SnapshotScheduler.run_once() 한 번 호출 → 첫 universe 로딩(또는 degraded 마킹).
  2. tick(TREX_SNAPSHOT_TICK_SEC, 기본 60s): 현재 KST 시각이 TREX_SNAPSHOT_SCHEDULE_KST(기본 07:30) 이상이고 오늘 아직 안 돌았으면 fire.
  3. fire 단계: PortfolioHubReadPort.whoami() 로 daemon_id 도출(1회 캐시) → get_portfolios(daemon_id) 로 universe 페치 → SnapshotStore.swap(items) atomic 교체 → reconcile_positions(store, broker) 정합 검증.
  4. 실패 분기: PortfolioHubUnavailableError → degraded = portfolio_hub_unavailable; 응답 파싱 실패 → snapshot_fetch_failed. 둘 다 다음 RPC 응답 envelope 에 즉시 노출(REQ-028 AC-2).

Flow: 포지션 정합 검증 (BAT-008 / REQ-031)

reconcile_positions 는 portfolio-hub 보유 수량을 symbol 별로 합산해 broker get_balance() 결과와 비교한다.

  1. BrokerPort.get_balance() → list[BalanceItem(symbol, hldg_qty)] 조회. KIS 어댑터는 TTTC8434R/VTTC8434R inquire-balance, paper 는 결정론 fill aggregate. 0수량 row 는 양쪽 모두 drop(D-2 leftover 처리, Decision 7).
  2. 자산을 symbol 로 그룹화해 hub_total = sum(asset_quantity) 계산 → broker 의 같은 symbol hldg_qty 와 비교.
  3. 불일치: 해당 symbol 의 모든 portfolio_id 에 SnapshotStore.set_warning(pid, True) → 다음 envelope 부터 응답에서 마스킹. 첫 raise 시 한 번 로그.
  4. 일치: 이전에 warning 이었던 자산은 자동 해제 → 다음 envelope 부터 다시 포함. resume 과 달리 자동·임시 overlay(영속 X).

Flow: pause/resume 제어 (BAT-008 / REQ-032)

운영자가 자산을 매매에서 제외/복귀시키는 두 경로(CLI + RPC)는 동일한 snapshot_pause Postgres 테이블을 가리킨다.

  1. CLI: trex-core pause <portfolio_id> / trex-core resume <portfolio_id> → PostgreSQLPauseStore.pause/resume 호출(portfolio_id PK upsert/delete).
  2. RPC: 운영자가 trex.control.req 큐에 {action: pause|resume|list, portfolio_id, actor} 발행 → make_control_rpc_handler 가 pause action 을 같은 store 로 위임 → {status, paused[], action} 응답. (BAT-012: 구 trex.pause.req → trex.control.req 확장)
  3. 응답 산출: build_snapshot_envelope 가 pause_store.list_paused() 를 매 요청마다 읽음 → mask(pending | warning | pause) 합성. resume 후 다음 요청부터 즉시 복귀(별도 reload 불필요).
  4. 영속: daemon 재시작 + 일일 refresh 후에도 snapshot_pause row 유지(universe swap 은 pause 를 만지지 않음).

Flow: 스냅샷 RPC 응답 산출 (BAT-008 / REQ-028, REQ-029)

trex.snapshot.req 메시지 처리.

  1. 요청 payload 디코드: {view ∈ {all, buyable, held}, strategy_key?, trading_date?}.
  2. slot_service.compute_slot_capacity(today, total_slots) → available_slots = idle_count.
  3. build_snapshot_envelope(store, pause_store, slot_repo, view, strategy_key, available_slots, trading_date):
    • base 집합: ALL = universe ∪ holding, BUYABLE = universe − (pending ∪ holding)(available_slots=0 이면 []), HELD = holding 슬롯만.
    • 마스킹: pending(slot) OR warning(REQ-031) OR pause(REQ-032) — 어느 하나라도면 제외. slot overlay > portfolio-hub 보유 여부(불변식 A).
    • strategy_key 일치(==) 직교 필터.
    • degraded 상태가 있으면 degraded=true + reason + sanitized message 동봉.
  4. envelope: {assets[], view, available_slots, degraded, reason, message} — assets 는 항상 배열, null 금지. 항목 필드는 symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params, price 필드는 없음.

Flow: per-slot 워치독 갱신 (BAT-010/011 / REQ-034, REQ-040)

전략 모듈의 생존 신호를 추적해 자율 안전 매도가 정상 운용 슬롯에 개입하지 않게 한다.

  1. trex-snapshot-rpc 가 trex.snapshot.req 를 처리할 때 응답에 포함된 모든 holding 슬롯에 대해 WatchdogStore.update(slot_id)(last_seen_at = now)를 호출한다. 전략 모듈이 스냅샷을 폴링하는 한 그 슬롯은 "활성"으로 판정된다.
  2. update_thresholds intent(REQ-040)도 수신 시 해당 슬롯 last_seen_at 을 리셋한다(임계값 갱신 = 생존 신호).
  3. WatchdogStore.is_expired(slot_id, TREX_WATCHDOG_TIMEOUT) 가 True 이고 grace period(TREX_WATCHDOG_GRACE_PERIOD, 데몬 부팅 직후) 밖이면 그 슬롯은 워치독 만료 → forced_close/가격 트리거 대상이 된다.
  4. in-memory store 라 데몬 재시작 시 초기화되며 grace period 가 자동 재시작된다(부팅 직후 오탐 방지).

Flow: forced_close_at 만기 자동 매도 (BAT-011 / REQ-037)

trex-safety-sell worker 가 TREX_FORCED_CLOSE_CHECK_INTERVAL 주기로 수행.

  1. 오늘 holding 슬롯 중 워치독 만료(grace 밖)이고 forced_close_at <= now(UTC) 인 슬롯을 선별.
  2. 대상 슬롯에 SlotService.sell(actor="trex-safety-sell", reason="forced_close_at") 호출 → holding→pending_sell(REQ-036). 이후 체결은 집행 루프가 처리.
  3. 워치독 활성 슬롯·forced_close_at=None 슬롯은 제외. 이미 pending_sell 이면 idempotent(중복 매도 없음). KIS 호출 없음(DB + in-memory 비교만).

Flow: stop-loss·take-profit 가격 체크 (BAT-011 / REQ-038)

trex-safety-sell worker 가 TREX_STOP_LOSS_POLL_INTERVAL 주기로 수행.

  1. holding + 워치독 만료 + (stop_loss_price 또는 take_profit_price 설정) 슬롯만 대상(워치독 활성 슬롯은 KIS 호출조차 안 함).
  2. BrokerPort.get_current_price(symbol) → KIS 주식현재가 시세 inquire-price(tr FHKST01010100, 실전/모의 공용, BAT-011 CHG-04 정정)의 stck_prpr. paper 는 프로그램된 가격(미설정 시 None).
  3. 현재가 <= stop_loss_price → 손절(reason="stop_loss"), 현재가 >= take_profit_price → 익절(reason="take_profit")으로 SlotService.sell(). 가격 미수신(None) 시 skip(오탐 방지, 다음 tick 재시도).
  4. 쿼터 가드: 시간당 호출 상한 N × (3600 / interval) 추정이 임계 초과 시 경고 로그(REQ-038 AC-8).

Flow: 장 마감 강제 청산 (BAT-011 / REQ-039)

TREX_MARKET_CLOSE_LIQUIDATION_TIME(HH:MM KST, optional) 설정 시에만 동작.

  1. trex-safety-sell worker 가 매 tick 현재 KST 시각을 평가 → 설정 시각 도달 + 같은 거래일 미발동(last_liquidation_date 가드)이면 발동.
  2. 발동 시 워치독·grace 무관 모든 holding 슬롯에 SlotService.sell(reason="market_close_liquidation"). 이미 pending_sell 은 idempotent.
  3. 미설정이면 동작하지 않음. in-memory 가드라 재시작 시 같은 날 재발동 가능(설계상 허용 — REQ-039 Notes).

Flow: safety-sell 재시도 쿨다운 (BAT-014 / REQ-052)

영속 거부(휴장/잔고/규제)로 동일 슬롯에 매도가 무한 재제출되는 것을 막는 per-slot 쿨다운. forced_close/stop_loss/take_profit 트리거에 적용(market_close 제외 — _last_liquidation_date로 하루 1회 보호).

  1. run_once tick 시작 시 _holding()에 없는 슬롯의 쿨다운 상태(_last_attempt/_attempt_count)를 정리(FR-4).
  2. 트리거가 _safe_sell 호출 직전, 슬롯이 now - last_attempt_at < TREX_SAFETY_SELL_MIN_RETRY_INTERVAL(기본 600s)이면 건너뜀(stop-loss/take-profit 은 KIS 시세 poll 도 skip).
  3. 쿨다운 만료 후 트리거 조건이 여전히 충족되면 재시도 + last_attempt_at 갱신 + attempt_count 증가(성공 여부 무관).
  4. attempt_count >= TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT(기본 3) 도달 시 재시도 직전 운영자 WARNING 로그(slot_id/count/reason/last_attempt_at).
  5. 트리거 조건 해소(가격 회복 등)·체결 완료(슬롯 holding 이탈) 시 재시도/카운트 증가 없음. 데몬 재시작 시 쿨다운 초기화(재시작 직후 1회 재시도 허용).

Flow: kill-switch 비상 정지 (BAT-012 / REQ-043/045/046)

운영자가 trex.control.req 로 비상 정지를 발동해 범위별 holding 을 즉시 청산하고 당일 전략을 차단한다.

  1. trex-control-rpc 가 {action:"kill_switch", scope, portfolio_id?} 수신 → KillSwitchService.fire().
  2. 대상 범위(portfolio 또는 전역)의 holding 슬롯을 SlotService.sell(actor="kill-switch", reason="emergency_stop") 로 순차 전이(holding→pending_sell). 이미 pending_sell 은 idempotent(REQ-036). 이후 체결은 집행 루프가 처리.
  3. 발동 범위를 killswitch_exclusion(portfolio_id|'*', trading_date) 에 day-scoped 로 표시(REQ-044) + 발동 이력 로깅(actor/scope/처리 수).
  4. masking: 이후 snapshot RPC 응답 산출이 mask = pending|warning|pause|killswitch_today 로 대상 자산을 제외 → 당일 추가 매수 차단. 전역('*')이면 전체 universe 마스킹.
  5. 자동 복귀: killswitch_exclusion 은 trading_date 키라 다음 거래일 조회에서 빠짐(별도 동작 불필요). 재시작해도 같은 거래일이면 영속 유지.
  6. 수동 해제: {action:"resume_kill_switch", scope, portfolio_id?} → KillSwitchService.resume() 가 day-scoped row 삭제 → 다음 응답부터 정상 노출. 이미 청산된 슬롯은 비가역(매도 되돌리지 않음).
  7. 독립성: kill-switch 는 자율 안전 매도 worker(dcy-007)와 스레드/상태 공유 없음. 동일 슬롯 동시 sell() 은 REQ-036 idempotency 로 1회만 전이.

Flow: multi-instance 라우팅 (BAT-013 / REQ-049/050)

다중 인스턴스를 동일 RabbitMQ 에 붙여도 split-brain 없이 운영하기 위한 큐/exchange 분리.

  1. 각 인스턴스는 --config inst-N.toml(instance_key)로 기동 → 수신 큐명이 {base}.{instance_key} 로 분리(default 는 리터럴 유지, 하위호환).
  2. declare_topology(instance_key): trex.direct(topic) + trex.broadcast(fanout) 선언 + 인스턴스 intent/snapshot/control 큐를 trex.direct 에 {instance_key}.{channel} 로 바인딩 + control 큐를 trex.broadcast 에 dual 바인딩.
  3. directed: strategy/운영자가 trex.direct 에 inst-1.intent 로 publish → trex.intent.req.inst-1 에만 전달(다른 인스턴스 큐 미수신). 인스턴스별 WatchdogStore/SafetySellService 가 자기 큐만 소비 → split-brain 방지(NFR-3).
  4. broadcast: trex.broadcast 에 publish → 모든 인스턴스 control 큐 동시 전달(global kill-switch 등). 중복 처리는 idempotency 로 무해.
  5. 기존 reply-to RPC 직접 큐 경로(default)는 그대로 동작 — 추가 바인딩은 비파괴적(FR-5 무회귀).

Flow: broker degrade 분기 (BAT-007 / REQ-024)

  1. transport connection drop → 어댑터가 _mark_disconnected() + 백오프(1s→2s→4s→8s→16s→30s cap) 재연결.
  2. application 레이어는 transport.health() 가 disconnected/reconnecting 인 동안 신규 발행/RPC 가 TransportUnavailableError 로 빠르게 실패한다 → 전략은 매매 보류.
  3. core 자율 안전 매도(stop-loss·take-profit / forced_close / 장 마감 청산)는 transport 와 무관하게 trex-safety-sell worker 가 수행한다(아래 safety-sell 흐름; dcy-007 REQ-037~041 구현, BAT-010/011). 이전 "A-5 미구현" 항목은 dcy-007 로 해소됨.
  4. 로컬 버퍼링 없음 — broker 복구 후 옛 intent 가 자동 재생되지 않는다(전략이 신선 상태로 재판단).

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 롤백.
    • 가격/주문유형 결정(dcy-012/REQ-013, issue #10): _dispatch 는 side 분기 없이 슬롯 limit_price 한 필드만 읽는다 — 값이 있으면 지정가(order_type="00", 그 가격), 없으면 시장가(order_type="01"). limit_price 는 pending 진입 시 set 되고 pending 이탈(→holding/→closed) 시 clear 되므로, 매수 지정가(과거 entry_price)가 매도 주문에 재사용되지 않는다. 자율 매도(safety-sell/kill-switch)는 가격 미전달 → 시장가로 즉시 체결(미체결 pending_sell 갇힘 방지, issue #9 연계).
  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).
  • 슬롯 limit_price 생애주기(dcy-012/REQ-002): IDLE→PENDING_BUY(매수 지정가 set) → PENDING_BUY→HOLDING(clear) → HOLDING→PENDING_SELL(매도 지정가 set 또는 None=시장가) → PENDING_SELL→CLOSED(clear). 롤백·복구(→IDLE) 전이에서도 clear.
  • 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅).

Flow: 체결 → ledger outbox (BAT-006)

집행 루프 fill 전이 직후 ExecutionService 가 ledger 기록을 outbox 에 적재해 슬롯 전이 critical path 와 ledger write 를 분리한다(원장 기록 단일 주체 = 집행 경로, NFR-3).

  • ledger skip 가시화(dcy-011/REQ-018, issue #11): _record_ledger 가 portfolio_id 또는 execution_id 누락으로 skip 하면 무성 반환하지 않고 WARN 로그 + ExecutionResult.ledger_skipped 카운터로 노출한다(슬롯 전이는 그대로 진행). portfolio_id 필수화(REQ-007)로 주원인은 제거됐고, 잔여 누락은 운영 점검 쿼리(REQ-055: closed 슬롯 ↔ ledger_outbox anti-join, find_ledger_gap_slots)로 영구 탐지한다.
  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).
  • 2026-06-05 :: BAT-007 — transport 전환(HTTP /v1 API 제거, RabbitMQ 메시징 도입: 스냅샷/intent RPC + 이벤트 topic, sync+thread daemon, intent_log 멱등 ack-after-commit, publisher confirms + DLQ, per-strategy durable queue + TTL/max-length, broker degrade 분기) (REQ-021~026).
  • 2026-06-06 :: BAT-008 — portfolio-hub 스냅샷 read 레이어 추가(PortfolioHubReadPort + hub_read/stub_read 어댑터, SnapshotStore in-memory + snapshot_pause Postgres, SnapshotScheduler 일일 refresh, reconcile_positions 정합 검증 + BrokerPort.get_balance() 확장, trex.snapshot.req envelope 응답 + degraded 신호, trex.pause.req 제어 RPC, trex-core CLI) (REQ-027~033; 옵션 D AC-2/AC-3 은 portfolio-hub#8 배포 후 별도 구현).