feat: propagate intent venue through execution
This commit is contained in:
@@ -37,7 +37,7 @@ _SLOT_COLUMNS = (
|
||||
"order_id, strategy_id, broker_order_id, portfolio_id, "
|
||||
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
|
||||
"created_at, updated_at, instance_key, retired_at, "
|
||||
"timeout_sec, slippage_pct, occupancy_seq"
|
||||
"timeout_sec, slippage_pct, occupancy_seq, venue"
|
||||
)
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ class PostgreSQLSlotRepository:
|
||||
"broker_order_id=%s, portfolio_id=%s, sell_strategy_id=%s, "
|
||||
"forced_close_at=%s, stop_loss_price=%s, take_profit_price=%s, "
|
||||
"instance_key=%s, retired_at=%s, timeout_sec=%s, slippage_pct=%s, "
|
||||
"occupancy_seq=%s, updated_at=%s WHERE slot_id=%s",
|
||||
"occupancy_seq=%s, venue=%s, updated_at=%s WHERE slot_id=%s",
|
||||
(
|
||||
slot.status.value,
|
||||
slot.symbol,
|
||||
@@ -236,6 +236,7 @@ class PostgreSQLSlotRepository:
|
||||
slot.timeout_sec,
|
||||
slot.slippage_pct,
|
||||
slot.occupancy_seq,
|
||||
slot.venue,
|
||||
slot.updated_at or datetime.now(tz=timezone.utc),
|
||||
slot.slot_id,
|
||||
),
|
||||
@@ -361,7 +362,7 @@ class PostgreSQLSlotRepository:
|
||||
# forced_close_at(11) stop_loss_price(12) take_profit_price(13)
|
||||
# created_at(14) updated_at(15) instance_key(16) retired_at(17)
|
||||
# timeout_sec(18) slippage_pct(19) [dcy-022/REQ-068]
|
||||
# occupancy_seq(20) [dcy-028/REQ-081]
|
||||
# occupancy_seq(20) [dcy-028/REQ-081], venue(21)
|
||||
return Slot(
|
||||
slot_id=row[0] if isinstance(row[0], UUID) else UUID(str(row[0])),
|
||||
status=SlotState(row[1]),
|
||||
@@ -384,6 +385,7 @@ class PostgreSQLSlotRepository:
|
||||
timeout_sec=int(row[18]) if row[18] is not None else None,
|
||||
slippage_pct=f(row[19]),
|
||||
occupancy_seq=int(row[20]) if len(row) > 20 and row[20] is not None else 0,
|
||||
venue=str(row[21]) if len(row) > 21 and row[21] is not None else "KRX",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Preserve the admitted intent venue until dispatch/cancel. Existing rows are
|
||||
-- explicitly backward compatible with the historical KRX-only execution path.
|
||||
ALTER TABLE daily_slot
|
||||
ADD COLUMN IF NOT EXISTS venue TEXT NOT NULL DEFAULT 'KRX';
|
||||
|
||||
ALTER TABLE daily_slot
|
||||
DROP CONSTRAINT IF EXISTS daily_slot_venue_check;
|
||||
|
||||
ALTER TABLE daily_slot
|
||||
ADD CONSTRAINT daily_slot_venue_check
|
||||
CHECK (venue IN ('KRX', 'NXT', 'SOR'));
|
||||
@@ -197,6 +197,7 @@ class ExecutionService:
|
||||
qty=qty,
|
||||
limit_price=limit,
|
||||
order_type="00" if limit is not None else "01",
|
||||
venue=slot.venue,
|
||||
)
|
||||
)
|
||||
if ack.accepted and ack.broker_order_id:
|
||||
@@ -548,12 +549,14 @@ class ExecutionService:
|
||||
return
|
||||
qty = int(slot.qty or 0)
|
||||
try:
|
||||
accepted = self._broker.cancel_order(
|
||||
slot.broker_order_id,
|
||||
side=side,
|
||||
symbol=slot.symbol or "",
|
||||
qty=qty,
|
||||
)
|
||||
cancel_kwargs = {
|
||||
"side": side,
|
||||
"symbol": slot.symbol or "",
|
||||
"qty": qty,
|
||||
}
|
||||
if slot.venue != "KRX":
|
||||
cancel_kwargs["venue"] = slot.venue
|
||||
accepted = self._broker.cancel_order(slot.broker_order_id, **cancel_kwargs)
|
||||
r.timeout_cancelled += 1
|
||||
_log.warning(
|
||||
"timeout cancel sent: slot=%s status=%s broker_order_id=%s "
|
||||
@@ -665,12 +668,14 @@ class ExecutionService:
|
||||
side = "buy" if slot.status is SlotState.PENDING_BUY else "sell"
|
||||
qty = int(slot.qty or 0)
|
||||
try:
|
||||
self._broker.cancel_order(
|
||||
slot.broker_order_id,
|
||||
side=side,
|
||||
symbol=slot.symbol or "",
|
||||
qty=qty,
|
||||
)
|
||||
cancel_kwargs = {
|
||||
"side": side,
|
||||
"symbol": slot.symbol or "",
|
||||
"qty": qty,
|
||||
}
|
||||
if slot.venue != "KRX":
|
||||
cancel_kwargs["venue"] = slot.venue
|
||||
self._broker.cancel_order(slot.broker_order_id, **cancel_kwargs)
|
||||
except Exception: # noqa: BLE001 - best-effort
|
||||
_log.warning(
|
||||
"operator cancel failed: slot=%s broker_order_id=%s",
|
||||
|
||||
@@ -75,6 +75,7 @@ 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
|
||||
venue: str = "KRX"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -93,6 +94,7 @@ class SellIntent:
|
||||
# dcy-022 / REQ-068: execution policy (optional, backward-compat).
|
||||
timeout_sec: Optional[int] = None
|
||||
slippage_pct: Optional[float] = None
|
||||
venue: str = "KRX"
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -152,6 +154,7 @@ class IntentService:
|
||||
"entry_price": intent.entry_price,
|
||||
"strategy_id": intent.strategy_id,
|
||||
"sell_strategy_id": intent.sell_strategy_id,
|
||||
"venue": intent.venue,
|
||||
}
|
||||
prior = self._dedup(intent.intent_id, "buy", payload)
|
||||
if prior is not None:
|
||||
@@ -169,6 +172,7 @@ class IntentService:
|
||||
"reason": intent.reason,
|
||||
"sell_strategy_id": intent.sell_strategy_id,
|
||||
"qty": intent.qty,
|
||||
"venue": intent.venue,
|
||||
}
|
||||
prior = self._dedup(intent.intent_id, "sell", payload)
|
||||
if prior is not None:
|
||||
@@ -207,6 +211,9 @@ class IntentService:
|
||||
dcy-036 / REQ-114: entry gate — if entry_allowed_fn returns False,
|
||||
BuyIntent is rejected before any slot lookup."""
|
||||
|
||||
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).
|
||||
# None or True → pass; False → reject new entry.
|
||||
# SellIntent is NOT checked (청산 경로는 차단하지 않음 AC-5).
|
||||
@@ -292,6 +299,7 @@ class IntentService:
|
||||
take_profit_price=intent.take_profit_price,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
venue=intent.venue,
|
||||
extra={"intent_id": intent.intent_id, "symbol": symbol},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
@@ -300,6 +308,9 @@ class IntentService:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "already_active")
|
||||
|
||||
def _admit_sell(self, intent: SellIntent) -> IntentDecision:
|
||||
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)
|
||||
if slot is None:
|
||||
return IntentDecision(intent.intent_id, "rejected", None, "slot_not_found")
|
||||
@@ -342,6 +353,7 @@ class IntentService:
|
||||
qty=intent.qty,
|
||||
timeout_sec=effective_timeout,
|
||||
slippage_pct=intent.slippage_pct,
|
||||
venue=intent.venue,
|
||||
extra={"intent_id": intent.intent_id},
|
||||
)
|
||||
except SlotStateMismatchError:
|
||||
|
||||
@@ -117,6 +117,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 "KRX",
|
||||
)
|
||||
d = intent_service.submit_buy(buy)
|
||||
elif intent_type == "sell":
|
||||
@@ -132,6 +133,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 "KRX",
|
||||
)
|
||||
d = intent_service.submit_sell(sell)
|
||||
else:
|
||||
@@ -626,4 +628,3 @@ def _parse_date(v) -> date_cls:
|
||||
def _today() -> date_cls:
|
||||
return datetime.now(tz=timezone.utc).date()
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ class Slot:
|
||||
# holding episode. Used by the strategy-state CAS to reject writes computed
|
||||
# against a prior occupancy of a reused slot (point-ABA guard, dcy-028 R-1).
|
||||
occupancy_seq: int = 0
|
||||
# Order venue carried from the admitted intent through dispatch/cancel.
|
||||
# It is reset to KRX when the permanent asset slot returns to INACTIVE.
|
||||
venue: str = "KRX"
|
||||
|
||||
|
||||
# Canonical column contract for `Slot` (dcy-001 OQ-2 / FR-5).
|
||||
@@ -84,6 +87,7 @@ EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
|
||||
"timeout_sec", # dcy-022 / REQ-068 (BAT-032)
|
||||
"slippage_pct", # dcy-022 / REQ-068 (BAT-032)
|
||||
"occupancy_seq", # dcy-028 / REQ-081 (BAT-042)
|
||||
"venue",
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class SlotService:
|
||||
# dcy-022 / REQ-068: execution policy (set on PENDING entry, cleared on exit).
|
||||
timeout_sec: Optional[int] = None,
|
||||
slippage_pct: Optional[float] = None,
|
||||
venue: Optional[str] = None,
|
||||
extra: Optional[dict[str, Any]] = None,
|
||||
) -> Slot:
|
||||
"""Transition slot from one state to another atomically.
|
||||
@@ -133,6 +134,7 @@ class SlotService:
|
||||
take_profit_price=take_profit_price,
|
||||
timeout_sec=timeout_sec,
|
||||
slippage_pct=slippage_pct,
|
||||
venue=venue,
|
||||
)
|
||||
self._repo.update_slot(updated)
|
||||
|
||||
@@ -173,6 +175,7 @@ class SlotService:
|
||||
take_profit_price: Optional[Decimal] = None,
|
||||
timeout_sec: Optional[int] = None,
|
||||
slippage_pct: Optional[float] = None,
|
||||
venue: Optional[str] = None,
|
||||
) -> Slot:
|
||||
"""Compute the post-transition Slot row.
|
||||
|
||||
@@ -201,6 +204,7 @@ class SlotService:
|
||||
take_profit_price=None,
|
||||
timeout_sec=None,
|
||||
slippage_pct=None,
|
||||
venue="KRX",
|
||||
updated_at=datetime.now(tz=timezone.utc),
|
||||
)
|
||||
|
||||
@@ -267,6 +271,7 @@ class SlotService:
|
||||
timeout_sec=new_timeout_sec,
|
||||
slippage_pct=new_slippage_pct,
|
||||
occupancy_seq=new_occupancy_seq,
|
||||
venue=venue if venue is not None else slot.venue,
|
||||
updated_at=datetime.now(tz=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user