fix(daemon): buy 심볼 resolver 결함 수정 + REL-004 draft (운영자 run 증거)
REL-004(미릴리즈 BAT-014~021 통합) draft-release 작성. confirm-release 전 운영자 프로그램 run-the-app 검증(실 데몬 + trex-core CLI e2e)을 수행했고, 369 passed 가 놓친 실 프로덕션 버그를 검출·수정했다. - fix(REQ-007 CHG-20260608-03): Daemon._resolve_symbol 이 SnapshotAsset.symbol (없는 속성)을 접근 → 데몬 buy admission 매 요청 AttributeError → intent RPC 타임아웃(실 데몬 매수 전면 불능). asset.item.symbol 로 수정. 단위 테스트가 stub resolver 를 주입해 실 배선을 한 번도 실행하지 않아 미검출이었음 - test: tests/runtime/test_daemon_resolve_symbol.py — 실 Daemon._resolve_symbol × 실 SnapshotStore 회귀 가드(수정 전 실패하는 테스트) - docs(release): rel-004 draft(app-service, BAT-014~021) + Pre-Release Run Evidence(데몬 기동 + universe/slots/enter/exit e2e + ledger completed + 버그 검출·수정 + LIVE 미검증 한계) + releases/index REL-004 행 검증: 실 데몬 재기동 후 buy/sell e2e admitted→체결→closed + ledger completed, 단위 337 passed, ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,3 +11,4 @@
|
|||||||
| REL-001 | app-service | feedback-captured | BAT-001~008 | Discovery: orb-v1 전략 모듈, 부분 매도(#5) / Change-req: REQ-030 옵션 D, 전략 필드 실데이터, 이벤트 publisher idle 견고성, paper execution_id Redis(#4) | [rel-001](rel-001_20260606_trex-core-slot-core-baseline.md) |
|
| REL-001 | app-service | feedback-captured | BAT-001~008 | Discovery: orb-v1 전략 모듈, 부분 매도(#5) / Change-req: REQ-030 옵션 D, 전략 필드 실데이터, 이벤트 publisher idle 견고성, paper execution_id Redis(#4) | [rel-001](rel-001_20260606_trex-core-slot-core-baseline.md) |
|
||||||
| REL-002 | tooling | released | BAT-009 | Change-req: paper execution_id Redis(#4) | [rel-002](rel-002_20260606_paper-broker-env-tuning.md) |
|
| REL-002 | tooling | released | BAT-009 | Change-req: paper execution_id Redis(#4) | [rel-002](rel-002_20260606_paper-broker-env-tuning.md) |
|
||||||
| REL-003 | app-service | released | BAT-010, BAT-011, BAT-012, BAT-013 | dcy-007 (자율 안전 매도) + dcy-008 (kill-switch) + dcy-009 (다중 인스턴스 토폴로지) — 통합 릴리즈 | [rel-003](rel-003_20260607_safety-killswitch-multi-instance.md) |
|
| REL-003 | app-service | released | BAT-010, BAT-011, BAT-012, BAT-013 | dcy-007 (자율 안전 매도) + dcy-008 (kill-switch) + dcy-009 (다중 인스턴스 토폴로지) — 통합 릴리즈 | [rel-003](rel-003_20260607_safety-killswitch-multi-instance.md) |
|
||||||
|
| REL-004 | app-service | draft | BAT-014, BAT-015, BAT-016, BAT-017, BAT-018, BAT-019, BAT-020, BAT-021 | dcy-010 (safety-sell 쿨다운) + dcy-011~017 (buy/sell 계약·KIS 취소·trading_date TZ·ledger 백오프 버그픽스 + 운영자 CLI + 워치독 가시화) — 통합 릴리즈 | [rel-004](rel-004_20260608_safety-cooldown-bugfix-hardening-cli.md) |
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# REL-004: safety-sell 쿨다운 + 운영 버그픽스/하드닝 + 운영자 CLI 통합 릴리즈
|
||||||
|
|
||||||
|
- Status: draft
|
||||||
|
- Profile: app-service
|
||||||
|
- Included Batch: BAT-014, BAT-015, BAT-016, BAT-017, BAT-018, BAT-019, BAT-020, BAT-021
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Included:
|
||||||
|
- BAT-014 (dcy-010 / REQ-052~054): safety-sell per-slot 재시도 쿨다운 + 영속 거부 경고
|
||||||
|
- BAT-015 (dcy-011 / REQ-007·018·021 change-req + REQ-055): buy intent portfolio_id 필수화 + symbol 서버 resolve + ledger skip 가시화 + closed↔outbox anti-join 점검
|
||||||
|
- BAT-016 (dcy-012 / REQ-002·003·006·007·013·015·036 change-req): 매도 가격 버그 정정 — 슬롯 `entry_price`→`limit_price` rename(+마이그레이션 006) + dispatch 단일 필드 + sell optional 가격
|
||||||
|
- BAT-017 (dcy-013 / REQ-012·013·014·015·018·031 change-req): KIS 외부 취소 감지(cncl_yn) + 부분 체결 회계 + pending stale 워치독(Alert-only)
|
||||||
|
- BAT-018 (dcy-014 / REQ-005·009·048 change-req): trading_date 설정 타임존(KST) 분리 + 일자 경계 일일 preallocation
|
||||||
|
- BAT-019 (dcy-015 / REQ-017·018·019·020 change-req): ledger outbox 영속 오류 분류 + 지수 백오프/dead-letter(+마이그레이션 007)
|
||||||
|
- BAT-020 (dcy-016 / REQ-021·022·026·032·034 change-req): 운영자 CLI 확장(universe/slots/enter/exit, 데몬 RPC 통일) + 워치독 억제 읽기
|
||||||
|
- BAT-021 (dcy-017 / REQ-021·032·034·037·038·039·042 change-req): 워치독 소유권 가시화(snapshot 워치독 상태 + safety-sell 발동 로깅)
|
||||||
|
- Excluded:
|
||||||
|
- BAT-001~013 (REL-001/002/003 에서 이미 released)
|
||||||
|
- dcy-018+ 후속 주제, Redis projection·push 알림(dcy-017 OQ-1 미채택), dead-letter 재처리 트리거(dcy-015 후속)
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
1. 배포 전: 대상 인스턴스 데몬 정지(또는 거래 세션 외 시간), 현재 리비전 SHA 기록.
|
||||||
|
2. DB 마이그레이션 적용(멱등): `006_rename_entry_price_to_limit_price.sql`(daily_slot 컬럼 rename) + `007_ledger_outbox_delivery_failed_and_backoff.sql`(status enum/next_retry_at). 데몬 기동 시 `apply_migrations` 가 자동 적용하나, 운영 `trex_core` DB 에 최초 적용되므로 사전 백업 권장.
|
||||||
|
3. 신 리비전(`6d7bca1` 또는 그 이후 main HEAD) 배포 + 데몬 재기동(`--config <inst>.toml`). 기동 로그에서 RUNNING/비-degraded 확인.
|
||||||
|
4. 스냅샷 RPC degraded=false·available_slots·asset_count 확인. trading_date 가 KST 당일과 일치하는지 확인.
|
||||||
|
5. 운영자 CLI 스모크: `trex-core universe`/`slots`(워치독 상태 표시) — paper 또는 비거래 시간.
|
||||||
|
6. 거래 세션 재개.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
- 조건: 기동 실패, 마이그레이션 오류, buy/sell admission·집행 회귀, ledger 전송 비정상, safety-sell 오작동.
|
||||||
|
- 절차: 신 리비전 → 직전 released 리비전(REL-003 시점)으로 롤백 + 데몬 재기동.
|
||||||
|
- 주의: 마이그레이션 006(컬럼 rename)은 비가역적 rename 이다 — 롤백 시 구 코드는 `entry_price` 컬럼을 기대하므로, 코드 롤백과 함께 역마이그레이션(`limit_price`→`entry_price` rename) 또는 컬럼 alias 가 필요하다. 007 은 additive(컬럼/enum 추가)라 구 코드와 공존 가능. 배포 전 역마이그레이션 스크립트를 준비한다.
|
||||||
|
- 데이터: 마이그레이션은 순수 rename/additive 라 행 데이터 보존. ledger_outbox `delivery_failed` 행은 롤백 후 구 코드가 모르는 status 이므로 list_pending 에서 자연 제외(무해).
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
- 배포 전:
|
||||||
|
- 전체 인프라 게이트 테스트 green (PG `trex_core_test` + RabbitMQ): `369 passed` (최신 main).
|
||||||
|
- `ruff check trex_core tests` All passed.
|
||||||
|
- 마이그레이션 006/007 idempotent 재적용 확인(컬럼 계약 가드 green).
|
||||||
|
- 역마이그레이션(006 rename 되돌리기) 스크립트 준비.
|
||||||
|
- 배포 후:
|
||||||
|
- 데몬 RUNNING + 비-degraded, 8개 백그라운드 스레드 기동(`trex-preallocation` 포함).
|
||||||
|
- trading_date == KST 당일, 슬롯 preallocation 정상.
|
||||||
|
- 스냅샷 RPC envelope(assets/available_slots/degraded) 정상 + `watchdog` 필드 노출.
|
||||||
|
- `trex-core universe`/`slots` 응답 정상(워치독 소유 표시).
|
||||||
|
- (가능 시) paper buy/sell 1회 e2e: portfolio_id 기반 admission → 체결 → ledger 기록.
|
||||||
|
|
||||||
|
## Pre-Release Run Evidence (operator-facing smoke)
|
||||||
|
|
||||||
|
confirm-release 전 "운영자가 쓰는 프로그램이 실제로 동작한다"는 run-the-app 증거. 테스트 통과(369 passed)와 별개로 실 데몬 기동 + `trex-core` CLI 구동을 검증했다.
|
||||||
|
|
||||||
|
- 환경: paper 브로커 + 테스트 PostgreSQL(`trex_core_test`) + 테스트 RabbitMQ, portfolio-hub stub(universe 2종), `--config <inst.toml>`(instance_key=`rel004-smoke`), revision = main HEAD(수정 후).
|
||||||
|
- 데몬 기동: `trex-core-daemon --config ...` → 마이그레이션 적용(006/007 포함), RabbitMQ 연결, snapshot scheduler boot(stub universe swap), preallocation, 8 스레드 기동, RUNNING. 기동 후 에러 로그 없음.
|
||||||
|
- 운영자 CLI(데몬 RPC 통일) 구동 결과:
|
||||||
|
- `trex-core universe` → snapshot RPC, assets 출력, degraded=false (BAT-020).
|
||||||
|
- `trex-core slots` / `slots --status held --json` → 보유 슬롯 + `watchdog` 필드(slot_id/expired/last_seen) 노출 (BAT-021/dcy-016).
|
||||||
|
- `trex-core enter pf-005930 --capital ... --yes` → `status=admitted slot_id=...` → paper 체결 → holding (BAT-015/018).
|
||||||
|
- `trex-core enter ... --dry-run` → 미제출 미리보기(안전장치), `--json`+enter 는 `--yes` 강제 (BAT-020).
|
||||||
|
- `trex-core exit <slot_id> --yes` → `status=admitted` → 시장가 매도 체결 → closed(held count 0) (BAT-016).
|
||||||
|
- `ledger_outbox`: buy/sell 모두 `status=completed`(outbox durability + writer drain, BAT-019 경로 정상).
|
||||||
|
- **검출·수정한 결함(이 run 이 아니었으면 release 에 포함됐을 버그)**: `Daemon._resolve_symbol` 이 `SnapshotAsset.symbol`(존재하지 않음)을 접근해 데몬 buy admission 이 매 요청 `AttributeError` → intent RPC 타임아웃. **실 데몬 buy 가 전면 불능**이었으나 단위 테스트는 stub resolver 를 주입해 미검출(369 passed). `asset.item.symbol` 로 수정 + 회귀 테스트(`tests/runtime/test_daemon_resolve_symbol.py`) 추가 → 재기동 후 buy/sell e2e 정상(REQ-007 CHG-20260608-03). 수정 후 단위 `337 passed`.
|
||||||
|
- 한계: 실계좌(LIVE KIS) 주문은 미검증(paper 한정) — LIVE full-stack 은 운영자 배포 evidence(아래)에서 확인.
|
||||||
|
|
||||||
|
## Release Execution Evidence
|
||||||
|
- Release Status Transition Target: released
|
||||||
|
|
||||||
|
### Deployment Facts
|
||||||
|
- Executed At:
|
||||||
|
- <배포 시작 시각>
|
||||||
|
- Completed At:
|
||||||
|
- <배포 완료 시각>
|
||||||
|
- Environment:
|
||||||
|
- <배포 대상 환경 (예: 운영 trex_core DB + 대상 instance_key)>
|
||||||
|
- Operator:
|
||||||
|
- <배포 수행자>
|
||||||
|
- Revision or Artifact:
|
||||||
|
- <커밋 SHA (main HEAD)>
|
||||||
|
- Commands or Procedure:
|
||||||
|
- <마이그레이션 적용 + 데몬 재기동 절차 요약>
|
||||||
|
|
||||||
|
### Profile-Specific Checks
|
||||||
|
- app-service:
|
||||||
|
- Health Endpoint:
|
||||||
|
- <스냅샷 RPC degraded 상태/available_slots/asset_count + 확인 시각>
|
||||||
|
- Startup Log Fields:
|
||||||
|
- <기동 로그 RUNNING/instance_key/8 스레드 확인>
|
||||||
|
- Request Log Fields:
|
||||||
|
- <correlation_id 포함 RPC 처리 로그 확인>
|
||||||
|
- Smoke or Manual Verification:
|
||||||
|
- <CLI universe/slots + (가능 시) paper buy/sell e2e 결과>
|
||||||
|
|
||||||
|
### Rollback Outcome
|
||||||
|
- Rollback Required:
|
||||||
|
- <yes 또는 no>
|
||||||
|
- Rollback Trigger:
|
||||||
|
- <발생 시 원인, 없으면 none>
|
||||||
|
- Rollback Action:
|
||||||
|
- <수행한 조치, 없으면 none>
|
||||||
|
|
||||||
|
### Release Decision Evidence
|
||||||
|
- Released Decision:
|
||||||
|
- <released 전환 여부>
|
||||||
|
- Rationale:
|
||||||
|
- <released 전환 판단 근거>
|
||||||
|
- Recorded By:
|
||||||
|
- <기록자>
|
||||||
|
- Recorded At:
|
||||||
|
- <기록 시각>
|
||||||
|
|
||||||
|
## Feedback Handoff
|
||||||
|
- Observation Summary:
|
||||||
|
- <운영 관찰 사실 요약 또는 없음>
|
||||||
|
- Discovery Input:
|
||||||
|
- <후속 discovery 후보 (예: WatchdogStore 영속 승격, Redis projection/push 알림, dead-letter 재처리 트리거)>
|
||||||
|
- REQ Input:
|
||||||
|
- <후속 req 후보>
|
||||||
|
- Change Request Input:
|
||||||
|
- <기존 req 변경 후보 (예: buy/sell 외부 payload entry_price 필드명 rename, snapshot envelope slot_id 노출)>
|
||||||
@@ -235,3 +235,41 @@
|
|||||||
#### Validation Plan
|
#### Validation Plan
|
||||||
|
|
||||||
- BAT-016: 가격 전달 매도 → 지정가 / 미전달 → 시장가 단위 + 핸들러 payload.
|
- BAT-016: 가격 전달 매도 → 지정가 / 미전달 → 시장가 단위 + 핸들러 payload.
|
||||||
|
|
||||||
|
### CHG-20260608-03
|
||||||
|
|
||||||
|
- Date: 2026-06-08
|
||||||
|
- Author: Justin Ko
|
||||||
|
|
||||||
|
#### Change Summary
|
||||||
|
|
||||||
|
- (change-req, REL-004 사전 run 검증에서 발견된 결함 수정) 데몬 buy 심볼 resolver 버그 정정: `Daemon._resolve_symbol` 이 `SnapshotStore.get()` 반환값(`SnapshotAsset`)에서 `asset.symbol` 을 접근했으나 symbol 은 `asset.item.symbol` 에 있어 `AttributeError` → 데몬 buy admission 이 매번 크래시(intent RPC 타임아웃). `asset.item.symbol` 로 수정. 단위 테스트가 stub resolver 를 주입해 실 데몬 배선을 한 번도 실행하지 않아 369 passed 에도 누락됐다 — 실 데몬 기동 + CLI e2e(REL-004 smoke)에서 검출. Implemented 유지.
|
||||||
|
|
||||||
|
#### Intent
|
||||||
|
|
||||||
|
- dcy-011/REQ-007 의 buy 심볼 서버 resolve 가 실 런타임(daemon + SnapshotStore)에서 동작하도록 배선 결함 수정 + 회귀 가드 추가.
|
||||||
|
|
||||||
|
#### Acceptance Criteria Delta
|
||||||
|
|
||||||
|
- (변경 없음 — AC 동일; 배선 결함 수정. 실 데몬 resolver 가 SnapshotAsset.item.symbol 을 읽음)
|
||||||
|
|
||||||
|
#### Impacted Area Delta
|
||||||
|
|
||||||
|
- Modules: `trex_core/runtime/daemon.py`(`_resolve_symbol` → `asset.item.symbol`)
|
||||||
|
- Tests: tests/runtime/test_daemon_resolve_symbol.py(실 Daemon._resolve_symbol × 실 SnapshotStore 회귀)
|
||||||
|
|
||||||
|
#### Delivery Trace Delta
|
||||||
|
|
||||||
|
- Batches: BAT-015
|
||||||
|
- Releases: REL-004 (사전 run 검증에서 검출·수정)
|
||||||
|
|
||||||
|
#### Revalidation Impact
|
||||||
|
|
||||||
|
- Reverification Needed: yes (REL-004 live smoke + 회귀 테스트로 완료)
|
||||||
|
- Existing Implementation Invalidated: yes (데몬 buy admission 경로 — 실 런타임에서 전면 실패였음)
|
||||||
|
- Status Recommendation: Implemented (배선 수정 + 회귀 가드)
|
||||||
|
- Follow-up Action: REL-004 confirm-release 전 live smoke 증거에 반영.
|
||||||
|
|
||||||
|
#### Validation Plan
|
||||||
|
|
||||||
|
- 실 데몬 기동(paper + test PG/RabbitMQ) + `trex-core enter pf-005930 --yes` → admitted/체결/ledger completed e2e + test_daemon_resolve_symbol 회귀.
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Daemon buy-symbol resolver regression (dcy-011/REQ-007, REL-004 smoke finding).
|
||||||
|
|
||||||
|
Exercises the REAL Daemon._resolve_symbol against a REAL SnapshotStore — the gap
|
||||||
|
that let `asset.symbol` (wrong; SnapshotStore.get returns a SnapshotAsset whose
|
||||||
|
symbol is on `.item.symbol`) ship: unit tests injected a stub resolver, so the
|
||||||
|
daemon wiring was never run until the REL-004 live smoke timed out every buy.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from trex_core.application.portfolio import PortfolioItem, SnapshotStore
|
||||||
|
from trex_core.runtime.daemon import Daemon
|
||||||
|
|
||||||
|
|
||||||
|
class _StoreHolder:
|
||||||
|
"""Minimal stand-in carrying just the attribute Daemon._resolve_symbol uses."""
|
||||||
|
|
||||||
|
def __init__(self, store):
|
||||||
|
self.snapshot_store = store
|
||||||
|
|
||||||
|
|
||||||
|
def _store(*items):
|
||||||
|
s = SnapshotStore()
|
||||||
|
s.swap(list(items))
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_symbol_reads_item_symbol():
|
||||||
|
store = _store(PortfolioItem(portfolio_id="pf-005930", symbol="005930",
|
||||||
|
cash_balance=0.0, asset_quantity=0.0))
|
||||||
|
holder = _StoreHolder(store)
|
||||||
|
# call the real method with a stub self (no full daemon construction needed)
|
||||||
|
assert Daemon._resolve_symbol(holder, "pf-005930") == "005930"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_symbol_unknown_returns_none():
|
||||||
|
holder = _StoreHolder(_store())
|
||||||
|
assert Daemon._resolve_symbol(holder, "pf-missing") is None
|
||||||
@@ -208,9 +208,12 @@ class Daemon:
|
|||||||
# ------------------------------------------------------------ buy symbol resolve
|
# ------------------------------------------------------------ buy symbol resolve
|
||||||
def _resolve_symbol(self, portfolio_id: str):
|
def _resolve_symbol(self, portfolio_id: str):
|
||||||
"""dcy-011 / REQ-007: resolve symbol from the universe snapshot for a
|
"""dcy-011 / REQ-007: resolve symbol from the universe snapshot for a
|
||||||
portfolio_id at buy admission (None → unknown_portfolio reject)."""
|
portfolio_id at buy admission (None → unknown_portfolio reject).
|
||||||
|
|
||||||
|
SnapshotStore.get returns a SnapshotAsset wrapping the PortfolioItem;
|
||||||
|
the symbol lives on `.item.symbol`."""
|
||||||
asset = self.snapshot_store.get(portfolio_id)
|
asset = self.snapshot_store.get(portfolio_id)
|
||||||
return asset.symbol if asset is not None else None
|
return asset.item.symbol if asset is not None else None
|
||||||
|
|
||||||
# ------------------------------------------------------------ event hook
|
# ------------------------------------------------------------ event hook
|
||||||
def _make_event_emitter(self):
|
def _make_event_emitter(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user