import type { AttributionItem, BacktestRunRequest, CoverageTimeline, DatasetSummary, DrawdownPoint, ExperimentDetail, ExperimentSummary, Health, MarketBarItem, MonthlyReturns, PortfolioAllocationRequest, PortfolioAllocationResponse, SeriesPoint, StrategyCatalogItem, SymbolCatalogItem, TradePageResponse, } from "./types"; async function request(path: string, options: RequestInit = {}): Promise { const response = await fetch(path, { ...options, headers: { "Content-Type": "application/json", ...(options.headers || {}), }, }); if (!response.ok) { let errorDetail = `HTTP ${response.status}`; try { const errorJson = await response.json(); if (errorJson.detail) { errorDetail = typeof errorJson.detail === "string" ? errorJson.detail : JSON.stringify(errorJson.detail); } } catch { // ignore json parse error on non-json error responses } throw new Error(errorDetail); } return response.json() as Promise; } export const api = { // System health: () => request("/health"), // Dataset datasetSummary: () => request("/api/v1/dataset/summary"), symbols: (search?: string, sector?: string) => { const params = new URLSearchParams(); if (search) params.append("search", search); if (sector && sector !== "ALL") params.append("sector", sector); const qs = params.toString() ? `?${params.toString()}` : ""; return request(`/api/v1/dataset/symbols${qs}`); }, coverageTimeline: () => request("/api/v1/dataset/coverage-timeline"), // Market Data bars: ( symbol: string, start?: string, end?: string, resolution: "15m" | "1h" | "1d" = "15m", limit = 1500 ) => { const params = new URLSearchParams({ symbol, resolution, limit: limit.toString(), }); if (start) params.append("start", start); if (end) params.append("end", end); return request(`/api/v1/market/bars?${params.toString()}`); }, // Strategies & Backtesting strategies: () => request("/api/v1/strategies"), runBacktest: (payload: BacktestRunRequest) => request("/api/v1/backtest/run", { method: "POST", body: JSON.stringify(payload), }), streamBacktest: async ( payload: BacktestRunRequest, onProgress: (progress: { progress_pct: number; current_date: string; current_equity: number; trades_count: number; point?: { timestamp: string; equity: number }; last_trade?: { symbol: string; net_pnl: number; exit_time: string; exit_reason: string }; }) => void, onComplete: (detail: ExperimentDetail) => void, onError: (err: Error) => void ) => { try { const response = await fetch("/api/v1/backtest/stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const reader = response.body?.getReader(); if (!reader) throw new Error("ReadableStream not supported"); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n\n"); buffer = lines.pop() || ""; for (const block of lines) { const line = block.trim(); if (line.startsWith("data: ")) { try { const data = JSON.parse(line.slice(6)); if (data.type === "progress") { onProgress(data); } else if (data.type === "complete") { onComplete(data); } } catch (e) { console.error("Error parsing stream chunk:", e); } } } } } catch (err: any) { onError(err instanceof Error ? err : new Error(String(err))); } }, // Experiments experiments: (offset = 0, limit = 50) => request<{ items: ExperimentSummary[]; total: number }>( `/api/v1/experiments?offset=${offset}&limit=${limit}` ), experiment: (id: string) => request(`/api/v1/experiments/${encodeURIComponent(id)}`), equity: (id: string, maxPoints = 1500) => request( `/api/v1/experiments/${encodeURIComponent(id)}/equity?maximum_points=${maxPoints}` ), drawdown: (id: string, maxPoints = 1500) => request( `/api/v1/experiments/${encodeURIComponent(id)}/drawdown?maximum_points=${maxPoints}` ), trades: ( id: string, options: { symbol?: string; outcome?: "win" | "loss"; exitReason?: string; offset?: number; limit?: number; } = {} ) => { const params = new URLSearchParams({ offset: (options.offset ?? 0).toString(), limit: (options.limit ?? 100).toString(), }); if (options.symbol) params.append("symbol", options.symbol); if (options.outcome) params.append("outcome", options.outcome); if (options.exitReason) params.append("exit_reason", options.exitReason); return request( `/api/v1/experiments/${encodeURIComponent(id)}/trades?${params.toString()}` ); }, attribution: (id: string) => request(`/api/v1/experiments/${encodeURIComponent(id)}/attribution`), monthly: (id: string) => request(`/api/v1/experiments/${encodeURIComponent(id)}/monthly`), compare: (ids: string[]) => { const params = new URLSearchParams(); ids.forEach((id) => params.append("ids", id)); return request(`/api/v1/experiments/compare?${params.toString()}`); }, // Portfolio Allocation allocatePortfolio: (req: PortfolioAllocationRequest) => request("/api/v1/portfolio/allocate", { method: "POST", body: JSON.stringify(req), }), // Export URLs exportTradesCsvUrl: (id: string) => `/api/v1/reports/${encodeURIComponent(id)}/export-csv?kind=trades`, exportEquityCsvUrl: (id: string) => `/api/v1/reports/${encodeURIComponent(id)}/export-csv?kind=equity`, };