mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
feat(definitions): 统一定义选择器前端 + 冒烟 API 按 id 复检 + 备份导入冒烟展示
- POST /api/definitions/smoke 支持 id 直查已存定义 (smoke_stored), 统一选择器逐条冒烟无需回传原始定义; 不存在 fail-closed 404 - DefinitionPicker 可复用组件: 跨 kind 目录 (21 内置信号实测) + kind 筛选/搜索/依赖 chips/预热/坏定义隔离展示 + 逐条冒烟 - 设置页新增「定义目录」页签; Backup 导入结果新增「导入冒烟」小节 (apply 响应 smoke 逐条渲染, 失败条目带 issue 编码) - 验收: pnpm build 过; 浏览器 E2E 导出→覆盖导入 275 项→ 冒烟 21/21 通过零 issue; 后端 36 契约测试过 Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
@@ -15,7 +15,10 @@ router = APIRouter(prefix="/api/definitions", tags=["definitions"])
|
||||
|
||||
class SmokeRequest(BaseModel):
|
||||
kind: str = Field(pattern="^(signal|factor)$")
|
||||
definition: dict
|
||||
definition: dict | None = None
|
||||
# 二选一: 传 id 时按存储重查该定义复检 (统一选择器的"逐条冒烟"),
|
||||
# 不必先把原始定义回传服务端。
|
||||
id: str | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
@@ -26,6 +29,12 @@ def list_definitions(request: Request) -> dict:
|
||||
@router.post("/smoke")
|
||||
def smoke_definition(req: SmokeRequest) -> dict:
|
||||
"""导入冒烟: 返回 ok/issues/dependencies/warmup_bars, 不落盘。"""
|
||||
if req.id is not None:
|
||||
return defs.smoke_stored(req.kind, req.id, _data_dir(None))
|
||||
if req.definition is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=422, detail="definition 与 id 必须提供其一")
|
||||
return defs.smoke_definition(req.kind, req.definition)
|
||||
|
||||
|
||||
|
||||
@@ -269,6 +269,25 @@ def smoke_definition(kind: str, definition: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def smoke_stored(kind: str, definition_id: str, data_dir: Path) -> dict:
|
||||
"""按 id 复检已存定义 (统一选择器逐条冒烟); 不存在时 fail-closed 报错。"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.factors import store as factor_store
|
||||
from app.strategy import custom_signals
|
||||
|
||||
if kind == "signal":
|
||||
definitions = custom_signals.load_all(data_dir)
|
||||
elif kind == "factor":
|
||||
definitions = factor_store.load_all(data_dir)
|
||||
else:
|
||||
raise HTTPException(status_code=422, detail=f"未知 kind: {kind}")
|
||||
for definition in definitions:
|
||||
if definition.get("id") == definition_id:
|
||||
return smoke_definition(kind, definition)
|
||||
raise HTTPException(status_code=404, detail=f"定义不存在: {kind}/{definition_id}")
|
||||
|
||||
|
||||
def catalog(data_dir: Path) -> list[dict]:
|
||||
"""全部已存定义的统一目录视图 (signal + factor), 供统一选择器消费。"""
|
||||
from app.factors import store as factor_store
|
||||
|
||||
@@ -15,7 +15,7 @@ import polars as pl
|
||||
import pytest
|
||||
|
||||
from app.config import settings
|
||||
from app.definitions import catalog, smoke_definition
|
||||
from app.definitions import catalog, smoke_definition, smoke_stored
|
||||
from app.services import config_backup
|
||||
|
||||
|
||||
@@ -60,6 +60,20 @@ def test_smoke_ok_for_valid_signal_and_factor() -> None:
|
||||
assert factor["warmup_bars"] == 6
|
||||
|
||||
|
||||
def test_smoke_stored_by_id_roundtrip(tmp_path: Path):
|
||||
"""统一选择器逐条冒烟: 按 id 从存储复检, 不存在时 fail-closed 404。"""
|
||||
from app.strategy import custom_signals
|
||||
|
||||
custom_signals.save_one(tmp_path, {**_GOOD_SIGNAL})
|
||||
result = smoke_stored("signal", "smoke_ok", tmp_path)
|
||||
assert result["ok"] is True
|
||||
|
||||
import fastapi
|
||||
with pytest.raises(fastapi.HTTPException) as ei:
|
||||
smoke_stored("signal", "missing_id", tmp_path)
|
||||
assert ei.value.status_code == 404
|
||||
|
||||
|
||||
def test_smoke_flags_typos_and_invalid_definitions() -> None:
|
||||
typo_signal = smoke_definition("signal", _TYPO_SIGNAL)
|
||||
assert not typo_signal["ok"]
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 统一定义选择器 — 跨 kind (信号/因子) 的目录浏览、选取与逐条冒烟 (P4)。
|
||||
*
|
||||
* 数据: GET /api/definitions (统一目录视图; 坏定义不阻断目录, 单条携带 error)。
|
||||
* 冒烟: POST /api/definitions/smoke 按 id 复检 (校验 → 编译 → 依赖可解析),
|
||||
* 导入/编辑后的定义当场验证, 不必跑一次回测才发现拼错列名。
|
||||
*
|
||||
* onSelect 提供时行可选中 (供表单嵌入); 否则为纯浏览器。
|
||||
*/
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { FlaskConical, Loader2, Search, SearchX } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import type { DefinitionsCatalogItem, DefinitionSmokeResult } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
|
||||
const KIND_LABEL: Record<string, string> = { signal: '信号', factor: '因子' }
|
||||
const LANG_LABEL: Record<string, string> = { conditions: '条件', dsl: 'DSL', python: 'Python' }
|
||||
|
||||
type Kind = 'signal' | 'factor'
|
||||
|
||||
/** 依赖列 chips: 超出 3 个折叠为 +N, 悬停看全量。 */
|
||||
function DepChips({ deps }: { deps: string[] }) {
|
||||
if (deps.length === 0) return <span className="text-[11px] text-muted">无依赖</span>
|
||||
const visible = deps.slice(0, 3)
|
||||
const rest = deps.length - visible.length
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-1" title={deps.join(', ')}>
|
||||
{visible.map((d) => (
|
||||
<span key={d} className="rounded-full bg-elevated px-1.5 py-px text-[10px] text-muted">{d}</span>
|
||||
))}
|
||||
{rest > 0 && <span className="text-[10px] text-muted">+{rest}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 单条冒烟结果: ok 绿勾 / issues 逐条红字。 */
|
||||
function SmokeResultView({ r }: { r: DefinitionSmokeResult }) {
|
||||
if (r.ok) {
|
||||
return (
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-accent">
|
||||
<span>冒烟通过</span>
|
||||
{r.warmup_bars != null && <span className="text-muted">预热 {r.warmup_bars} bars</span>}
|
||||
{r.dependencies.length > 0 && (
|
||||
<span className="text-muted">依赖 {r.dependencies.join(', ')}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="mt-1.5 space-y-0.5">
|
||||
{r.issues.map((i, idx) => (
|
||||
<div key={idx} className="text-[11px] text-danger">
|
||||
<span className="rounded bg-danger/10 px-1 py-px font-mono text-[10px]">{i.code}</span> {i.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DefinitionPicker({ kinds = ['signal', 'factor'], onSelect, selectedId }: {
|
||||
kinds?: ReadonlyArray<Kind>
|
||||
onSelect?: (item: DefinitionsCatalogItem) => void
|
||||
selectedId?: string
|
||||
}) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [kindFilter, setKindFilter] = useState<Kind | 'all'>(kinds.length === 1 ? kinds[0] : 'all')
|
||||
const [smokeOf, setSmokeOf] = useState<string | null>(null) // 当前展开冒烟结果的 id
|
||||
|
||||
const listQ = useQuery({
|
||||
queryKey: QK.definitions,
|
||||
queryFn: () => api.definitionsList(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const smokeM = useMutation({
|
||||
mutationFn: (item: DefinitionsCatalogItem) => api.definitionsSmoke(item.kind, item.id),
|
||||
onSuccess: (_r, item) => setSmokeOf(item.id),
|
||||
})
|
||||
|
||||
const items = useMemo(() => {
|
||||
const all = listQ.data?.items ?? []
|
||||
const kw = search.trim().toLowerCase()
|
||||
return all.filter((it) =>
|
||||
(kindFilter === 'all' || it.kind === kindFilter)
|
||||
&& (!kw || it.id.toLowerCase().includes(kw) || it.label.toLowerCase().includes(kw)),
|
||||
)
|
||||
}, [listQ.data, kindFilter, search])
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const all = listQ.data?.items ?? []
|
||||
return {
|
||||
signal: all.filter((it) => it.kind === 'signal').length,
|
||||
factor: all.filter((it) => it.kind === 'factor').length,
|
||||
}
|
||||
}, [listQ.data])
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
{/* 过滤行: kind chips + 搜索框 */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{kinds.length > 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{(['all', ...kinds] as const).map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => setKindFilter(k)}
|
||||
className={`rounded-full px-2.5 py-1 text-[11px] transition-colors ${
|
||||
kindFilter === k
|
||||
? 'bg-accent/15 text-accent'
|
||||
: 'text-muted hover:bg-elevated hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{k === 'all' ? `全部 ${counts.signal + counts.factor}` : `${KIND_LABEL[k]} ${counts[k]}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative ml-auto min-w-0 flex-1 sm:max-w-56">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="搜索 id 或名称…"
|
||||
className="w-full rounded-btn border border-border bg-surface py-1.5 pl-8 pr-2.5 text-xs text-foreground placeholder:text-muted focus:border-accent/40 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 目录列表 */}
|
||||
{listQ.isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-8 text-xs text-muted">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> 加载定义目录…
|
||||
</div>
|
||||
) : listQ.isError ? (
|
||||
<div className="py-8 text-center text-xs text-danger">定义目录加载失败, 请刷新重试</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-1.5 py-8 text-xs text-muted">
|
||||
<SearchX className="h-4 w-4" />
|
||||
{search || kindFilter !== 'all' ? '没有匹配的定义' : '暂无自定义信号 / 因子 — 可在信号库或因子库中创建, 或从配置备份导入'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{items.map((it) => {
|
||||
const selectable = !!onSelect
|
||||
const selected = selectedId === it.id
|
||||
const smoking = smokeM.isPending && smokeM.variables?.id === it.id
|
||||
return (
|
||||
<div
|
||||
key={`${it.kind}:${it.id}`}
|
||||
onClick={selectable ? () => onSelect!(it) : undefined}
|
||||
className={`rounded-btn border px-3 py-2 transition-colors ${
|
||||
selected
|
||||
? 'border-accent/40 bg-accent/[0.06]'
|
||||
: selectable
|
||||
? 'cursor-pointer border-border/70 hover:border-accent/25 hover:bg-elevated/40'
|
||||
: 'border-border/70'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`shrink-0 rounded-full px-1.5 py-px text-[10px] ${
|
||||
it.kind === 'signal' ? 'bg-accent/10 text-accent' : 'bg-secondary/10 text-secondary'
|
||||
}`}>
|
||||
{KIND_LABEL[it.kind]}
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-xs font-medium text-foreground" title={it.id}>
|
||||
{it.label}
|
||||
</span>
|
||||
<span className="shrink-0 truncate font-mono text-[10px] text-muted" title={it.id}>{it.id}</span>
|
||||
<span className="ml-auto flex shrink-0 items-center gap-1.5">
|
||||
{it.lang && LANG_LABEL[it.lang] && (
|
||||
<span className="rounded-full bg-elevated px-1.5 py-px text-[10px] text-muted">{LANG_LABEL[it.lang]}</span>
|
||||
)}
|
||||
{it.timeframe && it.timeframe !== 'daily' && (
|
||||
<span className="rounded-full bg-elevated px-1.5 py-px text-[10px] text-muted">{it.timeframe}</span>
|
||||
)}
|
||||
{it.kind === 'signal' && it.enabled === false && (
|
||||
<span className="rounded-full bg-warning/10 px-1.5 py-px text-[10px] text-warning">已停用</span>
|
||||
)}
|
||||
{it.kind === 'factor' && it.status && it.status !== 'active' && (
|
||||
<span className="rounded-full bg-elevated px-1.5 py-px text-[10px] text-muted">{it.status}</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); smokeM.mutate(it) }}
|
||||
disabled={smoking}
|
||||
title="冒烟: 校验 → 编译 → 依赖可解析 (不跑回测)"
|
||||
className="inline-flex items-center gap-1 rounded-btn border border-border px-2 py-1 text-[10px] text-muted transition-colors hover:border-accent/30 hover:text-accent disabled:opacity-50"
|
||||
>
|
||||
{smoking ? <Loader2 className="h-3 w-3 animate-spin" /> : <FlaskConical className="h-3 w-3" />}
|
||||
冒烟
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 元信息行: 依赖 / 预热 / 坏定义错误 */}
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
{it.error ? (
|
||||
<span className="text-[11px] text-danger" title={it.error}>定义异常: {it.error}</span>
|
||||
) : (
|
||||
<>
|
||||
<DepChips deps={it.dependencies ?? []} />
|
||||
{it.warmup_bars != null && (
|
||||
<span className="text-[10px] text-muted">预热 {it.warmup_bars} bars</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 冒烟结果 (仅最近一条展开, 避免列表抖动) */}
|
||||
{smokeOf === it.id && smokeM.variables?.id === it.id && smokeM.data && (
|
||||
<div className="mt-1.5 border-t border-border/60 pt-1.5">
|
||||
<SmokeResultView r={smokeM.data} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2173,6 +2173,32 @@ export interface ConfigBackupApplyCategory {
|
||||
imported: number
|
||||
skipped_conflict: number
|
||||
failed: { file: string; error: string }[]
|
||||
/** 导入即冒烟 (仅 custom_signals/custom_factors): 逐条校验+编译+依赖检查结果 */
|
||||
smoke?: (DefinitionSmokeResult & { file: string })[]
|
||||
}
|
||||
|
||||
/** 统一定义目录项 (GET /api/definitions) — 信号+因子统一视图 */
|
||||
export interface DefinitionsCatalogItem {
|
||||
kind: 'signal' | 'factor'
|
||||
id: string
|
||||
label: string
|
||||
enabled?: boolean
|
||||
timeframe?: string
|
||||
lang: string
|
||||
status?: string
|
||||
group?: string
|
||||
dependencies?: string[]
|
||||
warmup_bars?: number | null
|
||||
/** 目录不因单个坏定义失败 — 坏条目携带错误原因 (隔离要求) */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 导入冒烟结果 (POST /api/definitions/smoke) */
|
||||
export interface DefinitionSmokeResult {
|
||||
ok: boolean
|
||||
issues: { code: string; message: string }[]
|
||||
dependencies: string[]
|
||||
warmup_bars: number | null
|
||||
}
|
||||
|
||||
export interface ConfigBackupApplyResult {
|
||||
@@ -3818,6 +3844,18 @@ export const api = {
|
||||
})
|
||||
},
|
||||
|
||||
// ── 统一定义目录 / 导入冒烟 (P4) ──
|
||||
|
||||
definitionsList: () =>
|
||||
request<{ items: DefinitionsCatalogItem[] }>('/api/definitions'),
|
||||
|
||||
/** 逐条冒烟: 按 id 复检已存定义 (definition 与 id 二选一) */
|
||||
definitionsSmoke: (kind: 'signal' | 'factor', id: string) =>
|
||||
request<DefinitionSmokeResult & { file?: string }>('/api/definitions/smoke', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ kind, id }),
|
||||
}),
|
||||
|
||||
analysisMenu: (id: string) =>
|
||||
request<AnalysisMenu>(`/api/analysis-menus/${encodeURIComponent(id)}`),
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ export const QK = {
|
||||
backtestCandidates: ['backtest-candidates'] as const,
|
||||
factorColumns: ['backtest-factor-columns'] as const,
|
||||
factorLibrary: (assetType: string) => ['factors-library', assetType] as const,
|
||||
/** 统一定义目录 (信号+因子, P4) — 定义选择器数据源 */
|
||||
definitions: ['definitions'] 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,
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
import { Suspense, lazy, useEffect, useState, type ComponentType } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Database, KeyRound, LayoutGrid, Radio, Sparkles, Settings2, PanelLeftClose, PanelLeftOpen, Clock3, Info, HardDriveDownload } from 'lucide-react'
|
||||
import { Database, KeyRound, LayoutGrid, Library, Radio, Sparkles, Settings2, PanelLeftClose, PanelLeftOpen, Clock3, Info, HardDriveDownload } from 'lucide-react'
|
||||
// 面板按 tab 按需加载: 8 个面板源码 270KB+, 同步打包会让设置页 chunk 膨胀
|
||||
const SettingsAIPanel = lazy(() => import('./settings/AI').then(m => ({ default: m.SettingsAIPanel })))
|
||||
const SettingsApiTokensPanel = lazy(() => import('./settings/ApiTokens').then(m => ({ default: m.SettingsApiTokensPanel })))
|
||||
const SettingsBackupPanel = lazy(() => import('./settings/Backup').then(m => ({ default: m.SettingsBackupPanel })))
|
||||
const SettingsDefinitionsPanel = lazy(() => import('./settings/Definitions').then(m => ({ default: m.SettingsDefinitionsPanel })))
|
||||
const SettingsMonitoringPanel = lazy(() => import('./settings/Monitoring').then(m => ({ default: m.SettingsMonitoringPanel })))
|
||||
const SettingsPageAdminPanel = lazy(() => import('./settings/PageAdmin').then(m => ({ default: m.SettingsPageAdminPanel })))
|
||||
const SettingsTimeoutPanel = lazy(() => import('./settings/Timeout').then(m => ({ default: m.SettingsTimeoutPanel })))
|
||||
@@ -38,6 +39,7 @@ const TABS: readonly TabDef[] = [
|
||||
{ key: 'pages', label: '页面管理', icon: LayoutGrid, panel: SettingsPageAdminPanel },
|
||||
{ key: 'timeout', label: '网络设置', icon: Clock3, panel: SettingsTimeoutPanel },
|
||||
{ key: 'api-tokens', label: '开放接口', icon: KeyRound, panel: SettingsApiTokensPanel },
|
||||
{ key: 'definitions', label: '定义目录', icon: Library, panel: SettingsDefinitionsPanel },
|
||||
{ key: 'backup', label: '配置备份', icon: HardDriveDownload, panel: SettingsBackupPanel },
|
||||
{ key: 'system', label: '系统设置', icon: Settings2, panel: SettingsSystemPanel },
|
||||
{ key: 'about', label: '关于', icon: Info, panel: SettingsAboutPanel },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { CheckCircle2, Download, FileArchive, RotateCcw, ShieldAlert, TriangleAlert, Upload } from 'lucide-react'
|
||||
import { CheckCircle2, Download, FileArchive, FlaskConical, RotateCcw, ShieldAlert, TriangleAlert, Upload } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
import type { ConfigBackupApplyResult, ConfigBackupInspect } from '@/lib/api'
|
||||
import { toast } from '@/components/Toast'
|
||||
@@ -478,6 +478,36 @@ export function SettingsBackupPanel() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 导入即冒烟: 信号/因子逐条 校验→编译→依赖可解析 结果 */}
|
||||
{applyResult.categories.some((c) => (c.smoke?.length ?? 0) > 0) && (
|
||||
<div className="space-y-1.5 border-t border-border/60 pt-2">
|
||||
<div className="flex items-center gap-1.5 text-[11px] font-medium text-secondary">
|
||||
<FlaskConical className="h-3.5 w-3.5 text-accent" />
|
||||
导入冒烟
|
||||
<span className="font-normal text-muted">校验 → 编译 → 依赖可解析 (不跑回测)</span>
|
||||
</div>
|
||||
{applyResult.categories.flatMap((c) =>
|
||||
(c.smoke ?? []).map((sm) => (
|
||||
<div key={`${c.key}:${sm.file}`} className="rounded-btn bg-elevated/50 px-2.5 py-1.5">
|
||||
<div className="flex items-center gap-1.5 text-[11px]">
|
||||
{sm.ok
|
||||
? <CheckCircle2 className="h-3 w-3 shrink-0 text-accent" />
|
||||
: <TriangleAlert className="h-3 w-3 shrink-0 text-danger" />}
|
||||
<span className="truncate font-mono text-muted" title={sm.file}>{sm.file}</span>
|
||||
{sm.ok && sm.warmup_bars != null && (
|
||||
<span className="ml-auto shrink-0 text-muted">预热 {sm.warmup_bars} bars</span>
|
||||
)}
|
||||
</div>
|
||||
{!sm.ok && sm.issues.map((i, idx) => (
|
||||
<div key={idx} className="mt-0.5 pl-4.5 text-[11px] text-danger">
|
||||
<span className="rounded bg-danger/10 px-1 py-px font-mono text-[10px]">{i.code}</span> {i.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(applyResult.skipped_unselected > 0 || applyResult.out_of_scope_files > 0) && (
|
||||
<div className="space-y-0.5 text-[11px] text-muted">
|
||||
{applyResult.skipped_unselected > 0 && (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 统一定义目录面板 — "一切皆扩展"的消费面 (P4)。
|
||||
*
|
||||
* 跨 kind (信号/因子) 的统一目录 + 逐条导入冒烟。定义 (信号/因子/策略/
|
||||
* 数据集) 都走同一条 契约校验 → 编译 → 依赖可解析 链路; 这里是它们的
|
||||
* 总目录与体检入口, 也是配置备份导入后核对冒烟结果的落点。
|
||||
*/
|
||||
import { Library } from 'lucide-react'
|
||||
import { DefinitionPicker } from '@/components/DefinitionPicker'
|
||||
|
||||
export function SettingsDefinitionsPanel() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
<div className="rounded-card border border-border bg-surface p-4">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-btn bg-accent/10 text-accent">
|
||||
<Library className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">统一定义目录</div>
|
||||
<div className="mt-0.5 text-[11px] leading-relaxed text-muted">
|
||||
自定义信号与因子的统一视图 (语言 / 依赖 / 预热 / 状态)。
|
||||
「冒烟」当场执行 校验 → 编译 → 依赖可解析, 拼错列名 (W001)
|
||||
无需跑回测即可发现; 导入配置备份时同一检查自动执行。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<DefinitionPicker kinds={['signal', 'factor']} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user