fix: converge broker failures without retry
This commit is contained in:
@@ -71,9 +71,8 @@ class OrderAck:
|
||||
accepted: bool
|
||||
broker_order_id: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
# Actual venue used for submission. KIS resolves AUTO before the HTTP call;
|
||||
# execution persists this value with the broker order id so restart/cancel
|
||||
# never has to infer the original venue.
|
||||
# Actual venue used for submission. Execution persists this value with the
|
||||
# broker order id so restart/cancel never has to infer the original venue.
|
||||
venue: Optional[str] = None
|
||||
raw: dict[str, Any] = field(default_factory=dict)
|
||||
submitted_at: Optional[datetime] = None
|
||||
|
||||
@@ -77,9 +77,8 @@ class BuyIntent:
|
||||
# slippage_pct: effective limit = entry_price*(1+pct/100) buy / *(1-pct/100) sell.
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
# Strategy wire default. AUTO is resolved at the broker boundary so
|
||||
# strategies remain session-agnostic while explicit KRX/NXT/SOR is kept.
|
||||
venue: str = "AUTO"
|
||||
# Missing strategy venue uses KIS SOR; explicit KRX/NXT/SOR is preserved.
|
||||
venue: str = "SOR"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -98,7 +97,7 @@ class SellIntent:
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
venue: str = "AUTO"
|
||||
venue: str = "SOR"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -229,7 +228,7 @@ class IntentService:
|
||||
timeout_error = self._timeout_error(intent.timeout_sec)
|
||||
if timeout_error is not None:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, timeout_error)
|
||||
if intent.venue not in {"AUTO", "KRX", "NXT", "SOR"}:
|
||||
if intent.venue not in {"KRX", "NXT", "SOR"}:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "invalid_venue")
|
||||
|
||||
# dcy-036 / REQ-114: day-start entry gate (fail-closed).
|
||||
@@ -347,7 +346,7 @@ class IntentService:
|
||||
timeout_error = self._timeout_error(intent.timeout_sec)
|
||||
if timeout_error is not None:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, timeout_error)
|
||||
if intent.venue not in {"AUTO", "KRX", "NXT", "SOR"}:
|
||||
if intent.venue not in {"KRX", "NXT", "SOR"}:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "invalid_venue")
|
||||
|
||||
slot = self._repo.fetch_slot(intent.slot_id)
|
||||
|
||||
@@ -30,32 +30,6 @@ from trex_core.domain.slot import (
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---- sell-reject retryability (issue #27 / REQ-007 CHG-20260615-01) ---------
|
||||
# core-owned reason -> retryable mapping for the rejected SellIntent response.
|
||||
# Maintained with the reject vocabulary (intents.py _admit_sell/_dedup) so a new
|
||||
# reason gets a correct flag without the daemon touching a table. Unrecognized
|
||||
# reasons default to retryable=True: a stranded stop-loss (held losing position)
|
||||
# is worse than bounded, observable hammering on a terminal decision.
|
||||
_SELL_REJECT_RETRYABLE: dict[str, bool] = {
|
||||
"slot_state_changed": True,
|
||||
"slot_not_holding:pending_buy": True,
|
||||
"duplicate_in_flight": True,
|
||||
"slot_not_found": False,
|
||||
"slot_not_holding:inactive": False,
|
||||
"slot_not_holding:pending_sell": False,
|
||||
"slot_not_holding:error": False,
|
||||
}
|
||||
|
||||
|
||||
def _sell_reject_retryable(reason: Optional[str]) -> bool:
|
||||
"""Map a rejected-sell reason to retryable (issue #27). Default True (safety)."""
|
||||
if reason is None:
|
||||
return True
|
||||
# malformed payload: client defect — retrying the same bytes is pointless.
|
||||
if reason.startswith("bad_request"):
|
||||
return False
|
||||
return _SELL_REJECT_RETRYABLE.get(reason, True)
|
||||
|
||||
|
||||
# ---- intent RPC handler -----------------------------------------------------
|
||||
|
||||
@@ -117,7 +91,7 @@ def make_intent_rpc_handler(
|
||||
# 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")),
|
||||
venue=_opt_str(body.get("venue")) or "AUTO",
|
||||
venue=_opt_str(body.get("venue")) or "SOR",
|
||||
)
|
||||
d = intent_service.submit_buy(buy)
|
||||
elif intent_type == "sell":
|
||||
@@ -133,7 +107,7 @@ def make_intent_rpc_handler(
|
||||
# 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")),
|
||||
venue=_opt_str(body.get("venue")) or "AUTO",
|
||||
venue=_opt_str(body.get("venue")) or "SOR",
|
||||
)
|
||||
d = intent_service.submit_sell(sell)
|
||||
else:
|
||||
@@ -146,9 +120,6 @@ def make_intent_rpc_handler(
|
||||
"intent_id": intent_id, "status": "rejected",
|
||||
"slot_id": None, "reason": f"bad_request:{exc}",
|
||||
}
|
||||
# issue #27: sell-side malformed payload carries retryable=False.
|
||||
if intent_type == "sell":
|
||||
payload["retryable"] = _sell_reject_retryable(payload["reason"])
|
||||
return RpcResponse(payload=payload)
|
||||
payload = {
|
||||
"intent_id": d.intent_id,
|
||||
@@ -156,9 +127,6 @@ def make_intent_rpc_handler(
|
||||
"slot_id": (str(d.slot_id) if d.slot_id else None),
|
||||
"reason": d.reason,
|
||||
}
|
||||
# issue #27 / REQ-007 AC-7: additive retryable on rejected sell only.
|
||||
if intent_type == "sell" and d.status == "rejected":
|
||||
payload["retryable"] = _sell_reject_retryable(d.reason)
|
||||
return RpcResponse(payload=payload)
|
||||
|
||||
return handler
|
||||
|
||||
Reference in New Issue
Block a user