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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user