lfqian's picture
Remove date suffix from model names (e.g., claude_sonnet_4_20250514 -> claude_sonnet_4)
5f139dc
<template>
<DataTable :value="rows" :rows="10" :rowsPerPageOptions="[10,25,50]" paginator scrollable scrollHeight="flex" :loading="loading" :sortMode="'multiple'" :multiSortMeta="multiSortMeta" v-model:expandedRows="expandedRows" :dataKey="'key'" @sort="onSort" @rowToggle="onRowToggle" @rowExpand="onRowExpand" :selection="selection" @update:selection="onSelectionUpdate" class="agent-table-scroll">
<Column v-if="selectable" selectionMode="multiple" :style="{ width: '50px', minWidth: '50px' }" frozen />
<Column expander :style="{ width: '50px', minWidth: '50px' }" frozen />
<Column field="agent_name" header="Agent & Model" :style="{ minWidth: '200px' }" frozen>
<template #body="{ data }">
<div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span>{{ data.agent_name }}</span>
<span style="font-size: 1.25rem;">{{ getRankMedal(data) }}</span>
</div>
<div style="color:#6b7280; font-size: 0.875rem;">{{ formatModelName(data.model) }}</div>
<!-- <div style="color:#6b7280; font-size: 0.875rem;">{{ data.strategy_label }}</div> -->
</div>
</template>
</Column>
<!-- <Column field="asset" header="Asset"/> -->
<Column field="ret_with_fees" header="Return" sortable :style="{ minWidth: '180px' }">
<template #body="{ data }">
<div>
<div :style="pctStyle(data.ret_with_fees)">{{ fmtSignedPct(data.ret_with_fees) }}</div>
<div :style="subPctStyle(data.ret_no_fees)">(No Fees: {{ fmtSignedPct(data.ret_no_fees) }})</div>
</div>
</template>
</Column>
<Column field="vs_bh_with_fees" header="Vs Buy & Hold" sortable :style="{ minWidth: '140px' }">
<template #body="{ data }">
<span :style="pctStyle(data.vs_bh_with_fees)">{{ fmtSignedPct(data.vs_bh_with_fees) }}</span>
</template>
</Column>
<Column field="sharpe" header="Sharpe Ratio" sortable :style="{ minWidth: '120px' }">
<template #body="{ data }">
{{ fmtNum(data.sharpe) }}
</template>
</Column>
<Column field="win_rate" header="Win Rate" sortable :style="{ minWidth: '110px' }">
<template #body="{ data }">
{{ fmtPctNeutral(data.win_rate) }}
</template>
</Column>
<template #expansion="slotProps">
<ExpansionContent :rowData="slotProps.data" />
</template>
</DataTable>
</template>
<script>
import ExpansionContent from './ExpansionContent.vue'
export default {
name: 'AgentTable',
props: { rows: { type: Array, default: () => [] }, loading: { type: Boolean, default: false }, selectable: { type: Boolean, default: false }, selection: { type: Array, default: () => [] } },
emits: ['update:selection'],
components: { ExpansionContent },
data(){
return {
expandedRows: [],
multiSortMeta: [
{ field: 'ret_with_fees', order: -1 }
]
}
},
computed: {
rankedRows() {
// Sort rows by ret_with_fees descending to determine rank
return [...this.rows].sort((a, b) => {
const aVal = Number(a.ret_with_fees) || 0
const bVal = Number(b.ret_with_fees) || 0
return bVal - aVal
})
}
},
methods: {
getRankMedal(data) {
// Find the rank of this row based on ret_with_fees
const rank = this.rankedRows.findIndex(row => row.key === data.key) + 1
if (rank === 1) return '🥇'
if (rank === 2) return '🥈'
if (rank === 3) return '🥉'
return ''
},
formatModelName(model) {
if (!model) return ''
// Remove date suffix pattern (8 digits at the end, like _20250514)
// Also handles patterns like _YYYYMMDD or just YYYYMMDD at the end
return model.replace(/_?\d{8}$/, '')
},
onSelectionUpdate(val){
this.$emit('update:selection', Array.isArray(val) ? val : [])
},
onSort(){
// close all rows when sorting
this.expandedRows = []
},
onRowToggle(e){
// keep only one expanded row at a time
const val = e.data || e
if (Array.isArray(val)) {
// when using array mode, restrict to the last toggled row
this.expandedRows = val.length ? [val[val.length - 1]] : []
} else if (val && typeof val === 'object') {
// object mode; keep only the last key
const keys = Object.keys(val)
if (!keys.length) { this.expandedRows = {}; return }
const lastKey = keys[keys.length - 1]
const map = {}
map[lastKey] = true
this.expandedRows = map
} else {
this.expandedRows = []
}
},
onRowExpand(e){
// ensure only the current row is expanded
const row = e && e.data
if (!row) { this.expandedRows = []; return }
// DataTable may track expandedRows as array or map depending on mode
if (Array.isArray(this.expandedRows)) {
this.expandedRows = [row]
} else {
const map = {}
map[row.key] = true
this.expandedRows = map
}
},
fmtMoney(v){ try{ return `$${Number(v).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}` }catch{return v} },
fmtNum(v){ if(v==null) return '-'; return Number(v).toFixed(2) },
// v is always a fraction (0.12 = 12%, 1.5 = 150%). Always render with two decimals + sign
fmtSignedPct(v){
if(v==null) return '-'
const pct = Number(v) * 100
const sign = pct > 0 ? '+' : (pct < 0 ? '-' : '')
return `${sign}${Math.abs(pct).toFixed(2)}%`
},
// neutral percentage (no color/sign) for win rate. v is already in percentage form (0-100).
fmtPctNeutral(v){
if(v==null) return '-'
return `${Number(v).toFixed(2)}%`
},
pctStyle(v){
const val = Number(v)
if (val > 0) return { color: '#16a34a', fontWeight: 'bold' } // green-600
if (val < 0) return { color: '#dc2626', fontWeight: 'bold' } // red-600
return {}
},
subPctStyle(v){
const val = Number(v)
if (val > 0) return { color: '#22c55e', fontSize: '0.8rem'} // green-500
if (val < 0) return { color: '#ef4444', fontSize: '0.8rem'} // red-500
return { color: '#6b7280', fontSize: '0.8rem'} // gray-500 for neutral
}
}
}
</script>
<style scoped>
/* Enable horizontal scrolling */
:deep(.agent-table-scroll .p-datatable-wrapper) {
overflow-x: auto;
}
:deep(.agent-table-scroll .p-datatable-table) {
min-width: 800px;
}
/* Frozen column styles */
:deep(.agent-table-scroll .p-frozen-column) {
background: #ffffff;
z-index: 1;
}
:deep(.agent-table-scroll .p-datatable-thead > tr > th.p-frozen-column) {
background: #F6F8FB;
}
/* Better scrollbar */
:deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar {
height: 8px;
}
:deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
:deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
:deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>