lfqian commited on
Commit
d6e1b11
·
1 Parent(s): ea281dc

Add time window slider with Return and Volatility calculation

Browse files
Files changed (1) hide show
  1. src/views/RequestView.vue +87 -4
src/views/RequestView.vue CHANGED
@@ -7,10 +7,19 @@
7
  <span class="ama-gradient">Assets in the Arena</span>
8
  </h2>
9
  <p class="section-sub">
10
- Live from dataService • B&amp;H sparkline (ECharts LineChart) • 1M change
11
  </p>
12
  </header>
13
 
 
 
 
 
 
 
 
 
 
14
  <div class="grid grid-assets-4">
15
  <article v-for="a in assets" :key="a.code" class="card asset-card">
16
  <div class="asset-head">
@@ -36,6 +45,10 @@
36
  {{ signedPct(a.change1m) }}
37
  </div>
38
  </div>
 
 
 
 
39
  <div class="stat">
40
  <div class="stat-label">Runs</div>
41
  <div class="stat-value mono">{{ a.runs }}</div>
@@ -165,6 +178,11 @@ export default {
165
 
166
  // dynamic from dataService
167
  assets: [],
 
 
 
 
 
168
 
169
  // agents (no performance fields shown)
170
  agents: [
@@ -252,6 +270,36 @@ export default {
252
  fmtUSD(n) {
253
  return (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
254
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  /* ===== Dynamic assets built from B&H equity ===== */
257
  async buildAssetSeq(assetCode) {
@@ -282,10 +330,27 @@ export default {
282
  console.log('[RequestView] Assets in rows:', assetsInRows)
283
  if (!assetsInRows.length) { this.assets = []; return }
284
 
 
 
 
 
 
 
 
 
 
 
285
  const cards = []
286
  for (const code of assetsInRows) {
287
- const seq = await this.buildAssetSeq(code)
288
- console.log('[RequestView] Asset', code, 'seq length:', seq?.length || 0)
 
 
 
 
 
 
 
289
  if (!seq.length) continue
290
 
291
  // B&H equity as normalized series for spark and change
@@ -302,6 +367,9 @@ export default {
302
  const spark = this.sampleSeries(bh.map(v => v), 12)
303
  console.log('[RequestView] Asset', code, 'spark data:', spark)
304
  const change1m = this.pctChange(spark[0], spark[spark.length - 1])
 
 
 
305
 
306
  // trading day span
307
  let days = 0
@@ -323,6 +391,7 @@ export default {
323
  month: spark,
324
  sparkColor: (code === 'BTC') ? '#f59e0b' : (code === 'ETH' ? '#6366f1' : '#22c55e'),
325
  change1m,
 
326
  runs, agents, days,
327
  eod: lastDate
328
  })
@@ -333,6 +402,13 @@ export default {
333
  cards.sort((a,b) => (order.indexOf(a.code) - order.indexOf(b.code)))
334
  this.assets = cards
335
  }
 
 
 
 
 
 
 
336
  }
337
  }
338
  </script>
@@ -354,6 +430,13 @@ export default {
354
  .clamp-2{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
355
  .w-full{ width:100%; }
356
 
 
 
 
 
 
 
 
357
  /* ===== Sections ===== */
358
  .section{ margin-top:18px; }
359
  .section-head{ margin-bottom:8px; }
@@ -394,7 +477,7 @@ export default {
394
 
395
  .asset-body{ display:flex; flex-direction:column; gap:10px; }
396
  .spark-wrap{ width:100%; height:90px; overflow:hidden; border-radius:8px; background:#F6F8FB; border:1px solid #E7ECF3; position:relative; }
397
- .asset-stats{ display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:6px; }
398
  .stat{ background:#F6F8FB; border:1px solid #E7ECF3; border-radius:8px; padding:4px 6px; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; }
399
  .stat-label{ font-size:9px; color:#6b7280; margin-bottom:2px; white-space:nowrap; }
400
  .stat-value{ font-weight:700; font-size:11.5px; color:#0f172a; line-height:1.2; }
 
7
  <span class="ama-gradient">Assets in the Arena</span>
8
  </h2>
9
  <p class="section-sub">
10
+ Live from dataService • B&amp;H sparkline (ECharts LineChart) • Return & Volatility
11
  </p>
12
  </header>
13
 
14
+ <!-- Time Window Slider -->
15
+ <div class="time-window-control">
16
+ <div class="time-window-header">
17
+ <span class="time-window-label">Time Window:</span>
18
+ <span class="time-window-range">{{ formatDate(dateRange[0]) }} - {{ formatDate(dateRange[1]) }}</span>
19
+ </div>
20
+ <Slider v-model="dateRange" :min="0" :max="maxDateIndex" :range="true" class="time-slider" />
21
+ </div>
22
+
23
  <div class="grid grid-assets-4">
24
  <article v-for="a in assets" :key="a.code" class="card asset-card">
25
  <div class="asset-head">
 
45
  {{ signedPct(a.change1m) }}
46
  </div>
47
  </div>
48
+ <div class="stat">
49
+ <div class="stat-label">Volatility</div>
50
+ <div class="stat-value mono">{{ formatPct(a.volatility) }}</div>
51
+ </div>
52
  <div class="stat">
53
  <div class="stat-label">Runs</div>
54
  <div class="stat-value mono">{{ a.runs }}</div>
 
178
 
179
  // dynamic from dataService
180
  assets: [],
181
+
182
+ // time window control
183
+ allDates: [],
184
+ dateRange: [0, 0],
185
+ maxDateIndex: 0,
186
 
187
  // agents (no performance fields shown)
188
  agents: [
 
270
  fmtUSD(n) {
271
  return (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
272
  },
273
+ formatPct(p) {
274
+ const v = Number(p || 0) * 100
275
+ return `${Math.abs(v).toFixed(2)}%`
276
+ },
277
+ formatDate(index) {
278
+ if (!this.allDates || index < 0 || index >= this.allDates.length) return '—'
279
+ return this.allDates[index]
280
+ },
281
+ calculateVolatility(values) {
282
+ if (!Array.isArray(values) || values.length < 2) return 0
283
+
284
+ // Calculate returns
285
+ const returns = []
286
+ for (let i = 1; i < values.length; i++) {
287
+ if (values[i-1] > 0) {
288
+ returns.push((values[i] - values[i-1]) / values[i-1])
289
+ }
290
+ }
291
+
292
+ if (returns.length === 0) return 0
293
+
294
+ // Calculate mean
295
+ const mean = returns.reduce((sum, r) => sum + r, 0) / returns.length
296
+
297
+ // Calculate variance
298
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length
299
+
300
+ // Return standard deviation (volatility)
301
+ return Math.sqrt(variance)
302
+ },
303
 
304
  /* ===== Dynamic assets built from B&H equity ===== */
305
  async buildAssetSeq(assetCode) {
 
330
  console.log('[RequestView] Assets in rows:', assetsInRows)
331
  if (!assetsInRows.length) { this.assets = []; return }
332
 
333
+ // Initialize all dates from first asset to set up slider
334
+ if (this.allDates.length === 0 && assetsInRows.length > 0) {
335
+ const firstSeq = await this.buildAssetSeq(assetsInRows[0])
336
+ if (firstSeq.length > 0) {
337
+ this.allDates = firstSeq.map(r => r.date).filter(d => d >= '2025-08-01')
338
+ this.maxDateIndex = this.allDates.length - 1
339
+ this.dateRange = [0, this.maxDateIndex]
340
+ }
341
+ }
342
+
343
  const cards = []
344
  for (const code of assetsInRows) {
345
+ const fullSeq = await this.buildAssetSeq(code)
346
+ console.log('[RequestView] Asset', code, 'full seq length:', fullSeq?.length || 0)
347
+ if (!fullSeq.length) continue
348
+
349
+ // Filter sequence based on date range
350
+ const startDate = this.allDates[this.dateRange[0]] || '2025-08-01'
351
+ const endDate = this.allDates[this.dateRange[1]] || this.allDates[this.allDates.length - 1]
352
+ const seq = fullSeq.filter(r => r.date >= startDate && r.date <= endDate)
353
+ console.log('[RequestView] Asset', code, 'filtered seq length:', seq?.length || 0, 'from', startDate, 'to', endDate)
354
  if (!seq.length) continue
355
 
356
  // B&H equity as normalized series for spark and change
 
367
  const spark = this.sampleSeries(bh.map(v => v), 12)
368
  console.log('[RequestView] Asset', code, 'spark data:', spark)
369
  const change1m = this.pctChange(spark[0], spark[spark.length - 1])
370
+
371
+ // Calculate volatility
372
+ const volatility = this.calculateVolatility(bh)
373
 
374
  // trading day span
375
  let days = 0
 
391
  month: spark,
392
  sparkColor: (code === 'BTC') ? '#f59e0b' : (code === 'ETH' ? '#6366f1' : '#22c55e'),
393
  change1m,
394
+ volatility,
395
  runs, agents, days,
396
  eod: lastDate
397
  })
 
402
  cards.sort((a,b) => (order.indexOf(a.code) - order.indexOf(b.code)))
403
  this.assets = cards
404
  }
405
+ },
406
+
407
+ watch: {
408
+ dateRange() {
409
+ // Rebuild assets when date range changes
410
+ this.rebuildAssets().catch(() => {})
411
+ }
412
  }
413
  }
414
  </script>
 
430
  .clamp-2{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
431
  .w-full{ width:100%; }
432
 
433
+ /* ===== Time Window Control ===== */
434
+ .time-window-control{ margin:12px 0 16px; padding:16px; background:#F6F8FB; border:1px solid #E7ECF3; border-radius:12px; }
435
+ .time-window-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
436
+ .time-window-label{ font-weight:700; font-size:14px; color:#0f172a; }
437
+ .time-window-range{ font-size:13px; color:#64748b; font-family: ui-monospace, monospace; }
438
+ .time-slider{ margin-top:8px; }
439
+
440
  /* ===== Sections ===== */
441
  .section{ margin-top:18px; }
442
  .section-head{ margin-bottom:8px; }
 
477
 
478
  .asset-body{ display:flex; flex-direction:column; gap:10px; }
479
  .spark-wrap{ width:100%; height:90px; overflow:hidden; border-radius:8px; background:#F6F8FB; border:1px solid #E7ECF3; position:relative; }
480
+ .asset-stats{ display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); gap:6px; }
481
  .stat{ background:#F6F8FB; border:1px solid #E7ECF3; border-radius:8px; padding:4px 6px; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; }
482
  .stat-label{ font-size:9px; color:#6b7280; margin-bottom:2px; white-space:nowrap; }
483
  .stat-value{ font-weight:700; font-size:11.5px; color:#0f172a; line-height:1.2; }