Files
portfolio-hub/docs/srs/Interface/req-037_ledger-trade-write-accounting-and-state-response.md
T
genichinandClaude Opus 4.8 2a19b7f058 fix: ledger write idempotency for TimescaleDB hypertable (BAT-016 rework)
BAT-016's initial implementation passed only against mocked repositories and
broke on a real database. Two defects fixed:

1. insert_ledger_trade used `ON CONFLICT (portfolio_id, order_id, execution_id)
   WHERE order_id IS NOT NULL` but portfolio_ledger is a TimescaleDB hypertable,
   which cannot have that partial unique index (must include the partitioning
   column recorded_at). The statement errored against Postgres. Replaced with
   application-level idempotency: SELECT-before-insert inside the existing
   `SELECT ... FOR UPDATE` portfolio row lock, which serializes concurrent
   same-portfolio writes (race-safe). Duplicate returns current authoritative
   portfolio state (dcy-006 OQ-4 b).

2. `with self._connect() as conn:` closes the shared psycopg3 connection on exit,
   leaving the reused repository connection unusable. Switched to
   `with conn.transaction():` (commits without closing).

Added tests/test_ledger_consolidation_integration.py — real-Postgres integration
tests (new / duplicate / negative-balance) that close the mock-only verification
gap. Full suite: 376 passed.

SDLC: REQ-036/037 CHG-20260605-03 reconcile OQ-7 (ii) (DB partial unique →
application-level idempotency) since the hypertable makes the DB constraint
infeasible. REQ-036/037/038 → Implemented; BAT-016 → release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:11:17 +09:00

12 KiB

REQ-037: 신 path 회계 책임 단일화 + 응답에 portfolio 상태 포함

  • Status: Implemented
  • Type: Interface
  • Priority: High
  • Owner: Justin Ko

Intent

  • POST /v1/portfolios/{portfolio_id}/ledger/trades를 portfolio 회계의 단일 권위 endpoint로 단일화한다. ledger 기록 시 portfolio cash_balance/asset_quantity를 트랜잭션 갱신하고, 응답에 거래 반영 후 권위 상태를 포함해 trex-core daemon의 drift를 원천 차단한다(issue #8).
  • 현재 audit-only 흐름(insert_ledger_trade가 ledger_trades INSERT만 수행)을 portfolio_ledger 단일 원장 기록 + portfolio 갱신 + 응답 확장으로 재정의한다.

Requirement

  • 신 path handler(app/api/v1_portfolios.py:514-588 record_ledger_trade_v1)가 다음을 단일 DB 트랜잭션으로 수행한다(dcy-006 FR-6):
    • portfolios row를 SELECT ... FOR UPDATE로 잠근다(FR-7, lost update 방지).
    • 회계 공식 적용: side="buy" → cash_balance -= qty*price + fee + tax, asset_quantity += qty; side="sell" → cash_balance += qty*price - fee - tax, asset_quantity -= qty.
    • 잔고/수량 부족 거부 검증을 적용하지 않는다 — 음수 cash_balance/asset_quantity 모두 허용(dcy-006 OQ-3). 음수 검증을 제거한 회계 헬퍼를 ledger 흐름 전용으로 추출하거나 inline 구현(기존 apply_buy/apply_sell은 레거시 전용으로 보존, R-8).
    • portfolio_ledger에 INSERT(REQ-036 신 스키마 사용). broker 식별자 order_id/execution_id/side/executed_at + 갱신 후 잔고 snapshot(cash_balance_after, asset_quantity_after, total_value_after) 기록.
    • portfolios 테이블 UPDATE(cash_balance, asset_quantity, last_trade_at, updated_at).
  • duplicate 감지(dcy-006 FR-15, CHG-20260605-03 재정의): application-level idempotency — SELECT ... FOR UPDATE로 portfolios row를 잠근 상태에서 (portfolio_id, order_id, execution_id)로 기존 portfolio_ledger row를 SELECT한다. 존재하면 duplicate(INSERT/UPDATE 없이 현재 권위 상태 응답), 없으면 INSERT. portfolio_ledger가 TimescaleDB hypertable이라 DB partial UNIQUE index/ON CONFLICT가 불가능하므로(REQ-036 CHG-20260605-03) DB 제약 대신 row lock 직렬화로 race-safe 보장. order_id NULL(레거시/수동)은 dedup 조회 생략 → append-only. 트랜잭션은 conn.transaction() 사용(psycopg3 with conn:는 공유 connection을 close하므로).
  • status="new"(신규 row): 거래 반영 후 권위 상태를 응답한다(FR-2).
  • status="duplicate"(재호출): ledger row 재생성 없이 duplicate 호출 시점의 portfolios 테이블 현재 권위 상태를 SELECT로 조회해 응답한다(FR-3, dcy-006 OQ-4 (b)).
  • LedgerTradeResponse(app/schemas/v1.py)에 cash_balance_after: float, asset_quantity_after: float 두 필드를 응답 root에 flat으로 비파괴 추가한다(FR-1, dcy-006 OQ-2 (b)). 기존 응답 필드는 모두 보존.
  • 기존 인가(daemon 3단계: token daemon_id == portfolio.managed_by_daemon_id), idempotency, status 의미를 보존한다(FR-4).
  • 도메인 모델 통합(FR-13): LedgerEntry와 LedgerTrade dataclass를 LedgerEntry 단일로 통합. LedgerTrade 삭제. broker 필드(ledger_entry_id, order_id, execution_id, side, executed_at)를 LedgerEntry에 흡수.
  • repository 통합(FR-14): insert_ledger_trade를 portfolio_ledger insert + portfolios UPDATE 단일 트랜잭션 함수로 재정의(또는 통합). _row_to_ledger_trade는 _row_to_ledger_entry와 통합.

Acceptance Criteria

  1. Given service token + daemon 배정 active portfolio(cash=1,000,000, qty=0) + buy 10@80,000 fee 100, When POST /v1/portfolios/{id}/ledger/trades, Then HTTP 201 + status="new" + 응답 cash_balance_after=199900(1,000,000 - 800,000 - 100) + asset_quantity_after=10. DB portfolios도 동일 값으로 갱신.
  2. Given AC-1 직후 같은 (portfolio_id, order_id, execution_id)로 재호출, When 동일 payload, Then HTTP 200 + status="duplicate" + 응답 cash_balance_after/asset_quantity_after는 현재 portfolios 권위 값. portfolio_ledger row는 1건 유지(재생성 없음).
  3. Given sell로 보유 수량보다 큰 수량 또는 cash를 초과하는 buy, When 호출, Then HTTP 201(거부하지 않음) + 음수 cash_balance_after 또는 음수 asset_quantity_after 반환(음수 허용, OQ-3).
  4. Given 기존 응답 필드(ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at, recorded_at, status), When AC-1 응답 확인, Then 모두 보존되고 cash_balance_after/asset_quantity_after만 추가된다.
  5. Given service token daemon_id IS NULL 또는 daemon mismatch, When 호출, Then 기존 인가 동작(HTTP 403 fail-closed) 보존.
  6. Given codebase grep grep -rn "ledger_trades\|insert_ledger_trade\|class LedgerTrade\b" app/, When 본 REQ + REQ-036 구현 후, Then ledger_trades 테이블 INSERT 코드 및 LedgerTrade dataclass 참조가 0건(LedgerEntry 단일).
  7. Given 동시 ledger write 2건(같은 portfolio), When 트랜잭션 실행, Then SELECT FOR UPDATE row lock으로 lost update 없이 두 거래가 순차 반영된다(회귀 테스트).

Impacted Area

  • Docs:
    • docs/interface-contract.md — POST /v1/portfolios/{id}/ledger/trades 회계 동작 + 응답 schema(cash_balance_after/asset_quantity_after) + status별 동작 명문화
    • docs/runtime-flows.md — ledger write flow에 회계 적용 + portfolio 갱신 + 트랜잭션 lock 단계 + portfolio_ledger 단일 원장 명문화
  • Modules:
    • app/api/v1_portfolios.py:record_ledger_trade_v1 — 통합 함수 호출 + 응답 확장
    • app/repositories/postgres.py — insert_ledger_trade 재정의(portfolio_ledger insert + portfolios UPDATE 단일 트랜잭션 + FOR UPDATE + application-level SELECT-before-insert idempotency + conn.transaction()), _row_to_ledger_* 통합
    • app/repositories/portfolio_repository.py — interface 시그니처 갱신
    • app/domain/models.py — LedgerEntry 단일화, LedgerTrade 삭제
    • app/domain/services.py 또는 신규 — 음수 검증 제거한 ledger 전용 회계 헬퍼 추출
    • app/schemas/v1.py — LedgerTradeResponse에 cash_balance_after/asset_quantity_after 추가
  • Tests:
    • 갱신 tests/test_v1_portfolios_ledger.py — AC-1~7 (응답 신규 필드 + 회계 + duplicate + 음수 + 인가 보존 + 동시성)
    • 갱신 tests/test_v1.py, tests/test_daemon_assignment.py — LedgerEntry 통합 + 응답 schema 반영

Notes

  • 본 REQ는 dcy-006 FR-1/2/3/4/6/7/13/14/15 산출물 + FR-5(interface-contract/runtime-flows) 일부 흡수.
  • REQ-036(마이그레이션) 선행 필요 — portfolio_ledger 신 스키마 위에서 동작.
  • 음수 허용은 신 path 한정. 레거시 buy/sell은 기존 거부 정책 유지(REQ-038 deprecated 대상).
  • 별도 REL(REL-003 등)로 분리 배포(dcy-006 A-4).
  • Source: dcy-006 FR-1/2/3/4/6/7/13/14/15, OQ-1~4/6/7.

Change Log

CHG-20260605-04

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • Status: Approved → Implemented

Intent

  • BAT-016 confirm-batch-verification 완료. 신 path 회계 책임 단일화 + 응답 cash_balance_after/asset_quantity_after + duplicate 현 상태 + 도메인/repository 통합 구현. 실 DB 통합 테스트(new/duplicate/음수) 통과, AC-1~7 검증. 전체 376 passed. CHG-20260605-03(ON CONFLICT→application-level idempotency + conn.transaction()) 정합 반영.

Acceptance Criteria Delta

  • No change

Impacted Area Delta

  • No change

Delivery Trace Delta

  • Batches:
    • BAT-016 (release-candidate)
  • Releases:
    • REL-003 (예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: implemented
  • Follow-up Action: REL-003 배포 시 trex-core 응답 명세 정합 최종 확인

Validation Plan

  • 실 DB 통합 테스트 + pytest 376 passed (완료)

CHG-20260605-03

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • Requirement 재정의: duplicate 감지를 ON CONFLICT ... WHERE order_id IS NOT NULL(DB partial index)에서 **application-level SELECT-before-insert(under FOR UPDATE lock)**로 변경. portfolio_ledger가 TimescaleDB hypertable이라 partial unique index/ON CONFLICT가 불가능(REQ-036 CHG-20260605-03). 추가로 트랜잭션을 conn.transaction()으로 처리(psycopg3 with conn: connection close 결함 회피). Status Approved 유지.

Intent

  • BAT-016 verify에서 실 DB 미동작(ON CONFLICT 에러 + 공유 connection close) 2건 결함 발견. hypertable 현실에 맞춰 idempotency 설계 정합 + 결함 수정.

Acceptance Criteria Delta

  • No change (AC-1~7 그대로; AC-2 duplicate idempotency는 application layer로 동일 충족). 검증 방식만 mock → 실 DB 통합 테스트 추가.

Impacted Area Delta

  • Modules: app/repositories/postgres.py insert_ledger_trade — ON CONFLICT 제거 + SELECT-before-insert + conn.transaction() 반영
  • Tests: tests/test_ledger_consolidation_integration.py(실 DB) 추가

Delivery Trace Delta

  • Batches: BAT-016 (in-delivery)
  • Releases: none (REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: yes (실 DB 통합 테스트로 재검증 — 376 passed)
  • Existing Implementation Invalidated: no (구현이 본 재정의에 맞춰 수정됨)
  • Status Recommendation: keep-current
  • Follow-up Action: BAT-016 verification 실 DB evidence 반영

Validation Plan

  • pytest 376 passed(통합 3 포함) + 실 DB new/duplicate/음수 통합 테스트

CHG-20260605-02

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • Status: Proposed → Approved

Intent

  • 승인 게이트 통과: 필수 필드 모두 충족, AC-1~7 구체성(수치 회계 예시 + 음수 허용 + duplicate + 동시성) 확인 완료, blocker 없음.

Acceptance Criteria Delta

  • No change

Impacted Area Delta

  • No change

Delivery Trace Delta

  • Batches: none (미정)
  • Releases: none (미정 — REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: promote-to-approved
  • Follow-up Action: batch 계획 편입; REQ-036 선행 의존성 확인

CHG-20260605-01

  • Date: 2026-06-05
  • Author: Justin Ko

Change Summary

  • 최초 생성 — dcy-006 confirm-discovery 후 draft-req 실행. dcy-006 회계 책임 단일화 + 응답 확장 산출물.

Intent

  • 신 path를 portfolio 회계 단일 권위 endpoint로 재정의하고 응답에 거래 후 권위 상태를 포함해 trex-core drift 차단.

Acceptance Criteria Delta

  • Added:
    • AC-1~7 최초 작성

Impacted Area Delta

  • Docs: docs/interface-contract.md, docs/runtime-flows.md — 최초 지정
  • Modules: app/api/v1_portfolios.py, app/repositories/postgres.py, app/repositories/portfolio_repository.py, app/domain/models.py, app/domain/services.py, app/schemas/v1.py — 최초 지정
  • Tests: tests/test_v1_portfolios_ledger.py, tests/test_v1.py, tests/test_daemon_assignment.py — 최초 지정

Delivery Trace Delta

  • Batches: none (미정)
  • Releases: none (미정 — REL-003 등 분리 예정)

Revalidation Impact

  • Reverification Needed: no
  • Existing Implementation Invalidated: no
  • Status Recommendation: keep-current
  • Follow-up Action: confirm-req 후 batch 계획 편입; REQ-036 선행 의존성 확인

Validation Plan

  • pytest TestClient로 AC-1~7 검증; 동시성은 회귀 또는 staging concurrent test