feat: add initial frontend setup with styles, types, and testing framework

- Created a global CSS file for styling the frontend with responsive design.
- Introduced TypeScript types for various entities including Research, Alpha, and Account.
- Implemented Playwright tests for account management and data synchronization workflows.
- Configured TypeScript with strict settings and included necessary libraries.
- Set up Vite as the build tool with React plugin and API proxy configuration.
- Added a Python script to initialize environment variables securely.
This commit is contained in:
yuxuanhui
2026-09-07 14:54:20 +08:00
commit 3cd280d068
53 changed files with 10888 additions and 0 deletions
+369
View File
@@ -0,0 +1,369 @@
import { useEffect, useRef, useState } from "react";
import {
Banner,
Button,
Checkbox,
Descriptions,
Empty,
Input,
Select,
SideSheet,
Spin,
TabPane,
Tabs,
Tag,
TextArea,
Toast,
} from "@douyinfe/semi-ui-19";
import {
api,
displayValue,
formatTime,
patch,
post,
stateOptions,
} from "../api";
import type {
AlphaDetail as Detail,
Pnl,
Research,
ResearchState,
} from "../types";
import { PnlChart } from "./PnlChart";
export function AlphaDetail({
id,
version,
timezone,
taskPanelOpen,
onClose,
onSaved,
onTask,
}: {
id: string | null;
version: string;
timezone?: string;
taskPanelOpen: boolean;
onClose: () => void;
onSaved: () => void;
onTask: () => void;
}) {
const [detail, setDetail] = useState<Detail | null>(null);
const [pnl, setPnl] = useState<Pnl | null>(null);
const [research, setResearch] = useState<Research | null>(null);
const [tagText, setTagText] = useState("");
const [error, setError] = useState("");
const [saving, setSaving] = useState(false);
const [tab, setTab] = useState("overview");
const dirty = useRef(false);
useEffect(() => {
dirty.current = false;
setDetail(null);
setResearch(null);
setPnl(null);
setError("");
setTab("overview");
}, [id]);
useEffect(() => {
let active = true;
if (id)
api<Detail>(`/alphas/${id}`)
.then((value) => {
if (active) {
setDetail(value);
setError("");
if (!dirty.current) {
setResearch(value.research);
setTagText(value.research.tags.join(", "));
}
}
})
.catch((e) => {
if (active) setError(e.message);
});
return () => {
active = false;
};
}, [id, version]);
useEffect(() => {
let active = true;
if (id && tab === "pnl")
api<Pnl>(`/alphas/${id}/pnl`)
.then((p) => {
if (active) setPnl(p);
})
.catch((e) => {
if (active) Toast.error(e.message);
});
return () => {
active = false;
};
}, [id, tab, version]);
async function fetchPnl() {
try {
await post("/sync-jobs", { kind: "pnl_refresh", alpha_ids: [id] });
onTask();
} catch (e) {
Toast.error((e as Error).message);
}
}
async function save() {
if (!research) return;
setSaving(true);
try {
const { updated_at: _, ...editable } = research;
await patch(`/alphas/${id}/research`, {
...editable,
tags: tagText
.split(/[,,]/)
.map((s) => s.trim())
.filter(Boolean),
});
dirty.current = false;
Toast.success("研究记录已保存");
onSaved();
} catch (e) {
Toast.error((e as Error).message);
} finally {
setSaving(false);
}
}
return (
<SideSheet
closeOnEsc={!taskPanelOpen}
className="alpha-detail"
visible={Boolean(id)}
width={800}
title={
<span>
Alpha 详情 <code className="muted">{id}</code>
</span>
}
onCancel={onClose}
>
{error ? (
<Banner type="danger" description={error} />
) : !detail || !research ? (
<Spin />
) : (
<>
<div className="detail-title">
<div>
<h2>{detail.name || detail.id}</h2>
<div className="inline-actions">
<Tag>{detail.alpha_type ?? "类型未提供"}</Tag>
<Tag>{detail.language ?? "语言未提供"}</Tag>
<Tag>{detail.status ?? "状态未提供"}</Tag>
</div>
</div>
<a
href={`https://platform.worldquantbrain.com/alpha/${encodeURIComponent(detail.id)}`}
target="_blank"
rel="noreferrer"
>
<Button>在 BRAIN 中打开 ↗</Button>
</a>
</div>
<Tabs activeKey={tab} onChange={setTab}>
<TabPane tab="概览与表达式" itemKey="overview">
<div className="detail-section">
<Descriptions
data={["region", "universe", "stage"].map((key) => ({
key,
value: displayValue(detail[key as keyof Detail]),
}))}
/>
<p className="muted">
平台创建:{formatTime(detail.date_created, timezone)} ·
最近同步:{formatTime(detail.synced_at, timezone)}
</p>
{detail.expression !== null && (
<CodeBlock
title={
detail.language === "PYTHON" ? "Python 代码" : "表达式"
}
value={detail.expression}
/>
)}
{detail.selection !== null && (
<CodeBlock title="Selection" value={detail.selection} />
)}
{detail.combo !== null && (
<CodeBlock title="Combo" value={detail.combo} />
)}
<h3>回测设置</h3>
<Descriptions
align="left"
data={Object.entries(detail.settings).map(([key, value]) => ({
key,
value: displayValue(value),
}))}
/>
</div>
</TabPane>
<TabPane tab="指标与检查" itemKey="metrics">
<MetricTable title="IS 指标" values={detail.is_metrics} />
<MetricTable title="OS 指标" values={detail.os_metrics} />
<h3>平台已有检查结果</h3>
<p className="muted">
展示已同步结果,查看此页不会触发新的平台检查。
</p>
{detail.checks.length ? (
detail.checks.map((check, i) => (
<div className="check-row" key={i}>
<span>{displayValue(check.name)}</span>
<span>
{displayValue(check.value)} / {displayValue(check.limit)}
</span>
<Tag
color={
check.result === "PASS"
? "green"
: check.result === "FAIL"
? "red"
: "grey"
}
>
{displayValue(check.result)}
</Tag>
</div>
))
) : (
<Empty description="平台尚未返回检查结果" />
)}
</TabPane>
<TabPane tab="PnL 曲线" itemKey="pnl">
<div className="section-toolbar">
<div>
<h3>累计 PnL</h3>
<p className="muted">
{pnl?.fetched_at
? `缓存更新于 ${formatTime(pnl.fetched_at, timezone)}`
: "尚未获取 PnL"}
</p>
</div>
<Button onClick={() => void fetchPnl()}>
{pnl?.cached ? "刷新 PnL" : "获取 PnL"}
</Button>
</div>
{pnl?.cached ? (
<>
<PnlChart points={pnl.points} />
<p className="muted">
{pnl.points.length} 条记录 · 缺失数据保留为断点
</p>
</>
) : (
<Empty description="点击获取 PnL,从平台读取并缓存曲线数据" />
)}
</TabPane>
<TabPane tab="研究记录" itemKey="research">
<div className="detail-section">
<p className="muted">
这些记录只保存在你的工作空间,平台同步不会覆盖。
</p>
<label>
研究状态
<Select
aria-label="研究状态"
optionList={stateOptions}
value={research.state}
onChange={(state) => {
dirty.current = true;
setResearch({
...research,
state: state as ResearchState,
});
}}
/>
</label>
<label>
标签
<Input
aria-label="研究标签"
value={tagText}
onChange={(value) => {
dirty.current = true;
setTagText(value);
}}
placeholder="以逗号分隔,例如 低换手, 分析师预期"
/>
</label>
<label>
研究备注
<TextArea
aria-label="研究备注"
value={research.note}
onChange={(note) => {
dirty.current = true;
setResearch({ ...research, note });
}}
rows={8}
maxCount={20000}
placeholder="记录假设、观察、下一步实验……"
/>
</label>
<Checkbox
checked={research.favorite}
onChange={(e) => {
dirty.current = true;
setResearch({
...research,
favorite: Boolean(e.target.checked),
});
}}
>
收藏此 Alpha
</Checkbox>
<div className="detail-save">
<Button
theme="solid"
loading={saving}
onClick={() => void save()}
>
保存研究记录
</Button>
</div>
</div>
</TabPane>
</Tabs>
</>
)}
</SideSheet>
);
}
function CodeBlock({ title, value }: { title: string; value: string }) {
return (
<>
<h3>{title}</h3>
<pre className="code-block">
<code>{value}</code>
</pre>
</>
);
}
function MetricTable({
title,
values,
}: {
title: string;
values: Record<string, unknown>;
}) {
const entries = Object.entries(values).filter(([key]) => key !== "checks");
return (
<div className="detail-section">
<h3>{title}</h3>
{entries.length ? (
<Descriptions
data={entries.map(([key, value]) => ({
key,
value: displayValue(value),
}))}
/>
) : (
<p className="muted">未提供</p>
)}
</div>
);
}