Files
trex-core/docs/operations.md
T
genichinandClaude Opus 4.8 5bdd72040e docs(operations): 무데이터/죽은 종목 위생 운영자 런북 추가 (dcy-017 후속)
dcy-017 지속 무데이터 알람의 후속 운영 절차를 operations.md §7로 문서화.
- pause 시맨틱 코드 검증 결과 기술(신규 매수만 차단, 보유분 exit는 정상)
- Step 1 알람검증 / Step 2 포지션확인 / Step 3a pause(가역) / Step 3b 영구제거
- 보유 포지션 안전성 + 영구삭제 시 고아 방지 순서 명시
- 코드 변경 없음(기존 trex-core pause/resume 기능 문서화)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-22 10:36:50 +09:00

20 KiB

trex-core 운영자 런북 (Operations Runbook)

대상: trex-core 데몬을 배포·운영·디버깅하는 운영자. 범위: 배포/실행, 운영자 CLI, 로깅/관측성, 디버깅/트러블슈팅. 출처: dcy-029(REQ-089) — .claude/skills/run-trex-core-daemon/SKILL.md 사실 자료 + 관측성 기능(REQ-084~088) 사용법 통합.


1. 배포 / 실행

trex-core 데몬은 HTTP 표면이 없다(BAT-007). 모든 전략-facing 트래픽은 RabbitMQ로 흐른다(스냅샷/슬롯 RPC, intent RPC, 슬롯 이벤트). 운영자 조작은 trex-core CLI.

필수 인프라

PostgreSQL + RabbitMQ가 떠 있어야 한다. (paper 브로커는 외부 증권사 불필요.)

환경 변수 (config/.env, gitignore)

변수 필수 설명
TREX_POSTGRESQL_DSN 예 psycopg DSN
TREX_RABBITMQ_URL 실사용 시 예 예 amqp://admin:admin@localhost:5672/ (미설정 시 in-memory stub — 전략 모듈 도달 불가)
TREX_BROKER 아니오 paper(기본) 또는 kis
TREX_PORTFOLIO_HUB_BASE_URL 아니오 설정 시 실제 portfolio-hub universe+ledger, 미설정 시 stub universe(pf-005930/pf-000660)
TREX_LOG_FORMAT 아니오 text(기본, 사람용) 또는 json(Loki 적재용) — §3
TREX_EXECUTION_INTERVAL_SEC / TREX_LEDGER_INTERVAL_SEC 아니오 루프 간격(기본 1.0)

인스턴스 TOML

--config <name> (BAT-013 이후 필수)는 config/<name>.toml을 해석 — instance_key(큐 이름 trex.intent.req.<key> 등; default는 literal), max_active_slots(M cap).

기동 / 종료

# 기동 (로그 리다이렉트). 테스트는 trex_core_test DB로 — 라이브 DB 금지.
.venv/bin/trex-core-daemon --config <instance> &> /tmp/trex-core.log &

# 종료 (SIGTERM → 워커 join + RabbitMQ close, 마지막 로그 'Normal shutdown')
pkill -TERM -f trex-core-daemon

데몬은 TCP 포트를 열지 않는다. "ready" = 4개 워커 스레드 + RabbitMQ 토폴로지 선언 완료. /tmp/trex-core.log에 ERROR/traceback이 없으면 부팅 성공.

안전 수칙

  • config/.env는 라이브 KIS 키 + portfolio_hub 토큰 + 라이브 PostgreSQL DSN을 담는다 — 커밋 금지, 라이브 DB에 파괴적 작업 금지.
  • 테스트는 TREX_BROKER=paper + 파생 trex_core_test DSN.

2. 운영자 CLI

.venv/bin/trex-core <command> [options]    # 콘솔 스크립트
명령 용도 경로
slots [--status all|holding|buyable|pending|error] 슬롯/포지션 목록 RPC
universe 오늘 universe 자산 RPC
enter <portfolio_id> [--qty/--entry-price/--stop-loss/--take-profit/--forced-close-at] 매수 intent RPC
exit <slot_id> [--price] 매도 intent RPC
cancel <slot_id> 대기 주문 취소(백스톱) RPC
pause/resume/paused portfolio 일시정지 direct-DB
report [--json] 보유·오늘 체결·실현 PnL·대기 주문 direct-DB (REQ-088)
  • enter/exit 안전장치: 확인 프롬프트 기본 ON, --yes(paper 생략), live(broker=kis)는 --yes --live 이중 ack, --dry-run 미리보기. --json은 비대화형이라 enter/exit에 --yes 필수.
  • 인스턴스 라우팅: --config <toml> 또는 --instance <key>, 미지정 시 default.

report — 일일 운영 리포트 (REQ-088 / dcy-029)

.venv/bin/trex-core report                 # 사람용 표
.venv/bin/trex-core report --json          # 자동화용 JSON

출력: 현재 보유(symbol/qty/avg_price), 오늘(KST) 체결 건수, 오늘 실현 PnL(= ledger_outbox net 매매 현금흐름 = Σ매도(대금−수수료−세금) − Σ매수(대금+수수료), 수수료·세금 반영 BAT-038 정책), 대기 주문.

  • "오늘" 경계는 trading_date(KST 00:00~익일 00:00). 전일 체결은 제외.
  • 일중 진입·청산 전략에서는 net ≈ 실현손익. 미청산 보유의 평가손익은 포함하지 않는다(실현분만).

3. 로깅 / 관측성 (REQ-084~087 / dcy-029)

포맷

  • 모든 로그 라인: <ISO8601 KST> <LEVEL> <logger> [cid=<slot_id>] <msg>.
  • TREX_LOG_FORMAT=json → Loki 적재용 한 줄 JSON(ts/level/logger/msg/correlation_id + 구조화 필드). Grafana/LogQL 대시보드용.
  • correlation id = slot_id (intent→order→fill 공통 조인 키). 한 슬롯의 submit/fill/reject 로그가 같은 cid를 공유 → 한 거래 추적.

주문 결과 로그 (grep 키)

grep "event=order_submit" /tmp/trex-core.log    # 제출 주문
grep "event=order_fill"   /tmp/trex-core.log    # 체결(fill_price/filled_qty/fee/tax)
grep "event=order_reject" /tmp/trex-core.log    # 거부 — ERROR 레벨
  • 거부는 ERROR 레벨로 즉시 보인다(조용한 롤백 제거). 호가단위 위반(issue #28) 같은 거부가 event=order_reject ... reason=...로 노출.

청산 사유 해석 — reason + actor (REQ-086)

  • safety-sell fired: ... reason=<r> actor=trex-safety-sell → 안전망 자율청산(전략 침묵으로 watchdog가 슬롯 소유). reason=stop_loss여도 이는 "전략 손절"이 아니라 안전망이 SL선에서 청산한 것.
  • actor=execution_loop → 집행 루프 정상 체결/롤백. 전략 SellIntent 매도는 전략 actor.
  • 즉 reason(조건)과 actor(주체)를 함께 보고 안전망/전략을 구분한다.

4. DLQ 가시성 / 이벤트 발행 경보 (REQ-093 / dcy-031 / BAT-047)

trex-core는 trex.events.dlq(durable) 큐와 구조화 로그를 통해 이벤트 발행 실패를 운영자에게 노출한다.

DLQ 유입 원인

원인 경로 로그
구독자 없는 이벤트(unroutable) UnroutableError → _publish_dlq(reason="unroutable") ERROR DLQ entry: reason=unroutable rk=...
DLQ 재발행 자체 실패 _publish_dlq 내 exception ERROR DLQ publish failed (rk=...)
발행 최종 실패(재연결+재시도 후) _make_event_emitter → TransportUnavailableError ERROR event publish failed (rk=... slot_id=... strategy=...)

로그 기반 경보 (Loki / grep)

# DLQ 유입 확인
grep '"level":"error"' /tmp/trex-core.log | grep "DLQ"
# 또는 text 포맷
grep "ERROR.*DLQ" /tmp/trex-core.log

# 발행 최종 실패 확인
grep "event publish failed" /tmp/trex-core.log

Loki LogQL:

{job="trex-core"} | json | level="error" | line_contains "DLQ"
{job="trex-core"} | json | level="error" | line_contains "event publish failed"

RabbitMQ queue depth 확인

# rabbitmqctl로 DLQ 깊이 조회
rabbitmqctl list_queues name messages | grep dlq

# RabbitMQ 관리 UI: http://<broker>:15672 → Queues → trex.events.dlq

운영자 조치 절차

  1. DLQ 유입 경보 수신: 로그 ERROR ... DLQ entry 발견.
  2. 원인 분류:
    • reason=unroutable: 해당 strategy_id의 전략 데몬(orb/tsl)이 꺼져 있거나 큐 바인딩이 없는 경우. 전략 데몬 기동 상태 확인.
    • DLQ publish failed: RabbitMQ 자체 연결 문제. broker 상태 확인.
    • event publish failed: 발행 연결 재연결+재시도 후에도 실패. broker 재시작 또는 네트워크 장애. trex-core 재기동 검토.
  3. DLQ 내용 소비(선택): 적체된 메시지는 수동 drain 또는 strategy 재기동 후 catch-up.
  4. 임계값 기준: DLQ depth > 0 = 경보 대상. depth > 100 = 즉시 조치 권고.

idle heartbeat 예방 (REQ-092)

trex-core는 발행 연결의 idle 종료를 예방하기 위해 trex-execution 루프 각 틱(기본 1초)마다 event_publisher.keepalive_tick()(process_data_events(time_limit=0))을 호출한다. heartbeat=30s 설정 대비 충분한 I/O 주기를 보장한다.


5. 체결가 백필 WARN 발생 시 운영자 점검 절차 (REQ-095/096 / dcy-032 / BAT-048)

KIS 체결조회가 체결수량은 보고하면서 평균체결가(avg)를 None 또는 0으로 반환하는 순간 레이스가 발생하면, trex-core는 주문 지정가로 백필해 슬롯 평단·원장 cost basis에 0이 저장되지 않도록 보호하고 WARN 로그를 발행한다.

WARN 유형 및 로그 식별

WARN 유형 reason 발생 조건 의미
avg_price_backfill reason=avg_price_backfill avg_price=None/0, limit_price>0 지정가로 백필 성공 — 값은 보정됐으나 추적 필요
fill_price_unresolvable reason=fill_price_unresolvable avg_price=None/0, limit_price=None/≤0 백필 불가 — 체결 skip, 슬롯 PENDING 유지
# avg_price_backfill 확인 (백필 성공)
grep "avg_price_backfill" /tmp/trex-core.log

# fill_price_unresolvable 확인 (백필 불가, 운영자 즉시 개입)
grep "fill_price_unresolvable" /tmp/trex-core.log

Loki LogQL:

{job="trex-core"} | json | line_contains "avg_price_backfill"
{job="trex-core"} | json | line_contains "fill_price_unresolvable"

avg_price_backfill WARN 발생 시 점검 항목

  1. 로그에서 슬롯·주문 정보 확인: slot_id, symbol, broker_order_id, avg_price(원 값, None 또는 0), backfill_price(대체 지정가)를 로그에서 추출.
  2. 원장 레코드 확인: 해당 slot_id의 ledger_outbox price가 백필값(지정가)으로 기록됐는지 확인.
    psql "$TREX_POSTGRESQL_DSN" -c "SELECT * FROM ledger_outbox WHERE correlation_id LIKE '%<broker_order_id>%'"
    
  3. 슬롯 평단 확인: trex-core slots 또는 DB에서 해당 슬롯의 avg_price가 0이 아닌 지정가 기반 값인지 확인.
  4. 판단: 백필 지정가는 "0보다 합리적인 근사"지만 실제 체결가와 다를 수 있다. 중요한 포지션이라면 KIS HTS/API에서 실제 체결가를 확인해 ledger_outbox 수기 보정 여부를 검토한다(Discovery R-1).

fill_price_unresolvable WARN 발생 시 즉시 조치

  1. 로그에서 슬롯 정보 확인: slot_id, symbol, broker_order_id 추출.
  2. 슬롯 상태 확인: trex-core slots --status pending — 해당 슬롯이 PENDING_BUY 상태로 고착됐는지 확인.
  3. KIS HTS/API에서 실제 체결 여부와 가격 확인.
  4. 운영자 조치 선택:
    • 체결됐다면: trex-core cancel <slot_id>로 슬롯을 롤백 후 재진입 매수.
    • 미체결이라면: trex-core cancel <slot_id>로 주문 취소 → 슬롯 rollback(다음 폴링에서 is_terminal_reject 경로).
  5. 상황이 반복된다면: Discovery 또는 이슈 트래커에 기록 — limit_price=None 매수 주문이 들어온 경로 조사 필요(정상 운용 시 orb 전략은 지정가 주문을 제출한다, Discovery A-1).

6. 디버깅 / 트러블슈팅

증상 원인 조치
매수가 계속 거부 (구) 슬리피지 한계가 호가단위 위반(issue #28) dcy-030으로 수정됨(호가단위 스냅). grep event=order_reject로 사유 확인
slots returned: 0 universe reconcile 미실행(hub down/boot race) 스케줄러 로그 slot reconcile: 확인; hub 미설정 시 stub 2슬롯
intent rejected: unknown_slot slot_id/portfolio_id가 인스턴스 슬롯셋에 없음 먼저 slots로 조회 후 그 slot_id 사용
intent rejected: capacity_full max_active_slots(M) 도달 매도 후 재시도 또는 M 상향
매도 미발생 + WARN sell dispatch skipped 브로커 잔고 0(divergence) reconcile(운영자) — 미러 fallback 없음(설계)
RpcTimeoutError 데몬 미기동/소비 스레드 크래시 /tmp/trex-core.log traceback 확인
미체결 pending_* 고착 취소 감지 누락/timeout 미설정/fill_price_unresolvable trex-core cancel <slot_id> 백스톱; pending stale WARN + fill_price_unresolvable WARN 확인(§5)
avg_price_backfill WARN KIS avg_price 순간 레이스(None/0 반환) 슬롯 평단·원장 price는 지정가로 보정됨 — §5 점검 절차 참고

DB 직접 조회

# 슬롯 상태 분포
psql "$TREX_POSTGRESQL_DSN" -c "SELECT instance_key, status, COUNT(*) FROM daily_slot GROUP BY 1,2 ORDER BY 1,2"
# ledger outbox 상태
psql "$TREX_POSTGRESQL_DSN" -c "SELECT status, last_outcome, COUNT(*) FROM ledger_outbox GROUP BY 1,2"
# 슬롯 전이 이력(reason+actor)
psql "$TREX_POSTGRESQL_DSN" -c "SELECT slot_id, from_state, to_state, reason, actor, created_at FROM slot_transition_log ORDER BY created_at DESC LIMIT 20"


7. 무데이터/죽은 종목 위생 (dcy-017 후속)

dcy-017이 "지속 무데이터" WARN 다이제스트를 운영자에게 표시한다. 그 후속 조치로 해당 종목을 매매 유니버스에서 pause 또는 영구 제거하는 절차.

용어: portfolio_id = trex-core에서 종목을 식별하는 키 (portfolio-hub asset_id와 동일). pause/resume 명령의 대상이다.

트리거

  • dcy-017 지속 무데이터 WARN 알람(시세 N회 연속 무응답).
  • 운영자가 직접 관찰: KRX 거래정지/상장폐지 공시, 종목 이상 감지.

pause 시맨틱 (코드 검증 결과)

아래는 코드 직접 확인 결과다. 추측 없이 file:line을 근거로 기술한다.

pause가 하는 것 — 신규 진입(매수) 차단

pause_store.pause(portfolio_id) = INSERT INTO snapshot_pause (portfolio_id, ...) ON CONFLICT DO UPDATE (adapters/portfolio/pause_store.py:23). 이후 slots RPC 응답에서 해당 slot의 paused=True 필드가 반환된다 (application/portfolio.py:309-313, application/messaging_handlers.py:264-273).

trex-core 코어 자체(application/intents.py _admit_buy, line 179~257)에는 pause 체크가 없다. pause는 전략 데몬(orb 등)이 slots 응답의 paused=True를 읽고 그 slot에 buy intent를 보내지 않는 방식으로 신규 진입을 차단한다. 전략 데몬이 pause를 무시하고 buy intent를 보내면 코어는 수락한다(전략 데몬 쪽 정책).

pause가 하지 않는 것 — 기존 보유분 매도는 막지 않는다

application/safety_sell.py의 check_forced_close / check_prices / check_market_close 모두 self._holding(now) (= slot_repo.list_holding_slots())를 직접 조회하고 pause_store를 참조하지 않는다. tsl 전략 SellIntent, trex-core exit 명령도 마찬가지다.

결론: HOLDING 슬롯이 있는 종목을 pause해도 그 보유분은 safety-sell, tsl 전략, 운영자 exit 등 모든 매도 경로로 정상 청산된다. pause는 신규 매수만 막고, 기존 포지션의 exit은 방해하지 않는다. 포지션이 있어도 pause 명령은 안전하다.

resume 원복

pause_store.resume(portfolio_id) = DELETE FROM snapshot_pause WHERE portfolio_id = %s (adapters/portfolio/pause_store.py:35). 삭제 즉시 다음 slots RPC 응답부터 paused=False로 반환되어 정상 진입이 재개된다.


Step 1 — 무데이터 알람 검증

알람 수신 후 즉시 종목 상태를 확인한다.

# 해당 종목의 슬롯 상태 확인 (symbol 또는 portfolio_id로 필터)
.venv/bin/trex-core slots --symbol <SYMBOL>
.venv/bin/trex-core slots --status all   # 전체 목록에서 확인

KRX 공시/HTS에서 상태 분류:

상태 의미 권장 조치
일시적 시세 장애 단기 데이터 공백, 종목 정상 관망 → 자동 복구 확인
임시 거래정지 (공시/사유 있음) 거래 재개 예정 가능 pause (Step 3a)
장기 거래정지 / 상폐 예정 재개 불명확 pause 또는 영구 제거 검토 (Step 3b)
상장폐지 완료 거래 불가 포지션 0 확인 후 영구 제거 (Step 3b)

Step 2 — 포지션 확인

pause 또는 제거 전에 해당 종목에 보유 포지션이 있는지 반드시 확인한다.

# 1) 운영자 리포트 — 보유·대기 주문 한눈에 확인
.venv/bin/trex-core report

# 2) 해당 종목 슬롯 상세 조회
.venv/bin/trex-core slots --symbol <SYMBOL>
# 또는 portfolio_id를 알고 있을 때
.venv/bin/trex-core slots --status holding

# 3) DB 직접 확인 (정밀 조회)
psql "$TREX_POSTGRESQL_DSN" -c \
  "SELECT slot_id, status, symbol, qty, avg_price, portfolio_id FROM daily_slot WHERE symbol = '<SYMBOL>'"

출력의 status 확인:

  • inactive — 포지션 없음. pause 또는 제거가 자유롭다.
  • holding — 보유 포지션 있음. pause는 안전하다(아래 설명). 영구 삭제는 포지션 청산 후에만 한다.
  • pending_buy / pending_sell — 주문 대기 중. 완료 대기 또는 trex-core cancel <slot_id>로 취소 후 진행.

Step 3a — pause (가역, 권장 기본)

임시 거래정지, 무데이터 장기화, 불확실한 상황에서는 pause가 기본 선택이다. 언제든 resume으로 원복 가능하다.

# pause 적용 (direct-DB write, 데몬 RPC 불필요)
.venv/bin/trex-core pause <portfolio_id>
# 예: trex-core pause pf-005930

# pause 상태 확인
.venv/bin/trex-core paused

# 슬롯 조회 시 paused=True 확인
.venv/bin/trex-core slots --symbol <SYMBOL>

pause 후 동작:

  • 다음 slots RPC 응답부터 해당 slot의 paused=True 반환 → orb 등 전략 데몬이 그 slot에 buy intent를 보내지 않음 → 신규 매수 차단.
  • 기존 HOLDING 포지션은 영향 없음 — safety-sell(watchdog, stop-loss, 장마감 청산), tsl 전략 SellIntent, trex-core exit 모두 pause와 무관하게 정상 작동.
  • pause는 Postgres snapshot_pause 테이블에 영속 저장되므로 데몬 재시작 후에도 유지된다.

복구 (종목이 정상화되면):

.venv/bin/trex-core resume <portfolio_id>
# 예: trex-core resume pf-005930

# 확인
.venv/bin/trex-core paused   # 목록에서 사라졌는지
.venv/bin/trex-core slots --symbol <SYMBOL>   # paused=False 확인

Step 3b — 영구 제거 (상장폐지 등, 드문 케이스)

안전 경고: 영구 제거는 비가역이다. 반드시 포지션이 0인 것을 확인한 후 진행한다. portfolio-hub 삭제는 고아 슬롯 위험이 있으므로 아래 순서를 엄수한다.

전제 조건:

  1. 해당 종목 slot이 inactive 상태 (보유·대기 포지션 없음).
  2. 포지션이 남아 있으면 먼저 청산(tsl/safety-sell 대기 또는 trex-core exit으로 수동 매도) 후 진행.

절차:

  1. trex-core pause <portfolio_id> — 추가 진입 차단(옵션이나 권장).
  2. trex-core slots --symbol <SYMBOL> — status=inactive, qty=None 확인.
  3. portfolio-hub에서 해당 portfolio(asset) 삭제 → portfolio-hub 소관 작업. 직접 invocation은 portfolio-hub 운영자에게 요청하거나 사장님과 협의. (portfolio-hub가 삭제하면 다음 universe 스냅샷 갱신 시 해당 slot이 자동 retire된다.)
  4. 다음 날 또는 스냅샷 갱신 후 trex-core slots에서 해당 종목이 사라졌는지 확인.

portfolio-hub 삭제 없이 trex-core DB에서 slot을 직접 제거하면 고아 원장 레코드가 생길 수 있다. DB 직접 삭제는 금지한다.


안전 경고 요약

[PAUSE]   가역·원장보존. 언제든 resume 가능. 포지션 있어도 안전(매도 경로 유지).
[DELETE]  영구·비가역. 반드시 포지션 0 확인 후. portfolio-hub와 협의.

포지션(HOLDING)이 있는 종목은 pause만 적용하라.
영구 삭제는 포지션을 완전히 청산한 뒤에만 수행한다.

관련 문서

  • docs/interface-contract.md — RabbitMQ 토폴로지 + 메시지 payload + reason/actor 어휘
  • docs/runtime-flows.md — 흐름 다이어그램(스냅샷/intent RPC/이벤트/집행 루프/로깅)
  • .claude/skills/run-trex-core-daemon/SKILL.md — AI 에이전트용 실행 레시피(이 런북의 사실 자료 출처)