Files
sanguo_vnpy_v2/frontend/src/components/backtest/MetricCards.vue
T
claude_dev c049e0e97d
CI/CD / test (push) Successful in 8s
CI/CD / nas-deploy (push) Successful in 21s
CI/CD / nas-verify (push) Successful in 3s
fix(frontend): MetricCards fmt 兜底字符串值(治结果页整页渲染崩溃)
后端 statistics 字段可返字符串(如 sharpe_ratio="0",vnpy原始统计),原 fmt 直接 value.toFixed 在字符串上抛 TypeError。stack trace 证实:Result.vue onMounted 赋值 relativeMetrics 后 MetricCards fmt 崩溃,中断 Vue re-render → echarts setOption 从未执行 → 指标卡+所有图表全空(cta_3919916e 零成交复现,canvas:0)。Number() 统一转换兜底。vue-tsc RC=0。
2026-08-02 19:51:51 +08:00

97 lines
3.0 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
interface RelativeMetrics {
total_return: number | null
annual_return: number | null
alpha: number | null
beta: number | null
sharpe_ratio: number | null
sortino_ratio: number | null
information_ratio: number | null
annual_volatility: number | null
max_drawdown: number | null
benchmark_return: number | null
benchmark_volatility: number | null
}
const props = defineProps<{ metrics: RelativeMetrics }>()
// null/NaN/字符串 安全格式化。后端 statistics 字段可能返字符串(如 sharpe_ratio="0",
// vnpy 原始统计),原 value.toFixed 在字符串上抛 TypeError 致整个结果页渲染崩溃
// (指标卡 + 所有图表全空)。Number() 统一转换兜底。
function fmt(value: number | null | undefined, digits: number, percent = false): string {
const n = Number(value)
if (value == null || Number.isNaN(n)) return '—'
return percent ? `${(n * 100).toFixed(digits)}%` : n.toFixed(digits)
}
// 格式化百分比(×100,保留2位小数)
function formatPercent(value: number | null): string {
return fmt(value, 2, true)
}
// 格式化小数(保留3位小数)
function formatDecimal(value: number | null): string {
return fmt(value, 3)
}
// 格式化Sharpe等比率(保留2位小数)
function formatRatio(value: number | null): string {
return fmt(value, 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>