"use client"; /** 交易信号(v2 §15):基于选股评分排名 + 趋势规则生成 BUY / WATCH / SELL, * 每条带理由(为什么);支持当前/历史 as_of,历史记录点击回看。 */ import { useEffect, useState } from "react"; import { apiGet, apiPost } from "@/lib/api"; import type { FactorMeta, SelectionQuery, SignalMeta, SignalResult, SignalRun, } from "@/lib/types"; import { PageHeader, Card, Pill, Field, Btn, Banner, Empty, SkeletonLines, } from "@/components/ui"; export default function SignalsPage() { const [factors, setFactors] = useState([]); const [factor, setFactor] = useState("momentum_60"); const [asOf, setAsOf] = useState(""); const [symbols, setSymbols] = useState("600519.SH,000001.SZ,300750.SZ,601318.SH,000858.SZ"); const [buyRank, setBuyRank] = useState(2); const [sellRank, setSellRank] = useState(5); const [result, setResult] = useState(null); const [signalId, setSignalId] = 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("/signals?limit=8").then(setHistory).catch(() => alive && setHistory([])); return () => { alive = false; }; }, []); function buildQuery(): SelectionQuery { return { universe: { exclude_st: true, min_listing_days: 0, symbols: symbols.split(/[,,]/).map((s) => s.trim()).filter(Boolean) }, as_of: asOf || null, method: "score", factors: [{ name: factor, weight: 1 }], conditions: [], top_n: sellRank + 10, }; } async function run() { setRunning(true); setError(""); setResult(null); try { const run = await apiPost("/signals", { query: buildQuery(), rules: { buy_rank_threshold: buyRank, sell_rank_threshold: sellRank, max_output_rank: 60 }, }); setSignalId(run.signal_id); setResult(run.result); apiGet("/signals?limit=8").then(setHistory).catch(() => undefined); } catch (e) { setError((e as Error).message); } finally { setRunning(false); } } async function openHistory(id: string) { try { const detail = await apiGet(`/signals/${id}`); setSignalId(id); setResult(detail); } catch (e) { setError((e as Error).message); } } return ( <> {signalId} : null} />
setBuyRank(Number(e.target.value))} /> setSellRank(Number(e.target.value))} /> setAsOf(e.target.value)} /> setSymbols(e.target.value)} /> {running ? "生成中…" : "生成信号"}
{error ?
{error}
: null}
{!result && !running && history === null ? : null} {result ? : null} {history && history.length > 0 ? (
{history.map((m) => ( ))}
) : null} ); } function ResultView({ result }: { result: SignalResult }) { const st = result.statistics; return ( BUY {st.buy} WATCH {st.watch} SELL {st.sell} } > {result.events.length === 0 ? ( ) : (
{result.events.map((e) => ( ))}
类型 代码 得分 价格 触发理由
{e.signal_type} {e.symbol} {e.score?.toFixed(4)} {e.price ?? "-"}
{e.trigger_reason.map((r) => ( {r} ))}
)}
); }