# 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-019) - 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|rejected, slot_id?, reason?}`. 체결/종료는 응답 아닌 이벤트로 | | update_thresholds | `trex.intent.req` | `action="update_thresholds"`, `reply_to` | reply-to 큐로 `{status:updated|ignored, slot_id, reason?}` (REQ-040) | | 이벤트 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": "", "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": "", "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": "" # 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:"}`. 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 .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=` | `{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 — 운영자 해소 대상(자동 재시도 중단) - `LedgerPermanentError` :: portfolio-hub **404/400/422**(dcy-015/REQ-017, issue #13) — 재시도로 해소 불가(미등록 포트폴리오·malformed). outbox 가 즉시 터미널 `delivery_failed` 처리(재시도 0) - `LedgerRetryableError` :: 5xx / timeout / network / 408 / 429 — outbox 가 **지수 백오프 + jitter** 로 재시도, `max_attempts` 초과 시 `delivery_failed`(dead-letter) ### 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(정밀 모델 후속). - `cancelled :: bool` :: 외부 취소 감지(dcy-013/REQ-012, issue #9). 기본 False. - `state` 값과 집행 처리(dcy-013): - `accepted` / `partially_filled`(취소 아님) :: 다음 tick 대기. (FR-3) `TREX_PENDING_STALE_THRESHOLD`(초, 0=비활성) 초과 시 Alert-only WARN+카운터(상태 미변경). - `filled` :: 전량 체결 → `pending_buy→holding` / `pending_sell→closed` + 원장 1건. - `cancelled`(체결 0 외부 취소) :: `is_terminal_reject` → 롤백(`pending_sell→holding` / `pending_buy→idle`), `clear_dispatch` → 자동 재매도 가능. - `partially_filled` + `cancelled=True`(부분 체결 후 외부 취소) :: `is_partial_cancel` → 체결분 원장 1회 기록 + 슬롯 qty 차감(매도: 잔여 held / 매수: 체결분만) + `holding` 복귀. 매도는 cost basis(`avg_price`) 유지. - KIS 매핑: `inquire-daily-ccld` 응답 row 의 `cncl_yn="Y"` 를 취소로 해석한다(미지/부재 값은 보수적으로 비취소 처리, stale 워치독이 backstop). ## 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 배포 후).