2026-09-08 19:28:44 +08:00
|
|
|
import { useEffect, useState } from "react";
|
2026-09-11 23:21:08 +08:00
|
|
|
import { Table, Toast } from "@douyinfe/semi-ui-19";
|
2026-09-08 19:28:44 +08:00
|
|
|
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>
|
2026-09-11 23:21:08 +08:00
|
|
|
<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
|
2026-09-08 19:28:44 +08:00
|
|
|
? "未提供"
|
2026-09-11 23:21:08 +08:00
|
|
|
: `${formatNumber(alpha.turnover * 100)}%`,
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
2026-09-08 19:28:44 +08:00
|
|
|
<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>
|
2026-09-10 14:09:41 +08:00
|
|
|
{pnl && <PnlChart points={pnl.points} series={pnl.series} />}
|
2026-09-08 19:28:44 +08:00
|
|
|
<small>
|
|
|
|
|
{String(result.count)} 条记录 · 缓存于{" "}
|
|
|
|
|
{formatTime(result.fetched_at as string, timezone)}
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p>尚无 PnL 缓存,可以请求创建 PnL 刷新任务。</p>
|
|
|
|
|
);
|
|
|
|
|
}
|