feat(rel-006): dcy-022/023 + BAT-032~034 — 실행 정책·qty 검증·allocated_capital 제거 통합

BAT-032 (dcy-022): timeout_sec/slippage_pct 실행 정책, 미체결 자동 취소,
보호 지정가, `trex-core cancel` 백스톱. 마이그레이션 012.
BAT-033: `trex-core enter --qty` 필수 검증 — 미지정 시 RPC 전 즉시 오류.
BAT-034 (dcy-023): allocated_capital 죽은 필드 전 경로 제거 (domain·intent·
persistence·snapshot·CLI). 마이그레이션 013(비가역 DROP). 외부 발신자
unknown key 무시 수신 의미론 적용.
REL-006 confirmed: BAT-026~034 통합 릴리즈, pre-release smoke 증거 포함.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 07:42:03 +09:00
co-authored by Claude Sonnet 4.6
parent 5ee5f7f4e2
commit fb05a73130
56 changed files with 3151 additions and 103 deletions
@@ -0,0 +1,162 @@
"""Execution policy (timeout_sec / slippage_pct) lifecycle tests (dcy-022 / REQ-068).
slot_service.transit_slot sets timeout_sec/slippage_pct on PENDING entry and
clears them when the slot exits to HOLDING, INACTIVE, or ERROR.
"""
from __future__ import annotations
import pytest
from trex_core.domain.slot import SlotService, SlotState
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
INST = "default"
@pytest.fixture
def repo():
return InMemorySlotRepository()
@pytest.fixture
def service(repo):
return SlotService(repo, instance_key=INST)
def _seed(repo):
repo.reconcile_universe(INST, {"PF-1"})
return repo.list_slots(INST)[0]
# ----------------------------------------------------------------------------
# PENDING_BUY: execution policy set on entry
# ----------------------------------------------------------------------------
def test_timeout_sec_set_on_pending_buy_entry(service, repo):
slot = _seed(repo)
updated = service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
timeout_sec=30,
)
assert updated.timeout_sec == 30
def test_slippage_pct_set_on_pending_buy_entry(service, repo):
slot = _seed(repo)
updated = service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
slippage_pct=0.5,
)
assert updated.slippage_pct == 0.5
def test_both_policy_fields_set_together(service, repo):
slot = _seed(repo)
updated = service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
timeout_sec=60, slippage_pct=1.0,
)
assert updated.timeout_sec == 60
assert updated.slippage_pct == 1.0
def test_policy_fields_default_to_none_when_not_given(service, repo):
slot = _seed(repo)
updated = service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
)
assert updated.timeout_sec is None
assert updated.slippage_pct is None
# ----------------------------------------------------------------------------
# PENDING_SELL: same lifecycle as PENDING_BUY
# ----------------------------------------------------------------------------
def test_slippage_pct_set_on_pending_sell_entry(service, repo):
slot = _seed(repo)
service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="buy", actor="t", symbol="005930", qty=10,
)
service.transit_slot(
slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
reason="filled", actor="t",
)
updated = service.transit_slot(
slot.slot_id, SlotState.HOLDING, SlotState.PENDING_SELL,
reason="sell", actor="t",
timeout_sec=45, slippage_pct=0.3,
)
assert updated.timeout_sec == 45
assert updated.slippage_pct == 0.3
# ----------------------------------------------------------------------------
# HOLDING: execution policy cleared on exit from pending
# ----------------------------------------------------------------------------
def test_policy_cleared_on_transition_to_holding(service, repo):
slot = _seed(repo)
service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
timeout_sec=30, slippage_pct=0.5,
)
updated = service.transit_slot(
slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
reason="filled", actor="t",
)
assert updated.timeout_sec is None
assert updated.slippage_pct is None
# ----------------------------------------------------------------------------
# INACTIVE: execution policy cleared on inactive reset
# ----------------------------------------------------------------------------
def test_policy_cleared_on_transition_to_inactive(service, repo):
slot = _seed(repo)
service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
timeout_sec=30, slippage_pct=0.5,
)
updated = service.transit_slot(
slot.slot_id, SlotState.PENDING_BUY, SlotState.INACTIVE,
reason="rejected", actor="t",
)
assert updated.timeout_sec is None
assert updated.slippage_pct is None
# ----------------------------------------------------------------------------
# PENDING state: existing policy preserved when not overridden
# ----------------------------------------------------------------------------
def test_existing_policy_preserved_when_not_overridden(service, repo):
"""PENDING→PENDING re-entry keeps existing policy if not overridden."""
slot = _seed(repo)
service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="r", actor="t", symbol="005930", qty=10,
timeout_sec=30, slippage_pct=0.5,
)
# Simulate a HOLDING→PENDING_BUY increase (re-enters pending with no new policy).
# We need to go to HOLDING first, then re-enter PENDING_BUY.
service.transit_slot(
slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
reason="filled", actor="t",
)
# From HOLDING re-enter PENDING_BUY without providing policy — policy starts fresh
updated = service.transit_slot(
slot.slot_id, SlotState.HOLDING, SlotState.PENDING_BUY,
reason="increase", actor="t", qty=5,
)
# Policy cleared during HOLDING, not re-set → None (fresh PENDING entry)
assert updated.timeout_sec is None
assert updated.slippage_pct is None
+1 -3
View File
@@ -54,7 +54,6 @@ class TestTransitHappyPath:
actor="buy_decision_thread",
symbol="005930",
qty=10,
allocated_capital=1_000_000,
strategy_id="orb_v1",
)
assert updated.status is SlotState.PENDING_BUY
@@ -94,7 +93,7 @@ class TestTransitHappyPath:
service.transit_slot(
slot.slot_id, SlotState.INACTIVE, SlotState.PENDING_BUY,
reason="orb_breakout", actor="buy", symbol="005930", qty=10,
allocated_capital=1_000_000, strategy_id="orb_v1",
strategy_id="orb_v1",
)
rolled = service.transit_slot(
slot.slot_id, SlotState.PENDING_BUY, SlotState.INACTIVE,
@@ -102,7 +101,6 @@ class TestTransitHappyPath:
)
assert rolled.status is SlotState.INACTIVE
assert rolled.qty is None
assert rolled.allocated_capital is None
assert rolled.strategy_id is None
assert rolled.limit_price is None
assert rolled.broker_order_id is None