fix: reconcile KIS correction order chains

This commit is contained in:
Hermes Agent
2026-09-12 22:12:19 +09:00
parent 6017bde744
commit 06c3f06b5d
6 changed files with 864 additions and 149 deletions
@@ -12,7 +12,7 @@ from zoneinfo import ZoneInfo
from trex_core.adapters.broker.kis import KISBroker
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore
from trex_core.application.execution import ExecutionService
from trex_core.application.intents import BuyIntent, IntentService
from trex_core.application.intents import BuyIntent, IntentService, SellIntent
from trex_core.domain.slot import SlotService, SlotState
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
@@ -30,12 +30,15 @@ class _ExchangeState:
self.orders: list[dict[str, str]] = []
self.fill_orders = False
self.reject_orders = False
self.next_side = "02"
self.correction_cancel_sell = False
self.balance_qty: int | None = None
def submit(self, body: dict[str, str]) -> tuple[bool, dict[str, object]]:
self.submit_attempts += 1
order = {
"pdno": body["PDNO"],
"sll_buy_dvsn_cd": "02",
"sll_buy_dvsn_cd": self.next_side,
"ord_qty": body["ORD_QTY"],
"ord_unpr": body["ORD_UNPR"],
"ord_dt": VIRTUAL_AT.strftime("%Y%m%d"),
@@ -65,6 +68,20 @@ class _ExchangeState:
order["rjct_qty"] = order["ord_qty"]
order["rmn_qty"] = "0"
row = dict(order)
if self.correction_cancel_sell and order["sll_buy_dvsn_cd"] == "01":
row["rmn_qty"] = "0"
rows.append(row)
rows.append(
{
**row,
"odno": f"{int(order['odno']) + 1:010d}",
"orgn_odno": order["odno"],
"cncl_yn": "",
"cnc_cfrm_qty": "0",
"rmn_qty": "0",
}
)
continue
if self.fill_orders and order["rjct_qty"] == "0":
row["tot_ccld_qty"] = row["ord_qty"]
row["avg_prvs"] = row["ord_unpr"]
@@ -72,11 +89,13 @@ class _ExchangeState:
return {"rt_cd": "0", "output1": rows}
def balance(self) -> dict[str, object]:
quantity = (
sum(int(order["ord_qty"]) for order in self.orders if order["rjct_qty"] == "0")
if self.fill_orders
else 0
)
quantity = self.balance_qty
if quantity is None:
quantity = (
sum(int(order["ord_qty"]) for order in self.orders if order["rjct_qty"] == "0")
if self.fill_orders
else 0
)
rows = [{"pdno": SYMBOL, "hldg_qty": str(quantity)}] if quantity else []
return {"rt_cd": "0", "output1": rows}
@@ -265,3 +284,54 @@ def test_exchange_reject_after_broker_ack_rolls_back_and_allows_one_retry():
finally:
server.shutdown()
server.server_close()
def test_stable_correction_chain_returns_pending_sell_to_holding(monkeypatch):
state = _ExchangeState(first_attempt_reaches_exchange=True)
server = _serve(state)
mono = [100.0]
monkeypatch.setattr("trex_core.adapters.broker.kis.monotonic", lambda: mono[0])
try:
repo, slots, intents, execution, slot_id = _wire(_broker(server))
assert slot_id is not None
execution.run_once()
state.fill_orders = True
execution = ExecutionService(
slots,
repo,
_broker(server),
instance_key=INSTANCE,
clock=lambda: VIRTUAL_AT,
)
execution.reconcile()
bought = repo.fetch_slot(slot_id)
assert bought is not None
assert bought.status is SlotState.HOLDING
state.fill_orders = False
state.balance_qty = 2
state.next_side = "01"
state.correction_cancel_sell = True
admitted = intents.submit_sell(
SellIntent(intent_id="virtual-sell-corrected", slot_id=slot_id)
)
assert admitted.status == "admitted"
execution.run_once()
unresolved = repo.fetch_slot(slot_id)
assert unresolved is not None
assert unresolved.status is SlotState.PENDING_SELL
assert state.submit_attempts == 2
mono[0] += 90
execution.run_once()
holding = repo.fetch_slot(slot_id)
assert holding is not None
assert holding.status is SlotState.HOLDING
assert holding.qty == 2
assert holding.broker_order_id is None
assert state.submit_attempts == 2
finally:
server.shutdown()
server.server_close()