Files
worldquant-alpha-system/frontend/src/ai/AlphaToolCard.tsx
T

190 lines
6.1 KiB
TypeScript
Raw Normal View History

import { useEffect, useState } from "react";
import { Table, Toast } from "@douyinfe/semi-ui-19";
import { api, formatNumber, formatTime, stateLabels } from "../api";
import { PnlChart } from "../components/PnlChart";
import type { Alpha, Pnl, Research } from "../types";
import type { ToolCardProps } from "./types";
export function AlphaToolCard({ call, onAction, timezone }: ToolCardProps) {
const result = call.result ?? {};
const items = Array.isArray(result.items) ? result.items : [];
const isSearch = call.name === "search_alphas";
const alphas = (
isSearch ? items : call.name === "get_alpha" && result.id ? [result] : []
) as Alpha[];
const openAlpha = (id: string) =>
onAction({ type: "open_alpha", alpha_id: id, nonce: Date.now() });
return (
<>
{call.preview.targets?.map((target) => (
<details
key={target.alpha_id}
open={call.preview.targets!.length === 1}
>
<summary>{target.alpha_id} 的修改</summary>
{(["note", "tags", "favorite", "state"] as const)
.filter(
(key) =>
JSON.stringify(target.before[key]) !==
JSON.stringify(target.after[key]),
)
.map((key) => (
<div className="ai-diff" key={key}>
<strong>
{
{
note: "备注",
tags: "标签",
favorite: "收藏",
state: "研究状态",
}[key]
}
</strong>
<del>
<span>修改前</span>
{researchValue(key, target.before[key])}
</del>
<ins>
<span>修改后</span>
{researchValue(key, target.after[key])}
</ins>
</div>
))}
</details>
))}
{alphas.length > 0 && (
<div className="ai-alpha-results">
{alphas.map((alpha) => (
<div key={alpha.id}>
<button className="text-link" onClick={() => openAlpha(alpha.id)}>
{alpha.name || alpha.id}
</button>
<small>
{alpha.id} · {alpha.region ?? "地区未提供"}
</small>
<Table<Alpha>
dataSource={[alpha]}
rowKey="id"
pagination={false}
size="small"
tableLayout="fixed"
columns={[
{
title: "Sharpe",
key: "sharpe",
render: (_, alpha) => formatNumber(alpha.sharpe),
},
{
title: "Fitness",
key: "fitness",
render: (_, alpha) => formatNumber(alpha.fitness),
},
{
title: "Turnover",
key: "turnover",
render: (_, alpha) =>
alpha.turnover == null
? "未提供"
: `${formatNumber(alpha.turnover * 100)}%`,
},
]}
/>
<small>本地快照 · {formatTime(alpha.synced_at, timezone)}</small>
</div>
))}
</div>
)}
{isSearch && (
<p>
共 {String(result.total ?? 0)} 条,当前返回 {items.length} 条。
{!!result.filters && (
<button
className="text-link"
onClick={() =>
onAction({
type: "apply_filters",
filters: result.filters as Record<string, unknown>,
nonce: Date.now(),
})
}
>
应用到列表
</button>
)}
</p>
)}
{call.name === "get_alpha_facets" && call.status === "completed" && (
<p>
已同步 {String(result.total ?? 0)} 条 Alpha · 收藏{" "}
{String(result.favorites ?? 0)} 条<br />
地区:
{Array.isArray(result.region) ? result.region.join("、") : "未提供"}
</p>
)}
{call.name === "get_alpha_pnl" && call.status === "completed" && (
<ChatPnl result={result} timezone={timezone} />
)}
{result.ok === true && (
<p>
操作已保存。
{typeof result.alpha_id === "string" && (
<button
className="text-link"
onClick={() => openAlpha(result.alpha_id as string)}
>
打开 Alpha
</button>
)}
</p>
)}
{typeof result.updated === "number" && (
<p>已更新 {result.updated} 条研究记录。</p>
)}
</>
);
}
function researchValue(key: keyof Research, value: unknown) {
if (key === "favorite") return value ? "已收藏" : "未收藏";
if (key === "state")
return (
stateLabels[String(value) as keyof typeof stateLabels] ?? String(value)
);
if (Array.isArray(value)) return value.join("、") || "无";
return String(value || "空");
}
function ChatPnl({
result,
timezone,
}: {
result: Record<string, unknown>;
timezone?: string;
}) {
const [pnl, setPnl] = useState<Pnl | null>(null);
useEffect(() => {
let active = true;
if (result.cached && typeof result.alpha_id === "string")
api<Pnl>(`/alphas/${encodeURIComponent(result.alpha_id)}/pnl`)
.then((value) => {
if (active) setPnl(value);
})
.catch((error) => {
if (active) Toast.error(error.message);
});
return () => {
active = false;
};
}, [result.alpha_id, result.fetched_at, result.cached]);
return result.cached ? (
<div>
{pnl && <PnlChart points={pnl.points} series={pnl.series} />}
<small>
{String(result.count)} 条记录 · 缓存于{" "}
{formatTime(result.fetched_at as string, timezone)}
</small>
</div>
) : (
<p>尚无 PnL 缓存,可以请求创建 PnL 刷新任务。</p>
);
}