feat: resolve and persist actual order venue

This commit is contained in:
jordan
2026-07-29 17:10:32 +09:00
parent 6398ff250c
commit a656350a35
18 changed files with 304 additions and 47 deletions
+69 -3
View File
@@ -1,10 +1,12 @@
"""Venue propagation from the strategy wire contract to broker execution."""
from __future__ import annotations
from uuid import UUID
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
from trex_core.application.broker_port import BalanceItem, FillStatus, OrderAck, OrderRequest
from trex_core.application.execution import ExecutionService
from trex_core.application.intents import IntentService
from trex_core.application.intents import BuyIntent, IntentService
from trex_core.application.messaging import RpcRequest
from trex_core.application.messaging_handlers import make_intent_rpc_handler
from trex_core.domain.slot import SlotService, SlotState
@@ -12,13 +14,18 @@ from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
class RecordingBroker:
def __init__(self) -> None:
def __init__(self, *, actual_venue: str | None = None) -> None:
self.orders: list[OrderRequest] = []
self.cancel_venues: list[str] = []
self.actual_venue = actual_venue
def submit_order(self, req: OrderRequest) -> OrderAck:
self.orders.append(req)
return OrderAck(accepted=True, broker_order_id=f"order-{len(self.orders)}")
return OrderAck(
accepted=True,
broker_order_id=f"order-{len(self.orders)}",
venue=self.actual_venue or req.venue,
)
def poll_fill(self, broker_order_id: str, *, side: str, symbol: str) -> FillStatus:
return FillStatus(state="accepted")
@@ -104,6 +111,41 @@ def test_invalid_venue_is_rejected_before_slot_transition() -> None:
assert repo.list_slots("default")[0].status is SlotState.INACTIVE
def test_omitted_wire_venue_is_auto_then_actual_venue_is_persisted() -> None:
repo = InMemorySlotRepository()
service = SlotService(repo)
intents = IntentService(
service,
repo,
InMemoryIntentStore(),
resolve_symbol=lambda portfolio_id: "005930",
)
repo.reconcile_universe("default", {"PF-1": "005930"})
broker = RecordingBroker(actual_venue="NXT")
execution = ExecutionService(service, repo, broker)
handler = make_intent_rpc_handler(intents)
response = handler(
RpcRequest(
payload={
"intent_type": "buy",
"intent_id": "auto-buy",
"portfolio_id": "PF-1",
"qty": 1,
},
correlation_id="auto-buy",
)
)
slot_id = UUID(response.payload["slot_id"])
pending = repo.fetch_slot(slot_id)
assert pending is not None and pending.venue == "AUTO"
execution.run_once()
dispatched = repo.fetch_slot(slot_id)
assert broker.orders[0].venue == "AUTO"
assert dispatched is not None and dispatched.venue == "NXT"
def test_inactive_transition_resets_venue_to_krx() -> None:
repo, intents, _, _ = _wire()
handler = make_intent_rpc_handler(intents)
@@ -131,6 +173,30 @@ def test_inactive_transition_resets_venue_to_krx() -> None:
assert reset.venue == "KRX"
def test_holding_transition_resets_next_order_venue_to_auto() -> None:
repo, intents, _, _ = _wire()
decision = intents.submit_buy(
BuyIntent(
intent_id="nxt-buy-holding",
portfolio_id="PF-1",
qty=1,
venue="NXT",
)
)
slot = repo.fetch_slot(decision.slot_id)
service = SlotService(repo)
holding = service.transit_slot(
slot.slot_id,
SlotState.PENDING_BUY,
SlotState.HOLDING,
reason="fill",
actor="test",
qty=1,
avg_price=1000,
)
assert holding.venue == "AUTO"
def test_operator_cancel_uses_original_nxt_venue() -> None:
repo, intents, execution, broker = _wire()
handler = make_intent_rpc_handler(intents)