c049e0e97d
后端 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。
97 lines
3.0 KiB
Vue
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>
|