feat: add scoped alpha sync and local self-correlation
This commit is contained in:
@@ -31,6 +31,7 @@ import type {
|
||||
ResearchState,
|
||||
} from "../types";
|
||||
import { PnlChart } from "./PnlChart";
|
||||
import { SelfCorrelationPanel } from "./SelfCorrelationPanel";
|
||||
|
||||
export function AlphaDetail({
|
||||
id,
|
||||
@@ -304,6 +305,17 @@ export function AlphaDetail({
|
||||
<Empty description="点击获取 PnL,从平台读取并缓存曲线数据" />
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab="本地自相关" itemKey="correlation">
|
||||
{tab === "correlation" && id && (
|
||||
<SelfCorrelationPanel
|
||||
key={id}
|
||||
id={id}
|
||||
version={version}
|
||||
timezone={timezone}
|
||||
onTask={onTask}
|
||||
/>
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab="研究记录" itemKey="research">
|
||||
<div className="detail-section">
|
||||
<p className="muted">
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Input, Modal, Toast } from "@douyinfe/semi-ui-19";
|
||||
import { post } from "../api";
|
||||
import type { Submission } from "../types";
|
||||
|
||||
export function AlphaSyncDialog({
|
||||
submission,
|
||||
suspended,
|
||||
onClose,
|
||||
onTask,
|
||||
}: {
|
||||
submission: Submission;
|
||||
suspended: boolean;
|
||||
onClose: () => void;
|
||||
onTask: () => void;
|
||||
}) {
|
||||
const [from, setFrom] = useState("");
|
||||
const [to, setTo] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const label = submission === "UNSUBMITTED" ? "待提交" : "已提交";
|
||||
const dateLabel = submission === "UNSUBMITTED" ? "创建日期" : "提交日期";
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
async function start() {
|
||||
if (!from || !to || from > to || to > today) {
|
||||
setError("请选择有效的起止日期,结束日期不能晚于今天(UTC)。");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await post("/sync-jobs", {
|
||||
kind: "daily_sync",
|
||||
submission,
|
||||
date_from: from,
|
||||
date_to: to,
|
||||
});
|
||||
onClose();
|
||||
onTask();
|
||||
} catch (e) {
|
||||
Toast.error((e as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title={`按天同步${label} Alpha`}
|
||||
visible={!suspended}
|
||||
onCancel={onClose}
|
||||
okText="开始同步"
|
||||
okButtonProps={{ "aria-label": "开始同步" }}
|
||||
confirmLoading={busy}
|
||||
onOk={() => void start()}
|
||||
>
|
||||
<p className="muted">
|
||||
按{dateLabel}(UTC)逐天获取,包含隐藏记录。相同起止日期表示只同步当天。
|
||||
</p>
|
||||
<div className="sync-date-range">
|
||||
<label>
|
||||
开始日期
|
||||
<Input
|
||||
aria-label="同步开始日期"
|
||||
type="date"
|
||||
max={today}
|
||||
value={from}
|
||||
onChange={(value) => {
|
||||
setFrom(value);
|
||||
if (!to) setTo(value);
|
||||
setError("");
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
结束日期
|
||||
<Input
|
||||
aria-label="同步结束日期"
|
||||
type="date"
|
||||
min={from || undefined}
|
||||
max={today}
|
||||
value={to}
|
||||
onChange={(value) => {
|
||||
setTo(value);
|
||||
setError("");
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="error-text" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<p className="muted">
|
||||
任务面板显示当前日期和进度,中断后可继续未完成部分。
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -72,6 +72,28 @@ export function JobPanel({
|
||||
</Tag>
|
||||
</div>
|
||||
<p className="muted">{formatTime(job.created_at, timezone)}</p>
|
||||
{job.payload?.submission && (
|
||||
<p className="muted">
|
||||
{job.payload.submission === "SUBMITTED" ? "已提交" : "待提交"}
|
||||
{job.payload.date_from
|
||||
? ` · ${job.payload.date_from} 至 ${job.payload.date_to}(UTC)`
|
||||
: " · 全量"}
|
||||
</p>
|
||||
)}
|
||||
{job.checkpoint?.date && (
|
||||
<p className="muted">
|
||||
同步日期:{job.checkpoint.date} · 已完成{" "}
|
||||
{job.checkpoint.dates_completed} / {job.checkpoint.dates_total} 天
|
||||
</p>
|
||||
)}
|
||||
{job.kind === "self_correlation" && job.checkpoint?.alpha_id && (
|
||||
<p className="muted">
|
||||
{job.checkpoint.alpha_id} ·{" "}
|
||||
{job.checkpoint.phase === "calculating"
|
||||
? "计算相关性"
|
||||
: `准备 PnL:${job.checkpoint.references_loaded ?? 0} / ${job.checkpoint.references_total ?? 0} 个基准`}
|
||||
</p>
|
||||
)}
|
||||
<div className="job-numbers">
|
||||
<span>
|
||||
已处理 <b>{job.processed}</b>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user