drift-quant / frontend /src /api /client.ts
deveshc23
feat: Add Free Gradio Space support, Git LFS tracking for data artifacts, and Quant Allocator
3e33e2e
Raw History Blame Contribute Delete
6.4 kB
import type {
AttributionItem,
BacktestRunRequest,
CoverageTimeline,
DatasetSummary,
DrawdownPoint,
ExperimentDetail,
ExperimentSummary,
Health,
MarketBarItem,
MonthlyReturns,
PortfolioAllocationRequest,
PortfolioAllocationResponse,
SeriesPoint,
StrategyCatalogItem,
SymbolCatalogItem,
TradePageResponse,
} from "./types";
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
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<T>;
}
export const api = {
// System
health: () => request<Health>("/health"),
// Dataset
datasetSummary: () => request<DatasetSummary>("/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<SymbolCatalogItem[]>(`/api/v1/dataset/symbols${qs}`);
},
coverageTimeline: () => request<CoverageTimeline>("/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<MarketBarItem[]>(`/api/v1/market/bars?${params.toString()}`);
},
// Strategies & Backtesting
strategies: () => request<StrategyCatalogItem[]>("/api/v1/strategies"),
runBacktest: (payload: BacktestRunRequest) =>
request<ExperimentDetail>("/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<ExperimentDetail>(`/api/v1/experiments/${encodeURIComponent(id)}`),
equity: (id: string, maxPoints = 1500) =>
request<SeriesPoint[]>(
`/api/v1/experiments/${encodeURIComponent(id)}/equity?maximum_points=${maxPoints}`
),
drawdown: (id: string, maxPoints = 1500) =>
request<DrawdownPoint[]>(
`/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<TradePageResponse>(
`/api/v1/experiments/${encodeURIComponent(id)}/trades?${params.toString()}`
);
},
attribution: (id: string) =>
request<AttributionItem[]>(`/api/v1/experiments/${encodeURIComponent(id)}/attribution`),
monthly: (id: string) =>
request<MonthlyReturns>(`/api/v1/experiments/${encodeURIComponent(id)}/monthly`),
compare: (ids: string[]) => {
const params = new URLSearchParams();
ids.forEach((id) => params.append("ids", id));
return request<ExperimentSummary[]>(`/api/v1/experiments/compare?${params.toString()}`);
},
// Portfolio Allocation
allocatePortfolio: (req: PortfolioAllocationRequest) =>
request<PortfolioAllocationResponse>("/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`,
};