feat(rel-007): dcy-025/024 + BAT-035/036 — slots.req 즉시 교체 + safety_sell_enabled

BAT-035 (dcy-025): trex.snapshot.req → trex.slots.req 즉시 교체
- topology: SNAPSHOT_REQ_QUEUE 제거, SLOTS_REQ_QUEUE = "trex.slots.req" 추가
- portfolio.py: View/build_snapshot_envelope/SnapshotEnvelope/_asset_to_dict 제거;
  slot_to_dict 시그니처 확장(snapshot_store/watchdog_store/pause_store/watchdog_timeout_sec)
  — asset_quantity(hub)/cash_balance/cash_estimated/warned/watchdog{}/paused 병합 (REQ-074)
- messaging_handlers.py: make_snapshot_rpc_handler 제거, make_slots_rpc_handler 추가
  — filter(all/inactive/pending/holding/error) + spy(bool) 처리; strategy 필드 제외 (REQ-073/074)
- daemon.py: _snapshot_rpc_loop → _slots_rpc_loop, make_slots_rpc_handler 연결
- cli.py: universe/slots → slots_queue + spy=True + server-side filter; buyable→inactive 매핑 (REQ-063)
- rabbitmq.py: snapshot_queue → slots_queue
- docs: interface-contract.md + runtime-flows.md slots.req 계약으로 업데이트 (REQ-075)
- REQ-028: Implemented → Deprecated (코드 삭제 완료)
- 테스트: test_snapshot_handler/selection/rpc_watchdog/messaging_snapshot_consumer 삭제;
  test_slots_handler.py(신규, 13 cases), test_killswitch_masking_resume 재작성,
  test_ledger_option_d._apply_cash_estimate → slots handler 경유, test_topology/cli_operator 갱신

BAT-036 (dcy-024): per-instance safety_sell_enabled 플래그
- config.py: DaemonInstanceConfig + DaemonConfig 에 safety_sell_enabled: bool = True 추가;
  from_toml 파싱, from_config 전파
- daemon.py: start() workers 조건부 — safety_sell_enabled=false 시 trex-safety-sell 미기동 + WARNING
- tests: test_instance_config.py 에 safety_sell_enabled 파싱 3개 케이스 추가

REQ: 073/074/075/021/034/063/048/005 Approved → Implemented

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:45:45 +09:00
co-authored by Claude Opus 4.8
parent fb05a73130
commit f0ad37747b
44 changed files with 1963 additions and 1189 deletions
+55 -129
View File
@@ -25,7 +25,7 @@
## Interface Summary
- RabbitMQ `trex.snapshot.req` :: 전략 모듈의 스냅샷 RPC (request-reply, BAT-007/BAT-008 envelope)
- RabbitMQ `trex.slots.req` :: 전략 모듈/운영자의 슬롯 읽기 RPC (request-reply, BAT-035 — REQ-028 `trex.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` 확장)
@@ -52,7 +52,7 @@
| 종류 | 이름 | 타입/속성 | 용도 |
|---|---|---|---|
| 큐 | `trex.snapshot.req` | durable | 스냅샷 RPC 요청 큐 (read-only) |
| 큐 | `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 메시지 격리 |
@@ -63,145 +63,69 @@
| 패턴 | 표면 | 요청 속성 | 응답 |
|---|---|---|---|
| 스냅샷 RPC | `trex.snapshot.req` | `correlation_id`, `reply_to=amq.rabbitmq.reply-to` | reply-to 큐로 envelope(`assets[]`/`slots[]`/`available_slots`/…); stub 경로는 `{view, strategy_id, slots[], capacity{}}` |
| 슬롯 RPC | `trex.slots.req` | `correlation_id`, `reply_to=amq.rabbitmq.reply-to` | reply-to 큐로 `{slots[], degraded, reason, message}` (REQ-073/074, BAT-035) |
| intent RPC | `trex.intent.req` | `correlation_id` = intent 멱등키, `reply_to=amq.rabbitmq.reply-to`, `delivery_mode=2` | reply-to 큐로 `{intent_id, status:admitted|rejected, slot_id?, reason?}`. 체결/종료는 응답 아닌 이벤트로 |
| update_thresholds | `trex.intent.req` | `action="update_thresholds"`, `reply_to` | reply-to 큐로 `{status:updated|ignored, slot_id, reason?}` (REQ-040) |
| 이벤트 pub/sub | `trex.events` (topic) | routing key `{event_type}.{strategy_id}.{symbol}` | 전략 큐가 자기 `strategy_id` 패턴으로 바인딩 |
### Message contracts
#### `trex.snapshot.req` 요청 payload
#### `trex.slots.req` 요청 payload (REQ-073 AC-1, BAT-035)
```
{ "view": "open" | "all", "strategy_id": str | null,
"trading_date": "YYYY-MM-DD"? # optional — kill-switch 당일 제외 마스크 기준일만 (BAT-030)
{ "filter": "all" | "inactive" | "pending" | "holding" | "error", # default "all"
"spy": true | false # default false; true = watchdog 갱신 안 함(운영자 조회)
}
```
응답 payload는 슬롯 목록과 capacity 를 운반한다. view 모드와 strategy_id 는 payload(요청 매핑 원칙 — REQ-021). BAT-030: 슬롯 조회는 instance 스코프(영구 슬롯)라 `trading_date` 로 필터하지 않는다.
- `filter`: 서버 측 슬롯 status 필터. `"inactive"` 는 buyable 슬롯(universe에 바인딩된 매수 가능 슬롯). 미인식 값은 `"all"` 로 처리.
- `spy=true`: 응답에 포함된 holding 슬롯의 워치독 `last_seen_at` 을 갱신하지 않는다(운영자 CLI 조회가 전략 생존 신호로 오인되지 않도록 — REQ-034 CHG-01).
#### `trex.intent.req` 요청 payload (buy)
```
{ "intent_type": "buy",
"intent_id": "<correlation_id 와 동일>",
"slot_id": str(UUID), # CANONICAL key (dcy-020/REQ-061) — snapshot slots[] round-trip
"portfolio_id": str?, # legacy dual-accept 매핑(점진 전환용)
"qty": float?, "allocated_capital": float?,
"entry_price": float?, "strategy_id": str?, "sell_strategy_id": str?,
"forced_close_at": "ISO-8601"?,
"stop_loss_price": number?, "take_profit_price": number?,
"timeout_sec": int?, # dcy-022/REQ-068: N초 미체결 시 자동 취소·롤백 (optional)
"slippage_pct": float? } # dcy-022/REQ-068: entry_price*(1+pct/100) 실효 지정가 (optional)
```
- **`slot_id` 정본(dcy-020/REQ-061, BAT-029/030)**: 매수의 정식 키 — 전략 모듈은 snapshot `slots[]` 의 `slot_id` 를 그대로 회신한다. 미스 시 `unknown_slot` 거부. `portfolio_id` 는 legacy dual-accept(미스 시 `unknown_portfolio`); 둘 다 없으면 `slot_id_or_portfolio_required`. `trading_date` 는 계약에서 제거(BAT-030).
- admission(REQ-059): 대상 영구 자산 슬롯이 active(holding/pending) 면 `already_active`; instance active 수 ≥ `max_active_slots`(M) 면 `capacity_full`(FCFS·무치환). **`symbol` 은 client payload 에 없다** — core 가 슬롯의 `portfolio_id` 로 universe 에서 resolve 한다.
- `stop_loss_price`/`take_profit_price`(선택, REQ-035/038): 매수 시 설정하면 워치독 만료 후 safety-sell 가격 트리거의 기준이 된다. 미포함 시 NULL(트리거 비활성).
- `forced_close_at`(선택, REQ-066/037): 시간 기반 자동청산(만기) 절대시각. 운영자 CLI `enter --forced-close-at <ISO8601>`(절대시간)로 설정하며(REQ-032 CHG-20260609-01, issue #17, BAT-024), 미지정 시 payload 에 미포함(NULL). 설정 시 forced-close 만기 매도 루프(REQ-037)가 그 시각에 시장가 청산한다. 상대시간 옵션은 제공하지 않는다.
- **`timeout_sec`(선택, dcy-022/REQ-068)**: 주문 유효시간(초). N초 내 미체결이면 core 가 `cancel_order` 를 브로커에 송신하고 다음 poll 에서 슬롯을 롤백한다(pending_buy→inactive 또는 pending_sell→holding). 미지정 시 per-instance TOML `default_order_timeout_sec`(opt-in, 미설정=OFF=무기한). per-intent 값이 우선.
- **`slippage_pct`(선택, dcy-022/REQ-068)**: 체결 보호 밴드(%). `entry_price` 있을 때만 유효 — 실효 지정가 = `entry_price*(1+pct/100)` (매수, 올림) / `*(1-pct/100)` (매도, 내림). `entry_price` 없으면(시장가) 무시. 미지정=보호밴드 없음.
sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason":..., "sell_strategy_id":..., "limit_price": float?, "qty": float?, "timeout_sec": int?, "slippage_pct": float?}`.
- **`limit_price` 선택(dcy-012/REQ-007)**: 매도 지정가. **지정 시 지정가 매도(`ORD_DVSN="00"`, 그 가격)**, **미지정 시 시장가 매도(`ORD_DVSN="01"`)**. 자율 매도(safety-sell/kill-switch)는 가격을 싣지 않아 항상 시장가로 체결된다. 매수가(과거 `entry_price`)는 매도 가격으로 **재사용하지 않는다**(issue #10 — 슬롯 `limit_price` 는 pending 이탈 시 clear). buy payload 의 `entry_price` 필드명은 본 주기에서 유지(하위호환).
- **하위호환(dcy-022/REQ-068)**: `timeout_sec`·`slippage_pct` 두 필드 모두 optional — 미포함 시 기존 동작(무기한 지정가/시장가)과 동일(byte-동등). core 수신 측 먼저 배포 가능(발신 측과 lockstep 불필요).
#### `trex.intent.req` `update_thresholds` 액션 (BAT-011 / REQ-040)
```
{ "action": "update_thresholds",
"slot_id": str,
"stop_loss_price": number | null?, # 생략=유지, null=해제
"take_profit_price": number | null?, # 생략=유지, null=해제
"forced_close_at": "ISO-8601" | null?, # 생략=유지, null=해제
"actor": str? }
```
- 매수 이후 손절·익절·강제 만기 시각을 갱신한다. 세 필드는 독립적이며 payload 에 **키가 없으면 기존 값을 유지**, 명시적 `null` 이면 해제한다(REQ-040 AC-4/AC-5).
- `holding` 슬롯에서만 적용된다. 처리 성공 시 해당 슬롯의 워치독 `last_seen_at` 을 리셋한다(전략 모듈 생존 신호, REQ-034).
- 응답: `{status: "updated", slot_id}` / 비-holding 은 `{status: "ignored", slot_id, reason: "not_holding"}` / 미존재는 `{status: "ignored", slot_id, reason: "slot_not_found"}`.
#### `trex.events` payload (BAT-007)
```
{ "event_type": "slot_filled_buy"|"slot_filled_sell"|...,
"slot_id": str, "strategy_id": str?, "symbol": str?,
"status": "<SlotState 값>", "qty": float?, "avg_price": float?,
"portfolio_id": str?,
"sequence_at": "ISO-8601" } # BAT-030: trading_date 키 제거(영구 슬롯)
```
- `sequence_at` = 슬롯 `updated_at` 의 단조 증가 토큰. 수신자는 last-wins 적용 가능 → 순서 보증 불요.
- 이벤트의 `strategy_id`/`symbol` 은 routing key 에, 본문에도 함께 운반(이벤트 매핑 원칙 — REQ-021).
### Reliability rules (REQ-023)
- intent RPC: durable + persistent + manual ack-after-commit + `intent_log` correlation_id dedup. 단일 consumer + prefetch=1 → FIFO.
- 스냅샷 RPC: read-only, manual ack, 별도 dedup 불요.
- 이벤트: publisher confirms 활성, unroutable → DLQ. 페이로드는 현재 상태 + 단조 시퀀스(last-wins).
- per-strategy 이벤트 queue: TTL/max-length 초과 시 `drop-head` + 스냅샷 RPC reconcile 로 보정.
### Degrade (REQ-024)
- 어댑터 `TransportHealth` ∈ {connected, disconnected, reconnecting}. 어플리케이션 레이어는 disconnected 상태에서 `TransportUnavailableError` 로 빠르게 실패한다(버퍼링 0, 보수적 중단).
- 재연결은 백오프 1s→2s→4s→8s→16s→30s cap, 어댑터가 자체 구현.
- core 자율 안전 매도는 transport 와 무관하게 `trex-safety-sell` worker 가 동작한다(forced_close / stop-loss·take-profit / 장 마감, dcy-007 REQ-037~041, BAT-010/011 구현 완료).
### `trex.snapshot.req` envelope (BAT-008 / REQ-028)
전략 모듈이 보내는 요청:
```
{ "view": "all" | "buyable" | "held" # default "all"
"strategy_key": "<slug>" # optional, == filter on active_strategy_id
"trading_date": "YYYY-MM-DD" # optional, default UTC today
"touch_watchdog": bool # optional, default true (dcy-016/REQ-021)
"actor": "operator" # optional; "operator" => touch_watchdog 강제 false
}
```
- **워치독 억제(dcy-016/REQ-021/034)**: 기본(`touch_watchdog` 미지정/true)은 응답에 포함된 holding 슬롯의 워치독 `last_seen_at` 을 갱신한다(전략 폴링=생존 신호, REQ-034). `touch_watchdog=false` 또는 `actor="operator"` 면 갱신을 건너뛴다 — 운영자 CLI 조회(`universe`/`slots`)가 죽은 전략을 살아있는 것으로 오인시켜 safety-sell 을 무력화하지 않도록(R-4). 운영자 CLI 는 항상 억제 플래그를 보낸다. 기본값은 현행 유지(전략 모듈 무변경, 하위호환).
코어가 reply-to 큐로 보내는 응답 envelope:
```
{ "assets": [ # always a list (REQ-028 AC-1, never null)
{ "symbol": str,
"asset_id": str, # == portfolio_id (dcy-004 OQ-1)
"cash_balance": number,
"asset_quantity": number,
"active_strategy_id": str | null,
"strategy_params": object, # opaque, pass-through
"cash_estimated": true? # dcy-018/OQ-4 — cash_balance 가 미정산 추정값일 때만(라벨)
}, ...
코어가 reply-to 큐로 보내는 응답 payload:
```json
{
"slots": [
{ "slot_id": "str (UUID)",
"status": "inactive | pending_buy | pending_sell | holding | error",
"symbol": "str | null",
"qty": "float | null", // slot mirror (REQ-062)
"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", // qty != asset_quantity 시 true (REQ-062)
"watchdog": { // null if watchdog store unavailable
"expired": "bool",
"last_seen": "ISO-8601 | null"
},
"paused": "bool"
}
],
"view": "all" | "buyable" | "held", # echo
"available_slots": int, # admission headroom = max(0, M − active); M 미설정 시 inactive 수 (REQ-059)
"degraded": bool, # REQ-028 AC-2
"reason": "portfolio_hub_unavailable" | "snapshot_fetch_failed"
| "cash_position_stale" | null,
"message": str | null # sanitized — no credentials/raw exceptions
"degraded": "bool",
"reason": "portfolio_hub_unavailable | snapshot_fetch_failed | cash_position_stale | null",
"message": "str | null" // sanitized — no credentials/raw exceptions
}
```
**No `price` field on `assets[]` items** (REQ-028 AC-3 / REQ-027 AC-1) — strategy-facing universe.
- 슬롯 상태 (REQ-028 AC-4, BAT-022): envelope 은 strategy `assets[]`(universe) 와 **별도로** 데몬의 per-slot 상태를 운반한다. 운영자용(`trex-core slots`)이며 universe 와 명확히 구분된다.
```
"slots": [ # always a list (REQ-028 AC-4, never null)
{ "slot_id": str, # buy intent 의 canonical key (REQ-061)
"status": "inactive"|"pending_buy"|"holding"|"pending_sell"|"error",
"symbol": str | null,
"qty": number | null,
"avg_price": number | null, # 매수 체결가/평단
"entry_price": number | null, # pending 주문 지정가 (dcy-012 외부 호환명; 내부 limit_price)
"allocated_capital": number | null,
"strategy_id": str | null, "sell_strategy_id": str | null,
"portfolio_id": str | null, # == asset_id (영구 자산 바인딩)
"forced_close_at": "ISO-8601" | null,
"universe_detached": bool } # soft-retire/tombstone 마커 (REQ-058/063)
]
```
- BAT-007 stub 경로와 BAT-008 envelope 경로가 **동일 `slots[]` 스키마**(`portfolio.slot_to_dict`)를 낸다. `assets[]`(portfolio 단위, universe, no-price) 와 독립이며 strategy 소비자는 무시 가능(additive, 하위호환). `slots[]` 는 operator-facing 이라 `entry_price`/`avg_price` 가격을 노출한다(no-price 규칙은 `assets[]` 한정).
**필드 소스 테이블** (REQ-074 AC-1):
- 워치독 상태 (dcy-017/REQ-034, 선택 필드): 응답에 holding 슬롯의 워치독 상태를 read-only 로 동봉한다.
```
"watchdog": [ # best-effort; 실패 시 생략(NFR-1)
{ "slot_id": str, "portfolio_id": str | null, "symbol": str | null,
"expired": bool, # true => 전략 이탈(안전망 소유)
"last_seen": "ISO-8601" | null } # 마지막 스냅샷 폴링 시각
]
```
- `trex-core slots` 가 `expired=true` 를 **WATCHDOG-OWNED(전략 이탈)** 로 표시한다. 운영자 조회(`actor="operator"`/`touch_watchdog=false`)는 이 노출이 워치독을 리셋하지 않는다(비침습, dcy-016).
| 필드 | 권위 소스 | 비고 |
|---|---|---|
| `slot_id`, `status`, `avg_price`, `entry_price`, `forced_close_at`, `universe_detached` | slot (core) | 슬롯 row 직접 |
| `qty` | slot (core) | 슬롯 mirror — broker position |
| `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` | 파생 | `qty != asset_quantity` (REQ-074 AC-2) |
| `watchdog` | WatchdogStore | 만료 여부 + last_seen; null=store 미연결 |
| `paused` | PauseStore | durable pause overlay (REQ-032) |
- 워치독 신호 (REQ-034): `trex.snapshot.req` 처리는 read-only 응답 산출에 더해, 응답에 포함된 모든 `holding` 슬롯의 워치독 `last_seen_at` 을 갱신한다. 즉 전략 모듈이 스냅샷을 폴링하는 것 자체가 해당 슬롯의 생존 신호이며, 폴링이 끊긴(워치독 만료) 슬롯만 safety-sell 트리거 대상이 된다. (dcy-016: `touch_watchdog=false`/`actor="operator"` 요청은 이 갱신을 건너뛴다 — 운영자 조회 제외.)
- **strategy 필드 없음** (REQ-074 AC-5): `strategy_id`, `sell_strategy_id`, `strategy_params`, `active_strategy_id` 는 응답에 포함되지 않는다.
- `spy=false`(기본): holding 슬롯 응답 후 `WatchdogStore.update(slot_id)` 호출 — 전략 폴링 = 생존 신호.
- `spy=true`: watchdog 갱신 건너뜀 — 운영자 CLI(`universe`/`slots`) 가 항상 사용.
### 운영자 CLI ↔ RPC 매핑 (dcy-016 / REQ-032)
@@ -209,8 +133,8 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason
| CLI | RPC 큐 | payload | 비고 |
|---|---|---|---|
| `universe` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | 자산 목록 조회(워치독 억제) |
| `slots [--status all\|holding\|buyable\|pending]` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | envelope `slots[]`(실제 슬롯 상태) 표시(REQ-028 AC-4/REQ-032, BAT-022) — universe `assets[]` 아님. `--status` 는 슬롯 status 기준 client-side 필터 |
| `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) |
@@ -251,8 +175,8 @@ resume_kill_switch 요청·응답 (REQ-046):
다중 인스턴스 운영 시 큐명과 라우팅을 `instance_key` 로 분리한다(dcy-009).
- **큐명 규칙**: `{base}.{instance_key}`(예: `trex.intent.req.inst-1`). `instance_key = "default"` 는 기존 리터럴 큐명(`trex.intent.req` 등)을 그대로 사용한다(하위호환, FR-5).
- **`trex.direct`(topic exchange)**: 특정 인스턴스 지정 전달. routing key = `{instance_key}.{channel_type}`(`channel_type ∈ {intent, snapshot, control}`). 각 인스턴스 큐가 자신의 routing key 로 바인딩 → `inst-1.intent` publish 는 `trex.intent.req.inst-1` 에만 도달.
- **큐명 규칙**: `{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.intent` publish 는 `trex.intent.req.inst-1` 에만 도달.
- **`trex.broadcast`(fanout exchange)**: 전역 제어. 각 인스턴스의 **control 큐가 dual 바인딩**되어, broadcast publish 는 모든 인스턴스 control 큐에 동시 전달(global kill-switch 등). 중복 전달은 핸들러 idempotency(REQ-036/advisory lock)로 무해.
- **기동**: 데몬은 `--config <path>.toml`(필수)로 `instance_key`/`portfolio_hub_token` 을 읽는다(`.env` 아님, REQ-048).
@@ -524,4 +448,6 @@ resume_kill_switch 요청·응답 (REQ-046):
- 2026-06-05 :: BAT-007 — HTTP `/v1` API 인터페이스 제거(adapters/api + fastapi/uvicorn 의존 정리), RabbitMQ messaging 계약 추가(스냅샷 RPC `trex.snapshot.req`, intent RPC `trex.intent.req`, 이벤트 topic `trex.events`, 전략별 durable queue + TTL/max-length, DLQ, publisher confirms, ack-after-commit), `MessagingTransport` Protocol + `TransportHealth` 노출 (REQ-021~026).
- 2026-06-06 :: BAT-008 — `trex.snapshot.req` envelope 계약 정의(`assets[]/view/available_slots/degraded/reason/message`, no price field), `trex.pause.req` 제어 RPC 추가, `PortfolioHubReadPort`/`SnapshotStore`/`PauseStore`/`SnapshotScheduler` 인터페이스, `BrokerPort.get_balance()` 확장, `trex-core` CLI 추가 (REQ-027~033; REQ-030 옵션 D 는 portfolio-hub#8 배포 후).
- 2026-06-10 :: BAT-030 (dcy-020 마감) — buy intent 계약 키 `slot_id` 정본(+`portfolio_id` dual-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-11 :: BAT-035 (dcy-025) — `trex.snapshot.req` → `trex.slots.req` lockstep 교체: 요청 `{filter, spy}`, 응답 `{slots[], degraded, reason, message}`. 슬롯 row 에 hub `asset_quantity`/`cash_balance` + `warned`/`cash_estimated`/`watchdog`/`paused` 병합(REQ-073/074). `trex.snapshot.req` envelope(`assets[]`/`available_slots`/view enum) 제거(REQ-028 Deprecated). strategy 필드 제거(REQ-074 AC-5). CLI `universe`/`slots` → `trex.slots.req`(spy=true). `trex.direct` channel_type `snapshot` → `slots`.
- 2026-06-11 :: BAT-036 (dcy-024) — `safety_sell_enabled: bool=True` per-instance TOML 플래그 추가(REQ-048 CHG-01). `false` 시 `trex-safety-sell` worker 미기동 + WARNING 로그(REQ-005 CHG-01).
- 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)