feat(reconcile): safely seed canonical cost basis

This commit is contained in:
Hermes Agent
2026-07-17 09:57:08 +09:00
parent 18bd8d9f0b
commit 81a853a314
25 changed files with 1689 additions and 66 deletions
@@ -24,9 +24,8 @@
| 모듈 | 변경/소유권 | 구현 경계 |
| --- | --- | --- |
| `trex_core/application/broker_provider.py` (신규) | `BrokerProvider` protocol, source metadata, credential resolution, broker reuse/invalidation policy의 application ownership | portfolio-hub read port와 KIS/paper construction을 조립하되 CLI 출력/DB write를 소유하지 않는다. |
| `trex_core/runtime/broker_factory.py` (신규) 또는 `runtime/daemon.py`의 명시적 factory | `DaemonConfig` → provider assembly. 기존 `_build_broker` 직접 사용자를 provider로 이관 | runtime composition만 담당하며 credential 값을 로그/JSON으로 만들지 않는다. 구현 시 순환 import가 생기면 factory를 별도 runtime module로 둔다. |
| `trex_core/runtime/daemon.py` | daemon은 process-lifetime provider에서 broker를 획득하고 scheduler에 같은 provider/reload callback을 주입 | daemon이 별도 env broker를 만들거나 scheduler와 다른 credential source를 해석하지 않는다. |
| `trex_core/runtime/broker_provider.py` | `BrokerProvider` protocol, source metadata, credential resolution, broker reuse/invalidation policy의 runtime composition ownership | portfolio-hub read port와 KIS/paper construction을 조립하되 CLI 출력/DB write를 소유하지 않는다. 현재 구현의 `ConfigBrokerProvider`가 daemon과 CLI의 공통 경계다. |
| `trex_core/runtime/daemon.py` | `DaemonConfig` → provider assembly 및 process-lifetime provider 주입 | runtime composition만 담당하며 credential 값을 로그/JSON으로 만들지 않는다. legacy `_build_broker`는 env-only compatibility helper로 남아 있으므로 새 broker-read 경로에서 직접 사용하지 않는다. |
| `trex_core/application/snapshot_scheduler.py` | day-start fetched credential 성공/실패 및 broker reload를 provider contract로 호출 | scheduler의 entry gate 역할은 유지하되, fetched failure 뒤 env credential으로 trading/broker-read를 계속하는 현재 fallback은 제거한다. |
| `trex_core/runtime/cli.py` | `--config`가 있으면 daemon과 동일한 `DaemonConfig.from_config(toml_path, env_file=...)`로 instance TOML을 로드하고, `_cmd_sync`/`_cmd_verify`가 그 config의 같은 provider를 호출 | broker read가 성공하기 전 slot/warning write, `broker_qty_zero`, verify success를 만들지 않으며 CLI가 TOML `credential_fetch_enabled`를 env-only default로 덮어쓰지 않는다. |
| `trex_core/adapters/broker/kis.py` | typed sanitized `BrokerBalanceError`, strict `get_balance()`, token/error mapping | credential/token/header/raw account/payload를 error message, log, CLI JSON에 싣지 않는다. |
@@ -275,3 +274,54 @@ REQ-119~121 implementation and independent QC evidence가 확정된 뒤에만 RE
- [ ] 4-case reconciliation matrix가 existing A/B/C behavior와 additive divergence effect를 구분한다.
- [ ] REQ-119~122 AC별 test/document evidence가 §9에 연결되었다.
- [ ] live service, credentials, production DB, `sync --live`를 사용하지 않았고 implementation/baseline update authorization과 혼동하지 않았다.
## 12. Evidence boundary and unresolved gates
### Source evidence inspected
The following repository evidence is the basis for this design (paths are relative to
this trex-core root):
- `trex_core/runtime/broker_provider.py`: `ConfigBrokerProvider` owns one broker per
process/command, distinguishes `env`/`fetched`/`break_glass`, and converts fetched
credential read failures to an allow-listed `BrokerCredentialUnavailable` code.
- `trex_core/adapters/broker/kis.py` and
`trex_core/application/broker_port.py`: balance reads validate `rt_cd` and
`output1`, return `[]` only for a validated empty response, and raise the
allow-listed `BrokerBalanceError` codes for failure classes.
- `trex_core/domain/slot/repository.py`,
`trex_core/domain/slot/in_memory_repository.py`, and
`trex_core/adapters/persistence/postgresql.py`: the instance-and-symbol query is
read-only, deterministic, and implemented with the same ordering rule in both
adapters.
- `trex_core/application/reconciliation.py`: the query is called only for an
unmatched positive broker holding; the result selects the additive decision code
without a persistence mutation.
- `tests/runtime/test_broker_provider.py`,
`tests/runtime/test_cli_broker_failure.py`, and
`tests/adapters/broker/test_kis.py`: fetched fallback prevention, CLI failure
boundary, sanitized balance failure, and validated-empty fixtures exist. The
repository parity evidence remains fake-driver based, not a live PostgreSQL
engine run.
### Unresolved blockers and explicit non-decisions
1. **Cross-service contract gate — unresolved for implementation/release:** this
repository contains only the consumer-side `PortfolioHubReadPort`. A live
portfolio-hub contract/integration fixture proving the credential-version and
`trading_enabled` semantics is not available here. Implementation must not
change the portfolio-hub API; any required wire change stops at a lead-owned
cross-service approval gate.
2. **Security gate — unresolved for release:** source-level sanitization and
deny-list tests are present, but production log sink, tracing, and portfolio-hub
observability configuration are outside this repository and have not been
independently reviewed. No credential or raw account evidence may be collected
until that review is complete.
3. **Migration gate — currently no schema migration is designed:** the lookup is
read-only over existing `daily_slot` columns and does not authorize an index or
schema change. If production query-plan evidence later requires an index, that
is a separate migration-approved batch, not an implementation assumption here.
4. **Scope/approval gate — explicit hold:** baseline document edits, production
configuration changes, deployment, and `sync --live` remain outside this design
gate. The next authorized step is `run-batch-implementation`; it must not be
started from this task without the lead's gate transition.
@@ -0,0 +1,40 @@
# BAT-058 Design
- Status: in-progress
- Batch: BAT-058
## 핵심 설계 결정
### 1) cost-basis 응답 파싱 규칙
- `portfolio-hub` 응답에서 `cost_basis_per_unit`(canonical)을 최우선으로 사용
- canonical key가 존재하면 `avg_cost/wac/cost_basis`로 fallback 금지
- canonical이 없을 때만 legacy chain(`avg_cost`, `wac`, `cost_basis`)를 순차적으로 시도
- 유효성: `None` 금지, 숫자 변환 가능, 0보다 큰 값만 허용
### 2) 진단값 분리
- adapter 수준에서 기존 `get_cost_basis()`는 값만 반환
- 새 진단 API `get_cost_basis_with_diagnostics()` 추가:
- `(value, source, reason)`
- public `cost_basis_source`는 `canonical` / `legacy` / `none`만 허용한다. adapter의 `avg_cost` / `wac` / `cost_basis` alias는 `legacy`로 정규화하며, 진단 port의 미승인/비문자 source는 `none`으로 정규화한다.
- public `cost_basis_reason`는 `missing`, `invalid`, `http_status_<3-digit>`, `transport`, `invalid_json`, `invalid_payload`, `not_configured`, `diagnostic_exception`, `diagnostic_source_invalid` 또는 유효한 값이 seed되었을 때의 `null`만 허용한다. `http_status_<3-digit>`만 동적 형식이며 provider field name, parser detail, 임의 오류 텍스트는 공개 값으로 허용하지 않는다.
- adapter parser의 field-level detail은 private implementation detail이다. sync 결과와 `sync --dry-run/--live` 출력에는 위 source/reason taxonomy만 노출한다.
### 3) sync fail-closed 정책
- `slot.status == INACTIVE`, `broker_qty > 0`, 동기화 결과 `avg_price` 미확보 시:
- `dry_run=True`: write 없음 preview 반환 (`after`는 before와 동일), `skipped=True`, `skip_reason=cost_basis_unavailable`
- `dry_run=False`: no-write fail-closed. `error='cost_basis_unavailable'`, `skipped=True`, `after=before`
- 기존 `HOLDING` 및 `PENDING_*`, `ERROR` 경로는 기존 규칙 유지
### 4) 로그/출력
- sync 결과 객체에 아래 필드 추가: `seeded_avg_price`, `cost_basis_source`, `cost_basis_reason`
- CLI 출력에서 위 값 표시
- 실패 시 reason을 노출해 운영자가 어떤 fallback/오류가 발생했는지 즉시 확인 가능
## 비(非)설계 대상
- `portfolio-hub` API 스키마 변경/인증 변경
- 실제 live sync 수행이나 운영 배포 실행
## 검증 전략
- adapter 단위 테스트: canonical 우선/legacy fallback/invalid canonical non-fallback/이유 분리
- sync application 테스트: inactive fail-closed와 dry-run preview, holding 기본 동기화 동작 회귀 보존
- 최소 통합: 기존 테스트셋에서 관련 케이스가 모두 통과되는지 확인
@@ -0,0 +1,33 @@
# BAT-058 Implementation
- Status: in-progress
- Batch: BAT-058
## 변경 대상
1. `trex_core/adapters/portfolio/hub_read.py`
- `get_cost_basis_with_diagnostics()` 새 메서드 추가
- canonical(`cost_basis_per_unit`) 우선 파싱 + legacy fallback 체인 적용
- invalid/parse 오류 reason 문자열 반환
2. `trex_core/application/reconciliation.py`
- `sync_slot_to_broker()`에서 cost-basis 진단 메서드 사용
- inactive onboarding & no cost basis 시 dry-run/no-write + live fail-closed 구현
- sync 결과에 `seeded_avg_price`, `cost_basis_source`, `cost_basis_reason` 추가
3. `trex_core/runtime/cli.py`
- sync 결과 출력에 `cost_basis.seeded`, `cost_basis.source`, `cost_basis.reason`, `error` 표시
4. `tests/adapters/portfolio/test_hub_read.py`
- get_cost_basis canonical/legacy/invalid/오류 reason 검증 추가
5. `tests/application/test_sync.py`
- inactive no-cost_basis live/dry-run 동작
- hold/warning scenarios 회귀 보존
## 상태
- planning: completed
- design: completed
- implementation: completed
- draft-batch-verification: completed
@@ -0,0 +1,29 @@
# BAT-058: cost-basis canonical parse 및 안전 sync seed hotfix
- Status: release-candidate (lead `confirm-batch-verification`, 2026-07-17; Release gate/approval 미실행)
- Profile: standard
- Source Discovery: dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding
- Source REQ: REQ-123
- Included REQ: REQ-123
- Excluded REQ: 없음
## Scope
### In Scope
- REQ-123(제안된 cost-basis canonical parse 및 WAC unavailable fail-closed seed)
- portfolio-hub 응답 `cost_basis_per_unit` canonical 우선 파싱 고정
- legacy alias fallback/invalid 처리 규칙 구현
- dry-run 진단 및 live no-write fail-closed semantics
- adapter/application regression test 확장 및 baseline 문서 정합
### Out of Scope
- portfolio-hub API/schema/인증/토큰/설정 변경
- DB migration / 데이터 삭제 / 실서버 직접 수정
- `sync --live`, 배포/운영 재시작 실행은 별도 release 승인 단계
## Notes
- 이 batch는 `suggest-batch-reqs` 다음 단계로 생성한 lead-승인 배치입니다.
- implementation 전환 직전까지는 설계 산출물을 대체하지 않고, 해당 scope만 구현/검증합니다.
@@ -0,0 +1,43 @@
# BAT-058 Planning
- Status: in-progress
- Batch: BAT-058
- Profile: standard
## Included REQ
- REQ-123 (Approved): cost-basis canonical 우선 파싱 + safe sync seeding
## Delivery Plan
1. `trex_core.adapters.portfolio.hub_read`에서 `cost-basis` 응답 파싱 순서 정립
- 우선순위: `cost_basis_per_unit`(canonical) -> `avg_cost` -> `wac` -> `cost_basis`
- canonical가 present이면 legacy fallback를 금지하고 이유를 기록
- 유효성 검사(형변환/양수) 실패 시 source/reason 분리
2. `sync_slot_to_broker`에 WAC 사용 경로를 cost-basis 진단 결과와 함께 통합
- INACTIVE + broker_qty>0 + 가격 미확보 시 dry-run은 preview만
- live에서는 no-write fail-closed + exit fail
3. `tests/adapters/portfolio/test_hub_read.py`와 `tests/application/test_sync.py`를 AC-기반으로 확장
- canonical 우선 규칙, invalid canonical non-fallback, unavailable reason 보존
- inactive onboarding no-write dry-run / live fail-closed
4. `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/operations.md` baseline 동기화
5. `docs/batches/bat-058...` 산출물(implementation + verification) 작성 후 리드 검토 제출
## Dependencies
- modules: `trex_core/adapters/portfolio/hub_read.py`, `trex_core/application/reconciliation.py`, `trex_core/runtime/cli.py`
- tests: `tests/adapters/portfolio/test_hub_read.py`, `tests/application/test_sync.py`
- docs: `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/operations.md`
## Design Gate
- Design Required Before Implementation: yes
- Reason: 계약 precedence 및 fail-closed semantics이 배포 안전성에 직결되어, 미리 수용 기준(재기록/no-write/오류 로그)을 고정해야 함.
## Milestones
1. planning 확정 (현재)
2. design 작성
3. implementation 완료
4. draft-batch-verification 작성 및 테스트 실행
5. QC handoff
## Risks
- R1 High: canonical 값 parse 실패 시 legacy fallback를 허용하면 REQ-123 위반 가능성(의도치 않은 source 사용)
- R2 High: inactive onboarding에서 가격 미확보일 때 live write가 진행될 경우 거래원장·slot 상태 오염
- R3 Medium: 비용/시간 과다로 기존 `cost_basis` 회귀 테스트 간과 시 interface contract drift
@@ -0,0 +1,74 @@
# BAT-058 Verification
- Status: release-candidate (lead `confirm-batch-verification`, 2026-07-17)
- Batch: BAT-058
- Verified By: delivery-runner (implementation evidence) + independent `dev-qc` QC-7
- Confirmed By: trex-dev-lead (QC-7 `PASS-WITH-NOTES` 수용; 사용자 지시: “QC-7 PASS-WITH-NOTES로 batch verification을 진행해”)
## Verification 범위
- Scope: REQ-123 및 `dcy-038` 기반 hotfix 범위
- `trex_core/adapters/portfolio/hub_read.py`
- `trex_core/application/reconciliation.py`
- `trex_core/runtime/cli.py`
- `tests/adapters/portfolio/test_hub_read.py`
- `tests/application/test_sync.py`
## AC 점검 결과
| AC | 내용 | 결과 | 근거 |
|---|---|---|---|
| AC-1 | canonical `cost_basis_per_unit` 우선 파싱 + 유효성 검증 | ✅ PASS | `test_get_cost_basis_with_diagnostics_prefers_canonical_when_valid`, `test_get_cost_basis_with_diagnostics_canonical_null_blocks_legacy`, `test_get_cost_basis_with_diagnostics_canonical_non_numeric_blocks_legacy`, `test_get_cost_basis_with_diagnostics_canonical_negative_blocks_legacy` |
| AC-2 | canonical invalid 시 legacy masking 금지, canonical 부재 시 legacy fallback 순차 적용 | ✅ PASS | `test_get_cost_basis_with_diagnostics_canonical_invalid_blocks_legacy`, `test_get_cost_basis_with_diagnostics_legacy_chain_fallback_order`, `test_get_cost_basis_with_diagnostics_legacy_skips_invalid_values` |
| AC-3 | 성공/실패 이유 분류(reason) | ✅ PASS | `test_get_cost_basis_with_diagnostics_canonical_null_blocks_legacy`, `test_get_cost_basis_with_diagnostics_canonical_non_numeric_blocks_legacy`, `test_get_cost_basis_with_diagnostics_canonical_negative_blocks_legacy`, `test_get_cost_basis_with_diagnostics_missing_field_returns_missing_reason`, `test_get_cost_basis_with_diagnostics_all_legacy_invalid_returns_last_reason` |
| AC-4 | non-2xx/transport 실패 사유 분류 | ✅ PASS | `test_get_cost_basis_with_diagnostics_non_2xx_or_transport_keeps_diagnostic` |
| AC-5 | dry-run no-write preview (seeded=true/false 반영) | ✅ PASS | `test_sync_dry_run_no_write`, `test_sync_dry_run_inactive_valid_canonical_seed_previews_without_writes` |
| AC-6 | dry-run `inactive + broker_qty>0 + avg_price unavailable` no-write preview | ✅ PASS | `test_sync_dry_run_no_write` |
| AC-7 | live fail-closed (status/qty/avg_price 모두 미반영, non-zero 종료) | ✅ PASS | independent QC-7이 `test_sync_fail_closed_skipped_error_exits_nonzero_without_writes` 및 legacy-only `0.0`/negative/NaN/+Inf no-write 회귀를 재실행·검토함; CLI rc=1, mutation SQL 0, warning clear 0 확인 |
| AC-8 | HOLDING 정상 동기화 회귀 및 seed 적용 유지 | ✅ PASS | `test_sync_holding_qty_mismatch_updated`, `test_sync_seeds_avg_price_from_cost_basis_port`, `test_sync_seeds_avg_price_holding_with_cost_basis`, `test_sync_degrades_gracefully_cost_basis_returns_none` |
| AC-9 | response-contract + regression 테스트 전체 통과 | ✅ PASS | `tests/runtime/test_cli_operator.py`, `tests/adapters/portfolio/test_hub_read.py`, `tests/application/test_sync.py` (independent QC-7 affected suite: 113 passed; full suite: 833 passed, 47 skipped) |
| AC-10 | public diagnostic source/reason contract normalization (raw field/detail non-leak) 및 release gate | ✅ PASS (release 실행은 별도 gate) | independent QC-7이 `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/operations.md`와 bounded diagnostic/no-leak 구현을 대조함. REQ가 요구하는 actual release artifact·approval·live evidence는 Release 단계에서만 생성/판정하며, 본 batch verification은 이를 승인하거나 대체하지 않음. |
## 독립 QC-7 및 Lead `confirm-batch-verification`
- QC-7 실행: 2026-07-17 00:30:22 UTC 시작, supervised child exit `0`.
- QC verdict: `PASS-WITH-NOTES`.
- QC-7 독립 재실행: legacy-invalid focused `8 passed, 19 deselected`; affected suite `113 passed`; full suite `833 passed, 47 skipped`; Ruff `All checks passed`; compileall exit `0`; `git diff --check` exit `0`.
- Lead 판정: 사용자 지시에 따라 QC-7 `PASS-WITH-NOTES`를 수용하여 BAT-058의 `confirm-batch-verification`을 완료하고 `release-candidate`로 승격한다.
- Note 해소: REQ-123 AC-10의 release artifact/explicit approval/dry-run evidence/live execution은 **Release 단계의 별도 gate**다. 이 batch confirmation은 release approval, `sync --live`, 배포, credential 접근을 승인하지 않는다.
- Ledger note: 이전 QC 반복 횟수 표기는 과거 handoff 사이에 불일치가 있어 이 verification의 판정 근거로 사용하지 않는다. 이번 confirmation의 근거는 QC-7의 독립 verdict와 위 재실행 증적이다.
## 실행 커맨드 및 실제 재실행 결과
아래는 delivery worktree에서 실행한다. 공유 가상환경은 worktree의 상위 repository root에 있으므로 `git rev-parse`로 worktree root를 해석해 사용한다.
```bash
WORKTREE="$(git rev-parse --show-toplevel)"
PYTHON="${TREX_CORE_PYTHON:-$(cd "$WORKTREE/../.." && pwd)/.venv312/bin/python}"
cd "$WORKTREE"
"$PYTHON" -m pytest tests/runtime/test_cli_operator.py tests/adapters/portfolio/test_hub_read.py tests/application/test_sync.py -q
"$PYTHON" -m pytest -q
"$PYTHON" -m ruff check trex_core/runtime/cli.py trex_core/adapters/portfolio/hub_read.py trex_core/application/reconciliation.py tests/runtime/test_cli_operator.py tests/adapters/portfolio/test_hub_read.py tests/application/test_sync.py
"$PYTHON" -m compileall -q trex_core/runtime/cli.py trex_core/adapters/portfolio/hub_read.py trex_core/application/reconciliation.py tests/runtime/test_cli_operator.py tests/adapters/portfolio/test_hub_read.py tests/application/test_sync.py
```
- focused pytest (2026-07-17, QC-5 재실행): `105 passed in 0.28s`
- full pytest (2026-07-17, QC-5 재실행): `825 passed, 47 skipped in 1.71s`
- ruff (2026-07-17, QC-5 재실행): `All checks passed!`
- compileall (2026-07-17, QC-5 재실행): `compileall: PASS` (exit 0; quiet mode)
- `git diff --check` (2026-07-17, QC-5 재실행): `PASS` (exit 0; no output)
- QC-6 targeted pytest (2026-07-17): `tests/application/test_sync.py -q -k legacy_only_invalid_wac` → `8 passed, 19 deselected in 0.05s`.
- QC-6 focused pytest (2026-07-17): `tests/runtime/test_cli_operator.py tests/adapters/portfolio/test_hub_read.py tests/application/test_sync.py -q` → `113 passed in 0.29s`.
- QC-6 full pytest (2026-07-17): `pytest -q` → `833 passed, 47 skipped in 1.72s`.
- QC-6 ruff (2026-07-17): 대상 6개 source/test 파일 → `All checks passed!` (exit 0).
- QC-6 compileall (2026-07-17): 대상 6개 source/test 파일 → exit 0 (quiet mode; no output).
## 미해결/미실행 항목
- 실운영 배포, `sync --live` 실행, DB 직접 수정은 실행하지 않음.
- 본 문서는 implementation self-approval가 아니다. 독립 QC-7 `PASS-WITH-NOTES`와 lead `confirm-batch-verification`까지 반영한 batch verification 기록이며, release approval은 포함하지 않는다.
## 위험/차단 요인
- 없음. 단, Release 단계에서 REQ-123 AC-10의 explicit release approval, 단일 대상 live 실행 승인, rollback 기준, post-release monitoring plan을 별도 `draft-release` 산출물로 점검해야 한다.
+1
View File
@@ -63,3 +63,4 @@
| BAT-053 | standard | release-candidate | REQ-112, REQ-113, REQ-114, REQ-115 | dcy-036 | [bat-053](./bat-053_20260628_daystart-credential-fetch-failsafe/) |
| BAT-054 | standard | in-progress | REQ-113 (AC-2/AC-4 보강), REQ-115 (AC-1) | dcy-036 | [bat-054](./bat-054_20260628_daystart-credential-kis-inject/) |
| BAT-057 | standard | release-candidate | REQ-119, REQ-120, REQ-121, REQ-122 | dcy-037 | [bat-057](./bat-057_20260714_operator-broker-truth-hotfix/) |
| BAT-058 | standard | release-candidate | REQ-123 | dcy-038 | [bat-058](./bat-058_20260716_cost-basis-safe-sync-seed/) |