"""Slot-core daemon entrypoint (REQ-005, REQ-012/013/014). 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.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 token_cache = None if k.redis_url: from trex_core.adapters.broker.token_cache import RedisTokenCache token_cache = RedisTokenCache(k.redis_url) 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, token_cache=token_cache, ) return PaperBroker() def build_app(config: DaemonConfig) -> FastAPI: 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, slot_repo=slot_repo, intent_service=intent_service, 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): apply_migrations(config.dsn) 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 def main() -> None: # pragma: no cover - process entrypoint import uvicorn config = DaemonConfig.from_env() app = build_app(config) uvicorn.run(app, host=config.host, port=config.port) if __name__ == "__main__": # pragma: no cover main()