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