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:
@@ -59,13 +59,17 @@ class BuyIntent:
|
||||
slot_id: Optional[UUID] = None
|
||||
symbol: Optional[str] = None
|
||||
qty: Optional[float] = None
|
||||
allocated_capital: Optional[float] = None
|
||||
entry_price: Optional[float] = None
|
||||
strategy_id: Optional[str] = None
|
||||
sell_strategy_id: Optional[str] = None
|
||||
forced_close_at: Optional[datetime] = None
|
||||
stop_loss_price: Optional[Decimal] = None # REQ-035
|
||||
take_profit_price: Optional[Decimal] = None # REQ-035
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
# timeout_sec: cancel_order after N seconds without fill (None = config default or OFF).
|
||||
# slippage_pct: effective limit = entry_price*(1+pct/100) buy / *(1-pct/100) sell.
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -81,6 +85,9 @@ class SellIntent:
|
||||
# dcy-021 / REQ-062: optional partial-sell quantity. None => sell the whole
|
||||
# position (broker balance); set => partial sell of min(qty, broker balance).
|
||||
qty: Optional[float] = None
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -96,6 +103,7 @@ class IntentService:
|
||||
*,
|
||||
instance_key: str = "default",
|
||||
max_active_slots: Optional[int] = None,
|
||||
default_order_timeout_sec: Optional[int] = None,
|
||||
) -> None:
|
||||
self._svc = slot_service
|
||||
self._repo = slot_repo
|
||||
@@ -108,6 +116,9 @@ class IntentService:
|
||||
# expected to set max_active_slots in the instance TOML — design §5).
|
||||
self._instance_key = instance_key
|
||||
self._max_active_slots = max_active_slots
|
||||
# dcy-022 / REQ-068: default timeout when intent omits timeout_sec.
|
||||
# None = OFF = indefinite wait (legacy behavior). Per-intent value overrides.
|
||||
self._default_order_timeout_sec = default_order_timeout_sec
|
||||
|
||||
# ------------------------------------------------------------
|
||||
def lookup(self, intent_id: str) -> Optional[IntentDecision]:
|
||||
@@ -123,7 +134,6 @@ class IntentService:
|
||||
"portfolio_id": intent.portfolio_id,
|
||||
"symbol": intent.symbol,
|
||||
"qty": intent.qty,
|
||||
"allocated_capital": intent.allocated_capital,
|
||||
"entry_price": intent.entry_price,
|
||||
"strategy_id": intent.strategy_id,
|
||||
"sell_strategy_id": intent.sell_strategy_id,
|
||||
@@ -213,6 +223,12 @@ class IntentService:
|
||||
if (not is_increase and self._max_active_slots is not None
|
||||
and self._repo.count_active(self._instance_key) >= self._max_active_slots):
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "capacity_full")
|
||||
# dcy-022 / REQ-068: resolve effective timeout (per-intent > config default).
|
||||
effective_timeout = (
|
||||
intent.timeout_sec
|
||||
if intent.timeout_sec is not None
|
||||
else self._default_order_timeout_sec
|
||||
)
|
||||
try:
|
||||
updated = self._svc.transit_slot(
|
||||
slot.slot_id,
|
||||
@@ -222,7 +238,6 @@ class IntentService:
|
||||
actor="intent_api",
|
||||
symbol=symbol,
|
||||
qty=intent.qty,
|
||||
allocated_capital=intent.allocated_capital,
|
||||
# dcy-012 / REQ-007: buy entry price maps to the slot's
|
||||
# side-agnostic pending limit_price (cleared on fill→HOLDING).
|
||||
limit_price=intent.entry_price,
|
||||
@@ -232,6 +247,8 @@ class IntentService:
|
||||
forced_close_at=intent.forced_close_at,
|
||||
stop_loss_price=intent.stop_loss_price,
|
||||
take_profit_price=intent.take_profit_price,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
extra={"intent_id": intent.intent_id, "symbol": symbol},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
@@ -247,6 +264,12 @@ class IntentService:
|
||||
return IntentDecision(
|
||||
intent.intent_id, "rejected", None, f"slot_not_holding:{slot.status.value}"
|
||||
)
|
||||
# dcy-022 / REQ-068: resolve effective timeout.
|
||||
effective_timeout = (
|
||||
intent.timeout_sec
|
||||
if intent.timeout_sec is not None
|
||||
else self._default_order_timeout_sec
|
||||
)
|
||||
try:
|
||||
updated = self._svc.transit_slot(
|
||||
slot.slot_id,
|
||||
@@ -261,6 +284,8 @@ class IntentService:
|
||||
# dispatch caps at the broker balance. Entering PENDING_SELL takes
|
||||
# this qty as-is (None = "sell all" sentinel).
|
||||
qty=intent.qty,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
extra={"intent_id": intent.intent_id},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
|
||||
Reference in New Issue
Block a user