Spaces:
Running
Running
File size: 5,782 Bytes
ac784c2 |
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 |
<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">
<Column v-if="selectable" selectionMode="multiple" style="width: 3rem" />
<Column expander style="width: 3rem" />
<Column field="agent_name" header="Agent & Model & Strategy">
<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;">{{ 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>
<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>
<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>
<template #body="{ data }">
{{ fmtNum(data.sharpe) }}
</template>
</Column>
<Column field="win_rate" header="Win Rate" sortable>
<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 ''
},
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>
</style>
|