fix(paper): BAT-039 — paper order seq Redis 영속화 (재시작 멱등 키 충돌 방지, REQ-011/issue #4)

REQ-011 change-req(CHG-20260612-01)→구현→confirm-req-implemented(CHG-20260612-02)
전체 delivery. paper 한정 결정성 버그(프로덕션 무관).

- seq_store.py 신규: SeqStore 포트 + RedisSeqStore(원자 INCR, 재시작 단조·영속)
  + InMemorySeqStore(폴백/테스트). REQ-016 token_cache 패턴 재사용.
- paper.py: optional seq_store 주입. 미주입 시 기존 in-memory 동작 byte-for-byte 유지.
- config.py/daemon.py: TREX_PAPER_REDIS_URL → RedisSeqStore wiring
  (key=trex:broker:paper:{instance_key}:order-seq).
- 런타임 Redis 오류는 전파(silent in-memory 폴백 금지 — 버그 재현 방지).
- tests: 재시작 단조성/correlation_id 비충돌/in-memory 폴백/fake redis INCR (5).
  434 passed (+5), ruff clean.

REQ-011 Approved→Implemented, BAT-039 release-candidate. issue #4 close.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:16:11 +09:00
co-authored by Claude Opus 4.8
parent 0e5ce32f6b
commit 9871008bb6
13 changed files with 470 additions and 6 deletions
+13 -2
View File
@@ -11,6 +11,7 @@ import threading
from decimal import Decimal
from typing import Optional
from trex_core.adapters.broker.seq_store import SeqStore
from trex_core.application.broker_port import (
BalanceItem,
FillStatus,
@@ -47,12 +48,18 @@ class PaperBroker:
fill_after_polls: int = 0,
qty_per_poll: Optional[int] = None,
reject_symbols: Optional[set[str]] = None,
seq_store: Optional[SeqStore] = None,
) -> None:
self._fill_after = fill_after_polls
self._qty_per_poll = qty_per_poll
self._reject = reject_symbols or set()
self._orders: dict[str, _Order] = {}
self._seq = 0
# REQ-011 CHG-20260612-01 (issue #4): when injected, order seq is minted
# from a persistent monotonic store so it survives daemon restarts and a
# post-restart sell does not collide its correlation_id with a prior buy.
# Unset (no Redis configured) keeps the byte-for-byte in-memory counter.
self._seq_store = seq_store
self._lock = threading.Lock()
# REQ-038: programmable current price for safety-sell scenario tests.
self._prices: dict[str, Decimal] = {}
@@ -62,8 +69,12 @@ class PaperBroker:
def submit_order(self, req: OrderRequest) -> OrderAck:
with self._lock:
self._seq += 1
oid = f"PAPER-{self._seq:06d}"
if self._seq_store is not None:
seq = self._seq_store.next_seq()
else:
self._seq += 1
seq = self._seq
oid = f"PAPER-{seq:06d}"
self._orders[oid] = _Order(oid, req)
return OrderAck(accepted=True, broker_order_id=oid)