2026-05-13 03:22:23 +00:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en" data-bs-theme="dark">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Vol Surface — Options Pricer</title>
|
|
|
|
|
<link rel="stylesheet" href="/assets/tabler.min.css">
|
|
|
|
|
<link rel="stylesheet" href="/assets/tabler-vendors.min.css">
|
|
|
|
|
<script src="/assets/apexcharts.min.js"></script>
|
|
|
|
|
<script src="/assets/alpine.min.js" defer></script>
|
|
|
|
|
<script src="/assets/tabler.min.js" defer></script>
|
|
|
|
|
<style>
|
|
|
|
|
.navbar-vertical {
|
|
|
|
|
width: 15rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navbar-vertical .navbar-brand {
|
|
|
|
|
padding: 1.25rem 1rem;
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-card {
|
|
|
|
|
background: #1e2030;
|
|
|
|
|
border: 1px solid #2d3045;
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-card .card-header {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-bottom: 1px solid #2d3045;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#skewChart,
|
|
|
|
|
#termChart {
|
|
|
|
|
background: #1e2030;
|
|
|
|
|
border-radius: 0 0 0.5rem 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge-metric {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border-radius: 0.375rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 0.5rem;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline .stat-label {
|
|
|
|
|
color: #8b95a7;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline .stat-value {
|
|
|
|
|
font-size: 1.05rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline .stat-value.positive {
|
|
|
|
|
color: #51cf66;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline .stat-value.negative {
|
|
|
|
|
color: #ff6b6b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-inline .stat-value.amber {
|
|
|
|
|
color: #ffd43b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[x-cloak] {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-wrapper {
|
|
|
|
|
padding-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apexcharts-tooltip {
|
|
|
|
|
background: #1e2030 !important;
|
|
|
|
|
border: 1px solid #2d3045 !important;
|
|
|
|
|
color: #ffffff !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.apexcharts-tooltip-title {
|
|
|
|
|
background: #2d3045 !important;
|
|
|
|
|
border-bottom: 1px solid #3a3f5a !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.skew-table th {
|
|
|
|
|
background: #1a1c2e;
|
|
|
|
|
color: #8b95a7;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
border-bottom: 1px solid #2d3045;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.skew-table td {
|
|
|
|
|
border-bottom: 1px solid #1e2030;
|
|
|
|
|
color: #d0d5e0;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.skew-table tbody tr:hover td {
|
|
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.skew-table .mono {
|
|
|
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body class="antialiased">
|
|
|
|
|
<div class="wrapper" x-data="surfaceApp()" x-init="init()">
|
|
|
|
|
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
|
|
|
|
<div class="container-fluid">
|
2026-05-13 07:17:40 +00:00
|
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
2026-05-13 03:22:23 +00:00
|
|
|
<span class="navbar-toggler-icon"></span>
|
|
|
|
|
</button>
|
2026-05-13 07:17:40 +00:00
|
|
|
<h1 class="navbar-brand navbar-brand-autodark">
|
2026-05-13 03:22:23 +00:00
|
|
|
<a href="index.html" class="d-flex align-items-center gap-2 text-decoration-none">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-primary" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<rect x="4" y="8" width="4" height="8" rx="1"/>
|
|
|
|
|
<line x1="6" y1="4" x2="6" y2="8"/>
|
|
|
|
|
<line x1="6" y1="16" x2="6" y2="20"/>
|
|
|
|
|
<rect x="16" y="6" width="4" height="10" rx="1"/>
|
|
|
|
|
<line x1="18" y1="2" x2="18" y2="6"/>
|
|
|
|
|
<line x1="18" y1="16" x2="18" y2="22"/>
|
2026-05-13 03:22:23 +00:00
|
|
|
</svg>
|
2026-05-13 07:17:40 +00:00
|
|
|
<span class="fw-bold">Options Pricer</span>
|
2026-05-13 03:22:23 +00:00
|
|
|
</a>
|
2026-05-13 07:17:40 +00:00
|
|
|
</h1>
|
|
|
|
|
<div class="collapse navbar-collapse" id="sidebar-menu">
|
2026-05-13 03:22:23 +00:00
|
|
|
<ul class="navbar-nav pt-lg-3">
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
2026-05-13 03:22:23 +00:00
|
|
|
<a class="nav-link" href="index.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<rect x="4" y="4" width="6" height="5" rx="2"/>
|
|
|
|
|
<rect x="4" y="13" width="6" height="7" rx="2"/>
|
|
|
|
|
<rect x="14" y="4" width="6" height="11" rx="2"/>
|
|
|
|
|
<rect x="14" y="19" width="6" height="1" rx=".5"/>
|
2026-05-13 03:22:23 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Dashboard</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
2026-05-13 03:22:23 +00:00
|
|
|
<a class="nav-link" href="chain.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<rect x="3" y="5" width="18" height="14" rx="2"/>
|
|
|
|
|
<path d="M3 10l18 0"/>
|
|
|
|
|
<path d="M10 5v14"/>
|
2026-05-13 03:22:23 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Options Chain</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item active">
|
|
|
|
|
<a class="nav-link" href="surface.html">
|
2026-05-13 03:22:23 +00:00
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M3 12c1.333 -4.667 2.667 -7 4 -7s2.667 2.333 4 7s2.667 7 4 7s2.667 -2.333 4 -7"/>
|
2026-05-13 03:22:23 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Vol Surface</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
IV Spike Scanner
Backend: GET /api/scan?symbols=SYM1,SYM2,... — for each symbol fetches
the front-expiry options chain plus 30-day realized vol and returns
{ spot, change, changePct, atmIv, hv30, ivHv, spike, expiry }. Spike flag
is on when IV/HV ≥ 1.5 or |today's % change| ≥ 3. Defaults to ~15 popular
tickers when no list is given; cap of 30 symbols/scan.
Frontend: new scanner.html page — symbol input (with "Use defaults" / "Use
watchlist" shortcuts), summary cards (count · spikes · biggest mover ·
highest IV/HV), sortable results table with spike rows highlighted, and
shortcut buttons to open each symbol on Chain or Surface.
Scanner added to all sidebars between Vol Surface and Strategy P/L.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:32:14 +00:00
|
|
|
|
Add Top Movers screener (mid-cap+, options-tradable)
New /movers page surfaces Yahoo Finance's predefined screeners
(day_gainers, day_losers, most_actives, most_shorted_stocks)
filtered to common equities with market cap >= $2B, so every
listed name has a deep options chain. Per-row actions jump
straight into Chain / Vol Surface / IV Spike Scanner, or pin
the symbol to the Tracker watchlist.
- datafetch.ts: fetchMovers(category, count) using yf.screener,
post-filtered to quoteType=EQUITY and marketCap >= $2B
- options.ts: GET /api/movers?category=&count=
- movers.html: Tabler page with 4-tab segmented control, sortable
table, summary cards, volume-vs-avg ratio highlighting hot names
- All page sidebars: insert "Movers" link between Vol Surface
and Scanner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:47:32 +00:00
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" href="movers.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M3 17l6 -6l4 4l8 -8"/>
|
|
|
|
|
<path d="M14 7l7 0l0 7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Movers</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
IV Spike Scanner
Backend: GET /api/scan?symbols=SYM1,SYM2,... — for each symbol fetches
the front-expiry options chain plus 30-day realized vol and returns
{ spot, change, changePct, atmIv, hv30, ivHv, spike, expiry }. Spike flag
is on when IV/HV ≥ 1.5 or |today's % change| ≥ 3. Defaults to ~15 popular
tickers when no list is given; cap of 30 symbols/scan.
Frontend: new scanner.html page — symbol input (with "Use defaults" / "Use
watchlist" shortcuts), summary cards (count · spikes · biggest mover ·
highest IV/HV), sortable results table with spike rows highlighted, and
shortcut buttons to open each symbol on Chain or Surface.
Scanner added to all sidebars between Vol Surface and Strategy P/L.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:32:14 +00:00
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" href="scanner.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<circle cx="11" cy="11" r="7"/>
|
|
|
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
|
|
|
<path d="M11 8v6"/>
|
|
|
|
|
<path d="M8 11h6"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Scanner</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
<a class="nav-link" href="strategy.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M4 19l4 -6l4 2l4 -8l4 5"/>
|
|
|
|
|
<path d="M4 4v16h16"/>
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Strategy P/L</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
2026-05-13 07:04:52 +00:00
|
|
|
<a class="nav-link" href="positions.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<rect x="3" y="7" width="18" height="13" rx="2"/>
|
|
|
|
|
<path d="M8 7v-2a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v2"/>
|
|
|
|
|
<line x1="12" y1="12" x2="12" y2="12.01"/>
|
|
|
|
|
<path d="M3 13a20 20 0 0 0 18 0"/>
|
2026-05-13 07:04:52 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Positions</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
2026-05-13 03:22:23 +00:00
|
|
|
<a class="nav-link" href="tracker.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/>
|
|
|
|
|
<path d="M12 12m-5 0a5 5 0 1 0 10 0a5 5 0 1 0 -10 0"/>
|
|
|
|
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
|
|
|
|
<path d="M15 12l-3 -3"/>
|
2026-05-13 03:22:23 +00:00
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Tracker</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 07:17:40 +00:00
|
|
|
<li class="nav-item ">
|
2026-05-13 07:04:52 +00:00
|
|
|
<a class="nav-link" href="settings.html">
|
|
|
|
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
2026-05-13 07:17:40 +00:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/>
|
2026-05-13 07:04:52 +00:00
|
|
|
<circle cx="12" cy="12" r="3"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="nav-link-title">Settings</span>
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2026-05-13 03:22:23 +00:00
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Page content -->
|
|
|
|
|
<div class="page-wrapper">
|
|
|
|
|
<div class="page-header d-print-none">
|
|
|
|
|
<div class="container-xl">
|
|
|
|
|
<div class="row g-2 align-items-center">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<h2 class="page-title">Volatility Surface</h2>
|
|
|
|
|
<div class="text-secondary mt-1">IV skew analysis and term structure</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="page-body">
|
|
|
|
|
<div class="container-xl">
|
|
|
|
|
|
|
|
|
|
<!-- Toolbar -->
|
|
|
|
|
<div class="card mb-4" style="background:#161824; border:1px solid #2d3045;">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="row g-3 align-items-end">
|
|
|
|
|
<div class="col-12 col-sm-auto">
|
|
|
|
|
<label class="form-label text-secondary" for="symbolInput">Symbol</label>
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input
|
|
|
|
|
id="symbolInput"
|
|
|
|
|
type="text"
|
|
|
|
|
class="form-control"
|
|
|
|
|
style="background:#1e2030; border-color:#2d3045; color:#fff; width:100px; text-transform:uppercase;"
|
|
|
|
|
placeholder="SPY"
|
|
|
|
|
x-model="symbol"
|
|
|
|
|
@keydown.enter="fetchExpirations()"
|
|
|
|
|
@input="symbol = symbol.toUpperCase()"
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
aria-label="Ticker symbol"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-secondary"
|
|
|
|
|
@click="fetchExpirations()"
|
|
|
|
|
:disabled="lookingUp || !symbol"
|
|
|
|
|
aria-label="Look up expirations for symbol"
|
|
|
|
|
>
|
|
|
|
|
<span x-show="lookingUp" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
|
|
|
|
<span x-text="lookingUp ? '…' : 'Lookup'"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-sm-auto">
|
|
|
|
|
<label class="form-label text-secondary" for="expirySelect">Expiry</label>
|
|
|
|
|
<select
|
|
|
|
|
id="expirySelect"
|
|
|
|
|
class="form-select"
|
|
|
|
|
style="background:#1e2030; border-color:#2d3045; color:#fff; min-width:160px;"
|
|
|
|
|
x-model="expiry"
|
|
|
|
|
:disabled="loading || expirations.length === 0"
|
|
|
|
|
aria-label="Select expiry date"
|
|
|
|
|
>
|
|
|
|
|
<option value="" disabled>Select expiry…</option>
|
|
|
|
|
<template x-for="exp in expirations" :key="exp">
|
|
|
|
|
<option :value="exp" x-text="exp"></option>
|
|
|
|
|
</template>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-sm-auto">
|
|
|
|
|
<button
|
|
|
|
|
class="btn btn-primary"
|
|
|
|
|
@click="loadSurface()"
|
|
|
|
|
:disabled="loading || !symbol || !expiry"
|
|
|
|
|
aria-label="Load volatility surface"
|
|
|
|
|
>
|
|
|
|
|
<span x-show="loading" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
|
|
|
<span x-text="loading ? 'Loading…' : 'Load Surface'"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="col-12 col-sm-auto ms-sm-auto" x-show="errorMsg" x-cloak>
|
|
|
|
|
<div class="alert alert-danger mb-0 py-2 px-3" role="alert">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
|
|
|
|
fill="none" stroke="currentColor" stroke-width="2" class="me-1">
|
|
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
|
|
</svg>
|
|
|
|
|
<span x-text="errorMsg"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Skew metric badges -->
|
|
|
|
|
<div class="mb-4" x-show="hasData" x-cloak>
|
|
|
|
|
<div class="d-flex flex-wrap gap-3">
|
|
|
|
|
|
|
|
|
|
<div class="stat-inline">
|
|
|
|
|
<span class="stat-label">ATM IV</span>
|
|
|
|
|
<span class="stat-value" x-text="formatPct(currentMetrics.atmIV)"></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="stat-inline">
|
|
|
|
|
<span class="stat-label">RR25</span>
|
|
|
|
|
<span
|
|
|
|
|
class="stat-value"
|
|
|
|
|
:class="{
|
|
|
|
|
'positive': currentMetrics.rr25 > 0.005,
|
|
|
|
|
'negative': currentMetrics.rr25 < -0.005
|
|
|
|
|
}"
|
|
|
|
|
x-text="formatPctSigned(currentMetrics.rr25)"
|
|
|
|
|
></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="stat-inline">
|
|
|
|
|
<span class="stat-label">Fly25</span>
|
|
|
|
|
<span
|
|
|
|
|
class="stat-value"
|
|
|
|
|
:class="{ 'amber': Math.abs(currentMetrics.fly25) > 0.002 }"
|
|
|
|
|
x-text="formatPctSigned(currentMetrics.fly25)"
|
|
|
|
|
></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Greeks card -->
|
|
|
|
|
<div class="card mb-4" style="background:#161824; border:1px solid #2d3045;" x-show="hasData" x-cloak>
|
|
|
|
|
<div class="card-header" style="border-bottom:1px solid #2d3045;">
|
|
|
|
|
<h3 class="card-title text-white mb-0">Greeks</h3>
|
|
|
|
|
<div class="card-options">
|
|
|
|
|
<span class="text-secondary small">ATM & nearest ITM · <span x-text="expiry"></span></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body p-0">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-vcenter skew-table mb-0">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th scope="col">Type</th>
|
|
|
|
|
<th scope="col" class="text-end">Strike</th>
|
|
|
|
|
<th scope="col" class="text-end">IV</th>
|
|
|
|
|
<th scope="col" class="text-end">Mid</th>
|
|
|
|
|
<th scope="col" class="text-end">Delta</th>
|
|
|
|
|
<th scope="col" class="text-end">Gamma</th>
|
|
|
|
|
<th scope="col" class="text-end">Theta</th>
|
|
|
|
|
<th scope="col" class="text-end">Vega</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<template x-if="greeks.atmCall">
|
|
|
|
|
<tr>
|
|
|
|
|
<td><span class="badge bg-blue-lt text-blue">ATM Call</span></td>
|
|
|
|
|
<td class="mono text-end" x-text="greeks.atmCall.strike"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPct(greeks.atmCall.iv)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPrice(greeks.atmCall.midPrice)"></td>
|
|
|
|
|
<td class="mono text-end text-success" x-text="fmtGreek(greeks.atmCall.delta)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtGreek4(greeks.atmCall.gamma)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.atmCall.theta)"></td>
|
|
|
|
|
<td class="mono text-end text-warning" x-text="fmtGreek(greeks.atmCall.vega)"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="greeks.atmPut">
|
|
|
|
|
<tr>
|
|
|
|
|
<td><span class="badge bg-orange-lt text-orange">ATM Put</span></td>
|
|
|
|
|
<td class="mono text-end" x-text="greeks.atmPut.strike"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPct(greeks.atmPut.iv)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPrice(greeks.atmPut.midPrice)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.atmPut.delta)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtGreek4(greeks.atmPut.gamma)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.atmPut.theta)"></td>
|
|
|
|
|
<td class="mono text-end text-warning" x-text="fmtGreek(greeks.atmPut.vega)"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="greeks.itmCall">
|
|
|
|
|
<tr style="border-top:1px solid #2d3045;">
|
|
|
|
|
<td><span class="badge bg-blue-lt" style="color:#8ec8ff;">ITM Call</span></td>
|
|
|
|
|
<td class="mono text-end" x-text="greeks.itmCall.strike"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPct(greeks.itmCall.iv)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPrice(greeks.itmCall.midPrice)"></td>
|
|
|
|
|
<td class="mono text-end text-success" x-text="fmtGreek(greeks.itmCall.delta)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtGreek4(greeks.itmCall.gamma)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.itmCall.theta)"></td>
|
|
|
|
|
<td class="mono text-end text-warning" x-text="fmtGreek(greeks.itmCall.vega)"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="greeks.itmPut">
|
|
|
|
|
<tr>
|
|
|
|
|
<td><span class="badge bg-orange-lt" style="color:#ffb347;">ITM Put</span></td>
|
|
|
|
|
<td class="mono text-end" x-text="greeks.itmPut.strike"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPct(greeks.itmPut.iv)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtPrice(greeks.itmPut.midPrice)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.itmPut.delta)"></td>
|
|
|
|
|
<td class="mono text-end" x-text="fmtGreek4(greeks.itmPut.gamma)"></td>
|
|
|
|
|
<td class="mono text-end text-danger" x-text="fmtGreek(greeks.itmPut.theta)"></td>
|
|
|
|
|
<td class="mono text-end text-warning" x-text="fmtGreek(greeks.itmPut.vega)"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Charts row -->
|
|
|
|
|
<div class="row g-4 mb-4" x-show="hasData" x-cloak>
|
|
|
|
|
|
|
|
|
|
<!-- IV Skew chart -->
|
|
|
|
|
<div class="col-12 col-xl-7">
|
|
|
|
|
<div class="chart-card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<h3 class="card-title text-white mb-0">
|
|
|
|
|
IV Skew —
|
|
|
|
|
<span x-text="symbol"></span>
|
|
|
|
|
<span class="text-secondary ms-1" x-text="expiry"></span>
|
|
|
|
|
</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body p-0">
|
|
|
|
|
<div id="skewChart" style="min-height:320px;" role="img" aria-label="IV skew line chart showing calls and puts by strike price"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Term structure chart -->
|
|
|
|
|
<div class="col-12 col-xl-5">
|
|
|
|
|
<div class="chart-card">
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<h3 class="card-title text-white mb-0">ATM IV Term Structure</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body p-0">
|
|
|
|
|
<div id="termChart" style="min-height:320px;" role="img" aria-label="ATM IV bar chart across expiry dates"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Skew table -->
|
|
|
|
|
<div class="card" style="background:#161824; border:1px solid #2d3045;" x-show="hasData" x-cloak>
|
|
|
|
|
<div class="card-header" style="border-bottom:1px solid #2d3045;">
|
|
|
|
|
<h3 class="card-title text-white mb-0">Per-Expiry Skew Metrics</h3>
|
|
|
|
|
<div class="card-options">
|
|
|
|
|
<span class="text-secondary small" x-text="`${skewTable.length} expiries`"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-vcenter skew-table mb-0" aria-label="Per-expiry skew metrics table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th scope="col">Expiry</th>
|
|
|
|
|
<th scope="col" class="text-end">ATM IV</th>
|
|
|
|
|
<th scope="col" class="text-end">RR25</th>
|
|
|
|
|
<th scope="col" class="text-end">RR10</th>
|
|
|
|
|
<th scope="col" class="text-end">Fly25</th>
|
|
|
|
|
<th scope="col" class="text-center">Skew Direction</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<template x-for="row in skewTable" :key="row.expiry">
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="mono" x-text="row.expiry"></td>
|
|
|
|
|
<td class="mono text-end" x-text="formatPct(row.atmIV)"></td>
|
|
|
|
|
<td
|
|
|
|
|
class="mono text-end"
|
|
|
|
|
:class="{
|
|
|
|
|
'text-success': row.rr25 > 0.005,
|
|
|
|
|
'text-danger': row.rr25 < -0.005
|
|
|
|
|
}"
|
|
|
|
|
x-text="formatPctSigned(row.rr25)"
|
|
|
|
|
></td>
|
|
|
|
|
<td
|
|
|
|
|
class="mono text-end"
|
|
|
|
|
:class="{
|
|
|
|
|
'text-success': row.rr10 > 0.005,
|
|
|
|
|
'text-danger': row.rr10 < -0.005
|
|
|
|
|
}"
|
|
|
|
|
x-text="formatPctSigned(row.rr10)"
|
|
|
|
|
></td>
|
|
|
|
|
<td
|
|
|
|
|
class="mono text-end"
|
|
|
|
|
:class="{ 'text-warning': Math.abs(row.fly25) > 0.002 }"
|
|
|
|
|
x-text="formatPctSigned(row.fly25)"
|
|
|
|
|
></td>
|
|
|
|
|
<td class="text-center">
|
|
|
|
|
<span
|
|
|
|
|
class="badge"
|
|
|
|
|
:class="{
|
|
|
|
|
'bg-danger-lt text-danger': row.rr25 < -0.005,
|
|
|
|
|
'bg-success-lt text-success': row.rr25 > 0.005,
|
|
|
|
|
'bg-secondary-lt text-secondary': row.rr25 >= -0.005 && row.rr25 <= 0.005
|
|
|
|
|
}"
|
|
|
|
|
x-text="row.rr25 < -0.005 ? 'Put Skew' : row.rr25 > 0.005 ? 'Call Skew' : 'Flat'"
|
|
|
|
|
></span>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Empty state -->
|
|
|
|
|
<div class="card text-center py-5" style="background:#161824; border:1px solid #2d3045;" x-show="!hasData && !loading" x-cloak>
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
|
|
|
|
|
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round" class="text-secondary mb-3">
|
|
|
|
|
<path d="M3 3v18h18"></path>
|
|
|
|
|
<path d="M7 16l4-8 4 4 4-4"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<h3 class="text-secondary">No surface loaded</h3>
|
|
|
|
|
<p class="text-muted">Enter a symbol and click Lookup, then select an expiry and click Load Surface.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Loading skeleton -->
|
|
|
|
|
<div x-show="loading" x-cloak>
|
|
|
|
|
<div class="row g-4 mb-4">
|
|
|
|
|
<div class="col-12 col-xl-7">
|
|
|
|
|
<div class="card" style="background:#161824; border:1px solid #2d3045; min-height:340px;">
|
|
|
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
|
|
|
<div class="spinner-border text-primary" role="status">
|
|
|
|
|
<span class="visually-hidden">Loading skew chart…</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-xl-5">
|
|
|
|
|
<div class="card" style="background:#161824; border:1px solid #2d3045; min-height:340px;">
|
|
|
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
|
|
|
<div class="spinner-border text-primary" role="status">
|
|
|
|
|
<span class="visually-hidden">Loading term structure chart…</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
<script src="/assets/viewstate-store.js"></script>
|
2026-05-13 03:22:23 +00:00
|
|
|
<script>
|
|
|
|
|
const CHART_BG = '#1e2030';
|
|
|
|
|
const CHART_GRID = '#2d3045';
|
|
|
|
|
const CHART_LABEL = '#8b95a7';
|
|
|
|
|
const CHART_TOOLTIP_BG = '#1a1c2e';
|
|
|
|
|
const COLOR_CALL = '#4d9ef7'; // blue
|
|
|
|
|
const COLOR_PUT = '#ff8c42'; // orange
|
|
|
|
|
|
|
|
|
|
const BASE_CHART_OPTS = {
|
|
|
|
|
chart: {
|
|
|
|
|
background: CHART_BG,
|
|
|
|
|
foreColor: CHART_LABEL,
|
|
|
|
|
toolbar: { show: false },
|
|
|
|
|
fontFamily: 'inherit',
|
|
|
|
|
animations: { enabled: true, speed: 400 }
|
|
|
|
|
},
|
|
|
|
|
grid: {
|
|
|
|
|
borderColor: CHART_GRID,
|
|
|
|
|
strokeDashArray: 3
|
|
|
|
|
},
|
|
|
|
|
tooltip: {
|
|
|
|
|
theme: 'dark',
|
|
|
|
|
style: { fontSize: '12px' }
|
|
|
|
|
},
|
|
|
|
|
dataLabels: { enabled: false },
|
|
|
|
|
legend: {
|
|
|
|
|
labels: { colors: '#d0d5e0' }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function surfaceApp() {
|
|
|
|
|
return {
|
|
|
|
|
symbol: 'SPY',
|
|
|
|
|
expiry: '',
|
|
|
|
|
expirations: [],
|
|
|
|
|
analytics: {},
|
|
|
|
|
loading: false,
|
|
|
|
|
lookingUp: false,
|
|
|
|
|
hasData: false,
|
|
|
|
|
errorMsg: '',
|
|
|
|
|
skewTable: [],
|
|
|
|
|
currentMetrics: { atmIV: 0, rr25: 0, fly25: 0 },
|
|
|
|
|
greeks: { atmCall: null, atmPut: null, itmCall: null, itmPut: null },
|
|
|
|
|
skewChartInstance: null,
|
|
|
|
|
termChartInstance: null,
|
|
|
|
|
|
|
|
|
|
async init() {
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
// restore last loaded surface (survives navigating away & back)
|
|
|
|
|
const vs = ViewState.load('surface');
|
|
|
|
|
if (vs) {
|
|
|
|
|
this.symbol = vs.symbol ?? this.symbol;
|
|
|
|
|
this.expirations = vs.expirations ?? [];
|
|
|
|
|
this.expiry = vs.expiry ?? '';
|
|
|
|
|
if (vs.analytics) {
|
|
|
|
|
this.analytics = vs.analytics;
|
|
|
|
|
this._processAnalytics(vs.analytics);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_persist(includeAnalytics) {
|
|
|
|
|
ViewState.save('surface', {
|
|
|
|
|
symbol: this.symbol, expirations: this.expirations, expiry: this.expiry,
|
|
|
|
|
analytics: includeAnalytics ? this.analytics : null,
|
|
|
|
|
});
|
2026-05-13 03:22:23 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async fetchExpirations() {
|
|
|
|
|
if (!this.symbol) return;
|
|
|
|
|
this.errorMsg = '';
|
|
|
|
|
this.lookingUp = true;
|
|
|
|
|
this.expirations = [];
|
|
|
|
|
this.expiry = '';
|
|
|
|
|
this.hasData = false;
|
|
|
|
|
try {
|
|
|
|
|
const sym = this.symbol.toUpperCase().trim();
|
|
|
|
|
const res = await fetch(`/api/expirations?symbol=${encodeURIComponent(sym)}`);
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
|
|
|
const env = await res.json();
|
|
|
|
|
const data = env.data ?? env;
|
|
|
|
|
this.expirations = data.expirations || (Array.isArray(data) ? data : []);
|
|
|
|
|
if (this.expirations.length > 0) this.expiry = this.expirations[0];
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
this._persist(false);
|
2026-05-13 03:22:23 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
this.errorMsg = 'Failed to look up symbol: ' + err.message;
|
|
|
|
|
} finally {
|
|
|
|
|
this.lookingUp = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async loadSurface() {
|
|
|
|
|
if (!this.symbol || !this.expiry) return;
|
|
|
|
|
this.loading = true;
|
|
|
|
|
this.errorMsg = '';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
`/api/analytics?symbol=${encodeURIComponent(this.symbol)}&expiry=${encodeURIComponent(this.expiry)}`
|
|
|
|
|
);
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
|
|
|
const env = await res.json();
|
|
|
|
|
const data = env.data ?? env;
|
|
|
|
|
this.analytics = data;
|
|
|
|
|
this._processAnalytics(data);
|
Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves
via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss
with unbounded detection, breakevens, net Greeks, auto-detected strategy name)
- chain.html: per-row Buy/Sell buttons add legs to a localStorage basket;
basket badge in toolbar; auto-scroll to ATM row on load
- Persist per-page view state (symbol, expiry, loaded data, charts) across
navigation via viewstate-store.js for chain/surface/tracker/dashboard
- New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js
- Strategy P/L nav link added to all sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 04:01:57 +00:00
|
|
|
this._persist(true);
|
2026-05-13 03:22:23 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
this.errorMsg = 'Failed to load surface: ' + err.message;
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_processAnalytics(data) {
|
|
|
|
|
const volSurface = data.volSurface || {};
|
|
|
|
|
// skewMetrics is an object keyed by expiry string
|
|
|
|
|
const skewMetrics = data.skewMetrics || {};
|
|
|
|
|
|
|
|
|
|
// Current expiry metrics (field name is atmIv, lowercase v)
|
|
|
|
|
const currentSkew = skewMetrics[this.expiry] || {};
|
|
|
|
|
this.currentMetrics = {
|
|
|
|
|
atmIV: currentSkew.atmIv ?? 0,
|
|
|
|
|
rr25: currentSkew.rr25 ?? 0,
|
|
|
|
|
fly25: currentSkew.fly25 ?? 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build skew table from all expiries
|
|
|
|
|
const allExpiries = volSurface.expiries || Object.keys(skewMetrics);
|
|
|
|
|
this.skewTable = allExpiries.map(exp => {
|
|
|
|
|
const m = skewMetrics[exp] || {};
|
|
|
|
|
return {
|
|
|
|
|
expiry: exp,
|
|
|
|
|
atmIV: m.atmIv ?? 0,
|
|
|
|
|
rr25: m.rr25 ?? 0,
|
|
|
|
|
rr10: m.rr10 ?? 0,
|
|
|
|
|
fly25: m.fly25 ?? 0
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.greeks = data.greeks || { atmCall: null, atmPut: null, itmCall: null, itmPut: null };
|
|
|
|
|
this.hasData = true;
|
|
|
|
|
|
|
|
|
|
// Wait a tick for x-show to render the divs
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this._renderSkewChart(data);
|
|
|
|
|
this._renderTermChart(data);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_renderSkewChart(data) {
|
|
|
|
|
const strikes = data.strikes || [];
|
|
|
|
|
const callIVs = data.callIVs || [];
|
|
|
|
|
const putIVs = data.putIVs || [];
|
|
|
|
|
const spotPrice = data.spot ?? null;
|
|
|
|
|
|
|
|
|
|
const callSeries = callIVs.map(v => v != null ? parseFloat((v * 100).toFixed(2)) : null);
|
|
|
|
|
const putSeries = putIVs.map(v => v != null ? parseFloat((v * 100).toFixed(2)) : null);
|
|
|
|
|
|
|
|
|
|
const annotations = spotPrice != null ? {
|
|
|
|
|
xaxis: [{
|
|
|
|
|
x: spotPrice,
|
|
|
|
|
borderColor: '#ffd43b',
|
|
|
|
|
strokeDashArray: 5,
|
|
|
|
|
label: {
|
|
|
|
|
text: `Spot ${spotPrice}`,
|
|
|
|
|
style: { color: '#ffd43b', background: '#1a1c2e', fontSize: '11px' }
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
} : {};
|
|
|
|
|
|
|
|
|
|
const opts = {
|
|
|
|
|
...BASE_CHART_OPTS,
|
|
|
|
|
chart: {
|
|
|
|
|
...BASE_CHART_OPTS.chart,
|
|
|
|
|
type: 'line',
|
|
|
|
|
height: 320
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{ name: 'Calls', data: callSeries, color: COLOR_CALL },
|
|
|
|
|
{ name: 'Puts', data: putSeries, color: COLOR_PUT }
|
|
|
|
|
],
|
|
|
|
|
xaxis: {
|
|
|
|
|
categories: strikes,
|
|
|
|
|
title: { text: 'Strike', style: { color: CHART_LABEL } },
|
|
|
|
|
labels: {
|
|
|
|
|
style: { colors: CHART_LABEL },
|
|
|
|
|
rotate: -30,
|
|
|
|
|
formatter: v => Number(v).toFixed(0)
|
|
|
|
|
},
|
|
|
|
|
axisBorder: { color: CHART_GRID },
|
|
|
|
|
axisTicks: { color: CHART_GRID }
|
|
|
|
|
},
|
|
|
|
|
yaxis: {
|
|
|
|
|
title: { text: 'Implied Volatility (%)', style: { color: CHART_LABEL } },
|
|
|
|
|
labels: {
|
|
|
|
|
style: { colors: CHART_LABEL },
|
|
|
|
|
formatter: v => v != null ? `${v.toFixed(1)}%` : ''
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
stroke: { width: 2, curve: 'smooth' },
|
|
|
|
|
markers: { size: 0 },
|
|
|
|
|
annotations
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (this.skewChartInstance) {
|
|
|
|
|
this.skewChartInstance.updateOptions(opts, true, true);
|
|
|
|
|
} else {
|
|
|
|
|
this.skewChartInstance = new ApexCharts(document.getElementById('skewChart'), opts);
|
|
|
|
|
this.skewChartInstance.render();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_renderTermChart(data) {
|
|
|
|
|
const skewMetrics = data.skewMetrics || {};
|
|
|
|
|
const volSurface = data.volSurface || {};
|
|
|
|
|
const allExpiries = volSurface.expiries || Object.keys(skewMetrics) || [];
|
|
|
|
|
|
|
|
|
|
const atmValues = allExpiries.map(exp => {
|
|
|
|
|
const m = skewMetrics[exp] || {};
|
|
|
|
|
return m.atmIv != null ? parseFloat((m.atmIv * 100).toFixed(2)) : 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Color gradient: blue (near) to green (far)
|
|
|
|
|
const barColors = allExpiries.map((_, i) => {
|
|
|
|
|
const ratio = allExpiries.length > 1 ? i / (allExpiries.length - 1) : 0;
|
|
|
|
|
const r = Math.round((1 - ratio) * 0x4d + ratio * 0x40);
|
|
|
|
|
const g = Math.round((1 - ratio) * 0x9e + ratio * 0xc0);
|
|
|
|
|
const b = Math.round((1 - ratio) * 0xf7 + ratio * 0x60);
|
|
|
|
|
return `rgb(${r},${g},${b})`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const opts = {
|
|
|
|
|
...BASE_CHART_OPTS,
|
|
|
|
|
chart: {
|
|
|
|
|
...BASE_CHART_OPTS.chart,
|
|
|
|
|
type: 'bar',
|
|
|
|
|
height: 320
|
|
|
|
|
},
|
|
|
|
|
series: [{
|
|
|
|
|
name: 'ATM IV',
|
|
|
|
|
data: atmValues
|
|
|
|
|
}],
|
|
|
|
|
colors: barColors,
|
|
|
|
|
plotOptions: {
|
|
|
|
|
bar: {
|
|
|
|
|
distributed: true,
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
columnWidth: '60%'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
xaxis: {
|
|
|
|
|
categories: allExpiries,
|
|
|
|
|
title: { text: 'Expiry', style: { color: CHART_LABEL } },
|
|
|
|
|
labels: {
|
|
|
|
|
style: { colors: CHART_LABEL },
|
|
|
|
|
rotate: -40,
|
|
|
|
|
rotateAlways: allExpiries.length > 6
|
|
|
|
|
},
|
|
|
|
|
axisBorder: { color: CHART_GRID },
|
|
|
|
|
axisTicks: { color: CHART_GRID }
|
|
|
|
|
},
|
|
|
|
|
yaxis: {
|
|
|
|
|
title: { text: 'ATM IV (%)', style: { color: CHART_LABEL } },
|
|
|
|
|
labels: {
|
|
|
|
|
style: { colors: CHART_LABEL },
|
|
|
|
|
formatter: v => `${v.toFixed(1)}%`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
legend: { show: false },
|
|
|
|
|
tooltip: {
|
|
|
|
|
...BASE_CHART_OPTS.tooltip,
|
|
|
|
|
y: {
|
|
|
|
|
formatter: v => `${v.toFixed(2)}%`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (this.termChartInstance) {
|
|
|
|
|
this.termChartInstance.updateOptions(opts, true, true);
|
|
|
|
|
} else {
|
|
|
|
|
this.termChartInstance = new ApexCharts(document.getElementById('termChart'), opts);
|
|
|
|
|
this.termChartInstance.render();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Greeks formatting helpers
|
|
|
|
|
fmtPct(v) { return v == null || isNaN(v) ? '—' : `${(v*100).toFixed(2)}%`; },
|
|
|
|
|
fmtPrice(v) { return v == null || isNaN(v) ? '—' : `$${v.toFixed(2)}`; },
|
|
|
|
|
fmtGreek(v) { return v == null || isNaN(v) ? '—' : v.toFixed(4); },
|
|
|
|
|
fmtGreek4(v) { return v == null || isNaN(v) ? '—' : v.toFixed(5); },
|
|
|
|
|
|
|
|
|
|
// Formatting helpers
|
|
|
|
|
formatPct(v) {
|
|
|
|
|
if (v == null || isNaN(v)) return '—';
|
|
|
|
|
return `${(v * 100).toFixed(2)}%`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
formatPctSigned(v) {
|
|
|
|
|
if (v == null || isNaN(v)) return '—';
|
|
|
|
|
const pct = (v * 100).toFixed(2);
|
|
|
|
|
return v >= 0 ? `+${pct}%` : `${pct}%`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Demo data for development / when API is unavailable
|
|
|
|
|
_demoExpirations() {
|
|
|
|
|
return [
|
|
|
|
|
'2025-05-16', '2025-05-23', '2025-06-20',
|
|
|
|
|
'2025-07-18', '2025-09-19', '2025-12-19', '2026-01-16'
|
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_demoAnalytics() {
|
|
|
|
|
const expiries = this._demoExpirations();
|
|
|
|
|
const strikes = [];
|
|
|
|
|
const callIVs = [];
|
|
|
|
|
const putIVs = [];
|
|
|
|
|
const spot = 540;
|
|
|
|
|
|
|
|
|
|
for (let k = spot - 60; k <= spot + 60; k += 5) {
|
|
|
|
|
strikes.push(k);
|
|
|
|
|
const moneyness = (k - spot) / spot;
|
|
|
|
|
// Put skew: OTM puts have higher IV
|
|
|
|
|
const callIV = 0.14 + moneyness * 0.05 + Math.random() * 0.005;
|
|
|
|
|
const putIV = 0.14 - moneyness * 0.08 + Math.random() * 0.005;
|
|
|
|
|
callIVs.push(Math.max(0.05, callIV));
|
|
|
|
|
putIVs.push(Math.max(0.05, putIV));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skewMetrics = {};
|
|
|
|
|
expiries.forEach((exp, i) => {
|
|
|
|
|
const dte = (i + 1) * 30;
|
|
|
|
|
const baseVol = 0.12 + 0.04 * Math.sqrt(dte / 30);
|
|
|
|
|
skewMetrics[exp] = {
|
|
|
|
|
atmIv: baseVol + (Math.random() - 0.5) * 0.01,
|
|
|
|
|
rr25: -0.02 - i * 0.003 + (Math.random() - 0.5) * 0.004,
|
|
|
|
|
rr10: -0.035 - i * 0.004,
|
|
|
|
|
fly25: 0.005 + i * 0.001
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
spot,
|
|
|
|
|
strikes,
|
|
|
|
|
callIVs,
|
|
|
|
|
putIVs,
|
|
|
|
|
volSurface: { expiries },
|
|
|
|
|
skewMetrics
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|