feat(frontend): 结果页组件—MetricCards指标卡+5图(基准/Alpha/Beta/波动率/回撤)+API封装
This commit is contained in:
@@ -129,3 +129,47 @@ export async function getOptimizationResults(taskId: string): Promise<OptRow[]>
|
||||
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<RelativeMetrics> {
|
||||
const { data } = await apiClient.get<{ relative_metrics: RelativeMetrics }>(`/task/${taskId}/result`)
|
||||
return data.relative_metrics
|
||||
}
|
||||
|
||||
export async function getBenchmarkCurve(taskId: string): Promise<BenchmarkCurveData> {
|
||||
const { data } = await apiClient.get<BenchmarkCurveData>(`/task/${taskId}/benchmark-curve`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getRiskSeries(taskId: string): Promise<RiskSeriesData> {
|
||||
const { data } = await apiClient.get<RiskSeriesData>(`/task/${taskId}/risk-series`)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { EChartsCoreOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
|
||||
|
||||
const ALPHA = '#61a0a8' // Alpha 绿
|
||||
|
||||
const props = defineProps<{ dates: string[]; alpha: number[] }>()
|
||||
const el = ref<HTMLDivElement>()
|
||||
const { setOption } = useChart(el)
|
||||
|
||||
function render(): void {
|
||||
if (!props.dates.length || !props.alpha.length) return
|
||||
|
||||
const option: EChartsCoreOption = {
|
||||
title: darkTitle('Alpha'),
|
||||
tooltip: darkTooltip(),
|
||||
grid: darkGrid(),
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.dates,
|
||||
...darkAxis(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: 'Alpha',
|
||||
...darkAxis(),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Alpha',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: ALPHA, width: 1.6 },
|
||||
areaStyle: { color: ALPHA, opacity: 0.12 },
|
||||
data: props.alpha,
|
||||
},
|
||||
],
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
onMounted(render)
|
||||
watch(() => [props.dates, props.alpha], render, { deep: true })
|
||||
</script>
|
||||
|
||||
<template><div ref="el" class="chart-box" /></template>
|
||||
<style scoped>.chart-box { width: 100%; height: 280px; }</style>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { EChartsCoreOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
|
||||
|
||||
const STRATEGY = '#c23531' // 策略红
|
||||
const BENCHMARK = '#2f4554' // 基准蓝
|
||||
|
||||
const props = defineProps<{ dates: string[]; strategy: number[]; benchmark: number[] }>()
|
||||
const el = ref<HTMLDivElement>()
|
||||
const { setOption } = useChart(el)
|
||||
|
||||
function render(): void {
|
||||
if (!props.dates.length || !props.strategy.length || !props.benchmark.length) return
|
||||
|
||||
const option: EChartsCoreOption = {
|
||||
title: darkTitle('基准曲线对比'),
|
||||
tooltip: darkTooltip(),
|
||||
grid: darkGrid(),
|
||||
legend: {
|
||||
data: ['策略', '基准'],
|
||||
textStyle: { color: '#e6edf3', fontSize: 12 },
|
||||
top: 24,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.dates,
|
||||
...darkAxis(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: '净值',
|
||||
...darkAxis(),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: '策略',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: STRATEGY, width: 1.6 },
|
||||
areaStyle: { color: STRATEGY, opacity: 0.12 },
|
||||
data: props.strategy,
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
name: '基准',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: BENCHMARK, width: 1.6 },
|
||||
areaStyle: { color: BENCHMARK, opacity: 0.12 },
|
||||
data: props.benchmark,
|
||||
},
|
||||
],
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
onMounted(render)
|
||||
watch(() => [props.dates, props.strategy, props.benchmark], render, { deep: true })
|
||||
</script>
|
||||
|
||||
<template><div ref="el" class="chart-box" /></template>
|
||||
<style scoped>.chart-box { width: 100%; height: 320px; }</style>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { EChartsCoreOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
|
||||
|
||||
const BETA = '#61a0a8' // Beta 绿
|
||||
|
||||
const props = defineProps<{ dates: string[]; beta: number[] }>()
|
||||
const el = ref<HTMLDivElement>()
|
||||
const { setOption } = useChart(el)
|
||||
|
||||
function render(): void {
|
||||
if (!props.dates.length || !props.beta.length) return
|
||||
|
||||
const option: EChartsCoreOption = {
|
||||
title: darkTitle('Beta'),
|
||||
tooltip: darkTooltip(),
|
||||
grid: darkGrid(),
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.dates,
|
||||
...darkAxis(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: 'Beta',
|
||||
...darkAxis(),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Beta',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: BETA, width: 1.6 },
|
||||
areaStyle: { color: BETA, opacity: 0.12 },
|
||||
data: props.beta,
|
||||
},
|
||||
],
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
onMounted(render)
|
||||
watch(() => [props.dates, props.beta], render, { deep: true })
|
||||
</script>
|
||||
|
||||
<template><div ref="el" class="chart-box" /></template>
|
||||
<style scoped>.chart-box { width: 100%; height: 280px; }</style>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { EChartsCoreOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
|
||||
|
||||
const DRAWDOWN = '#d48265' // 回撤橙
|
||||
|
||||
const props = defineProps<{ dates: string[]; drawdown: number[] }>()
|
||||
const el = ref<HTMLDivElement>()
|
||||
const { setOption } = useChart(el)
|
||||
|
||||
function render(): void {
|
||||
if (!props.dates.length || !props.drawdown.length) return
|
||||
|
||||
const option: EChartsCoreOption = {
|
||||
title: darkTitle('回撤'),
|
||||
tooltip: darkTooltip(),
|
||||
grid: darkGrid(),
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.dates,
|
||||
...darkAxis(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: '回撤',
|
||||
...darkAxis(),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: '回撤',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: DRAWDOWN, width: 1.6 },
|
||||
areaStyle: { color: DRAWDOWN, opacity: 0.3 },
|
||||
data: props.drawdown,
|
||||
},
|
||||
],
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
onMounted(render)
|
||||
watch(() => [props.dates, props.drawdown], render, { deep: true })
|
||||
</script>
|
||||
|
||||
<template><div ref="el" class="chart-box" /></template>
|
||||
<style scoped>.chart-box { width: 100%; height: 280px; }</style>
|
||||
@@ -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('基准波动率')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const props = defineProps<{ metrics: RelativeMetrics }>()
|
||||
|
||||
// 格式化百分比(×100,保留2位小数)
|
||||
function formatPercent(value: number): string {
|
||||
return `${(value * 100).toFixed(2)}%`
|
||||
}
|
||||
|
||||
// 格式化小数(保留3位小数)
|
||||
function formatDecimal(value: number): string {
|
||||
return value.toFixed(3)
|
||||
}
|
||||
|
||||
// 格式化Sharpe等比率(保留2位小数)
|
||||
function formatRatio(value: number): string {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
const metricCards = computed(() => [
|
||||
{ label: '总收益率', value: formatPercent(props.metrics.total_return) },
|
||||
{ label: '年化收益率', value: formatPercent(props.metrics.annual_return) },
|
||||
{ label: 'Alpha', value: formatPercent(props.metrics.alpha) },
|
||||
{ label: 'Beta', value: formatDecimal(props.metrics.beta) },
|
||||
{ label: 'Sharpe比率', value: formatRatio(props.metrics.sharpe_ratio) },
|
||||
{ label: 'Sortino比率', value: formatRatio(props.metrics.sortino_ratio) },
|
||||
{ label: '信息比率', value: formatRatio(props.metrics.information_ratio) },
|
||||
{ label: '年化波动率', value: formatPercent(props.metrics.annual_volatility) },
|
||||
{ label: '最大回撤', value: formatPercent(props.metrics.max_drawdown) },
|
||||
{ label: '基准收益率', value: formatPercent(props.metrics.benchmark_return) },
|
||||
{ label: '基准波动率', value: formatPercent(props.metrics.benchmark_volatility) },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="metric-cards">
|
||||
<div v-for="card in metricCards" :key="card.label" class="metric-card">
|
||||
<div class="metric-label">{{ card.label }}</div>
|
||||
<div class="metric-value">{{ card.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #8b949e;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #e6edf3;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import type { EChartsCoreOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { darkTitle, darkTooltip, darkGrid, darkAxis } from '@/utils/echartsDark'
|
||||
|
||||
const STRATEGY = '#c23531' // 策略红
|
||||
const BENCHMARK = '#2f4554' // 基准蓝
|
||||
|
||||
const props = defineProps<{ dates: string[]; strategy: number[]; benchmark: number[] }>()
|
||||
const el = ref<HTMLDivElement>()
|
||||
const { setOption } = useChart(el)
|
||||
|
||||
function render(): void {
|
||||
if (!props.dates.length || !props.strategy.length || !props.benchmark.length) return
|
||||
|
||||
const option: EChartsCoreOption = {
|
||||
title: darkTitle('波动率对比'),
|
||||
tooltip: darkTooltip(),
|
||||
grid: darkGrid(),
|
||||
legend: {
|
||||
data: ['策略波动率', '基准波动率'],
|
||||
textStyle: { color: '#e6edf3', fontSize: 12 },
|
||||
top: 24,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.dates,
|
||||
...darkAxis(),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: '波动率',
|
||||
...darkAxis(),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: '策略波动率',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: STRATEGY, width: 1.6 },
|
||||
data: props.strategy,
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
name: '基准波动率',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { color: BENCHMARK, width: 1.6 },
|
||||
data: props.benchmark,
|
||||
},
|
||||
],
|
||||
}
|
||||
setOption(option)
|
||||
}
|
||||
|
||||
onMounted(render)
|
||||
watch(() => [props.dates, props.strategy, props.benchmark], render, { deep: true })
|
||||
</script>
|
||||
|
||||
<template><div ref="el" class="chart-box" /></template>
|
||||
<style scoped>.chart-box { width: 100%; height: 280px; }</style>
|
||||
Reference in New Issue
Block a user