발행 전용 연결의 idle heartbeat 사망 후 첫 발행 유실 결함 제거. - REQ-091 발행 재시도 + 강제 재연결(_force_reconnect, is_open 불신) - REQ-092 idle keepalive(execution-loop 단일스레드 틱, process_data_events time_limit=0) - REQ-093 DLQ 가시성 + emit 실패 ERROR 경보(구조화 로그, dcy-029 재사용) - REQ-094 baseline 문서 정합(runtime-flows/operations/project-structure) 독립 opus QC PASS-WITH-NOTES → 시정 2건(ruff F401, AC-3 프로덕션 경로) 완료. 554 passed/47 skipped/회귀 0/ruff clean. NFR-1 wire 무회귀(interface-contract diff 0). BAT-047 release-candidate, REQ-091~094 Implemented. dcy-031. M2 트랙B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
182 lines
9.5 KiB
Markdown
182 lines
9.5 KiB
Markdown
# 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).
|
|
|
|
### 기동 / 종료
|
|
```bash
|
|
# 기동 (로그 리다이렉트). 테스트는 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
|
|
|
|
```bash
|
|
.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)
|
|
```bash
|
|
.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 키)
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
# 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:
|
|
```logql
|
|
{job="trex-core"} | json | level="error" | line_contains "DLQ"
|
|
{job="trex-core"} | json | level="error" | line_contains "event publish failed"
|
|
```
|
|
|
|
### RabbitMQ queue depth 확인
|
|
|
|
```bash
|
|
# 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. 디버깅 / 트러블슈팅
|
|
|
|
| 증상 | 원인 | 조치 |
|
|
|---|---|---|
|
|
| 매수가 계속 거부 | (구) 슬리피지 한계가 호가단위 위반(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 미설정 | `trex-core cancel <slot_id>` 백스톱; pending stale WARN 확인 |
|
|
|
|
### DB 직접 조회
|
|
```bash
|
|
# 슬롯 상태 분포
|
|
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"
|
|
```
|
|
|
|
---
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
- `docs/interface-contract.md` — RabbitMQ 토폴로지 + 메시지 payload + reason/actor 어휘
|
|
- `docs/runtime-flows.md` — 흐름 다이어그램(스냅샷/intent RPC/이벤트/집행 루프/로깅)
|
|
- `.claude/skills/run-trex-core-daemon/SKILL.md` — AI 에이전트용 실행 레시피(이 런북의 사실 자료 출처)
|