feat: 自动候选筛选与股票研究工作台

This commit is contained in:
2026-10-09 20:39:23 +08:00
parent 92329ddce2
commit 57882ff568
32 changed files with 1343 additions and 87 deletions
@@ -0,0 +1,14 @@
import {useEffect,useState} from "react"
import {request,ApiError} from "@/lib/auth"
type Bar={date:string;open:string;high:string;low:string;close:string}
type History={source:string;adjustment:string;received_at:string;bars:Bar[]}
export function StockHistoryChart({symbol,exchange,onSessionExpired}:{symbol:string;exchange:string;onSessionExpired:()=>void}){
const [data,setData]=useState<History|null>(null),[error,setError]=useState("")
useEffect(()=>{const c=new AbortController();setData(null);setError("");const end=new Date(),start=new Date();start.setDate(start.getDate()-90);request<History>(`/api/admin/v1/market/history?symbol=${symbol}&exchange=${exchange}&start=${start.toISOString().slice(0,10)}&end=${end.toISOString().slice(0,10)}`,{signal:c.signal}).then(result=>{if(!c.signal.aborted)setData(result)}).catch(e=>{if(c.signal.aborted)return;if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"日线读取失败")});return()=>c.abort()},[symbol,exchange])
const bars=data?.bars.slice(-40)??[]
// Integer cents for price parsing; division below only maps values to SVG pixels.
const cents=(price:string)=>{const [whole,fraction=""]=price.split(".");return Number(whole)*100+Number(fraction.padEnd(2,"0"))}
const values=bars.map(b=>cents(b.close)),min=Math.min(...values),max=Math.max(...values),range=max-min||1
const points=values.map((value,i)=>`${20+i*600/Math.max(values.length-1,1)},${150-(value-min)*120/range}`).join(" ")
return <div className="rounded-xl border bg-card p-4"><div className="flex items-center justify-between"><strong className="text-sm">日线收盘走势</strong><span className="text-xs text-muted-foreground">未复权 · 最近 {bars.length} 个交易日</span></div>{error?<p className="mt-5 text-sm text-destructive">{error}</p>:!data?<p className="py-12 text-center text-sm text-muted-foreground">正在获取真实日线…</p>:bars.length===0?<p className="py-12 text-center text-sm text-muted-foreground">此范围没有可用日线。</p>:<><svg viewBox="0 0 640 185" className="mt-4 w-full" role="img" aria-label={`${exchange}:${symbol} 真实日线收盘价`}><path d="M20 30H620 M20 90H620 M20 150H620" fill="none" stroke="var(--color-border)" strokeDasharray="4 4"/><polyline points={points} fill="none" stroke="var(--primary)" strokeWidth="2.5" strokeLinejoin="round"/><text x="20" y="178" fontSize="11" fill="#79736c">{bars[0].date}</text><text x="620" y="178" textAnchor="end" fontSize="11" fill="#79736c">{bars[bars.length-1].date}</text></svg><p className="text-xs text-muted-foreground">末日收盘 ¥{bars[bars.length-1].close} · 来源 {data.source} · 接收 {new Date(data.received_at).toLocaleTimeString("zh-CN")}</p></>}</div>
}