Files
qlib/frontend/web/lib/jobs.ts
T
Simon f13b34c59e feat(web): 字段库页 + 界面单位换算 + 因子目录参数化 + 选股策略条件表单
字段库与单位:
- `/fields`:字段库管理页(中文名/说明可改、可停用;kind 是引擎事实不可改);
  单位只在字段自己的阶梯里选(总市值 = 万元/亿元),越界 422 原样展示。
- `lib/units.ts`:界面单位 ⇄ 基准单位换算集中一处,条件输入按界面单位回显、
  提交前换回基准单位(引擎只认基准单位,库里存的也永远是基准单位)。
- `SelectionStrategyForm` 取代 `StrategyParamsForm`:一个策略只定义「怎么选」
  (股票池 + 因子 + 过滤条件),条件字段来自字段库接口而不是前端硬编码枚举。

因子参数化(名字即身份,界面不许藏):
- `/factors` 新增「参数」列与展开行:精确引擎键、每个参数的允许范围、来源、依赖列
  (依赖列标明「引擎事实,不可改」);说明文案从「不可修改」改为
  「改参数 = 新建参数化因子 = 新身份,旧因子/既有策略不变义」。
- 「新建参数化因子」卡片:模板下拉 + 受控窗口(min/max)+ 方向枚举下拉,
  实时预览规范键/中文名/渲染公式;后端 422 的原文原样展示,不静默截断。
- 因子下拉显示中文名(含参数)与短键,**value 一律是完整引擎键**;
  停用的因子从各页候选里消失(/strategies、/selection、/signals、/factors/compose),
  既有策略/归档仍按名字解析;`/factors/compose` 顺手修了勾选框点击目标过小。
- `/fields` 加提示:字段库只列内置因子名,参数化因子在 /factors 管理,
  选股策略条件下拉里会一并出现。

tsc --noEmit 0 error;verify_ui_alignment 8 页 160 项全过(另跑 3 个选因子页 51/54)。
2026-10-01 16:38:29 +08:00

93 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 异步 Job 研究执行(Phase 4):POST /api/jobs 提交 → 轮询 GET /api/jobs/{id}。
*
* 大样本研究(全市场)可能耗时数十秒到分钟级,经异步 Job 后台执行,
* 避免 HTTP 长阻塞(AGENT §19)。页面提交后即时返回 job_id,再轮询到终态。
*/
import { apiGet, apiPost } from "./api";
import type { ResearchSpec } from "./types";
export interface JobSubmit {
job_id: string;
status: string;
}
export interface JobStatusResp {
job_id: string;
status: string;
/** 执行阶段(data_loading / backtesting / analysis…)—— 用于给用户真实进度而非假进度条 */
stage?: string | null;
/** 归档后的实验 id(后端在 Job 完成时写入) */
experiment_id?: string | null;
result?: unknown;
error?: string | null;
spec?: Record<string, unknown>;
started_at?: string | null;
finished_at?: string | null;
}
export function submitJob(spec: ResearchSpec): Promise<JobSubmit> {
return apiPost<JobSubmit>("/jobs", spec);
}
/** 任务终态(success / failed / cancelled / timeout)与结果 */
export interface JobOutcome<T> {
status: string;
result: T | null;
error?: string;
/** 归档实验 id:结果出来后前端可直接给出「去对比」入口 */
experimentId?: string | null;
}
/** 轮询直到 success / failed / cancelled,或超时(默认 10 分钟)。 */
export async function waitJob<T>(
jobId: string,
timeoutMs = 600_000,
onStage?: (info: { stage: string; elapsedMs: number }) => void,
): Promise<JobOutcome<T>> {
const deadline = Date.now() + timeoutMs;
const t0 = Date.now();
while (Date.now() < deadline) {
const job = await apiGet<JobStatusResp>(`/jobs/${jobId}`);
// 阶段来自后端真实执行状态,避免前端用假进度条假装在跑
if (onStage && job.stage) onStage({ stage: job.stage, elapsedMs: Date.now() - t0 });
if (job.status === "success") {
return {
status: job.status,
result: (job.result as T) ?? null,
experimentId: job.experiment_id ?? null,
};
}
if (job.status === "failed" || job.status === "cancelled") {
return { status: job.status, result: null, error: job.error ?? undefined };
}
await new Promise((resolve) => setTimeout(resolve, 1500));
}
return { status: "timeout", result: null, error: "等待结果超时,请稍后在「实验」页查看归档" };
}
/** 阶段中文名(后端 stage 枚举 → 用户可读) */
export const STAGE_LABEL: Record<string, string> = {
queued: "排队中",
data_loading: "加载行情与因子数据",
selection: "逐择股日选股",
backtesting: "撮合与净值结算",
analysis: "汇总指标与曲线",
done: "完成",
};
/** 提交一个回测组合为异步 Job(POST /api/combos/run,不保存组合)。 */
export async function submitComboJob(combo: unknown): Promise<JobSubmit> {
return apiPost<JobSubmit>("/combos/run", combo);
}
/**
* 运行**已保存**的回测组合(POST /api/combos/{id}/run)。
*
* 与 submitComboJob 的区别:这里用库里的那份参数,页面上未保存的改动不参与 ——
* 「从组合库直接运行」必须跑库里存的那套,否则用户改了一半的表单会污染既有组合的结果。
*/
export async function runSavedCombo(comboId: string): Promise<JobSubmit> {
return apiPost<JobSubmit>(`/combos/${encodeURIComponent(comboId)}/run`, {});
}