Deliver BAT-004: broker execution + KIS adapter + durable handoff
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>
This commit is contained in:
+44
-11
@@ -1,34 +1,52 @@
|
||||
"""Slot-core daemon entrypoint (REQ-005).
|
||||
"""Slot-core daemon entrypoint (REQ-005, REQ-012/013/014).
|
||||
|
||||
Builds the FastAPI app wired to PostgreSQL adapters, applies migrations and
|
||||
preallocates the trading day's slots on startup (lifecycle, not a public
|
||||
endpoint), then serves via uvicorn. Console script: `trex-core-daemon`.
|
||||
Builds the FastAPI app wired to PostgreSQL adapters + a broker (paper|KIS),
|
||||
applies migrations and preallocates slots on startup, runs a background
|
||||
execution loop (claim -> submit -> poll -> transition, with restart
|
||||
reconciliation), and serves the /v1 API via uvicorn.
|
||||
Console script: `trex-core-daemon`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from trex_core.adapters.api.app import create_app
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
from trex_core.adapters.persistence.migrate import apply_migrations
|
||||
from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore # noqa: F401
|
||||
from trex_core.adapters.persistence.postgresql import (
|
||||
PostgreSQLIntentStore,
|
||||
PostgreSQLSlotRepository,
|
||||
)
|
||||
from trex_core.application.broker_port import BrokerPort
|
||||
from trex_core.application.execution import ExecutionService
|
||||
from trex_core.application.intents import IntentService
|
||||
from trex_core.domain.slot import SlotService
|
||||
from trex_core.runtime.config import DaemonConfig
|
||||
|
||||
|
||||
def _build_broker(config: DaemonConfig) -> BrokerPort:
|
||||
if config.broker == "kis":
|
||||
from trex_core.adapters.broker.kis import KISBroker
|
||||
|
||||
k = config.kis
|
||||
return KISBroker(
|
||||
k.account_ref, app_key=k.app_key, app_secret=k.app_secret,
|
||||
base_url=k.base_url, timeout=k.timeout, allow_live=k.allow_live,
|
||||
)
|
||||
return PaperBroker()
|
||||
|
||||
|
||||
def build_app(config: DaemonConfig) -> FastAPI:
|
||||
"""Construct the daemon app with PostgreSQL adapters + startup lifecycle."""
|
||||
slot_repo = PostgreSQLSlotRepository(config.dsn)
|
||||
intent_store = PostgreSQLIntentStore(config.dsn)
|
||||
slot_service = SlotService(slot_repo)
|
||||
intent_service = IntentService(slot_service, slot_repo, intent_store)
|
||||
broker = _build_broker(config)
|
||||
execution = ExecutionService(slot_service, slot_repo, broker)
|
||||
|
||||
base = create_app(
|
||||
slot_service=slot_service,
|
||||
@@ -37,14 +55,29 @@ def build_app(config: DaemonConfig) -> FastAPI:
|
||||
default_total_slots=config.total_slots,
|
||||
)
|
||||
|
||||
stop = threading.Event()
|
||||
|
||||
def _loop() -> None:
|
||||
while not stop.wait(config.execution_interval_sec):
|
||||
try:
|
||||
execution.run_once()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
continue
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
# Startup: migrate schema, preallocate today's slots (idempotent).
|
||||
apply_migrations(config.dsn)
|
||||
today = datetime.now(tz=timezone.utc).date()
|
||||
slot_service.preallocate_daily_slots(today, config.total_slots)
|
||||
yield
|
||||
# Shutdown: connection-per-call adapters hold no pooled state to close.
|
||||
slot_service.preallocate_daily_slots(
|
||||
datetime.now(tz=timezone.utc).date(), config.total_slots
|
||||
)
|
||||
execution.reconcile() # restart recovery before serving (REQ-014)
|
||||
worker = threading.Thread(target=_loop, name="trex-execution", daemon=True)
|
||||
worker.start()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stop.set()
|
||||
worker.join(timeout=config.execution_interval_sec * 3)
|
||||
|
||||
base.router.lifespan_context = lifespan
|
||||
return base
|
||||
|
||||
Reference in New Issue
Block a user