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
+44 -3
View File
@@ -80,13 +80,17 @@ def make_intent_rpc_handler(
portfolio_id=_opt_str(body.get("portfolio_id")) or None,
slot_id=(UUID(str(body["slot_id"])) if body.get("slot_id") else None),
qty=_opt_float(body.get("qty")),
allocated_capital=_opt_float(body.get("allocated_capital")),
# dcy-023 / REQ-007 AC-6: allocated_capital is no longer a
# contract field — senders may still include it; it is ignored.
entry_price=_opt_float(body.get("entry_price")),
strategy_id=_opt_str(body.get("strategy_id")),
sell_strategy_id=_opt_str(body.get("sell_strategy_id")),
forced_close_at=_opt_dt(body.get("forced_close_at")),
stop_loss_price=_opt_decimal(body.get("stop_loss_price")),
take_profit_price=_opt_decimal(body.get("take_profit_price")),
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
timeout_sec=_opt_int(body.get("timeout_sec")),
slippage_pct=_opt_float(body.get("slippage_pct")),
)
d = intent_service.submit_buy(buy)
elif intent_type == "sell":
@@ -99,6 +103,9 @@ def make_intent_rpc_handler(
limit_price=_opt_float(body.get("limit_price")),
# dcy-021 / REQ-062: optional partial-sell qty; absent => sell all.
qty=_opt_float(body.get("qty")),
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
timeout_sec=_opt_int(body.get("timeout_sec")),
slippage_pct=_opt_float(body.get("slippage_pct")),
)
d = intent_service.submit_sell(sell)
else:
@@ -357,11 +364,19 @@ def _err(reason: str) -> RpcResponse:
# ---- control RPC handler (REQ-043, BAT-012) ---------------------------------
def make_control_rpc_handler(pause_store: PauseStore, *, kill_switch_service=None):
def make_control_rpc_handler(
pause_store: PauseStore,
*,
kill_switch_service=None,
cancel_pending_fn=None,
):
"""RPC handler for `trex.control.req` — action discriminator dispatch (REQ-043).
Actions: `pause`/`resume`/`list` (REQ-032, delegated), `kill_switch` (REQ-045),
`resume_kill_switch` (REQ-046). Unknown actions return an error response.
`resume_kill_switch` (REQ-046), `cancel_pending` (dcy-022/REQ-071).
Unknown actions return an error response.
cancel_pending_fn: callable(slot_id: str) -> str — injected from ExecutionService.
"""
pause_handler = make_pause_rpc_handler(pause_store)
@@ -374,6 +389,8 @@ def make_control_rpc_handler(pause_store: PauseStore, *, kill_switch_service=Non
return _handle_resume_kill_switch(body, kill_switch_service)
if action in ("pause", "resume", "list"):
return pause_handler(req)
if action == "cancel_pending":
return _handle_cancel_pending(body, cancel_pending_fn)
return _err(f"unknown_action:{action}")
return handler
@@ -409,6 +426,24 @@ def _handle_kill_switch(body: dict, svc) -> RpcResponse:
return RpcResponse(payload=payload)
def _handle_cancel_pending(body: dict, cancel_fn) -> RpcResponse:
"""dcy-022 / REQ-071: operator backstop cancel for a pending slot."""
if cancel_fn is None:
return _err("cancel_pending_unsupported")
try:
slot_id = str(body["slot_id"])
except KeyError:
return _err("slot_id_required")
result = cancel_fn(slot_id)
if result == "ok":
return RpcResponse(payload={"status": "ok", "slot_id": slot_id})
if result == "not_pending":
return RpcResponse(payload={"status": "ignored", "slot_id": slot_id, "reason": "not_pending"})
if result == "slot_not_found":
return RpcResponse(payload={"status": "error", "slot_id": slot_id, "reason": "slot_not_found"})
return RpcResponse(payload={"status": "error", "slot_id": slot_id, "reason": result})
def _handle_resume_kill_switch(body: dict, svc) -> RpcResponse:
"""REQ-046: clear the day-scoped exclusion (sold slots stay sold)."""
if svc is None:
@@ -469,6 +504,12 @@ def _opt_float(v) -> Optional[float]:
return float(v)
def _opt_int(v) -> Optional[int]:
if v is None:
return None
return int(v)
def _opt_dt(v) -> Optional[datetime]:
if v is None:
return None