feat(strategy-state): persist instance-scoped slot maps
This commit is contained in:
@@ -74,11 +74,14 @@
|
||||
```
|
||||
{ "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": 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:
|
||||
@@ -524,21 +527,21 @@ get_current_price(symbol: str, market: str = "J") -> Optional[Decimal]
|
||||
- pika BlockingConnection 은 thread-safe 가 아니므로 consumer/publisher 마다 자기 connection/channel 을 가져야 한다.
|
||||
- 토폴로지 선언은 멱등하지만 attribute(durable, args) 가 기존 큐와 다르면 RabbitMQ 가 PRECONDITION_FAILED 로 거부 → batch 마이그레이션 시 큐 재선언 절차 필요.
|
||||
|
||||
## Interface: `trex.slots.strategy_state.set` RPC (BAT-042 / dcy-028, REQ-080)
|
||||
## 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-state 를 occupancy_seq+version CAS 로 write. read 는 `trex.slots.req` 에 piggyback(별도 RPC 아님).
|
||||
- Purpose: 슬롯별 opaque strategy-instance state entry를 version CAS로 write. read는 `trex.slots.req`에 piggyback(별도 RPC 아님).
|
||||
|
||||
### Inputs
|
||||
- `{slot_id, strategy_id, expected_version:int, expected_occupancy_seq:int, data:str}`. `correlation_id = "{slot_id}:{strategy_id}:{expected_version}"`(멱등), delivery persistent. `data` 는 opaque UTF-8(core 무해석), byte size cap(기본 4KB ceiling, 운영 권장 1~2KB).
|
||||
- `{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_id, version:<new>}`.
|
||||
- 실패 reason 전체 집합: `occupancy_changed`(+`occupancy_seq`) / `version_conflict`(+`version`) / `slot_inactive` / `state_too_large` / `bad_request:<detail>`. 소비자는 미인식 reason 에 fail-closed.
|
||||
- 성공: `{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
|
||||
- read 표면(`trex.slots.req`)의 각 슬롯 행에 additive 로 `occupancy_seq`(int)와, 보유 시 `strategy_state` 맵(`{<strategy_id>:{version,data,updated_at}}`)이 실린다(REQ-073/074 CHG). 기존 소비자는 unknown 필드 무시로 하위호환. `strategy_state` 는 strategy_* reasoning 필드(strategy_id 등, REQ-074 AC-5 제외 대상)와 구분되는 opaque 상태 맵이다.
|
||||
- slot strategy-state는 occupancy와 독립된 slot-scoped `data[strategy_instance_id] = {version,data,updated_at}` map이다. close/reopen은 entry를 삭제하거나 version을 초기화하지 않는다. Entry에는 `strategy_id`나 `occupancy_seq` metadata가 없으며, Slot 자체의 `occupancy_seq` semantics는 별도로 유지된다.
|
||||
|
||||
## Shared Constraints
|
||||
|
||||
|
||||
+11
-12
@@ -34,11 +34,12 @@
|
||||
|
||||
1. `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).
|
||||
2. setup: `apply_migrations(dsn)`(멱등; 010 inactive CHECK·011 컬럼 drop 포함) → `execution.reconcile()`(REQ-014) → 이벤트 publisher 의 `declare_topology()`. (일일 preallocation 은 BAT-030 에서 폐기 — 슬롯 생성은 universe reconcile 이 담당.)
|
||||
3. start: 7개 백그라운드 스레드 동시 기동
|
||||
3. start: 8개 백그라운드 스레드 동시 기동
|
||||
- `trex-execution` :: 브로커 dispatch/poll 루프 (REQ-013/014; 매도 수량 브로커 정본 REQ-062). **각 틱 시작 시 `event_publisher.keepalive_tick()` 호출** (REQ-092 — pika 스레드 안전 규칙: 발행 연결 소유 스레드에서만 I/O). 틱 간격(기본 1s) < heartbeat/2(15s) → 서버 측 idle 종료 예방.
|
||||
- `trex-ledger` :: ledger outbox 드레인 (REQ-019)
|
||||
- `trex-intent-rpc` :: `trex.intent.req` 소비자 (prefetch=1, FIFO, dedup via intent_log; `update_thresholds` 액션 포함 REQ-040)
|
||||
- `trex-slots-rpc` :: `trex.slots.req` 소비자 (spy=false 시 holding 슬롯 워치독 `last_seen_at` 갱신 REQ-034; `safety_sell_enabled=false` 시 `trex-safety-sell` 미기동 BAT-036)
|
||||
- `trex-slots-rpc` :: `trex.slots.req` 소비자 (spy=false 시 holding 슬롯 워치독 `last_seen_at` 갱신 REQ-034; `safety_sell_enabled=false` 시 `trex-safety-sell` 미기동 BAT-036)
|
||||
- `trex-strategy-state-set` :: `trex.slots.strategy_state.set` 소비자 (slot-scoped strategy-instance state version CAS)
|
||||
- `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)
|
||||
@@ -138,7 +139,7 @@
|
||||
|
||||
`trex.slots.req` 메시지 처리 (`make_slots_rpc_handler`).
|
||||
|
||||
1. 요청 payload 디코드: `{filter ∈ {all, inactive, pending, holding, error}, spy: bool}`.
|
||||
1. 요청 payload 디코드: `{filter ∈ {all, inactive, pending, holding, error}, spy: bool, strategy_instance_id?: str}`. `spy=false` 전략 요청은 non-empty string `strategy_instance_id`가 필수이며, malformed 값은 slots/state 노출과 watchdog 갱신 없이 `bad_request:strategy_instance_id` 기존 envelope으로 종료한다. `spy=true` 운영자 요청은 ID를 요구하거나 supplied ID로 결과를 필터하지 않는다.
|
||||
2. `slot_repo.list_slots(instance_key)` → status 기준 서버 필터 적용(`inactive`/`pending`/`holding`/`error`; 미인식 = `all`).
|
||||
3. `spy=false` 면 `slot_repo.list_holding_slots()` 의 모든 holding 슬롯에 `WatchdogStore.update(slot_id)`.
|
||||
4. 각 슬롯에 `slot_to_dict(s, snapshot_store=..., watchdog_store=..., pause_store=..., watchdog_timeout_sec=...)` 적용 후 broker holdings snapshot 오버레이를 적용해 병합 row 산출:
|
||||
@@ -149,7 +150,7 @@
|
||||
- 추정 현금(REQ-029 OQ-4): `cash_estimated=True` + `cash_balance += pending ledger delta`(best-effort)
|
||||
- 워치독: `watchdog: {expired, last_seen}` inline (WatchdogStore)
|
||||
- 오버레이: `paused` (PauseStore, REQ-032)
|
||||
- strategy 필드 없음 (REQ-074 AC-5)
|
||||
- strategy-state: `spy=false`면 요청 `strategy_instance_id`와 정확히 일치하는 `{version,data,updated_at}` entry 하나만(없으면 생략), `spy=true`면 `{strategy_instance_id: {version,data,updated_at}}` 전체 map
|
||||
5. degraded 상태 동봉: `SnapshotStore.degraded_state()` → `{degraded, reason, message}`.
|
||||
6. 응답: `{slots[], degraded, reason, message}` — `slots` 는 항상 배열, null 금지.
|
||||
|
||||
@@ -346,17 +347,15 @@ timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분:
|
||||
6. slot_transition_log 에 전이 기록 1행을 append 하고 갱신된 슬롯을 반환한다.
|
||||
7. 외부 I/O(브로커 등)는 lock 밖에서 호출자가 수행한다(lock 보유 중 금지).
|
||||
|
||||
## Flow: `slot strategy-state 저장소` (BAT-042 / dcy-028, REQ-079~082)
|
||||
## Flow: `slot strategy-state 저장소` (REQ-0009)
|
||||
|
||||
전략 데몬(예: trex-tsl)이 슬롯 점유 동안 누적하는 opaque 상태(예: TSL peak)를 슬롯 수명에 묶어 server-side 로 보관한다. core 는 `data` 를 해석하지 않는다(opaque UTF-8 + byte size cap 만 검증).
|
||||
전략 상태는 슬롯 점유와 독립된 opaque 상태다. core는 inner `data`를 해석하지 않고 UTF-8 byte size cap만 검증하며, 하나의 슬롯별 map `data[strategy_instance_id] = {version,data,updated_at}`를 보관한다.
|
||||
|
||||
1. **read (piggyback, C-1)**: 데몬이 매 사이클 `trex.slots.req` 로 슬롯을 읽으면 각 행에 `occupancy_seq`(int)와, 보유 시 `strategy_state` 맵(`{<strategy_id>: {version,data,updated_at}}`)이 함께 실린다. 맵은 핸들러가 반환 slot_id 들로 `StrategyStateStore.get_for_slots()` 1회 batched 조회 후 group by 해 조립한다(추가 왕복 없음). 슬롯 현재 occupancy_seq 와 다른 stale 엔트리는 노출하지 않는다.
|
||||
2. **evaluate (pure)**: 데몬은 행의 `strategy_state[strategy_id]` 를 read-only 입력으로 `evaluate()` 에 넣는다(상태 write 는 evaluate 밖, 엔진이 수행).
|
||||
3. **write (별도 RPC, C-2)**: 상태가 바뀌면 `trex.slots.strategy_state.set` 으로 `{slot_id, strategy_id, expected_version, expected_occupancy_seq, data}` 를 보낸다. core 는 슬롯의 현재 occupancy_seq·active 여부를 조회해 CAS 검증: 점유 불일치 → `occupancy_changed`, version 불일치 → `version_conflict`, inactive/미존재 → `slot_inactive`, size 초과 → `state_too_large`, 일치(없으면 expected_version=0) → commit + version+1. malformed → `bad_request`. 동일 입력 재전송은 무부작용.
|
||||
4. **occupancy 가드 (C-2b, R-1)**: 슬롯은 monotonic `occupancy_seq` 를 갖고 새 점유 시작(`inactive→pending_buy`)마다 ++. 영구 슬롯 재사용 시 read v0→close→reopen→write v0 의 ABA(옛 점유 계산이 새 점유에 바인딩)를 occupancy_seq CAS 가 구조적으로 차단한다.
|
||||
5. **close 삭제 (C-3)**: 슬롯이 어떤 경로로든 `→INACTIVE`(전략 매도/safety-sell/forced-close/operator) 되면 `transit_slot` 단일 전이 지점에서 해당 slot_id 의 모든 strategy_state 를 삭제한다. 재사용 슬롯은 새 occupancy_seq + 빈 상태로 시작 — 전략/데몬은 cleanup 책임이 없다(슬롯이 source of truth + GC).
|
||||
1. **read (piggyback)**: `spy=false` 전략 요청은 required non-empty `strategy_instance_id`의 exact entry만 각 slot 행의 single `strategy_state` object로 받으며, 없으면 field가 생략된다. 다른 ID나 전체 map은 노출하지 않는다. `spy=true` 운영자 요청은 ID 없이도 각 slot의 complete instance map을 받고 watchdog을 갱신하지 않는다.
|
||||
2. **write (별도 RPC)**: `trex.slots.strategy_state.set` payload는 `{slot_id, strategy_instance_id, expected_version, data}`다. active slot, size limit, 요청 entry version만 CAS한다. entry가 없으면 `expected_version=0`, 성공하면 그 entry version만 증가한다. `version_conflict`, `slot_inactive`, `state_too_large`, `bad_request:*`는 write를 적용하지 않는다.
|
||||
3. **lifecycle/concurrency**: `→INACTIVE`와 재점유는 map을 삭제하거나 숨기지 않으며 version도 초기화하지 않는다. Slot 자체의 `occupancy_seq`는 기존대로 유지하지만 strategy state persistence/read/write에는 사용하지 않는다. 서로 다른 instance entry의 concurrent 성공 write는 모두 보존된다.
|
||||
|
||||
저장은 별도 `slot_strategy_state` 테이블(복합 PK `(slot_id, strategy_id)`), 슬롯 본체에는 `occupancy_seq` 컬럼 1개만 추가(full 3계층 분할은 후속 Discovery). full 3계층(occupancy 테이블 분할)·다중 writer 동시성·장기 보존은 범위 밖.
|
||||
저장은 별도 `slot_strategy_state` 테이블의 slot-scoped JSON map이며 entry에는 identity metadata를 중복 저장하지 않는다. core-side instance registry, legacy mapping/backfill, caller 인증은 없다.
|
||||
|
||||
## Flow Constraints
|
||||
|
||||
|
||||
Reference in New Issue
Block a user