Tracker watchlist: summary cards + auto-load first symbol
Each watched symbol now appears as a small card showing live spot, ATM IV (color-coded by level), and RR25 — fetched from /api/analytics. Click a card to load that symbol's full IV/RR/Fly history charts; the active card gets a blue border. On page open: if there's a watchlist but no history loaded yet, auto-open the first watched symbol so the page isn't empty. Refresh button re-pulls all watchlist metrics. Empty state hints at adding symbols from the Strategy page's "Save to Tracker" button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -278,15 +278,41 @@
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
|
||||
<!-- Watchlist (symbols saved from Strategy page) -->
|
||||
<div class="mb-3 d-flex align-items-center flex-wrap gap-2" x-show="watchlist.length > 0" x-cloak>
|
||||
<span class="text-secondary small me-1">Watchlist:</span>
|
||||
<template x-for="s in watchlist" :key="s">
|
||||
<span class="badge bg-blue-lt" style="cursor:pointer; padding:.45rem .6rem;">
|
||||
<span @click="loadSymbol(s)" x-text="s" :class="s === symbol.toUpperCase().trim() ? 'fw-bold' : ''"></span>
|
||||
<span class="text-danger ms-1" style="cursor:pointer;" @click="removeWatch(s)" :title="'Remove ' + s + ' from watchlist'">✕</span>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Watchlist summary cards (symbols saved from Strategy page) -->
|
||||
<div class="mb-3" x-show="watchlist.length > 0" x-cloak>
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<h3 class="text-white mb-0" style="font-size:1rem;">Watchlist <span class="text-secondary small fw-normal" x-text="'(' + watchlist.length + ')'"></span></h3>
|
||||
<button class="btn btn-sm btn-outline-secondary" @click="_loadWatchlistData()" :disabled="watchlistLoading">
|
||||
<span x-show="watchlistLoading" class="spinner-border spinner-border-sm me-1"></span>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<template x-for="s in watchlist" :key="s">
|
||||
<div class="col-6 col-md-4 col-lg-3 col-xl-2">
|
||||
<div class="watch-card" style="background:#1e2030; border:1px solid #2d3045; border-radius:.5rem; padding:.6rem .75rem; cursor:pointer;"
|
||||
:class="s === symbol.toUpperCase().trim() ? 'border-primary' : ''"
|
||||
:style="s === symbol.toUpperCase().trim() ? 'background:#1e2030; border:1px solid #4d9ef7; border-radius:.5rem; padding:.6rem .75rem; cursor:pointer;' : 'background:#1e2030; border:1px solid #2d3045; border-radius:.5rem; padding:.6rem .75rem; cursor:pointer;'"
|
||||
@click="loadSymbol(s)">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<span class="fw-bold text-white" style="font-family:'JetBrains Mono',monospace;" x-text="s"></span>
|
||||
<span class="text-danger small" style="cursor:pointer;" @click.stop="removeWatch(s)" :title="'Remove ' + s">✕</span>
|
||||
</div>
|
||||
<div class="mt-1" style="font-size:.78rem; color:#cbd3df;">
|
||||
<template x-if="watchlistData[s]">
|
||||
<div>
|
||||
<div>Spot <strong class="mono" x-text="watchlistData[s].spot ? '$' + watchlistData[s].spot.toFixed(2) : '—'"></strong></div>
|
||||
<div>ATM IV <strong class="mono" :style="watchlistData[s].atmIv > 0.4 ? 'color:#ff8c42' : watchlistData[s].atmIv > 0.2 ? 'color:#ffd43b' : 'color:#51cf66'" x-text="watchlistData[s].atmIv ? (watchlistData[s].atmIv*100).toFixed(1) + '%' : '—'"></strong></div>
|
||||
<div :style="watchlistData[s].rr25 > 0.005 ? 'color:#51cf66' : watchlistData[s].rr25 < -0.005 ? 'color:#ff6b6b' : 'color:#8b95a7'">
|
||||
RR25 <span class="mono" x-text="watchlistData[s].rr25 != null ? ((watchlistData[s].rr25 >= 0 ? '+' : '') + (watchlistData[s].rr25 * 100).toFixed(2) + '%') : '—'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!watchlistData[s]"><div class="text-secondary">loading…</div></template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
@@ -297,7 +323,11 @@
|
||||
<path d="M21 21l-1.5-1.5M16.5 16.5L12 12M12 12L7.5 7.5M7.5 7.5L3 3"></path>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
</svg>
|
||||
<p class="text-muted">No snapshot data. Enter a symbol and click Load History.</p>
|
||||
<p class="text-muted">
|
||||
No snapshot data yet. <span x-show="watchlist.length > 0">Click a watchlist card above, or </span>enter a symbol and click <strong>Load History</strong>.
|
||||
<br>
|
||||
<span class="small">Tip: add symbols to the watchlist from the <a href="strategy.html">Strategy</a> page's <em>Save to Tracker</em> button.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Charts row -->
|
||||
@@ -647,6 +677,8 @@
|
||||
error: '',
|
||||
|
||||
watchlist: [],
|
||||
watchlistData: {},
|
||||
watchlistLoading: false,
|
||||
|
||||
_charts: { atmIv: null, rr25: null, fly25: null },
|
||||
|
||||
@@ -661,7 +693,13 @@
|
||||
if (this.snapshots.length) this.$nextTick(() => this.renderCharts());
|
||||
}
|
||||
this._loadWatchlist();
|
||||
window.addEventListener('storage', (e) => { if (e.key === 'optionsPricer:watchlist') this._loadWatchlist(); });
|
||||
window.addEventListener('storage', (e) => { if (e.key === 'optionsPricer:watchlist') { this._loadWatchlist(); this._loadWatchlistData(); } });
|
||||
// populate the watchlist cards
|
||||
if (this.watchlist.length) this._loadWatchlistData();
|
||||
// if we have nothing loaded yet but a watchlist exists, auto-open the first
|
||||
if (this.snapshots.length === 0 && this.watchlist.length > 0) {
|
||||
this.loadSymbol(this.watchlist[0]);
|
||||
}
|
||||
},
|
||||
|
||||
_loadWatchlist() {
|
||||
@@ -669,6 +707,38 @@
|
||||
catch { this.watchlist = []; }
|
||||
},
|
||||
|
||||
async _loadWatchlistData() {
|
||||
if (this.watchlist.length === 0) return;
|
||||
this.watchlistLoading = true;
|
||||
try {
|
||||
const data = { ...this.watchlistData };
|
||||
// fetch each symbol's nearest analytics in parallel
|
||||
await Promise.all(this.watchlist.map(async (sym) => {
|
||||
try {
|
||||
const r = await fetch('/api/analytics?symbol=' + encodeURIComponent(sym));
|
||||
if (!r.ok) return;
|
||||
const e = await r.json();
|
||||
const d = e.data ?? e;
|
||||
// skewMetrics is keyed by expiry — pick the nearest
|
||||
const expiries = (d.volSurface?.expiries || Object.keys(d.skewMetrics || {})).sort();
|
||||
const front = expiries[0];
|
||||
const m = (d.skewMetrics || {})[front] || {};
|
||||
data[sym] = {
|
||||
spot: d.spot ?? null,
|
||||
atmIv: m.atmIv ?? d.atmIv ?? null,
|
||||
rr25: m.rr25 ?? null,
|
||||
fly25: m.fly25 ?? null,
|
||||
expiry: front || null,
|
||||
ts: new Date().toISOString(),
|
||||
};
|
||||
} catch {}
|
||||
}));
|
||||
this.watchlistData = data;
|
||||
} finally {
|
||||
this.watchlistLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
loadSymbol(s) {
|
||||
this.symbol = s;
|
||||
this.fetchExpirations();
|
||||
@@ -678,6 +748,9 @@
|
||||
removeWatch(s) {
|
||||
this.watchlist = this.watchlist.filter(x => x !== s);
|
||||
try { localStorage.setItem('optionsPricer:watchlist', JSON.stringify(this.watchlist)); } catch {}
|
||||
delete this.watchlistData[s];
|
||||
// keep reactivity by reassigning
|
||||
this.watchlistData = { ...this.watchlistData };
|
||||
},
|
||||
|
||||
_persist() {
|
||||
|
||||
Reference in New Issue
Block a user