File size: 7,778 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
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
201
202
203
<template>
  <div style="position: relative;">
    <canvas ref="perfCanvas" height="280"></canvas>
    <div v-if="isZoomed" style="position: absolute; top: 10px; right: 10px; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; z-index: 10;">
      <button 
        @click="resetZoom"
        style="padding: 6px 12px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"
        @mouseover="$event.target.style.background='#2563eb'"
        @mouseout="$event.target.style.background='#3b82f6'"
      >
        Reset Zoom
      </button>
      <span style="font-size: 11px; color: #64748b; background: rgba(255,255,255,0.95); padding: 4px 8px; border-radius: 3px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
        Drag to zoom | Shift+Drag to pan
      </span>
    </div>
  </div>
</template>

<script>
import { getAllDecisions } from '../lib/dataCache'
import { STRATEGIES } from '../lib/strategies'
import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
import { Chart, LineElement, PointElement, LinearScale, TimeScale, CategoryScale, LineController, Legend, Tooltip } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'

// vertical crosshair plugin
const vLinePlugin = {
  id: 'vLinePlugin',
  afterDatasetsDraw(chart, args, pluginOptions) {
    const active = (typeof chart.getActiveElements === 'function') ? chart.getActiveElements() : (chart.tooltip && chart.tooltip._active) || []
    if (!active || !active.length) return
    const { datasetIndex, index } = active[0]
    const meta = chart.getDatasetMeta(datasetIndex)
    const pt = meta && meta.data && meta.data[index]
    if (!pt) return
    const x = pt.x
    const { top, bottom } = chart.chartArea
    const ctx = chart.ctx
    ctx.save()
    ctx.beginPath()
    ctx.moveTo(x, top)
    ctx.lineTo(x, bottom)
    ctx.lineWidth = (pluginOptions && pluginOptions.lineWidth) || 1
    ctx.strokeStyle = (pluginOptions && pluginOptions.color) || 'rgba(0,0,0,0.35)'
    ctx.setLineDash((pluginOptions && pluginOptions.dash) || [4, 4])
    ctx.stroke()
    ctx.restore()
  }
}

Chart.register(LineElement, PointElement, LinearScale, TimeScale, CategoryScale, LineController, Legend, Tooltip, vLinePlugin, zoomPlugin)

export default {
  name: 'PerformanceChart',
  props: {
    data: { type: Object, required: true }
  },
  data(){
    return { 
      chart: null,
      isZoomed: false
    }
  },
  watch: {
    data: {
      handler(){ this.draw() }, immediate: true, deep: true
    }
  },
  mounted(){ this.$nextTick(() => this.draw()) },
  beforeUnmount(){ try{ this.chart && this.chart.destroy() }catch(_){} },
  methods: {
    getSeqFromIds(row){
      const all = getAllDecisions() || []
      const ids = row && (row.decision_ids || row.ids || [])
      let seq = []
      if (ids && ids.length) {
        seq = all.filter(r => ids.includes(r.id))
      } else if (row) {
        // Only fallback to full data if no decision_ids were provided at all
        seq = all.filter(r => r.agent_name === row.agent_name && r.asset === row.asset && r.model === row.model)
      }
      seq.sort((a,b) => (a.date > b.date ? 1 : -1))
      return seq
    },
    fmtMoney(v){ try{ return `$${Number(v).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}` }catch{return v} },
    async draw(){
      if (!this.$refs.perfCanvas || !this.data) return
      const row = this.data
      const rawSeq = this.getSeqFromIds(row)
      const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
      const seq = isCrypto ? rawSeq : (await filterRowsToNyseTradingDays(rawSeq) || [])
      if (!seq.length) { try{ this.chart && this.chart.destroy() }catch(_){}; return }

      const strategyCfg = (STRATEGIES || []).find(s => s.id === row.strategy) || { strategy: 'long_short', tradingMode: 'normal', fee: 0.0005 }
      const st = computeStrategyEquity(seq, 100000, strategyCfg.fee, strategyCfg.strategy, strategyCfg.tradingMode)
      const bh = computeBuyHoldEquity(seq, 100000)
      const labels = seq.map(s => s.date)
      // keep arrays aligned in length
      const n = Math.min(st.length, bh.length, labels.length)
      const stS = st.slice(0, n)
      const bhS = bh.slice(0, n)
      const lab = labels.slice(0, n)

      const stRet = stS.length ? ((stS[stS.length-1] - stS[0]) / stS[0]) * 100 : 0
      const bhRet = bhS.length ? ((bhS[bhS.length-1] - bhS[0]) / bhS[0]) * 100 : 0
      const vsBh = stRet - bhRet
      const agentColor = vsBh >= 0 ? '#16a34a' : '#dc2626' // green when outperform, red when underperform
      const agentBgColor = vsBh >= 0 ? 'rgba(22,163,74,0.15)' : 'rgba(220,38,38,0.15)'
      const baselineColor = '#3b82f6' // blue

      try{ this.chart && this.chart.destroy() }catch(_){ }
      this.chart = new Chart(this.$refs.perfCanvas.getContext('2d'), {
        type: 'line',
        data: {
          labels: lab,
          datasets: [
            {
              label: 'Agent Balance',
              data: stS,
              borderColor: agentColor,
              backgroundColor: agentBgColor,
              pointRadius: 3,
              tension: 0.2
            },
            {
              label: 'Baseline',
              data: bhS,
              borderColor: baselineColor,
              borderDash: [6,4],
              pointRadius: 0,
              tension: 0.2
            }
          ]
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          animation: false,
          interaction: { mode: 'index', intersect: false },
          plugins: {
            legend: { display: true },
            tooltip: {
              callbacks: {
                title: (items) => items && items.length ? `Date: ${items[0].label}` : '',
                afterTitle: () => '',
                label: (ctx) => `${ctx.dataset.label}: ${this.fmtMoney(ctx.parsed.y)}`,
                afterBody: (items) => {
                  const idx = items && items.length ? items[0].dataIndex : null
                  let sRet = stRet
                  let bRet = bhRet
                  if (idx != null && idx >= 0) {
                    if (stS.length > 0 && stS[idx] != null) sRet = ((stS[idx] - stS[0]) / stS[0]) * 100
                    if (bhS.length > 0 && bhS[idx] != null) bRet = ((bhS[idx] - bhS[0]) / bhS[0]) * 100
                  }
                  return [
                    `Strategy Return: ${sRet.toFixed(2)}%`,
                    `Baseline Return: ${bRet.toFixed(2)}%`
                  ]
                }
              }
            },
            vLinePlugin: { color: 'rgba(0,0,0,0.35)', lineWidth: 1, dash: [4,4] },
            zoom: {
              zoom: {
                drag: {
                  enabled: true,
                  backgroundColor: 'rgba(59, 130, 246, 0.2)',
                  borderColor: 'rgba(59, 130, 246, 0.8)',
                  borderWidth: 1
                },
                mode: 'x',
                onZoomComplete: () => {
                  this.isZoomed = true
                }
              },
              pan: {
                enabled: true,
                mode: 'x',
                modifierKey: 'shift'
              },
              limits: {
                x: { min: 'original', max: 'original' }
              }
            }
          },
          scales: {
            x: { ticks: { autoSkip: true, maxTicksLimit: 8 } },
            y: { beginAtZero: false }
          }
        }
      })
    },
    resetZoom(){
      if (this.chart) {
        this.chart.resetZoom()
        this.isZoomed = false
      }
    }
  }
}
</script>