fix: reconcile KIS correction order chains
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user