功能更新

This commit is contained in:
2026-08-15 08:57:15 +08:00
parent 50fd032b45
commit c1c43d2ff7
30 changed files with 1908 additions and 888 deletions

View File

@@ -1,16 +1,23 @@
import type {
AdjustMode,
BacktestRequest,
BacktestResponse,
CurrentUser,
EventBacktestRequest,
EventBacktestResponse,
LoginRequest,
LoginResponse,
PreviewResponse,
ScreenerQueryItem,
ScreenerRunRequest,
ScreenerRunResponse,
ScreenerSyncRequest,
ScreenerSyncStatus,
StockFacets,
StockListResponse,
SyncRequest,
SyncResponse,
Timeframe,
} from './types';
// dev 用 Vite 代理(/api -> :8000生产构建设 VITE_API_BASE 指向后端地址。
@@ -81,6 +88,28 @@ export async function syncData(req: SyncRequest): Promise<SyncResponse> {
return (await res.json()) as SyncResponse;
}
/** 自然语言事件回测。全市场扫描较慢timeout 放宽到 15 分钟。 */
export async function postEventBacktest(req: EventBacktestRequest): Promise<EventBacktestResponse> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 15 * 60 * 1000);
try {
const res = await apiFetch('/api/backtest/event', {
method: 'POST',
body: JSON.stringify(req),
signal: ctrl.signal,
});
if (!res.ok) throw new ApiError(await readError(res, `事件回测失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as EventBacktestResponse;
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
throw new ApiError('事件回测超时(全市场扫描较慢,可先缩短日期范围或指定单只股票)', 0);
}
throw e;
} finally {
clearTimeout(timer);
}
}
export async function runScreener(req: ScreenerRunRequest): Promise<ScreenerRunResponse> {
const res = await apiFetch('/api/screener/run', { method: 'POST', body: JSON.stringify(req) });
if (!res.ok) throw new ApiError(await readError(res, `选股失败 (HTTP ${res.status})`), res.status);
@@ -99,8 +128,96 @@ export async function getScreenerSyncStatus(): Promise<ScreenerSyncStatus> {
return (await res.json()) as ScreenerSyncStatus;
}
export async function getStockPreview(tsCode: string, limit = 260): Promise<PreviewResponse> {
const res = await apiFetch(`/api/screener/preview/${encodeURIComponent(tsCode)}?limit=${limit}`);
export async function getStockPreview(
tsCode: string,
opts: {
limit?: number;
adjust?: AdjustMode;
timeframe?: Timeframe;
mas?: number[];
} = {},
): Promise<PreviewResponse> {
const { limit = 10000, adjust = 'qfq', timeframe = '1d', mas } = opts;
const q = new URLSearchParams({
limit: String(limit),
adjust,
timeframe,
...(mas?.length ? { mas: mas.join(',') } : {}),
});
const res = await apiFetch(`/api/screener/preview/${encodeURIComponent(tsCode)}?${q.toString()}`);
if (!res.ok) throw new ApiError(await readError(res, `获取个股详情失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as PreviewResponse;
}
export async function getStocks(params: {
search?: string;
market?: string;
industry?: string;
area?: string;
watched_only?: boolean;
limit?: number;
offset?: number;
}): Promise<StockListResponse> {
const q = new URLSearchParams();
if (params.search) q.set('search', params.search);
if (params.market) q.set('market', params.market);
if (params.industry) q.set('industry', params.industry);
if (params.area) q.set('area', params.area);
if (params.watched_only) q.set('watched_only', 'true');
q.set('limit', String(params.limit ?? 100));
q.set('offset', String(params.offset ?? 0));
const res = await apiFetch(`/api/stocks?${q.toString()}`);
if (!res.ok) throw new ApiError(await readError(res, `获取股票列表失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as StockListResponse;
}
export async function getStockFacets(): Promise<StockFacets> {
const res = await apiFetch('/api/stocks/facets');
if (!res.ok) throw new ApiError(await readError(res, `获取筛选项失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as StockFacets;
}
// ---------- 用户偏好 / 自选股 / 提问历史 ----------
export async function getPreferences(): Promise<Record<string, unknown>> {
const res = await apiFetch('/api/preferences');
if (!res.ok) throw new ApiError(await readError(res, `获取偏好失败 (HTTP ${res.status})`), res.status);
const data = (await res.json()) as { prefs: Record<string, unknown> };
return data.prefs ?? {};
}
export async function putPreferences(prefs: Record<string, unknown>): Promise<Record<string, unknown>> {
const res = await apiFetch('/api/preferences', { method: 'PUT', body: JSON.stringify({ prefs }) });
if (!res.ok) throw new ApiError(await readError(res, `保存偏好失败 (HTTP ${res.status})`), res.status);
const data = (await res.json()) as { prefs: Record<string, unknown> };
return data.prefs ?? {};
}
export async function getWatchlist(): Promise<string[]> {
const res = await apiFetch('/api/watchlist');
if (!res.ok) throw new ApiError(await readError(res, `获取自选股失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as string[];
}
export async function addWatchlist(tsCode: string): Promise<string[]> {
const res = await apiFetch('/api/watchlist', { method: 'POST', body: JSON.stringify({ ts_code: tsCode }) });
if (!res.ok) throw new ApiError(await readError(res, `加自选失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as string[];
}
export async function removeWatchlist(tsCode: string): Promise<string[]> {
const res = await apiFetch(`/api/watchlist/${encodeURIComponent(tsCode)}`, { method: 'DELETE' });
if (!res.ok) throw new ApiError(await readError(res, `移除自选失败 (HTTP ${res.status})`), res.status);
return (await res.json()) as string[];
}
export async function getScreenerQueries(limit = 20): Promise<ScreenerQueryItem[]> {
const res = await apiFetch(`/api/screener/queries?limit=${limit}`);
if (!res.ok) throw new ApiError(await readError(res, `获取提问历史失败 (HTTP ${res.status})`), res.status);
const data = (await res.json()) as { items: ScreenerQueryItem[] };
return data.items ?? [];
}
export async function deleteScreenerQuery(id: number): Promise<void> {
const res = await apiFetch(`/api/screener/queries/${id}`, { method: 'DELETE' });
if (!res.ok && res.status !== 401) throw new ApiError(`删除失败 (HTTP ${res.status})`, res.status);
}