192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
|
|
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
|
|||
|
|
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 === "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>
|
|||
|
|
);
|
|||
|
|
}
|