Implement REQ-011~015 (dcy-003): the order-execution layer on top of the BAT-002 slot-core daemon, connecting intents to real orders. - application: BrokerPort (submit/poll_fill/cancel) + ExecutionService (claim -> submit -> poll -> transition -> rollback -> reconcile) - adapters/broker: PaperBroker (deterministic async double) and KISBroker (KIS Open API: token, order-cash, inquire-daily-ccld, cancel; VTS sandbox tr_ids; live account gated behind allow_live) - persistence: option-A execution queue (find_pending_for_dispatch FOR UPDATE SKIP LOCKED, mark_dispatched idempotency guard, clear_dispatch) - runtime: background execution loop in the daemon lifespan + startup reconcile; config selects paper|kis Verification (real KIS VTS 모의투자 + PostgreSQL): 92 passed, ruff clean, domain import sweep shows zero broker/web/db deps, buy->holding / sell->closed / reject->idle, idempotent dispatch (exactly one order), reconcile after crash creates no duplicate order, KIS sandbox token+submit round-trip, and a daemon E2E through the background loop. Align the four baseline docs. BAT-004 -> release-candidate; REQ-011~015 -> Implemented. KIS live-account go-live, partial-fill accrual, option-B outbox, connection pooling, and the safety floor remain deferred to follow-up Discoveries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
1.9 KiB
Python
51 lines
1.9 KiB
Python
"""Paper broker deterministic async double tests (REQ-011)."""
|
|
from __future__ import annotations
|
|
|
|
from trex_core.adapters.broker.paper import PaperBroker
|
|
from trex_core.application.broker_port import OrderRequest
|
|
|
|
|
|
def _req(symbol="005930", qty=10, price=1000):
|
|
return OrderRequest(slot_id="s1", side="buy", symbol=symbol, qty=qty, limit_price=price)
|
|
|
|
|
|
def test_submit_does_not_synchronously_fill():
|
|
b = PaperBroker(fill_after_polls=1)
|
|
ack = b.submit_order(_req())
|
|
assert ack.accepted and ack.broker_order_id
|
|
# next poll is still 'accepted' (async contract, no synchronous shortcut)
|
|
fs = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930")
|
|
assert fs.state == "accepted" and fs.filled_qty == 0
|
|
|
|
|
|
def test_fills_after_configured_polls():
|
|
b = PaperBroker(fill_after_polls=1)
|
|
ack = b.submit_order(_req())
|
|
b.poll_fill(ack.broker_order_id, side="buy", symbol="005930") # poll 1 -> accepted
|
|
fs = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930") # poll 2 -> filled
|
|
assert fs.is_filled and fs.filled_qty == 10 and fs.avg_price == 1000.0
|
|
|
|
|
|
def test_immediate_fill_on_first_poll():
|
|
b = PaperBroker(fill_after_polls=0)
|
|
ack = b.submit_order(_req())
|
|
fs = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930")
|
|
assert fs.is_filled
|
|
|
|
|
|
def test_partial_then_full():
|
|
b = PaperBroker(fill_after_polls=0, qty_per_poll=4)
|
|
ack = b.submit_order(_req(qty=10))
|
|
f1 = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930")
|
|
assert f1.state == "partially_filled" and f1.filled_qty == 4
|
|
b.poll_fill(ack.broker_order_id, side="buy", symbol="005930") # 8
|
|
f3 = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930") # 10
|
|
assert f3.is_filled and f3.filled_qty == 10
|
|
|
|
|
|
def test_reject():
|
|
b = PaperBroker(reject_symbols={"005930"})
|
|
ack = b.submit_order(_req())
|
|
fs = b.poll_fill(ack.broker_order_id, side="buy", symbol="005930")
|
|
assert fs.state == "rejected"
|