import { useState, useMemo } from 'react' import { useQuery, useMutation } from '@tanstack/react-query' import { motion } from 'framer-motion' import { Play, BarChart3, Clock } from 'lucide-react' import { api, type FactorColumn, type FactorBacktestResult, type GroupStat } from '@/lib/api' import { fmtPct, priceColorClass } from '@/lib/format' import { EmptyState } from '@/components/EmptyState' import { DatePicker } from '@/components/DatePicker' import { FactorICChart } from './charts/FactorICChart' import { FactorGroupNavChart } from './charts/FactorGroupNavChart' const formatDate = (date: Date) => date.toISOString().slice(0, 10) const monthsAgo = (months: number) => { const date = new Date() date.setMonth(date.getMonth() - months) return formatDate(date) } const TODAY = formatDate(new Date()) const THREE_MONTHS_AGO = monthsAgo(3) const INPUT_CLS = `w-full px-2.5 py-1.5 rounded-input bg-surface border border-border text-xs focus:outline-none focus:border-accent transition-colors duration-150 ease-smooth` function StatCard({ label, value, highlight }: { label: string value: string | null | undefined highlight?: 'bull' | 'bear' | 'neutral' }) { const colorCls = highlight === 'bull' ? 'text-bull' : highlight === 'bear' ? 'text-bear' : '' return (
{label}
{value ?? '—'}
) } function LoadingPanel({ symbolsText }: { symbolsText: string }) { return (
正在计算因子分析
{symbolsText} · 完成后会一次性刷新 IC、分层收益和净值曲线。
{['读取因子', '计算 IC', '分层回测', '汇总指标'].map(item => (
{item}
))}
分层净值预览
等待后端返回完整结果
{[46, 38, 54, 50, 64, 58, 74, 68, 84, 78, 90, 86].map((h, i) => (
))}
) } export function FactorBacktest() { const [factorName, setFactorName] = useState('momentum_20d') const [symbols, setSymbols] = useState('') const [start, setStart] = useState(THREE_MONTHS_AGO) const [end, setEnd] = useState(TODAY) const [nGroups, setNGroups] = useState(5) const [weight, setWeight] = useState<'equal' | 'factor_weight'>('equal') const [fees, setFees] = useState('2') const [result, setResult] = useState(null) const columns = useQuery({ queryKey: ['backtest-factor-columns'], queryFn: api.factorColumns, }) // 按 group 分类的因子 const factorGroups = useMemo(() => { const cols = columns.data?.columns ?? [] const groups: Record = {} for (const c of cols) { ;(groups[c.group] ??= []).push(c) } return groups }, [columns.data]) // 当前因子描述 const factorDesc = useMemo(() => { return columns.data?.columns.find(c => c.id === factorName)?.desc ?? '' }, [columns.data, factorName]) const run = useMutation({ mutationFn: () => api.factorRun({ factor_name: factorName, symbols: symbols ? symbols.split(',').map(s => s.trim()).filter(Boolean) : null, start: start || null, end: end || undefined, n_groups: nGroups, rebalance: 'daily', weight, fees_pct: Number(fees) / 10000, }), onSuccess: (data) => { if (data.error) { setResult(data) } else { setResult(data) } }, }) const applyRange = (months: number) => { setStart(monthsAgo(months)) setEnd(formatDate(new Date())) } const applyAllRange = () => { setStart('') setEnd(formatDate(new Date())) } const rangeKey = end === TODAY && start === THREE_MONTHS_AGO ? '3m' : end === TODAY && start === monthsAgo(6) ? '6m' : end === TODAY && start === monthsAgo(12) ? '1y' : end === TODAY && start === '' ? 'all' : 'custom' const rangeTitle = rangeKey === '3m' ? '近 3 个月' : rangeKey === '6m' ? '近 6 个月' : rangeKey === '1y' ? '近 1 年' : rangeKey === 'all' ? '全部历史' : '自定义区间' const rangeButtonCls = (key: string) => `rounded-btn px-2 py-1 text-[11px] font-medium transition-colors ${rangeKey === key ? 'bg-accent/15 text-accent' : 'text-muted hover:bg-elevated/70 hover:text-secondary' }` return (
{/* 配置面板 */}
因子配置
选择因子、区间和分组方式。默认最近 3 个月。
{factorDesc && (

{factorDesc}

)}
setSymbols(e.target.value)} placeholder="留空则使用全市场,建议最近3个月" className={`w-full px-2.5 py-1.5 rounded-input bg-surface border border-border text-xs font-mono focus:outline-none focus:border-accent transition-colors duration-150 ease-smooth`} />
回测区间
{rangeTitle}
setFees(e.target.value)} className={INPUT_CLS} />
{/* 结果面板 */}
{result?.error && !result.ic_mean && (
{result.error}
)} {run.isError && (
{String((run.error as any).message)}
)} {!result && !run.isPending && ( )} {run.isPending && result && (
正在重新计算,当前暂时展示上一次因子分析结果,完成后会自动替换。
)} {run.isPending && !result && ( )} {result && result.ic_mean != null && ( {/* IC/IR 指标 */}

因子预测能力

Rank IC · 日度调仓 {result.elapsed_ms > 0 && ( {result.elapsed_ms.toFixed(0)} ms )}
0.03 ? 'bull' : result.ic_mean < -0.03 ? 'bear' : 'neutral' : undefined} /> 0.5 ? (result.ir > 0 ? 'bull' : 'bear') : 'neutral' : undefined} />
{/* IC 时序图 */} {result.ic_series.length > 0 && (
IC 时序
)} {/* 分层净值 */} {result.group_nav.length > 0 && (
分层净值曲线
)} {/* 分层统计表 */} {result.group_stats.length > 0 && (
{result.group_stats.map((g: GroupStat) => ( ))} {/* 多空行 */} {result.long_short_stats?.total_return != null && ( )}
分组 总收益 年化 最大回撤 夏普 胜率
{g.label} {fmtPct(g.total_return)} {fmtPct(g.annual_return)} {fmtPct(g.max_drawdown)} {g.sharpe?.toFixed(2)} {fmtPct(g.win_rate)}
多空({result.long_short_stats.top_group ?? ''}-{result.long_short_stats.bottom_group ?? ''}) {fmtPct(result.long_short_stats.total_return as number)} {fmtPct(result.long_short_stats.max_drawdown as number)}
)} {/* 数据概要 */}
{result.n_symbols} 只标的 {result.n_dates} 个交易日 run_id: {result.run_id}
)}
) }