"use client"; /** 股票筛选(选股系统 M6,v2 §14/§21.1): * 评分模式(多因子加权 TopN)与条件模式(结构化条件 AND); * 支持当前/历史 as_of;结果落库可查(selection_id),右侧展示最近选股历史。 */ import { useEffect, useState } from "react"; import { apiGet, apiPost } from "@/lib/api"; import type { FactorMeta, SelectionCondition, SelectionMeta, SelectionQuery, SelectionResult, SelectionRun, } from "@/lib/types"; import { PageHeader, Card, Pill, Field, Btn, Banner, Empty, SkeletonLines, } from "@/components/ui"; const FIELD_OPTIONS: { value: string; label: string; kind: "num" | "str" | "ref" }[] = [ { value: "static.industry", label: "行业 industry", kind: "str" }, { value: "static.market", label: "市场 market", kind: "str" }, { value: "momentum_20", label: "动量 20 日", kind: "num" }, { value: "momentum_60", label: "动量 60 日", kind: "num" }, { value: "close", label: "收盘价 close", kind: "num" }, { value: "close_ma60_ref", label: "收盘 > MA60", kind: "ref" }, { value: "volume_ratio_5_60", label: "量比", kind: "num" }, { value: "fundamental.roe", label: "ROE(%)", kind: "num" }, { value: "fundamental.eps", label: "EPS", kind: "num" }, ]; const OPS = ["gt", "gte", "lt", "lte", "eq", "ne", "in"]; function emptyCondition(): SelectionCondition { return { field: "static.industry", op: "eq", value: "白酒" }; } export default function SelectionPage() { const [factors, setFactors] = useState([]); const [mode, setMode] = useState<"score" | "condition">("score"); const [asOf, setAsOf] = useState(""); // 空 = 最近交易日 const [excludeSt, setExcludeSt] = useState(true); const [topN, setTopN] = useState(20); const [factorRows, setFactorRows] = useState([{ name: "momentum_60", weight: 1 }]); const [conds, setConds] = useState([emptyCondition()]); const [result, setResult] = useState(null); const [selectionId, setSelectionId] = useState(""); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [history, setHistory] = useState(null); useEffect(() => { let alive = true; apiGet("/factors") .then((list) => alive && list.length > 0 && setFactors(list)) .catch((e: Error) => alive && setError(e.message)); apiGet("/selections?limit=8") .then((rows) => alive && setHistory(rows)) .catch(() => alive && setHistory([])); return () => { alive = false; }; }, []); function buildQuery(): SelectionQuery { if (mode === "score") { return { universe: { exclude_st: excludeSt, min_listing_days: 0 }, as_of: asOf || null, method: "score", factors: factorRows.filter((r) => r.name), conditions: [], top_n: topN, }; } return { universe: { exclude_st: excludeSt, min_listing_days: 0 }, as_of: asOf || null, method: "condition", factors: [], conditions: conds.map((c) => ({ ...c, ...(c.field === "close_ma60_ref" ? { field: "close", op: c.op, ref: "ma60", value: null } : {}), })), top_n: null, }; } async function run() { setRunning(true); setError(""); setResult(null); try { const run = await apiPost("/selections", buildQuery()); setSelectionId(run.selection_id); setResult(run.result); apiGet("/selections?limit=8").then(setHistory).catch(() => undefined); } catch (e) { setError((e as Error).message); } finally { setRunning(false); } } async function openHistory(id: string) { setError(""); try { const detail = await apiGet(`/selections/${id}`); setSelectionId(id); setResult(detail); } catch (e) { setError((e as Error).message); } } return ( <> {selectionId} ) : null } />
setAsOf(e.target.value)} />
{mode === "score" ? ( <>
评分因子与权重
{factorRows.map((r, i) => (
{ const next = [...factorRows]; next[i] = { ...next[i], weight: Number(e.target.value) }; setFactorRows(next); }} />
))}
setTopN(Number(e.target.value))} />
) : ( <>
条件(全部满足才入选)
{conds.map((c, i) => { const meta = FIELD_OPTIONS.find((f) => f.value === (c.field === "close" && c.ref ? "close_ma60_ref" : c.field)); const isRef = c.field === "close" && c.ref === "ma60"; return (
{!isRef ? ( { const next = [...conds]; const raw = e.target.value; const val = c.op === "in" ? raw.split(/[,,]/).map((s) => s.trim()).filter(Boolean) : (meta?.kind === "num" && !c.field.startsWith("static.") ? Number(raw) : raw); next[i] = { ...next[i], value: val as never }; setConds(next); }} /> ) : null}
); })} )}
{running ? "筛选中…" : "执行选股"}
{error ? (
{error}
) : null}
{!result && !running && history === null ? : null} {result ? : null} {history && history.length > 0 ? (
{history.map((m) => ( ))}
) : null} ); } function ResultView({ result }: { result: SelectionResult }) { const s = result.statistics; if (result.candidates.length === 0) { return ( ); } return ( 范围 {s.universe_size} 评分 {s.evaluated} 选出 {s.selected} } >
{result.candidates.map((c) => ( ))}
# 代码 得分 因子值 入选理由
{c.rank} {c.symbol} {c.score.toFixed(4)} {Object.entries(c.factor_values) .map(([k, v]) => `${k}=${typeof v === "number" ? v.toFixed(4) : v}`) .join(" ")}
{c.selection_reason.map((r) => ( {r.slice(0, 40)} ))}
{result.unimplemented.length ? (
{result.unimplemented.join(";")}
) : null}
); }