Spaces:
Running
Running
File size: 7,149 Bytes
5fa7a59 3a36642 5fa7a59 5f139dc 5fa7a59 3a36642 5fa7a59 3a36642 5fa7a59 3a36642 5fa7a59 3a36642 5fa7a59 5f139dc 5fa7a59 3a36642 5fa7a59 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
<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>
|