"""FastAPI app factory for the slot-core daemon (REQ-005/007/008). `create_app` wires the HTTP /v1 surface to injected services so the same app runs against the in-memory adapters (tests) or PostgreSQL adapters (daemon). All slot writes go through IntentService → SlotService (single writer). """ from __future__ import annotations from datetime import date as date_cls from typing import Optional from fastapi import FastAPI, HTTPException, Query from trex_core.adapters.api.schemas import ( BuyIntentRequest, CapacityResponse, HealthResponse, IntentResponse, SellIntentRequest, SlotSnapshot, ) from trex_core.application.intents import BuyIntent, IntentService, SellIntent from trex_core.domain.slot import SlotRepository, SlotService def create_app( *, slot_service: SlotService, slot_repo: SlotRepository, intent_service: IntentService, default_total_slots: int, ) -> FastAPI: app = FastAPI(title="trex-core slot daemon", version="0.1.0") @app.get("/v1/healthz", response_model=HealthResponse) def healthz() -> HealthResponse: return HealthResponse() @app.post("/v1/intents/buy", response_model=IntentResponse) def submit_buy(req: BuyIntentRequest) -> IntentResponse: d = intent_service.submit_buy( BuyIntent( intent_id=req.intent_id, trading_date=req.trading_date, symbol=req.symbol, qty=req.qty, allocated_capital=req.allocated_capital, entry_price=req.entry_price, strategy_id=req.strategy_id, sell_strategy_id=req.sell_strategy_id, portfolio_id=req.portfolio_id, forced_close_at=req.forced_close_at, ) ) return IntentResponse(intent_id=d.intent_id, status=d.status, slot_id=d.slot_id, reason=d.reason) @app.post("/v1/intents/sell", response_model=IntentResponse) def submit_sell(req: SellIntentRequest) -> IntentResponse: d = intent_service.submit_sell( SellIntent( intent_id=req.intent_id, slot_id=req.slot_id, reason=req.reason, sell_strategy_id=req.sell_strategy_id, ) ) return IntentResponse(intent_id=d.intent_id, status=d.status, slot_id=d.slot_id, reason=d.reason) @app.get("/v1/intents/{intent_id}", response_model=IntentResponse) def get_intent(intent_id: str) -> IntentResponse: d = intent_service.lookup(intent_id) if d is None: raise HTTPException(status_code=404, detail="intent not found") return IntentResponse(intent_id=d.intent_id, status=d.status, slot_id=d.slot_id, reason=d.reason) @app.get("/v1/slots", response_model=list[SlotSnapshot]) def list_slots( status: Optional[str] = Query(default=None), trading_date: Optional[date_cls] = Query(default=None), ) -> list[SlotSnapshot]: day = trading_date or _today() slots = slot_repo.list_slots(day) if status is not None: slots = [s for s in slots if s.status.value == status] return [ SlotSnapshot( slot_id=s.slot_id, trading_date=s.trading_date, status=s.status.value, symbol=s.symbol, qty=s.qty, avg_price=s.avg_price, entry_price=s.entry_price, allocated_capital=s.allocated_capital, strategy_id=s.strategy_id, sell_strategy_id=s.sell_strategy_id, forced_close_at=s.forced_close_at, ) for s in slots ] @app.get("/v1/capacity", response_model=CapacityResponse) def capacity( trading_date: Optional[date_cls] = Query(default=None), total_slots: Optional[int] = Query(default=None), ) -> CapacityResponse: day = trading_date or _today() cap = slot_service.compute_slot_capacity(day, total_slots or default_total_slots) return CapacityResponse( idle_count=cap.idle_count, holding_count=cap.holding_count, pending_count=cap.pending_count, total_slots=cap.total_slots, scarcity_ratio=cap.scarcity_ratio, ) return app def _today() -> date_cls: from datetime import datetime, timezone return datetime.now(tz=timezone.utc).date()