feat(web): 因子研究/因子组合/股票筛选接入统一作业反馈;阶段圆点按作业类型区分
接着 /backtest 的那次改造,把其余会跑异步 Job 的页面也切到同一套
`useJobRunner` + `JobProgress`(用户原话:包括因子测试等所有测试都帮我完善用户反馈):
- **/factors**(一个因子一个 Job 的批量场景):删掉 `running/processed/current/jobId` 与
按「已处理数 ÷ 总数」自算的 `Progress` **假百分比**;每轮把 `第 i/共 n 个` 交给反馈条,
阶段/作业号/已用秒数全部来自后端。取消 = 用户明确意图 → 保留已出的报告、不再跑后续因子,
不报错;单个因子失败仍继续跑其余因子,并把后端原文累积成**批级清单**(多因子时反馈条
只能显示最后一个作业,前几个失败不能丢)。
- **/factors/compose**:删掉 `value={30}` 的假进度条;`archiveId` / 复用 `BacktestResultView` /
「新页面放大」全部保留;failed/cancelled 交给反馈条,页面 error 只留参数与目录错误
(同一失败不在两处各说一遍)。
- **/selection**:异步选股切反馈条;**同步**的「执行选股」保留原 loading(`POST /selections`
没有 job_id,套上会去 `GET /jobs/{signal}` 撞 404)。
- **/signals**:`POST /api/signals` 是同步接口,**不套**作业反馈(不编作业号、不编阶段),
改为点击即现的 `role="status"` 提示,如实写明「同步请求、请求期间不能关页、无阶段无取消、
出错显示后端原文」。
- **阶段圆点按作业类型区分**(修掉一个真实缺陷):原来全站共用一张含 `queued/done` 的
`STAGE_ORDER`,因子测试页实测出现过**裸英文** `factor_calculation` 且 4 个圆点全灰
(`indexOf` = -1),还画出了因子测试根本不存在的「逐择股日选股 / 撮合与净值结算」。
现在 `STAGE_PIPELINES = { factor_test: [加载→计算因子值→汇总], backtest: [加载→撮合→汇总],
selection: [逐择股日选股] }`(阶段序列**不含 queued/done**,那是作业状态不是阶段),
未知阶段显示「执行中(stage)」并保留已推进的圆点,不整排灰、不露裸枚举。
验证(真实浏览器 CDP,读数原文已记录):
- /factors:点击后 0.2s 内 `submitting`→`queued` + 作业号;+30s Pill「计算因子值」、
圆点 `✓ 加载行情与因子数据 / ● 计算因子值 / ○ 汇总指标与曲线`(无「选股/撮合」、无英文枚举);
成功态给「去对比 / 打开归档」。
- /selection:圆点只有 `逐择股日选股` 一段;取消 → 「已取消,没有归档」;
另实测撞并发上限时如实显示后端原文「系统繁忙:并发研究任务已达上限」。
- /factors/compose:`submitting→queued→撮合与净值结算→success(EXP-…)`,业务结果与 5 处放大入口照旧。
- /backtest 回归:圆点由 4 个变 3 个(去掉后端**从不上报**的 selection 阶段),取消仍「已取消 + 没归档」。
- 自检产生的 12 个实验归档已全部删除(bulk-delete count:12,复查无残留)。
This commit is contained in:
@@ -8,7 +8,7 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet, apiPost } from "@/lib/api";
|
||||
import { SymbolLink } from "@/lib/symbols";
|
||||
import { waitJob } from "@/lib/jobs";
|
||||
import { useJobRunner } from "@/lib/jobs";
|
||||
import { factorOptionLabel, pickableFactors } from "@/lib/factors";
|
||||
import type {
|
||||
FactorMeta,
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Empty,
|
||||
SkeletonLines,
|
||||
} from "@/components/ui";
|
||||
import { JobProgress } from "@/components/JobProgress";
|
||||
|
||||
const FIELD_OPTIONS: { value: string; label: string; kind: "num" | "str" | "ref" }[] = [
|
||||
{ value: "static.industry", label: "行业 industry", kind: "str" },
|
||||
@@ -80,8 +81,22 @@ export default function SelectionPage() {
|
||||
* 拿它拼 /backtest?from_selection=<JOB id> 会让接收端 GET /api/selections/{id} 404 → 坏链接。
|
||||
*/
|
||||
const [savedSelectionId, setSavedSelectionId] = useState("");
|
||||
/**
|
||||
* 同步选股(POST /selections)正在执行 —— 这条路径是**同步接口**,没有 job_id,
|
||||
* 所以不能套 useJobRunner(它要轮询 /jobs/{id})。保留 running 只用于这条路径的按钮态。
|
||||
*/
|
||||
const [running, setRunning] = useState(false);
|
||||
const [asyncBusy, setAsyncBusy] = useState(false);
|
||||
/**
|
||||
* 异步选股(POST /selections/jobs)的作业反馈统一交给 `useJobRunner`:
|
||||
* 提交瞬间就有「正在提交作业…」,之后是真实阶段 + 每秒自增的已用时间 + 作业号 + 可取消。
|
||||
* 此前只有一个 asyncBusy 布尔值,用户点了「异步(全市场)」只看到按钮变灰,
|
||||
* 不知道到底提交成功没、作业号是多少(正是「点了没反应」的投诉点)。
|
||||
*/
|
||||
const job = useJobRunner<SelectionResult>("异步选股(全市场)");
|
||||
/** 异步作业是否在跑:既用于禁用两个按钮,也用于切换异步按钮的文案。 */
|
||||
const asyncActive =
|
||||
job.state.phase === "submitting" || job.state.phase === "queued" || job.state.phase === "running";
|
||||
/** 只留「同步选股失败 / 历史读取失败」这类**作业之外**的错误;异步作业的失败由反馈条显示后端原文 */
|
||||
const [error, setError] = useState("");
|
||||
const [history, setHistory] = useState<SelectionMeta[] | null>(null);
|
||||
|
||||
@@ -146,26 +161,24 @@ export default function SelectionPage() {
|
||||
|
||||
/** 异步(全市场等长任务):提交 Job 后台执行并轮询(解决同步 60s+)。 */
|
||||
async function runAsync() {
|
||||
setAsyncBusy(true);
|
||||
setError("");
|
||||
setResult(null);
|
||||
// 同上:异步任务不落库选股记录,直通回测必须隐藏按钮(不给坏链接)
|
||||
setSavedSelectionId("");
|
||||
try {
|
||||
const { job_id } = await apiPost<{ job_id: string }>("/selections/jobs", buildQuery());
|
||||
const out = await waitJob<SelectionResult>(job_id);
|
||||
if (out.status === "success" && out.result) {
|
||||
setSelectionId(job_id);
|
||||
setResult(out.result);
|
||||
apiGet<SelectionMeta[]>("/selections?limit=8").then(setHistory).catch(() => undefined);
|
||||
} else {
|
||||
setError(`任务${out.status}${out.error ? `:${out.error}` : ""}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
setAsyncBusy(false);
|
||||
// 提交闭包把真实 job_id 记下来:页头那个 Pill 沿用老行为显示本次运行的 id,
|
||||
// 而 job.state.jobId 在 await 之后读到的仍是本轮渲染的旧值(React 状态不会在同一 tick 内更新)
|
||||
let submittedId = "";
|
||||
const out = await job.run(async () => {
|
||||
const r = await apiPost<{ job_id: string }>("/selections/jobs", buildQuery());
|
||||
submittedId = r.job_id;
|
||||
return r;
|
||||
});
|
||||
if (out.status === "success" && out.result) {
|
||||
setSelectionId(submittedId);
|
||||
setResult(out.result);
|
||||
apiGet<SelectionMeta[]>("/selections?limit=8").then(setHistory).catch(() => undefined);
|
||||
}
|
||||
// 失败 / 超时 / 取消:反馈条已给出后端原文或「已取消(不产生归档)」,不在这里重复报错
|
||||
}
|
||||
|
||||
async function openHistory(id: string) {
|
||||
@@ -378,13 +391,21 @@ export default function SelectionPage() {
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<Btn variant="primary" icon="play" loading={running} disabled={running || asyncBusy} onClick={run}>
|
||||
<Btn variant="primary" icon="play" loading={running} disabled={running || asyncActive} onClick={run}>
|
||||
{running ? "筛选中…" : "执行选股"}
|
||||
</Btn>
|
||||
<Btn variant="ghost" icon="clock" loading={asyncBusy} disabled={running || asyncBusy} onClick={runAsync}>
|
||||
{asyncBusy ? "后台执行中…" : "异步(全市场)"}
|
||||
<Btn variant="ghost" icon="clock" loading={asyncActive} disabled={running || asyncActive} onClick={runAsync}>
|
||||
{asyncActive ? "后台执行中…" : "异步(全市场)"}
|
||||
</Btn>
|
||||
</div>
|
||||
{/* 异步作业的反馈条:点了立刻有字 + 真实阶段 + 可取消(同步按钮不走这里) */}
|
||||
<JobProgress
|
||||
state={job.state}
|
||||
onCancel={job.cancel}
|
||||
onDismiss={job.reset}
|
||||
anchorId="selection-job-progress"
|
||||
kind="selection"
|
||||
/>
|
||||
{error ? (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Banner tone="error">{error}</Banner>
|
||||
|
||||
Reference in New Issue
Block a user