From d0315ccbdfe1cbacb2864d8675af9d655f3053e7 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 11 Jul 2026 13:54:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E7=BB=93=E6=9E=9C=E9=A1=B5?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E2=80=94MetricCards=E6=8C=87=E6=A0=87?= =?UTF-8?q?=E5=8D=A1+5=E5=9B=BE(=E5=9F=BA=E5=87=86/Alpha/Beta/=E6=B3=A2?= =?UTF-8?q?=E5=8A=A8=E7=8E=87/=E5=9B=9E=E6=92=A4)+API=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/backtest.ts | 44 ++++++++++ .../components/backtest/AlphaChart.spec.ts | 38 ++++++++ .../src/components/backtest/AlphaChart.vue | 51 +++++++++++ .../backtest/BenchmarkCurve.spec.ts | 40 +++++++++ .../components/backtest/BenchmarkCurve.vue | 66 ++++++++++++++ .../src/components/backtest/BetaChart.spec.ts | 38 ++++++++ .../src/components/backtest/BetaChart.vue | 51 +++++++++++ .../components/backtest/DrawdownChart.spec.ts | 38 ++++++++ .../src/components/backtest/DrawdownChart.vue | 51 +++++++++++ .../components/backtest/MetricCards.spec.ts | 75 ++++++++++++++++ .../src/components/backtest/MetricCards.vue | 87 +++++++++++++++++++ .../backtest/VolatilityChart.spec.ts | 40 +++++++++ .../components/backtest/VolatilityChart.vue | 64 ++++++++++++++ 13 files changed, 683 insertions(+) create mode 100644 frontend/src/components/backtest/AlphaChart.spec.ts create mode 100644 frontend/src/components/backtest/AlphaChart.vue create mode 100644 frontend/src/components/backtest/BenchmarkCurve.spec.ts create mode 100644 frontend/src/components/backtest/BenchmarkCurve.vue create mode 100644 frontend/src/components/backtest/BetaChart.spec.ts create mode 100644 frontend/src/components/backtest/BetaChart.vue create mode 100644 frontend/src/components/backtest/DrawdownChart.spec.ts create mode 100644 frontend/src/components/backtest/DrawdownChart.vue create mode 100644 frontend/src/components/backtest/MetricCards.spec.ts create mode 100644 frontend/src/components/backtest/MetricCards.vue create mode 100644 frontend/src/components/backtest/VolatilityChart.spec.ts create mode 100644 frontend/src/components/backtest/VolatilityChart.vue diff --git a/frontend/src/api/backtest.ts b/frontend/src/api/backtest.ts index 6315ae2..892bd1b 100644 --- a/frontend/src/api/backtest.ts +++ b/frontend/src/api/backtest.ts @@ -129,3 +129,47 @@ export async function getOptimizationResults(taskId: string): Promise return data.results } +// ----- Task 5+6: Backtest result page components ----- + +export interface RelativeMetrics { + total_return: number + annual_return: number + alpha: number + beta: number + sharpe_ratio: number + sortino_ratio: number + information_ratio: number + annual_volatility: number + max_drawdown: number + benchmark_return: number + benchmark_volatility: number +} + +export interface BenchmarkCurveData { + dates: string[] + strategy: number[] + benchmark: number[] +} + +export interface RiskSeriesData { + dates: string[] + alpha: number[] + beta: number[] + drawdown: number[] +} + +export async function getRelativeMetrics(taskId: string): Promise { + const { data } = await apiClient.get<{ relative_metrics: RelativeMetrics }>(`/task/${taskId}/result`) + return data.relative_metrics +} + +export async function getBenchmarkCurve(taskId: string): Promise { + const { data } = await apiClient.get(`/task/${taskId}/benchmark-curve`) + return data +} + +export async function getRiskSeries(taskId: string): Promise { + const { data } = await apiClient.get(`/task/${taskId}/risk-series`) + return data +} + diff --git a/frontend/src/components/backtest/AlphaChart.spec.ts b/frontend/src/components/backtest/AlphaChart.spec.ts new file mode 100644 index 0000000..5480b6a --- /dev/null +++ b/frontend/src/components/backtest/AlphaChart.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import AlphaChart from './AlphaChart.vue' + +// Mock echarts to avoid canvas issues in jsdom +vi.mock('echarts', () => ({ + init: vi.fn(() => ({ + setOption: vi.fn(), + dispose: vi.fn(), + resize: vi.fn(), + })), +})) + +describe('AlphaChart.vue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders chart container without crashing', () => { + const props = { + dates: ['2024-01-01', '2024-01-02', '2024-01-03'], + alpha: [0.02, 0.025, 0.03], + } + + const wrapper = mount(AlphaChart, { props }) + expect(wrapper.find('.chart-box').exists()).toBe(true) + }) + + it('passes props correctly', () => { + const props = { + dates: ['2024-01-01', '2024-01-02'], + alpha: [0.02, 0.025], + } + + const wrapper = mount(AlphaChart, { props }) + expect(wrapper.props()).toEqual(props) + }) +}) diff --git a/frontend/src/components/backtest/AlphaChart.vue b/frontend/src/components/backtest/AlphaChart.vue new file mode 100644 index 0000000..160eb7d --- /dev/null +++ b/frontend/src/components/backtest/AlphaChart.vue @@ -0,0 +1,51 @@ + + + + diff --git a/frontend/src/components/backtest/BenchmarkCurve.spec.ts b/frontend/src/components/backtest/BenchmarkCurve.spec.ts new file mode 100644 index 0000000..1b2403d --- /dev/null +++ b/frontend/src/components/backtest/BenchmarkCurve.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import BenchmarkCurve from './BenchmarkCurve.vue' + +// Mock echarts to avoid canvas issues in jsdom +vi.mock('echarts', () => ({ + init: vi.fn(() => ({ + setOption: vi.fn(), + dispose: vi.fn(), + resize: vi.fn(), + })), +})) + +describe('BenchmarkCurve.vue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders chart container without crashing', () => { + const props = { + dates: ['2024-01-01', '2024-01-02', '2024-01-03'], + strategy: [1.0, 1.02, 1.05], + benchmark: [1.0, 1.01, 1.03], + } + + const wrapper = mount(BenchmarkCurve, { props }) + expect(wrapper.find('.chart-box').exists()).toBe(true) + }) + + it('passes props correctly', () => { + const props = { + dates: ['2024-01-01', '2024-01-02'], + strategy: [1.0, 1.02], + benchmark: [1.0, 1.01], + } + + const wrapper = mount(BenchmarkCurve, { props }) + expect(wrapper.props()).toEqual(props) + }) +}) diff --git a/frontend/src/components/backtest/BenchmarkCurve.vue b/frontend/src/components/backtest/BenchmarkCurve.vue new file mode 100644 index 0000000..f9c46c5 --- /dev/null +++ b/frontend/src/components/backtest/BenchmarkCurve.vue @@ -0,0 +1,66 @@ + + + + diff --git a/frontend/src/components/backtest/BetaChart.spec.ts b/frontend/src/components/backtest/BetaChart.spec.ts new file mode 100644 index 0000000..d9c5297 --- /dev/null +++ b/frontend/src/components/backtest/BetaChart.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import BetaChart from './BetaChart.vue' + +// Mock echarts to avoid canvas issues in jsdom +vi.mock('echarts', () => ({ + init: vi.fn(() => ({ + setOption: vi.fn(), + dispose: vi.fn(), + resize: vi.fn(), + })), +})) + +describe('BetaChart.vue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders chart container without crashing', () => { + const props = { + dates: ['2024-01-01', '2024-01-02', '2024-01-03'], + beta: [0.98, 0.99, 1.01], + } + + const wrapper = mount(BetaChart, { props }) + expect(wrapper.find('.chart-box').exists()).toBe(true) + }) + + it('passes props correctly', () => { + const props = { + dates: ['2024-01-01', '2024-01-02'], + beta: [0.98, 0.99], + } + + const wrapper = mount(BetaChart, { props }) + expect(wrapper.props()).toEqual(props) + }) +}) diff --git a/frontend/src/components/backtest/BetaChart.vue b/frontend/src/components/backtest/BetaChart.vue new file mode 100644 index 0000000..e3a41eb --- /dev/null +++ b/frontend/src/components/backtest/BetaChart.vue @@ -0,0 +1,51 @@ + + + + diff --git a/frontend/src/components/backtest/DrawdownChart.spec.ts b/frontend/src/components/backtest/DrawdownChart.spec.ts new file mode 100644 index 0000000..81fdde1 --- /dev/null +++ b/frontend/src/components/backtest/DrawdownChart.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import DrawdownChart from './DrawdownChart.vue' + +// Mock echarts to avoid canvas issues in jsdom +vi.mock('echarts', () => ({ + init: vi.fn(() => ({ + setOption: vi.fn(), + dispose: vi.fn(), + resize: vi.fn(), + })), +})) + +describe('DrawdownChart.vue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders chart container without crashing', () => { + const props = { + dates: ['2024-01-01', '2024-01-02', '2024-01-03'], + drawdown: [0.0, -0.02, -0.08], + } + + const wrapper = mount(DrawdownChart, { props }) + expect(wrapper.find('.chart-box').exists()).toBe(true) + }) + + it('passes props correctly', () => { + const props = { + dates: ['2024-01-01', '2024-01-02'], + drawdown: [0.0, -0.02], + } + + const wrapper = mount(DrawdownChart, { props }) + expect(wrapper.props()).toEqual(props) + }) +}) diff --git a/frontend/src/components/backtest/DrawdownChart.vue b/frontend/src/components/backtest/DrawdownChart.vue new file mode 100644 index 0000000..adf8df4 --- /dev/null +++ b/frontend/src/components/backtest/DrawdownChart.vue @@ -0,0 +1,51 @@ + + + + diff --git a/frontend/src/components/backtest/MetricCards.spec.ts b/frontend/src/components/backtest/MetricCards.spec.ts new file mode 100644 index 0000000..ca4d3c8 --- /dev/null +++ b/frontend/src/components/backtest/MetricCards.spec.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import MetricCards from './MetricCards.vue' + +describe('MetricCards.vue', () => { + it('renders 10 metric cards with correct values', () => { + const metrics = { + total_return: 0.1532, + annual_return: 0.0821, + alpha: 0.0245, + beta: 0.98, + sharpe_ratio: 1.23, + sortino_ratio: 1.45, + information_ratio: 0.67, + annual_volatility: 0.12, + max_drawdown: -0.0824, + benchmark_return: 0.0650, + benchmark_volatility: 0.11, + } + + const wrapper = mount(MetricCards, { + props: { metrics }, + }) + + const text = wrapper.text() + + // Check percentage formatting (×100, 2 decimals) + expect(text).toContain('15.32%') // total_return + expect(text).toContain('8.21%') // annual_return + expect(text).toContain('2.45%') // alpha + expect(text).toContain('0.98') // beta (not percentage) + expect(text).toContain('1.23') // sharpe_ratio + expect(text).toContain('1.45') // sortino_ratio + expect(text).toContain('0.67') // information_ratio + expect(text).toContain('12.00%') // annual_volatility + expect(text).toContain('-8.24%') // max_drawdown + expect(text).toContain('6.50%') // benchmark_return + expect(text).toContain('11.00%') // benchmark_volatility + }) + + it('renders all metric labels', () => { + const metrics = { + total_return: 0.1, + annual_return: 0.1, + alpha: 0.1, + beta: 1.0, + sharpe_ratio: 1.0, + sortino_ratio: 1.0, + information_ratio: 0.5, + annual_volatility: 0.1, + max_drawdown: -0.05, + benchmark_return: 0.08, + benchmark_volatility: 0.1, + } + + const wrapper = mount(MetricCards, { + props: { metrics }, + }) + + const text = wrapper.text() + + // Check all metric names are present + expect(text).toContain('总收益率') + expect(text).toContain('年化收益率') + expect(text).toContain('Alpha') + expect(text).toContain('Beta') + expect(text).toContain('Sharpe比率') + expect(text).toContain('Sortino比率') + expect(text).toContain('信息比率') + expect(text).toContain('年化波动率') + expect(text).toContain('最大回撤') + expect(text).toContain('基准收益率') + expect(text).toContain('基准波动率') + }) +}) diff --git a/frontend/src/components/backtest/MetricCards.vue b/frontend/src/components/backtest/MetricCards.vue new file mode 100644 index 0000000..6979d2c --- /dev/null +++ b/frontend/src/components/backtest/MetricCards.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/components/backtest/VolatilityChart.spec.ts b/frontend/src/components/backtest/VolatilityChart.spec.ts new file mode 100644 index 0000000..d27e620 --- /dev/null +++ b/frontend/src/components/backtest/VolatilityChart.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import VolatilityChart from './VolatilityChart.vue' + +// Mock echarts to avoid canvas issues in jsdom +vi.mock('echarts', () => ({ + init: vi.fn(() => ({ + setOption: vi.fn(), + dispose: vi.fn(), + resize: vi.fn(), + })), +})) + +describe('VolatilityChart.vue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders chart container without crashing', () => { + const props = { + dates: ['2024-01-01', '2024-01-02', '2024-01-03'], + strategy: [0.12, 0.13, 0.11], + benchmark: [0.10, 0.11, 0.10], + } + + const wrapper = mount(VolatilityChart, { props }) + expect(wrapper.find('.chart-box').exists()).toBe(true) + }) + + it('passes props correctly', () => { + const props = { + dates: ['2024-01-01', '2024-01-02'], + strategy: [0.12, 0.13], + benchmark: [0.10, 0.11], + } + + const wrapper = mount(VolatilityChart, { props }) + expect(wrapper.props()).toEqual(props) + }) +}) diff --git a/frontend/src/components/backtest/VolatilityChart.vue b/frontend/src/components/backtest/VolatilityChart.vue new file mode 100644 index 0000000..1b66420 --- /dev/null +++ b/frontend/src/components/backtest/VolatilityChart.vue @@ -0,0 +1,64 @@ + + + +