import time import uuid from fastapi import APIRouter, HTTPException, Query, Request, Response from pydantic import BaseModel from app.bars.models import Timeframe from app.analysis.levels import Side from app.analysis.manual_lines import ManualLine router = APIRouter(prefix="/api") class LineCreate(BaseModel): tf: Timeframe side: Side anchor_t: int anchor_p: float end_t: int end_p: float note: str = "" hidden: bool = False class LinePatch(BaseModel): side: Side | None = None note: str | None = None hidden: bool | None = None @router.get("/health") def health(): return {"status": "ok", "service": "chart"} @router.get("/status") def status(request: Request): runtime = request.app.state.runtime return { "stream": runtime.stream.status, "source": runtime.stream.source.name, "symbol": runtime.stream.symbol, "last_bar_t": runtime.stream.last_bar_t, "bars_held": runtime.store.counts(), "warm": {tf.value: bool(runtime.store.get(tf)) for tf in Timeframe}, } @router.get("/bars") def bars(request: Request, tf: str = "1m", limit: int = Query(500, ge=1, le=5000)): try: timeframe = Timeframe(tf) except ValueError as exc: raise HTTPException(400, "Unknown timeframe") from exc values = request.app.state.runtime.store.get(timeframe, limit) return {"tf": timeframe.value, "bars": [bar.to_dict() for bar in values]} @router.get("/levels") def levels(request: Request, tf: str = "all"): values = request.app.state.runtime.levels if tf != "all": try: timeframe = Timeframe(tf) except ValueError as exc: raise HTTPException(400, "Unknown timeframe") from exc values = [level for level in values if level.tf is timeframe] return {"levels": [level.to_dict() for level in values]} @router.get("/confluence") def confluence(request: Request): runtime = request.app.state.runtime return { "price": runtime.price, "clusters": [cluster.to_dict() for cluster in runtime.clusters], } @router.post("/lines", status_code=201) def create_line(request: Request, payload: LineCreate): if payload.end_t == payload.anchor_t: raise HTTPException(400, "Line endpoints must have different times") line = ManualLine( id=f"ml_{uuid.uuid4().hex}", tf=payload.tf, side=payload.side, anchor_t=payload.anchor_t, anchor_p=payload.anchor_p, slope=(payload.end_p - payload.anchor_p) / (payload.end_t - payload.anchor_t), last_t=payload.end_t, created_at=int(time.time()), note=payload.note, hidden=payload.hidden, ) runtime = request.app.state.runtime runtime.manual_lines.add(line) runtime.rebuild_levels() return line.to_level().to_dict() @router.patch("/lines/{line_id}") def patch_line(request: Request, line_id: str, payload: LinePatch): changes = payload.model_dump(exclude_none=True) try: line = request.app.state.runtime.manual_lines.update(line_id, changes) except KeyError as exc: raise HTTPException(404, "Line not found") from exc request.app.state.runtime.rebuild_levels() return line.to_level().to_dict() @router.delete("/lines/{line_id}", status_code=204) def delete_line(request: Request, line_id: str): try: request.app.state.runtime.manual_lines.delete(line_id) except KeyError as exc: raise HTTPException(404, "Line not found") from exc request.app.state.runtime.rebuild_levels() return Response(status_code=204)