diff --git a/frontend/src/api/factor.ts b/frontend/src/api/factor.ts index b5d2337..7dff52b 100644 --- a/frontend/src/api/factor.ts +++ b/frontend/src/api/factor.ts @@ -34,6 +34,40 @@ export function reportUrl(taskId: string, factor: string): string { return `/api/v1/task/${taskId}/report/${factor}?token=${encodeURIComponent(auth.token ?? '')}` } +// —— Tears 分层序列(方案A:分析时序列化,前端 ECharts 暗色渲染) —— + +export interface TearsPeriodData { + count: number + ic_mean: number | null + ic_std: number | null + icir: number | null + t_stat: number | null + win_rate: number | null + ic_dates: string[] + ic_values: number[] + monthly_ic: MonthlyIcPoint[] + quantile_keys: string[] + nav_dates: string[] + quantile_nav: Record + quantile_annual: Record + ls_nav: number[] + ls_annual: number + ls_max_dd: number +} + +export interface TearsData { + factor: string + generated_at?: string + factor_autocorr: number | null + periods: Record +} + +export async function getTearsData(taskId: string, factor: string): Promise { + const { data } = await apiClient.get(`/task/${taskId}/tears/${factor}`) + return data +} + + // —— Factor Evaluation API —— export interface EvalRun { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index bfa8fe1..d5d9cb8 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -24,6 +24,7 @@ const routes: RouteRecordRaw[] = [ { path: 'backtest/history', name: 'bt-history', component: () => import('@/views/backtest/History.vue') }, { path: 'factor/leaderboard', name: 'fc-leaderboard', component: () => import('@/views/factor/Leaderboard.vue') }, { path: 'factor/leaderboard/:factor', name: 'fc-leaderboard-detail', component: () => import('@/views/factor/LeaderboardDetail.vue') }, + { path: 'factor/compare', name: 'fc-compare', component: () => import('@/views/factor/Compare.vue') }, { path: 'factor/batch', name: 'fc-batch', component: () => import('@/views/factor/BatchEval.vue') }, { path: 'factor/new', name: 'fc-new', component: () => import('@/views/factor/New.vue') }, { path: 'factor/progress/:id', name: 'fc-progress', component: () => import('@/views/backtest/Progress.vue') }, diff --git a/frontend/src/stores/factorCompare.ts b/frontend/src/stores/factorCompare.ts new file mode 100644 index 0000000..0ac6d86 --- /dev/null +++ b/frontend/src/stores/factorCompare.ts @@ -0,0 +1,69 @@ +// 因子对比托盘(设计稿②):排行榜行内/详情页加入,2~6 个起对比,localStorage 持久化防刷新丢 +import { defineStore } from 'pinia' + +const LS_KEY = 'factorCompare.v1' +const MAX = 6 + +// 线色板(设计稿 FCOL 同款):按加入顺序分配,托盘 chips 与对比页各图共用 +const PALETTE = ['#00e5ff', '#b48cff', '#ffb000', '#5f8fd9', '#d98fd9', '#2ee68a'] + +function loadPersisted(): { factors: string[]; runId: string } { + try { + const raw = localStorage.getItem(LS_KEY) + if (!raw) return { factors: [], runId: '' } + const parsed = JSON.parse(raw) as { factors?: unknown; runId?: unknown } + const factors = Array.isArray(parsed.factors) + ? parsed.factors.filter((f): f is string => typeof f === 'string').slice(0, MAX) + : [] + return { factors, runId: typeof parsed.runId === 'string' ? parsed.runId : '' } + } catch { + return { factors: [], runId: '' } + } +} + +interface FactorCompareState { + factors: string[] + runId: string +} + +export const useFactorCompareStore = defineStore('factorCompare', { + state: (): FactorCompareState => loadPersisted(), + getters: { + count: (state): number => state.factors.length, + canCompare: (state): boolean => state.factors.length >= 2, + isFull: (state): boolean => state.factors.length >= MAX, + }, + actions: { + isSelected(factor: string): boolean { + return this.factors.includes(factor) + }, + /** 加入/移除;满了返回 false(调用方提示) */ + toggle(factor: string, runId?: string): boolean { + if (this.factors.includes(factor)) { + this.factors = this.factors.filter((f) => f !== factor) + } else { + if (this.factors.length >= MAX) return false + this.factors = [...this.factors, factor] + if (runId) this.runId = runId + } + this.persist() + return true + }, + remove(factor: string): void { + this.factors = this.factors.filter((f) => f !== factor) + this.persist() + }, + clear(): void { + this.factors = [] + this.runId = '' + this.persist() + }, + colorOf(factor: string): string { + const i = this.factors.indexOf(factor) + return i >= 0 ? PALETTE[i % PALETTE.length] : PALETTE[0] + }, + persist(): void { + localStorage.setItem(LS_KEY, JSON.stringify({ factors: this.factors, runId: this.runId })) + }, + }, +}) diff --git a/frontend/src/views/Layout.vue b/frontend/src/views/Layout.vue index d616761..55e9cea 100644 --- a/frontend/src/views/Layout.vue +++ b/frontend/src/views/Layout.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' +import CompareTray from '@/views/factor/CompareTray.vue' const router = useRouter() const route = useRoute() @@ -234,6 +235,9 @@ function onLogout(): void { + + +
diff --git a/frontend/src/views/factor/Compare.vue b/frontend/src/views/factor/Compare.vue new file mode 100644 index 0000000..09cee3b --- /dev/null +++ b/frontend/src/views/factor/Compare.vue @@ -0,0 +1,668 @@ + + + + + diff --git a/frontend/src/views/factor/CompareTray.vue b/frontend/src/views/factor/CompareTray.vue new file mode 100644 index 0000000..b6d5cc7 --- /dev/null +++ b/frontend/src/views/factor/CompareTray.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/views/factor/Leaderboard.vue b/frontend/src/views/factor/Leaderboard.vue index 6793424..7313f05 100644 --- a/frontend/src/views/factor/Leaderboard.vue +++ b/frontend/src/views/factor/Leaderboard.vue @@ -1,9 +1,12 @@ + +