summaryrefslogtreecommitdiff
path: root/frontend/app
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-20 00:22:32 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-20 00:22:32 -0700
commit25360aacb8aab46e7e579707eb9704759af9536d (patch)
tree028f654f97dc23c7bc088bc3b625185f4fb71287 /frontend/app
parent68b4f52829cdb2d6951faf8037fb002083ebd0a5 (diff)
feat: implement options tab with Black-Scholes pricer and vol surface
Adds a fully interactive options tab: Terminal view (3-column Bloomberg- style with pricer, chain, smile/term-structure/greek curves) and Surface view (polar smile dial + IV heatmap). Uses synthetic vol surface until a live yfinance chain endpoint is wired up. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'frontend/app')
-rw-r--r--frontend/app/globals.css1
-rw-r--r--frontend/app/options.css490
-rw-r--r--frontend/app/page.tsx87
3 files changed, 538 insertions, 40 deletions
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 7c4ad44..cbbf2be 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -1,3 +1,4 @@
@import "./design-tokens.css";
@import "./prism-shell.css";
+@import "./options.css";
diff --git a/frontend/app/options.css b/frontend/app/options.css
new file mode 100644
index 0000000..4055041
--- /dev/null
+++ b/frontend/app/options.css
@@ -0,0 +1,490 @@
+/* Options Tab */
+
+.opt-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: var(--sp-5);
+ padding-bottom: var(--sp-3);
+ border-bottom: 1px solid var(--line-1);
+ flex-wrap: wrap;
+}
+.opt-header .ticker {
+ display: flex; align-items: baseline; gap: var(--sp-4);
+ flex-wrap: wrap;
+}
+.opt-header .ticker-row { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
+.opt-header .sym {
+ font-family: var(--font-display); font-size: var(--fs-48);
+ font-weight: 500; color: var(--fg-1); line-height: 0.95;
+ letter-spacing: -0.025em;
+}
+.opt-header .name {
+ font-family: var(--font-display); font-style: italic;
+ font-size: var(--fs-18); color: var(--fg-2); font-weight: 400;
+ line-height: 1;
+}
+.opt-header .tab-eyebrow {
+ font-family: var(--font-sans); font-size: var(--fs-12);
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--brass); font-weight: 600;
+ display: block;
+}
+.opt-header .px-block { display: flex; align-items: baseline; gap: 10px; }
+.opt-header .px {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-24); color: var(--fg-1); font-weight: 500; line-height: 1;
+}
+.opt-header .chg {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-13); white-space: nowrap;
+}
+.opt-header .chg.pos { color: var(--positive); }
+.opt-header .chg.neg { color: var(--negative); }
+
+.opt-expiry-bar {
+ display: flex; align-items: center; gap: var(--sp-3);
+ padding: var(--sp-3) 0;
+ flex-wrap: wrap;
+}
+.opt-expiry-bar .lbl {
+ font-family: var(--font-sans); font-size: 11px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600;
+ margin-right: 4px;
+}
+
+.opt-view {
+ display: inline-flex;
+ border: 1px solid var(--line-2);
+ background: var(--ink-2);
+ border-radius: var(--r-1);
+ padding: 3px;
+ gap: 2px;
+}
+.opt-view button {
+ font-family: var(--font-sans); font-size: var(--fs-13);
+ font-weight: 500;
+ background: transparent; border: none; color: var(--fg-3);
+ padding: 6px 14px; cursor: pointer; border-radius: var(--r-1);
+ letter-spacing: 0.02em;
+ display: inline-flex; align-items: center; gap: 6px;
+ transition: color 150ms, background 150ms;
+}
+.opt-view button:hover { color: var(--fg-1); }
+.opt-view button.active {
+ background: var(--ink-0); color: var(--fg-1);
+ box-shadow: inset 0 0 0 1px var(--line-2);
+}
+.opt-view button .glyph {
+ font-family: var(--font-mono); font-size: 11px;
+ color: var(--brass); letter-spacing: 0;
+}
+
+.opt-expiries { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
+.opt-exp-chip {
+ font-family: var(--font-mono); font-size: var(--fs-12);
+ color: var(--fg-2); background: var(--ink-2);
+ border: 1px solid var(--line-2); border-radius: var(--r-1);
+ padding: 4px 10px; cursor: pointer;
+ display: inline-flex; gap: 6px; align-items: baseline;
+ transition: all 150ms;
+ white-space: nowrap; flex-shrink: 0;
+}
+.opt-exp-chip:hover { color: var(--fg-1); border-color: var(--line-3); }
+.opt-exp-chip.active { color: var(--brass-ink); background: var(--brass); border-color: var(--brass); }
+.opt-exp-chip .dte { font-size: 10px; color: var(--fg-3); letter-spacing: 0.06em; }
+.opt-exp-chip.active .dte { color: var(--brass-ink); opacity: 0.75; }
+
+.opt-strip {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ background: var(--ink-1);
+ border: 1px solid var(--line-1);
+ border-radius: var(--r-3);
+ overflow: hidden;
+}
+.opt-strip > div {
+ padding: var(--sp-3) var(--sp-4);
+ border-right: 1px solid var(--line-1);
+ display: flex; flex-direction: column; gap: 4px;
+}
+.opt-strip > div:last-child { border-right: none; }
+.opt-strip .k {
+ font-family: var(--font-sans); font-size: 11px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600;
+}
+.opt-strip .v {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-20); color: var(--fg-1); font-weight: 500; line-height: 1;
+}
+.opt-strip .v.accent { color: var(--brass-bright); }
+.opt-strip .v.gain { color: var(--positive); }
+.opt-strip .v.loss { color: var(--negative); }
+.opt-strip .s { font-family: var(--font-mono); font-size: 11px; color: var(--fg-3); }
+
+.opt-grid {
+ display: grid;
+ grid-template-columns: 320px 1fr 360px;
+ gap: var(--sp-5);
+ align-items: start;
+ min-width: 0;
+}
+.opt-grid.dense { grid-template-columns: 280px 1fr 320px; gap: var(--sp-3); }
+.opt-grid.sparse { grid-template-columns: 340px 1fr 380px; gap: var(--sp-6); }
+.opt-grid > .opt-col { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
+
+@media (max-width: 1280px) {
+ .opt-grid { grid-template-columns: 280px 1fr 320px; gap: var(--sp-4); }
+}
+@media (max-width: 1120px) {
+ .opt-grid {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas: "pricer charts" "chain chain";
+ }
+ .opt-grid > .opt-col:nth-child(1) { grid-area: pricer; }
+ .opt-grid > .opt-col:nth-child(2) { grid-area: chain; }
+ .opt-grid > .opt-col:nth-child(3) { grid-area: charts; }
+}
+
+.opt-pricer {
+ background: var(--ink-1);
+ border: 1px solid var(--line-1);
+ border-radius: var(--r-3);
+ box-shadow: var(--shadow-1);
+ padding: var(--sp-4);
+ display: flex; flex-direction: column; gap: var(--sp-3);
+}
+.opt-pricer .head { display: flex; justify-content: space-between; align-items: baseline; }
+.opt-pricer .head h3 {
+ font-family: var(--font-display); font-size: var(--fs-20);
+ color: var(--fg-1); font-weight: 500; margin: 0;
+}
+
+.opt-cp {
+ display: inline-flex; border: 1px solid var(--line-2);
+ background: var(--ink-2); border-radius: var(--r-1); padding: 2px; gap: 1px;
+}
+.opt-cp button {
+ background: transparent; border: none;
+ color: var(--fg-3); padding: 3px 10px;
+ font-family: var(--font-sans); font-size: var(--fs-12); font-weight: 600;
+ cursor: pointer; border-radius: var(--r-1); letter-spacing: 0.02em;
+}
+.opt-cp button.active { color: var(--fg-1); background: var(--ink-3); }
+.opt-cp button.active.C { color: var(--positive); }
+.opt-cp button.active.P { color: var(--negative); }
+
+.opt-sliders { display: flex; flex-direction: column; gap: var(--sp-2); }
+.opt-slide {
+ display: grid;
+ grid-template-columns: 28px 1fr 76px;
+ align-items: center;
+ gap: var(--sp-2);
+}
+.opt-slide .g {
+ font-family: var(--font-mono); font-size: var(--fs-14);
+ color: var(--brass); font-weight: 500;
+}
+.opt-slide input[type=range] {
+ -webkit-appearance: none; appearance: none;
+ width: 100%; height: 4px; background: var(--ink-3);
+ border-radius: 999px; outline: none; cursor: pointer;
+}
+.opt-slide input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none; appearance: none;
+ width: 14px; height: 14px; border-radius: 50%;
+ background: var(--fg-1); border: 1.5px solid var(--brass);
+ cursor: grab;
+ box-shadow: 0 0 0 2px rgba(194,170,122,0.18);
+}
+.opt-slide input[type=range]::-moz-range-thumb {
+ width: 14px; height: 14px; border-radius: 50%;
+ background: var(--fg-1); border: 1.5px solid var(--brass);
+}
+.opt-slide .val {
+ display: flex; align-items: center; gap: 2px;
+ background: var(--ink-2); border: 1px solid var(--line-2);
+ border-radius: var(--r-1); padding: 2px 6px;
+ justify-content: flex-end;
+}
+.opt-slide .val input {
+ width: 100%; background: transparent; border: none; outline: none;
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-13); color: var(--fg-1); text-align: right; padding: 0;
+}
+.opt-slide .val .unit {
+ font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); margin-left: 1px;
+}
+.opt-slide.market input[type=range]::-webkit-slider-thumb { border-color: var(--positive); }
+.opt-slide .meta {
+ grid-column: 2 / -1;
+ font-family: var(--font-mono); font-size: 10px;
+ color: var(--fg-4); margin-top: -2px;
+ display: flex; gap: 6px; align-items: center;
+}
+.opt-slide .meta button {
+ background: transparent; border: 1px solid var(--line-2);
+ border-radius: var(--r-0); padding: 0 4px;
+ font-family: var(--font-mono); font-size: 9px;
+ color: var(--fg-3); cursor: pointer; letter-spacing: 0.06em;
+}
+.opt-slide .meta button:hover { color: var(--brass); border-color: var(--brass); }
+
+.opt-output {
+ display: grid; grid-template-columns: 1fr 1fr;
+ gap: var(--sp-3);
+ padding: var(--sp-3) var(--sp-3) var(--sp-2);
+ border-top: 1px solid var(--line-1);
+ border-bottom: 1px solid var(--line-1);
+ background: var(--ink-0);
+ border-radius: var(--r-2);
+ align-items: end; min-width: 0;
+}
+.opt-output > div { min-width: 0; }
+.opt-output .fair-lbl {
+ font-family: var(--font-sans); font-size: 11px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600; white-space: nowrap;
+}
+.opt-output .fair {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: clamp(24px, 3vw, 32px);
+ color: var(--brass-bright); font-weight: 500;
+ line-height: 1; letter-spacing: -0.02em;
+ display: flex; align-items: baseline; gap: 4px;
+ overflow: hidden; text-overflow: clip;
+}
+.opt-output .fair .cur { font-size: 0.55em; color: var(--fg-3); }
+.opt-output .mid-lbl {
+ font-family: var(--font-sans); font-size: 11px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600; white-space: nowrap;
+}
+.opt-output .mid {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: clamp(16px, 2.2vw, 20px);
+ color: var(--fg-2); font-weight: 500; line-height: 1;
+}
+.opt-output .delta {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-12); margin-top: 2px;
+}
+.opt-output .delta.pos { color: var(--positive); }
+.opt-output .delta.neg { color: var(--negative); }
+.opt-output .iv-bar {
+ grid-column: 1 / -1;
+ display: grid; grid-template-columns: auto 1fr auto;
+ align-items: center; gap: var(--sp-3);
+ padding-top: var(--sp-2); margin-top: var(--sp-2);
+ border-top: 1px dashed var(--line-1);
+}
+.opt-output .iv-bar .lbl {
+ font-family: var(--font-sans); font-size: 10px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600;
+}
+.opt-output .iv-bar .iv-track {
+ position: relative; height: 4px;
+ background: var(--ink-3); border-radius: 999px;
+}
+.opt-output .iv-bar .iv-mkt {
+ position: absolute; top: -3px; width: 2px; height: 10px;
+ background: var(--fg-3);
+}
+.opt-output .iv-bar .iv-solved {
+ position: absolute; top: -4px; width: 10px; height: 12px;
+ background: var(--brass); border-radius: 2px;
+ transform: translateX(-50%);
+}
+.opt-output .iv-bar .iv-val {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-13); color: var(--brass);
+}
+
+.opt-greeks {
+ display: grid; grid-template-columns: repeat(5, 1fr);
+ gap: 3px; min-width: 0;
+}
+.opt-greek {
+ padding: 6px 2px;
+ background: var(--ink-2); border: 1px solid var(--line-1);
+ border-radius: var(--r-1);
+ display: flex; flex-direction: column; gap: 2px; align-items: center;
+ cursor: default; transition: border-color 150ms; min-width: 0;
+}
+.opt-greek:hover { border-color: var(--brass); }
+.opt-greek .g { font-family: var(--font-mono); font-size: var(--fs-13); color: var(--fg-3); font-weight: 500; }
+.opt-greek .v { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-13); color: var(--fg-1); font-weight: 500; }
+.opt-greek .v.neg { color: var(--negative); }
+.opt-greek .n { font-family: var(--font-sans); font-size: 9px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--fg-4); white-space: nowrap; }
+
+.opt-solve {
+ background: var(--ink-1); border: 1px solid var(--line-1);
+ border-radius: var(--r-3);
+ padding: var(--sp-3) var(--sp-4);
+ display: flex; flex-direction: column; gap: 6px;
+ box-shadow: var(--shadow-1);
+}
+.opt-solve .head {
+ font-family: var(--font-sans); font-size: 11px;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ color: var(--fg-3); font-weight: 600;
+ padding-bottom: 4px; border-bottom: 1px solid var(--line-1); margin-bottom: 2px;
+}
+.opt-solve .row { display: grid; grid-template-columns: 24px 1fr auto; align-items: center; gap: var(--sp-2); padding: 4px 0; }
+.opt-solve .row .g { font-family: var(--font-mono); font-size: var(--fs-14); color: var(--brass); }
+.opt-solve .row .l { font-family: var(--font-sans); font-size: var(--fs-13); color: var(--fg-2); }
+.opt-solve .row .v { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-14); color: var(--fg-1); }
+
+.opt-chain-wrap {
+ background: var(--ink-1); border: 1px solid var(--line-1);
+ border-radius: var(--r-3); overflow: hidden;
+ box-shadow: var(--shadow-1);
+ display: flex; flex-direction: column;
+}
+.opt-chain-head {
+ padding: var(--sp-3) var(--sp-4);
+ display: flex; justify-content: space-between; align-items: baseline;
+ border-bottom: 1px solid var(--line-1);
+ background: var(--ink-1);
+}
+.opt-chain-head h3 { font-family: var(--font-display); font-size: var(--fs-20); color: var(--fg-1); font-weight: 500; margin: 0; }
+.opt-chain-head .sub { font-family: var(--font-mono); font-size: var(--fs-12); color: var(--fg-3); }
+.opt-chain { width: 100%; border-collapse: collapse; }
+.opt-chain th {
+ font-family: var(--font-sans); font-size: 10px; font-weight: 600;
+ color: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-wider);
+ padding: 6px 4px; background: var(--ink-0);
+ border-bottom: 1px solid var(--line-1); text-align: right;
+ position: sticky; top: 0; z-index: 1;
+}
+.opt-chain th.k { text-align: center; color: var(--brass); }
+.opt-chain th.side-c { color: var(--positive); }
+.opt-chain th.side-p { color: var(--negative); }
+.opt-chain th.group {
+ font-family: var(--font-display); font-style: italic;
+ font-size: var(--fs-14); text-transform: none; letter-spacing: 0; padding-bottom: 0;
+}
+.opt-chain td {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-12); color: var(--fg-1);
+ padding: 5px 4px; text-align: right; border-bottom: 1px solid var(--line-1);
+}
+.opt-chain-wrap.compact .opt-chain th { padding: 5px 3px; font-size: 9px; }
+.opt-chain-wrap.compact .opt-chain td { padding: 4px 3px; font-size: 11px; }
+.opt-chain-wrap.compact .opt-chain-head { padding: var(--sp-2) var(--sp-3); }
+.opt-chain-wrap.compact .opt-chain-head h3 { font-size: var(--fs-16); }
+.opt-chain-wrap.compact .opt-chain-head .sub { font-size: 10px; }
+.opt-chain td.k { text-align: center; color: var(--fg-2); font-weight: 500; }
+.opt-chain td.itm { color: var(--fg-1); }
+.opt-chain td.otm { color: var(--fg-3); }
+.opt-chain td.dim { color: var(--fg-4); font-size: var(--fs-12); }
+.opt-chain td.iv { color: var(--brass); }
+.opt-chain tr { cursor: pointer; transition: background 100ms; }
+.opt-chain tr:hover { background: rgba(194,170,122,0.05); }
+.opt-chain tr.atm td { background: rgba(194,170,122,0.10); }
+.opt-chain tr.atm td.k { color: var(--brass-bright); font-weight: 600; }
+.opt-chain tr.selected td { background: rgba(194,170,122,0.22); }
+.opt-chain tr.selected td.k { color: var(--brass-bright); }
+.opt-chain-scroll { max-height: 540px; overflow: auto; }
+
+.opt-chart-card {
+ background: var(--ink-1); border: 1px solid var(--line-1);
+ border-radius: var(--r-3); padding: var(--sp-3) var(--sp-4);
+ box-shadow: var(--shadow-1);
+}
+.opt-chart-card .head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--sp-2); }
+.opt-chart-card h4 { font-family: var(--font-sans); font-size: var(--fs-13); font-weight: 600; color: var(--fg-1); margin: 0; letter-spacing: 0.01em; }
+.opt-chart-card .eyebrow { font-family: var(--font-sans); font-size: 10px; text-transform: uppercase; letter-spacing: var(--tr-wider); color: var(--fg-3); font-weight: 600; }
+
+.opt-svg .axis { stroke: var(--line-2); stroke-width: 1; fill: none; }
+.opt-svg .grid { stroke: var(--line-1); stroke-width: 1; fill: none; stroke-dasharray: 2 4; }
+.opt-svg .curve { stroke: var(--fg-2); stroke-width: 1.5; fill: none; stroke-linecap: round; stroke-linejoin: round; }
+.opt-svg .curve.accent { stroke: var(--brass-bright); stroke-width: 2; }
+.opt-svg .curve.fill { stroke: none; fill: var(--brass); opacity: 0.10; }
+.opt-svg .curve.fade1 { stroke: var(--fg-3); stroke-width: 1.2; opacity: 0.5; }
+.opt-svg .curve.fade2 { stroke: var(--fg-4); stroke-width: 1; opacity: 0.4; }
+.opt-svg .marker { fill: var(--brass-bright); stroke: var(--ink-0); stroke-width: 1.5; }
+.opt-svg .marker-line { stroke: var(--brass); stroke-width: 1; stroke-dasharray: 3 3; }
+.opt-svg text { font-family: var(--font-mono); font-size: 10px; fill: var(--fg-3); font-variant-numeric: tabular-nums; }
+.opt-svg text.label { font-family: var(--font-sans); font-size: 10px; text-transform: uppercase; letter-spacing: var(--tr-wider); fill: var(--fg-4); font-weight: 600; }
+.opt-svg text.brass { fill: var(--brass-bright); }
+.opt-svg text.atm { fill: var(--brass-bright); font-weight: 600; }
+.opt-svg .crosshair { stroke: var(--fg-3); stroke-width: 1; stroke-dasharray: 2 3; }
+
+.opt-greek-multi { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
+.opt-greek-mini {
+ background: var(--ink-0); border: 1px solid var(--line-1);
+ border-radius: var(--r-1); padding: 6px 8px 4px; position: relative;
+}
+.opt-greek-mini .lbl {
+ font-family: var(--font-sans); font-size: 10px; color: var(--fg-3); font-weight: 600;
+ text-transform: uppercase; letter-spacing: var(--tr-wider);
+ display: flex; justify-content: space-between; align-items: baseline;
+}
+.opt-greek-mini .lbl .v {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: var(--fs-12); color: var(--fg-1); letter-spacing: 0; text-transform: none;
+}
+
+.opt-surface {
+ background: var(--ink-1); border: 1px solid var(--line-1);
+ border-radius: var(--r-3); padding: var(--sp-4);
+ box-shadow: var(--shadow-1);
+ display: flex; flex-direction: column; gap: var(--sp-3);
+}
+.opt-surface .head {
+ display: flex; justify-content: space-between; align-items: baseline;
+ padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line-1);
+}
+.opt-surface .head h3 {
+ font-family: var(--font-display); font-size: var(--fs-24);
+ color: var(--fg-1); font-weight: 500; margin: 0; letter-spacing: -0.01em;
+}
+.opt-surface .head h3 em { font-style: italic; color: var(--fg-3); }
+.opt-polar-wrap { display: grid; grid-template-columns: 1fr; align-items: center; justify-items: center; position: relative; }
+.opt-polar { width: 100%; max-width: 720px; height: auto; }
+.opt-polar .ring { stroke: var(--line-2); fill: none; stroke-dasharray: 2 4; }
+.opt-polar .ring.outer { stroke: var(--line-1); stroke-dasharray: 0; }
+.opt-polar .spoke { stroke: var(--line-1); stroke-width: 1; }
+.opt-polar .spoke.atm { stroke: var(--brass); stroke-width: 1.2; }
+.opt-polar .expiry { fill: none; stroke-linejoin: round; stroke-linecap: round; }
+.opt-polar .expiry-fill { stroke: none; }
+.opt-polar text.tick { font-family: var(--font-mono); font-size: 10px; fill: var(--fg-4); }
+.opt-polar text.tick.atm { fill: var(--brass-bright); font-weight: 500; }
+.opt-polar text.iv { font-family: var(--font-mono); font-size: 9px; fill: var(--fg-4); letter-spacing: 0.04em; }
+.opt-polar .eye { fill: var(--ink-0); stroke: var(--brass); stroke-width: 1; }
+.opt-polar text.eye-lbl { font-family: var(--font-sans); font-size: 9px; fill: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-wider); }
+.opt-polar text.eye-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; fill: var(--brass-bright); font-weight: 500; }
+.opt-polar .dot { fill: var(--brass-bright); stroke: var(--ink-0); stroke-width: 2; }
+
+.opt-surface-legend {
+ display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap;
+ font-family: var(--font-mono); font-size: var(--fs-12); color: var(--fg-3);
+}
+.opt-surface-legend .item { display: inline-flex; gap: 6px; align-items: center; cursor: pointer; }
+.opt-surface-legend .swatch { width: 22px; height: 2px; border-radius: 2px; }
+.opt-surface-legend .item.muted { opacity: 0.45; }
+.opt-surface-legend .item.muted:hover { opacity: 1; }
+
+.opt-heat { display: grid; grid-template-columns: 60px 1fr; gap: var(--sp-2); align-items: stretch; }
+.opt-heat .ylabs { display: flex; flex-direction: column; justify-content: space-around; font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); text-align: right; padding-right: 4px; }
+.opt-heat .grid { display: grid; gap: 2px; }
+.opt-heat .cell {
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums;
+ font-size: 10px; text-align: center;
+ padding: 6px 2px; border-radius: 2px; position: relative;
+ transition: transform 100ms; cursor: pointer;
+}
+.opt-heat .cell:hover { transform: scale(1.04); z-index: 2; outline: 1px solid var(--brass); }
+.opt-heat .cell.cursor { outline: 1.5px solid var(--brass-bright); outline-offset: 0; }
+.opt-heat .xlabs { display: grid; gap: 2px; margin-top: 4px; font-family: var(--font-mono); font-size: 10px; color: var(--fg-3); text-align: center; }
+.opt-heat .xlabs span.atm { color: var(--brass-bright); }
+
+.opt-payoff {
+ background: var(--ink-1); border: 1px solid var(--line-1);
+ border-radius: var(--r-3); padding: var(--sp-3) var(--sp-4);
+ box-shadow: var(--shadow-1);
+}
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 5c14488..175f072 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { AppShell } from "@/components/prism/AppShell";
import { FinancialsPage } from "@/components/prism/FinancialsPage";
import { ValuationPage } from "@/components/prism/ValuationPage";
+import { OptionsPage } from "@/components/prism/options/OptionsPage";
import { ChartCard } from "@/components/prism/ChartCard";
import { KPIStrip } from "@/components/prism/KPIStrip";
import { Sidebar } from "@/components/prism/Sidebar";
@@ -322,46 +323,52 @@ function OverviewClient() {
}
>
<MarketStrip indices={marketCards} />
- {!selectedTicker ? <EmptyOverviewState watchlist={watchlist} onSelectTicker={navigateToTicker} /> : null}
- {selectedTicker && overviewState === "loading" ? <LoadingOverviewState symbol={selectedTicker} /> : null}
- {selectedTicker && overviewState === "invalid" ? <InvalidTickerState symbol={selectedTicker} onClear={clearTicker} /> : null}
- {selectedTicker && overviewState === "error" ? <ErrorOverviewState message={overviewError || "Ticker data unavailable"} /> : null}
- {overview && overviewState === "ready" ? (
- tab === "valuation" ? (
- <ValuationPage
- ticker={selectedTicker}
- overview={overview}
- isSaved={isSaved}
- onToggleWatchlist={addOrRemoveCurrentTicker}
- />
- ) : tab === "financials" ? (
- <FinancialsPage
- ticker={selectedTicker}
- overview={overview}
- isSaved={isSaved}
- onToggleWatchlist={addOrRemoveCurrentTicker}
- />
- ) : (
- <>
- <TickerHeader overview={overview} onToggleWatchlist={addOrRemoveCurrentTicker} isSaved={isSaved} />
- <KPIStrip items={kpis} />
- <div className="psm-main-grid">
- <div className="psm-column">
- <ChartCard symbol={overview.profile.symbol} period={period} points={history} chartState={chartState} chartError={chartError} onChangePeriod={setPeriod} />
- <VolumeCard overview={overview} />
- <SignalCard overview={overview} />
- <PriceVsValueCard overview={overview} valuation={valuation} valState={valState} />
- </div>
- <div className="psm-column">
- <ProfileCard overview={overview} />
- <ShortInterestCard overview={overview} />
- <ValuationOverviewCard overview={overview} valuation={valuation} valState={valState} />
- <QualityCard overview={overview} />
- </div>
- </div>
- </>
- )
- ) : null}
+ {tab === "options" ? (
+ <OptionsPage ticker={selectedTicker} overview={overview} />
+ ) : (
+ <>
+ {!selectedTicker ? <EmptyOverviewState watchlist={watchlist} onSelectTicker={navigateToTicker} /> : null}
+ {selectedTicker && overviewState === "loading" ? <LoadingOverviewState symbol={selectedTicker} /> : null}
+ {selectedTicker && overviewState === "invalid" ? <InvalidTickerState symbol={selectedTicker} onClear={clearTicker} /> : null}
+ {selectedTicker && overviewState === "error" ? <ErrorOverviewState message={overviewError || "Ticker data unavailable"} /> : null}
+ {overview && overviewState === "ready" ? (
+ tab === "valuation" ? (
+ <ValuationPage
+ ticker={selectedTicker}
+ overview={overview}
+ isSaved={isSaved}
+ onToggleWatchlist={addOrRemoveCurrentTicker}
+ />
+ ) : tab === "financials" ? (
+ <FinancialsPage
+ ticker={selectedTicker}
+ overview={overview}
+ isSaved={isSaved}
+ onToggleWatchlist={addOrRemoveCurrentTicker}
+ />
+ ) : (
+ <>
+ <TickerHeader overview={overview} onToggleWatchlist={addOrRemoveCurrentTicker} isSaved={isSaved} />
+ <KPIStrip items={kpis} />
+ <div className="psm-main-grid">
+ <div className="psm-column">
+ <ChartCard symbol={overview.profile.symbol} period={period} points={history} chartState={chartState} chartError={chartError} onChangePeriod={setPeriod} />
+ <VolumeCard overview={overview} />
+ <SignalCard overview={overview} />
+ <PriceVsValueCard overview={overview} valuation={valuation} valState={valState} />
+ </div>
+ <div className="psm-column">
+ <ProfileCard overview={overview} />
+ <ShortInterestCard overview={overview} />
+ <ValuationOverviewCard overview={overview} valuation={valuation} valState={valState} />
+ <QualityCard overview={overview} />
+ </div>
+ </div>
+ </>
+ )
+ ) : null}
+ </>
+ )}
</AppShell>
);