58 KiB
Trex Core Interface Contract
- Baseline Mode: mixed
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-09-20 (trex-core Issue #97 ledger outbox idle observability)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); HTTP API = dcy-002 / BAT-002; operator broker truth = dcy-037 / BAT-057; ledger venue transport = portfolio-hub ProofLine req-0009
- 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
/v1API(슬롯 코어 데몬, 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.slots.req:: 전략 모듈/운영자의 슬롯 읽기 RPC (request-reply, BAT-035 — REQ-028trex.snapshot.req대체) - 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-coreCLI :: 운영 명령 — pause/resume/paused(direct-DB, BAT-008) + universe/slots/enter/exit(데몬 RPC, dcy-016/BAT-020) + cancel(pending 취소 백스톱, dcy-022/BAT-032)- 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 경계 +
TransportHealthenum (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
/v1API 표면이 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
/v1API 를 대체하는 메시지 기반 통신. 코어가 단독 admission + 슬롯 전이(단일 writer 불변식 A) 를 유지. - Stability: experimental (BAT-007)
Topology
| 종류 | 이름 | 타입/속성 | 용도 |
|---|---|---|---|
| 큐 | trex.slots.req |
durable | 슬롯 RPC 요청 큐 (read-only) — REQ-028 trex.snapshot.req 대체(BAT-035) |
| 큐 | 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.slots.req |
correlation_id, reply_to=amq.rabbitmq.reply-to |
reply-to 큐로 {slots[], degraded, reason, message, ledger_outbox_metrics?} (ledger_outbox_metrics는 spy=true 전용, REQ-073/074, AC-0040) |
| 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 패턴으로 바인딩 |
slot_filled_sell의strategy_id는 SELL admission에서 확정해sell_strategy_id에 보존한다. 우선순위는 요청의 non-emptysell_strategy_id→ 기존 보존sell_strategy_id→ legacy/safety 경로의 non-emptystrategy_id다.- SELL terminal transition이 owner 필드를 clear하기 전에 보존 owner를 routing hint로 캡처한다. full fill, partial-fill cancel, 외부 cancel 후 재매도, 재기동 후 reprocessing에도 같은 owner를 사용한다.
- 위 source가 모두 없으면
slot_filled_sell._.{symbol}을 발행하지 않는다. 금융 상태 전이와 ledger/outbox는 유지하고, 해당 tick의sell_event_owner_missingcounter·ERROR log·best-effort alarm을 남긴다. ownerless event를 일반 unroutable DLQ 메시지로 합성하지 않는다. - event payload의 기존
slot_id,event_type,sequence_atidentity는 producer가 보존한다. delivery는 at-least-once이며 consumer의 멱등 처리나 exactly-once는 이 인터페이스의 보장이 아니다.
Message contracts
trex.slots.req 요청 payload (REQ-073 AC-1, BAT-035; REQ-116 BAT-055)
{ "filter": "all" | "inactive" | "pending" | "holding" | "error", # default "all"
"spy": true | false, # default false; true = watchdog 갱신 안 함(운영자 조회)
"strategy": str, # optional (REQ-116 BAT-055) — exact-match on strategy_name
"strategy_instance_id": str # required non-empty string when spy=false; ignored for spy=true filtering
}
filter: 서버 측 슬롯 status 필터."inactive"는 buyable 슬롯(universe에 바인딩된 매수 가능 슬롯). 미인식 값은"all"로 처리.spy=true: 응답에 포함된 holding 슬롯의 워치독last_seen_at을 갱신하지 않는다(운영자 CLI 조회가 전략 생존 신호로 오인되지 않도록 — REQ-034 CHG-01).spy=false:strategy_instance_id는 required non-empty string이다. 누락·비문자열·빈 값은{slots:[], available_slots:null, degraded:false, reason:"bad_request:strategy_instance_id", message:"strategy_instance_id must be a non-empty string"}를 반환하며 watchdog을 갱신하지 않는다.- strategy-state visibility:
spy=false는 exact requested entry 하나를strategy_state: {version,data,updated_at}로만 반환하고 missing entry는 생략한다.spy=true는 supplied ID와 무관하게 complete{strategy_instance_id: {version,data,updated_at}}map을 반환한다. strategy(REQ-116 additive): 지정 시strategy_name == strategy인 슬롯 행만 반환. 대소문자 exact-match. 미지정 시 기존과 완전 동일.filter와 AND 조합. 매칭 없으면slots=[](에러 없음).available_slots는 strategy 필터와 무관하게 인스턴스 전체 기준으로 계산(REQ-073 AC-6 불변).
코어가 reply-to 큐로 보내는 응답 payload:
{
"slots": [
{ "slot_id": "str (UUID)",
"status": "inactive | pending_buy | pending_sell | holding | error",
"symbol": "str | null",
"qty": "float | null", // holding: broker authority / unavailable=null
"qty_source": "broker | unavailable | null", // non-holding=null
"broker_quantity_available": "bool", // non-holding=false
"broker_quantity_captured_at": "ISO-8601 | null", // non-holding=null
"slot_quantity": "float | null", // core daily_slot mirror (diagnostic)
"asset_quantity": "float | null", // hub authority (REQ-062)
"avg_price": "float | null",
"entry_price": "float | null",
"portfolio_id": "str | null",
"forced_close_at": "ISO-8601 | null",
"universe_detached": "bool",
"cash_balance": "float | null", // hub authority
"cash_estimated": "bool", // true = pending ledger outbox delta 반영
"warned": "bool", // slot_quantity != asset_quantity 시 true
"watchdog": { // null if watchdog store unavailable
"expired": "bool",
"last_seen": "ISO-8601 | null"
},
"paused": "bool",
"reconcile_warned": "bool", // durable B/C warning 또는 unknown case 존재
"reconcile_warning_cases": ["B", "C"], // 활성 case code 목록
"trade_eligible": "bool", // known blocking/unknown case가 없을 때만 true
"strategy_name": "str | null", // REQ-116 additive (BAT-055) — portfolio-hub PortfolioItem 출처
"strategy_params": "dict", // REQ-116 additive (BAT-055) — portfolio-hub opaque dict; {} if absent
"markets": ["str"] // REQ-116 additive (BAT-055) — portfolio-hub markets list; [] if absent
}
],
"excluded_slots": {
"reconcile_quantity_mismatch": "int",
"active_reconcile_warning_cases": {"B": "int", "C": "int"}
},
"degraded": "bool",
"reason": "portfolio_hub_unavailable | snapshot_fetch_failed | cash_position_stale | warn_store_unavailable | null",
"message": "str | null" // sanitized — no credentials/raw exceptions
}
필드 소스 테이블 (REQ-074 AC-1, REQ-116 추가):
| 필드 | 권위 소스 | 비고 |
|---|---|---|
slot_id, status, avg_price, entry_price, forced_close_at, universe_detached |
slot (core) | 슬롯 row 직접 |
qty |
broker account snapshot | holding 슬롯에서만 authoritative. unavailable/stale 시 null |
qty_source |
파생(metadata) | holding 슬롯: broker | unavailable; non-holding은 null |
broker_quantity_available |
파생(metadata) | holding 슬롯 broker qty freshness 통과 여부; non-holding=false |
broker_quantity_captured_at |
broker account snapshot | holding 슬롯 broker qty capture 시각; non-holding=null |
slot_quantity |
slot (core) | 기존 daily_slot.qty mirror(진단용) |
symbol, portfolio_id |
slot (core) | 자산 바인딩 |
asset_quantity |
hub (SnapshotStore) | portfolio-hub 권위값 (REQ-062) |
cash_balance |
hub (SnapshotStore) | portfolio-hub 권위값; cash_estimated=true 시 ledger outbox delta 추정값 |
cash_estimated |
ledger outbox | pending delta 있을 때 true (REQ-029) |
warned |
파생 | slot_quantity != asset_quantity (REQ-074 AC-2) |
watchdog |
WatchdogStore | 만료 여부 + last_seen; null=store 미연결 |
paused |
PauseStore | durable pause overlay (REQ-032) |
reconcile_warned, reconcile_warning_cases |
ReconcileWarningStore | 동일 slot의 durable B/C 원인을 동시에 보존·노출 |
trade_eligible |
파생(safety gate) | 활성 B/C 또는 지원하지 않는 warning case가 있으면 false |
strategy_name |
hub (PortfolioItem via SnapshotStore) | REQ-116 additive. portfolio-hub 정본. hub 미제공 시 null |
strategy_params |
hub (PortfolioItem via SnapshotStore) | REQ-116 additive. opaque dict; hub 미제공 시 {} |
markets |
hub (PortfolioItem via SnapshotStore) | REQ-116 additive. list[str]; hub 미제공 시 [] |
- slot-origin strategy 필드 없음 (REQ-074 AC-5, REQ-116 additive 재도입 후에도 유지):
strategy_id,sell_strategy_id,active_strategy_id는 응답에 포함되지 않는다.strategy_name/strategy_params/markets는 PortfolioItem(hub) 출처이므로 AC-5 제외 대상과 구분된다 — REQ-116 Notes 참조. - Reconcile warning fail-closed: B/C warning이 활성인 holding slot은 응답에서 제외된다. warning store 조회 실패, empty metadata, unknown case는
slots=[],degraded=true,reason="warn_store_unavailable"로 처리한다. intent admission도 동일 durable source를 재확인하며 unknown case를 거래 허용 근거로 사용하지 않는다. - Breaking change (holding
qty) rollout: 기존 전략 클라이언트가qty=slot mirror로 가정했다면slot_quantity를 읽도록 전환해야 한다. 권장 경로는 (1) 클라이언트에qty_source/broker_quantity_available체크 추가 → (2) fail-closed (qty is null) 처리 배포 → (3)qty를 broker 정본으로 사용하도록 cutover 순서다. spy=false(기본): holding 슬롯 응답 후WatchdogStore.update(slot_id)호출 — 전략 폴링 = 생존 신호.spy=true: watchdog 갱신 건너뜀 — 운영자 CLI(universe/slots) 가 항상 사용.
spy=true ledger outbox metrics (AC-0040)
ledger_outbox_metrics는spy=true운영자 응답의 top-level additive field다.spy=false전략 응답에는 key 자체를 포함하지 않는다.- 정상 snapshot은
available=true,scope="process_lifetime", configuredinstance_key와 다음 누적 정수를 제공한다.claims:empty_polls_total,claimed_batches_total,unavailable_total,lost_total,foreign_instance_totaloutcomes:completed_total,duplicate_completed_total,retryable_failure_total,conflict_blocked_total,permanent_failure_total,dead_lettered_total
- 각 값은 해당 daemon process의 configured instance writer에서만 누적하며 0 이상의 정수다. snapshot은 내부 mutable counter와 분리된 새 값이고 daemon restart 때 0으로 초기화된다.
- metrics reader가 없거나 조회에 실패하면 slots 조회는 계속 성공하고
available=false,scope="process_lifetime", configuredinstance_key,reason="ledger_outbox_metrics_unavailable"만 반환한다. raw exception text는 응답에 포함하지 않는다. - ledger writer는 fixed
ledger_interval_seccadence를 유지한다. empty claim은empty_polls_total만 증가시키고 INFO를 남기지 않는다. non-empty batch INFO만instance_key, non-zerocount,limit, repository claim 순서의correlation_idsJSON string array를 포함한다. _ledger_loop의 예상 밖 writer exception은reason=ledger_writer_run_once_failed, configured instance, exception class name만 ERROR로 남긴다. raw exception message, traceback과exc_info는 금지하며 loop는 다음 configured poll을 계속한다.
운영자 CLI ↔ RPC 매핑 (dcy-016 / REQ-032)
운영자 CLI 명령은 데몬 RabbitMQ RPC 로 통일된다(단일 writer admission 보존, DB 직접 쓰기 없음). 인스턴스 라우팅: --config <toml>(canonical, REQ-048) 또는 --instance <key>(단축·override), 미지정 시 default.
| CLI | RPC 큐 | payload | 비고 |
|---|---|---|---|
universe |
trex.slots.req[.{instance}] |
{filter:"inactive", spy:true} |
inactive 슬롯 목록 조회(워치독 억제) |
slots [--status all|holding|buyable|pending|error] |
trex.slots.req[.{instance}] |
{filter:"<status>", spy:true} — buyable→filter:"inactive" |
슬롯 상태 목록(server-side filter, 워치독 억제, BAT-035) |
enter <portfolio_id> [--capital/--qty/--stop-loss/--take-profit/--forced-close-at ...] |
trex.intent.req[.{instance}] |
buy intent(portfolio_id 필수, symbol 서버 resolve; --forced-close-at <ISO8601>→forced_close_at, REQ-032 CHG-20260609-01) |
안전장치 필수; --forced-close-at 형식은 제출 전 검증 |
exit <slot_id> [--price] |
trex.intent.req[.{instance}] |
sell intent(slot_id, optional limit_price) |
가격 미지정 시 시장가 |
pause/resume/paused |
(RPC 아님) | — | direct-DB 유지(OQ-3) |
report [--json] |
(RPC 아님) | — | direct-DB 시점 집계: 현재 보유 / 오늘(KST) 체결·실현 PnL(ledger_outbox net 매매 현금흐름, 수수료·세금 반영) / 대기 주문. 신규 RPC 없음(REQ-088/dcy-029 OQ-1) |
- enter/exit 안전장치(OQ-2): 확인 프롬프트 기본 ON,
--yes(paper 생략), live(broker=kis+allow_live)는--yes --live이중 ack,--dry-run미리보기.--json은 비대화형이라 enter/exit 에--yes필수(미지정 시 자동 제출 없이 에러). 모든 명령 출력에 대상 인스턴스 표시, admission 응답(status/slot_id/reason)을 표시하며 rejected 면 비0 종료.
청산 사유 어휘 — reason + actor (REQ-086 / dcy-029 FR-3)
슬롯 청산은 두 축으로 노출된다(신규 reason 값·필드 없음; 기존 reason·actor를 함께 본다):
- 조건축
reason— 안전망(safety-sell) 자율청산 family:stop_loss·take_profit·forced_close_at·market_close_liquidation. 운영자 매도:operator_exit. 비상정지:emergency_stop. 정상 체결:buy_filled/sell_filled/sell_filled_partial. - 주체축
actor—trex-safety-sell(안전망 watchdog 소유 자율청산) ·execution_loop(집행 루프 체결/롤백) ·operator(운영자) · 전략(전략 SellIntent).
핵심: stop_loss/take_profit/forced_close_at 는 모두 watchdog expired(전략 침묵 → 안전망 소유) 에서만 발화하므로, actor=trex-safety-sell 가 동반될 때의 reason=stop_loss 는 "전략의 손절"이 아니라 안전망 자율청산이다. 로그(safety-sell fired ... reason= actor=), slot_transition_log, 운영 report 가 reason+actor 를 함께 노출해 오해(2026-06-15 064400)를 제거한다.
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). CLItrex-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_exclusionrow 삭제)해 정상 매매로 복귀. 이미 청산(매도)된 슬롯은 되돌리지 않는다(매도 비가역). -
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.snapshot.req→trex.slots.req로 교체됨(BAT-035). trex.direct(topic exchange): 특정 인스턴스 지정 전달. routing key ={instance_key}.{channel_type}(channel_type ∈ {intent, slots, control}). 각 인스턴스 큐가 자신의 routing key 로 바인딩 →inst-1.intentpublish 는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_idscope 조회.
PortfolioItem Fields
portfolio_id :: str:: == asset_id (dcy-004 OQ-1)symbol :: strcash_balance :: floatasset_quantity :: float:: integer shares semanticallyactive_strategy_id :: str | Nonestrategy_params :: dict:: opaque pass-through (no core parsing)strategy_name :: str | None:: REQ-116 additive (BAT-055) — portfolio-hub 정본; 미제공 시 Nonemarkets :: list[str]:: REQ-116 additive (BAT-055) — portfolio-hub 정본; 미제공 시 []
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: LedgerPort → portfolio-hub trade write (BAT-006; portfolio-hub REQ-0009)
- Type: external-service (HTTP, portfolio-hub)
- Consumers / Producers:
LedgerWriter(소비) →PortfolioHubLedger(transport adapter) → portfolio-hub - Purpose: durable
LedgerTrade를POST /v1/portfolios/{portfolio_id}/ledger/trades요청으로 직렬화한다.
Venue Provenance Mapping
| Core field | HTTP request field | Contract |
|---|---|---|
LedgerTrade.venue |
execution_venue |
실제 체결 venue. KRX, NXT, null; SOR 금지 |
LedgerTrade.order_venue |
order_venue |
원래 주문 routing venue. KRX, NXT, SOR |
PortfolioHubLedger._payload()는 두 값을 그대로 전달하며order_venue에서execution_venue를 추론하지 않는다.- Core producer,
LedgerTradedomain model, durable outbox schema와 broker-routing 동작은 변경하지 않는다. 기존 outbox가 보존한 두 필드가 HTTP 경계에서 누락되지 않도록 하는 transport mapping이다. - Rollout 순서: nullable fields를 수용하는 portfolio-hub API/migration을 먼저 또는 동시에 배포한 뒤 이 adapter를 배포한다. 구형 Core가 두 필드를 보내지 않는 mixed-version 요청은 Hub에서
null로 수용한다.
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. 직전warningflag 는 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()returnsTrue(성공) /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
- transit_slot(slot_id, from_state, to_state, reason, actor, **fields) :: advisory lock 하 원자 상태 전이 (INACTIVE 진입 시 점유 초기화 + 자산 정체성 보존)
- sell(slot_id, actor, reason, limit_price=None) :: holding 슬롯 자율 매도(멱등)
- update_thresholds(slot_id, ...) :: SL/TP/forced_close 부분 갱신
- compute_slot_capacity(max_active_slots=None) :: instance 스코프 가용성 스냅샷 (BAT-030)
Outputs
- Slot :: 전이 후 슬롯 행 스냅샷
- SlotCapacity :: inactive/holding/pending/active/available 카운트 + total + scarcity_ratio
- slot_transition_log append :: 모든 전이의 append-only 감사 기록(부수 효과)
Error Contract
- SlotTransitionError :: 허용되지 않은 (from_state, to_state) 전이
- SlotStateMismatchError :: read-write 사이 상태 변동(레이스 감지)
- SlotNotFoundError :: 존재하지 않는 slot_id
Compatibility Rules
- 5 상태 / 13 전이 계약(REQ-057, dcy-021 holding→pending_buy 증액 포함)을 깨는 변경은 breaking 으로 취급한다.
- 상태 변경 API 는 transactional_advisory_lock(인스턴스 스코프 lock id) 안에서만 수행하고 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 컨텍스트 매니저
- list_slots(instance_key) / fetch_slot / update_slot :: daily_slot CRUD (instance 스코프)
- find_slot_by_portfolio / count_active / list_holding_slots / reconcile_universe :: 자산 중심 표면 (REQ-058/059/062)
- list_undispatched_pending / mark_dispatched / list_dispatched_pending / clear_dispatch :: 집행 큐 (instance 스코프)
- append_transition_log :: append-only 전이 로그
- count_slots_by_status(instance_key) :: 상태별 집계
Outputs
- Slot / SlotTransitionRecord :: 도메인 모델
- dict[SlotState, int] :: 상태별 카운트
- dict[str, int] :: reconcile 결과 {created, retired, tombstoned}
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어댑터 또는InMemoryLedgerstub - Purpose: 체결을 외부 회계/포지션 원장에 기록한다. orbit
post_ledger_trade패턴. - Stability: experimental (v1)
Inputs
- record_trade(trade: LedgerTrade) :: 단일 체결을 원장에 기록 (
execution_id멱등) →LedgerWriteOutcome반환
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
LedgerWriteOutcome(dcy-018/REQ-017) ::{result, cash_balance_after?, asset_quantity_after?}result = LedgerWriteResult.NEW:: 새 원장 엔트리 적재result = LedgerWriteResult.DUPLICATE_OK:: 같은 execution_id 가 이미 기록되어 있음(서버 멱등)cash_balance_after/asset_quantity_after:: portfolio-hub#8 ledger write 응답 root flat 권위값(option D).new(201)·duplicate(200) 에 존재, 응답 누락 시 None(write-back skip). 409/422 는 예외(아래)라 outcome 미반환.
- write-back(REQ-019/030):
LedgerWriter가new·duplicateoutcome 의*_after(None 아님)를SnapshotStore.update_cash_position(asset_id, cash, qty)로 반영(best-effort). 절대 권위값이라 duplicate 도 멱등. - 스냅샷 응답 추정 현금(REQ-029/OQ-4): write-back 전 공백 동안 자산에
cash_balance += 미정산 pending 합+cash_estimated: true플래그(라벨). write-back 도착 시 권위값으로 수렴(pending 제거).
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:: 어댑터 NEWduplicate_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(..., venue, order_venue, ...)—ON CONFLICT (correlation_id) DO NOTHING이 outbox dedup 게이트.order_venue는 원주문 KRX/NXT/SOR,venue는 broker fill에서 확인한 실제 KRX/NXT 체결 거래소다. Portfolio Hub wire에는 아직 보내지 않는다.
OrderRequest DTO (BAT-004 / REQ-011, BAT-056 / REQ-117)
포트-어댑터 경계에서 주문 요청을 전달하는 frozen dataclass.
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
slot_id |
str | (필수) | 발원 슬롯 (감사/상관 식별자) |
side |
str | (필수) | 'buy' | 'sell' |
symbol |
str | (필수) | 종목코드 (KIS PDNO) |
qty |
int | (필수) | 주문 수량 |
limit_price |
int | None | None | 지정가 (None = 시장가) |
order_type |
str | "00" |
KIS ORD_DVSN: 00=지정가, 01=시장가 |
venue |
str | "KRX" |
거래소 구분 (REQ-117 / BAT-056 additive): KRX=정규 KRX, NXT=대체거래소(연장세션), SOR=최선집행(현재 비권고 — OQ-1 잔여) |
Additive Notes (REQ-117)
venue필드는 BAT-056에서 additive 추가됐으며, 현재 계약에서 기존 orb/tsl을 포함한 미지정 호출은SOR로 정규화한다.OrderRequest는 AMQP wire 위를 직접 흐르지 않는다. buy/sell intent의 선택적venue누락은SOR이고, 명시한 KRX/NXT/SOR는 그대로 슬롯에 보존한다. 폐기된AUTO와 그 밖의 값은invalid_venue로 admission 전 거부한다.- KIS 어댑터는 KRX/NXT/SOR를 시간과 무관하게 보존해
EXCG_ID_DVSN_CD로 전송한다. OrderAck.venue는 원주문 KRX/NXT/SOR를broker_order_id와 함께 슬롯에 원자 저장하며 재시작·취소에 재사용한다. 실제 체결 거래소는FillStatus.execution_venue의 KRX/NXT로 별도 전달해 outboxvenue에 기록한다. SOR 또는 미지원 코드는 실제 거래소로 추론하지 않는다.
BrokerPort Protocol 메서드 (BAT-056 / REQ-118)
Balance-read failure and reconciliation boundary (BAT-057 / REQ-120/121)
get_balance()는 검증된list[BalanceItem]만 반환한다. KIS의 token/auth/authorization/transport/response/rejection 실패는 빈 목록으로 축소되지 않고BrokerBalanceError를 raise한다. 안정 code allow-list는token_issuance_failed,authentication_failed,authorization_failed,transport_failed,malformed_response,broker_rejected,ownership_mismatch다.sync와verifyCLI는 첫 balance read를 완료한 뒤 cache한 결과를 해당 invocation에 전달한다. 이 read 또는 broker credential resolution이 실패하면 exit code1이며,--jsonstderr는{ "error": {"code": "…", "message": "broker balance unavailable"} }형태의 안전한 값만 낸다. upstream payload, credential, account 값은 계약상 출력하지 않는다.reconcile_core_holdings()의 broker-only 결과는unmanaged_position(대상 instance의 managed slot 없음) 또는managed_non_holding_divergence(동일 instance의 non-holding managed slot 존재)다.find_managed_slot_by_symbol(instance_key, symbol)조회는 instance-scoped이며, 두 분류 모두 slot 생성, 자동 주문·청산을 하지 않는다.managed_non_holding_divergence는 기존 reconcile warning lifecycle을 유지한다. 반면unmanaged_position은(instance_key, symbol)keyed durable state로 추적되며quantity,first_seen_at,last_seen_at,quantity_changed_at,resolved_at, lifecycle state와 latest operator evidence/disposition을 저장한다.unmanaged_position의 absence resolution은 explicit authoritative full-balance snapshot reader에서만 허용된다. Ownership/projected broker 또는 marker가 없는 balance reader는 detect/update 입력일 수 있어도 resolved authority는 아니다.- Operator surface는
trex-core unmanaged list,trex-core unmanaged audit <symbol>,trex-core unmanaged record <symbol> <action> --evidence-ref ...이며 audit actor는 CLI의 trusted local identity에서 결정된다. 임의 caller-supplied actor string은 current contract가 아니다. - 이 변경은 AMQP 또는 portfolio-hub external wire contract는 바꾸지 않지만, internal PostgreSQL persistence에는 unmanaged-position state/audit schema를 추가한다.
Account snapshot recovery (dcy-041 / GitHub #45)
- 기능은
TREX_ACCOUNT_SNAPSHOT_ENABLED=false가 기본인 KIS 전용 선택 기능이다. - KIS 보유조회와 현금주문 가능금액 조회(
inquire-psbl-order.output.nrcvb_buy_amt)를 기본 300초 간격의 단일 계정 소유자가 갱신한다. 두 값은 독립 캡처 시각을 갖는다. - 신규매수는 미만료 현금 스냅샷과 기존
pending_buy지정가 예약금액을 합산해 검사한다. 수량·지정가가 없거나 예약금액을 계산할 수 없으면 매수를 차단한다. KIS 주문 응답이 최종 현금 한도 권위이며 거부된 주문을 자동 재전송하지 않는다. - 현금 검사와
PENDING_BUY전이는 동일 instance advisory lock 안에서 수행되어 같은 instance의 동시 처리자가 아직 커밋되지 않은 예약을 함께 통과하지 못한다. pending_sell수량은 매 tick 실시간 보유조회를 먼저 시도한다. 오직transport_failed이고 기본 3600초 안의 동일계정 스냅샷이 있을 때만 대체한다.- 인증·인가·응답형식·브로커거부·소유권 불일치·계정 해시 변경·만료는 캐시로 우회하지 않는다.
sync,verify, 일일 스냅샷 스케줄러와 재시작 원장대조는 기존처럼 실시간 조회만 사용한다.- 소유권 래퍼가 설정된 혼합계정은
broker total = manual protected + strategy owned를 유지하며 전략 소유분만 보유수량 스냅샷에 들어간다.
cancel_order
cancel_order(broker_order_id: str, *, side: str, symbol: str, qty: int, venue: str = "KRX") -> CancelOutcome
venue: 원주문 venue. 취소 시 원주문과 동일한EXCG_ID_DVSN_CD를 사용해야 한다. 기본값"KRX"로 기존 호출자 하위 호환.CancelOutcome.state:requested(요청 접수),rejected(명시적 거부),unknown(응답유실·전송 실패).requested도 최종 취소가 아니며poll_fill()의cancelled|filled만 주문의 최종 상태를 확정한다.- Core는 외부 취소 호출 전에
unknown과 시도 횟수를 원자적으로 영속화하고 응답 뒤 실제 즉시 결과로 교체한다. 따라서 요청과 저장 사이에 프로세스가 종료돼도 재시작 시 먼저 체결조회를 수행한다. requested는 재시도하지 않는다.rejected|unknown은 후속poll_fill()이 원주문을 open으로 확인하고 bounded backoff·최대 시도 조건을 만족한 경우에만 재시도한다. row 미가시성·missing·accepted만으로 blind retry하지 않는다. 운영자 반복 요청은 기존cancel_already_recorded경계를 유지한다.
get_current_price
get_current_price(symbol: str, market: str = "J") -> Optional[Decimal]
market: KISFID_COND_MRKT_DIV_CODE. 기본값"J"(KRX 주식) → 기존 safety-sell 경로 무영향."UN"지정 시 해외/NXT 연장세션 시세 조회 가능.
KIS 어댑터 EXCG 명시화 (REQ-118)
submit_order: body에EXCG_ID_DVSN_CD: req.venue항상 명시 전송 (이전: 미전송·서버 기본값 의존 → 명시화로 견고성 향상). 실측(2026-07-06): NXT(EXCG=NXT) 주문 rt_cd=0 확인.- venue 누락은
SOR로 제출하며, 명시 KRX/NXT/SOR는 그대로 제출한다. core는 wall-clock으로 venue를 선택하지 않는다. cancel_order: body에EXCG_ID_DVSN_CD: venue명시 전송. 실측(2026-07-06): NXT 취소 rt_cd=0 확인.poll_fill: KIS 신형 일별주문체결조회 실전TTTC0081R, 모의VTTC0081R을 사용하고EXCG_ID_DVSN_CD=ALL을 전달한다. 일반 주문번호 조회와 결과불명 주문 탐색이 같은 계약으로 KRX·NXT 체결을 확인한다. 일반 주문은 영속화된order_date(YYYYMMDD)를 조회일로 사용하며, row가 없으면 bounded 제출 직후 visibility grace 이후unknown으로 fail-closed 한다.order_submitted_at/order_date를 알 수 없는 legacy pending row는 mutableupdated_at로 추정하지 않고 CRITICAL alarm과 pending을 유지한다.
FillStatus 확장 (BAT-006 / REQ-018)
state, filled_qty, avg_price, raw외에 다음 필드를 운반한다.execution_id :: str?:: 결정적 합성(브로커별 규칙). filled 상태에서 필수.fee :: float:: BrokerPortcalculate_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→inactive+ 원장 1건.cancelled(체결 0 외부 취소) ::is_terminal_reject→ 롤백(pending_sell→holding/pending_buy→inactive),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). 체결수량이 0이고rjct_qty >= ord_qty > 0이면 브로커 접수 뒤 거래소 전량거부로 해석해rejected로 종료한다.rjct_qty일부만으로는 미체결 잔여 상태를 추정하지 않는다.
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.dlqexchange/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 마이그레이션 시 큐 재선언 절차 필요.
Interface: trex.slots.strategy_state.set RPC (REQ-0009)
- Type: external-service (strategy daemon ↔ core write RPC)
- Consumers / Producers: 전략 데몬(produce) ↔ core 핸들러(consume). per-instance 큐(
{base}.{instance_key}), direct reply-to, JSON. - Purpose: 슬롯별 opaque strategy-instance state entry를 version CAS로 write. read는
trex.slots.req에 piggyback(별도 RPC 아님).
Inputs
{slot_id, strategy_instance_id, expected_version:int, data:str}.strategy_instance_id는 required non-empty string이며correlation_id는 저장 멱등성 key가 아니다.data는 opaque UTF-8(core 무해석), byte size cap(기본 4KB ceiling, 운영 권장 1~2KB).
Outputs / Outcomes
- 성공:
{ok:true, slot_id, strategy_instance_id, version:<new>}. - 실패 reason 전체 집합:
version_conflict(+version) /slot_inactive/state_too_large/bad_request:<detail>. 소비자는 미인식 reason에 fail-closed.
Compatibility Rules
- slot strategy-state는 occupancy와 독립된 slot-scoped
data[strategy_instance_id] = {version,data,updated_at}map이다. close/reopen은 entry를 삭제하거나 version을 초기화하지 않는다. Entry에는strategy_id나occupancy_seqmetadata가 없으며, Slot 자체의occupancy_seqsemantics는 별도로 유지된다.
Shared Constraints
- 도메인은 외부 의존(브로커, DB 드라이버 등)을 직접 호출하지 않는다(순수 도메인 경계).
- 모든 상태 변경은 단일 advisory lock(SLOT_ADVISORY_LOCK_ID)으로 직렬화한다.
Current Gaps / Planned Changes
- 영속화 구현체(PostgreSQL)와 그에 딸린 SQL 스키마/마이그레이션은 이번 이관 범위 밖이며 향후 도입 예정.
- 외부 HTTP API / CLI 표면 없음(trex-orbit 의
trex slotCLI 미이식). - 패키지 네임스페이스 결정(plan.md OQ-1)에 따라 import 경로 계약이 확정된다.
Interface: trex.alerts (알람 채널, dcy-033)
- Type: amqp-messaging (topic exchange)
- Consumers / Producers: 모든 모듈(trex-core/orb/tsl/kdh)(발행자) ↔ trex-notifier(단일 소비자)
- Purpose: 제품군 CRITICAL 알람 발신 전용 채널.
trex.events와 독립 분리. best-effort 비차단. - Stability: experimental (BAT-049)
Exchange 토폴로지
| 종류 | 이름 | 타입/속성 | 용도 |
|---|---|---|---|
| 익스체인지 | trex.alerts |
topic, durable=True | 알람 envelope 발행 전용 독립 채널 |
- 발행자: 모든 모듈(trex-core, trex-orb-v1, trex-tsl-v1, kis-data-hub). 각 모듈이 자신의 RabbitMQ 연결에서 직접 발행.
- 소비자: trex-notifier 단독. 자신의 큐를
alarm.#(또는 severity별alarm.critical.#) 패턴으로 바인딩. 큐 선언·바인딩은 trex-notifier 소유. - 발행 속성:
mandatory=False— notifier 큐 미연결(unroutable) 시 silent drop. core 발행 경로 비차단. - 기존 exchange 불변:
trex.events(topic),trex.events.dlq(fanout),trex.direct(topic),trex.broadcast(fanout) 등 기존 AMQP 엔티티는 이 채널 추가로 변경되지 않는다(가산·격리 원칙).
Routing Key 규약
alarm.{severity}.{source}
severity∈critical|warn|info(소문자 리터럴)source= 발신 모듈 식별자 (예:trex-core,trex-orb-v1,trex-tsl-v1,kis-data-hub)- 예시:
alarm.critical.trex-core,alarm.warn.trex-tsl-v1
Alarm Envelope JSON 스키마 (schema_version="1")
{
"source": "trex-core",
"severity": "critical",
"code": "kill_switch_fired",
"title": "긴급 청산 발동: global scope, liquidated=3",
"fields": {"scope": "global", "liquidated": 3},
"occurred_at": "2026-06-21T09:30:00+09:00",
"schema_version": "1"
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
source |
str | 필수 | 발신 모듈 식별자. 소비자(trex-notifier)의 연쇄 분석 기준. |
severity |
"critical" | "warn" | "info" |
필수 | 등급 (소문자 리터럴 3개). |
code |
str | 필수 | 알람 식별 코드. ALARM-INVENTORY.md의 code 키 채택. |
title |
str | 필수 | 사람이 읽는 1행 요약. 자격증명·내부 예외 본문 제외. |
fields |
dict[str, any] | 선택 | 구조화 컨텍스트. opaque — core/notifier 내용 강제 파싱 없음. 빈 dict {} 허용. |
occurred_at |
ISO-8601 datetime | 필수 | 발생 시점. 권장: KST+09:00 offset-aware. UTC도 수용. |
schema_version |
str | 필수 | 현재 "1". 소비자는 미인식 버전 무시·WARN. |
trex-core CRITICAL code 목록 (schema_version="1")
trex-core가 source="trex-core", severity="critical"로 발행하는 초기 배선 code:
| code | 발생 지점 | 조건 |
|---|---|---|
fill_price_unresolvable |
execution.py |
체결가·지정가 모두 미해결 → 슬롯 PENDING 고착 |
sell_skipped_no_balance |
execution.py |
매도 dispatch skip (브로커 잔고 0) |
core_holding_broker_flat |
reconciliation.py |
슬롯 보유인데 브로커 잔고 0 (정산 불일치) |
ledger_delivery_failed |
ledger_outbox.py |
원장 영구 실패/dead-letter/409 충돌 |
kill_switch_fired |
killswitch.py |
긴급 청산 발동 |
publish_unavailable_after_reconnect |
adapters/messaging/rabbitmq.py |
재연결 후에도 이벤트 발행 불가 |
dlq_publish_failed |
adapters/messaging/rabbitmq.py |
DLQ 발행 실패 |
portfolio_hub_unavailable |
portfolio.py / snapshot_scheduler.py |
포트폴리오/현금 정보 부실화 |
schema_verification_failed |
reset.py |
리셋 후 schema 검증 실패 |
migration_failed |
reset.py |
마이그레이션 실패 |
Update Trigger (알람 envelope 스키마)
- 알람 envelope 스키마를 변경(필드 추가 포함)하면 이 섹션을 갱신하고
schema_version을 올리거나 additive 변경 여부를 명시한다. - breaking 변경은
schema_version주 버전 증가 필수. - additive 변경(신규 선택 필드 추가)은
schema_version유지 가능하되 이 섹션에 변경 이력 기술. - 다른 모듈(orb/tsl/kdh)은 이 계약 섹션을 단일 출처로 참조한다.
trex_coreimport 없이 envelope 구성에 필요한 모든 정보가 이 섹션에 포함된다.
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.
- 알람 envelope 스키마 변경 시
Interface: trex.alerts섹션 Update Trigger 참조.
Change Log
- 2026-06-03 :: bootstrap-baseline 으로 초기 인터페이스 계약 baseline 생성(SlotService + SlotRepository Protocol).
- 2026-06-03 :: BAT-002 — HTTP
/v1API 계약(데몬, 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
/v1API 인터페이스 제거(adapters/api + fastapi/uvicorn 의존 정리), RabbitMQ messaging 계약 추가(스냅샷 RPCtrex.snapshot.req, intent RPCtrex.intent.req, 이벤트 topictrex.events, 전략별 durable queue + TTL/max-length, DLQ, publisher confirms, ack-after-commit),MessagingTransportProtocol +TransportHealth노출 (REQ-021~026). - 2026-06-06 :: BAT-008 —
trex.snapshot.reqenvelope 계약 정의(assets[]/view/available_slots/degraded/reason/message, no price field),trex.pause.req제어 RPC 추가,PortfolioHubReadPort/SnapshotStore/PauseStore/SnapshotScheduler인터페이스,BrokerPort.get_balance()확장,trex-coreCLI 추가 (REQ-027~033; REQ-030 옵션 D 는 portfolio-hub#8 배포 후). - 2026-06-10 :: BAT-030 (dcy-020 마감) — buy intent 계약 키
slot_id정본(+portfolio_iddual-accept,trading_date제거), 거부 어휘(unknown_slot/already_active/capacity_full/slot_id_or_portfolio_required),slots[]5-state +universe_detached(trading_date 키 제거), 이벤트 payload trading_date 제거,available_slots=admission headroom, SlotService/SlotRepository 자산 중심 계약, 매도 체결pending_sell→inactive(REQ-056~065). - 2026-06-15 :: BAT-043 (issue #27, REQ-007 CHG-20260615-01) — rejected SellIntent 응답에 additive
retryable: bool을 도입했으나, GitHub #60 승인 계약으로 폐기됐다. 현재 strategy RPC는 retryability·broker attempt·KIS failure를 전달하지 않는다. - 2026-06-15 :: BAT-042 (dcy-028) — slot strategy-state 계약: 신규 write RPC
trex.slots.strategy_state.set(occupancy_seq+version CAS, reject reason 집합occupancy_changed/version_conflict/slot_inactive/state_too_large/bad_request),trex.slots.req행에 additiveoccupancy_seq+strategy_state맵(REQ-073 AC-7/REQ-074 AC-6, opaque UTF-8+size cap, strategy_* reasoning 필드와 구분). close(→INACTIVE) 시 state 자동 삭제 (REQ-079~082). - 2026-06-11 :: BAT-035 (dcy-025) —
trex.snapshot.req→trex.slots.reqlockstep 교체: 요청{filter, spy}, 응답{slots[], degraded, reason, message}. 슬롯 row 에 hubasset_quantity/cash_balance+warned/cash_estimated/watchdog/paused병합(REQ-073/074).trex.snapshot.reqenvelope(assets[]/available_slots/view enum) 제거(REQ-028 Deprecated). strategy 필드 제거(REQ-074 AC-5). CLIuniverse/slots→trex.slots.req(spy=true).trex.directchannel_typesnapshot→slots. - 2026-06-11 :: BAT-036 (dcy-024) —
safety_sell_enabled: bool=Trueper-instance TOML 플래그 추가(REQ-048 CHG-01).false시trex-safety-sellworker 미기동 + WARNING 로그(REQ-005 CHG-01). - 2026-06-21 :: BAT-049 (dcy-033) — 알람 채널
trex.alerts(topic, durable) 신규 추가. 알람 envelope 표준 JSON 스키마(7필드: source/severity/code/title/fields/occurred_at/schema_version, schema_version="1"), routing keyalarm.{severity}.{source}, 발행자(모든 모듈)/소비자(trex-notifier), mandatory=False best-effort 정책. trex-core CRITICAL code 10종 초기 배선. 기존 wire 계약 불변(가산·격리). (REQ-098~101) - 2026-06-10 :: BAT-031 (dcy-021 포지션 사이징) — 매수 intent 가 holding 슬롯 대상이면 증액(qty+price 추가 매수; M 캡 미적용); 매도 intent 에
qty(부분 매도, 선택) 추가 — 지정 시min(qty, 브로커잔고)부분 매도(잔량 holding), 미지정 시 전량(→inactive). 상태머신 13 전이(holding→pending_buy). avg_price 증액 가중평균·부분 매도 cost basis 유지. (REQ-066/057/061/062) - 2026-07-04 :: BAT-055 (dcy-001/002 v3 FR-8, REQ-116) —
trex.slots.reqadditive 확장: 요청에 선택적strategy: str필터 추가(exact-match,filterAND 조합,available_slots불변 — REQ-073 AC-6); 응답 슬롯 행에strategy_name(null)/strategy_params({})/markets([]) 3필드 추가(PortfolioItem/hub 정본, safe default). 기존 소비자(orb·tsl) backward compatible(unknown 필드 무시). REQ-074 AC-5 additive 재도입(dcy-025 OQ-4 후속 확정). PortfolioItem에strategy_name/markets필드 추가 + hub_read.py 파싱 확장. - 2026-07-29 :: BAT-063 (REQ-118 CHG-20260729-01) —
poll_fill과 결과불명 주문 탐색을 KIS 신형 일별주문체결조회 (TTTC0081R/VTTC0081R,EXCG_ID_DVSN_CD=ALL)로 단일화했다. - 2026-07-06 :: BAT-056 (dcy-002 FR-6, REQ-117/118) —
OrderRequest.venue: str = "KRX"additive 추가(기존 위치 인수 순서 불변 — AMQP wire 계약 영향 없음, 브로커 어댑터 계층 DTO까지만). KIS 어댑터: submit_order/cancel_order body에EXCG_ID_DVSN_CD항상 명시 전송(기존 미전송 → KRX 명시화 견고성 향상), cancel_order/get_current_price 시그니처 venue/market 파라미터 추가(기본값 KRX/"J" — 기존 호출자 무영향). paper broker 시그니처 정합(venue/market 무시). 실측 근거: 2026-07-06 NXT 주문/취소 rt_cd=0 확인. - 2026-08-24 :: issue #62 —
trex.slots.reqholding rowqty를 broker holdings snapshot 정본으로 전환.qty_source/broker_quantity_available/broker_quantity_captured_at/slot_quantity를 추가하고, broker qty unavailable 또는 stale이면 fail-closed(qty=null, no core/hub fallback)로 응답.
REQ-123 Contract Addendum: cost-basis canonical parsing & sync diagnostics
trex-core REQ-123 적용 시, portfolio-hub cost-basis contract는 다음을 준수한다.
GET /v1/portfolios/{portfolio_id}/holdings/cost-basis응답의cost_basis_per_unit(canonical)을 먼저 확인한다.- canonical 값이 유효한 양수 수치면 이를
avg_priceseed 후보로 사용한다. - canonical key가 존재하지만 null / non-numeric / non-positive / 범위 외이면 legacy fallback(
avg_cost→wac→cost_basis)은 시도하지 않는다. - canonical이 부재한 경우에만 approved legacy 순서로 fallback을 검증한다.
- canonical/legacy parsing 실패는 실패 원인을 reason 코드(
cost_basis_reason)로 분리해 운영 진단으로 노출한다. sync/sync --dry-run결과에는seeded_avg_price(bool),cost_basis_source(canonical/legacy/none),cost_basis_reason필드가 들어간다.cost_basis_reason의 공개 값은missing,invalid,http_status_<3-digit>,transport,invalid_json,invalid_payload,not_configured,diagnostic_exception,diagnostic_source_invalid또는 유효한 값이 seed되었을 때의null로 한정한다.http_status_<3-digit>만 동적 형식이다. provider field name, parser detail, 임의 오류 텍스트는 공개cost_basis_reason계약 값이 아니다.- 비밀(credential/token/계좌정보)은 로그/출력에 노출하지 않는다.