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;