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
+4
View File
@@ -91,6 +91,9 @@ class PaperBrokerConfig:
fill_after_polls: int = 0
qty_per_poll: Optional[int] = None
reject_symbols: frozenset[str] = frozenset()
# REQ-011 CHG-20260612-01 (issue #4): optional Redis URL for a persistent
# order-seq store. Empty => in-memory counter (prior behaviour).
redis_url: str = ""
@dataclass(frozen=True)
@@ -257,6 +260,7 @@ class DaemonConfig:
reject_symbols=frozenset(
s.strip() for s in reject_raw.split(",") if s.strip()
),
redis_url=os.environ.get("TREX_PAPER_REDIS_URL", "").strip(),
)
portfolio_hub: Optional[PortfolioHubConfig] = None
pf_base = os.environ.get("TREX_PORTFOLIO_HUB_BASE_URL", "").strip()
+9
View File
@@ -84,10 +84,19 @@ def _build_broker(config: DaemonConfig) -> BrokerPort:
base_url=k.base_url, timeout=k.timeout, allow_live=k.allow_live,
token_cache=token_cache,
)
seq_store = None
if config.paper.redis_url:
from trex_core.adapters.broker.seq_store import RedisSeqStore
seq_store = RedisSeqStore(
config.paper.redis_url,
key=f"trex:broker:paper:{config.instance_key}:order-seq",
)
return PaperBroker(
fill_after_polls=config.paper.fill_after_polls,
qty_per_poll=config.paper.qty_per_poll,
reject_symbols=set(config.paper.reject_symbols),
seq_store=seq_store,
)