chore: commit current workspace changes
This commit is contained in:
@@ -1,35 +1,52 @@
|
||||
# BAT-006 Implementation
|
||||
|
||||
- Status: draft
|
||||
- Status: complete
|
||||
- Batch: BAT-006
|
||||
|
||||
## Plan Summary
|
||||
- LedgerPort 계약과 portfolio-hub/stub adapter 를 추가하고, execution fill 경로가 ledger outbox work item 을 적재하도록 연결한다. retry worker 와 dedup/outcome 처리를 구현한 뒤 baseline 문서를 실제 코드와 맞춘다.
|
||||
- LedgerPort 계약 + portfolio-hub HTTP 어댑터 + 결정론 stub 원장을 추가하고, 집행 루프 fill 전이 직후에 ledger outbox 에 work item 을 적재하도록 연결했다. `LedgerWriter` 가 outbox 를 drain 해 portfolio-hub write 를 시도하고, NEW/DUPLICATE_OK/conflict/retryable 결과를 `completed/duplicate_completed/retryable_failure/conflict_blocked` outcome 으로 분류한다. paper/KIS 브로커는 결정적 `execution_id` 와 `fee` 를 `FillStatus` 로 운반한다. baseline 4문서를 ledger 레이어로 정합시켰다.
|
||||
|
||||
## Changed Files
|
||||
- 예정:
|
||||
- `trex_core/application/*` — LedgerPort, ledger outbox service, execution fill hook.
|
||||
- `trex_core/adapters/ledger/*` — in-memory/stub ledger, portfolio-hub adapter.
|
||||
- `trex_core/adapters/broker/*` — FillStatus execution_id/fee 운반.
|
||||
- `trex_core/runtime/*` — portfolio-hub config, retry worker lifecycle.
|
||||
- persistence/migration 파일 — ledger outbox work item store.
|
||||
- `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/data-model.md`.
|
||||
- tests — ledger adapter, fill lifecycle, outbox retry/dedup/conflict, import sweep.
|
||||
- 신규
|
||||
- `trex_core/application/ledger_port.py` — `LedgerPort` Protocol, `LedgerTrade`, `LedgerWriteResult`, `LedgerError`/`LedgerConflictError`/`LedgerRetryableError` (REQ-017).
|
||||
- `trex_core/application/ledger_outbox.py` — `LedgerOutbox` Protocol, `LedgerWorkItem`, `OutboxOutcome`, `WorkItemStatus`, `LedgerWriter`, `InMemoryLedgerOutbox` (REQ-019).
|
||||
- `trex_core/adapters/ledger/__init__.py`, `in_memory.py` (`InMemoryLedger` stub), `portfolio_hub.py` (`PortfolioHubLedger` HTTP 어댑터) (REQ-017).
|
||||
- `trex_core/adapters/persistence/sql/002_create_ledger_outbox.sql` — `ledger_outbox` 테이블, `correlation_id` PK, status/outcome CHECK (REQ-019).
|
||||
- `tests/adapters/ledger/{__init__,test_in_memory_ledger,test_portfolio_hub_ledger}.py` — REQ-017 AC-1/2/3.
|
||||
- `tests/adapters/persistence/test_postgresql_ledger_outbox.py` — REQ-019 PG outbox (PG-gated).
|
||||
- `tests/application/{test_execution_ledger,test_ledger_outbox}.py` — REQ-018 + REQ-019.
|
||||
- `tests/domain/test_domain_import_sweep.py` — REQ-017 AC-4 (NFR-1) 정적 import 스윕.
|
||||
- 수정
|
||||
- `trex_core/application/broker_port.py` — `FillStatus.execution_id/fee/tax` 확장 + 공유 `calculate_fee(side, amount)` 헬퍼 (REQ-018).
|
||||
- `trex_core/adapters/broker/paper.py` — `filled` 상태에서 `paper:{broker_order_id}:{filled_qty}` execution_id + fee 합성.
|
||||
- `trex_core/adapters/broker/kis.py` — `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}` execution_id + fee 운반.
|
||||
- `trex_core/application/execution.py` — `ExecutionService` 가 fill 전이 직후 `LedgerWorkItem` 을 outbox 에 enqueue, `portfolio_id` 부재면 skip, slot 전이 우선 보장 (REQ-018 + REQ-019).
|
||||
- `trex_core/adapters/persistence/postgresql.py` — `PostgreSQLLedgerOutbox` (`enqueue`/`list_pending`/`get`/`mark_outcome`).
|
||||
- `trex_core/runtime/config.py` — `PortfolioHubConfig`, `DaemonConfig.portfolio_hub`/`ledger_interval_sec`, env 로딩.
|
||||
- `trex_core/runtime/daemon.py` — `_build_ledger`, ledger writer 백그라운드 루프, `ExecutionService(..., ledger_outbox=outbox)`.
|
||||
- `tests/conftest.py`, `tests/adapters/persistence/test_migration_and_drift.py` — `ledger_outbox` 테이블 반영.
|
||||
- `.env.example` — `TREX_PORTFOLIO_HUB_BASE_URL/TOKEN/TIMEOUT`, `TREX_LEDGER_INTERVAL_SEC` 추가.
|
||||
|
||||
## Execution Log
|
||||
1. 대기: `draft-batch-planning`/`draft-batch-design` 확인 후 구현 시작.
|
||||
1. baseline 테스트 88 passed / 8 skipped 확인.
|
||||
2. REQ-017: `LedgerPort` + `LedgerTrade` + 예외 + `InMemoryLedger` stub + `PortfolioHubLedger` 어댑터 추가, contract 테스트 4종 작성.
|
||||
3. REQ-018: `FillStatus.execution_id/fee/tax` 확장, paper/KIS 결정적 합성 + `calculate_fee` 헬퍼, `ExecutionService._on_filled` 에서 fill 전이 직후 ledger outbox enqueue, `portfolio_id` 부재 시 skip + `ExecutionResult.ledger_enqueued/ledger_skipped` 카운터 추가.
|
||||
4. REQ-019: `LedgerOutbox` Protocol + `LedgerWorkItem`/`OutboxOutcome`/`WorkItemStatus`, `LedgerWriter` (correlation_id 로컬 dedup + 어댑터 `LedgerWriteResult.DUPLICATE_OK` 매핑 → `duplicate_completed`), `InMemoryLedgerOutbox`, `PostgreSQLLedgerOutbox`, `002_create_ledger_outbox.sql` 마이그레이션.
|
||||
5. 데몬 lifespan 에 ledger writer thread 추가, `PortfolioHubConfig` env 로딩.
|
||||
6. REQ-020: runtime-flows / interface-contract / project-structure / data-model 4문서를 ledger 레이어로 정합.
|
||||
7. 정적 import 스윕 테스트(`tests/domain/test_domain_import_sweep.py`) 로 `trex_core/domain` 의 ledger/HTTP 의존 0 검증.
|
||||
8. 최종 테스트: 109 passed / 12 skipped (PG·KIS 게이트). ruff lint 0 issues.
|
||||
|
||||
## Validation
|
||||
- 예정:
|
||||
- stub ledger `record_trade` 단위테스트.
|
||||
- portfolio-hub adapter request contract + 409 mapping 테스트.
|
||||
- buy/sell fill lifecycle 통합테스트.
|
||||
- duplicate execution_id 중복 0 테스트.
|
||||
- retryable failure 후 eventually completed 테스트.
|
||||
- `conflict_blocked` 자동 재시도 중단 테스트.
|
||||
- `trex_core/domain` import sweep.
|
||||
- baseline 문서-코드 정합 리뷰.
|
||||
- 명령: `.venv/bin/python -m pytest -q`
|
||||
- 결과: `109 passed, 12 skipped, 1 warning`.
|
||||
- 명령: `.venv/bin/ruff check trex_core tests`
|
||||
- 결과: `All checks passed!`
|
||||
- 추가 테스트 21개(REQ-017 7 / REQ-018 3 / REQ-019 7 / NFR-1 import 스윕 1 / PG outbox 4 PG-skip).
|
||||
- portfolio-hub 어댑터 contract 는 `httpx.MockTransport` 로 path/body/auth/409/5xx/network-error 모두 커버.
|
||||
- buy/sell lifecycle 테스트로 슬롯 전이와 ledger 기록 분리, dedup, 재시도 결국 기록, 409 무한루프 방지, ledger 장애 시 슬롯 전이 우선 보장을 확인.
|
||||
|
||||
## Remaining Risks
|
||||
- portfolio-hub 실제 계약/자격증명 미가용 시 external integration evidence 가 stub/contract 수준으로 제한될 수 있다.
|
||||
- outbox persistence schema 가 기존 repository transaction 경계와 맞지 않으면 design 조정이 필요하다.
|
||||
- portfolio-hub 의 실제 4xx 응답 본문(특히 `duplicate` 플래그) 계약은 orbit 패턴을 가정한다. 실통합 시 응답 본문이 다르면 `PortfolioHubLedger.record_trade` 의 `LedgerWriteResult.DUPLICATE_OK` 판별을 조정해야 한다. 본 batch 는 stub/contract 수준 evidence 로 검증했다.
|
||||
- KIS `execution_id` 합성에 사용되는 `ord_dt`/`ord_gno_brno` 가 일부 KIS 응답에서 비어 있으면 결정성이 약화될 수 있다. 현재는 누락 시 `today`/빈 문자열로 폴백한다. 운영 데이터 관찰 후 보강 여부 판단(후속 후보).
|
||||
- 부분체결(partially_filled) 단계에서는 ledger 기록을 enqueue 하지 않는다. 부분체결 누적 fill 의 정밀 회계는 후속 Discovery 범위로 남는다.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# BAT-006: portfolio-hub ledger 통합
|
||||
|
||||
- Status: draft
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-004
|
||||
- Included REQ: REQ-017, REQ-018, REQ-019, REQ-020
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# BAT-006 Verification
|
||||
|
||||
- Status: draft
|
||||
- Status: approved
|
||||
- Batch: BAT-006
|
||||
|
||||
## Acceptance Mapping
|
||||
@@ -27,14 +27,45 @@
|
||||
- AC-3: baseline 문서 변경 이력에 REQ-017~020 반영 기록 evidence.
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- 대기: 구현 후 테스트 결과 기록.
|
||||
- Manual:
|
||||
- 대기: baseline 문서-코드 정합 리뷰 기록.
|
||||
- Logs:
|
||||
- 대기: 필요 시 portfolio-hub adapter contract 또는 skip/blocker 로그 기록.
|
||||
|
||||
### Test
|
||||
- 명령: `.venv/bin/python -m pytest -q`
|
||||
- 결과: `109 passed, 12 skipped, 1 warning in 0.98s` (PG 4건 + KIS 1건 + 기존 PG 7건은 DSN/credentials 게이트로 skip).
|
||||
- REQ-017 AC-1: `tests/adapters/ledger/test_in_memory_ledger.py::test_record_trade_preserves_fields_and_is_idempotent_on_execution_id` — 단일 호출 후 `count()==1` + 필드 보존, 동일 execution_id 재호출 후에도 `count()==1`.
|
||||
- REQ-017 AC-2: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_post_contract_includes_execution_id_and_path_uses_asset_id` — `httpx.MockTransport` 로 path `/v1/assets/PF-77/ledger/trades`, body 에 `execution_id`/`side` 포함, `Authorization: Bearer …` 헤더 캡처.
|
||||
- REQ-017 AC-3: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_409_surfaces_as_ledger_conflict_error` + `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` — 409 → `LedgerConflictError`; outbox row `status=conflict_blocked`, 후속 writer tick 에서 어댑터 재호출 없음.
|
||||
- REQ-017 AC-4: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` — `trex_core/domain` AST 워크가 `httpx`/`fastapi`/`uvicorn`/`psycopg`/`redis`/`trex_core.adapters`/`trex_core.runtime`/`trex_core.application` 0 회 import 확인.
|
||||
- REQ-018 AC-1: `tests/application/test_execution_ledger.py::test_buy_fill_enqueues_one_ledger_item_and_writer_records_it` — buy intent → execution.run_once → slot=holding, ledger_enqueued=1, writer.run_once → ledger.count()=1, `LedgerTrade(asset_id="PF-1", side="buy", qty=10, price=1000.0)`.
|
||||
- REQ-018 AC-2: `tests/application/test_execution_ledger.py::test_sell_fill_enqueues_sell_ledger_item` — buy→holding→sell intent→execution.run_once → slot=closed, ledger 에 `["buy","sell"]` 순서.
|
||||
- REQ-018 AC-3: 위 buy 테스트가 `t.execution_id.startswith("paper:PAPER-")`, `t.fee > 0.0`, `t.tax == 0.0` 직접 검증.
|
||||
- REQ-018 AC-4: `tests/application/test_execution_ledger.py::test_portfolio_id_absent_skips_ledger_but_transitions_slot` — portfolio_id=None → ledger_enqueued=0, ledger_skipped=1, 슬롯은 HOLDING.
|
||||
- REQ-018 AC-5: `tests/domain/test_domain_import_sweep.py` (도메인 격리) + `tests/application/test_execution_ledger.py` (오직 `ExecutionService._record_ledger` 만 outbox.enqueue 호출 — code review). `grep -r "ledger_outbox.enqueue\|LedgerOutbox.enqueue" trex_core/` 결과는 `ExecutionService` 단일 호출지점.
|
||||
- REQ-019 AC-1: `tests/application/test_execution_ledger.py::test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 같은 trade 재 enqueue 시 outbox row 1개 유지, 별도 outbox 에서 재처리해도 ledger.count()=1.
|
||||
- REQ-019 AC-2: `tests/application/test_execution_ledger.py::test_retryable_failure_then_eventually_completes` + `tests/application/test_ledger_outbox.py::test_writer_retries_until_success` — retryable 2회 + completed 1회로 ledger.count()=1, 슬롯은 즉시 HOLDING.
|
||||
- REQ-019 AC-3: `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` + `tests/application/test_ledger_outbox.py::test_writer_marks_conflict_blocked_and_does_not_retry` — 후속 writer tick stats 모두 0.
|
||||
- REQ-019 AC-4: `tests/application/test_execution_ledger.py::test_duplicate_completed_when_reprocessing_terminal_item` + `test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 별도 outbox 에 동일 trade 재 enqueue → writer.run_once stats.duplicates=1, stats.completed=0, ledger.count()=1.
|
||||
- REQ-019 AC-5: `tests/application/test_execution_ledger.py::test_slot_transition_proceeds_when_ledger_is_down` — `InMemoryLedger(fail_n_then_succeed=100)` 으로 모든 writer tick 실패, slot 은 HOLDING, outbox row `status=PENDING` 유지(무유실).
|
||||
- PG 통합 (DSN 제공 시): `tests/adapters/persistence/test_postgresql_ledger_outbox.py` 4건 — enqueue 멱등, writer completed/conflict_blocked/retryable 행 컬럼 검증. CI 환경에 DSN 없어 skip 4건은 명시적으로 PG 게이트.
|
||||
|
||||
### Manual
|
||||
- baseline 문서 ↔ 코드 정합 리뷰 (REQ-020 AC-1/AC-2):
|
||||
- `docs/runtime-flows.md` 새 섹션 `Flow: 체결 → ledger outbox` + `Flow: ledger writer` 가 `ExecutionService._record_ledger` (`trex_core/application/execution.py:117-141`) 과 `LedgerWriter.run_once`/`_process` (`trex_core/application/ledger_outbox.py:96-149`) 로직 정합. paper/KIS execution_id 합성 규칙은 `trex_core/adapters/broker/paper.py:73-79`, `trex_core/adapters/broker/kis.py:177-192` 와 일치.
|
||||
- `docs/interface-contract.md` 새 섹션 `Interface: LedgerPort` + `Interface: LedgerOutbox/LedgerWriter` 가 `trex_core/application/ledger_port.py` 시그니처, `LedgerWriteResult.NEW/DUPLICATE_OK`, `LedgerConflictError`/`LedgerRetryableError` 와 정합. portfolio-hub endpoint 표는 `trex_core/adapters/ledger/portfolio_hub.py:_payload`/`record_trade` 와 정합.
|
||||
- `docs/project-structure.md` 디렉토리 트리에 `trex_core/application/ledger_port.py`, `ledger_outbox.py`, `trex_core/adapters/ledger/`, persistence 의 `PostgreSQLLedgerOutbox` + `002_create_ledger_outbox.sql`, runtime 의 ledger writer 루프가 반영. 실제 디렉토리 트리(`find trex_core -type f -name '*.py'`)와 일치 확인.
|
||||
- `docs/data-model.md` 새 엔티티 `LedgerTrade`(application DTO) + `ledger_outbox` 테이블 정의가 `002_create_ledger_outbox.sql` (PK `correlation_id`, status/last_outcome CHECK) 와 1:1 정합. `FillStatus` 확장(`execution_id/fee/tax`) 도 명시.
|
||||
- code-search (REQ-020 AC-2 + REQ-018 AC-5 보조):
|
||||
- `grep -r "LedgerOutbox.enqueue\|self._ledger_outbox.enqueue" trex_core/` → `trex_core/application/execution.py` 단 1곳(execution path 단일 기록 주체).
|
||||
- `grep -r "import httpx\|from httpx" trex_core/domain/` → 결과 0(NFR-1 보강).
|
||||
|
||||
### Logs
|
||||
- `.venv/bin/ruff check trex_core tests` → `All checks passed!` (style/lint blocker 0).
|
||||
- 정적 import 스윕 자동화: `tests/domain/test_domain_import_sweep.py` 가 CI 의 일부로 회귀 가드.
|
||||
- portfolio-hub 실 통합 evidence 는 본 batch 범위 밖(`PortfolioHubLedger` 는 `httpx.MockTransport` 로 contract 검증). 실 통합은 운영 환경에서 수집 — `Remaining Risks` 와 `docs/srs/Integration/req-017_…md` Notes 의 A-1/DATA 위임 항목으로 추적.
|
||||
|
||||
## Result
|
||||
- Ready for Release: false
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- 구현 및 검증 미진행.
|
||||
- 없음.
|
||||
- Notes:
|
||||
- portfolio-hub 실 계약/자격증명 evidence 는 stub + contract 수준이며, 실 통합 검증은 운영 단계에서 수집한다(dcy-004 DATA 위임 사항).
|
||||
- 부분체결 누적/정밀 세금 모델은 후속 Discovery 로 분리됨(이번 범위 밖).
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
| BAT-003 | batch-lite | release-candidate | REQ-010 | dcy-002 | [bat-003](./bat-003_20260604_daemon-dotenv-config/) |
|
||||
| BAT-004 | standard | release-candidate | REQ-011, REQ-012, REQ-013, REQ-014, REQ-015 | dcy-003 | [bat-004](./bat-004_20260604_broker-execution-and-outbox/) |
|
||||
| BAT-005 | batch-lite | release-candidate | REQ-016 | dcy-003 | [bat-005](./bat-005_20260604_kis-token-redis-cache/) |
|
||||
| BAT-006 | standard | draft | REQ-017, REQ-018, REQ-019, REQ-020 | dcy-004 | [bat-006](./bat-006_20260604_portfolio-hub-ledger-integration/) |
|
||||
| BAT-006 | standard | release-candidate | REQ-017, REQ-018, REQ-019, REQ-020 | dcy-004 | [bat-006](./bat-006_20260604_portfolio-hub-ledger-integration/) |
|
||||
|
||||
+54
-2
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-03
|
||||
- Last Updated (KST): 2026-06-04 (BAT-006)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery)
|
||||
|
||||
## Project Summary
|
||||
@@ -14,7 +14,7 @@
|
||||
- 자동매매 일일 거래 슬롯 생명주기 도메인 (equity trading slot lifecycle)
|
||||
|
||||
## Primary Persistence Backend
|
||||
- PostgreSQL (BAT-002, REQ-006) — 코어 데몬의 durable 단일 writer. 도메인은 여전히 storage-agnostic(SlotRepository Protocol); in-memory 어댑터는 테스트용으로 잔존. 슬라이스 1 테이블: daily_slot / daily_symbol_entries / slot_transition_log / intent_log (daily_slot_archive·prune 은 범위 밖).
|
||||
- PostgreSQL (BAT-002, REQ-006; BAT-006 확장) — 코어 데몬의 durable 단일 writer. 도메인은 여전히 storage-agnostic(SlotRepository Protocol); in-memory 어댑터는 테스트용으로 잔존. 슬라이스 1 테이블: daily_slot / daily_symbol_entries / slot_transition_log / intent_log / ledger_outbox (daily_slot_archive·prune 은 범위 밖).
|
||||
|
||||
## Purpose
|
||||
- This document captures the approved cross-cutting data model baseline for the project.
|
||||
@@ -105,6 +105,57 @@
|
||||
- `try_insert` 는 INSERT ... ON CONFLICT DO NOTHING(dedup 게이트). 중복 intent_id 는 재전이 없이 기존 decision 반환.
|
||||
- 완전 비동기 outbox(상태머신 + 소비 루프)로의 승격은 브로커 집행 Discovery 로 Deferred(이 테이블이 전구체).
|
||||
|
||||
## Entity: `LedgerTrade` (BAT-006)
|
||||
|
||||
- Purpose: portfolio-hub 원장에 기록되는 단일 체결 거래 데이터. application 경계 DTO(영속 엔티티 아님 — 영속 표현은 `ledger_outbox` 에서 운반).
|
||||
- Source of Truth: 어댑터 호출 시 in-memory; 영속 표현은 `ledger_outbox` 의 컬럼들.
|
||||
|
||||
### Key Fields
|
||||
|
||||
- asset_id :: str :: 외부 원장 path param (= 슬롯 `portfolio_id`, dcy-004 OQ-1)
|
||||
- order_id :: str :: 로컬 주문 식별자 (`broker_order_id`)
|
||||
- execution_id :: str :: 결정적 합성 (paper / KIS 규칙). 멱등 키.
|
||||
- side :: 'buy' | 'sell'
|
||||
- qty :: float
|
||||
- price :: float
|
||||
- fee :: float :: BrokerPort `calculate_fee(side, qty*price)`
|
||||
- tax :: float :: 기본 0(정밀 모델 후속)
|
||||
- executed_at :: datetime?
|
||||
- correlation_id (derived) :: `{asset_id}:{order_id}:{execution_id}`
|
||||
|
||||
### State Rules
|
||||
|
||||
- 동일 `execution_id` 재기록 시 ledger 어댑터/서버가 dedup 한다(`LedgerWriteResult.DUPLICATE_OK` 또는 server-side 멱등). 로컬에서는 outbox PK 가 dedup gate.
|
||||
|
||||
## Entity: `ledger_outbox` (BAT-006)
|
||||
|
||||
- Purpose: 체결 → portfolio-hub 원장 기록을 슬롯 전이와 분리해 무유실 + 멱등으로 처리하기 위한 durable work-item store(dcy-004 OQ-2, orbit `LedgerWriter` 패턴).
|
||||
- Source of Truth: `ledger_outbox` (PostgreSQL) / 테스트는 in-memory `InMemoryLedgerOutbox`
|
||||
- Lifecycle: pending → completed | conflict_blocked ; retryable_failure 는 status `pending` 유지(attempts++)
|
||||
|
||||
### Key Fields
|
||||
|
||||
- correlation_id :: TEXT PK :: `{asset_id}:{order_id}:{execution_id}` — 로컬 dedup 게이트
|
||||
- asset_id / order_id / execution_id :: TEXT
|
||||
- side :: TEXT CHECK ('buy','sell')
|
||||
- qty / price / fee / tax :: NUMERIC(18,4)
|
||||
- executed_at :: TIMESTAMPTZ?
|
||||
- status :: TEXT CHECK ('pending','completed','conflict_blocked')
|
||||
- attempts :: INTEGER :: 재시도 누적
|
||||
- last_outcome :: TEXT? CHECK ('completed','duplicate_completed','retryable_failure','conflict_blocked')
|
||||
- last_error :: TEXT?
|
||||
- created_at / updated_at :: TIMESTAMPTZ
|
||||
|
||||
### State Rules
|
||||
|
||||
- `enqueue` 는 `INSERT ... ON CONFLICT (correlation_id) DO NOTHING`(dedup 게이트). 동일 fill 이 다시 enqueue 되어도 새 행을 만들지 않는다.
|
||||
- writer 결과 매핑:
|
||||
- NEW → outcome `completed`, status `completed`.
|
||||
- DUPLICATE_OK 또는 로컬 dedup hit → outcome `duplicate_completed`, status `completed`.
|
||||
- 409 → outcome `conflict_blocked`, status `conflict_blocked` (자동 재시도 중단, 운영자 해소).
|
||||
- 5xx/network/timeout → outcome `retryable_failure`, status `pending` (다음 tick 재시도).
|
||||
- slot 전이는 outbox 적재와 분리되어 critical path 유지.
|
||||
|
||||
## Shared Consistency Rules
|
||||
|
||||
- (trading_date, symbol) 재진입 카운트는 closed->idle 복구와 원자적으로 증가한다.
|
||||
@@ -134,3 +185,4 @@
|
||||
- 2026-06-03 :: bootstrap-baseline 으로 초기 데이터 모델 baseline 생성(Slot + SlotTransitionRecord + 재진입 카운터).
|
||||
- 2026-06-03 :: BAT-002 — PostgreSQL 영속화 확정 + `intent_log` 엔티티 추가(REQ-006/007/009).
|
||||
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
||||
- 2026-06-04 :: BAT-006 — ledger 통합 데이터 모델 반영(LedgerTrade DTO + `ledger_outbox` 테이블/`correlation_id` 멱등/outcome 어휘, FillStatus.execution_id/fee/tax 운반) (REQ-017~020).
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- 문서 ID: dcy-005_20260604_portfolio-hub-asset-snapshot-api
|
||||
- 이슈명: portfolio-hub 종목 스냅샷 read API — 매수 전략 모듈에 슬롯-제외 후보 제공
|
||||
- 작성 시각(KST): 2026-06-04 22:46
|
||||
- 마지막 갱신 시각(KST): 2026-06-04 22:46
|
||||
- 마지막 갱신 시각(KST): 2026-06-04 23:55
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: 없음
|
||||
@@ -36,7 +36,8 @@
|
||||
- portfolio-hub read 어댑터(get_assets/get_portfolios)가 trex-core 에 없다. dcy-004/BAT-006 은 write 방향 `LedgerPort` + portfolio-hub 어댑터만 추가하며, read 포트는 다루지 않는다.
|
||||
- 결과적으로 매수 전략 모듈이 "무엇을 살지" 정할 portfolio-hub 종목 정보를 trex-core 를 통해 받을 경로가 없다.
|
||||
- 기대 상태
|
||||
- trex-core 가 portfolio-hub 종목 스냅샷을 read API 로 제공하고, **현재 슬롯을 점유(pending/holding)한 종목을 제외한 매수 후보 리스트**를 매수 전략 모듈에 반환한다.
|
||||
- trex-core daemon 이 설정 가능한 시각(기본 07:30 KST, .env)에 일일 스케줄로 portfolio-hub 에서 자신의 `daemon_id` universe(= `managed_by_daemon_id` 가 해당 daemon 인 portfolio 집합)를 가져와 스냅샷으로 보유한다. universe 항목은 portfolio 단위로 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON)를 담고 **가격(price)은 제외**한다.
|
||||
- 매수 전략 모듈 요청 시 trex-core 가 이 스냅샷을 기반으로 **현재 슬롯을 점유(pending/holding)한 종목을 제외한 매수 후보 리스트**를 반환한다.
|
||||
- 가용 슬롯이 없으면(capacity 소진) 매수 후보가 없음을 명확한 시맨틱(빈 리스트)으로 반환한다.
|
||||
- 이해관계자
|
||||
- 문서 작성자/구현자(AI agent)
|
||||
@@ -53,12 +54,15 @@
|
||||
- 매수 전략 모듈이 매수 후보를 정하려고 trex-core 스냅샷 API 를 호출 → portfolio-hub 종목 중 이미 슬롯을 점유한 종목을 뺀 후보 리스트를 받는다.
|
||||
- 모든 슬롯이 차 있어 더 살 수 없는 상황 → 스냅샷 API 가 빈 후보 리스트를 반환해 전략이 매수를 시도하지 않는다.
|
||||
- 막 체결되어 슬롯이 pending/holding 인 종목 → portfolio-hub 포지션에 아직 반영되기 전이라도 core slot overlay 로 후보에서 즉시 제외되어 중복 매수를 막는다.
|
||||
- 스냅샷 페치 시 portfolio-hub 포지션과 실제 브로커 계좌 포지션이 어긋난 자산 → `warning` 으로 표시되어 그 자산은 매수 후보에서 빠지고 매매 전략이 수행되지 않는다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- portfolio-hub read 포트 + 어댑터: `PortfolioHubReadPort.get_assets()/get_positions()` 추상화 + portfolio-hub 어댑터(orbit get_assets/get_portfolios 패턴, dcy-004 어댑터/자격증명 재사용) + 결정론 stub.
|
||||
- 종목 스냅샷 read API: trex-core 가 portfolio-hub 종목 스냅샷을 read 엔드포인트로 제공하고, **현재 슬롯 점유(pending/holding) 종목을 제외**한 매수 후보 리스트를 반환한다(slot overlay).
|
||||
- portfolio-hub read 포트 + 어댑터: `PortfolioHubReadPort.get_portfolios(daemon_id)` 추상화 + portfolio-hub 어댑터(`managed_by_daemon_id` 로 daemon universe 조회, dcy-004 어댑터/자격증명 재사용) + 결정론 stub. 전략파라미터는 trex-core 가 해석하지 않는 opaque JSON 으로 운반한다.
|
||||
- 일일 스냅샷 갱신: 설정 시각(기본 07:30 KST, .env)에 스케줄러(신규 daemon 컴포넌트)가 universe 를 페치해 스냅샷을 갱신한다. 현금/포지션은 baseline + core in-flight 체결 보정(옵션 C).
|
||||
- 포지션 정합 검증 + warning: 페치 시 portfolio-hub 포지션 vs 실제 브로커 계좌 포지션을 대조하고, 불일치 자산은 `warning` 상태로 매수 후보에서 제외한다.
|
||||
- 종목 스냅샷 read API: trex-core 가 보유 스냅샷을 read 엔드포인트로 제공하고, **현재 슬롯 점유(pending/holding) 종목을 제외**한 매수 후보 리스트를 반환한다(slot overlay).
|
||||
- 빈 후보 시맨틱: 가용 슬롯이 0 이면 매수 후보를 빈 리스트로 반환한다(표현 방식 null vs empty 는 OQ-6).
|
||||
- baseline 갱신: interface-contract(read 포트/엔드포인트)·runtime-flows(스냅샷 조회 흐름)·project-structure(read 어댑터) 갱신.
|
||||
|
||||
@@ -66,8 +70,8 @@
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- 신규: `trex_core/application` read 포트, `trex_core/adapters/ledger`(또는 신규 `adapters/portfolio`) read 어댑터 + stub, `trex_core/adapters/api` 스냅샷 read 엔드포인트 + 응답 스키마, slot overlay(점유 종목 제외) 로직.
|
||||
- 기존: capacity/slot 조회 재사용(점유 종목·가용 슬롯 판정).
|
||||
- 신규: `trex_core/application` read 포트, 신규 `adapters/portfolio` read 어댑터 + stub, `trex_core/adapters/api` 스냅샷 read 엔드포인트 + 응답 스키마, slot overlay(점유 종목 제외) + 현금/포지션 in-flight 보정 로직, 일일 스냅샷 스케줄러(런타임 신규), 포지션 정합 검증 + `warning` 상태, `config` 에 `daemon_id` + 스냅샷 페치 시각 주입.
|
||||
- 기존: capacity/slot 조회 재사용(점유 종목·가용 슬롯 판정), 브로커 계좌 포지션 조회(BrokerPort 잔고조회 — 정합 검증용) 재사용/확장.
|
||||
- 문서 영향:
|
||||
- interface-contract/runtime-flows/project-structure 갱신.
|
||||
- 운영 영향:
|
||||
@@ -77,24 +81,26 @@
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: trex-core 가 portfolio-hub 종목 스냅샷을 read API 엔드포인트로 제공한다(GET, 매수 전략 모듈이 HTTP 로 소비). 응답 단위(자산 universe / 포지션 / 후보)는 OQ-1 에서 확정한다.
|
||||
- FR-1: trex-core 가 종목 스냅샷을 read API 엔드포인트로 제공한다(GET, 매수 전략 모듈이 HTTP 로 소비). 스냅샷 단위는 daemon 이 관리하는 portfolio universe 이며, 항목당 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON)를 담고 가격(price)은 제외한다. (OQ-1 Answered)
|
||||
- FR-2: 스냅샷 응답에서 **현재 슬롯을 점유(pending/holding)한 종목을 제외**한 매수 후보 리스트를 반환한다. 제외 판정은 core 의 권위 슬롯 상태를 사용한다(slot overlay).
|
||||
- FR-3: 가용 슬롯이 0(capacity 소진)이면 매수 후보를 빈 리스트로 반환한다. 빈 리스트의 표현(빈 배열 vs null vs 메타 필드)은 OQ-6 결정에 따른다.
|
||||
- FR-4: portfolio-hub read 접근을 `PortfolioHubReadPort`(get_assets/get_positions) 추상화 + portfolio-hub 어댑터 + 결정론 stub 로 제공하고, dcy-004 의 portfolio-hub 어댑터/자격증명을 재사용한다.
|
||||
- FR-5: baseline 문서(interface-contract/runtime-flows/project-structure)를 read 포트·스냅샷 엔드포인트·read 어댑터에 맞게 갱신한다.
|
||||
- FR-4: portfolio-hub read 접근을 `PortfolioHubReadPort.get_portfolios(daemon_id)` 추상화 + portfolio-hub 어댑터(`managed_by_daemon_id` 기준) + 결정론 stub 로 제공하고, dcy-004 의 portfolio-hub 어댑터/자격증명을 재사용한다. 전략파라미터는 opaque JSON 으로 통과시킨다(trex-core 가 스키마를 해석하지 않음).
|
||||
- FR-5: baseline 문서(interface-contract/runtime-flows/project-structure)를 read 포트·스냅샷 엔드포인트·read 어댑터·일일 스케줄러에 맞게 갱신한다.
|
||||
- FR-6: trex-core daemon 이 설정 가능한 시각(기본 07:30 KST, .env 로 지정)에 일일 스케줄로 `daemon_id` universe 를 페치해 스냅샷을 보유한다. universe 멤버십·전략파라미터는 일일 스냅샷으로 고정하고, 현금/포지션은 스냅샷 baseline 에 그날 core 의 in-flight 체결분을 반영해 보정한다(옵션 C).
|
||||
- FR-7: 스냅샷 페치 시 각 portfolio 의 portfolio-hub 포지션과 실제 브로커 계좌 포지션이 일치하는지 검증한다. 불일치하는 자산은 `warning` 상태로 표시하고 매수 후보에서 제외해 해당 자산에 매매 전략을 수행하지 않는다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: 도메인 순수성 유지 — read 포트는 application 계약, 어댑터는 adapters 레이어 격리. `trex_core/domain` 은 portfolio-hub/네트워크 의존 0(import 스윕).
|
||||
- NFR-2: slot overlay 일관성 — 점유 종목 제외/가용 슬롯 판정은 core 의 권위 슬롯 상태(강한 일관성)를 사용한다. portfolio-hub 자산 데이터의 신선도 정책은 OQ-2 에 따른다.
|
||||
- NFR-2: 일관성 — 점유 종목 제외/가용 슬롯 판정은 core 의 권위 슬롯 상태(강한 일관성)를 사용한다. 현금/포지션은 일일 baseline + core in-flight 체결 보정으로 유지하고(옵션 C), 스냅샷 페치 시 브로커 계좌와 정합 검증한다(불일치 자산 `warning` 제외).
|
||||
- NFR-3: 가용성 degrade — portfolio-hub 불가 시 스냅샷 read 의 동작(에러 vs 캐시 fallback vs 빈 응답)은 OQ-5 정책을 따른다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- portfolio-hub read 포트 + 어댑터 + stub, 종목 스냅샷 read API, 슬롯 점유 종목 제외(slot overlay), 가용 슬롯 0 → 빈 후보, baseline 갱신.
|
||||
- portfolio-hub read 포트 + 어댑터 + stub, 종목 스냅샷 read API, 슬롯 점유 종목 제외(slot overlay), 가용 슬롯 0 → 빈 후보, 설정 시각 일일 스냅샷 + 현금/포지션 in-flight 보정(옵션 C), 포지션 정합 검증 + `warning` 제외, baseline 갱신.
|
||||
- Out of Scope (후속 Discovery)
|
||||
- 매수 전략 의사결정 로직 자체(점수화·랭킹·진입 타이밍) — 전략 모듈 책임.
|
||||
- portfolio-hub ↔ core 양방향 reconciliation, 포지션 재구성, asset master 영속 동기화.
|
||||
@@ -104,34 +110,38 @@
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (Medium): stale 스냅샷 — portfolio-hub 자산 데이터를 캐시하면 오래된 데이터 위에서 매수 후보가 만들어질 수 있다.
|
||||
- 완화 방안(OQ-2): 신선도/캐시 정책(live pass-through 또는 TTL+무효화) 확정. slot overlay 는 core 권위 상태라 강한 일관성 유지.
|
||||
- R-1 (Medium): stale 스냅샷 — 일일 캐시한 현금/포지션이 장중에 어긋날 수 있다.
|
||||
- 완화 방안(OQ-2 Answered, 옵션 C): 현금/포지션을 baseline + core in-flight 체결 보정으로 유지. daemon 이 유일 매매 주체이고 장중 입출금이 없다는 전제(A-6)로 정확. 추가로 페치 시 브로커 계좌 정합 검증(FR-7).
|
||||
- R-2 (Medium): in-flight race — dcy-004 ledger outbox 가 eventually-consistent 라, 막 체결한 종목이 portfolio-hub 포지션에 아직 반영되기 전 후보에 중복 노출될 수 있다.
|
||||
- 완화 방안(OQ-4): core slot overlay(pending/holding 제외)로 이 gap 을 메운다. portfolio-hub 포지션과 core 슬롯 상태의 합성 규칙을 명확화한다.
|
||||
- R-3 (Medium): 가용성 결합 — portfolio-hub 장애가 매수 전략 read 를 막는다.
|
||||
- 완화 방안(OQ-5): 캐시 fallback 또는 명시적 degrade 응답 정책. 어댑터 격리(NFR-1).
|
||||
- R-4 (Low): 책임 경계 확장 — core 가 portfolio-hub read gateway 로 비대해질 위험.
|
||||
- 완화 방안: slot overlay 부가가치(점유 제외·가용 판정)로 범위를 한정하고 단순 패스스루 프록시는 지양(OQ-3).
|
||||
- R-5 (Medium): 포지션 drift — portfolio-hub baseline 포지션이 실제 브로커 계좌와 어긋나면(과거 누락·정합 오류) 옵션 C 의 보정 baseline 자체가 틀린다.
|
||||
- 완화 방안(FR-7): 스냅샷 페치 시 portfolio-hub 포지션 vs 브로커 계좌 포지션 정합 검증, 불일치 자산은 `warning` 으로 매매 제외(보수적 안전). 일치 기준·회복 규칙은 OQ-8.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: portfolio-hub 는 orbit 과 동일 read 계약(get_assets/get_portfolios 류)을 제공한다.
|
||||
- A-1: portfolio-hub 는 daemon 인증(`auth_tokens.daemon_id`)으로 자신이 관리하는 portfolio 집합을 조회하는 read 계약을 제공한다. (근거: portfolio-hub `app/api/v1_portfolios.py`·`v1_user_info.py` 의 `auth.daemon_id` 스코프)
|
||||
- A-2: trex-core 는 슬롯/capacity 의 권위 단일 소스이며, 점유 종목·가용 슬롯 판정에 자기 상태를 사용할 수 있다.
|
||||
- A-3: 매수 전략 모듈은 trex-core 외부 소비자이며 HTTP 로 스냅샷을 받는다.
|
||||
- A-4: dcy-004 의 portfolio-hub 어댑터/자격증명(.env `TREX_PORTFOLIO_HUB_*`)을 read 방향에서도 재사용한다.
|
||||
- A-5: portfolio-hub 의 `daemon` 은 1급 엔티티이며 portfolio 가 `managed_by_daemon_id` 로 daemon 에 귀속된다(1 daemon : N portfolio, 각 portfolio ≈ 1 asset, asset_id == portfolio_id). 전략파라미터는 portfolio-hub 가 `strategy_params` JSONB 로 보관한다. trex-core daemon 의 universe = 자신이 관리하는 portfolio 집합이다. (근거: portfolio-hub `app/domain/models.py` Daemon/Portfolio, `strategy_params` JSONB migration)
|
||||
- A-6: 이 trex-core daemon 은 자신이 관리하는 portfolio 의 유일한 매매 주체이며, 장중 거래 외 현금 변동(입출금)은 없다. 따라서 일일 baseline + core in-flight 체결 보정만으로 현금/포지션을 정확히 유지할 수 있다(옵션 C 전제). (사용자 확인 2026-06-04)
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: 스냅샷의 정의 — 응답이 (a) 거래 가능 universe(asset master), (b) 현재 포지션/보유, (c) 전략 후보/타깃 비중 중 무엇을 담는가(또는 조합)? 매수 전략에 필요한 최소 필드는?
|
||||
- 상태: Open
|
||||
- 처리 방안: 운영자/매수 전략 모듈 요구를 confirm-discovery 시점에 확정.
|
||||
- 종료 조건: 응답 단위와 필드 집합이 FR-1 에 반영되면 Answered.
|
||||
- OQ-2: 신선도/캐시 정책 — live pass-through 인가, 캐시(REQ-016 Redis 패턴)인가? 캐시라면 TTL/무효화 기준은?
|
||||
- 상태: Open
|
||||
- 처리 방안: 운영자 결정. 매수 결정의 stale 허용도 기준으로 판단.
|
||||
- 종료 조건: 캐시 여부와 신선도 기준이 NFR-2/R-1 에 반영되면 Answered.
|
||||
- OQ-1: 스냅샷의 정의 — 응답 단위와 필드.
|
||||
- 상태: Answered (2026-06-04)
|
||||
- 결정: 스냅샷 = daemon 이 관리하는 portfolio universe. 항목당 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON). 가격(price)은 사용하지 않으므로 제외. 전략파라미터는 portfolio-hub 가 `strategy_params` JSONB 로 보관하므로 trex-core 는 해석 없이 opaque JSON 으로 통과시킨다.
|
||||
- 반영 위치: `# 3` 기대 상태, `# 5` FR-1·FR-4, `# 6` A-5.
|
||||
- OQ-2: 신선도/캐시 정책 — universe 페치 주기와 현금/포지션 신선도.
|
||||
- 상태: Answered (2026-06-04)
|
||||
- 결정: universe 멤버십·전략파라미터는 설정 시각(기본 07:30 KST, .env) 일일 스냅샷으로 고정. 현금/포지션은 스냅샷 baseline 에 **core 의 그날 in-flight 체결분을 반영해 보정**한다(옵션 C). 근거: 이 daemon 이 자기 portfolio 의 유일 매매 주체이고(A-6) 장중 입출금 등 거래 외 변동이 없으므로 baseline + 자기 체결 보정으로 정확한 현금/포지션을 portfolio-hub 추가 호출(매 요청 live) 없이 유지 가능. 매수 전략의 현금 조회 주기가 빠를 수 있어(분 단위) 매 요청 live 호출(옵션 B)의 가용성 결합·지연을 회피한다.
|
||||
- 반영 위치: `# 5` FR-6·NFR-2, `# 6` R-1·A-6.
|
||||
- OQ-3: core 경유 정책 — 모든 소비자가 trex-core 를 경유하는가, 일부는 portfolio-hub 직접 호출을 허용하는가? core 경유의 부가가치(slot overlay)를 어디까지 의무화하는가?
|
||||
- 상태: Open
|
||||
- 처리 방안: 운영자 결정. 직접 호출 대비 indirection 비용과 slot 결합 가치 비교.
|
||||
@@ -148,6 +158,14 @@
|
||||
- 상태: Open
|
||||
- 처리 방안: 매수 전략 모듈 소비 계약에 맞춰 결정(REST 관례상 빈 배열 + 메타 필드 권장안 검토).
|
||||
- 종료 조건: 표현 방식이 FR-3 에 반영되면 Answered.
|
||||
- OQ-7: trex-core daemon 의 `daemon_id` 주입 방식 — .env 설정값으로 직접 주입하는가, portfolio-hub 인증 토큰(`auth_tokens.daemon_id`)에서 도출하는가?
|
||||
- 상태: Open
|
||||
- 처리 방안: 운영자 결정. dcy-004 자격증명 주입(.env `TREX_PORTFOLIO_HUB_*`) 패턴과 정합성 검토.
|
||||
- 종료 조건: 주입 방식이 config/FR-4 에 반영되면 Answered.
|
||||
- OQ-8: 포지션 정합 검증 세부 — 일치 판정 기준(정확 수량 일치 vs 허용 오차), `warning` 상태 회복 조건(다음 정합 성공 시 자동 해제 여부), 브로커 계좌 포지션 출처(KIS 잔고조회 재사용 범위).
|
||||
- 상태: Open
|
||||
- 처리 방안: 설계 시 확정. BrokerPort 잔고조회 계약 + `warning` 상태 저장/회복 규칙 정의.
|
||||
- 종료 조건: 정합 기준·회복 규칙·포지션 출처가 FR-7 에 반영되면 Answered.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
@@ -166,6 +184,7 @@
|
||||
- S-3: 가용 슬롯이 0 일 때 스냅샷 API 가 빈 후보 리스트를 반환한다.
|
||||
- S-4: 도메인 import 스윕에서 portfolio-hub/네트워크 의존이 0 으로 유지된다.
|
||||
- S-5: interface-contract/runtime-flows/project-structure 가 read 레이어와 정합한다.
|
||||
- S-6: 스냅샷 페치 시 portfolio-hub 포지션과 브로커 계좌 포지션이 불일치하는 자산이 `warning` 으로 표시되고 매수 후보에서 제외된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
@@ -175,6 +194,7 @@
|
||||
- S-3 측정 방식: 슬롯 소진(capacity 0) 후 스냅샷 호출 테스트 / 데이터 출처: 후보 리스트 길이.
|
||||
- S-4 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain` import 목록.
|
||||
- S-5 측정 방식: 문서-코드 정합 점검(리뷰) / 데이터 출처: baseline 문서 vs 실제 엔드포인트/포트.
|
||||
- S-6 측정 방식: 포지션 불일치 주입 후 스냅샷 페치 테스트 / 데이터 출처: 자산 `warning` 상태·후보 리스트.
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
@@ -186,7 +206,7 @@ REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지
|
||||
- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: 본 Discovery 의 리스크는 현재 전부 Medium/Low. 신선도(R-1)·race(R-2)·가용성(R-3) 처리 방침을 운영자가 확인.
|
||||
- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1~OQ-6 이 전부 Answered/Deferred 처리(현재 6개 모두 Open).
|
||||
- 기준: OQ-1~OQ-8 이 전부 Answered/Deferred 처리(현재 OQ-1·OQ-2 Answered, OQ-3~OQ-8 Open).
|
||||
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다.
|
||||
|
||||
@@ -204,8 +224,9 @@ REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- {{DECIDE_SNAPSHOT_SHAPE}} — 스냅샷 응답 단위/필드(universe vs 포지션 vs 후보) 확정 (OQ-1)
|
||||
- {{DECIDE_FRESHNESS_POLICY}} — live pass-through vs 캐시(TTL/무효화) 신선도 정책 (OQ-2)
|
||||
- ~~스냅샷 응답 단위/필드~~ → 결정됨: daemon 관리 portfolio universe(symbol/asset_id/현금/포지션/active_strategy_id/전략파라미터 opaque JSON), 가격 제외 (OQ-1, 2026-06-04)
|
||||
- ~~신선도 정책~~ → 결정됨: universe·전략파라미터 설정 시각 일일 캐시 + 현금/포지션 baseline+core in-flight 보정(옵션 C) (OQ-2, 2026-06-04)
|
||||
- {{DECIDE_DAEMON_ID_INJECTION}} — trex-core daemon 의 daemon_id 주입(.env vs 인증 토큰) (OQ-7)
|
||||
- {{DECIDE_EMPTY_SEMANTICS}} — 가용 슬롯 0 시 빈 배열 vs null vs 메타 필드 표현 (OQ-6)
|
||||
|
||||
## CONFIRM
|
||||
@@ -214,7 +235,7 @@ REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지
|
||||
- {{CONFIRM_INFLIGHT_OVERLAY}} — portfolio-hub 포지션 + core 슬롯 합성/제외 규칙, 가용성 degrade 정책 (OQ-4/OQ-5)
|
||||
|
||||
## DATA
|
||||
- {{DATA_PORTFOLIO_HUB_READ_CONTRACT}} — portfolio-hub read API 계약(get_assets/get_portfolios) 및 응답 필드(orbit 동일 가정 검증)
|
||||
- {{DATA_PORTFOLIO_HUB_READ_CONTRACT}} — portfolio-hub read API 계약(`managed_by_daemon_id` 로 daemon universe 조회) 및 응답 필드(cash_balance/asset_quantity/strategy_params JSONB) 확인
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
@@ -228,7 +249,7 @@ REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 보류
|
||||
- Handoff Rationale: 초기 Discovery 초안 생성 완료. OQ-1~OQ-6(스냅샷 정의·신선도/캐시·core 경유 정책·in-flight 합성·가용성 degrade·빈 후보 시맨틱)이 미확정이며, confirm-discovery 에서 운영자 결정 후 REQ Drafting 으로 인계한다.
|
||||
- Handoff Rationale: OQ-1(스냅샷 정의)·OQ-2(신선도, 옵션 C) Answered. OQ-3~OQ-8(core 경유 정책·in-flight 합성·가용성 degrade·빈 후보 시맨틱·daemon_id 주입·포지션 정합 세부)이 미확정이며, confirm-discovery 에서 운영자 결정 후 REQ Drafting 으로 인계한다.
|
||||
- Ready for REQ Drafting: false
|
||||
- Confirmed By: TBD
|
||||
- Confirmed At (KST): TBD
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-03 (BAT-002)
|
||||
- Last Updated (KST): 2026-06-04 (BAT-006)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); HTTP API = dcy-002 / BAT-002
|
||||
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
- HTTP `/v1` API :: 슬롯 코어 데몬이 노출, 매수·매도 전략 데몬이 호출(REQ-005/007/008)
|
||||
- SlotService :: 슬롯 생명주기 도메인 서비스(전이/사전할당/가용성/재진입 카운트)
|
||||
- SlotRepository (Protocol) :: 도메인 ↔ 저장소 경계 계약
|
||||
- BrokerPort (Protocol) :: 단일 브로커 세션 경계(submit/poll/cancel) + `OrderRequest`/`OrderAck`/`FillStatus`
|
||||
- LedgerPort (Protocol) :: 외부 원장(portfolio-hub) 기록 경계 + `LedgerTrade`/`LedgerWriteResult`/예외 (BAT-006)
|
||||
- LedgerOutbox (Protocol) + LedgerWriter :: 체결 → 원장 기록의 outbox/재시도/멱등 (BAT-006)
|
||||
|
||||
## Interface: `HTTP /v1` (슬롯 코어 데몬)
|
||||
|
||||
@@ -113,6 +116,83 @@
|
||||
- 구현체는 advisory lock 직렬화와 append-only 전이 로그 의미를 반드시 보장한다.
|
||||
- 구현체는 lock 보유 중 외부 I/O 를 수행하지 않는다.
|
||||
|
||||
## Interface: `LedgerPort` (BAT-006)
|
||||
|
||||
- Type: external-service (HTTP, portfolio-hub)
|
||||
- Consumers / Producers: `LedgerWriter`(소비) → `PortfolioHubLedger` 어댑터 또는 `InMemoryLedger` stub
|
||||
- Purpose: 체결을 외부 회계/포지션 원장에 기록한다. orbit `post_ledger_trade` 패턴.
|
||||
- Stability: experimental (v1)
|
||||
|
||||
### Inputs
|
||||
|
||||
- record_trade(trade: LedgerTrade) :: 단일 체결을 원장에 기록 (`execution_id` 멱등)
|
||||
|
||||
### `LedgerTrade` Fields
|
||||
|
||||
- asset_id :: str :: ledger path param (= 슬롯 `portfolio_id`, dcy-004 OQ-1)
|
||||
- order_id :: str :: 로컬 주문 식별자 (`broker_order_id`)
|
||||
- execution_id :: str :: 결정적 합성 (paper: `paper:{broker_order_id}:{filled_qty}`, KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`)
|
||||
- side :: 'buy' | 'sell'
|
||||
- qty / price :: float :: 체결 수량 / 평단
|
||||
- fee :: float :: `BrokerPort.calculate_fee(side, qty*price)` 결과
|
||||
- tax :: float :: 기본 0 (정밀 모델 후속)
|
||||
- executed_at :: datetime?
|
||||
- correlation_id (derived) :: `{asset_id}:{order_id}:{execution_id}` — 로컬 dedup 키
|
||||
|
||||
### Outputs / Outcomes
|
||||
|
||||
- `LedgerWriteResult.NEW` :: 새 원장 엔트리 적재
|
||||
- `LedgerWriteResult.DUPLICATE_OK` :: 같은 execution_id 가 이미 기록되어 있음(서버 멱등)
|
||||
|
||||
### Error Contract
|
||||
|
||||
- `LedgerConflictError` :: portfolio-hub 409 — 운영자 해소 대상(자동 재시도 중단)
|
||||
- `LedgerRetryableError` :: 5xx / timeout / network — outbox 가 재시도
|
||||
|
||||
### portfolio-hub Endpoint (어댑터 구현)
|
||||
|
||||
| HTTP | URL | Body | 응답 |
|
||||
|---|---|---|---|
|
||||
| POST | `/v1/assets/{asset_id}/ledger/trades` | `side, qty, price, fee, tax, executed_at, execution_id, order_id` | 2xx OK(`duplicate:true` 면 server-side dedup), 409 conflict, 5xx/network → retryable |
|
||||
|
||||
## Interface: `LedgerOutbox` Protocol + `LedgerWriter` (BAT-006)
|
||||
|
||||
- Type: internal-service (durable work-item store + drainer)
|
||||
- Consumers / Producers: `ExecutionService`(생산자, fill 시 enqueue) → `LedgerWriter`(소비자, 백그라운드 워커)
|
||||
- Purpose: 체결 → 원장 기록을 슬롯 전이와 분리해 무유실 + 멱등으로 처리한다. correlation_id 로 outbox 적재 dedup.
|
||||
- Stability: internal
|
||||
|
||||
### Inputs
|
||||
|
||||
- enqueue(trade: LedgerTrade) :: `correlation_id` 키로 멱등 적재
|
||||
- list_pending(limit) :: FIFO pending work item
|
||||
- get(correlation_id) :: 단일 row 조회
|
||||
- mark_outcome(correlation_id, outcome, error?) :: outcome 기록 + 상태 전이
|
||||
|
||||
### Outcomes (`OutboxOutcome`)
|
||||
|
||||
- `completed` :: 어댑터 NEW
|
||||
- `duplicate_completed` :: 어댑터 DUPLICATE_OK 또는 로컬 dedup 히트
|
||||
- `retryable_failure` :: 다음 tick 재시도 (status 는 `pending` 유지)
|
||||
- `conflict_blocked` :: 409, 자동 재시도 중단(운영자 해소)
|
||||
|
||||
### WorkItem Status
|
||||
|
||||
- pending → completed (성공/중복)
|
||||
- pending → conflict_blocked (409)
|
||||
- pending → pending (retryable, attempts++)
|
||||
|
||||
### Persistence
|
||||
|
||||
- PostgreSQL: `ledger_outbox(correlation_id PK, asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at, status, attempts, last_outcome, last_error, created_at, updated_at)` — `ON CONFLICT (correlation_id) DO NOTHING` 이 outbox dedup 게이트.
|
||||
|
||||
## `FillStatus` 확장 (BAT-006 / REQ-018)
|
||||
|
||||
- `state, filled_qty, avg_price, raw` 외에 다음 필드를 운반한다.
|
||||
- `execution_id :: str?` :: 결정적 합성(브로커별 규칙). filled 상태에서 필수.
|
||||
- `fee :: float` :: BrokerPort `calculate_fee` 결과(기본 0).
|
||||
- `tax :: float` :: 기본 0(정밀 모델 후속).
|
||||
|
||||
## Shared Constraints
|
||||
|
||||
- 도메인은 외부 의존(브로커, DB 드라이버 등)을 직접 호출하지 않는다(순수 도메인 경계).
|
||||
@@ -135,3 +215,4 @@
|
||||
- 2026-06-03 :: bootstrap-baseline 으로 초기 인터페이스 계약 baseline 생성(SlotService + SlotRepository Protocol).
|
||||
- 2026-06-03 :: BAT-002 — HTTP `/v1` API 계약(데몬, 6 엔드포인트) + 멱등/단일writer 불변식 추가(REQ-007/008/009).
|
||||
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
||||
- 2026-06-04 :: BAT-006 — LedgerPort/LedgerTrade/LedgerWriteResult 계약, LedgerOutbox/LedgerWriter, portfolio-hub `/v1/assets/{asset_id}/ledger/trades` 엔드포인트 + 409→`LedgerConflictError`, FillStatus.execution_id/fee/tax 확장 (REQ-017~020).
|
||||
|
||||
+17
-12
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-03
|
||||
- Last Updated (KST): 2026-06-04 (BAT-006)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery, 3-phase lifecycle 밖의 bootstrap 산출물)
|
||||
|
||||
## Project Summary
|
||||
@@ -62,25 +62,29 @@
|
||||
│ ├── domain/slot/ # slot 순수 도메인 (BAT-001, 무의존)
|
||||
│ │ ├── __init__.py · state.py · models.py(+EXPECTED_SLOT_FIELDS) · errors.py
|
||||
│ │ ├── repository.py(Protocol) · service.py · in_memory_repository.py
|
||||
│ ├── application/ # 유스케이스 (BAT-002, BAT-004)
|
||||
│ ├── application/ # 유스케이스 (BAT-002, BAT-004, BAT-006)
|
||||
│ │ ├── ports.py # IntentStore Protocol
|
||||
│ │ ├── intents.py # IntentService / BuyIntent / SellIntent / IntentDecision
|
||||
│ │ ├── broker_port.py # BrokerPort + OrderRequest/OrderAck/FillStatus (BAT-004)
|
||||
│ │ └── execution.py # ExecutionService (claim·dispatch·poll·transition·reconcile)
|
||||
│ ├── adapters/ # 인프라 어댑터 (BAT-002, BAT-004)
|
||||
│ │ ├── broker_port.py # BrokerPort + OrderRequest/OrderAck/FillStatus + calculate_fee (BAT-004/BAT-006)
|
||||
│ │ ├── execution.py # ExecutionService (claim·dispatch·poll·transition·reconcile + ledger enqueue)
|
||||
│ │ ├── ledger_port.py # LedgerPort + LedgerTrade/LedgerWriteResult/예외 (BAT-006)
|
||||
│ │ └── ledger_outbox.py # LedgerOutbox Protocol + LedgerWriter + InMemoryLedgerOutbox (BAT-006)
|
||||
│ ├── adapters/ # 인프라 어댑터 (BAT-002, BAT-004, BAT-006)
|
||||
│ │ ├── api/ # FastAPI: app.py(create_app /v1) · schemas.py
|
||||
│ │ ├── broker/ # paper.py(결정적 async 더블) · kis.py(KIS Open API) (BAT-004)
|
||||
│ │ └── persistence/ # postgresql.py · in_memory_intent_store.py · migrate.py · sql/
|
||||
│ └── runtime/ # 데몬 (BAT-002, BAT-004)
|
||||
│ ├── config.py # DaemonConfig + KISConfig(env, broker 선택/게이팅)
|
||||
│ └── daemon.py # build_app + lifespan(migrate/preallocate/reconcile/execution 루프)
|
||||
│ │ ├── ledger/ # in_memory.py(stub) · portfolio_hub.py(HTTP 어댑터) (BAT-006)
|
||||
│ │ └── persistence/ # postgresql.py(+PostgreSQLLedgerOutbox) · in_memory_intent_store.py · migrate.py · sql/(001_…, 002_create_ledger_outbox.sql)
|
||||
│ └── runtime/ # 데몬 (BAT-002, BAT-004, BAT-006)
|
||||
│ ├── config.py # DaemonConfig + KISConfig + PortfolioHubConfig(env)
|
||||
│ └── daemon.py # build_app + lifespan(migrate/preallocate/reconcile/execution 루프 + ledger writer 루프)
|
||||
└── tests/
|
||||
├── domain/slot/ # test_state / test_service / test_contract
|
||||
├── application/ # test_intent_concurrency · test_execution (집행 lifecycle)
|
||||
└── adapters/{api,broker,persistence}/ # API · paper/KIS · PG repo/migration/drift
|
||||
├── domain/ # test_domain_import_sweep (NFR-1 가드, BAT-006)
|
||||
├── application/ # test_intent_concurrency · test_execution · test_execution_ledger · test_ledger_outbox
|
||||
└── adapters/{api,broker,ledger,persistence}/ # API · paper/KIS · in_memory/portfolio-hub ledger · PG repo/migration/outbox
|
||||
```
|
||||
|
||||
> 참고: domain(BAT-001)은 무의존 코어. application/adapters/runtime 은 BAT-002 에서 추가, BAT-004 에서 broker/execution 레이어 확장. 웹/DB/브로커 의존(fastapi/uvicorn/psycopg/httpx)은 adapters/runtime 에만 격리(NFR). 브로커 주문은 BrokerPort 단일 세션(불변식 B), 슬롯 쓰기는 SlotService 단일 writer(불변식 A).
|
||||
> 참고: domain(BAT-001)은 무의존 코어. application/adapters/runtime 은 BAT-002 에서 추가, BAT-004 에서 broker/execution 레이어 확장, BAT-006 에서 ledger 레이어(application LedgerPort + adapters/ledger + runtime writer 루프 + persistence outbox)를 추가했다. 웹/DB/브로커/HTTP ledger 의존(fastapi/uvicorn/psycopg/httpx)은 adapters/runtime 에만 격리(NFR-1). 브로커 주문은 BrokerPort 단일 세션(불변식 B), 슬롯 쓰기는 SlotService 단일 writer(불변식 A), ledger 기록은 집행 경로 단일 주체(NFR-3).
|
||||
|
||||
## Package / Module Responsibilities
|
||||
|
||||
@@ -125,3 +129,4 @@
|
||||
- 2026-06-03 :: BAT-001 이식 완료 반영 — `trex_core/`·`tests/`·`pyproject.toml` planned→실제 정합(REQ-004).
|
||||
- 2026-06-03 :: BAT-002 — application/adapters/runtime 레이어(데몬+HTTP API+PostgreSQL) 추가, Primary Runtime=daemon+library(REQ-005~009).
|
||||
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
||||
- 2026-06-04 :: BAT-006 — ledger 레이어 추가(application LedgerPort/LedgerOutbox, adapters/ledger in_memory·portfolio_hub, persistence PostgreSQLLedgerOutbox + 002 마이그레이션, runtime ledger writer 루프, FillStatus 확장, NFR-1 import 스윕) (REQ-017~020).
|
||||
|
||||
+24
-1
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-03 (BAT-002)
|
||||
- Last Updated (KST): 2026-06-04 (BAT-006)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
|
||||
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
||||
|
||||
@@ -55,6 +55,28 @@
|
||||
- 모든 슬롯 쓰기는 SlotService(advisory lock) 단일 경로(불변식 A). 브로커 주문/취소는 BrokerPort 단일 세션만(불변식 B).
|
||||
- 어댑터: paper(결정적 async 더블) / KIS(모의투자 검증, 실계좌 게이팅).
|
||||
|
||||
## Flow: `체결 → ledger outbox` (BAT-006)
|
||||
|
||||
집행 루프 fill 전이 직후 `ExecutionService` 가 ledger 기록을 outbox 에 적재해 슬롯 전이 critical path 와 ledger write 를 분리한다(원장 기록 단일 주체 = 집행 경로, NFR-3).
|
||||
|
||||
1. fill 감지: `BrokerPort.poll_fill` 이 `state="filled"` 와 결정적 `execution_id`(paper: `paper:{broker_order_id}:{filled_qty}` / KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`), 계산된 `fee`(BrokerPort `calculate_fee(side, amount)`), `tax=0` 을 운반.
|
||||
2. 슬롯 전이: `pending_buy→holding`(avg_price/qty) 또는 `pending_sell→closed` 를 advisory lock 안에서 먼저 commit. 이 단계가 critical path.
|
||||
3. ledger enqueue: 슬롯의 `portfolio_id`(== ledger `asset_id`, OQ-1 결정) 가 있으면 `LedgerTrade(asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at)` 을 `LedgerOutbox.enqueue` 로 적재. 행 식별자는 `correlation_id = {asset_id}:{order_id}:{execution_id}`(로컬 dedup). `portfolio_id` 부재면 ledger 기록 skip 후 슬롯 전이는 정상 유지.
|
||||
4. `clear_dispatch` 로 `broker_order_id` 해제.
|
||||
|
||||
## Flow: `ledger writer` (백그라운드, BAT-006)
|
||||
|
||||
데몬 lifespan 에서 별도 백그라운드 워커가 ledger outbox 를 drain 한다. 슬롯 전이와 분리되어 eventually-consistent.
|
||||
|
||||
1. `LedgerOutbox.list_pending(limit)` 으로 pending work item 을 FIFO 로 가져온다.
|
||||
2. row 의 현재 상태가 이미 `completed` 면 (e.g. 동시/재처리) `duplicate_completed` 로 마킹하고 어댑터를 호출하지 않는다.
|
||||
3. `LedgerPort.record_trade(trade)` 호출.
|
||||
- `LedgerWriteResult.NEW` → outcome `completed`, row status `completed`.
|
||||
- `LedgerWriteResult.DUPLICATE_OK`(어댑터 server-side dedup) → outcome `duplicate_completed`, row status `completed`, ledger 중복 0.
|
||||
- `LedgerConflictError`(portfolio-hub 409) → outcome `conflict_blocked`, row status `conflict_blocked` (자동 재시도 중단, 운영자 해소 대상).
|
||||
- `LedgerRetryableError`(timeout/5xx/network) → outcome `retryable_failure`, row status `pending` 유지(다음 tick 재시도).
|
||||
4. portfolio-hub 장기 장애에도 outbox 가 적재되어 무유실. 재시작 시 PostgreSQL `ledger_outbox.status='pending'` 행을 그대로 이어 처리한다.
|
||||
|
||||
## Shared Components
|
||||
|
||||
- SlotRepository Protocol :: 저장소 추상화. 구현체(InMemorySlotRepository, 향후 PostgreSQLSlotRepository)를 주입.
|
||||
@@ -104,3 +126,4 @@
|
||||
- 2026-06-03 :: BAT-001 이식 완료 반영 — preallocate/transit 흐름이 `trex_core.domain.slot` 실제 코드로 구현됨(REQ-004). 영속화/CLI 흐름은 여전히 미구현(범위 밖).
|
||||
- 2026-06-03 :: BAT-002 — 데몬 lifecycle + 매수/매도 intent HTTP 흐름 추가, Primary Runtime=daemon+library(REQ-005~009). 브로커 집행/safety floor 흐름은 후속 Discovery.
|
||||
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
|
||||
- 2026-06-04 :: BAT-006 — ledger 통합 반영(체결→ledger outbox enqueue, LedgerWriter retry/dedup/409 흐름, FillStatus.execution_id/fee 운반, slot 전이와 ledger write 분리) (REQ-017~020).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# req-020: ledger 레이어 baseline 문서 정합
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Documentation
|
||||
- Priority: Medium
|
||||
- Owner: project maintainers
|
||||
@@ -112,3 +112,36 @@
|
||||
|
||||
#### Validation Plan
|
||||
- 기존 Acceptance Criteria 로 batch delivery 에서 검증.
|
||||
|
||||
### CHG-20260604-03
|
||||
- Date: 2026-06-04
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- Status 변경: Approved -> Implemented. BAT-006 release-candidate verification evidence 기반.
|
||||
|
||||
#### Intent
|
||||
- runtime-flows / interface-contract / project-structure / data-model 4문서를 ledger 레이어와 정합시킨 baseline 갱신을 구현 완료 상태로 동기화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added/Updated/Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs/Modules/Tests: 없음 (변경은 상태 전이만)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-006 (release-candidate)
|
||||
- Releases:
|
||||
- none
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: Implemented
|
||||
- Follow-up Action: release 단계는 별도 진행
|
||||
|
||||
#### Validation Plan
|
||||
- AC-1: 4 baseline 문서(runtime-flows/interface-contract/project-structure/data-model) 에 ledger 레이어 섹션 반영 완료.
|
||||
- AC-2: baseline 문서 ↔ 코드 정합 manual review (verification.md Manual 섹션). NFR-1 import 스윕 + grep 결과로 보강.
|
||||
- AC-3: 4 baseline 문서 Change Log 모두 2026-06-04 BAT-006 항목 추가.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# req-019: ledger outbox durability + correlation_id 멱등
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Exception
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
@@ -114,3 +114,38 @@
|
||||
|
||||
#### Validation Plan
|
||||
- 기존 Acceptance Criteria 로 batch delivery 에서 검증.
|
||||
|
||||
### CHG-20260604-03
|
||||
- Date: 2026-06-04
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- Status 변경: Approved -> Implemented. BAT-006 release-candidate verification evidence 기반.
|
||||
|
||||
#### Intent
|
||||
- ledger outbox durability + correlation_id 멱등 + outcome 분류 구현과 검증 evidence 확보 후 구현 완료 상태로 동기화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added/Updated/Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs/Modules/Tests: 없음 (변경은 상태 전이만)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-006 (release-candidate)
|
||||
- Releases:
|
||||
- none
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: Implemented
|
||||
- Follow-up Action: release 단계는 별도 진행
|
||||
|
||||
#### Validation Plan
|
||||
- AC-1: tests/application/test_execution_ledger.py::test_duplicate_execution_id_results_in_zero_duplicate_in_ledger.
|
||||
- AC-2: tests/application/test_execution_ledger.py::test_retryable_failure_then_eventually_completes + tests/application/test_ledger_outbox.py::test_writer_retries_until_success.
|
||||
- AC-3: tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry + tests/application/test_ledger_outbox.py::test_writer_marks_conflict_blocked_and_does_not_retry.
|
||||
- AC-4: tests/application/test_execution_ledger.py::test_duplicate_completed_when_reprocessing_terminal_item + test_duplicate_execution_id_results_in_zero_duplicate_in_ledger.
|
||||
- AC-5: tests/application/test_execution_ledger.py::test_slot_transition_proceeds_when_ledger_is_down.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# req-017: LedgerPort 추상화 + portfolio-hub 어댑터 + stub 원장
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Integration
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
@@ -113,3 +113,37 @@
|
||||
|
||||
#### Validation Plan
|
||||
- 기존 Acceptance Criteria 로 batch delivery 에서 검증.
|
||||
|
||||
### CHG-20260604-03
|
||||
- Date: 2026-06-04
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- Status 변경: Approved -> Implemented. BAT-006 release-candidate verification evidence 기반.
|
||||
|
||||
#### Intent
|
||||
- LedgerPort/portfolio-hub adapter/in-memory stub 구현과 검증 evidence 확보 후 구현 완료 상태로 동기화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added/Updated/Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs/Modules/Tests: 없음 (변경은 상태 전이만)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-006 (release-candidate)
|
||||
- Releases:
|
||||
- none (배포 미생성)
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: Implemented
|
||||
- Follow-up Action: release 단계는 별도 진행
|
||||
|
||||
#### Validation Plan
|
||||
- AC-1: tests/adapters/ledger/test_in_memory_ledger.py::test_record_trade_preserves_fields_and_is_idempotent_on_execution_id.
|
||||
- AC-2: tests/adapters/ledger/test_portfolio_hub_ledger.py::test_post_contract_includes_execution_id_and_path_uses_asset_id.
|
||||
- AC-3: tests/adapters/ledger/test_portfolio_hub_ledger.py::test_409_surfaces_as_ledger_conflict_error + tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry.
|
||||
- AC-4: tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# req-018: 체결→원장 기록 (집행 루프 fill hook)
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Integration
|
||||
- Priority: High
|
||||
- Owner: project maintainers
|
||||
@@ -114,3 +114,38 @@
|
||||
|
||||
#### Validation Plan
|
||||
- 기존 Acceptance Criteria 로 batch delivery 에서 검증.
|
||||
|
||||
### CHG-20260604-03
|
||||
- Date: 2026-06-04
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- Status 변경: Approved -> Implemented. BAT-006 release-candidate verification evidence 기반.
|
||||
|
||||
#### Intent
|
||||
- 집행 루프 fill→ledger hook + FillStatus.execution_id/fee 확장 구현과 검증 evidence 확보 후 구현 완료 상태로 동기화.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added/Updated/Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs/Modules/Tests: 없음 (변경은 상태 전이만)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches:
|
||||
- BAT-006 (release-candidate)
|
||||
- Releases:
|
||||
- none
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: Implemented
|
||||
- Follow-up Action: release 단계는 별도 진행
|
||||
|
||||
#### Validation Plan
|
||||
- AC-1: tests/application/test_execution_ledger.py::test_buy_fill_enqueues_one_ledger_item_and_writer_records_it.
|
||||
- AC-2: tests/application/test_execution_ledger.py::test_sell_fill_enqueues_sell_ledger_item.
|
||||
- AC-3: 위 buy 테스트가 FillStatus.execution_id/fee/tax 운반 직접 검증.
|
||||
- AC-4: tests/application/test_execution_ledger.py::test_portfolio_id_absent_skips_ledger_but_transitions_slot.
|
||||
- AC-5: tests/domain/test_domain_import_sweep.py + grep 결과(trex_core/application/execution.py 단일 enqueue 지점).
|
||||
|
||||
+5
-5
@@ -97,15 +97,15 @@
|
||||
| REQ-014 | 주문 핸드오프 durability + reconciliation | Exception | Implemented | High | project maintainers | [req-014](./Exception/req-014_order-handoff-durability-reconciliation.md) |
|
||||
| REQ-015 | 브로커/집행 baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-015](./Documentation/req-015_broker-execution-baseline-alignment.md) |
|
||||
| REQ-016 | KIS 인증 토큰 Redis 캐시 | Integration | Implemented | Medium | project maintainers | [req-016](./Integration/req-016_kis-token-redis-cache.md) |
|
||||
| REQ-017 | LedgerPort 추상화 + portfolio-hub 어댑터 + stub | Integration | Approved | High | project maintainers | [req-017](./Integration/req-017_ledger-port-and-portfolio-hub-adapter.md) |
|
||||
| REQ-018 | 체결→원장 기록 (집행 루프 fill hook) | Integration | Approved | High | project maintainers | [req-018](./Integration/req-018_fill-to-ledger-recording.md) |
|
||||
| REQ-019 | ledger outbox durability + correlation_id 멱등 | Exception | Approved | High | project maintainers | [req-019](./Exception/req-019_ledger-outbox-durability-idempotency.md) |
|
||||
| REQ-020 | ledger 레이어 baseline 문서 정합 | Documentation | Approved | Medium | project maintainers | [req-020](./Documentation/req-020_ledger-baseline-alignment.md) |
|
||||
| REQ-017 | LedgerPort 추상화 + portfolio-hub 어댑터 + stub | Integration | Implemented | High | project maintainers | [req-017](./Integration/req-017_ledger-port-and-portfolio-hub-adapter.md) |
|
||||
| REQ-018 | 체결→원장 기록 (집행 루프 fill hook) | Integration | Implemented | High | project maintainers | [req-018](./Integration/req-018_fill-to-ledger-recording.md) |
|
||||
| REQ-019 | ledger outbox durability + correlation_id 멱등 | Exception | Implemented | High | project maintainers | [req-019](./Exception/req-019_ledger-outbox-durability-idempotency.md) |
|
||||
| REQ-020 | ledger 레이어 baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-020](./Documentation/req-020_ledger-baseline-alignment.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-06-04 :: REQ-017~020 Approved → Implemented — BAT-006 release-candidate verification evidence 기반. (confirm-req-implemented)
|
||||
- 2026-06-04 :: REQ-017~020 Proposed -> Approved — 승인 게이트 통과. (confirm-req)
|
||||
- 2026-06-04 :: REQ-017~020 최초 생성(Proposed) — dcy-004 FR-1~FR-5 분해(NFR-1~3 흡수). (draft-req)
|
||||
- 2026-06-04 :: REQ-016 생성 + Approved — KIS 토큰 Redis 캐시 (minor-change). (draft-req/confirm-req)
|
||||
- 2026-06-04 :: REQ-011~015 Approved → Implemented — BAT-004 verification evidence 기반. (confirm-req-implemented)
|
||||
- 2026-06-04 :: REQ-011~015 Proposed → Approved — 승인 게이트 통과. (confirm-req)
|
||||
|
||||
@@ -11,7 +11,13 @@ import psycopg
|
||||
from trex_core.adapters.persistence.migrate import apply_migrations, migration_files
|
||||
from trex_core.domain.slot import EXPECTED_SLOT_FIELDS
|
||||
|
||||
_EXPECTED_TABLES = {"daily_slot", "daily_symbol_entries", "slot_transition_log", "intent_log"}
|
||||
_EXPECTED_TABLES = {
|
||||
"daily_slot",
|
||||
"daily_symbol_entries",
|
||||
"slot_transition_log",
|
||||
"intent_log",
|
||||
"ledger_outbox", # BAT-006 / REQ-019
|
||||
}
|
||||
|
||||
|
||||
def _columns(dsn: str, table: str) -> set[str]:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""PostgreSQL ledger outbox integration tests (REQ-019).
|
||||
|
||||
Skipped without TREX_POSTGRESQL_TEST_DSN (same gate as other PG suites).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from trex_core.adapters.ledger.in_memory import InMemoryLedger
|
||||
from trex_core.adapters.persistence.postgresql import PostgreSQLLedgerOutbox
|
||||
from trex_core.application.ledger_outbox import (
|
||||
LedgerWriter,
|
||||
OutboxOutcome,
|
||||
WorkItemStatus,
|
||||
)
|
||||
from trex_core.application.ledger_port import LedgerTrade
|
||||
|
||||
|
||||
def _trade(execution_id="paper:1:10") -> LedgerTrade:
|
||||
return LedgerTrade(
|
||||
asset_id="PF-1", order_id="ORD-1", execution_id=execution_id,
|
||||
side="buy", qty=10, price=1000.0, fee=1.5, tax=0.0,
|
||||
executed_at=datetime(2026, 6, 4, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
def test_enqueue_persists_and_is_idempotent(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean)
|
||||
a = outbox.enqueue(_trade())
|
||||
b = outbox.enqueue(_trade())
|
||||
assert a.correlation_id == b.correlation_id
|
||||
pending = outbox.list_pending()
|
||||
assert len(pending) == 1
|
||||
assert pending[0].status is WorkItemStatus.PENDING
|
||||
|
||||
|
||||
def test_writer_completes_persisted_item(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean)
|
||||
ledger = InMemoryLedger()
|
||||
writer = LedgerWriter(outbox, ledger)
|
||||
outbox.enqueue(_trade(execution_id="pg-1"))
|
||||
stats = writer.run_once()
|
||||
assert stats.completed == 1 and ledger.count() == 1
|
||||
row = outbox.get("PF-1:ORD-1:pg-1")
|
||||
assert row is not None and row.status is WorkItemStatus.COMPLETED
|
||||
assert row.last_outcome is OutboxOutcome.COMPLETED
|
||||
|
||||
|
||||
def test_writer_marks_conflict_blocked_on_pg(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean)
|
||||
ledger = InMemoryLedger(conflict_executions={"pg-x"})
|
||||
writer = LedgerWriter(outbox, ledger)
|
||||
outbox.enqueue(_trade(execution_id="pg-x"))
|
||||
writer.run_once()
|
||||
row = outbox.get("PF-1:ORD-1:pg-x")
|
||||
assert row.status is WorkItemStatus.CONFLICT_BLOCKED
|
||||
assert row.last_outcome is OutboxOutcome.CONFLICT_BLOCKED
|
||||
|
||||
|
||||
def test_writer_retries_on_pg(pg_clean):
|
||||
outbox = PostgreSQLLedgerOutbox(pg_clean)
|
||||
ledger = InMemoryLedger(fail_n_then_succeed=1)
|
||||
writer = LedgerWriter(outbox, ledger)
|
||||
outbox.enqueue(_trade(execution_id="pg-r"))
|
||||
writer.run_once()
|
||||
row1 = outbox.get("PF-1:ORD-1:pg-r")
|
||||
assert row1.status is WorkItemStatus.PENDING
|
||||
assert row1.last_outcome is OutboxOutcome.RETRYABLE_FAILURE
|
||||
writer.run_once()
|
||||
row2 = outbox.get("PF-1:ORD-1:pg-r")
|
||||
assert row2.status is WorkItemStatus.COMPLETED
|
||||
assert ledger.count() == 1
|
||||
@@ -89,19 +89,26 @@ def test_portfolio_id_absent_skips_ledger_but_transitions_slot():
|
||||
|
||||
|
||||
def test_duplicate_execution_id_results_in_zero_duplicate_in_ledger():
|
||||
"""REQ-019 AC-1: same execution_id enqueued twice = 1 ledger entry."""
|
||||
"""REQ-019 AC-1: same execution_id processed twice = 1 ledger entry."""
|
||||
ledger = InMemoryLedger()
|
||||
repo, svc, intents, execution, outbox, writer = _wire(PaperBroker(fill_after_polls=0),
|
||||
ledger=ledger)
|
||||
_buy(intents).slot_id
|
||||
_buy(intents)
|
||||
execution.run_once()
|
||||
writer.run_once()
|
||||
assert ledger.count() == 1
|
||||
# Re-enqueue the same trade explicitly (simulate accidental re-fill).
|
||||
# Same correlation_id arriving again (re-enqueue) is dedup'd at the outbox.
|
||||
first = outbox.all_items()[0]
|
||||
outbox.enqueue(first.trade) # idempotent on correlation_id
|
||||
stats = writer.run_once() # still terminal-completed; reports duplicate
|
||||
assert stats.duplicates >= 1
|
||||
outbox.enqueue(first.trade)
|
||||
assert len(outbox.all_items()) == 1
|
||||
# And even if the writer somehow re-processes (simulated by a fresh outbox
|
||||
# row for the same execution_id, e.g. after a manual replay), the ledger
|
||||
# adapter dedup'd it server-side and the outcome is duplicate_completed.
|
||||
replay_outbox = InMemoryLedgerOutbox()
|
||||
replay_writer = LedgerWriter(replay_outbox, ledger)
|
||||
replay_outbox.enqueue(first.trade)
|
||||
stats = replay_writer.run_once()
|
||||
assert stats.duplicates == 1 and stats.completed == 0
|
||||
assert ledger.count() == 1 # no duplicate in the ledger
|
||||
|
||||
|
||||
|
||||
+7
-1
@@ -9,7 +9,13 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
_PG_TABLES = ("intent_log", "slot_transition_log", "daily_symbol_entries", "daily_slot")
|
||||
_PG_TABLES = (
|
||||
"ledger_outbox",
|
||||
"intent_log",
|
||||
"slot_transition_log",
|
||||
"daily_symbol_entries",
|
||||
"daily_slot",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -126,13 +126,14 @@ class LedgerWriter:
|
||||
return stats
|
||||
|
||||
def _process(self, item: LedgerWorkItem) -> OutboxOutcome:
|
||||
# Local dedup: another writer pass (or restart) already completed this.
|
||||
# Local dedup: another writer pass (or restart) already completed this
|
||||
# outbox row. Skip the adapter call.
|
||||
existing = self._outbox.get(item.correlation_id)
|
||||
if existing is not None and existing.status is WorkItemStatus.COMPLETED:
|
||||
self._outbox.mark_outcome(item.correlation_id, OutboxOutcome.DUPLICATE_COMPLETED)
|
||||
return OutboxOutcome.DUPLICATE_COMPLETED
|
||||
try:
|
||||
self._ledger.record_trade(item.trade)
|
||||
result = self._ledger.record_trade(item.trade)
|
||||
except LedgerConflictError as exc:
|
||||
self._outbox.mark_outcome(
|
||||
item.correlation_id, OutboxOutcome.CONFLICT_BLOCKED, error=str(exc)
|
||||
@@ -143,6 +144,12 @@ class LedgerWriter:
|
||||
item.correlation_id, OutboxOutcome.RETRYABLE_FAILURE, error=str(exc)
|
||||
)
|
||||
return OutboxOutcome.RETRYABLE_FAILURE
|
||||
# Adapter-level dedup (server says "already recorded"): outcome is
|
||||
# duplicate_completed; the ledger has exactly one entry for this
|
||||
# execution_id either way (REQ-019 AC-1, AC-4).
|
||||
if result is LedgerWriteResult.DUPLICATE_OK:
|
||||
self._outbox.mark_outcome(item.correlation_id, OutboxOutcome.DUPLICATE_COMPLETED)
|
||||
return OutboxOutcome.DUPLICATE_COMPLETED
|
||||
self._outbox.mark_outcome(item.correlation_id, OutboxOutcome.COMPLETED)
|
||||
return OutboxOutcome.COMPLETED
|
||||
|
||||
|
||||
Reference in New Issue
Block a user