110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
from typing import Callable, Optional
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from fh_ob.book_store import BookStore, OrderBookTop5
|
|
|
|
|
|
class BookLevelResponse(BaseModel):
|
|
price: str
|
|
size: str
|
|
|
|
|
|
class OrderBookResponse(BaseModel):
|
|
symbol: str
|
|
bids: list[BookLevelResponse]
|
|
asks: list[BookLevelResponse]
|
|
ts_ms: int
|
|
|
|
|
|
class HealthResponse(BaseModel):
|
|
status: str
|
|
books_tracked: int
|
|
socket_clients: int
|
|
subscribed_symbols: int
|
|
connected: bool
|
|
last_update_ms: Optional[int] = None
|
|
reconnect_count: int = 0
|
|
last_reconnect_ms: Optional[int] = None
|
|
|
|
|
|
class SymbolOpRequest(BaseModel):
|
|
symbol: str
|
|
|
|
|
|
class SymbolsResponse(BaseModel):
|
|
symbols: list[str]
|
|
|
|
|
|
def create_app(
|
|
book_store: BookStore,
|
|
get_socket_clients: Optional[Callable[[], int]] = None,
|
|
get_subscribed_count: Optional[Callable[[], int]] = None,
|
|
is_connected: Optional[Callable[[], bool]] = None,
|
|
add_symbol: Optional[Callable[[str], bool]] = None,
|
|
remove_symbol: Optional[Callable[[str], bool]] = None,
|
|
get_symbols: Optional[Callable[[], list[str]]] = None,
|
|
get_reconnect_stats: Optional[Callable[[], tuple[int, int]]] = None,
|
|
) -> FastAPI:
|
|
app = FastAPI(title="FH+OB Debug API", description="Dev-only debug endpoint")
|
|
|
|
@app.get("/book/{symbol}", response_model=OrderBookResponse)
|
|
async def get_book(symbol: str) -> OrderBookResponse:
|
|
book = book_store.get(symbol)
|
|
if book is None:
|
|
raise HTTPException(status_code=404, detail=f"No book data for {symbol}")
|
|
return OrderBookResponse(
|
|
symbol=book.symbol,
|
|
bids=[BookLevelResponse(price=str(b.price), size=str(b.size)) for b in book.bids],
|
|
asks=[BookLevelResponse(price=str(a.price), size=str(a.size)) for a in book.asks],
|
|
ts_ms=book.ts_ms,
|
|
)
|
|
|
|
@app.get("/books", response_model=dict[str, OrderBookResponse])
|
|
async def get_all_books() -> dict[str, OrderBookResponse]:
|
|
books = book_store.get_all()
|
|
return {
|
|
symbol: OrderBookResponse(
|
|
symbol=book.symbol,
|
|
bids=[BookLevelResponse(price=str(b.price), size=str(b.size)) for b in book.bids],
|
|
asks=[BookLevelResponse(price=str(a.price), size=str(a.size)) for a in book.asks],
|
|
ts_ms=book.ts_ms,
|
|
)
|
|
for symbol, book in books.items()
|
|
}
|
|
|
|
@app.get("/symbols")
|
|
async def list_symbols():
|
|
return SymbolsResponse(symbols=get_symbols() if get_symbols else [])
|
|
|
|
@app.post("/symbols")
|
|
async def add_sym(req: SymbolOpRequest):
|
|
if add_symbol and add_symbol(req.symbol):
|
|
return SymbolsResponse(symbols=get_symbols() if get_symbols else [])
|
|
raise HTTPException(status_code=400, detail="Symbol not found or already subscribed")
|
|
|
|
@app.delete("/symbols/{symbol}")
|
|
async def rm_sym(symbol: str):
|
|
if remove_symbol and remove_symbol(symbol):
|
|
return SymbolsResponse(symbols=get_symbols() if get_symbols else [])
|
|
raise HTTPException(status_code=404, detail="Symbol not found or not subscribed")
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def health() -> HealthResponse:
|
|
books = book_store.get_all()
|
|
latest_ts = max((b.ts_ms for b in books.values()), default=None)
|
|
reconnects, last_reconnect_ms = get_reconnect_stats() if get_reconnect_stats else (0, None)
|
|
|
|
return HealthResponse(
|
|
status="ok" if (is_connected and is_connected()) else "degraded",
|
|
books_tracked=len(books),
|
|
socket_clients=get_socket_clients() if get_socket_clients else 0,
|
|
subscribed_symbols=get_subscribed_count() if get_subscribed_count else 0,
|
|
connected=is_connected() if is_connected else False,
|
|
last_update_ms=latest_ts,
|
|
reconnect_count=reconnects,
|
|
last_reconnect_ms=last_reconnect_ms,
|
|
)
|
|
|
|
return app |