Deliver BAT-002: slot-core daemon + HTTP intent API + PostgreSQL

Implement REQ-005~009 (dcy-002 slice 1) as an onion stack on top of the
BAT-001 pure domain:
- application: IntentService (admission + single-writer transition +
  intent_id idempotency via durable intent_log)
- adapters/persistence: PostgreSQLSlotRepository + PostgreSQLIntentStore +
  trex-core-owned migrations (daily_slot per Appendix A, daily_symbol_entries,
  slot_transition_log, intent_log) + in-memory intent store
- adapters/api: FastAPI /v1 (healthz, intents/buy|sell|{id}, slots, capacity)
- runtime: trex-core-daemon entrypoint with lifespan migrate+preallocate

Verification (against local PostgreSQL 18): 74 passed, ruff clean, domain
import sweep shows zero web/db deps (NFR-1), 20-thread concurrency admits
exactly the idle count (NFR-2), idempotent resubmit causes no double
transition (NFR-3), EXPECTED_SLOT_FIELDS<->information_schema drift test
(realizes dcy-001 OQ-2's deferred DB-side check), and a daemon E2E smoke.

Align all four baseline docs to the new daemon/API/persistence layers.
BAT-002 -> release-candidate; REQ-005~009 -> Implemented. Full async outbox,
broker execution, safety floor, and strategy daemons remain deferred to a
follow-up Discovery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:46:08 +09:00
co-authored by Claude Opus 4.8
parent a44f9fddb7
commit a4e263c8e7
41 changed files with 1778 additions and 38 deletions
View File
+27
View File
@@ -0,0 +1,27 @@
"""Daemon configuration resolution (REQ-005)."""
from __future__ import annotations
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class DaemonConfig:
dsn: str
total_slots: int
host: str
port: int
@staticmethod
def from_env() -> "DaemonConfig":
dsn = os.environ.get("TREX_POSTGRESQL_DSN")
if not dsn:
raise RuntimeError(
"TREX_POSTGRESQL_DSN is required to start the slot-core daemon."
)
return DaemonConfig(
dsn=dsn,
total_slots=int(os.environ.get("TREX_TOTAL_SLOTS", "10")),
host=os.environ.get("TREX_API_HOST", "127.0.0.1"),
port=int(os.environ.get("TREX_API_PORT", "8080")),
)
+62
View File
@@ -0,0 +1,62 @@
"""Slot-core daemon entrypoint (REQ-005).
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`.
"""
from __future__ import annotations
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.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.intents import IntentService
from trex_core.domain.slot import SlotService
from trex_core.runtime.config import DaemonConfig
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)
base = create_app(
slot_service=slot_service,
slot_repo=slot_repo,
intent_service=intent_service,
default_total_slots=config.total_slots,
)
@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.
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()