fix: fetch dataset scope options from WorldQuant platform

This commit is contained in:
yuxuanhui
2026-09-08 18:57:03 +08:00
parent 80cb7d2b80
commit 3d26827b49
13 changed files with 367 additions and 53 deletions
+124 -19
View File
@@ -35,6 +35,28 @@ type Scope = {
universe: string;
delay: number;
};
type PlatformOption = Omit<Scope, "universe"> & { universes: string[] };
type PlatformOptions = { instrument_options: PlatformOption[] };
function normalizeScope(rows: PlatformOption[], preferred: Scope): Scope {
const typed = rows.filter(
(r) => r.instrument_type === preferred.instrument_type,
);
const regional = typed.filter((r) => r.region === preferred.region);
const row =
regional.find((r) => r.delay === preferred.delay) ??
regional[0] ??
typed[0] ??
rows[0];
if (!row) return preferred;
return {
instrument_type: row.instrument_type,
region: row.region,
delay: row.delay,
universe: row.universes.includes(preferred.universe)
? preferred.universe
: row.universes[0],
};
}
type Note = { note: string; version: number; updated_at: string };
type Entry = {
id: string;
@@ -136,7 +158,18 @@ export function DatasetPage({
universe: "TOP3000",
delay: 1,
});
const [scopes, setScopes] = useState<Record<string, string[]>>({});
const scopeRef = useRef(scope);
scopeRef.current = scope;
const [scopes, setScopes] = useState<PlatformOption[]>([]);
const [scopeError, setScopeError] = useState("");
const [scopeRevision, setScopeRevision] = useState(0);
const selectedOption = scopes.find(
(r) =>
r.instrument_type === scope.instrument_type &&
r.region === scope.region &&
r.delay === scope.delay,
);
const scopeValid = !!selectedOption?.universes.includes(scope.universe);
const [browse, setBrowse] = useState<Browse>(initialBrowse);
const [fieldBrowse, setFieldBrowse] = useState<Browse>(initialBrowse);
const [size, setSize] = useState(25);
@@ -370,11 +403,34 @@ export function DatasetPage({
if (account) setSize(account.page_size);
}, [account?.page_size]);
useEffect(() => {
if (active)
api<Record<string, string[]>>("/catalog/scopes")
.then(setScopes)
.catch((e) => setError(e.message));
}, [active]);
if (!active) return;
let live = true;
setScopes([]);
setScopeError("");
api<PlatformOptions>("/catalog/scopes")
.then((result) => {
if (!live) return;
if (!result.instrument_options.length)
throw new Error("平台未返回可用研究范围");
setScopes(result.instrument_options);
const next = normalizeScope(
result.instrument_options,
scopeRef.current,
);
if (
(Object.keys(next) as (keyof Scope)[]).some(
(key) => next[key] !== scopeRef.current[key],
)
)
changeScope(next);
})
.catch((e) => {
if (live) setScopeError(e.message);
});
return () => {
live = false;
};
}, [active, version, scopeRevision]);
useEffect(() => {
if (!active) return;
let live = true;
@@ -535,6 +591,7 @@ export function DatasetPage({
if (!field) setSelected(row);
};
async function sync(datasetId?: string) {
if (!scopeValid) return;
setBusy(true);
try {
await post("/catalog/sync-jobs", {
@@ -848,6 +905,7 @@ export function DatasetPage({
<>
<section className="catalog-page">
{[
"InstrumentType",
"Region",
"Universe",
"Delay",
@@ -865,34 +923,68 @@ export function DatasetPage({
</span>
))}
<div className="catalog-tools">
<Select
aria-labelledby="catalog-label-InstrumentType"
value={scope.instrument_type}
disabled={!scopes.length}
optionList={options([
...new Set(scopes.map((r) => r.instrument_type)),
])}
onChange={(v) =>
changeScope(
normalizeScope(scopes, {
...scope,
instrument_type: String(v),
}),
)
}
/>
<Select
aria-labelledby="catalog-label-Region"
value={scope.region}
optionList={options(Object.keys(scopes))}
disabled={!scopes.length}
optionList={options([
...new Set(
scopes
.filter((r) => r.instrument_type === scope.instrument_type)
.map((r) => r.region),
),
])}
onChange={(v) =>
changeScope({
...scope,
region: String(v),
universe: scopes[String(v)][0],
})
changeScope(
normalizeScope(scopes, { ...scope, region: String(v) }),
)
}
/>
<Select
aria-labelledby="catalog-label-Universe"
value={scope.universe}
optionList={options(scopes[scope.region] ?? [])}
disabled={!selectedOption}
optionList={options(selectedOption?.universes ?? [])}
onChange={(v) => changeScope({ ...scope, universe: String(v) })}
/>
<Select
aria-labelledby="catalog-label-Delay"
value={scope.delay}
optionList={[
{ label: "Delay 0", value: 0 },
{ label: "Delay 1", value: 1 },
]}
onChange={(v) => changeScope({ ...scope, delay: Number(v) })}
disabled={!scopes.length}
optionList={scopes
.filter(
(r) =>
r.instrument_type === scope.instrument_type &&
r.region === scope.region,
)
.map((r) => ({ label: `Delay ${r.delay}`, value: r.delay }))}
onChange={(v) =>
changeScope(
normalizeScope(scopes, { ...scope, delay: Number(v) }),
)
}
/>
<Button loading={busy} onClick={() => void sync()}>
<Button
loading={busy}
disabled={!scopeValid}
onClick={() => void sync()}
>
同步目录
</Button>
<Button
@@ -907,6 +999,19 @@ export function DatasetPage({
{formatTime(data.synced_at, account?.timezone)}
</span>
</div>
{scopeError && (
<Banner
type="danger"
description={
<span>
{scopeError}{" "}
<Button onClick={() => setScopeRevision((v) => v + 1)}>
重试获取选项
</Button>
</span>
}
/>
)}
{error && <Banner type="danger" description={error} />}
<section className="library-panel">
<div className="catalog-tools">