"""Authenticated adapters; every mutation is committed before the execution lane wakes.""" from typing import Literal from fastapi import APIRouter, Depends, Query, Request from ..business import Business from ..research.contracts import ResearchPreviewInput from ..research.service import ResearchBuilder from ..security import require_auth from .contracts import ( ControlInput, DraftInput, DraftUpdate, EventPage, PreviewInput, PreviewOutput, ReferenceInput, RerunInput, ResultPage, RunOutput, RunPage, SchedulerInput, SchedulerOutput, StartInput, SubsetInput, ) router = APIRouter(prefix="/api/v1/backtests", tags=["backtests"], dependencies=[Depends(require_auth)]) @router.get("/capabilities") async def capabilities(request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.capabilities() @router.get("/config", response_model=SchedulerOutput) async def config(request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.config() @router.put("/config", response_model=SchedulerOutput) async def configure(body: SchedulerInput, request: Request): async with request.app.state.sessions.begin() as db: result = await Business(db).backtests.configure(body) request.app.state.runner.backtests.wake.set() return result @router.get("/drafts") async def drafts( request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0), q: str = Query("", max_length=200), sort: Literal["name", "updated_at"] = "updated_at", direction: Literal["asc", "desc"] = "desc", ): async with request.app.state.sessions() as db: return await Business(db).backtests.drafts(limit, offset, q, sort, direction) @router.post("/drafts", status_code=201) async def save_draft(body: DraftInput, request: Request): async with request.app.state.sessions.begin() as db: return await Business(db).backtests.save_draft(body) @router.get("/drafts/{draft_id}") async def draft(draft_id: str, request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.draft(draft_id) @router.put("/drafts/{draft_id}") async def update_draft(draft_id: str, body: DraftUpdate, request: Request): async with request.app.state.sessions.begin() as db: return await Business(db).backtests.save_draft(body, draft_id) @router.post("/previews", status_code=201, response_model=PreviewOutput) async def preview(body: PreviewInput, request: Request): async with request.app.state.sessions.begin() as db: return await Business(db).backtests.preview(body) @router.post("/research-previews", status_code=201, response_model=PreviewOutput) async def research_preview(body: ResearchPreviewInput, request: Request): """Prepare typed field bindings for any research producer; never start a simulation.""" async with request.app.state.sessions.begin() as db: return await ResearchBuilder(db, Business(db).backtests).prepare(body) @router.get("/previews/{preview_id}", response_model=PreviewOutput) async def get_preview( preview_id: str, request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0) ): async with request.app.state.sessions() as db: return await Business(db).backtests.get_preview(preview_id, limit, offset) @router.post("/runs", status_code=202, response_model=RunOutput) async def start(body: StartInput, request: Request): async with request.app.state.sessions.begin() as db: result = await Business(db).backtests.start(body) request.app.state.runner.backtests.wake.set() return result @router.get("/runs", response_model=RunPage) async def runs( request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0), source: str | None = Query(None, max_length=100), reference: str | None = Query(None, max_length=200), research_id: str | None = Query(None, max_length=200), q: str = Query("", max_length=200), sort: Literal["name", "created_at"] = "created_at", direction: Literal["asc", "desc"] = "desc", ): async with request.app.state.sessions() as db: return await Business(db).backtests.runs( limit, offset, source, reference, research_id, q, sort, direction ) @router.get("/sources", response_model=list[str]) async def sources(request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.sources() @router.get("/runs/{run_id}", response_model=RunOutput) async def run(run_id: str, request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.run(run_id) @router.get("/runs/{run_id}/results", response_model=ResultPage) async def results( run_id: str, request: Request, limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0) ): async with request.app.state.sessions() as db: return await Business(db).backtests.results(run_id, limit, offset) @router.get("/runs/{run_id}/events", response_model=EventPage) async def events( run_id: str, request: Request, after: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=100) ): async with request.app.state.sessions() as db: return await Business(db).backtests.events(run_id, after, limit) @router.get("/runs/{run_id}/attempts") async def attempts(run_id: str, request: Request): async with request.app.state.sessions() as db: return await Business(db).backtests.attempts(run_id) @router.post("/runs/{run_id}/control", response_model=RunOutput) async def control(run_id: str, body: ControlInput, request: Request): async with request.app.state.sessions.begin() as db: result = await Business(db).backtests.control(run_id, body) request.app.state.runner.backtests.wake.set() return result @router.post("/runs/{run_id}/rerun-preview", status_code=201, response_model=PreviewOutput) async def rerun(run_id: str, body: RerunInput, request: Request): async with request.app.state.sessions.begin() as db: return await Business(db).backtests.rerun(run_id, body) @router.post("/attempts/{attempt_id}/reference", response_model=RunOutput) async def attach_reference(attempt_id: str, body: ReferenceInput, request: Request): from fastapi import HTTPException from ..worldquant import WqError try: body.progress_url = request.app.state.runner.client.simulation_url(body.progress_url) except WqError as exc: raise HTTPException(422, str(exc)) from None async with request.app.state.sessions.begin() as db: result = await Business(db).backtests.attach_reference(attempt_id, body) request.app.state.runner.backtests.wake.set() return result @router.post("/previews/{preview_id}/subset", status_code=201, response_model=PreviewOutput) async def subset(preview_id: str, body: SubsetInput, request: Request): async with request.app.state.sessions.begin() as db: return await Business(db).backtests.subset(preview_id, body)