- 首页双入口(智能选股/策略回测):引入 vue-router,顶部导航 - 智能选股:自然语言 -> LLM 解析结构化条件(智谱 GLM,OpenAI 兼容,/v4 兼容)-> SQL 快照预筛 + pandas 指标过滤(复用 indicators 单一事实源) - 条件模型:指标 vs 常数/指标(value_indicator,如 DIF>DEA、close<布林下轨)、lookback+match 表达连续N天/近N天任一天、市值/PE/PB/换手率快照条件、默认排除 ST/退市/北交所 - 全市场数据同步:按 trade_date 批量拉取未复权日线(与回测 candles qfq 隔离),交易日历/股票列表本地缓存,daily_basic 仅最新截面,Tushare 限频兜底(分钟级重试/小时级降级) - 存储:DATABASE_URL 切远程 PostgreSQL(cirry.cn/stock),本地 SQLite 已移除 - .env 入库(私有仓库);smoke_test 扩展选股链路 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import type {
|
||
BacktestRequest,
|
||
BacktestResponse,
|
||
ScreenerRunRequest,
|
||
ScreenerRunResponse,
|
||
ScreenerSyncRequest,
|
||
ScreenerSyncStatus,
|
||
SyncRequest,
|
||
SyncResponse,
|
||
} from './types';
|
||
|
||
// dev 用 Vite 代理(/api -> :8000);生产构建设 VITE_API_BASE 指向后端地址。
|
||
const BASE = import.meta.env.VITE_API_BASE ?? '';
|
||
|
||
export async function postBacktest(req: BacktestRequest): Promise<BacktestResponse> {
|
||
const res = await fetch(`${BASE}/api/backtest`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(req),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`回测请求失败 (HTTP ${res.status}): ${await res.text()}`);
|
||
}
|
||
return (await res.json()) as BacktestResponse;
|
||
}
|
||
|
||
export async function syncData(req: SyncRequest): Promise<SyncResponse> {
|
||
const res = await fetch(`${BASE}/api/data/sync`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(req),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`数据拉取失败 (HTTP ${res.status}): ${await res.text()}`);
|
||
}
|
||
return (await res.json()) as SyncResponse;
|
||
}
|
||
|
||
// ---------- 智能选股 ----------
|
||
export async function runScreener(req: ScreenerRunRequest): Promise<ScreenerRunResponse> {
|
||
const res = await fetch(`${BASE}/api/screener/run`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(req),
|
||
});
|
||
if (!res.ok) {
|
||
// 后端 detail 字段携带可读中文原因(503 未配 key / 409 未同步 / 502 LLM 错)
|
||
let detail = '';
|
||
try {
|
||
detail = (await res.json())?.detail ?? '';
|
||
} catch {
|
||
detail = await res.text();
|
||
}
|
||
throw new Error(detail || `选股失败 (HTTP ${res.status})`);
|
||
}
|
||
return (await res.json()) as ScreenerRunResponse;
|
||
}
|
||
|
||
export async function startScreenerSync(req: ScreenerSyncRequest = {}): Promise<ScreenerSyncStatus> {
|
||
const res = await fetch(`${BASE}/api/screener/sync`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(req),
|
||
});
|
||
if (!res.ok) {
|
||
let detail = '';
|
||
try {
|
||
detail = (await res.json())?.detail ?? '';
|
||
} catch {
|
||
detail = await res.text();
|
||
}
|
||
throw new Error(detail || `启动同步失败 (HTTP ${res.status})`);
|
||
}
|
||
return (await res.json()) as ScreenerSyncStatus;
|
||
}
|
||
|
||
export async function getScreenerSyncStatus(): Promise<ScreenerSyncStatus> {
|
||
const res = await fetch(`${BASE}/api/screener/sync/status`);
|
||
if (!res.ok) throw new Error(`获取同步状态失败 (HTTP ${res.status})`);
|
||
return (await res.json()) as ScreenerSyncStatus;
|
||
}
|
||
|