fix: converge broker failures without retry

This commit is contained in:
Hermes Agent
2026-08-19 11:29:26 +09:00
parent be98965f2c
commit 15aeae6930
15 changed files with 181 additions and 211 deletions
+2 -3
View File
@@ -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
+5 -6
View File
@@ -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)
+2 -34
View File
@@ -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