[0]) {
+ closeEvents()
+ localStorage.removeItem(ACTIVE_RUN_KEY)
+ const token = connectionToken
+ update({
+ runId: null,
+ isPending: true,
+ cancelling: false,
+ reconnecting: false,
+ run: null,
+ progress: { phase: 'screening', label: '正在全量筛选因子(约 1-3 分钟)' },
+ result: null,
+ previousResult: current.result ?? current.previousResult,
+ error: null,
+ })
+ try {
+ const data = await api.miningAutoStart(payload)
+ if (connectionToken !== token || current.runId !== null) return
+ if (data.started && data.run) {
+ const run = data.run
+ localStorage.setItem(ACTIVE_RUN_KEY, run.run_id)
+ update({
+ runId: run.run_id,
+ run,
+ progress: run.progress ?? current.progress,
+ isPending: !TERMINAL_STATES.has(run.status),
+ })
+ if (TERMINAL_STATES.has(run.status)) {
+ await refreshTerminalRun(run.run_id, run.status, run, token)
+ } else {
+ connect(run.run_id)
+ }
+ return
+ }
+ // 无达标因子: 不创建任务, 以 task.error 呈现原因分布 (挖掘页红字展示)
+ const screening = data.screening
+ const topReasons = Object.entries(screening?.reason_counts ?? {})
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 3)
+ .map(([reason, count]) => `${reason} ${count} 个`)
+ .join(';')
+ update({
+ isPending: false,
+ progress: null,
+ error: `${screening?.n_total ?? 0} 个因子里 0 个达标,未启动挖掘。`
+ + (topReasons ? `主要失败原因:${topReasons}。` : '')
+ + '可改选「探索」档(门槛更低)或拉长日期区间后重试。',
+ })
+ } catch (error) {
+ if (connectionToken !== token || current.runId !== null) return
+ update({
+ isPending: false,
+ progress: null,
+ error: String((error as Error).message || error),
+ })
+ }
+}
+
export async function cancelMining() {
if (!current.runId || !current.isPending || current.cancelling) return
const runId = current.runId
diff --git a/frontend/src/lib/queryKeys.ts b/frontend/src/lib/queryKeys.ts
index f43fb42..72df3e8 100644
--- a/frontend/src/lib/queryKeys.ts
+++ b/frontend/src/lib/queryKeys.ts
@@ -53,6 +53,7 @@ export const QK = {
// Backtest
backtestStatus: ['backtest-status'] as const,
factorColumns: ['backtest-factor-columns'] as const,
+ factorLibrary: (assetType: string) => ['factors-library', assetType] as const,
miningRuns: ['backtest-mining-runs'] as const,
miningAvailability: (assetType: string, profile: string, start: string, end: string) =>
['backtest-mining-availability', assetType, profile, start, end] as const,
diff --git a/frontend/src/pages/AbnormalMoves.tsx b/frontend/src/pages/AbnormalMoves.tsx
index bc91f70..bbd8e37 100644
--- a/frontend/src/pages/AbnormalMoves.tsx
+++ b/frontend/src/pages/AbnormalMoves.tsx
@@ -659,8 +659,9 @@ function DeviationView({ onPreview }: {
{ruleChips()}
- 口径说明: 偏离值 = 个股 N 日累计涨跌幅 − 对应指数同期涨跌幅 (沪: 上证A指/上证指数,
- 深: 深证A指/深证成指, 北: 北证50)。阈值为交易所异常波动披露标准的近似值, 仅供风险提示,
+ 口径说明: 偏离值 = 个股 N 日累计涨跌幅 − 对应指数同期涨跌幅 (沪主板: 上证A指/上证指数,
+ 科创板: 科创50, 深主板: 深证A指/深证成指, 创业板: 创业板综指, 北: 北证50)。
+ 阈值为交易所异常波动披露标准的近似值, 仅供风险提示,
不构成监管认定。每只股票在 3日/10日/30日 三档各算一个接近度 (|偏离值| ÷ 该档阈值,
阈值随板块不同; 2026-07-06 起主板风险警示股票与普通股票同口径), 表格「接近度」列与状态取三档中的最高值,
来源窗口的偏离值颜色加重显示、其余窗口淡化; ≥100% 已触发、≥70% 边缘、≥50% 观察。
diff --git a/frontend/src/pages/Backtest.tsx b/frontend/src/pages/Backtest.tsx
index 4994750..6db02c5 100644
--- a/frontend/src/pages/Backtest.tsx
+++ b/frontend/src/pages/Backtest.tsx
@@ -1,21 +1,15 @@
-import { useState } from 'react'
-import { Navigate, useSearchParams } from 'react-router-dom'
-import { BarChart3, BookmarkCheck, FlaskConical, ShieldCheck } from 'lucide-react'
+import { useEffect, useState } from 'react'
+import { Navigate, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
+import { BookmarkCheck, FlaskConical, ShieldCheck } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
-import { FactorDiscovery } from './backtest/FactorDiscovery'
import { ResearchCandidatesDialog } from './backtest/ResearchCandidatesDialog'
import { RobustnessValidation } from './backtest/RobustnessValidation'
import { StrategyBacktest } from './backtest/StrategyBacktest'
import { type ResearchCandidate } from '@/lib/api'
-type Tab = 'factor' | 'strategy' | 'robustness'
+type Tab = 'strategy' | 'robustness'
-const MODES: Record = {
- factor: {
- title: '因子',
- subtitle: '批量筛选与单因子检验',
- icon: BarChart3,
- },
+const MODES: Record = {
strategy: {
title: '策略',
subtitle: '现有策略评估与候选沉淀',
@@ -30,17 +24,34 @@ const MODES: Record(null)
- // 旧链接兼容: 挖掘已升级为一级路由 /mining, 保留 run/candidate 参数重定向
- if (requestedTab === 'mining') {
+ // 跨页「载入复测」: 因子页候选弹窗经 router state 传入 (location.state), 消费后清除防止刷新重复载入
+ const stateCandidate = (location.state as { loadCandidate?: ResearchCandidate } | null)?.loadCandidate ?? null
+ useEffect(() => {
+ if (!stateCandidate) return
+ setPendingLoad(stateCandidate)
+ navigate({ pathname: location.pathname, search: location.search }, { replace: true })
+ }, [stateCandidate, location.pathname, location.search, navigate])
+
+ // 旧链接兼容: 因子已升级为一级路由 /factors, 保留其余参数重定向
+ if (requestedTab === 'factor') {
const next = new URLSearchParams(searchParams)
next.delete('tab')
const search = next.toString()
- return
+ return
+ }
+
+ // 旧链接兼容: 挖掘已并入因子页 (/factors?tab=mining), 保留 run/candidate 参数重定向
+ if (requestedTab === 'mining') {
+ const next = new URLSearchParams(searchParams)
+ next.set('tab', 'mining')
+ return
}
const activeTab: Tab = requestedTab && requestedTab in MODES
@@ -101,7 +112,6 @@ export function Backtest() {
/>
- {activeTab === 'factor' && }
{activeTab === 'strategy' && (
= {
+ inspect: { title: '检验', icon: Sigma },
+ library: { title: '因子库', icon: LibrarySquare },
+ editor: { title: '编辑器', icon: PenLine },
+ composite: { title: '组合', icon: Combine },
+ mining: { title: '挖掘', icon: Pickaxe },
+}
+
+export function Factors() {
+ const [searchParams, setSearchParams] = useSearchParams()
+ const [candidatesOpen, setCandidatesOpen] = useState(false)
+ const navigate = useNavigate()
+
+ const requestedTab = searchParams.get('tab')
+ const activeTab: Tab = requestedTab === 'library' || requestedTab === 'editor' || requestedTab === 'composite' || requestedTab === 'mining'
+ ? requestedTab
+ : 'inspect'
+ const focusFactor = activeTab === 'inspect' ? searchParams.get('focus') ?? '' : ''
+
+ const changeTab = (tab: Tab) => {
+ const next = new URLSearchParams(searchParams)
+ next.set('tab', tab)
+ next.delete('focus') // 切 tab 清除单因子聚焦
+ next.delete('edit') // 切 tab 清除编辑器载入目标
+ setSearchParams(next, { replace: true })
+ }
+
+ return (
+
+
{activeTab === 'library' ? '因子注册表目录' : activeTab === 'mining' ? '嵌套样本外因子与策略挖掘' : '因子研究与检验'}}
+ className="shrink-0 flex-wrap gap-x-4 gap-y-2 bg-base/95 px-3 lg:flex-nowrap lg:px-5"
+ right={(
+
+
+
+
+
+ )}
+ />
+
+
+ {activeTab === 'inspect' && }
+ {activeTab === 'editor' && }
+ {activeTab === 'composite' && }
+ {activeTab === 'mining' && (
+
+
+
+ )}
+ {activeTab === 'library' && (
+ {
+ const next = new URLSearchParams(searchParams)
+ next.set('tab', 'inspect')
+ next.set('focus', factorId)
+ setSearchParams(next)
+ }}
+ onEdit={factorId => {
+ const next = new URLSearchParams(searchParams)
+ next.set('tab', 'editor')
+ next.set('edit', factorId)
+ setSearchParams(next)
+ }}
+ />
+ )}
+
+
+ {candidatesOpen && (
+ setCandidatesOpen(false)}
+ onLoadStrategy={candidate => {
+ setCandidatesOpen(false)
+ // 跨页「载入复测」: 候选经 router state 交给回测策略页消费
+ navigate('/backtest?tab=strategy', { state: { loadCandidate: candidate } })
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/Mining.tsx b/frontend/src/pages/Mining.tsx
deleted file mode 100644
index 9c7fba2..0000000
--- a/frontend/src/pages/Mining.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useState } from 'react'
-import { BookmarkCheck } from 'lucide-react'
-import { PageHeader } from '@/components/PageHeader'
-import { MiningWorkbench } from './backtest/MiningWorkbench'
-import { ResearchCandidatesDialog } from './backtest/ResearchCandidatesDialog'
-
-export function Mining() {
- const [candidatesOpen, setCandidatesOpen] = useState(false)
-
- return (
-
-
嵌套样本外因子与策略挖掘}
- className="shrink-0 flex-wrap gap-x-4 gap-y-2 bg-base/95 px-3 lg:flex-nowrap lg:px-5"
- right={(
-
- )}
- />
-
-
-
-
-
- {candidatesOpen && setCandidatesOpen(false)} />}
-
- )
-}
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index f0ccb05..49ff2f9 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -6,14 +6,13 @@
import { useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { motion } from 'framer-motion'
-import { BarChart3, Database, Radio, SlidersHorizontal, Sparkles, Settings2, Zap, PanelLeftClose, PanelLeftOpen, Clock3 } from 'lucide-react'
+import { BarChart3, Database, Radio, SlidersHorizontal, Sparkles, Settings2, PanelLeftClose, PanelLeftOpen, Clock3 } from 'lucide-react'
import { SettingsAIPanel } from './settings/AI'
import { SettingsMonitoringPanel } from './settings/Monitoring'
import { SettingsExtPagesPanel } from './settings/ExtPages'
import { SettingsMenuSettingsPanel } from './settings/MenuSettings'
import { SettingsTimeoutPanel } from './settings/Timeout'
import { SettingsSystemPanel } from './settings/System'
-import { SettingsCustomSignalsPanel } from './settings/CustomSignals'
import { SettingsDataSourcesPanel } from './settings/DataSources'
import { PageHeader } from '@/components/PageHeader'
import { cn } from '@/lib/cn'
@@ -35,7 +34,6 @@ const TABS: readonly TabDef[] = [
{ key: 'ai', label: 'AI 设置', icon: Sparkles, panel: SettingsAIPanel },
{ key: 'monitoring', label: '实时监控', icon: Radio, panel: SettingsMonitoringPanel },
{ key: 'ext-pages', label: '扩展页面', icon: BarChart3, panel: SettingsExtPagesPanel },
- { key: 'signals', label: '信号库', icon: Zap, panel: SettingsCustomSignalsPanel },
{ key: 'timeout', label: '网络设置', icon: Clock3, panel: SettingsTimeoutPanel },
{ key: 'menus', label: '菜单设置', icon: SlidersHorizontal, panel: SettingsMenuSettingsPanel },
{ key: 'system', label: '系统设置', icon: Settings2, panel: SettingsSystemPanel },
diff --git a/frontend/src/pages/settings/CustomSignals.tsx b/frontend/src/pages/Signals.tsx
similarity index 54%
rename from frontend/src/pages/settings/CustomSignals.tsx
rename to frontend/src/pages/Signals.tsx
index c19b12d..eb075a8 100644
--- a/frontend/src/pages/settings/CustomSignals.tsx
+++ b/frontend/src/pages/Signals.tsx
@@ -1,11 +1,13 @@
import { useEffect, useRef, useState } from 'react'
+import { useSearchParams } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { Plus, Trash2, Zap, Settings2, Lock } from 'lucide-react'
+import { Lock, Plus, Settings2, Trash2, Zap } from 'lucide-react'
import { api, type CustomSignal } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { BUILTIN_SIGNAL_DEFINITIONS, type SignalKind } from '@/lib/signals'
import { CustomSignalDialog } from '@/components/signals/CustomSignalDialog'
import { Skeleton } from '@/components/data/Skeleton'
+import { PageHeader } from '@/components/PageHeader'
import { AnchorWrap } from '@/lib/useCardFlash'
type SignalSection = 'builtin' | 'custom'
@@ -17,12 +19,29 @@ const KIND_CLASS: Record = {
both: 'bg-muted/10 text-muted',
}
-export function SettingsCustomSignalsPanel({ highlight }: { highlight?: string } = {}) {
+/** 信号库独立页: 内置只读信号 + 自定义条件信号 (csg_*), 策略/回测/监控统一取用。 */
+export function Signals() {
+ const [searchParams] = useSearchParams()
+ const highlight = searchParams.get('highlight') ?? ''
+
+ return (
+
+ )
+}
+
+function SignalsBody({ highlight }: { highlight: string }) {
const qc = useQueryClient()
const list = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList })
const options = useQuery({ queryKey: QK.customSignalsOptions, queryFn: api.customSignalsOptions })
- const [activeSection, setActiveSection] = useState('builtin')
+ const [activeSection, setActiveSection] = useState('custom')
const [showForm, setShowForm] = useState(false)
const [editing, setEditing] = useState(null)
const [confirmingDeleteId, setConfirmingDeleteId] = useState(null)
@@ -31,10 +50,6 @@ export function SettingsCustomSignalsPanel({ highlight }: { highlight?: string }
const fields = options.data?.fields ?? []
const signals = list.data?.signals ?? []
const enabledCustomSignals = signals.filter(sig => sig.enabled).length
- const tabs = [
- { key: 'builtin' as const, label: '内置信号', count: BUILTIN_SIGNAL_DEFINITIONS.length, hint: '系统提供,只读' },
- { key: 'custom' as const, label: '自定义信号', count: signals.length, hint: `${enabledCustomSignals} 个已启用` },
- ]
useEffect(() => () => {
if (resetDeleteTimer.current) clearTimeout(resetDeleteTimer.current)
@@ -86,131 +101,70 @@ export function SettingsCustomSignalsPanel({ highlight }: { highlight?: string }
resetDeleteTimer.current = setTimeout(() => setConfirmingDeleteId(null), 3000)
}
+ const tabs = [
+ { key: 'custom' as const, label: '自定义信号', count: signals.length, hint: `${enabledCustomSignals} 个已启用` },
+ { key: 'builtin' as const, label: '内置信号', count: BUILTIN_SIGNAL_DEFINITIONS.length, hint: '系统预计算,只读' },
+ ]
+
return (
-
-
-
-
-
-
信号库
-
统一查看策略、回测与监控可用信号
-
- 内置信号由系统预计算,作为只读信号库展示;自定义信号可用「字段 + 运算符 + 值」组合条件创建,保存后可在策略、回测与监控中选择使用。
-
-
+
+ {/* 紧凑工具条: 分段切换 + 新建 */}
+
+
+ {tabs.map(tab => {
+ const active = activeSection === tab.key
+ return (
+
+ )
+ })}
+
+
+
自定义信号保存为 csg_* 列,条件字段可用行情指标与全部注册因子
-
-
-
-
-
-
-
-
-
- {tabs.map(tab => {
- const active = activeSection === tab.key
- return (
-
- )
- })}
-
-
-
-
-
- {activeSection === 'builtin' && (
-
-
-
-
-
-
内置信号
- 只读
-
-
这些信号由系统在 enriched 数据中预计算,策略选择器会直接展示。
-
-
ID 前缀:signal_
-
-
- {BUILTIN_SIGNAL_DEFINITIONS.map(sig => (
-
-
-
-
-
{sig.name}
-
- {KIND_LABEL[sig.kind]}
-
-
-
{sig.id}
-
-
{sig.category}
-
-
{sig.description}
-
- ))}
-
-
- )}
+
{activeSection === 'custom' && (
-
-
-
-
-
-
自定义信号
- 可配置
-
-
这些信号由你定义,可启用/停用,并在策略、回测与监控中作为 csg_* 信号使用。
-
-
-
-
-
+
+
+
{signals.map(sig => (
-
{sig.name}
+ {sig.name}
{KIND_LABEL[sig.kind]}
{!sig.enabled && 已停用}
-
csg_{sig.id}
+
csg_{sig.id}
-
-