"use client"; /** Stock Research Page(v3 §20.4):单只股票的统一研究视图。 * K 线(chart API,可切换不复权/前复权显示)+ 成交量 + MA + * 该股成交(fills)与未成交信号(signals)标记 + 最近选股/信号历史(点击看原因)。 * 全部数据来自 Chart API —— 前端不重算。 */ import { useEffect, useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { apiGet } from "@/lib/api"; import type { ChartMarker, ChartResult, Stock } from "@/lib/types"; import { PageHeader, Card, Pill, Btn, Banner, Empty, SkeletonLines, } from "@/components/ui"; import type { CandleDatum } from "@/components/StockChart/chartTypes"; import { StockChart, type ChartLibrary } from "@/components/StockChart/StockChart"; export default function StockResearchPage() { const params = useParams<{ symbol: string }>(); const symbol = (params.symbol ?? "").toUpperCase(); const [stock, setStock] = useState(null); const [chart, setChart] = useState(null); const [signalMarkers, setSignalMarkers] = useState([]); const [selectionMarkers, setSelectionMarkers] = useState([]); const [adjust, setAdjust] = useState<"none" | "qfq" | "hfq">("none"); const [chartLib, setChartLib] = useState("lightweight"); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); useEffect(() => { let alive = true; setLoading(true); setError(""); Promise.all([ apiGet(`/stocks/${symbol}`), apiGet(`/stocks/${symbol}/chart?adjust=${adjust}`), apiGet(`/stocks/${symbol}/signals`), apiGet(`/stocks/${symbol}/selections`), ]) .then(([st, ch, sig, sel]) => { if (!alive) return; setStock(st); setChart(ch); setSignalMarkers(sig); setSelectionMarkers(sel); }) .catch((e: Error) => alive && setError(e.message)) .finally(() => alive && setLoading(false)); return () => { alive = false; }; }, [symbol, adjust]); const candles: CandleDatum[] = useMemo( () => (chart?.bars ?? []) .filter((b) => b.open != null && b.close != null && b.low != null && b.high != null) .map((b) => ({ time: b.time, open: b.open as number, close: b.close as number, low: b.low as number, high: b.high as number, })), [chart] ); return ( <> {(["echarts", "lightweight"] as const).map((l) => ( setChartLib(l)}> {l === "echarts" ? "ECharts" : "Lightweight"} ))} {(["none", "qfq", "hfq"] as const).map((m) => ( setAdjust(m)} > {m === "none" ? "不复权" : m === "qfq" ? "前复权" : "后复权"} ))} } /> {error ? {error} : null} {loading && !chart ? ( ) : ( <> 共 {chart?.metadata.bar_count ?? 0} 根 {chart?.metadata.execution_price_basis ? ( 回测执行价口径 {chart.metadata.execution_price_basis} ) : null} } > {candles.length === 0 ? ( ) : ( )} {chart && (chart.signals.length || chart.fills.length) ? ( 成交 {chart.fills.length} 未成交信号 {chart.signals.length} } >
{[...chart.signals, ...chart.fills].map((m, i) => ( {m.time} {m.kind} {m.price ? ` @${m.price}` : ""} ))}
) : null}
{selectionMarkers.length === 0 ? ( ) : (
{selectionMarkers.slice(0, 12).map((m, i) => ( {m.time} {m.text[0]} ))}
)}
{signalMarkers.length === 0 ? ( ) : (
{signalMarkers.slice(0, 12).map((m, i) => ( {m.time} {m.kind} {m.price ? ` @${m.price}` : ""} ))}
)}
)} ); }