Files
worldquant-alpha-system/frontend/src/components/SelfCorrelationPanel.tsx
T
2026-09-10 11:45:05 +08:00

202 lines
6.3 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.
import { useEffect, useState } from "react";
import {
Banner,
Button,
Descriptions,
Empty,
Spin,
Table,
Tag,
Toast,
} from "@douyinfe/semi-ui-19";
import { api, correlationLabels, formatNumber, formatTime, post } from "../api";
import type { CorrelationResult } from "../types";
export function SelfCorrelationPanel({
id,
version,
timezone,
onTask,
}: {
id: string;
version: string;
timezone?: string;
onTask: () => void;
}) {
const [result, setResult] = useState<CorrelationResult | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
const controller = new AbortController();
setLoading(true);
api<{ result: CorrelationResult | null }>(
`/alphas/${id}/self-correlation`,
{ signal: controller.signal },
)
.then((value) => {
if (!controller.signal.aborted) {
setResult(value.result);
setError("");
}
})
.catch((e) => {
if (!controller.signal.aborted) setError(e.message);
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [id, version]);
async function check() {
setBusy(true);
try {
await post("/sync-jobs", { kind: "self_correlation", alpha_ids: [id] });
onTask();
} catch (e) {
Toast.error((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<div className="correlation-panel">
<div className="section-toolbar">
<h3>本地自相关</h3>
<Button onClick={() => void check()} loading={busy}>
{result ? "重新检测自相关" : "检测自相关"}
</Button>
</div>
<p className="muted">
与本地已同步的同地区已提交 Alpha 比较,排除自身。使用 PnL
缓存,缺失时自动获取;建议先全量同步已提交 Alpha。
</p>
<p className="muted">
近四年 PnL 日变化的 Pearson 相关系数,至少 30 个共同样本;本地告警线为
0.7,检测结果与平台检查分别记录。
</p>
{error && <Banner type="danger" description={error} />}
{loading && !result ? (
<Spin />
) : !result ? (
<Empty description="尚未检测自相关" />
) : (
<>
{result.stale && (
<Banner
type="warning"
description="PnL 或比较样本已更新,以下为上次结果,请重新检测。"
/>
)}
<div className="inline-actions">
<Tag
color={
result.stale
? "grey"
: result.status === "high"
? "orange"
: result.status === "low"
? "green"
: "grey"
}
>
{correlationLabels[result.status]}
</Tag>
<span className="muted">
计算于 {formatTime(result.calculated_at, timezone)}
</span>
</div>
<Descriptions
size="small"
align="left"
layout="horizontal"
column={3}
data={[
{
key: "最大相关系数",
value: formatNumber(result.max_correlation, 4),
},
{
key: "有效比较",
value: `${result.compared_count} / ${result.candidate_count}`,
},
{ key: "跳过样本", value: result.skipped_count },
{
key: "比较窗口",
value: result.window_from
? `${result.window_from} 至 ${result.window_to}`
: "未提供",
},
{
key: "目标 PnL 更新",
value: result.target_pnl_fetched_at
? formatTime(result.target_pnl_fetched_at, timezone)
: "未提供",
},
]}
/>
{result.reason && <p>{result.reason}</p>}
{result.status === "insufficient_data" &&
result.candidate_count === 0 && (
<p className="muted">
没有本地比较基准时可继续下方的平台提交检查;这不代表已通过相关性检查。
</p>
)}
{result.status === "partial" && (
<p>已有样本低于阈值,但部分基准无法比较,不能据此判断全部样本。</p>
)}
{result.matches.length > 0 && (
<>
<h3>相关系数最高的 Alpha(最多 10 条)</h3>
<Table
dataSource={result.matches}
rowKey="alpha_id"
size="small"
pagination={false}
columns={[
{
title: "Alpha",
dataIndex: "alpha_id",
render: (value) => (
<a
href={`https://platform.worldquantbrain.com/alpha/${encodeURIComponent(value)}`}
target="_blank"
rel="noreferrer"
>
{value}
</a>
),
},
{
title: "相关系数",
dataIndex: "correlation",
render: (value) => formatNumber(value, 4),
},
{ title: "共同样本", dataIndex: "sample_count" },
{
title: "PnL 更新",
dataIndex: "pnl_fetched_at",
render: (value) => formatTime(value, timezone),
},
]}
/>
</>
)}
{result.skipped.length > 0 && (
<details>
<summary>
跳过原因({result.skipped_count} 条,最多展示 100 条)
</summary>
{result.skipped.map((item) => (
<p key={item.alpha_id}>
<code>{item.alpha_id}</code>:{item.reason}
</p>
))}
</details>
)}
</>
)}
</div>
);
}