- REQ-017: LedgerPort contract (application/ledger_port.py) + portfolio-hub adapter + in-memory stub (adapters/ledger/), TREX_PORTFOLIO_HUB_* config - REQ-018: fill→ledger recording in execution loop; FillStatus carries execution_id (deterministic synth) + fee; paper/kis adapters updated - REQ-019: ledger outbox + retry worker (application/ledger_outbox.py), correlation_id idempotency, outcome vocabulary; 002_create_ledger_outbox.sql - Tests: in-memory/portfolio-hub ledger, execution ledger hook, outbox, domain import sweep Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""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.ledger import InMemoryLedger, PortfolioHubLedger
|
|
from trex_core.adapters.persistence.migrate import apply_migrations
|
|
from trex_core.adapters.persistence.postgresql import (
|
|
PostgreSQLIntentStore,
|
|
PostgreSQLLedgerOutbox,
|
|
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.application.ledger_outbox import LedgerWriter
|
|
from trex_core.application.ledger_port import LedgerPort
|
|
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_ledger(config: DaemonConfig) -> LedgerPort:
|
|
"""portfolio-hub when configured; otherwise in-memory stub (REQ-017)."""
|
|
pf = config.portfolio_hub
|
|
if pf is None:
|
|
return InMemoryLedger()
|
|
return PortfolioHubLedger(pf.base_url, token=pf.token or None, timeout=pf.timeout)
|
|
|
|
|
|
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)
|
|
outbox = PostgreSQLLedgerOutbox(config.dsn)
|
|
execution = ExecutionService(slot_service, slot_repo, broker, ledger_outbox=outbox)
|
|
ledger_writer = LedgerWriter(outbox, _build_ledger(config))
|
|
|
|
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 _execution_loop() -> None:
|
|
while not stop.wait(config.execution_interval_sec):
|
|
try:
|
|
execution.run_once()
|
|
except Exception: # noqa: BLE001 - loop must survive transient errors
|
|
continue
|
|
|
|
def _ledger_loop() -> None:
|
|
while not stop.wait(config.ledger_interval_sec):
|
|
try:
|
|
ledger_writer.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)
|
|
exec_worker = threading.Thread(target=_execution_loop, name="trex-execution", daemon=True)
|
|
ledger_worker = threading.Thread(target=_ledger_loop, name="trex-ledger", daemon=True)
|
|
exec_worker.start()
|
|
ledger_worker.start()
|
|
try:
|
|
yield
|
|
finally:
|
|
stop.set()
|
|
exec_worker.join(timeout=config.execution_interval_sec * 3)
|
|
ledger_worker.join(timeout=config.ledger_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()
|