BAT-032 (dcy-022): timeout_sec/slippage_pct 실행 정책, 미체결 자동 취소, 보호 지정가, `trex-core cancel` 백스톱. 마이그레이션 012. BAT-033: `trex-core enter --qty` 필수 검증 — 미지정 시 RPC 전 즉시 오류. BAT-034 (dcy-023): allocated_capital 죽은 필드 전 경로 제거 (domain·intent· persistence·snapshot·CLI). 마이그레이션 013(비가역 DROP). 외부 발신자 unknown key 무시 수신 의미론 적용. REL-006 confirmed: BAT-026~034 통합 릴리즈, pre-release smoke 증거 포함. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
39 KiB
Trex Core Runtime Flows
- Baseline Mode: mixed
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-10 (BAT-031 — dcy-021 포지션 사이징)
- 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)trex-core-reset:: DB 를 자산 중심 스키마로 wipe + 재초기화 (REQ-064, 운영자 실행 — 데이터 보존 cutover 대체; 파괴적·DB 전역, dry-run 기본)- 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 :: 슬롯 생명주기 도메인 서비스 (transit / sell / thresholds / capacity — 자산 중심, BAT-030)
- SlotRepository (Protocol) :: 저장소 구현체 주입 경계
- MessagingTransport (Protocol) :: 메시지 transport 추상화 (RabbitMQ / InMemory stub)
Flow: 데몬 lifecycle (BAT-002, BAT-007)
trex-core-daemon --config <path>.toml기동 →DaemonConfig.from_config(toml)=from_env()(TREX_POSTGRESQL_DSN/TREX_RABBITMQ_URL등) + TOML(instance_key,portfolio_hub_token,trading_timezone,max_active_slots) 병합.--config필수(없으면 오류 종료, BAT-013/REQ-048).instance_key로 큐명·라우팅·DB 행 스코프 분리(REQ-056).- setup:
apply_migrations(dsn)(멱등; 010 inactive CHECK·011 컬럼 drop 포함) →execution.reconcile()(REQ-014) → 이벤트 publisher 의declare_topology(). (일일 preallocation 은 BAT-030 에서 폐기 — 슬롯 생성은 universe reconcile 이 담당.) - start: 7개 백그라운드 스레드 동시 기동
trex-execution:: 브로커 dispatch/poll 루프 (REQ-013/014; 매도 수량 브로커 정본 REQ-062)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 + universe 슬롯 reconcile(REQ-058) + holding 불일치 reconcile(REQ-062)trex-safety-sell:: 자율 안전 매도 트리거 루프 (forced_close / stop-loss·take-profit / 장 마감, REQ-037/038/039)
- shutdown:
SIGINT/SIGTERM→stop.set()→ 모든 스레드 join + 모든 transportclose(). HTTP 표면 0, 별도 포트 바인딩 없음.
Flow: 스냅샷 RPC (RabbitMQ trex.snapshot.req, BAT-007)
- 전략 모듈이 RPC 요청을
trex.snapshot.req큐로 publish — payload 에view/strategy_id/trading_date, properties 에correlation_id+reply_to(direct reply-to). trex-snapshot-rpc스레드가 메시지 수신 →make_snapshot_rpc_handler(slot_repo, slot_service)로 슬롯 목록 + 가용성 계산.- 응답을
reply_to큐로 publish (correlation_id일치) → manual ack. - read-only 이므로 dedup 불요; 재요청 시 동일 응답을 다시 생성한다.
Flow: 매수/매도 intent admission RPC (RabbitMQ trex.intent.req, BAT-007)
- 전략 모듈이 RPC 요청을
trex.intent.req큐로 publish — payload{intent_type, intent_id, ...}, propertiescorrelation_id= intent 멱등키,reply_to. trex-intent-rpc스레드(단일, prefetch=1)가 메시지 수신 →IntentService.submit_buy/sell호출.intent_log.try_insert(intent_id, ...)가 dedup 게이트 — 중복 메시지는 기존 decision 재반환(슬롯 무전이).- 신규면 admission(자산 중심, REQ-059/061): slot_id 정본 키(미스 시
unknown_slot) 또는 portfolio_id dual-accept(미스 시unknown_portfolio; 둘 다 없으면slot_id_or_portfolio_required) 로 대상 영구 자산 슬롯 resolve → 이미 active(holding/pending) 면already_active→ symbol resolve(슬롯portfolio_id기준 universe) →count_active >= max_active_slots(M)면capacity_full(FCFS·무치환) →SlotService.transit_slot(inactive→pending_buy)→ Postgres durable commit. (buy payload 에 client symbol·trading_date 없음) - admission 응답(
status, slot_id?, reason?)을reply_to큐로 publish → manual ack. - ack-after-commit 시퀀스: durable commit 이 ack 보다 먼저이므로 ack 전 크래시 시 재전달돼도 (3) 의 dedup 으로 재실행되지 않는다.
- 체결/종료는 응답이 아니라 후속 이벤트(아래)로 전달 — admission 동기 + fill 비동기 2단계 시맨틱.
Flow: 슬롯 이벤트 발행/구독 (RabbitMQ trex.events, BAT-007)
- 체결: execution 루프가
pending_buy→holding(또는pending_sell→closed)을 commit + ledger outbox enqueue (REQ-018/019). - 같은 스레드에서
event_publisher.publish_event(rk, payload)호출.- routing key:
{event_type}.{strategy_id}.{symbol}(예:slot_filled_buy.A.005930). - payload: 현재 상태 +
sequence_at(슬롯updated_atiso) 운반 → 수신자는 last-wins 적용 가능 (순서 보증 불요). - publisher confirms 활성, 발행 실패는 로그 + DLQ 라우팅(
trex.events.dlq). publish 실패가 슬롯 전이를 롤백하지 않는다.
- routing key:
- 전략별 durable queue(
trex.events.strategy.{strategy_id})는*.{strategy_id}.#패턴으로 바인딩 → broker 단 필터로 자기 자산 이벤트만 수신. - 전략 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 으로 교체하고, 같은 패스에서 정합 검증을 수행한다.
- boot:
SnapshotScheduler.run_once()한 번 호출 → 첫 universe 로딩(또는 degraded 마킹). - tick(
TREX_SNAPSHOT_TICK_SEC, 기본 60s): 현재 KST 시각이TREX_SNAPSHOT_SCHEDULE_KST(기본 07:30) 이상이고 오늘 아직 안 돌았으면 fire. - fire 단계:
PortfolioHubReadPort.whoami()로 daemon_id 도출(1회 캐시) →get_portfolios(daemon_id)로 universe 페치 →SnapshotStore.swap(items)atomic 교체 →reconcile_positions(store, broker)정합 검증. - 실패 분기:
PortfolioHubUnavailableError→degraded = portfolio_hub_unavailable; 응답 파싱 실패 →snapshot_fetch_failed. 둘 다 다음 RPC 응답 envelope 에 즉시 노출(REQ-028 AC-2).
Flow: universe 슬롯 reconcile (BAT-028/030 / REQ-058 — 일일 preallocation 대체)
trex-snapshot-scheduler 가 universe fetch 직후 같은 패스에서 영구 자산 슬롯 집합을 universe 에 정합시킨다 (일일 preallocation/dcy-014 는 BAT-030 에서 폐기).
- 신규 자산(portfolio_id 에 슬롯 없음) →
inactive영구 슬롯 생성(멱등 —ON CONFLICT (instance_key, portfolio_id)partial unique). - universe 이탈 + inactive → soft-retire(
retired_atset; 행 보존, 운영 뷰universe_detached). 이탈 + holding/pending → tombstone(그대로 유지 — safety-sell 경로 보존). - 가드: universe 빈 응답이면 reconcile skip(degrade 가드); 직전 대비 ≥50% 축소면 retire 보류(
allow_retire=False, 대량 소실 가드 — dcy-020 OQ-2 D3). - 이어서
reconcile_core_holdings(REQ-062): 브로커 잔고 ground truth 로 core holding 검증 — core holding/브로커 0 → sloterror격리+WARN, 수량 불일치 → WARN(상태 불변), 브로커 보유/슬롯 없음 → WARN(unmanaged). 자동 청산 없음.
Flow: 포지션 정합 검증 (BAT-008 / REQ-031)
reconcile_positions 는 portfolio-hub 보유 수량을 symbol 별로 합산해 broker get_balance() 결과와 비교한다.
BrokerPort.get_balance() → list[BalanceItem(symbol, hldg_qty)]조회. KIS 어댑터는TTTC8434R/VTTC8434Rinquire-balance, paper 는 결정론 fill aggregate. 0수량 row 는 양쪽 모두 drop(D-2 leftover 처리, Decision 7).- 자산을
symbol로 그룹화해hub_total = sum(asset_quantity)계산 → broker 의 같은 symbolhldg_qty와 비교. - 불일치: 해당 symbol 의 모든 portfolio_id 에
SnapshotStore.set_warning(pid, True)→ 다음 envelope 부터 응답에서 마스킹. 첫 raise 시 한 번 로그. - 일치: 이전에
warning이었던 자산은 자동 해제 → 다음 envelope 부터 다시 포함. resume 과 달리 자동·임시 overlay(영속 X).
Flow: pause/resume 제어 (BAT-008 / REQ-032)
운영자가 자산을 매매에서 제외/복귀시키는 두 경로(CLI + RPC)는 동일한 snapshot_pause Postgres 테이블을 가리킨다.
- CLI:
trex-core pause <portfolio_id>/trex-core resume <portfolio_id>→PostgreSQLPauseStore.pause/resume호출(portfolio_idPK upsert/delete). - 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확장) - 응답 산출:
build_snapshot_envelope가pause_store.list_paused()를 매 요청마다 읽음 →mask(pending | warning | pause)합성. resume 후 다음 요청부터 즉시 복귀(별도 reload 불필요). - 영속: daemon 재시작 + 일일 refresh 후에도
snapshot_pauserow 유지(universe swap 은 pause 를 만지지 않음).
Flow: 운영자 CLI (BAT-020 / dcy-016, REQ-032)
운영자 CLI(trex-core)는 조회·매매를 데몬 RabbitMQ RPC 로 통일해 단일 writer admission 과 불변식을 보존한다(DB 직접 쓰기 없음). pause/resume/paused 만 기존 direct-DB 유지(OQ-3).
- 인스턴스 라우팅:
--config <toml>(canonical, REQ-048instance_key) 또는--instance <key>(단축·override), 미지정 시default. 큐명은_instance_queue규칙({base}또는{base}.{instance}). 모든 출력에 대상 인스턴스 표시(R-2). universe/slots:trex.snapshot.reqRPC(actor="operator"/touch_watchdog=false) → 응답 envelope 의 assets 출력. 워치독 억제로 운영자 조회가 전략 생존 신호로 오인되지 않는다(REQ-034, R-4).slots --status holding→view=held.enter <portfolio_id>/exit <slot_id>:trex.intent.reqbuy/sell RPC. buy 는 portfolio_id 필수(symbol 서버 resolve, dcy-011), sell 은 slot_id + optional price(시장가 기본, dcy-012). admission 응답(status/slot_id/reason)을 표시, rejected 면 비0 종료.- enter/exit 안전장치(OQ-2): 확인 프롬프트 기본 ON(대상 인스턴스·broker·요약, 기본 No) →
--yes(paper 생략) → live 는--yes --live이중 ack(TREX_KIS_ALLOW_LIVE배포 게이트 위 액션 게이트) →--dry-run미제출 미리보기.--json비대화형은 enter/exit 에--yes필수(미지정 시 자동 제출 없이 에러). - 실패 표시: RPC 타임아웃·데몬 미가동·라우팅 오류는 명확한 에러(비0 종료)로 표시(NFR-4). RPC 통일이라 데몬·RabbitMQ 미가동 시 조회 불가는 의도된 트레이드오프(R-3).
Flow: 스냅샷 RPC 응답 산출 (BAT-008 / REQ-028, REQ-029)
trex.snapshot.req 메시지 처리.
- 요청 payload 디코드:
{view ∈ {all, buyable, held}, strategy_key?, trading_date?}. slot_service.compute_slot_capacity(max_active_slots)→available_slots = available_count(= M 설정 시max(0, M − active), 미설정 시 inactive 수; REQ-059).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동봉.
- base 집합: ALL =
- envelope:
{assets[], view, available_slots, degraded, reason, message}—assets는 항상 배열, null 금지. 항목 필드는symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params, price 필드는 없음. - 추정 현금 오버레이(dcy-018/REQ-029, OQ-4): 미정산(pending) ledger outbox 행이 있는 자산은
cash_balance += pending cash delta(매수 −, 매도 +) +cash_estimated:true라벨로 노출 → option D write-back 도착 시 권위값으로 수렴(pending 제거). best-effort(실패 시 응답 불변). 수량은 슬롯 오버레이(REQ-029)가 정확값이라 추정 불요.
Flow: per-slot 워치독 갱신 (BAT-010/011 / REQ-034, REQ-040)
전략 모듈의 생존 신호를 추적해 자율 안전 매도가 정상 운용 슬롯에 개입하지 않게 한다.
trex-snapshot-rpc가trex.snapshot.req를 처리할 때 응답에 포함된 모든holding슬롯에 대해WatchdogStore.update(slot_id)(last_seen_at = now)를 호출한다. 전략 모듈이 스냅샷을 폴링하는 한 그 슬롯은 "활성"으로 판정된다.update_thresholdsintent(REQ-040)도 수신 시 해당 슬롯last_seen_at을 리셋한다(임계값 갱신 = 생존 신호).WatchdogStore.is_expired(slot_id, TREX_WATCHDOG_TIMEOUT)가 True 이고 grace period(TREX_WATCHDOG_GRACE_PERIOD, 데몬 부팅 직후) 밖이면 그 슬롯은 워치독 만료 → forced_close/가격 트리거 대상이 된다.- in-memory store 라 데몬 재시작 시 초기화되며 grace period 가 자동 재시작된다(부팅 직후 오탐 방지).
- 가시화(dcy-017/REQ-034): 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태
watchdog: [{slot_id, portfolio_id, symbol, expired, last_seen}]를 read-only 로 동봉한다(best-effort — 실패 시 응답 불변, NFR-1).trex-core slots가expired=true슬롯을 WATCHDOG-OWNED(전략 이탈) 로 표시한다. 운영자 조회는actor="operator"/touch_watchdog=false(dcy-016)라 이 노출이 워치독을 리셋하지 않는다(비침습). Redis projection·push 알림은 미채택(OQ-1).
Flow: forced_close_at 만기 자동 매도 (BAT-011 / REQ-037)
trex-safety-sell worker 가 TREX_FORCED_CLOSE_CHECK_INTERVAL 주기로 수행.
- 오늘
holding슬롯 중 워치독 만료(grace 밖)이고forced_close_at <= now(UTC)인 슬롯을 선별. - 대상 슬롯에
SlotService.sell(actor="trex-safety-sell", reason="forced_close_at")호출 →holding→pending_sell(REQ-036). 이후 체결은 집행 루프가 처리. - 워치독 활성 슬롯·
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 주기로 수행.
holding+ 워치독 만료 + (stop_loss_price또는take_profit_price설정) 슬롯만 대상(워치독 활성 슬롯은 KIS 호출조차 안 함).BrokerPort.get_current_price(symbol)→ KIS 주식현재가 시세inquire-price(trFHKST01010100, 실전/모의 공용, BAT-011 CHG-04 정정)의stck_prpr. paper 는 프로그램된 가격(미설정 시 None).현재가 <= stop_loss_price→ 손절(reason="stop_loss"),현재가 >= take_profit_price→ 익절(reason="take_profit")으로SlotService.sell(). 가격 미수신(None) 시 skip(오탐 방지, 다음 tick 재시도).- 쿼터 가드: 시간당 호출 상한
N × (3600 / interval)추정이 임계 초과 시 경고 로그(REQ-038 AC-8).
Flow: 장 마감 강제 청산 (BAT-011 / REQ-039)
TREX_MARKET_CLOSE_LIQUIDATION_TIME(HH:MM KST, optional) 설정 시에만 동작.
trex-safety-sellworker 가 매 tick 현재 KST 시각을 평가 → 설정 시각 도달 + 같은 거래일 미발동(last_liquidation_date가드)이면 발동.- 발동 시 워치독·grace 무관 모든
holding슬롯에SlotService.sell(reason="market_close_liquidation"). 이미pending_sell은 idempotent. - 미설정이면 동작하지 않음. 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회 보호).
run_oncetick 시작 시_holding()에 없는 슬롯의 쿨다운 상태(_last_attempt/_attempt_count)를 정리(FR-4).- 트리거가
_safe_sell호출 직전, 슬롯이now - last_attempt_at < TREX_SAFETY_SELL_MIN_RETRY_INTERVAL(기본 600s)이면 건너뜀(stop-loss/take-profit 은 KIS 시세 poll 도 skip). - 쿨다운 만료 후 트리거 조건이 여전히 충족되면 재시도 +
last_attempt_at갱신 +attempt_count증가(성공 여부 무관). attempt_count >= TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT(기본 3) 도달 시 재시도 직전 운영자WARNING로그(slot_id/count/reason/last_attempt_at).- 트리거 조건 해소(가격 회복 등)·체결 완료(슬롯 holding 이탈) 시 재시도/카운트 증가 없음. 데몬 재시작 시 쿨다운 초기화(재시작 직후 1회 재시도 허용).
Flow: safety-sell 가시화 (BAT-021 / dcy-017, REQ-037)
전략 이탈로 슬롯이 안전망(워치독) 소유로 넘어가거나 실제 청산이 발동하는 결과 기반 신호를 무성에서 가시화한다(issue #12). best-effort — 어떤 로깅/메트릭 실패도 워치독 판정·청산을 막지 않는다(NFR-1).
- 워치독 소유 진입/이탈:
run_once가 매 tick_update_ownership로 holding+워치독 만료(grace 밖) 슬롯 집합을 직전 tick 과 비교 → 진입(전략 이탈)=WARN, 이탈(복귀)=INFO + 카운터(owned:enter/owned:exit). grace 중엔 판정 안 함. - 발동 가시화(OQ-2 차등): WARN =
forced_close_at·stop_loss발동 / INFO =take_profit·market_close_liquidation발동 + 카운터(fire:{reason}). 모든 로그는slot_id·portfolio_id·reason·phase·instance_key구조화 필드 포함. - per-tick 스킵/no-fire 평가는 로그하지 않고 카운터만(30초 주기 로그 스팸 방지; 영속 거부 반복 경고는 REQ-052 가 별도 담당).
- WatchdogStore 가 in-memory 라 소유 상태 는 재시작 시 소실(폴링 재개 시 재구성); 발동 로그 는 이력으로 남는다(R-3). 메트릭은 in-process 카운터(
SafetySellService.counters).
Flow: kill-switch 비상 정지 (BAT-012 / REQ-043/045/046)
운영자가 trex.control.req 로 비상 정지를 발동해 범위별 holding 을 즉시 청산하고 당일 전략을 차단한다.
trex-control-rpc가{action:"kill_switch", scope, portfolio_id?}수신 →KillSwitchService.fire().- 대상 범위(portfolio 또는 전역)의
holding슬롯을SlotService.sell(actor="kill-switch", reason="emergency_stop")로 순차 전이(holding→pending_sell). 이미 pending_sell 은 idempotent(REQ-036). 이후 체결은 집행 루프가 처리. - 발동 범위를
killswitch_exclusion(portfolio_id|'*', trading_date)에 day-scoped 로 표시(REQ-044) + 발동 이력 로깅(actor/scope/처리 수). - masking: 이후 snapshot RPC 응답 산출이
mask = pending|warning|pause|killswitch_today로 대상 자산을 제외 → 당일 추가 매수 차단. 전역('*')이면 전체 universe 마스킹. - 자동 복귀:
killswitch_exclusion은trading_date키라 다음 거래일 조회에서 빠짐(별도 동작 불필요). 재시작해도 같은 거래일이면 영속 유지. - 수동 해제:
{action:"resume_kill_switch", scope, portfolio_id?}→KillSwitchService.resume()가 day-scoped row 삭제 → 다음 응답부터 정상 노출. 이미 청산된 슬롯은 비가역(매도 되돌리지 않음). - 독립성: kill-switch 는 자율 안전 매도 worker(dcy-007)와 스레드/상태 공유 없음. 동일 슬롯 동시 sell() 은 REQ-036 idempotency 로 1회만 전이.
Flow: multi-instance 라우팅 (BAT-013 / REQ-049/050)
다중 인스턴스를 동일 RabbitMQ 에 붙여도 split-brain 없이 운영하기 위한 큐/exchange 분리.
- 각 인스턴스는
--config inst-N.toml(instance_key)로 기동 → 수신 큐명이{base}.{instance_key}로 분리(default는 리터럴 유지, 하위호환). declare_topology(instance_key):trex.direct(topic) +trex.broadcast(fanout) 선언 + 인스턴스 intent/snapshot/control 큐를trex.direct에{instance_key}.{channel}로 바인딩 + control 큐를trex.broadcast에 dual 바인딩.- directed: strategy/운영자가
trex.direct에inst-1.intent로 publish →trex.intent.req.inst-1에만 전달(다른 인스턴스 큐 미수신). 인스턴스별 WatchdogStore/SafetySellService 가 자기 큐만 소비 → split-brain 방지(NFR-3). - broadcast:
trex.broadcast에 publish → 모든 인스턴스 control 큐 동시 전달(global kill-switch 등). 중복 처리는 idempotency 로 무해. - 기존 reply-to RPC 직접 큐 경로(default)는 그대로 동작 — 추가 바인딩은 비파괴적(FR-5 무회귀).
Flow: broker degrade 분기 (BAT-007 / REQ-024)
- transport connection drop → 어댑터가
_mark_disconnected()+ 백오프(1s→2s→4s→8s→16s→30s cap) 재연결. - application 레이어는
transport.health()가disconnected/reconnecting인 동안 신규 발행/RPC 가TransportUnavailableError로 빠르게 실패한다 → 전략은 매매 보류. - core 자율 안전 매도(stop-loss·take-profit / forced_close / 장 마감 청산)는 transport 와 무관하게
trex-safety-sellworker 가 수행한다(아래 safety-sell 흐름; dcy-007 REQ-037~041 구현, BAT-010/011). 이전 "A-5 미구현" 항목은 dcy-007 로 해소됨. - 로컬 버퍼링 없음 — broker 복구 후 옛 intent 가 자동 재생되지 않는다(전략이 신선 상태로 재판단).
Flow: 주문 집행 루프 (백그라운드, BAT-004)
데몬 lifecycle 에서 시작되는 단일 백그라운드 execution 루프가 주기적으로(설정 간격) 수행:
- dispatch:
list_undispatched_pending(instance_key)(status pending_* ANDbroker_order_id IS NULL; 인스턴스 스코프, REQ-056) →BrokerPort.submit_order→mark_dispatched(broker_order_id)(멱등 가드). 거부 시pending_buy→inactive/pending_sell→holding롤백.- 매도 수량 = 브로커 잔고 정본(dcy-020/REQ-062, D3): 매도 dispatch 는
broker.get_balance()의 해당 symbolhldg_qty로 수량을 정한다(슬롯 미러qtyfallback 없음). 브로커 0 이면 주문 미제출 + WARN +sell_skipped_no_balance카운터(divergence 는 reconcile 경로로 표면화). skip 된 매도가 큐를 막지 않도록 리스트 기반 dispatch. - 가격/주문유형 결정(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 연계). - 슬리피지 보호 지정가(dcy-022/REQ-070): 슬롯에
slippage_pct가 있고limit_price도 있으면, 실효 지정가 =limit_price×(1+pct/100)(매수, ceil) /limit_price×(1-pct/100)(매도, floor).limit_price없으면(시장가)slippage_pct무시. 미지정 슬롯은 기존 동작(회귀 없음).
- 매도 수량 = 브로커 잔고 정본(dcy-020/REQ-062, D3): 매도 dispatch 는
- poll:
list_dispatched_pending(instance_key)(broker_order_id NOT NULL) →BrokerPort.poll_fill→ 체결 시pending_buy→holding(avg_price/qty) /pending_sell→inactive(점유 초기화·자산 정체성 보존), 거부 시 롤백. 전이 후clear_dispatch.- 외부 취소 감지(dcy-013/REQ-012/013, issue #9): 계좌에서 직접 취소된 주문을 KIS
poll_fill이cncl_yn="Y"로 파싱한다. 체결 0 취소 →FillStatus(state="cancelled")→ 기존 롤백 경로(pending_sell→holding,clear_dispatch)로 자동 복구(영구 갇힘 제거). 부분 체결 후 취소(is_partial_cancel) → 체결분 원장 1회 기록 + 슬롯 qty 차감(매도: 잔여 held, cost basis 유지 / 매수: 체결분만) +holding복귀(OQ-1 A안, 과매도·이중기록 방지). - pending stale 워치독(dcy-013/REQ-014 FR-3, Alert-only):
accepted/partially_filled(취소 아님)로 다음 tick 대기 중인 슬롯이TREX_PENDING_STALE_THRESHOLD(초, 0=비활성)를 초과해 머물면 WARN 로그 +ExecutionResult.stale_pending카운터로 가시화한다. 상태는 변경하지 않는다(auto-remediation 후속) — 취소 감지 누락(R-1) 등 미지 원인의 무한 갇힘이 silent 로 남지 않게 하는 안전망. - timeout 자동 취소(dcy-022/REQ-069):
accepted/partially_filled상태 슬롯에timeout_sec가 있고 경과 시간(now - slot.updated_at)이 초과하면broker.cancel_order를 best-effort 로 송신하고ExecutionResult.timeout_cancelled를 증가시킨다. 슬롯 상태는 이 시점에 변경하지 않는다 — 다음 poll 에서is_terminal_reject(전량 취소) → rollback /is_partial_cancel(부분 체결 후 취소) → 부분 보존(기존 경로 재사용). 취소-체결 race:cancel_order가 이미 체결된 주문을 받으면 브로커가 거부 → 다음 pollis_filled→ 정상 체결 경로 수렴(임의 롤백 없음, FR-4). 기존timeout_sec없는 슬롯은 stale-alert 만 실행(REQ-014 회귀 없음).
- 외부 취소 감지(dcy-013/REQ-012/013, issue #9): 계좌에서 직접 취소된 주문을 KIS
- reconcile(재기동): 서빙 전
list_dispatched_pending재폴링으로 크래시 후 복구(중복 주문 0). 미디스패치(broker_order_id NULL) 슬롯은 다음 dispatch 가 1회만 송신.
- 모든 슬롯 쓰기는 SlotService(advisory lock) 단일 경로(불변식 A). 브로커 주문/취소는 BrokerPort 단일 세션만(불변식 B).
- 슬롯
limit_price생애주기(dcy-012/REQ-002): INACTIVE→PENDING_BUY(매수 지정가 set) → PENDING_BUY→HOLDING(clear) → HOLDING→PENDING_SELL(매도 지정가 set 또는 None=시장가) → PENDING_SELL→INACTIVE(clear). 롤백(→INACTIVE) 전이에서도 clear. 외부 취소 후 holding 복귀에서도 clear 되어 재매도는 시장가. - 실행 정책 생애주기(dcy-022/REQ-068):
timeout_sec·slippage_pct는limit_price와 동일한 생애주기(PENDING_* 진입 시 set, HOLDING/INACTIVE 탈출 시 clear). 슬롯의updated_at이 dispatch 시각 proxy 로 사용된다(mark_dispatched 가 갱신, stale-alert 와 동일 방법). - 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅).
운영자 cancel 백스톱 (dcy-022/REQ-071)
timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분:
- 운영자
trex-core cancel <slot_id>→trex.control.req에{action:"cancel_pending", slot_id}전송. - 제어 핸들러가
ExecutionService.cancel_pending_slot(slot_id)호출 → 슬롯 pending 여부 확인 →broker.cancel_order송신(슬롯 직접 전이 없음). - 다음 집행 루프 poll 에서
is_terminal_reject/is_partial_cancel경로로 rollback/부분 보존 수렴(단일 writer 불변식 A 보존).
- 비-pending(holding/inactive) 슬롯 →
{status:"ignored", reason:"not_pending"}. 미존재 →{status:"error", reason:"slot_not_found"}.
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_outboxanti-join,find_ledger_gap_slots)로 영구 탐지한다.
- 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(BrokerPortcalculate_fee(side, amount)),tax=0을 운반. - 슬롯 전이:
pending_buy→holding(avg_price/qty) 또는pending_sell→closed를 advisory lock 안에서 먼저 commit. 이 단계가 critical path. - ledger enqueue: 슬롯의
portfolio_id(== ledgerasset_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 후 슬롯 전이는 정상 유지. clear_dispatch로broker_order_id해제.
Flow: ledger writer (백그라운드, BAT-006)
데몬 lifespan 에서 별도 백그라운드 워커가 ledger outbox 를 drain 한다. 슬롯 전이와 분리되어 eventually-consistent.
LedgerOutbox.list_pending(limit)으로 pending work item 을 FIFO 로 가져온다. 백오프 중(next_retry_at > now)인 행은 건너뛴다(dcy-015/REQ-019 head-of-line 보호, NFR-3).- row 의 현재 상태가 이미
completed면 (e.g. 동시/재처리)duplicate_completed로 마킹하고 어댑터를 호출하지 않는다. LedgerPort.record_trade(trade)호출 →LedgerWriteOutcome{result, cash_balance_after?, asset_quantity_after?}.result=NEW→ outcomecompleted, row statuscompleted.result=DUPLICATE_OK(어댑터 server-side dedup) → outcomeduplicate_completed, row statuscompleted, ledger 중복 0.- option D write-back(dcy-018/REQ-019/030):
new·duplicateoutcome 의cash_balance_after/asset_quantity_after(None 아님)를SnapshotStore.update_cash_position(asset_id, …)로 반영(best-effort — 실패해도 ledger outcome 무차단, NFR-1).*_after가 절대 권위값이라 duplicate 도 멱등. 응답 누락(구 hub)·409/422 는 write-back skip. LedgerConflictError(portfolio-hub 409) → outcomeconflict_blocked, row statusconflict_blocked(자동 재시도 중단, 운영자 해소 대상).LedgerPermanentError(portfolio-hub 404/400/422, dcy-015/REQ-017, issue #13) → outcomepermanent_failure, row statusdelivery_failed(터미널, 재시도 0). 재시도로 해소 불가한 클라이언트 오류(미등록 포트폴리오 등) → 즉시 중단 + WARN.LedgerRetryableError(timeout/5xx/network/408/429) → 지수 백오프 + jitter(delay = min(base × 2^(attempts-1), cap),next_retry_at설정) 후 다음 due tick 에 재시도.max_attempts초과 시 outcomedead_lettered, row statusdelivery_failed(dead-letter, 터미널) + WARN. 무한 1초 재시도 제거.
- 터미널 실패 가시화(dcy-015/REQ-018):
delivery_failed(permanent/dead-letter)는WriterStats.delivery_failed카운터 + WARN 로그 + 구분 status 로 노출되어 정상pending과 구별된다(무성 영속 실패 금지). dead-letter 는 폐기가 아니라 운영자 재처리 가능 상태이며find_ledger_gap_slots(REQ-055)·skip 가시화와 연계해 누락을 추적한다. - 백오프 파라미터는 env 설정:
TREX_LEDGER_RETRY_BASE_SEC(기본 1.0)/TREX_LEDGER_RETRY_CAP_SEC(기본 300)/TREX_LEDGER_MAX_ATTEMPTS(기본 8). - portfolio-hub 장기 장애에도 outbox 가 적재되어 무유실. 재시작 시 PostgreSQL
ledger_outbox.status='pending'행을 그대로 이어 처리한다(터미널delivery_failed/conflict_blocked는 제외).
Shared Components
- SlotRepository Protocol :: 저장소 추상화(자산 중심: list/count/reconcile 모두 instance_key 스코프). 구현체(InMemorySlotRepository, PostgreSQLSlotRepository)를 주입.
- slot_advisory_lock_id(instance_key) :: 인스턴스 스코프 advisory lock 식별자 — 공유 DB 의 인스턴스 간 직렬화 격리(REQ-057 NFR-1).
- transactional_advisory_lock :: 단일 트랜잭션 + advisory lock 컨텍스트(외부 I/O 금지).
Flow: slot 상태 전이 (transit_slot) (BAT-030 자산 중심)
- 호출자가
transit_slot(slot_id, from_state, to_state, reason, actor, **fields)를 호출한다. - (from_state, to_state) 가 전이 패밀리(7 base[dcy-021 holding→pending_buy 증액 포함] + *→error + error→{inactive,holding} = 13)에 속하는지 먼저 검증한다(아니면 SlotTransitionError).
- 인스턴스 스코프 advisory lock(
slot_advisory_lock_id(instance_key)) 획득 후 슬롯을 fetch 한다(없으면 SlotNotFoundError). - 현재 status == from_state 인지 확인한다(아니면 SlotStateMismatchError — 레이스 감지).
- to_state 의미에 맞춰 필드를 계산하고 슬롯 행을 update 한다 — INACTIVE 진입 시 포지션·주문 점유 필드 초기화 + 자산 정체성(instance_key/portfolio_id/symbol/retired_at) 보존.
- slot_transition_log 에 전이 기록 1행을 append 하고 갱신된 슬롯을 반환한다.
- 외부 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
/v1API 제거, 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 어댑터,SnapshotStorein-memory +snapshot_pausePostgres,SnapshotScheduler일일 refresh,reconcile_positions정합 검증 +BrokerPort.get_balance()확장,trex.snapshot.reqenvelope 응답 + degraded 신호,trex.pause.req제어 RPC,trex-coreCLI) (REQ-027~033; 옵션 D AC-2/AC-3 은 portfolio-hub#8 배포 후 별도 구현). - 2026-06-10 :: BAT-030 (dcy-020 마감) — 자산 중심 전환 완료: 일일 preallocation 폐기(→universe reconcile), intent admission slot_id 정본 + M 캡, 집행 매도 수량 브로커 정본 + 인스턴스 스코프 dispatch, 5-state(inactive) 전이, DB 재초기화 도구(
trex-core-reset, 데이터 보존 cutover 대체 — REQ-064 CHG-20260610-03) (REQ-056~065).
Flow: 포지션 사이징 (BAT-031 / dcy-021, REQ-066)
보유(holding) 자산 슬롯의 수량을 명령형으로 조정한다. 전략이 스냅샷 cash+quantity 로 qty+price 를 계산해 보내고 core 는 실행만 한다(target 미저장).
- 증액: 보유 슬롯에 매수 intent(qty+price) → admission 이
holding→pending_buy(M 캡 미적용, active 수 불변) → dispatch 매수(qty=주문) → 체결pending_buy→holding: 보유 qty=브로커 잔고, avg_price=가중평균((old_qty×old_avg+filled×fill)/new_total, old_qty=브로커총−체결). 거부 시 브로커 잔고>0 → holding 복귀(기존 보유 유지), 0 → inactive. - 부분 감액: 보유 슬롯에 매도 intent(qty+price) →
holding→pending_sell(slot.qty=주문) → dispatch 매도min(주문, 브로커잔고)→ 체결 후 브로커 잔고>0 →pending_sell→holding(잔량, cost basis 유지) / 0 →pending_sell→inactive. 전량 매도(qty 미지정, safety-sell/kill-switch)는 slot.qty=None → dispatch=브로커 잔고. - 부분 체결: 실제 체결분만 ledger(REQ-018/019)·슬롯 반영(브로커 정본 resync), 미체결 잔량은 core 추적 안 함 — 전략이 다음 스냅샷 기준 재발행.
- 2026-06-10 :: BAT-031 (dcy-021) — 포지션 사이징(증액·부분 감액) 흐름 추가, transit 13 전이.