Files
trex-core/docs/interface-contract.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

27 KiB

Trex Core Interface Contract

  • 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 API = dcy-002 / BAT-002
  • Primary Runtime: daemon (HTTP API) + library (slot 도메인)

Project Summary

  • trex 제품군의 공용 베이스로, 첫 도메인으로 trex-orbit 의 일일 거래 슬롯 생명주기(slot) 순수 도메인을 storage-agnostic 하게 이식한 코어다.

Primary Domain

  • 자동매매 일일 거래 슬롯 생명주기 도메인 (equity trading slot lifecycle)

Purpose

  • This document captures the approved external and cross-boundary interface contracts for the project.
  • It is a cross-cutting reference for REQ drafting, batch design, implementation, verification, and handoff.

Scope

  • Covers the HTTP /v1 API(슬롯 코어 데몬, BAT-002), in-process domain-service APIs, and the persistence boundary Protocol.
  • Does not duplicate full business requirements already owned by REQ documents.
  • Does not replace low-level code references or generated API artifacts.

Interface Summary

  • RabbitMQ trex.snapshot.req :: 전략 모듈의 스냅샷 RPC (request-reply, BAT-007/BAT-008 envelope)
  • RabbitMQ trex.intent.req :: 전략 모듈의 매수/매도 admission RPC (request-reply, BAT-007)
  • RabbitMQ trex.events :: 체결/슬롯 상태 이벤트 topic exchange (pub/sub, BAT-007)
  • RabbitMQ trex.control.req :: 운영자 제어 RPC — pause/resume/list + kill_switch/resume_kill_switch (request-reply, BAT-012; 구 trex.pause.req 확장)
  • trex-core CLI :: pause/resume/paused 운영 명령 (BAT-008)
  • SlotService :: 슬롯 생명주기 도메인 서비스(전이/사전할당/가용성/재진입 카운트)
  • SlotRepository (Protocol) :: 도메인 ↔ 저장소 경계 계약
  • BrokerPort (Protocol) :: 단일 브로커 세션 경계(submit/poll/cancel) + OrderRequest/OrderAck/FillStatus
  • LedgerPort (Protocol) :: 외부 원장(portfolio-hub) 기록 경계 + LedgerTrade/LedgerWriteResult/예외 (BAT-006)
  • LedgerOutbox (Protocol) + LedgerWriter :: 체결 → 원장 기록의 outbox/재시도/멱등 (BAT-006)
  • MessagingTransport (Protocol) :: RabbitMQ / InMemory stub transport 경계 + TransportHealth enum (BAT-007)
  • PortfolioHubReadPort (Protocol) :: portfolio-hub universe read 경계 + PortfolioItem/PortfolioHubUnavailableError (BAT-008)
  • SnapshotStore + PauseStore + SnapshotScheduler :: in-memory snapshot + durable pause overlay + 일일 refresh (BAT-008)

BAT-007 에서 HTTP /v1 API 표면이 0 으로 제거됨(REQ-025). 이전 baseline 의 HTTP /v1 인터페이스 섹션은 더 이상 유효하지 않으며 본 인터페이스 인덱스에서 제거되었다. 전략-facing 통신은 위의 3 RabbitMQ 표면이 전부다.

Interface: RabbitMQ messaging (전략 모듈 ↔ trex-core, BAT-007)

  • Type: amqp-messaging
  • Consumers / Producers: trex-core daemon ↔ 전략 모듈(외부 소비자). RabbitMQ broker(TREX_RABBITMQ_URL) 가 transport.
  • Purpose: HTTP /v1 API 를 대체하는 메시지 기반 통신. 코어가 단독 admission + 슬롯 전이(단일 writer 불변식 A) 를 유지.
  • Stability: experimental (BAT-007)

Topology

종류 이름 타입/속성 용도
큐 trex.snapshot.req durable 스냅샷 RPC 요청 큐 (read-only)
큐 trex.intent.req durable, x-dead-letter-exchange=trex.events.dlq intent admission RPC 요청 큐
익스체인지 trex.events topic, durable 체결/슬롯 상태 이벤트 발행
익스체인지 trex.events.dlq fanout, durable 처리 실패/unroutable 메시지 격리
큐 trex.events.dlq durable DLQ 수집기 큐 (trex.events.dlq 익스체인지 바인딩)
큐 (전략별) trex.events.strategy.{strategy_id} durable, x-message-ttl=3.6e6, x-max-length=10000, x-overflow=drop-head 전략 모듈별 이벤트 수신 큐 (*.{strategy_id}.# 바인딩)

Patterns

패턴 표면 요청 속성 응답
스냅샷 RPC trex.snapshot.req correlation_id, reply_to=amq.rabbitmq.reply-to reply-to 큐로 {view, strategy_id, trading_date, slots[], capacity{}}
intent RPC trex.intent.req correlation_id = intent 멱등키, reply_to=amq.rabbitmq.reply-to, delivery_mode=2 reply-to 큐로 `{intent_id, status:admitted
update_thresholds trex.intent.req action="update_thresholds", reply_to reply-to 큐로 `{status:updated
이벤트 pub/sub trex.events (topic) routing key {event_type}.{strategy_id}.{symbol} 전략 큐가 자기 strategy_id 패턴으로 바인딩

Message contracts

trex.snapshot.req 요청 payload

{ "view": "open" | "all", "strategy_id": str | null, "trading_date": "YYYY-MM-DD" }

응답 payload는 슬롯 목록과 capacity 를 운반한다. view 모드와 strategy_id 는 payload(요청 매핑 원칙 — REQ-021).

trex.intent.req 요청 payload (buy)

{ "intent_type": "buy",
  "intent_id":   "<correlation_id 와 동일>",
  "trading_date": "YYYY-MM-DD",
  "portfolio_id": str,                 # REQUIRED (dcy-011/REQ-007) — canonical key(asset_id)
  "qty": float?, "allocated_capital": float?,
  "entry_price": float?, "strategy_id": str?, "sell_strategy_id": str?,
  "forced_close_at": "ISO-8601"?,
  "stop_loss_price": number?, "take_profit_price": number? }
  • portfolio_id 필수(dcy-011/REQ-007/021): 매수의 정식 식별자. 누락 시 admission 에서 portfolio_id_required 로 거부. symbol 은 client payload 에 없다 — core 가 admission 시 universe(SnapshotStore)에서 portfolio_id → symbol 을 resolve 해 슬롯/KIS 주문에 사용한다. universe 에 portfolio_id 가 없으면 unknown_portfolio 로 거부(하위호환·symbol fallback 없음, hard cutover).
  • stop_loss_price/take_profit_price(선택, REQ-035/038): 매수 시 설정하면 워치독 만료 후 safety-sell 가격 트리거의 기준이 된다. 미포함 시 NULL(트리거 비활성). sell payload 는 {"intent_type":"sell", "intent_id":..., "slot_id":..., "reason":..., "sell_strategy_id":..., "limit_price": float?}.
  • limit_price 선택(dcy-012/REQ-007): 매도 지정가. 지정 시 지정가 매도(ORD_DVSN="00", 그 가격), 미지정 시 시장가 매도(ORD_DVSN="01"). 자율 매도(safety-sell/kill-switch)는 가격을 싣지 않아 항상 시장가로 체결된다. 매수가(과거 entry_price)는 매도 가격으로 재사용하지 않는다(issue #10 — 슬롯 limit_price 는 pending 이탈 시 clear). buy payload 의 entry_price 필드명은 본 주기에서 유지(하위호환).

trex.intent.req update_thresholds 액션 (BAT-011 / REQ-040)

{ "action": "update_thresholds",
  "slot_id": str,
  "stop_loss_price": number | null?,     # 생략=유지, null=해제
  "take_profit_price": number | null?,   # 생략=유지, null=해제
  "forced_close_at": "ISO-8601" | null?, # 생략=유지, null=해제
  "actor": str? }
  • 매수 이후 손절·익절·강제 만기 시각을 갱신한다. 세 필드는 독립적이며 payload 에 키가 없으면 기존 값을 유지, 명시적 null 이면 해제한다(REQ-040 AC-4/AC-5).
  • holding 슬롯에서만 적용된다. 처리 성공 시 해당 슬롯의 워치독 last_seen_at 을 리셋한다(전략 모듈 생존 신호, REQ-034).
  • 응답: {status: "updated", slot_id} / 비-holding 은 {status: "ignored", slot_id, reason: "not_holding"} / 미존재는 {status: "ignored", slot_id, reason: "slot_not_found"}.

trex.events payload (BAT-007)

{ "event_type": "slot_filled_buy"|"slot_filled_sell"|...,
  "slot_id": str, "strategy_id": str?, "symbol": str?,
  "status": "<SlotState 값>", "qty": float?, "avg_price": float?,
  "portfolio_id": str?, "trading_date": "YYYY-MM-DD",
  "sequence_at": "ISO-8601" }
  • sequence_at = 슬롯 updated_at 의 단조 증가 토큰. 수신자는 last-wins 적용 가능 → 순서 보증 불요.
  • 이벤트의 strategy_id/symbol 은 routing key 에, 본문에도 함께 운반(이벤트 매핑 원칙 — REQ-021).

Reliability rules (REQ-023)

  • intent RPC: durable + persistent + manual ack-after-commit + intent_log correlation_id dedup. 단일 consumer + prefetch=1 → FIFO.
  • 스냅샷 RPC: read-only, manual ack, 별도 dedup 불요.
  • 이벤트: publisher confirms 활성, unroutable → DLQ. 페이로드는 현재 상태 + 단조 시퀀스(last-wins).
  • per-strategy 이벤트 queue: TTL/max-length 초과 시 drop-head + 스냅샷 RPC reconcile 로 보정.

Degrade (REQ-024)

  • 어댑터 TransportHealth ∈ {connected, disconnected, reconnecting}. 어플리케이션 레이어는 disconnected 상태에서 TransportUnavailableError 로 빠르게 실패한다(버퍼링 0, 보수적 중단).
  • 재연결은 백오프 1s→2s→4s→8s→16s→30s cap, 어댑터가 자체 구현.
  • core 자율 안전 매도는 transport 와 무관하게 trex-safety-sell worker 가 동작한다(forced_close / stop-loss·take-profit / 장 마감, dcy-007 REQ-037~041, BAT-010/011 구현 완료).

trex.snapshot.req envelope (BAT-008 / REQ-028)

전략 모듈이 보내는 요청:

{ "view": "all" | "buyable" | "held"   # default "all"
  "strategy_key": "<slug>"             # optional, == filter on active_strategy_id
  "trading_date": "YYYY-MM-DD"         # optional, default UTC today
}

코어가 reply-to 큐로 보내는 응답 envelope:

{ "assets": [                          # always a list (REQ-028 AC-1, never null)
    { "symbol": str,
      "asset_id": str,                 # == portfolio_id (dcy-004 OQ-1)
      "cash_balance": number,
      "asset_quantity": number,
      "active_strategy_id": str | null,
      "strategy_params": object        # opaque, pass-through
    }, ...
  ],
  "view": "all" | "buyable" | "held",  # echo
  "available_slots": int,              # idle_count snapshot
  "degraded": bool,                    # REQ-028 AC-2
  "reason":  "portfolio_hub_unavailable" | "snapshot_fetch_failed"
           | "cash_position_stale" | null,
  "message": str | null                # sanitized — no credentials/raw exceptions
}

No price field on items (REQ-028 AC-3 / REQ-027 AC-1).

  • 워치독 신호 (REQ-034): trex.snapshot.req 처리는 read-only 응답 산출에 더해, 응답에 포함된 모든 holding 슬롯의 워치독 last_seen_at 을 갱신한다. 즉 전략 모듈이 스냅샷을 폴링하는 것 자체가 해당 슬롯의 생존 신호이며, 폴링이 끊긴(워치독 만료) 슬롯만 safety-sell 트리거 대상이 된다.

trex.control.req 제어 RPC (BAT-012 / REQ-043, REQ-032)

운영자 제어 채널. action discriminator 로 분기한다(REQ-043). 기존 trex.pause.req(REQ-032)의 개념 확장이며, 단일 제어 큐 + action 패턴으로 기능마다 큐/consumer 를 늘리지 않는다.

action 목록: pause | resume | list (REQ-032) · kill_switch (REQ-045) · resume_kill_switch (REQ-046). 알 수 없는 action 은 {status:"error", reason:"unknown_action:<a>"}.

pause/resume/list 요청·응답:

{ "action": "pause" | "resume" | "list", "portfolio_id": str, "actor": str? }
→ { "status": "ok"|"error", "paused": [str,...], "action": echo, "reason"?: str }
  • pause 는 snapshot_pause(portfolio_id PK, 영구) upsert — daemon 재시작·일일 refresh 후 유지(REQ-032 AC-2). CLI trex-core pause/resume/paused 가 같은 store 를 본다.

kill_switch 요청·응답 (REQ-045):

{ "action": "kill_switch", "scope": "portfolio"|"global", "portfolio_id": str?, "actor": str? }
→ { "status": "ok", "scope": echo, "portfolio_id"?: str, "liquidated": int }
  • scope 범위의 모든 holding 슬롯을 SlotService.sell(reason="emergency_stop") 로 즉시 청산하고, 해당 범위를 당일 제외(killswitch_exclusion)로 표시한다. scope=portfolio 인데 portfolio_id 누락 시 {status:error, reason:portfolio_id_required}.

resume_kill_switch 요청·응답 (REQ-046):

{ "action": "resume_kill_switch", "scope": "portfolio"|"global", "portfolio_id": str? }
→ { "status": "ok", "scope": echo, "portfolio_id"?: str }
  • 해당 범위의 당일 제외만 해제(killswitch_exclusion row 삭제)해 정상 매매로 복귀. 이미 청산(매도)된 슬롯은 되돌리지 않는다(매도 비가역).

  • snapshot masking 소스(REQ-046): mask = pending | warning | pause | killswitch_today. kill-switch 당일 제외는 (portfolio_id, trading_date) day-scoped — 다음 거래일 자동 복귀. portfolio_id="*" sentinel 은 전역(전체 universe) 마스킹.

  • 하위호환: trex.pause.req 는 legacy alias. 운영 배포 시 제어 클라이언트는 trex.control.req 로 이전한다(rollout 결정, REQ-043 Notes).

Multi-instance routing (BAT-013 / REQ-049/050)

다중 인스턴스 운영 시 큐명과 라우팅을 instance_key 로 분리한다(dcy-009).

  • 큐명 규칙: {base}.{instance_key}(예: trex.intent.req.inst-1). instance_key = "default" 는 기존 리터럴 큐명(trex.intent.req 등)을 그대로 사용한다(하위호환, FR-5).
  • trex.direct(topic exchange): 특정 인스턴스 지정 전달. routing key = {instance_key}.{channel_type}(channel_type ∈ {intent, snapshot, control}). 각 인스턴스 큐가 자신의 routing key 로 바인딩 → inst-1.intent publish 는 trex.intent.req.inst-1 에만 도달.
  • trex.broadcast(fanout exchange): 전역 제어. 각 인스턴스의 control 큐가 dual 바인딩되어, broadcast publish 는 모든 인스턴스 control 큐에 동시 전달(global kill-switch 등). 중복 전달은 핸들러 idempotency(REQ-036/advisory lock)로 무해.
  • 기동: 데몬은 --config <path>.toml(필수)로 instance_key/portfolio_hub_token 을 읽는다(.env 아님, REQ-048).

Interface: PortfolioHubReadPort (Protocol, BAT-008)

  • Type: external-service (HTTP, portfolio-hub)
  • Consumers / Producers: SnapshotScheduler(소비) ↔ PortfolioHubReadAdapter 또는 StubPortfolioRead
  • Purpose: portfolio-hub universe(이 데몬이 관리하는 portfolio 집합) read 경계 — write 는 BAT-006 LedgerPort 와 분리.
  • Stability: experimental (BAT-008)

Inputs

  • whoami() -> str :: 자격증명/토큰에서 daemon_id 도출(1회). 별도 env 1차 출처 두지 않음.
  • get_portfolios(daemon_id) -> list[PortfolioItem] :: managed_by_daemon_id == daemon_id scope 조회.

PortfolioItem Fields

  • portfolio_id :: str :: == asset_id (dcy-004 OQ-1)
  • symbol :: str
  • cash_balance :: float
  • asset_quantity :: float :: integer shares semantically
  • active_strategy_id :: str | None
  • strategy_params :: dict :: opaque pass-through (no core parsing)

Outputs / Outcomes

  • 정상: list[PortfolioItem] (배열, 비면 []).
  • 어댑터 어떤 실패든 PortfolioHubUnavailableError 로 통일 (호출자는 SnapshotStore.set_degraded 로 전환).

Error Contract

  • PortfolioHubUnavailableError :: 네트워크/5xx/4xx/JSON 파싱 실패 등 fetch 불가. 자격증명/예외 본문은 호출 측에서 sanitize 후 노출(REQ-028 AC-2).

portfolio-hub Endpoint (어댑터 구현)

HTTP URL 응답
GET /v1/auth/whoami {daemon_id: str, ...}
GET /v1/portfolios?managed_by_daemon_id=<id> {items: [PortfolioItem, ...]} 또는 list root

Interface: SnapshotStore + PauseStore + SnapshotScheduler (Protocol, BAT-008)

  • Type: internal-service
  • Consumers / Producers: SnapshotScheduler(생산) → SnapshotStore(read 채널) ← make_snapshot_rpc_handler(소비). PauseStore(PostgreSQLPauseStore 또는 InMemoryPauseStore)는 CLI/RPC 양쪽이 쓰고 handler 가 읽는다.

Inputs

  • SnapshotStore.swap(items) :: atomic immutable swap. 직전 warning flag 는 carry-over(다음 reconcile 에서 자동 해제 판단).
  • SnapshotStore.set_warning(pid, on) :: 정합 결과 overlay(in-memory only).
  • SnapshotStore.update_cash_position(pid, cash, qty) :: 옵션 D wiring point(REQ-030 AC-2/AC-3 — portfolio-hub#8 배포 후에만 ledger writer 가 호출).
  • SnapshotStore.set_degraded(reason, message) / clear_degraded() :: degraded signal.
  • PauseStore.pause(pid, actor) / resume(pid) / list_paused() :: durable manual overlay.
  • SnapshotScheduler.run_once() :: fetch + swap + reconcile 1 회.
  • SnapshotScheduler.run_forever(stop, tick_sec) :: boot 즉시 1 회 + 매일 TREX_SNAPSHOT_SCHEDULE_KST 시각에 1 회.

Outputs / Outcomes

  • run_once() returns True(성공) / False(degraded). True 시 reconcile 한 패스가 함께 돈다.
  • list_assets() + degraded_state() 를 envelope builder 가 매 RPC 요청마다 읽음.

Compatibility Rules

  • pause 의 영속성 단위는 portfolio_id(= asset_id). 동일 portfolio_id 재 pause 는 upsert(actor만 갱신).

  • warning 은 in-memory only — 재시작 시 초기화되며 다음 정합 패스가 다시 판정한다.

  • universe swap 은 pause/warning 의 결정을 invalidate 하지 않는다(warning 은 carry-over, pause 는 store 가 별도 영속).

  • Type: internal-service

  • Consumers / Producers: 상위 애플리케이션(예: trex-orbit 류 매매 데몬)의 매수/매도/주문 결정 경로 및 운영자 도구

  • Purpose: 슬롯 상태 전이를 레이스-프리하게 수행하고 일일 슬롯 가용성/재진입 카운트를 계산한다.

  • Stability: internal

Inputs

  • preallocate_daily_slots(trading_date, count) :: 거래일 idle 슬롯 멱등 사전 할당 (count > 0)
  • transit_slot(slot_id, from_state, to_state, reason, actor, **fields) :: advisory lock 하 원자 상태 전이
  • get_today_entry_count(trading_date, symbol) :: 재진입 정책 입력 조회
  • compute_slot_capacity(trading_date, total_slots) :: 가용성 스냅샷 계산 (total_slots > 0)

Outputs

  • Slot :: 전이 후 슬롯 행 스냅샷
  • SlotCapacity :: idle/holding/pending 카운트 + scarcity_ratio
  • int :: 사전 할당 수 / 재진입 카운트
  • slot_transition_log append :: 모든 전이의 append-only 감사 기록(부수 효과)

Error Contract

  • SlotTransitionError :: 허용되지 않은 (from_state, to_state) 전이
  • SlotStateMismatchError :: read-write 사이 상태 변동(레이스 감지)
  • SlotNotFoundError :: 존재하지 않는 slot_id
  • ValueError :: count <= 0 또는 total_slots <= 0

Compatibility Rules

  • 6 상태 / 8 전이 패밀리 계약을 깨는 변경은 breaking 으로 취급한다.
  • 상태 변경 API 는 transactional_advisory_lock 안에서만 수행하고 lock 보유 중 외부 I/O 를 금지한다.

Interface: SlotRepository (Protocol)

  • Type: internal-service
  • Consumers / Producers: SlotService(소비) ↔ 저장소 구현체(InMemorySlotRepository / 향후 PostgreSQLSlotRepository)
  • Purpose: 도메인을 특정 저장소 기술에서 분리하는 영속화 경계 계약.
  • Stability: internal

Inputs

  • transactional_advisory_lock(lock_id) :: 단일 트랜잭션 + advisory lock 컨텍스트 매니저
  • pre_allocate_slots / fetch_slot / find_idle_slot / update_slot :: daily_slot CRUD
  • append_transition_log :: append-only 전이 로그
  • increment_symbol_entry / get_symbol_entry_count :: 재진입 카운터
  • count_slots_by_status :: 상태별 집계

Outputs

  • Slot / SlotTransitionRecord :: 도메인 모델
  • dict[SlotState, int] :: 상태별 카운트
  • int :: 재진입 카운트 / 사전 할당 수

Error Contract

  • SlotNotFoundError :: update 대상 row 부재

Compatibility Rules

  • 구현체는 advisory lock 직렬화와 append-only 전이 로그 의미를 반드시 보장한다.
  • 구현체는 lock 보유 중 외부 I/O 를 수행하지 않는다.

Interface: LedgerPort (BAT-006)

  • Type: external-service (HTTP, portfolio-hub)
  • Consumers / Producers: LedgerWriter(소비) → PortfolioHubLedger 어댑터 또는 InMemoryLedger stub
  • Purpose: 체결을 외부 회계/포지션 원장에 기록한다. orbit post_ledger_trade 패턴.
  • Stability: experimental (v1)

Inputs

  • record_trade(trade: LedgerTrade) :: 단일 체결을 원장에 기록 (execution_id 멱등)

LedgerTrade Fields

  • asset_id :: str :: ledger path param (= 슬롯 portfolio_id, dcy-004 OQ-1)
  • order_id :: str :: 로컬 주문 식별자 (broker_order_id)
  • execution_id :: str :: 결정적 합성 (paper: paper:{broker_order_id}:{filled_qty}, KIS: kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty})
  • side :: 'buy' | 'sell'
  • qty / 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} — 로컬 dedup 키

Outputs / Outcomes

  • LedgerWriteResult.NEW :: 새 원장 엔트리 적재
  • LedgerWriteResult.DUPLICATE_OK :: 같은 execution_id 가 이미 기록되어 있음(서버 멱등)

Error Contract

  • LedgerConflictError :: portfolio-hub 409 — 운영자 해소 대상(자동 재시도 중단)
  • LedgerRetryableError :: 5xx / timeout / network — outbox 가 재시도

portfolio-hub Endpoint (어댑터 구현)

HTTP URL Body 응답
POST /v1/assets/{asset_id}/ledger/trades side, qty, price, fee, tax, executed_at, execution_id, order_id 2xx OK(duplicate:true 면 server-side dedup), 409 conflict, 5xx/network → retryable

Interface: LedgerOutbox Protocol + LedgerWriter (BAT-006)

  • Type: internal-service (durable work-item store + drainer)
  • Consumers / Producers: ExecutionService(생산자, fill 시 enqueue) → LedgerWriter(소비자, 백그라운드 워커)
  • Purpose: 체결 → 원장 기록을 슬롯 전이와 분리해 무유실 + 멱등으로 처리한다. correlation_id 로 outbox 적재 dedup.
  • Stability: internal

Inputs

  • enqueue(trade: LedgerTrade) :: correlation_id 키로 멱등 적재
  • list_pending(limit) :: FIFO pending work item
  • get(correlation_id) :: 단일 row 조회
  • mark_outcome(correlation_id, outcome, error?) :: outcome 기록 + 상태 전이

Outcomes (OutboxOutcome)

  • completed :: 어댑터 NEW
  • duplicate_completed :: 어댑터 DUPLICATE_OK 또는 로컬 dedup 히트
  • retryable_failure :: 다음 tick 재시도 (status 는 pending 유지)
  • conflict_blocked :: 409, 자동 재시도 중단(운영자 해소)

WorkItem Status

  • pending → completed (성공/중복)
  • pending → conflict_blocked (409)
  • pending → pending (retryable, attempts++)

Persistence

  • PostgreSQL: ledger_outbox(correlation_id PK, asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at, status, attempts, last_outcome, last_error, created_at, updated_at) — ON CONFLICT (correlation_id) DO NOTHING 이 outbox dedup 게이트.

FillStatus 확장 (BAT-006 / REQ-018)

  • state, filled_qty, avg_price, raw 외에 다음 필드를 운반한다.
    • execution_id :: str? :: 결정적 합성(브로커별 규칙). filled 상태에서 필수.
    • fee :: float :: BrokerPort calculate_fee 결과(기본 0).
    • tax :: float :: 기본 0(정밀 모델 후속).

Interface: MessagingTransport (Protocol, BAT-007)

  • Type: internal-service (transport boundary)
  • Consumers / Producers: 데몬 런타임(소비) ↔ 구현체(RabbitMQTransport 운영용 / InMemoryTransport 결정론 stub)
  • Purpose: 메시지 transport 추상화. 도메인은 transport/AMQP 의존 0(NFR-1).
  • Stability: experimental (BAT-007)

Inputs

  • declare_topology() :: 익스체인지/큐/바인딩 선언(멱등)
  • publish_event(routing_key, payload, mandatory=True) :: trex.events 발행. publisher confirms; unroutable → DLQ
  • call_rpc(queue, payload, *, correlation_id, timeout) :: 클라이언트 RPC (직접 reply-to 큐 대기)
  • consume_rpc_forever(queue, handler, stop) :: 서버측 RPC 소비 루프(stop 까지 블로킹, 백오프 재연결)
  • consume_events_forever(queue, binding_pattern, handler, stop) :: 이벤트 구독 루프

Outputs / Outcomes

  • health() -> TransportHealth :: connected | disconnected | reconnecting
  • DLQ 라우팅 :: 처리 실패/unroutable 메시지는 trex.events.dlq exchange/queue 로 격리

Error Contract

  • TransportUnavailableError :: connected 가 아닌 상태에서 publish_event/call_rpc 호출 시 즉시 실패(REQ-024 — 로컬 버퍼링 금지)
  • RpcTimeoutError :: 설정된 timeout 안에 reply 없음

Compatibility Rules

  • pika BlockingConnection 은 thread-safe 가 아니므로 consumer/publisher 마다 자기 connection/channel 을 가져야 한다.
  • 토폴로지 선언은 멱등하지만 attribute(durable, args) 가 기존 큐와 다르면 RabbitMQ 가 PRECONDITION_FAILED 로 거부 → batch 마이그레이션 시 큐 재선언 절차 필요.

Shared Constraints

  • 도메인은 외부 의존(브로커, DB 드라이버 등)을 직접 호출하지 않는다(순수 도메인 경계).
  • 모든 상태 변경은 단일 advisory lock(SLOT_ADVISORY_LOCK_ID)으로 직렬화한다.

Current Gaps / Planned Changes

  • 영속화 구현체(PostgreSQL)와 그에 딸린 SQL 스키마/마이그레이션은 이번 이관 범위 밖이며 향후 도입 예정.
  • 외부 HTTP API / CLI 표면 없음(trex-orbit 의 trex slot CLI 미이식).
  • 패키지 네임스페이스 결정(plan.md OQ-1)에 따라 import 경로 계약이 확정된다.

Update Triggers

  • Update this document when an externally visible API, event, CLI, or file contract changes.
  • Update this document when a batch changes compatibility, validation, or error behavior.
  • Update this document when a new external consumer or producer is introduced.

Change Log

  • 2026-06-03 :: bootstrap-baseline 으로 초기 인터페이스 계약 baseline 생성(SlotService + SlotRepository Protocol).
  • 2026-06-03 :: BAT-002 — HTTP /v1 API 계약(데몬, 6 엔드포인트) + 멱등/단일writer 불변식 추가(REQ-007/008/009).
  • 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
  • 2026-06-04 :: BAT-006 — LedgerPort/LedgerTrade/LedgerWriteResult 계약, LedgerOutbox/LedgerWriter, portfolio-hub /v1/assets/{asset_id}/ledger/trades 엔드포인트 + 409→LedgerConflictError, FillStatus.execution_id/fee/tax 확장 (REQ-017~020).
  • 2026-06-05 :: BAT-007 — HTTP /v1 API 인터페이스 제거(adapters/api + fastapi/uvicorn 의존 정리), RabbitMQ messaging 계약 추가(스냅샷 RPC trex.snapshot.req, intent RPC trex.intent.req, 이벤트 topic trex.events, 전략별 durable queue + TTL/max-length, DLQ, publisher confirms, ack-after-commit), MessagingTransport Protocol + TransportHealth 노출 (REQ-021~026).
  • 2026-06-06 :: BAT-008 — trex.snapshot.req envelope 계약 정의(assets[]/view/available_slots/degraded/reason/message, no price field), trex.pause.req 제어 RPC 추가, PortfolioHubReadPort/SnapshotStore/PauseStore/SnapshotScheduler 인터페이스, BrokerPort.get_balance() 확장, trex-core CLI 추가 (REQ-027~033; REQ-030 옵션 D 는 portfolio-hub#8 배포 후).