feat: add Super Alpha research, management and MCP workflows
Deploy production / deploy (push) Successful in 56s

This commit is contained in:
yuxuanhui
2026-09-13 12:32:16 +08:00
parent 7c8188df9c
commit e256d6fef1
59 changed files with 3766 additions and 125 deletions
+79
View File
@@ -1,3 +1,4 @@
import { SuperAlphaResearchPage } from "./superalpha/SuperAlphaResearchPage";
import { FieldDirectory } from "./preparations/FieldDirectory";
import { DataPreparationPage } from "./preparations/DataPreparationPage";
import { SnapshotDialog } from "./preparations/SnapshotDialog";
@@ -66,6 +67,7 @@ export default function App() {
useEffect(() => {
if (
[
"superalpha-research",
"operators",
"templates",
"variants",
@@ -101,6 +103,9 @@ export default function App() {
const [alphaContext, setAlphaContext] = useState<PageContext>({
page: "alphas",
});
const [superAlphaContext, setSuperAlphaContext] = useState<PageContext>({
page: "superalphas",
});
const [backtestContext, setBacktestContext] = useState<PageContext>({
page: "backtests",
});
@@ -252,7 +257,23 @@ export default function App() {
location.hash = next;
setPage(next);
};
const navigationRequest = useRef(0);
const handleAction = (action: UIAction) => {
const request = ++navigationRequest.current;
if (action.type === "open_alpha" && !action.alpha_type) {
void api<{ alpha_type: string }>(
`/alphas/${encodeURIComponent(action.alpha_id)}`,
)
.then((alpha) => {
if (navigationRequest.current === request)
handleAction({
...action,
alpha_type: alpha.alpha_type || "REGULAR",
});
})
.catch((error: Error) => Toast.error(error.message));
return;
}
const destination = actionDestination(action);
if (!destination) return;
if (destination.chat === "open") setChatOpen(true);
@@ -261,6 +282,21 @@ export default function App() {
if (destination.page) changePage(destination.page);
setAIAction(action);
};
useEffect(() => {
if (!authenticated) return;
const locate = () => {
const target = pageFromHash(location.hash);
if (!["alphas", "superalphas"].includes(target)) return;
const id = new URLSearchParams(location.hash.split("?")[1]).get(
"alpha_id",
);
if (id)
handleAction({ type: "open_alpha", alpha_id: id, nonce: Date.now() });
};
locate();
window.addEventListener("hashchange", locate);
return () => window.removeEventListener("hashchange", locate);
}, [authenticated]);
const logout = async () => {
try {
await post("/auth/logout");
@@ -439,6 +475,44 @@ export default function App() {
onOverlay={focusBusiness}
/>
</div>
<div className="alpha-page-view" hidden={page !== "superalphas"}>
<AlphaPage
managementScope="super"
onAction={handleAction}
taskPanelOpen={showJobs}
account={account}
version={`${refreshKey}:${resourceVersions.alphas}:${completedVersion}`}
onTask={taskCreated}
onAccount={() => changePage("account")}
active={page === "superalphas"}
overlaySuspended={
page !== "superalphas" || (viewport < 1440 && chatOpen)
}
chatOffset={chatOffset}
onContext={setSuperAlphaContext}
action={
aiAction?.type === "open_alpha" ||
aiAction?.type === "apply_filters"
? aiAction
: null
}
onOverlay={focusBusiness}
/>
</div>
{visitedResearch.includes("superalpha-research") && (
<div
className="alpha-page-view"
hidden={page !== "superalpha-research"}
>
<SuperAlphaResearchPage
active={page === "superalpha-research"}
action={aiAction}
onAction={handleAction}
onContext={setResearchContext}
timezone={account?.timezone}
/>
</div>
)}
<div className="backtest-page-view" hidden={page !== "backtests"}>
{visitedBacktests && (
<BacktestPage
@@ -572,6 +646,11 @@ export default function App() {
? researchContext
: { page: "variants" as const },
alphas: alphaContext,
superalphas: superAlphaContext,
"superalpha-research":
researchContext.page === "superalpha-research"
? researchContext
: { page: "superalpha-research" as const },
datasets: datasetContext,
fields: { page: "fields" as const },
preparations: { page: "preparations" as const },
+12 -2
View File
@@ -15,6 +15,8 @@ export type PageContext = {
page:
| "home"
| "alphas"
| "superalphas"
| "superalpha-research"
| "account"
| "datasets"
| "fields"
@@ -48,9 +50,17 @@ export type PageContext = {
filters?: Record<string, unknown>;
};
export type AlphaUIAction =
| { type: "open_alpha"; alpha_id: string; nonce: number }
| { type: "open_alpha"; alpha_id: string; alpha_type?: string; nonce: number }
| { type: "apply_filters"; filters: Record<string, unknown>; nonce: number };
export type UIAction =
| {
type: "open_superalpha_research";
plan_id?: string;
version?: number;
experiment_id?: string;
alpha_id?: string;
nonce: number;
}
| { type: "open_feature"; asset_id: string; version?: number; nonce: number }
| { type: "open_template"; asset_id: string; version?: number; nonce: number }
| { type: "open_experiment"; experiment_id: string; nonce: number }
@@ -59,7 +69,7 @@ export type UIAction =
| { type: "open_research_input"; input_id: string; nonce: number }
| { type: "open_backtest"; run_id: string; nonce: number }
| { type: "open_backtest_preview"; preview_id: string; nonce: number }
| { type: "open_alpha"; alpha_id: string; nonce: number }
| { type: "open_alpha"; alpha_id: string; alpha_type?: string; nonce: number }
| { type: "apply_filters"; filters: Record<string, unknown>; nonce: number };
export type ToolCard = {
id: string;
+13
View File
@@ -68,6 +68,10 @@ const contextLabels: Record<
home: () => "上下文:首页看板",
alphas: (context) =>
`上下文:${context.alpha_id ? `Alpha ${context.alpha_id}` : "Alpha 列表"}${context.selected_ids?.length ? ` · 已选 ${context.selected_ids.length} 条` : ""}`,
superalphas: (c) =>
`上下文:Super Alpha 管理${c.alpha_id ? ` · ${c.alpha_id}` : ""}`,
"superalpha-research": (c) =>
`上下文:Super Alpha 研究${c.research_asset_id ? ` · 方案 ${c.research_asset_id}` : ""}${c.research_experiment_id ? ` · 候选 ${c.research_experiment_id}` : ""}`,
operators: () => "上下文:算子库",
templates: (context) =>
`上下文:模板工坊${context.research_asset_id ? ` · ${context.research_asset_id}` : ""}${context.research_experiment_id ? ` · 实验 ${context.research_experiment_id}` : ""}`,
@@ -100,6 +104,7 @@ type Destination = {
};
// Exhaustive action destinations prevent a new action silently falling into Alpha.
const destinations: Record<UIAction["type"], Destination> = {
open_superalpha_research: { page: "superalpha-research", chat: "responsive" },
open_feature: { page: "features", chat: "responsive" },
open_template: { page: "templates", chat: "responsive" },
open_experiment: { page: "templates", chat: "responsive" },
@@ -113,6 +118,14 @@ const destinations: Record<UIAction["type"], Destination> = {
};
export function actionDestination(action: UIAction): Destination | undefined {
if (action.type === "open_alpha" && action.alpha_type === "SUPER")
return { page: "superalphas", chat: "responsive" };
if (
action.type === "apply_filters" &&
(action.filters.management_scope === "super" ||
action.filters.alpha_type === "SUPER")
)
return { page: "superalphas", chat: "responsive" };
return Object.hasOwn(destinations, action.type)
? destinations[action.type]
: undefined;
+1
View File
@@ -85,6 +85,7 @@ export const jobLabels: Record<string, string> = {
catalog_full_sync: "全量同步数据目录与字段",
catalog_sync: "同步数据集目录",
field_sync: "同步数据字段",
super_selection_preview: "Super Alpha 组件预览",
full_sync: "全量同步 Alpha",
daily_sync: "按天同步 Alpha",
self_correlation: "本地自相关检测",
+67 -6
View File
@@ -1,3 +1,4 @@
import { ComponentsPanel } from "../superalpha/ComponentsPanel";
import {
CatalogTableToolbar,
CatalogIconAction,
@@ -90,6 +91,7 @@ export function BacktestPage({
}) {
const [view, setView] = useState("runs");
const [sourceFilter, setSourceFilter] = useState("");
const [alphaType, setAlphaType] = useState("");
const [q, setQ] = useState("");
const [filterDraft, setFilterDraft] = useState({ q: "", source: "" });
const [pageSize, setPageSize] = useState(25);
@@ -180,7 +182,9 @@ export function BacktestPage({
offset: (page - 1) * pageSize,
sort: sort.value,
direction: sort.direction,
...(view === "runs" ? { source: sourceFilter } : {}),
...(view === "runs"
? { source: sourceFilter, alpha_type: alphaType }
: {}),
});
const [data, c, kinds] = await Promise.all([
api<Page<Run> | Page<DraftSummary>>(`/backtests/${view}?${params}`),
@@ -199,7 +203,16 @@ export function BacktestPage({
} finally {
if (sequence === requestSequence.current) setLoading(false);
}
}, [page, pageSize, sourceFilter, q, view, sort.value, sort.direction]);
}, [
page,
pageSize,
sourceFilter,
alphaType,
q,
view,
sort.value,
sort.direction,
]);
useEffect(() => {
if (!active) return;
let alive = true;
@@ -472,6 +485,24 @@ export function BacktestPage({
const drawerVisible = active && !suspended;
return (
<section className="backtest-page">
{view === "runs" && (
<div className="backtest-toolbar">
<span>候选类型</span>
<Select
aria-label="回测候选类型"
value={alphaType}
optionList={[
{ value: "", label: "全部类型" },
{ value: "REGULAR", label: "REGULAR" },
{ value: "SUPER", label: "SUPER" },
]}
onChange={(v) => {
setAlphaType(String(v));
setPage(1);
}}
/>
</div>
)}
<CatalogTableToolbar
label="回测研究"
active={active && !suspended && !editor && !runId && !settingsDraft}
@@ -723,8 +754,21 @@ export function BacktestPage({
{
title: "表达式",
dataIndex: "expression",
width: 260,
width: 320,
ellipsis: true,
render: (_, c) => (
<span
title={
c!.alpha_type === "SUPER"
? `Selection: ${c!.selection}\nCombo: ${c!.combo}`
: c!.expression
}
>
{c!.alpha_type === "SUPER"
? `SUPER · Selection: ${c!.selection} · Combo: ${c!.combo}`
: c!.expression}
</span>
),
},
{
title: "最终参数",
@@ -1002,12 +1046,14 @@ export function BacktestPage({
dataIndex: "expression",
width: 260,
ellipsis: true,
render: (v, i) => (
render: (_, i) => (
<Button
theme="borderless"
onClick={() => setItemDetail(i!)}
>
{String(v)}
{i!.alpha_type === "SUPER"
? `SUPER · ${i!.selection} · ${i!.combo}`
: i!.expression}
</Button>
),
},
@@ -1055,6 +1101,7 @@ export function BacktestPage({
onAction({
type: "open_alpha",
alpha_id: i!.alpha_id!,
alpha_type: i!.alpha_type,
nonce: Date.now(),
})
}
@@ -1081,7 +1128,21 @@ export function BacktestPage({
收起候选详情
</Button>
</div>
<p>{itemDetail.expression}</p>
{itemDetail.alpha_type === "SUPER" ? (
<>
<h4>Selection</h4>
<pre>{itemDetail.selection}</pre>
<h4>Combo</h4>
<pre>{itemDetail.combo}</pre>
<ComponentsPanel
key={itemDetail.id}
url={`/backtests/items/${itemDetail.id}/artifact?kind=components`}
timezone={timezone}
/>
</>
) : (
<p>{itemDetail.expression}</p>
)}
{itemDetail.error && (
<Banner type="warning" description={itemDetail.error} />
)}
+13 -1
View File
@@ -12,6 +12,9 @@ export type SimulationSettings = {
language: "FASTEXPR";
visualization: boolean;
maxTrade: "ON" | "OFF";
selectionHandling?: "POSITIVE" | "NON_ZERO" | "NON_NAN";
selectionLimit?: number;
componentActivation?: "IS" | "OS";
maxPosition?: "ON" | "OFF";
};
export const initialSettings: SimulationSettings = {
@@ -34,9 +37,15 @@ export type Candidate = {
client_item_id: string;
expression: string;
settings: SimulationSettings;
alpha_type?: "REGULAR";
alpha_type?: "REGULAR" | "SUPER";
selection?: string;
combo?: string;
};
export type Source = {
research_kind?: string | null;
superalpha_plan_id?: string | null;
superalpha_plan_version?: number | null;
selection_snapshot_ids?: string[];
kind: string;
reference?: string | null;
batch_id?: string | null;
@@ -102,6 +111,9 @@ export type Run = {
scheduler: Scheduler;
};
export type Item = {
alpha_type: "REGULAR" | "SUPER";
selection?: string;
combo?: string;
id: string;
client_item_id: string;
expression: string;
+54
View File
@@ -1,3 +1,4 @@
import { ComponentsPanel } from "../superalpha/ComponentsPanel";
import { useEffect, useRef, useState } from "react";
import { formatAlphaMetric } from "../alphaMetrics";
import {
@@ -65,6 +66,7 @@ export function AlphaDetail({
chatOffset?: number;
}) {
const [detail, setDetail] = useState<Detail | null>(null);
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
const [pnl, setPnl] = useState<Pnl | null>(null);
const [research, setResearch] = useState<Research | null>(null);
const [tagText, setTagText] = useState("");
@@ -77,6 +79,7 @@ export function AlphaDetail({
dirty.current = false;
setConflict(false);
setDetail(null);
setDescriptions({});
setResearch(null);
setPnl(null);
setError("");
@@ -122,6 +125,21 @@ export function AlphaDetail({
active = false;
};
}, [id, tab, version]);
useEffect(() => {
if (!id || detail?.alpha_type !== "SUPER") return;
const c = new AbortController();
void api<{ descriptions: Record<string, string> }>(
`/superalpha/alphas/${id}`,
{ signal: c.signal },
)
.then((value) => {
if (!c.signal.aborted) setDescriptions(value.descriptions);
})
.catch((e: Error) => {
if (!c.signal.aborted) setError(e.message);
});
return () => c.abort();
}, [id, detail?.alpha_type, version]);
async function fetchPnl() {
try {
await post("/sync-jobs", { kind: "pnl_refresh", alpha_ids: [id] });
@@ -222,6 +240,19 @@ export function AlphaDetail({
<Button>在 BRAIN 中打开 ↗</Button>
</a>
</div>
{detail.alpha_type === "SUPER" && (
<Button
onClick={() =>
onAction({
type: "open_superalpha_research",
alpha_id: detail.id,
nonce: Date.now(),
})
}
>
复制为研究方案
</Button>
)}
<Tabs
className="alpha-detail-tabs"
activeKey={tab}
@@ -256,6 +287,18 @@ export function AlphaDetail({
{detail.combo !== null && (
<CodeBlock title="Combo" value={detail.combo} />
)}
{detail.alpha_type === "SUPER" && (
<>
<CodeBlock
title="Selection Description"
value={descriptions.selection || "未提供"}
/>
<CodeBlock
title="Combo Description"
value={descriptions.combo || "未提供"}
/>
</>
)}
<Divider />
<h3>回测设置</h3>
<DetailFieldGrid
@@ -266,6 +309,17 @@ export function AlphaDetail({
/>
</div>
</TabPane>
{detail.alpha_type === "SUPER" && (
<TabPane tab="组件证据" itemKey="components">
{tab === "components" && (
<ComponentsPanel
key={detail.id}
url={`/superalpha/alphas/${detail.id}/components`}
timezone={timezone}
/>
)}
</TabPane>
)}
<TabPane tab="指标" itemKey="metrics">
<MetricTable title="IS 指标" values={detail.is_metrics} />
<Divider />
+13 -1
View File
@@ -34,10 +34,22 @@ const navigation = [
},
{ id: "features", label: "特征工程", icon: IconBeaker, group: "研究实验" },
{ id: "variants", label: "Alpha 变体", icon: IconBeaker, group: "研究实验" },
{
id: "superalpha-research",
label: "Super Alpha 研究",
icon: IconBeaker,
group: "研究实验",
},
{ id: "backtests", label: "回测研究", icon: IconBeaker, group: "研究实验" },
{ id: "pipeline", label: "研究流水线", icon: IconBeaker, group: "研究编排" },
{ id: "quantflow", label: "QuantFlow", icon: IconBeaker, group: "研究编排" },
{ id: "alphas", label: "Alpha 管理", icon: IconGridView, group: "研究成果" },
{
id: "superalphas",
label: "Super Alpha 管理",
icon: IconGridView,
group: "研究成果",
},
{ id: "mcp-keys", label: "MCP Key", icon: IconCommand, group: "系统管理" },
{ id: "account", label: "个人信息", icon: IconUser, group: "" },
] as const;
@@ -285,7 +297,7 @@ export function AppSidebar({
>
<div className="command-items">
{navigation.map(({ id, label, icon: Icon }) => (
<button key={id} onClick={() => navigate(id)}>
<button key={id} aria-label={label} onClick={() => navigate(id)}>
<Icon />
{label}
</button>
+84 -21
View File
@@ -1,3 +1,4 @@
import { WorkspaceTable } from "../components/WorkspaceTable";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Banner,
@@ -10,7 +11,6 @@ import {
Popover,
Pagination,
Select,
Table,
Tag,
TextArea,
Toast,
@@ -118,6 +118,9 @@ const initialColumns = [
"source",
];
const columnLabels: Record<string, string> = {
selection: "Selection",
combo: "Combo",
component_count: "组件数(已核实)",
source: "研究来源",
name: "Alpha",
expression: "表达式",
@@ -147,6 +150,7 @@ const columnLabels: Record<string, string> = {
};
export function AlphaPage({
managementScope = "non_super",
account,
taskPanelOpen,
version,
@@ -160,6 +164,7 @@ export function AlphaPage({
onOverlay,
onAction,
}: {
managementScope?: "super" | "non_super";
account: Account | null;
taskPanelOpen: boolean;
version: string;
@@ -173,6 +178,21 @@ export function AlphaPage({
onOverlay: () => void;
onAction: (action: WorkspaceAction) => void;
}) {
const columnKey =
managementScope === "super" ? "superalpha-columns" : "alpha-columns";
const defaultColumns =
managementScope === "super"
? initialColumns.flatMap((c) =>
c === "expression" ? ["selection", "combo", "component_count"] : [c],
)
: initialColumns;
const menuColumns = Object.fromEntries(
Object.entries(columnLabels).filter(([k]) =>
managementScope === "super"
? k !== "expression"
: !["selection", "combo", "component_count"].includes(k),
),
);
const [data, setData] = useState<Page>({
items: [],
total: 0,
@@ -197,7 +217,7 @@ export function AlphaPage({
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
try {
const stored: unknown = JSON.parse(
localStorage.getItem("alpha-columns") || "null",
localStorage.getItem(columnKey) || "null",
);
return Array.isArray(stored)
? [
@@ -208,9 +228,9 @@ export function AlphaPage({
),
]),
]
: initialColumns;
: defaultColumns;
} catch {
return initialColumns;
return defaultColumns;
}
});
const [detailId, setDetailId] = useState<string | null>(null);
@@ -223,6 +243,7 @@ export function AlphaPage({
version: number;
text: string;
} | null>(null);
const selectionVersions = useRef<Record<string, number>>({});
const [bulkVersions, setBulkVersions] = useState<Record<string, number>>({});
const actionNonce = useRef(0);
const [bulk, setBulk] = useState({
@@ -250,6 +271,7 @@ export function AlphaPage({
const params = useMemo(
() => ({
...filters,
management_scope: managementScope,
submission,
submission_blocked: submissionBlocked || undefined,
sort,
@@ -257,15 +279,24 @@ export function AlphaPage({
limit: pageSize,
offset: (page - 1) * pageSize,
}),
[filters, submission, submissionBlocked, sort, direction, pageSize, page],
[
filters,
submission,
submissionBlocked,
sort,
direction,
pageSize,
page,
managementScope,
],
);
const query = queryString(params);
useEffect(() => {
if (!active) return;
onContext({
page: "alphas",
page: managementScope === "super" ? "superalphas" : "alphas",
alpha_id: detailId,
selected_ids: selected,
selected_ids: selected.slice(0, 100),
filters: Object.fromEntries(
Object.entries(params).filter(
([, value]) => value !== "" && value !== undefined && value !== null,
@@ -274,7 +305,7 @@ export function AlphaPage({
});
}, [active, detailId, selected, params, onContext]);
useEffect(() => {
if (!action || actionNonce.current === action.nonce) return;
if (!active || !action || actionNonce.current === action.nonce) return;
actionNonce.current = action.nonce;
if (action.type === "open_alpha") {
setDetailId(action.alpha_id);
@@ -306,7 +337,7 @@ export function AlphaPage({
if (nextSort) setSort(String(nextSort));
if (nextDirection) setDirection(String(nextDirection));
}
}, [action, onOverlay]);
}, [action, onOverlay, active]);
const refresh = useCallback(() => setLocalVersion((n) => n + 1), []);
useEffect(() => {
if (!active) return;
@@ -315,7 +346,9 @@ export function AlphaPage({
setError("");
Promise.all([
api<Page>(`/alphas?${query}`, { signal: controller.signal }),
api<Facets>("/alphas/facets", { signal: controller.signal }),
api<Facets>(`/alphas/facets?management_scope=${managementScope}`, {
signal: controller.signal,
}),
])
.then(([results, nextFacets]) => {
if (!controller.signal.aborted) {
@@ -333,6 +366,9 @@ export function AlphaPage({
}, [active, query, version, localVersion]);
useEffect(() => {
setSelected([]);
selectionVersions.current = {};
}, [filters, submission, submissionBlocked, managementScope]);
useEffect(() => {
tablePanel.current?.querySelector(".semi-table-body")?.scrollTo({ top: 0 });
}, [query]);
function updateDraft(key: string, value: unknown) {
@@ -464,6 +500,24 @@ export function AlphaPage({
</code>
),
},
...(["selection", "combo"] as const).map(
(key): ColumnProps<Alpha> => ({
key,
title: columnLabels[key],
width: 260,
render: (_, row) => (
<code title={row![`${key}_preview`]}>
{row![`${key}_preview`] || "未提供"}
</code>
),
}),
),
{
key: "component_count",
title: "组件数(已核实)",
width: 140,
render: (_, row) => row!.component_count ?? "未核实",
},
{
key: "source",
title: "研究来源",
@@ -953,6 +1007,7 @@ export function AlphaPage({
{error && <Banner type="danger" description={error} />}
<section className="library-panel" ref={tablePanel}>
<SavedViews
managementScope={managementScope}
submission={submission}
submissionBlocked={submissionBlocked}
selectedId={selectedViewId}
@@ -1005,7 +1060,7 @@ export function AlphaPage({
]),
];
setVisibleColumns(next);
localStorage.setItem("alpha-columns", JSON.stringify(next));
localStorage.setItem(columnKey, JSON.stringify(next));
}}
/>
<div
@@ -1058,7 +1113,7 @@ export function AlphaPage({
<CheckboxGroup
direction="vertical"
value={displayedColumns}
options={Object.entries(columnLabels).map(
options={Object.entries(menuColumns).map(
([value, label]) => ({
value,
label,
@@ -1071,10 +1126,7 @@ export function AlphaPage({
onChange={(values) => {
const next = values.map(String);
setVisibleColumns(next);
localStorage.setItem(
"alpha-columns",
JSON.stringify(next),
);
localStorage.setItem(columnKey, JSON.stringify(next));
}}
/>
</div>
@@ -1221,9 +1273,7 @@ export function AlphaPage({
onClick={() => {
setBulkVersions(
Object.fromEntries(
data.items
.filter((row) => selected.includes(row.id))
.map((row) => [row.id, row.research.version]),
selected.map((id) => [id, selectionVersions.current[id]]),
),
);
setBulkOpen(true);
@@ -1252,7 +1302,8 @@ export function AlphaPage({
</Button>
</div>
)}
<Table<Alpha>
<WorkspaceTable<Alpha>
fill
columns={columns}
dataSource={data.items}
rowKey="id"
@@ -1265,7 +1316,19 @@ export function AlphaPage({
}}
rowSelection={{
selectedRowKeys: selected,
onChange: (keys) => setSelected((keys ?? []).map(String)),
onChange: (keys) => {
const pageIds = new Set(data.items.map((r) => r.id));
const nextIds = (keys ?? [])
.map(String)
.filter((id) => pageIds.has(id));
for (const row of data.items)
if (nextIds.includes(row.id))
selectionVersions.current[row.id] = row.research.version;
setSelected((old) => [
...old.filter((id) => !pageIds.has(id)),
...nextIds,
]);
},
fixed: !compact,
}}
pagination={false}
+3 -3
View File
@@ -25,12 +25,12 @@ const scopes = [
[
"research:refresh",
"刷新研究数据",
"更新缓存、检查自相关及恢复 WorldQuant 认证",
"更新缓存、预览 SUPER 组件、检查自相关及恢复 WorldQuant 认证",
],
[
"research:write",
"保存研究模板",
"保存大模型总结的模板及来源,供后续批量回测",
"保存研究模板与方案",
"保存模板、Super Alpha 方案及构造候选,不执行回测",
],
["backtests:execute", "执行回测", "提交新的回测批次"],
["backtests:control", "控制回测", "暂停、继续、停止与恢复采集"],
+13 -2
View File
@@ -44,6 +44,7 @@ function filterSignature(filters: Record<string, unknown>) {
}
export function SavedViews({
managementScope = "non_super",
filters,
columns,
submission,
@@ -55,6 +56,7 @@ export function SavedViews({
suspended,
onOverlay,
}: {
managementScope?: "super" | "non_super";
filters: Record<string, unknown>;
columns: string[];
submission: Submission;
@@ -90,13 +92,22 @@ export function SavedViews({
views.push(...result.items);
if (views.length >= result.total || result.items.length === 0) break;
}
if (!controller.signal.aborted) setItems(views);
if (!controller.signal.aborted)
setItems(
views.filter(
(v) =>
(v.content.filters.management_scope ??
(v.content.filters.alpha_type === "SUPER"
? "super"
: "non_super")) === managementScope,
),
);
}
void load().catch((error: Error) => {
if (!controller.signal.aborted) Toast.error(error.message);
});
return () => controller.abort();
}, [revision]);
}, [revision, managementScope]);
function openEditor(mode: Editor["mode"], view?: View) {
const content = view?.content ?? { name: "", filters, columns };
+19
View File
@@ -5,6 +5,7 @@ import "./style.css";
export const sourceLabel = (kind: string) =>
({
superalpha: "Super Alpha 研究",
chatbox: "Chatbox 研究",
manual: "手工研究",
mcp: "MCP 研究",
@@ -33,6 +34,24 @@ export function SourceDetails({
<p>来源引用:{source.reference}</p>
)}
<div className="inline-actions">
{(source.superalpha_plan_id ||
((source.kind === "superalpha" ||
source.research_kind === "superalpha") &&
source.research_id)) && (
<Button
onClick={() =>
onAction({
type: "open_superalpha_research",
plan_id: source.superalpha_plan_id ?? undefined,
version: source.superalpha_plan_version ?? undefined,
experiment_id: source.research_id ?? undefined,
nonce: Date.now(),
})
}
>
查看 Super Alpha 研究
</Button>
)}
{source.research_id &&
["template", "variant", "feature", "pipeline", "quantflow"].includes(
source.kind,
@@ -17,6 +17,7 @@ import {
/** One typed settings value for all producers; fixed inputs constrain the scope, never the numeric parameters. */
export function SimulationSettingsEditor({
alphaType = "REGULAR",
value,
onChange,
rows,
@@ -28,6 +29,7 @@ export function SimulationSettingsEditor({
disabled = false,
onValidityChange,
}: {
alphaType?: "REGULAR" | "SUPER";
value: SimulationSettings;
onChange: (value: SimulationSettings) => void;
rows: SettingsRow[];
@@ -98,8 +100,10 @@ export function SimulationSettingsEditor({
disabled={disabled || loading}
min={options.minimum}
max={options.maximum}
precision={key === "decay" ? 0 : undefined}
step={key === "decay" ? 1 : 0.01}
precision={
["decay", "selectionLimit"].includes(key) ? 0 : undefined
}
step={["decay", "selectionLimit"].includes(key) ? 1 : 0.01}
validateStatus={message ? "error" : "default"}
onChange={(next) =>
onChange({
@@ -161,6 +165,23 @@ export function SimulationSettingsEditor({
<div className="settings-grid">
{["neutralization", "decay", "truncation"].map(field)}
</div>
{alphaType === "SUPER" && (
<>
<div className="settings-grid">
{["selectionHandling", "selectionLimit", "componentActivation"].map(
field,
)}
</div>
{["selectionHandling", "selectionLimit", "componentActivation"].some(
(k) => !row?.fields?.[k],
) && (
<small className="settings-hint">
平台元数据未提供全部 SUPER
设置范围;当前显示本地契约,账户适用性未核实。
</small>
)}
</>
)}
<details>
<summary>更多参数</summary>
<div className="settings-grid">
+20 -3
View File
@@ -86,6 +86,9 @@ export function asScopeOptions(rows: SettingsRow[]): ScopeOption[] {
return rows.map((row) => ({ ...row, universes: [row.universe] }));
}
export const parameterLabels: Record<string, string> = {
selectionHandling: "Selection Handling",
selectionLimit: "Selection Limit",
componentActivation: "Component Activation",
neutralization: "Neutralization",
decay: "Decay",
truncation: "Truncation",
@@ -99,6 +102,9 @@ export const parameterLabels: Record<string, string> = {
};
// These are application contract limits, not a substitute for account-specific market choices.
const contractFields: Record<string, FieldOption> = {
selectionHandling: { choices: ["POSITIVE", "NON_ZERO", "NON_NAN"] },
componentActivation: { choices: ["IS", "OS"] },
selectionLimit: { minimum: 1, maximum: 100000 },
decay: { minimum: 0, maximum: 10000 },
truncation: { minimum: 0, maximum: 1 },
pasteurization: { choices: ["ON", "OFF"] },
@@ -143,7 +149,10 @@ export function fieldOptions(
}
: {}),
};
if (result.choices && (key === "decay" || key === "truncation")) {
if (
result.choices &&
["decay", "truncation", "selectionLimit"].includes(key)
) {
result.choices = result.choices.filter(
(v) =>
typeof v === "number" &&
@@ -171,6 +180,13 @@ export function settingsErrors(
)
errors.push("所选数据准备必须具有相同组合,并与回测组合一致");
for (const key of Object.keys(parameterLabels)) {
if (
["selectionHandling", "selectionLimit", "componentActivation"].includes(
key,
) &&
value.selectionHandling === undefined
)
continue;
const current =
value[key as keyof SimulationSettings] ??
(key === "maxPosition" ? "OFF" : undefined);
@@ -183,12 +199,13 @@ export function settingsErrors(
`${parameterLabels[key]} 当前值 ${String(current ?? "未提供")} 不可用,请重新选择`,
);
if (
(key === "decay" || key === "truncation") &&
["decay", "truncation", "selectionLimit"].includes(key) &&
(typeof current !== "number" ||
!Number.isFinite(current) ||
current < options.minimum! ||
current > options.maximum! ||
(key === "decay" && !Number.isInteger(current)))
(["decay", "selectionLimit"].includes(key) &&
!Number.isInteger(current)))
)
errors.push(
`${parameterLabels[key]} 必须为 ${options.minimum}–${options.maximum} 的${key === "decay" ? "整数" : "数值"}`,
@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
import { Banner, Pagination, Spin } from "@douyinfe/semi-ui-19";
import { api, displayValue, formatTime } from "../api";
import { WorkspaceTable } from "../components/WorkspaceTable";
import type { Components } from "./types";
/** Component evidence is always read from its own immutable observation. */
export function ComponentsPanel({
url,
timezone,
}: {
url: string;
timezone?: string;
}) {
const [page, setPage] = useState(1);
const [data, setData] = useState<Components>();
const [error, setError] = useState("");
useEffect(() => {
setPage(1);
setData(undefined);
}, [url]);
useEffect(() => {
const c = new AbortController();
setError("");
void api<Components>(
`${url}${url.includes("?") ? "&" : "?"}limit=25&offset=${(page - 1) * 25}`,
{ signal: c.signal },
)
.then((value) => {
if (!c.signal.aborted) setData(value);
})
.catch((e: Error) => {
if (!c.signal.aborted) setError(e.message);
});
return () => c.abort();
}, [url, page]);
if (error) return <Banner type="danger" description={error} />;
if (!data) return <Spin />;
return (
<section className="super-components">
<p>
{data.source === "preview" ? "Selection 预览组件" : "回测实际组件"} ·{" "}
{data.complete
? `已核实 ${data.reported_total ?? data.total} 个`
: "完整性未核实"}{" "}
· 观察时间 {formatTime(data.observed_at, timezone)}
</p>
{data.complete && data.total === 0 && (
<Banner
type="warning"
description="组件池为空,回测可能无法产生结果。请调整 Selection。"
/>
)}
{(data.warnings ?? []).map((w, i) => (
<Banner key={i} type="warning" description={displayValue(w)} />
))}
<p className="muted">组件指纹:{data.component_hash ?? "未核实"}</p>
<WorkspaceTable
rowKey="id"
dataSource={data.items}
scroll={{ x: 1100, y: 320 }}
columns={[
{ title: "组件 ID", dataIndex: "id", width: 190 },
{
title: "平台选择值",
width: 180,
render: (_, r) =>
displayValue(r?.value ?? r?.selectionValue ?? r?.selection),
},
{
title: "可用指标与平台返回字段",
width: 730,
render: (_, r) => JSON.stringify(r),
},
]}
empty="未提供组件"
/>
<footer className="workspace-table-footer super-toolbar">
<span>已保存 {data.total} 条</span>
<Pagination
total={data.total}
currentPage={page}
pageSize={25}
onPageChange={setPage}
/>
</footer>
</section>
);
}
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
.super-research {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--semi-color-bg-0);
}
.super-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
margin: 0;
flex-shrink: 0;
min-height: 40px;
}
.super-toolbar > .semi-input-wrapper {
max-width: 320px;
}
.super-toolbar .super-spacer {
flex: 1;
}
.super-editor {
overflow: auto;
padding: 16px;
min-height: 0;
}
.super-editor label {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 14px;
}
.super-editor-grid {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 1fr);
gap: 24px;
}
.super-editor .semi-input-textarea {
font-family: ui-monospace, monospace;
}
.super-pane {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.super-pane > .semi-banner {
flex-shrink: 0;
}
.super-scroll {
min-height: 0;
overflow: auto;
padding: 12px;
}
.super-research pre,
.super-evidence pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.super-components {
min-width: 0;
}
.super-evidence {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.super-evidence > div {
min-width: 0;
}
@media (max-width: 900px) {
.super-editor-grid,
.super-evidence {
grid-template-columns: 1fr;
}
.super-toolbar {
flex-wrap: wrap;
}
}
+109
View File
@@ -0,0 +1,109 @@
import {
initialSettings,
type Candidate,
type SimulationSettings,
type Source,
} from "../backtests/types";
export type PlanSpec = {
name: string;
hypothesis: string;
reference: string;
selection: string;
combo: string;
settings: SimulationSettings;
variables: Record<string, { kind: string; values: (string | number)[] }>;
setting_variants: Record<string, (string | number | boolean)[]>;
include_baseline: boolean;
parent_plan_id?: string | null;
parent_plan_version?: number | null;
parent_alpha_id?: string | null;
parent_experiment_id?: string | null;
};
export const newPlan = (): PlanSpec => ({
name: "",
hypothesis: "",
reference: "",
selection: "",
combo: "1",
variables: {},
setting_variants: {},
include_baseline: true,
settings: {
...initialSettings,
selectionHandling: "POSITIVE",
selectionLimit: 100,
componentActivation: "IS",
},
});
export type Plan = {
id: string;
version: number;
latest_version: number;
name: string;
archived: boolean;
content: PlanSpec;
created_at: string;
};
export type Components = {
snapshot_id?: string;
job_id?: string;
item_id?: string;
source: string;
status: string;
complete: boolean;
reported_total?: number | null;
total: number;
observed_at: string | null;
component_hash?: string | null;
request_hash?: string;
warnings?: unknown[];
error?: string;
request?: { selection: string; settings: SimulationSettings };
items: { id: string; [key: string]: unknown }[];
};
export type Experiment = {
id: string;
name: string;
total: number;
candidates: Candidate[];
source: Source;
created_at: string;
evidence: {
plan: PlanSpec;
plan_reference: { id?: string; version?: number };
selection_snapshot_ids: string[];
combination_count: string;
mode: string;
seed: number;
annotations: Record<
string,
{ baseline: boolean; duplicate_of: string | null; request_hash: string }
>;
};
};
export type Result = {
id: string;
run_id: string;
client_item_id: string;
alpha_id?: string | null;
selection: string;
combo: string;
settings: SimulationSettings;
error?: string | null;
platform_status: string;
collection_status: string;
persistence_status: string;
metrics: {
is: Record<string, number | null>;
os: Record<string, number | null>;
};
checks: {
status: string;
counts: Record<string, number>;
total: number;
non_pass: unknown[];
};
result: { observed_at: string; complete: boolean } | null;
components: Components;
pnl_fetched_at: string | null;
};
+3
View File
@@ -39,6 +39,9 @@ export type Alpha = {
id: string;
name: string | null;
expression_preview: string;
selection_preview?: string;
combo_preview?: string;
component_count?: number | null;
alpha_type: string | null;
language: string | null;
stage: string | null;
+21 -12
View File
@@ -63,17 +63,26 @@ test("sidebar geometry, recent navigation, user menu and compact tools", async (
await page.getByRole("button", { name: "同步任务", exact: true }).click();
await expect(page.locator(".job-panel")).toBeVisible();
// The drawer must receive pointer events where the global tools sit behind it.
await expect.poll(() =>
page.getByRole("toolbar").getByRole("button").evaluateAll((buttons) =>
buttons.every((button) => {
const rect = button.getBoundingClientRect();
return Boolean(document.elementFromPoint(
rect.x + rect.width / 2,
rect.y + rect.height / 2,
)?.closest(".job-panel"));
}),
),
).toBe(true);
await expect
.poll(() =>
page
.getByRole("toolbar")
.getByRole("button")
.evaluateAll((buttons) =>
buttons.every((button) => {
const rect = button.getBoundingClientRect();
return Boolean(
document
.elementFromPoint(
rect.x + rect.width / 2,
rect.y + rect.height / 2,
)
?.closest(".job-panel"),
);
}),
),
)
.toBe(true);
await page.locator(".job-panel .semi-sidesheet-close").click();
await expect(page.locator(".job-panel")).not.toBeVisible();
await page.getByRole("button", { name: "切换研究助手" }).click();
@@ -85,7 +94,7 @@ test("sidebar geometry, recent navigation, user menu and compact tools", async (
await page.keyboard.press("Control+k");
await page
.getByRole("dialog")
.getByRole("button", { name: "Alpha 管理" })
.getByRole("button", { name: "Alpha 管理", exact: true })
.click();
await expect(
page.getByRole("tab", { name: "待提交", exact: true }),
+219
View File
@@ -0,0 +1,219 @@
import { expect, test } from "@playwright/test";
const headers = { "X-WQ-Request": "1" };
test("Super Alpha web lifecycle, generic simulation, evidence and type routing", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.goto("/#superalpha-research");
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
await page.getByRole("button", { name: "进入工作空间" }).click();
await expect(
page.getByRole("button", { name: "新建 Super Alpha 方案", exact: true }),
).toBeVisible();
await page.request.put("/api/v1/account/credentials", {
headers,
data: { email: "test@example.com", password: "synthetic-password" },
});
await page.request.post("/api/v1/account/connect", { headers });
await expect
.poll(
async () =>
(await (await page.request.get("/api/v1/account")).json())
.connection_status,
)
.toBe("connected");
await page
.getByRole("button", { name: "新建 Super Alpha 方案", exact: true })
.click();
await page.getByLabel("方案名称", { exact: true }).fill("浏览器 SUPER 验收");
await page
.getByLabel("研究假设", { exact: true })
.fill("比较低换手组件与等权基线");
await page
.getByLabel("Selection", { exact: true })
.fill("turnover < {threshold}");
await page.getByLabel("Combo", { exact: true }).fill("alpha");
await page
.getByLabel("参数候选 JSON", { exact: true })
.fill('{"threshold":{"kind":"number","values":[0.1,0.2]}}');
await page.getByRole("button", { name: "同步合法设置", exact: true }).click();
await page.getByRole("button", { name: "选择组合", exact: true }).click();
await page.getByRole("combobox", { name: "Region", exact: true }).click();
await page.getByRole("option").filter({ hasText: /^USA$/ }).click();
await page.getByRole("combobox", { name: "Universe", exact: true }).click();
await page
.getByRole("option")
.filter({ hasText: /^TOP3000$/ })
.click();
await page.getByRole("button", { name: "应用组合", exact: true }).click();
await page.getByRole("button", { name: "保存方案", exact: true }).click();
await expect(page.getByText("已保存版本 1", { exact: true })).toBeVisible();
await page
.getByRole("button", { name: "预览 Selection(首组参数)", exact: true })
.click();
await expect(page.locator(".super-components")).toContainText("已核实 2 个");
await page.getByRole("tab", { name: "固定候选", exact: true }).click();
const building = page.waitForResponse(
(r) =>
r.url().endsWith("/superalpha/candidates") &&
r.request().method() === "POST",
);
await page.getByRole("button", { name: "展开固定候选", exact: true }).click();
const experiment = await (await building).json();
expect(experiment.total).toBe(4);
const table = page.locator(
".super-research .semi-tabs-pane-active .workspace-table",
);
await expect(table.locator("tbody tr.semi-table-row")).toHaveCount(4);
expect(
Math.round(
(await table.locator("tbody tr.semi-table-row").first().boundingBox())!
.height,
),
).toBe(40);
await table.locator("thead .semi-checkbox").click();
await page
.getByRole("button", { name: "预览回测(4)", exact: true })
.click();
await expect(
page.getByText(/浏览器 SUPER 验收 · 4 条候选 · 4 个平台批次/),
).toBeVisible();
await expect(
page.getByText("SUPER · Selection: turnover < 0.1 · Combo: alpha", {
exact: true,
}),
).toBeVisible();
await page.getByRole("button", { name: "确认启动回测", exact: true }).click();
await expect(page.getByText("4 / 4 已保存", { exact: true })).toBeVisible({
timeout: 25000,
});
const result = await (
await page.request.get(
`/api/v1/superalpha/experiments/${experiment.id}/results`,
)
).json();
expect(
result.items.every(
(i: { persistence_status: string }) => i.persistence_status === "saved",
),
).toBe(true);
const alpha = result.items[0].alpha_id;
await page.goto(`/#alphas?alpha_id=${alpha}`);
await expect(
page.getByRole("button", { name: "Super Alpha 管理", exact: true }),
).toHaveAttribute("aria-current", "page");
await expect(page.locator(".alpha-detail")).toContainText(
"Selection rationale",
);
await page.getByRole("tab", { name: "组件证据", exact: true }).click();
await expect(page.locator(".alpha-detail .super-components")).toContainText(
"已核实 2 个",
);
await page
.getByRole("button", { name: "复制为研究方案", exact: true })
.click();
await expect(page.getByLabel("Selection", { exact: true })).toHaveValue(
result.items[0].selection,
);
await page.goto(`/#superalpha-research?experiment_id=${experiment.id}`);
await page.getByRole("tab", { name: "回测对照", exact: true }).click();
await expect(page.getByText("4 个回测项", { exact: true })).toBeVisible();
const checks = page.locator(
".super-research .semi-tabs-pane-active tbody input[type=checkbox]",
);
await checks.nth(0).focus();
await page.keyboard.press("Space");
await checks.nth(1).focus();
await page.keyboard.press("Space");
await page.getByRole("button", { name: "对照两项(2)" }).click();
await expect(page.getByRole("dialog")).toContainText("同池对照");
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).not.toBeVisible();
for (const width of [1440, 850, 390]) {
await page.setViewportSize({ width, height: 950 });
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
await page.screenshot({
path: `../output/playwright/superalpha-${width}.png`,
});
}
expect(errors).toEqual([]);
});
test("Super management scopes, saved views and cross-page selection", async ({
page,
}) => {
await page.goto("/#superalphas");
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
await page.getByRole("button", { name: "进入工作空间" }).click();
await expect(
page.getByRole("tab", { name: "待提交", exact: true }),
).toBeVisible();
await page.request.put("/api/v1/account/credentials", {
headers,
data: { email: "test@example.com", password: "synthetic-password" },
});
await page.request.post("/api/v1/account/connect", { headers });
await expect
.poll(
async () =>
(await (await page.request.get("/api/v1/account")).json())
.connection_status,
)
.toBe("connected");
const job = await (
await page.request.post("/api/v1/sync-jobs", {
headers,
data: {
kind: "daily_sync",
submission: "UNSUBMITTED",
date_from: "2025-01-01",
date_to: "2025-01-01",
},
})
).json();
await expect
.poll(
async () =>
(await (await page.request.get(`/api/v1/sync-jobs/${job.id}`)).json())
.status,
)
.toBe("completed");
await page.reload();
const panel = page.locator(".alpha-page-view:not([hidden])");
await expect(panel.locator(".alpha-link").first()).toBeVisible();
const data = await (
await page.request.get(
"/api/v1/alphas?management_scope=super&submission=UNSUBMITTED",
)
).json();
expect(
data.items.every((a: { alpha_type: string }) => a.alpha_type === "SUPER"),
).toBe(true);
await panel.locator("tbody .semi-checkbox").first().click();
await panel.locator(".semi-page-item").filter({ hasText: /^2$/ }).click();
await expect(panel.getByText("已选择 1 条", { exact: false })).toBeVisible();
await panel.getByRole("button", { name: "保存为新视图" }).click();
await page.getByLabel("视图名称").fill("SUPER 隔离视图");
await page.getByRole("button", { name: "保存视图", exact: true }).click();
await expect(
panel.getByRole("tab", { name: /SUPER 隔离视图/ }),
).toBeVisible();
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
await expect(
page.getByRole("tab", { name: /SUPER 隔离视图/ }),
).not.toBeVisible();
const regular = await (
await page.request.get("/api/v1/alphas?management_scope=non_super")
).json();
expect(
regular.items.every(
(a: { alpha_type: string }) => a.alpha_type !== "SUPER",
),
).toBe(true);
});
+14 -15
View File
@@ -65,7 +65,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
async () =>
(
await (
await page.request.get("/api/v1/alphas?submission=UNSUBMITTED")
await page.request.get("/api/v1/alphas?submission=UNSUBMITTED&q=TEST")
).json()
).total,
)
@@ -77,28 +77,26 @@ test("account → scoped sync → research → resync → PnL → filtered expor
await expect
.poll(
async () =>
(await (await page.request.get("/api/v1/alphas")).json()).total,
(await (await page.request.get("/api/v1/alphas?q=TEST")).json()).total,
{ timeout: 20000 },
)
.toBe(620);
await page.keyboard.press("Escape");
await expect(page.locator(".job-panel")).not.toBeVisible();
await page.getByRole("tab", { name: "待提交", exact: true }).click();
await expect(page.locator(".alpha-table-meta .count-pill")).toHaveText("413");
// Alpha 管理 now excludes SUPER; the upstream sync still imports both types.
await expect(page.locator(".alpha-page-view:not([hidden]) .alpha-table-meta .count-pill")).toHaveText("354");
await page.screenshot({
path: "../output/playwright/alpha-library.png",
fullPage: true,
});
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0008");
await page.getByRole("button", { name: "查询", exact: true }).click();
await expect(page.locator(".alpha-link")).toHaveCount(1);
await page.locator(".alpha-link").click();
await expect(
page.getByRole("heading", { name: "Selection", exact: true }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Combo", exact: true }),
page.getByRole("heading", { name: "表达式", exact: true }),
).toBeVisible();
await page.getByRole("tab", { name: "研究信息" }).click();
await page
@@ -115,7 +113,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
await expect
.poll(
async () =>
(await (await page.request.get("/api/v1/alphas/TEST0007/pnl")).json())
(await (await page.request.get("/api/v1/alphas/TEST0008/pnl")).json())
.cached,
)
.toBe(true);
@@ -131,7 +129,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
const job = await (
await page.request.post("/api/v1/sync-jobs", {
headers: { "X-WQ-Request": "1" },
data: { kind: "alpha_refresh", alpha_ids: ["TEST0007"] },
data: { kind: "alpha_refresh", alpha_ids: ["TEST0008"] },
})
).json();
await expect
@@ -143,7 +141,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
.toBe("completed");
await page.reload();
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0008");
await page.getByRole("button", { name: "查询", exact: true }).click();
await expect(page.locator(".alpha-link")).toHaveCount(1);
await page.locator(".alpha-link").click();
@@ -165,13 +163,14 @@ test("account → scoped sync → research → resync → PnL → filtered expor
expect(contents).toContain("TEST0619");
expect(contents).toContain("TEST0001");
expect(contents).not.toContain("TEST0000");
expect((contents.match(/TEST\d{4}/g) ?? []).length).toBe(413);
expect(contents).not.toContain("TEST0007");
expect((contents.match(/TEST\d{4}/g) ?? []).length).toBe(354);
await page.getByRole("button", { name: "用户菜单" }).click();
await page.getByRole("button", { name: "退出登录" }).click();
await expect(
page.getByRole("heading", { name: "登录研究工作空间" }),
).toBeVisible();
expect((await page.request.get("/api/v1/alphas")).status()).toBe(401);
expect((await page.request.get("/api/v1/alphas?q=TEST")).status()).toBe(401);
expect(failures).toEqual([]);
});
@@ -185,7 +184,7 @@ test("batch tags, column visibility, server pagination and saved preferences", a
page.getByRole("tab", { name: "待提交", exact: true }),
).toBeVisible();
// Each test can run alone against the isolated browser acceptance server.
if ((await (await page.request.get("/api/v1/alphas")).json()).total !== 620) {
if ((await (await page.request.get("/api/v1/alphas?q=TEST")).json()).total !== 620) {
const headers = { "X-WQ-Request": "1" };
await page.request.put("/api/v1/account/credentials", {
headers,
@@ -215,7 +214,7 @@ test("batch tags, column visibility, server pagination and saved preferences", a
await expect
.poll(
async () =>
(await (await page.request.get("/api/v1/alphas")).json()).total,
(await (await page.request.get("/api/v1/alphas?q=TEST")).json()).total,
{ timeout: 20000 },
)
.toBe(620);