position-tracker/public/index.html
Manohar dca5fea679 feat: add Analysis tab with position intelligence engine
Phase 1 of the position-tracker → analysis port:

Backend:
- src/ai/types.ts       — shared types (AnalysisInput, ClassifiedPosition, AnalysisResult, etc.)
- src/ai/greeks.ts      — added calcTheoreticalPrice() export for scenario analysis
- src/ai/classifier.ts  — pure-function position classifier (DEAD_WEIGHT/WORKING/UNDER_PRESSURE/HEDGE/DANGER_SHORT) with module-level hysteresis Map
- src/ai/portfolio-greeks.ts — aggregates per-unit Greeks × quantity into portfolio totals
- src/ai/risk-assessor.ts   — naked short detection, concentration, side imbalance, Greeks breach checks
- src/ai/scenario.ts        — P&L grid at ±100..±1000 spot moves per underlying
- src/ai/analyze.ts         — orchestrator: maps Position[] → AnalysisInput[], reads spot from market_cache, stores JSON in analysis_snapshots
- src/db/client.ts      — analysis_snapshots table + index
- src/tracker/poll.ts   — runAnalysis() called after every pollTick + forcePoll
- src/api/server.ts     — GET /api/analysis endpoint

Frontend (public/index.html):
- Multi-page nav with Dashboard | Analysis tabs (showView())
- Analysis view: summary cards (delta/theta/vega/risk level), action list (urgent positions sorted by urgency score), classification table with urgency bars, risk assessment details, scenario P&L grid per underlying
- loadAnalysis() + renderAnalysis() wired to tab switch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:07:24 +05:30

944 lines
62 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Angel · Position Tracker</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23FF6B4A'/%3E%3Cpolyline points='4,22 10,14 16,18 22,8 28,11' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root{--coral:#FF6B4A;--amber:#F5A623;--green:#2ECC71;--red:#E74C3C;--tr:.22s cubic-bezier(.4,0,.2,1)}
[data-theme="dark"]{--bg:#0D1117;--bg2:#161B22;--bg3:#1C2330;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#E6EDF3;--text2:#8B949E;--text3:#3D444D;--card:rgba(22,27,34,.97);--shadow:0 4px 28px rgba(0,0,0,.45);--shadow-lg:0 8px 48px rgba(0,0,0,.6);--glass:rgba(22,27,34,.8)}
[data-theme="light"]{--bg:#FAF7F4;--bg2:#FFF;--bg3:#F0EBE5;--border:rgba(0,0,0,.06);--border2:rgba(0,0,0,.13);--text:#1A1410;--text2:#6B6056;--text3:#C4B8B0;--card:rgba(255,255,255,.97);--shadow:0 4px 20px rgba(0,0,0,.08);--shadow-lg:0 8px 40px rgba(0,0,0,.13);--glass:rgba(255,255,255,.85)}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Geist',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;transition:background var(--tr),color var(--tr);overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:radial-gradient(ellipse 80vw 55vh at 8% 0%,rgba(255,107,74,.055) 0%,transparent 60%),radial-gradient(ellipse 60vw 45vh at 92% 100%,rgba(245,166,35,.04) 0%,transparent 60%)}
.wrap{max-width:1300px;margin:0 auto;padding:20px;position:relative;z-index:1}
nav{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;margin-bottom:20px;background:var(--glass);border:1px solid var(--border);border-radius:16px;backdrop-filter:blur(16px);box-shadow:var(--shadow)}
.nav-l{display:flex;align-items:center;gap:10px}
.logo{font-family:'DM Serif Display',serif;font-size:1.15rem;color:var(--coral);letter-spacing:-.02em}
.nav-sep{width:1px;height:18px;background:var(--border2)}
.nav-sub{font-size:.72rem;font-weight:600;color:var(--text2);letter-spacing:.07em;text-transform:uppercase}
.nav-r{display:flex;align-items:center;gap:8px}
.ts{font-size:.7rem;color:var(--text3);font-family:'Geist Mono',monospace}
.pill{display:flex;align-items:center;gap:5px;padding:4px 11px;border-radius:9999px;font-size:.7rem;font-weight:600;border:1px solid var(--border2);color:var(--text2);background:var(--bg3);transition:all var(--tr)}
.pill.open{color:var(--green);border-color:rgba(46,204,113,.3);background:rgba(46,204,113,.12)}
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}
.pill.open .dot,.ldot{animation:blink 2s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
.ibtn{width:34px;height:34px;border-radius:9px;border:1px solid var(--border2);background:var(--bg3);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);font-size:15px}
.ibtn:hover{background:var(--bg2);color:var(--text);border-color:var(--coral);transform:translateY(-1px)}
.g3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:16px}
.g6{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:16px}
.card{background:var(--card);border:1px solid var(--border);border-radius:18px;box-shadow:var(--shadow);transition:transform var(--tr),box-shadow var(--tr);overflow:hidden}
.card:hover{transform:translateY(-1px);box-shadow:var(--shadow-lg)}
.card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border)}
.card-title{font-size:.78rem;font-weight:600;letter-spacing:.03em}
.pcard{padding:22px 22px 18px;position:relative;overflow:hidden}
.pcard::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--ca),var(--cb));border-radius:18px 18px 0 0}
.pcard::after{content:'';position:absolute;top:-30px;right:-30px;width:100px;height:100px;border-radius:50%;background:var(--ca);opacity:.07;filter:blur(25px);pointer-events:none}
.pcard.pu{--ca:#2ECC71;--cb:#1abc9c}.pcard.pr{--ca:#3498DB;--cb:#2980b9}.pcard.pt{--ca:#FF6B4A;--cb:#F5A623}
.plbl{font-size:.67rem;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--text2);margin-bottom:8px}
.pval{font-family:'DM Serif Display',serif;font-size:2.2rem;line-height:1;letter-spacing:-.02em;margin-bottom:6px}
.pcard.pt .pval{font-size:2.7rem}
.psub{font-size:.68rem;color:var(--text3)}
.up{color:var(--green)}.dn{color:var(--red)}.fl{color:var(--text2)}
.mcard{padding:14px 16px}
.mlbl{font-size:.63rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:6px}
.mprice{font-family:'DM Serif Display',serif;font-size:1.35rem;letter-spacing:-.01em;color:var(--text);line-height:1.1}
.mchg{font-size:.68rem;font-family:'Geist Mono',monospace;margin-top:3px}
.mstale{font-size:.6rem;color:var(--text3);margin-top:2px}
.scard{padding:14px 16px}
.slbl{font-size:.67rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text2);margin-bottom:5px}
.sval{font-family:'DM Serif Display',serif;font-size:1.5rem;color:var(--text)}
.sval.amber{color:var(--amber)}
.range-btns{display:flex;gap:3px}
.rbtn{padding:3px 10px;border-radius:7px;font-size:.7rem;font-weight:600;border:1px solid var(--border2);cursor:pointer;background:transparent;color:var(--text2);transition:all .12s}
.rbtn:hover{color:var(--text);border-color:var(--coral)}.rbtn.on{background:var(--coral);border-color:var(--coral);color:#fff}
.chart-wrap{height:190px;position:relative;padding:12px 16px 16px}
.chart-empty{display:none;position:absolute;inset:0;align-items:center;justify-content:center;color:var(--text3);font-size:.78rem}
.chart-empty.show{display:flex}
.live-badge{display:flex;align-items:center;gap:5px;font-size:.67rem;font-weight:600;letter-spacing:.06em;color:var(--green);text-transform:uppercase}
.ldot{width:5px;height:5px;border-radius:50%;background:var(--green)}
table{width:100%;border-collapse:collapse}
thead th{padding:9px 18px;text-align:right;font-size:.63rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text3);border-bottom:1px solid var(--border)}
thead th:first-child{text-align:left}
tbody tr{transition:background .1s}tbody tr:hover{background:rgba(255,107,74,.025)}
tbody tr+tr{border-top:1px solid var(--border)}
td{padding:11px 18px;font-size:.8rem;text-align:right;color:var(--text)}td:first-child{text-align:left}
.sym{font-weight:600;font-size:.78rem}.sym-meta{font-size:.65rem;color:var(--text3);margin-top:1px;font-family:'Geist Mono',monospace}
.tte{font-size:.62rem;color:var(--amber);margin-top:2px;font-family:'Geist Mono',monospace}
.mono{font-family:'Geist Mono',monospace;font-size:.78rem}
.ql{color:var(--green);font-weight:600;font-family:'Geist Mono',monospace}.qs{color:var(--red);font-weight:600;font-family:'Geist Mono',monospace}
tfoot tr{border-top:2px solid var(--border2);background:var(--bg3)}
tfoot td{padding:10px 18px;font-weight:600;font-size:.78rem}
tfoot td:first-child{font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;color:var(--text2)}
.alert-up td:first-child{border-left:3px solid var(--green)}.alert-dn td:first-child{border-left:3px solid var(--red)}
.sg{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px}
.field{display:flex;flex-direction:column;gap:5px}
.field label{font-size:.67rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2)}
.field input{padding:8px 12px;border-radius:9px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.82rem;font-family:'Geist Mono',monospace;transition:border-color var(--tr);outline:none}
.field input:focus{border-color:var(--coral)}
.hint{font-size:.62rem;color:var(--text3)}
.btn-save{padding:8px 22px;border-radius:9px;background:var(--coral);border:none;color:#fff;font-size:.78rem;font-weight:600;cursor:pointer;transition:all var(--tr)}
.btn-save:hover{background:#e55a3a;transform:translateY(-1px)}
.save-msg{font-size:.72rem;color:var(--green);display:none;margin-left:10px}
/* ── Collapsible ── */
.collapsible > .card-head { cursor:pointer; user-select:none; }
.collapse-arrow { margin-left:auto; font-size:.75rem; color:var(--text3); transition:transform .25s ease; display:inline-block; }
.collapsible.collapsed .collapse-arrow { transform:rotate(-90deg); }
.collapse-body { overflow:hidden; transition:max-height .35s cubic-bezier(.4,0,.2,1); max-height:5000px; }
.collapsible.collapsed .collapse-body { max-height:0 !important; }
/* ── Mobile responsive ── */
@media (max-width: 768px) {
.wrap { padding: 12px; }
nav { padding: 10px 14px; flex-wrap: wrap; gap: 8px; }
.nav-sub { display: none; }
.ts { display: none; }
.g3, .g6 { grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
.g6 { grid-template-columns: repeat(3, 1fr); }
.pcard { padding: 16px 14px 12px; }
.pval { font-size: 1.6rem; }
.pcard.pt .pval { font-size: 2rem; }
.mcard { padding: 10px 12px; }
.mprice { font-size: 1.1rem; }
.scard { padding: 10px 12px; }
.card-head { padding: 12px 14px; flex-wrap: wrap; gap: 6px; }
.chart-wrap { height: 160px; padding: 8px 10px 12px; }
.range-btns { gap: 2px; }
.rbtn { padding: 2px 7px; font-size: .65rem; }
/* Tables: horizontal scroll */
.card > .collapse-body { overflow-x: auto; }
table { min-width: 520px; }
td, thead th { padding: 9px 10px; font-size: .75rem; }
.sym { font-size: .72rem; }
.sym-meta, .tte { font-size: .6rem; }
.mono { font-size: .72rem; }
/* Settings */
.sg { grid-template-columns: 1fr; gap: 10px; }
tfoot td { padding: 8px 10px; font-size: .72rem; }
}
@media (max-width: 480px) {
.g3 { grid-template-columns: 1fr; }
.g6 { grid-template-columns: repeat(2, 1fr); }
.pval { font-size: 1.4rem; }
.pcard.pt .pval { font-size: 1.7rem; }
}
.pf-stat{padding:10px 14px;border-right:1px solid var(--border);text-align:center}.pf-stat:last-child{border-right:none}.pfs-l{font-size:.6rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text3);margin-bottom:4px}.pfs-v{font-family:'DM Serif Display',serif;font-size:1.05rem;color:var(--text)}
.exp-tab{padding:2px 9px;border-radius:7px;font-size:.65rem;font-weight:600;border:1px solid var(--border2);cursor:pointer;background:transparent;color:var(--text2);transition:all .12s;font-family:'Geist Mono',monospace;}
.exp-tab:hover{border-color:var(--coral);color:var(--text)}
.exp-tab.active{background:var(--coral);border-color:var(--coral);color:#fff}
.nav-tabs{display:flex;gap:3px}
.nav-tab{padding:5px 14px;border-radius:9px;font-size:.72rem;font-weight:600;border:1px solid var(--border2);cursor:pointer;background:transparent;color:var(--text2);transition:all .12s;font-family:'Geist',sans-serif}
.nav-tab:hover{color:var(--text);border-color:var(--coral)}
.nav-tab.active{background:var(--coral);border-color:var(--coral);color:#fff}
@media(max-width:768px){.a-sum-grid{grid-template-columns:1fr 1fr!important}}
</style>
</head>
<body>
<div class="wrap">
<nav>
<div class="nav-l">
<span class="logo">Angel</span>
<div class="nav-sep"></div>
<div class="nav-tabs">
<button class="nav-tab active" data-target="dashboard" onclick="showView('dashboard')">Dashboard</button>
<button class="nav-tab" data-target="analysis" onclick="showView('analysis')">Analysis</button>
</div>
</div>
<div class="nav-r">
<span class="ts" id="ts">&#8212;</span>
<span id="s-err" style="display:none;font-size:.65rem;font-family:'Geist Mono',monospace"></span>
<div class="pill" id="mkt-pill"><span class="dot"></span><span id="mkt-lbl">Checking</span></div>
<button class="ibtn" id="theme-btn" onclick="toggleTheme()">&#9728;&#65039;</button>
<button class="ibtn" onclick="refresh()">&#8635;</button>
</div>
</nav>
<div data-view="dashboard" id="view-dashboard">
<div class="g3">
<div class="card pcard pu"><div class="plbl">Unrealised P&amp;L</div><div class="pval fl" id="v-u">&#8212;</div><div class="psub">Open positions MTM</div></div>
<div class="card pcard pr"><div class="plbl">Realised P&amp;L</div><div class="pval fl" id="v-r">&#8212;</div><div class="psub">Partial + full exits today</div></div>
<div class="card pcard pt"><div class="plbl">Total P&amp;L</div><div class="pval fl" id="v-t">&#8212;</div><div class="psub">Unrealised + Realised</div></div>
</div>
<div class="g6" id="market-grid">
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
<div class="card mcard"><div class="mlbl">Loading&#8230;</div><div class="mprice">&#8212;</div></div>
</div>
<div class="g3" style="margin-bottom:16px">
<div class="card scard"><div class="slbl">Open Positions</div><div class="sval" id="s-pos">&#8212;</div></div>
<div class="card scard"><div class="slbl">Booked P&amp;L Today</div><div class="sval" id="s-booked">&#8212;</div><div style="font-size:.6rem;color:var(--text3);margin-top:2px">Closed legs realised</div></div>
<div class="card scard"><div class="slbl">Alerts Today</div><div class="sval amber" id="s-alrt">&#8212;</div></div>
</div>
<div class="card collapsible" id="sec-chart" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-chart')">
<span class="card-title">Intraday P&amp;L Curve</span>
<div class="range-btns">
<button class="rbtn" onclick="loadChart(2)" id="b2">2H</button>
<button class="rbtn on" onclick="loadChart(8)" id="b8">8H</button>
<button class="rbtn" onclick="loadChart(24)" id="b24">24H</button>
<button class="rbtn" onclick="loadChart(72)" id="b72">3D</button>
</div>
<span class="collapse-arrow">&#9660;</span>
</div>
<div class="collapse-body">
<div class="chart-wrap"><canvas id="pnl-chart"></canvas><div class="chart-empty" id="chart-empty">No history yet &#8212; populates after first poll cycle</div></div>
</div>
</div>
<div class="card collapsible" id="sec-pos" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-pos')"><span class="card-title">Option Positions</span><div style="display:flex;align-items:center;gap:10px"><div class="live-badge"><span class="ldot"></span>Live</div><span id="pos-hpnl" style="font-family:'Geist Mono',monospace;font-size:.75rem;font-weight:600;display:none"></span></div><span class="collapse-arrow">&#9660;</span></div>
<div id="expiry-tabs-row" style="padding:8px 16px 0;display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--border)"></div>
<div class="collapse-body"><table>
<thead><tr><th>Symbol</th><th>Qty</th><th>LTP</th><th>Avg Cost</th><th>Unrealised</th><th>Realised</th><th>Total P&amp;L</th></tr></thead>
<tbody id="pos-body"><tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">Loading&#8230;</td></tr></tbody>
<tfoot id="pos-foot" style="display:none"><tr><td colspan="4">Portfolio Total</td><td id="f-u" class="mono">&#8212;</td><td id="f-r" class="mono">&#8212;</td><td id="f-t" class="mono">&#8212;</td></tr></tfoot>
</table></div>
</div>
<div class="card collapsible" id="sec-payoff" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-payoff')">
<span class="card-title">Strategy Payoff at Expiry</span>
<div style="display:flex;align-items:center;gap:8px">
<span id="pf-ul" style="font-size:.7rem;font-weight:600;color:var(--text2);font-family:Geist Mono,monospace"></span>
<span id="pf-strat" style="font-size:.68rem;padding:2px 8px;border-radius:9999px;background:var(--bg3);border:1px solid var(--border2);color:var(--text2)"></span>
</div>
<span class="collapse-arrow">&#9660;</span>
</div>
<div class="collapse-body">
<div style="display:grid;grid-template-columns:repeat(6,1fr);border-bottom:1px solid var(--border)">
<div class="pf-stat"><div class="pfs-l">NET PREMIUM</div><div class="pfs-v" id="pf-prem">&#8212;</div></div>
<div class="pf-stat"><div class="pfs-l">MAX PROFIT</div><div class="pfs-v up" id="pf-maxp">&#8212;</div></div>
<div class="pf-stat"><div class="pfs-l">MAX LOSS</div><div class="pfs-v dn" id="pf-maxl">&#8212;</div></div>
<div class="pf-stat"><div class="pfs-l">REWARD/RISK</div><div class="pfs-v" id="pf-rr">&#8212;</div></div>
<div class="pf-stat"><div class="pfs-l">BREAKEVEN(S)</div><div class="pfs-v" id="pf-be" style="font-size:.8rem">&#8212;</div></div>
<div class="pf-stat"><div class="pfs-l">DTE</div><div class="pfs-v" id="pf-dte">&#8212;</div></div>
</div>
<div style="padding:14px 16px 10px;height:250px;position:relative">
<canvas id="payoff-chart"></canvas>
<div id="pf-empty" style="display:none;position:absolute;inset:0;align-items:center;justify-content:center;color:var(--text3);font-size:.8rem;flex-direction:column;gap:6px">
<span style="font-size:1.4rem">&#128200;</span><span>No option positions to analyse</span>
</div>
</div>
<div style="padding:0 16px 10px;font-size:.63rem;color:var(--text3)">Theoretical payoff at expiry based on avg cost. Does not account for time value or IV. Current spot shown as vertical line.</div>
</div>
</div>
<div class="card collapsible" id="sec-closed" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-closed')">
<span class="card-title">Closed / Booked Positions</span>
<div style="display:flex;align-items:center;gap:8px">
<span id="closed-total" style="font-family:'Geist Mono',monospace;font-size:.75rem;font-weight:600;display:none"></span>
<span style="font-size:.65rem;color:var(--text3)">netqty = 0 today</span>
</div>
<span class="collapse-arrow">&#9660;</span>
</div>
<div class="collapse-body">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Type</th>
<th>Strike</th>
<th>Expiry</th>
<th>Exit LTP</th>
<th>Realised P&amp;L</th>
<th>Status</th>
</tr>
</thead>
<tbody id="closed-tbody">
<tr><td colspan="7" style="text-align:center;padding:28px;color:var(--text3)">Loading&#8230;</td></tr>
</tbody>
<tfoot id="closed-tfoot" style="display:none">
<tr>
<td colspan="5">Total Booked Today</td>
<td id="closed-foot" class="mono">&#8212;</td>
<td></td>
</tr>
</tfoot>
</table>
<div id="closed-stale" style="display:none;padding:10px 18px;font-size:.7rem;color:var(--amber);border-top:1px solid var(--border)"></div>
</div>
</div>
<div class="card collapsible" id="sec-alrt" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-alrt')"><span class="card-title">Alert History</span><div style="display:flex;align-items:center;gap:8px"><span id="alrt-hcount" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(245,166,35,.15);color:var(--amber);display:none"></span><span id="alrt-hmuted" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(231,76,60,.12);color:var(--red);display:none">MUTED</span></div><span class="collapse-arrow">&#9660;</span></div>
<div class="collapse-body"><table>
<thead><tr><th>Symbol</th><th>P&amp;L at Alert</th><th>Anchor</th><th>Move</th><th>LTP</th><th>Time (IST)</th></tr></thead>
<tbody id="alrt-body"><tr><td colspan="6" style="text-align:center;padding:28px;color:var(--text3)">No alerts yet</td></tr></tbody>
</table></div>
</div>
<div class="card collapsible" id="sec-cfg" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('sec-cfg')">
<span class="card-title">Alert Settings</span>
<span style="font-size:.68rem;color:var(--text3)">Changes apply immediately</span>
<span class="collapse-arrow">&#9660;</span></div>
<div class="collapse-body"><div style="padding:16px 20px">
<div class="sg">
<div class="field"><label>Global Threshold %</label><input id="cfg-pct" type="number" min="1" max="50" step="0.5" value="5"/><span class="hint">Alert when P&amp;L moves this % from anchor</span></div>
<div class="field"><label>Min Absolute Move (&#8377;)</label><input id="cfg-abs" type="number" min="50" max="10000" step="50" value="100"/><span class="hint">Guard for near-zero P&amp;L positions</span></div>
<div class="field"><label>Poll Interval (sec)</label><input id="cfg-poll" type="number" value="60" disabled style="opacity:.5;cursor:not-allowed"/><span class="hint">Requires restart to change</span></div>
</div>
<div style="display:flex;align-items:center;margin-bottom:18px">
<button class="btn-save" onclick="saveConfig()">Save Settings</button>
<span class="save-msg" id="save-msg">&#10003; Saved</span>
</div>
<div style="border-top:1px solid var(--border);padding-top:14px">
<div style="font-size:.68rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:10px">Per-position Overrides</div>
<table>
<thead><tr><th>Symbol</th><th>Custom Threshold %</th><th>Muted Until (IST)</th><th>Action</th></tr></thead>
<tbody id="override-body"><tr><td colspan="4" style="text-align:center;padding:16px;color:var(--text3)">Load positions first</td></tr></tbody>
</table>
</div>
</div>
</div>
</div><!-- end view-dashboard -->
<div data-view="analysis" id="view-analysis" style="display:none">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px" class="a-sum-grid">
<div class="card pcard pu"><div class="plbl">Net Delta</div><div class="pval fl" id="a-delta">&#8212;</div><div class="psub">Directional exposure</div></div>
<div class="card pcard" style="--ca:#F5A623;--cb:#E67E22"><div class="plbl">Net Theta / day</div><div class="pval fl" id="a-theta">&#8212;</div><div class="psub">Daily time decay (&#8377;)</div></div>
<div class="card pcard pr"><div class="plbl">Net Vega</div><div class="pval fl" id="a-vega">&#8212;</div><div class="psub">Vol sensitivity</div></div>
<div class="card pcard pt"><div class="plbl">Risk Level</div><div class="pval fl" id="a-risk-lvl">&#8212;</div><div class="psub" id="a-risk-sub">&#8212;</div></div>
</div>
<div class="card collapsible" id="a-sec-class" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('a-sec-class')">
<span class="card-title">Position Intelligence</span>
<div id="a-class-badges" style="display:flex;gap:5px;flex-wrap:wrap"></div>
<span class="collapse-arrow">&#9660;</span>
</div>
<div class="collapse-body">
<div id="a-action-list" style="padding:10px 18px 0;display:none"></div>
<table>
<thead><tr><th>Symbol</th><th>Category</th><th>DTE</th><th>IV%</th><th>Pos&#x394;</th><th>&#x398;/day</th><th>Unrealised</th><th>Urgency</th></tr></thead>
<tbody id="a-class-body"><tr><td colspan="8" style="text-align:center;padding:36px;color:var(--text3)">Waiting for first poll with market data&#8230;</td></tr></tbody>
</table>
</div>
</div>
<div class="card collapsible" id="a-sec-risk" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('a-sec-risk')"><span class="card-title">Risk Assessment</span><span class="collapse-arrow">&#9660;</span></div>
<div class="collapse-body"><div id="a-risk-body" style="padding:14px 20px;color:var(--text3);font-size:.8rem">Loading&#8230;</div></div>
</div>
<div class="card collapsible" id="a-sec-scen" style="margin-bottom:16px">
<div class="card-head" onclick="toggleCard('a-sec-scen')">
<span class="card-title">Scenario Analysis</span>
<span style="font-size:.68rem;color:var(--text3)">P&amp;L at SENSEX spot moves (fixed vol)</span>
<span class="collapse-arrow">&#9660;</span>
</div>
<div class="collapse-body"><div id="a-scen-body" style="padding:14px 20px;color:var(--text3);font-size:.8rem">Loading&#8230;</div></div>
</div>
<div style="font-size:.62rem;color:var(--text3);text-align:center;padding-bottom:20px">
Greeks from Black-Scholes with Newton-Raphson IV solver. Scenarios hold vol surface fixed.
<span id="a-asof" style="margin-left:8px"></span>
</div>
</div><!-- end view-analysis -->
</div><!-- end .wrap -->
<script>
let theme=localStorage.getItem('theme')||'dark';
document.documentElement.setAttribute('data-theme',theme);
document.getElementById('theme-btn').textContent=theme==='dark'?'\u2600\ufe0f':'\ud83c\udf19';
function toggleTheme(){theme=theme==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',theme);document.getElementById('theme-btn').textContent=theme==='dark'?'\u2600\ufe0f':'\ud83c\udf19';localStorage.setItem('theme',theme);if(chartInst){chartInst.destroy();chartInst=null;loadChart(curH);}}
const fmt=(n,d=0)=>n==null?'\u2014':(n>=0?'+\u20b9':'-\u20b9')+Math.abs(n).toLocaleString('en-IN',{maximumFractionDigits:d});
const cls=n=>n>50?'up':n<-50?'dn':'fl';
const toIST=s=>new Date(s+(s.includes('Z')?'':'Z')).toLocaleString('en-IN',{timeZone:'Asia/Kolkata',hour12:false,month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
function parseTTE(sym){const m=sym.match(/([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/);if(!m)return null;const exp=new Date('20'+m[2]+'-'+m[3].padStart(2,'0')+'-'+m[4]);if(isNaN(exp.getTime()))return null;const diff=Math.ceil((exp-new Date())/86400000);if(diff<0)return null;if(diff===0)return'\u26a1 Expires today';if(diff===1)return'\u26a0\ufe0f 1 day left';return diff+'d to expiry';}
let chartInst=null,curH=8;
async function loadChart(h=8){
curH=h;['b2','b8','b24','b72'].forEach(id=>document.getElementById(id)?.classList.remove('on'));
document.getElementById({2:'b2',8:'b8',24:'b24',72:'b72'}[h])?.classList.add('on');
const {summary,history}=await fetch('/api/pnl-history?hours='+h).then(r=>r.json());
if(summary){const set=(id,v)=>{const e=document.getElementById(id);e.textContent=fmt(v);e.className='pval '+cls(v)};set('v-u',summary.totalUnrealised);set('v-r',summary.totalRealised||0);set('v-t',summary.totalPnl);document.getElementById('s-pos').textContent=summary.openPositions;var bEl=document.getElementById('s-booked');if(bEl){bEl.textContent=fmt(summary.totalRealised||0);bEl.className='sval '+cls(summary.totalRealised||0);}var bEl=document.getElementById('s-booked');if(bEl){bEl.textContent=fmt(summary.bookedPnl||0);bEl.className='sval '+cls(summary.bookedPnl||0);}}
const canvas=document.getElementById('pnl-chart'),empty=document.getElementById('chart-empty');
if(!history||history.length<2){canvas.style.display='none';empty.classList.add('show');return;}
canvas.style.display='block';empty.classList.remove('show');
const dk=theme==='dark',gc=dk?'rgba(255,255,255,.05)':'rgba(0,0,0,.05)',tc=dk?'#3D444D':'#C4B8B0';
if(chartInst)chartInst.destroy();
chartInst=new Chart(canvas,{type:'line',data:{labels:history.map(h=>toIST(h.recorded_at)),datasets:[
{label:'Total P&L',data:history.map(h=>h.total_pnl),borderColor:'#FF6B4A',backgroundColor:'rgba(255,107,74,.07)',tension:.4,pointRadius:0,borderWidth:2,fill:true},
{label:'Unrealised',data:history.map(h=>h.total_unrealised),borderColor:'#2ECC71',backgroundColor:'transparent',tension:.4,pointRadius:0,borderWidth:1.5},
{label:'Realised',data:history.map(h=>h.total_realised),borderColor:'#3498DB',backgroundColor:'transparent',tension:.4,pointRadius:0,borderWidth:1.5,borderDash:[4,3]}
]},options:{responsive:true,maintainAspectRatio:false,animation:false,interaction:{intersect:false,mode:'index'},
plugins:{legend:{labels:{color:tc,boxWidth:10,font:{size:10,family:'Geist'}}},tooltip:{backgroundColor:dk?'#1C2330':'#fff',borderColor:dk?'rgba(255,255,255,.1)':'rgba(0,0,0,.1)',borderWidth:1,titleColor:tc,bodyColor:dk?'#E6EDF3':'#1A1410',padding:10,cornerRadius:8,callbacks:{label:c=>' '+c.dataset.label+': '+(c.raw>=0?'+':'')+'\u20b9'+Math.round(c.raw).toLocaleString('en-IN')}}},
scales:{x:{ticks:{color:tc,maxTicksLimit:8,font:{size:9,family:'Geist Mono'}},grid:{color:gc}},y:{ticks:{color:tc,font:{size:9,family:'Geist Mono'},callback:v=>(v>=0?'+':'')+'\u20b9'+Math.round(v).toLocaleString('en-IN')},grid:{color:gc}}}}});
}
async function loadMarket(){
const {ok,data=[]}=await fetch('/api/market').then(r=>r.json()).catch(()=>({ok:false,data:[]}));
const grid=document.getElementById('market-grid');
// Fixed labels for all 6 slots — always render in this order even if data missing
const SLOTS=[
{key:'SENSEX', label:'SENSEX', unit:'pts'},
{key:'NIFTY50', label:'NIFTY 50', unit:'pts'},
{key:'BANKNIFTY',label:'BANK NIFTY', unit:'pts'},
{key:'INDIAVIX', label:'INDIA VIX', unit:'%'},
{key:'CRUDEOIL', label:'Crude Oil', unit:'\u20b9/bbl'},
{key:'USDINR', label:'INR/USD', unit:'\u20b9'},
];
const map=Object.fromEntries(data.map(q=>[q.key,q]));
if(!window._mktCache)window._mktCache={};(data||[]).forEach(function(q){window._mktCache[q.key]=q;});grid.innerHTML=SLOTS.map(slot=>{
const q=map[slot.key];
if(!q){var c=(window._mktCache||{})[slot.key];if(c){q=c;}else{return '<div class="card mcard"><div class="mlbl">'+slot.label+'</div><div class="mprice" style="color:var(--text3)">—</div><div class="mchg" style="color:var(--text3)">—</div></div>';}}
const up=q.changePct>=0,cl=up?'up':'dn',arrow=up?'\u25b2':'\u25bc';
const price=q.price>10000?q.price.toLocaleString('en-IN',{maximumFractionDigits:0}):q.price.toFixed(2);
return '<div class="card mcard"><div class="mlbl">'+slot.label+'</div>'+
'<div class="mprice">'+price+' <span style="font-size:.62rem;color:var(--text2)">'+slot.unit+'</span></div>'+
'<div class="mchg '+cl+'">'+arrow+' '+Math.abs(q.changePct).toFixed(2)+'% <span style="opacity:.6">'+(q.change>=0?'+':'')+q.change.toFixed(0)+'</span></div>'+
(q.stale?'<div class="mstale">prev close'+(q.cachedAt?' \u00b7 '+q.cachedAt.slice(11,16)+' UTC':'')+'</div>':'')+'</div>';
}).join('');
}
// Parse expiry label from symbol name
function expiryLabel(sym){
// Format 1: UNDERLYING + YY + M(1) + DD + ... e.g. SENSEX2660476100CE -> Jun 04
var m1=sym.match(/^[A-Z]+(\d{2})(\d{1})(\d{2})\d+(CE|PE)$/);
if(m1){
var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var mo=parseInt(m1[2])-1;
return (months[mo]||m1[2])+' '+m1[3]+' 20'+m1[1];
}
// Format 2: UNDERLYING + YY + MON + DD + ... e.g. SENSEX26MAY76100CE -> May 28
var m2=sym.match(/^[A-Z]+(\d{2})(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(\d{2})\d+(CE|PE)$/i);
if(m2){return m2[2].charAt(0)+m2[2].slice(1).toLowerCase()+' 20'+m2[1];}
return 'Other';
}
var _activeExpiry='ALL';
async function loadPositions(){
const {data}=await fetch('/api/positions').then(r=>r.json());
const open=data.filter(p=>p.is_closed===0 && +p.netqty!==0);
// Build expiry tabs
const expiries=['ALL',...new Set(open.map(p=>expiryLabel(p.tradingsymbol)))].filter(Boolean);
// Use the row OUTSIDE card-head so clicks don't bubble to toggleCard
var tabsEl=document.getElementById('expiry-tabs-row');
if(tabsEl){
if(expiries.length>2){
tabsEl.style.display='flex';
tabsEl.innerHTML=expiries.map(function(e){
return '<button class="exp-tab'+(e===_activeExpiry?' active':'')+'" onclick="event.stopPropagation();setExpiry(this,\''+e+'\')">'+
(e==='ALL'?'All Expiries':e)+'</button>';
}).join('');
} else {
tabsEl.style.display='none';
tabsEl.innerHTML='';
}
}
// Filter by active expiry
const filtered=_activeExpiry==='ALL'?open:open.filter(p=>expiryLabel(p.tradingsymbol)===_activeExpiry);
const tbody=document.getElementById('pos-body'),tfoot=document.getElementById('pos-foot');
if(!filtered.length){tbody.innerHTML='<tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">No open positions'+(_activeExpiry!=='ALL'?' for '+_activeExpiry:'')+'</td></tr>';tfoot.style.display='none';return;}
const sU=filtered.reduce((s,p)=>s+p.unrealised_pnl,0),sR=filtered.reduce((s,p)=>s+p.realised_pnl,0),sT=filtered.reduce((s,p)=>s+p.total_pnl,0);
// Update collapsed header badge
const phEl=document.getElementById('pos-hpnl');
if(phEl){phEl.textContent=fmt(sT);phEl.className='';phEl.style.cssText='font-family:Geist Mono,monospace;font-size:.75rem;font-weight:600;display:inline;color:'+(sT>50?'var(--green)':sT<-50?'var(--red)':'var(--text2)');}
tbody.innerHTML=filtered.map(p=>{const tte=parseTTE(p.tradingsymbol);return'<tr><td><div class="sym">'+p.tradingsymbol+'</div><div class="sym-meta">'+p.exchange+' \u00b7 '+p.producttype+'</div>'+(tte?'<div class="tte">'+tte+'</div>':'')+'</td><td class="'+(p.netqty<0?'qs':'ql')+'">'+(p.netqty>0?'+':'')+p.netqty+'</td><td class="mono">\u20b9'+(+p.ltp).toFixed(2)+'</td><td class="mono" style="color:var(--text2)">\u20b9'+(+p.avg_price).toFixed(2)+'</td><td class="mono '+cls(p.unrealised_pnl)+'">'+fmt(p.unrealised_pnl)+'</td><td class="mono '+cls(p.realised_pnl)+'">'+fmt(p.realised_pnl)+'</td><td class="mono '+cls(p.total_pnl)+'" style="font-weight:600">'+fmt(p.total_pnl)+'</td></tr>';}).join('');
tfoot.style.display='';
const sf=(id,v)=>{const e=document.getElementById(id);e.textContent=fmt(v);e.className='mono '+cls(v)};sf('f-u',sU);sf('f-r',sR);sf('f-t',sT);
document.getElementById('override-body').innerHTML=filtered.map(p=>'<tr><td><div class="sym" style="font-size:.75rem">'+p.tradingsymbol+'</div></td><td><input type="number" min="1" max="50" step="0.5" placeholder="Global default" id="op-'+p.key+'" style="width:130px;padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.75rem;font-family:\'Geist Mono\',monospace;outline:none"/></td><td><input type="datetime-local" id="om-'+p.key+'" style="padding:4px 8px;border-radius:7px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-size:.72rem;outline:none"/></td><td><button onclick="saveOverride(\''+p.key+'\')" style="padding:3px 10px;border-radius:7px;background:var(--bg3);border:1px solid var(--border2);color:var(--text2);font-size:.72rem;cursor:pointer">Save</button></td></tr>').join('');
}
async function loadAlerts(){
const {data}=await fetch('/api/alerts?limit=20').then(r=>r.json());
const today=new Date().toISOString().slice(0,10);
const todayCnt=data.filter(a=>a.alerted_at.startsWith(today)).length;
document.getElementById('s-alrt').textContent=todayCnt;
// Update collapsed header badge
const ahEl=document.getElementById('alrt-hcount');
if(ahEl){if(todayCnt>0){ahEl.textContent=todayCnt+' today';ahEl.style.display='inline';}else{ahEl.style.display='none';}}
// Check if any position is muted
fetch('/api/config').then(r=>r.json()).then(cfg=>{
const muted=(cfg.overrides||[]).some(o=>o.muted_until&&new Date(o.muted_until)>new Date());
const mEl=document.getElementById('alrt-hmuted');
if(mEl)mEl.style.display=muted?'inline':'none';
}).catch(()=>{});
if(!data.length)return;
document.getElementById('alrt-body').innerHTML=data.map(a=>'<tr class="'+(a.direction==='up'?'alert-up':'alert-dn')+'"><td><div class="sym" style="font-size:.75rem">'+(a.direction==='up'?'\u25b2':'\u25bc')+' '+a.tradingsymbol+'</div></td><td class="mono '+cls(a.current_pnl)+'">'+fmt(a.current_pnl)+'</td><td class="mono" style="color:var(--text2)">'+fmt(a.anchor_pnl)+'</td><td class="mono '+cls(a.delta_abs)+'">'+fmt(a.delta_abs)+' <span style="opacity:.6;font-size:.72em">'+(a.delta_pct>0?'+':'')+parseFloat(a.delta_pct).toFixed(1)+'%</span></td><td class="mono">\u20b9'+parseFloat(a.ltp).toFixed(2)+'</td><td style="color:var(--text3);font-family:\'Geist Mono\',monospace;font-size:.72rem">'+toIST(a.alerted_at)+'</td></tr>').join('');
}
async function loadHealth(){
const d=await fetch('/api/health').then(r=>r.json());
const pill=document.getElementById('mkt-pill');
document.getElementById('mkt-lbl').textContent=d.marketOpen?'Market Open':'Market Closed';
pill.className='pill'+(d.marketOpen?' open':'');
const e=document.getElementById('s-err');
if(d.lastError){e.textContent=d.lastError.error.slice(0,30)+'\u2026';e.style.color='var(--red)';}
else{e.textContent='Healthy';e.style.color='var(--green)';}
document.getElementById('ts').textContent=new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false})+' IST';
}
async function loadConfig(){
const {global:g}=await fetch('/api/config').then(r=>r.json()).catch(()=>({}));
if(!g)return;
document.getElementById('cfg-pct').value=g.alertThresholdPct??5;
document.getElementById('cfg-abs').value=g.alertMinAbsInr??100;
document.getElementById('cfg-poll').value=g.pollIntervalSeconds??60;
}
async function saveConfig(){
const pct=parseFloat(document.getElementById('cfg-pct').value);
const abs=parseFloat(document.getElementById('cfg-abs').value);
await fetch('/api/config/global',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alertThresholdPct:pct,alertMinAbsInr:abs})});
const msg=document.getElementById('save-msg');msg.style.display='inline';setTimeout(()=>msg.style.display='none',2500);
}
async function saveOverride(key){
const pct=document.getElementById('op-'+key)?.value;
const mute=document.getElementById('om-'+key)?.value;
await fetch('/api/config/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alert_threshold_pct:pct?parseFloat(pct):null,muted_until:mute?new Date(mute).toISOString():null})});
}
async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket(),loadPayoff(),loadClosedPositions()]);}
function toggleCard(id){
const el=document.getElementById(id);
if(!el)return;
el.classList.toggle('collapsed');
const s=JSON.parse(localStorage.getItem('collapse')||'{}');
s[id]=el.classList.contains('collapsed');
localStorage.setItem('collapse',JSON.stringify(s));
}
(function restoreCollapse(){
try{
const s=JSON.parse(localStorage.getItem('collapse')||'{}');
Object.entries(s).forEach(([id,c])=>{if(c){const el=document.getElementById(id);if(el)el.classList.add('collapsed');}});
}catch(e){}
})();
/* ── Payoff Chart ─────────────────────────────────────────────────────── */
var payoffChartInst = null;
// Register afterDraw plugin for vertical annotation lines
Chart.register({
id: 'payoffLines',
afterDraw: function(chart) {
if(!chart.canvas._beLines)return;
var ctx=chart.ctx,xs=chart.canvas._chartXs||[];
if(!xs.length)return;
var xS=chart.scales.x,yS=chart.scales.y,lo=xs[0],hi=xs[xs.length-1],rng=hi-lo;
if(!rng)return;
function xP(v){return xS.left+(v-lo)/rng*(xS.right-xS.left);}
var sp=chart.canvas._spotLine||0;
ctx.save();
// 1SD and 2SD dotted lines
var sd=chart.canvas._sdInfo;
if(sd&&sp){
[{dist:sd.sd1,lbl:'1SD',col:'rgba(155,89,182,0.7)',dash:[3,4]},
{dist:sd.sd2,lbl:'2SD',col:'rgba(52,152,219,0.6)',dash:[2,5]}
].forEach(function(s){
[sp-s.dist,sp+s.dist].forEach(function(v){
var x=xP(v);if(x<xS.left||x>xS.right)return;
ctx.beginPath();ctx.setLineDash(s.dash);ctx.strokeStyle=s.col;ctx.lineWidth=1.2;
ctx.moveTo(x,yS.top);ctx.lineTo(x,yS.bottom);ctx.stroke();
ctx.setLineDash([]);ctx.fillStyle=s.col;ctx.font='8px monospace';ctx.textAlign='center';
ctx.fillText(s.lbl,x,yS.top+8);
});
});
}
// Breakeven lines + % from spot
(chart.canvas._beLines||[]).forEach(function(be){
var x=xP(be);if(x<xS.left||x>xS.right)return;
ctx.beginPath();ctx.setLineDash([5,4]);ctx.strokeStyle='rgba(245,166,35,0.9)';ctx.lineWidth=1.5;
ctx.moveTo(x,yS.top);ctx.lineTo(x,yS.bottom);ctx.stroke();
ctx.setLineDash([]);ctx.fillStyle='#F5A623';ctx.font='bold 9px monospace';ctx.textAlign='center';
var pct=sp>0?((be-sp)/sp*100).toFixed(1):'';
ctx.fillText(be.toLocaleString('en-IN')+(pct?' ('+(pct>0?'+':'')+pct+'%)':''),x,yS.top+10);
});
// Spot line
if(sp){var x=xP(sp);if(x>=xS.left&&x<=xS.right){
ctx.beginPath();ctx.setLineDash([6,3]);ctx.strokeStyle='rgba(255,107,74,0.95)';ctx.lineWidth=2.5;
ctx.moveTo(x,yS.top);ctx.lineTo(x,yS.bottom);ctx.stroke();
ctx.setLineDash([]);ctx.fillStyle='#FF6B4A';ctx.font='bold 10px monospace';ctx.textAlign='center';
ctx.fillText('Spot '+Number(sp).toLocaleString('en-IN'),x,yS.bottom-4);
}}
ctx.restore();
}
});
function parseOptionSym(sym) {
// Angel format: UNDERLYING + YY(2) + M(1) + DD(2) + STRIKE(N) + CE/PE
// e.g. SENSEX2651479000CE -> SENSEX, 26, 5, 14, 79000, CE
var RE1 = new RegExp('^([A-Z]+)(\\d{2})(\\d{1})(\\d{2})(\\d+)(CE|PE)$');
var m = sym.match(RE1);
if (m) {
var exp = new Date('20'+m[2]+'-'+m[3].padStart(2,'0')+'-'+m[4]);
return { underlying:m[1], strike:parseInt(m[5]), type:m[6], expiry:isNaN(exp)?null:exp };
}
// Fallback 2-digit month (Oct+)
var RE2 = new RegExp('^([A-Z]+)(\\d{2})(\\d{2})(\\d{2})(\\d+)(CE|PE)$');
m = sym.match(RE2);
if (m) {
var exp2 = new Date('20'+m[2]+'-'+m[3]+'-'+m[4]);
return { underlying:m[1], strike:parseInt(m[5]), type:m[6], expiry:isNaN(exp2)?null:exp2 };
}
return null;
}
function calcPayoff(opts, spot) {
var total = 0;
opts.forEach(function(p) {
var info = p._parsed;
if (!info) return;
var intrinsic = info.type === 'CE' ? Math.max(spot - info.strike, 0) : Math.max(info.strike - spot, 0);
total += (intrinsic - (+p.avg_price)) * (+p.netqty);
});
return total;
}
function detectStrategy(opts) {
var calls = opts.filter(function(p){return p._parsed.type==='CE';});
var puts = opts.filter(function(p){return p._parsed.type==='PE';});
var longs = opts.filter(function(p){return (+p.netqty)>0;});
var shorts= opts.filter(function(p){return (+p.netqty)<0;});
if (opts.length===1) return (longs.length?'Long ':'Short ')+(calls.length?'Call':'Put');
if (calls.length===2&&puts.length===0) return longs.length===1&&shorts.length===1?'Bull Call Spread':'Call Spread';
if (puts.length===2&&calls.length===0) return longs.length===1&&shorts.length===1?'Bear Put Spread':'Put Spread';
if (calls.length>=1&&puts.length>=1&&shorts.length===opts.length) return 'Short Strangle/Straddle';
if (calls.length>=1&&puts.length>=1&&longs.length===opts.length) return 'Long Strangle/Straddle';
if (opts.length===4) return 'Iron Condor/Butterfly';
return opts.length+'-Leg Strategy';
}
function calcSD(spot,dte,vix){var v=vix>1?vix/100:0.18;var dv=v/Math.sqrt(252);var move=spot*dv*Math.sqrt(Math.max(dte,1));return{sd1:move,sd2:move*2};}
async function loadPayoff() {
var canvas = document.getElementById('payoff-chart');
var emptyEl = document.getElementById('pf-empty');
if (!canvas || !emptyEl) return;
try {
var posRes = await fetch('/api/positions').then(function(r){return r.json();}).catch(function(){return {data:[]};});
var mktRes = await fetch('/api/market').then(function(r){return r.json();}).catch(function(){return {data:[]};});
var open = (posRes.data || []).filter(function(p){return p.is_closed===0;});
var expFiltered=(_activeExpiry&&_activeExpiry!=='ALL')?open.filter(function(p){return expiryLabel(p.tradingsymbol)===_activeExpiry;}):open;
var opts = expFiltered.filter(function(p){return /CE|PE/.test(p.tradingsymbol);}).map(function(p){
p._parsed = parseOptionSym(p.tradingsymbol);
return p;
}).filter(function(p){return p._parsed !== null;});
if (!opts.length) {
canvas.style.display = 'none'; emptyEl.style.display = 'flex'; return;
}
canvas.style.display = 'block'; emptyEl.style.display = 'none';
// Spot price from market data
var mktMap = {};
(mktRes.data || []).forEach(function(q){ mktMap[q.key] = q.price; });
var ul = opts[0]._parsed.underlying;
var mktKey = ul==='SENSEX'?'SENSEX': ul==='NIFTY'?'NIFTY50': ul==='BANKNIFTY'?'BANKNIFTY': null;
var spot = mktKey ? (mktMap[mktKey]||0) : 0;
// Price range: ±1.5 SD from spot (uses VIX if available, else 18% ann vol)
// This keeps the chart tight and readable — widens only as much as vol suggests
var vixForRange=(window._mktCache&&window._mktCache['INDIAVIX'])?window._mktCache['INDIAVIX'].price:0;
var sdRange = calcSD(spot||75000, typeof dte==='number'?dte:7, vixForRange);
var rangePad = sdRange.sd1 * 1.5;
var strikes = opts.map(function(p){return p._parsed.strike;});
var minS = Math.min.apply(null,strikes), maxS = Math.max.apply(null,strikes);
// Ensure all strikes are visible even if outside 1.5SD
var lo = Math.floor((Math.min(minS, spot-rangePad))/100)*100;
var hi = Math.ceil((Math.max(maxS, spot+rangePad))/100)*100;
var step = Math.max(Math.round((hi-lo)/100/100)*100, 50);
var xs=[], ys=[];
for (var s=lo; s<=hi; s+=step){ xs.push(s); ys.push(calcPayoff(opts,s)); }
var maxY = Math.max.apply(null,ys), minY = Math.min.apply(null,ys);
// Breakevens
var bes = [];
for (var i=1; i<ys.length; i++) {
if ((ys[i-1]<0&&ys[i]>=0)||(ys[i-1]>=0&&ys[i]<0)) {
bes.push(Math.round(xs[i-1]+(xs[i]-xs[i-1])*(-ys[i-1]/(ys[i]-ys[i-1]))));
}
}
var netPrem = opts.reduce(function(s,p){return s+(+p.avg_price)*(+p.netqty);},0);
var dte = opts[0]._parsed.expiry ? Math.max(0,Math.ceil((opts[0]._parsed.expiry-new Date())/86400000)) : '?';
var rr = minY!==0 ? (maxY/Math.abs(minY)).toFixed(2) : '∞';
function sv(id, txt, cls) {
var e = document.getElementById(id);
if (!e) return;
e.textContent = txt;
if (cls) e.className = 'pfs-v '+cls;
}
var pr = Math.round(netPrem).toLocaleString('en-IN');
sv('pf-prem', (netPrem>=0?'+':'')+'\u20b9'+pr, netPrem>=0?'up':'dn');
sv('pf-maxp', maxY>1e7?'\u221e':'+\u20b9'+Math.round(maxY).toLocaleString('en-IN'), 'up');
sv('pf-maxl', minY<-1e7?'\u2212\u221e':'\u20b9'+Math.round(minY).toLocaleString('en-IN'), 'dn');
sv('pf-rr', rr+'x');
sv('pf-be', bes.length ? bes.map(function(b){return b.toLocaleString('en-IN');}).join(', ') : 'None');
sv('pf-dte', dte+' days');
var ulEl = document.getElementById('pf-ul');
var stEl = document.getElementById('pf-strat');
if (ulEl) ulEl.textContent = ul;
if (stEl) stEl.textContent = detectStrategy(opts);
var dk = theme==='dark';
var gc = dk?'rgba(255,255,255,.05)':'rgba(0,0,0,.05)';
var tc = dk?'#3D444D':'#C4B8B0';
if (payoffChartInst) payoffChartInst.destroy();
var vixV=(window._mktCache&&window._mktCache['INDIAVIX'])?window._mktCache['INDIAVIX'].price:0;canvas._sdInfo=calcSD(spot,typeof dte==='number'?dte:7,vixV);canvas._beLines=bes;canvas._spotLine=spot;canvas._chartXs=xs;
payoffChartInst = new Chart(canvas, {
type: 'line',
data: {
labels: xs,
datasets: [
{label:'Profit Zone',data:ys.map(function(y){return y>=0?y:0;}),borderColor:'rgba(46,204,113,0.85)',backgroundColor:'rgba(46,204,113,0.12)',tension:0,pointRadius:0,borderWidth:2,fill:true},
{label:'Loss Zone',data:ys.map(function(y){return y<=0?y:0;}),borderColor:'rgba(231,76,60,0.85)',backgroundColor:'rgba(231,76,60,0.12)',tension:0,pointRadius:0,borderWidth:2,fill:true}]
},
options: {
responsive:true, maintainAspectRatio:false, animation:false,
interaction:{intersect:false, mode:'index'},
plugins: {
legend:{display:false},
tooltip:{
backgroundColor:dk?'#1C2330':'#fff',
borderColor:dk?'rgba(255,255,255,.1)':'rgba(0,0,0,.1)',
borderWidth:1, titleColor:tc, bodyColor:dk?'#E6EDF3':'#1A1410',
padding:10, cornerRadius:8,
callbacks:{
title:function(items){return 'Spot: \u20b9'+parseInt(items[0].label).toLocaleString('en-IN');},
label:function(item){
if (item.datasetIndex!==2) return null;
var v=item.raw;
return ' P&L: '+(v>=0?'+':'')+'\u20b9'+Math.round(v).toLocaleString('en-IN');
}
}
}
},
scales: {
x:{type:'linear', ticks:{color:tc,maxTicksLimit:10,font:{size:9,family:'monospace'},callback:function(v){return v.toLocaleString('en-IN');}}, grid:{color:gc}},
y:{ticks:{color:tc,font:{size:9,family:'monospace'},callback:function(v){return (v>=0?'+':'')+'\u20b9'+Math.round(v).toLocaleString('en-IN');}}, grid:{color:gc}}
}
}
});
} catch(err) {
console.error('loadPayoff error:', err);
}
}
/* ── End Payoff ───────────────────────────────────────────────────────── */
async function loadClosedPositions() {
var res = await fetch('/api/closed-positions').then(function(r){return r.json();}).catch(function(){return {ok:false,data:[]};});
var tbody = document.getElementById('closed-tbody');
var tfoot = document.getElementById('closed-tfoot');
var totalEl = document.getElementById('closed-total');
var staleEl = document.getElementById('closed-stale');
if (!tbody) return;
var tIST=new Date().toLocaleDateString('en-CA',{timeZone:'Asia/Kolkata'});
var data=(res.data||[]).filter(function(p){return p.updated_at&&p.updated_at.slice(0,10)>=tIST;});
var totalBooked = res.totalBooked || 0;
// Update collapsed header badge
if (totalEl) {
if (data.length > 0) {
totalEl.textContent = fmt(totalBooked);
totalEl.style.display = 'inline';
totalEl.style.color = totalBooked >= 0 ? 'var(--green)' : 'var(--red)';
} else {
totalEl.style.display = 'none';
}
}
// Update stat card
var bEl = document.getElementById('s-booked');
if (bEl && data.length > 0) {
bEl.textContent = fmt(totalBooked);
bEl.className = 'sval ' + cls(totalBooked);
}
if (!data.length) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:28px;color:var(--text3)">No closed positions today</td></tr>';
if (tfoot) tfoot.style.display = 'none';
return;
}
// Parse option symbol helper
function parseForDisplay(sym) {
var m = sym.match(/^([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/);
if (m) return { ul: m[1], expiry: m[3].padStart(2,'0')+'/'+m[4]+'/20'+m[2], strike: m[5], type: m[6] };
return { ul: sym, expiry: '—', strike: '—', type: '—' };
}
tbody.innerHTML = data.map(function(p) {
var info = parseForDisplay(p.tradingsymbol);
var typeBadge = info.type === 'CE'
? '<span style="background:rgba(52,152,219,.15);color:#3498DB;padding:1px 7px;border-radius:9999px;font-size:.68rem;font-weight:600">CE</span>'
: '<span style="background:rgba(155,89,182,.15);color:#9B59B6;padding:1px 7px;border-radius:9999px;font-size:.68rem;font-weight:600">PE</span>';
return '<tr>' +
'<td><div class="sym">' + p.tradingsymbol + '</div>' +
'<div class="sym-meta">' + p.exchange + ' \u00b7 ' + p.producttype + '</div></td>' +
'<td style="text-align:center">' + typeBadge + '</td>' +
'<td class="mono" style="text-align:center">&#8377;' + parseInt(info.strike).toLocaleString('en-IN') + '</td>' +
'<td style="text-align:center;color:var(--text2);font-size:.75rem">' + info.expiry + '</td>' +
'<td class="mono">&#8377;' + parseFloat(p.ltp).toFixed(2) + '</td>' +
'<td class="mono ' + cls(p.realised_pnl) + '" style="font-weight:600">' + fmt(p.realised_pnl) + '</td>' +
'<td><span style="background:rgba(46,204,113,.12);color:var(--green);padding:2px 8px;border-radius:9999px;font-size:.65rem;font-weight:600">&#10003; Booked</span></td>' +
'</tr>';
}).join('');
if (tfoot) {
tfoot.style.display = '';
var footEl = document.getElementById('closed-foot');
if (footEl) { footEl.textContent = fmt(totalBooked); footEl.className = 'mono ' + cls(totalBooked); }
}
// Show stale positions warning if any
var stale = res.stalePositions || [];
if (stale.length > 0 && staleEl) {
staleEl.style.display = 'block';
staleEl.textContent = '\u26a0\ufe0f ' + stale.length + ' position(s) temporarily disappeared from Angel API but still have open qty — they will reappear on next successful poll.';
} else if (staleEl) {
staleEl.style.display = 'none';
}
}
function setExpiry(btn,exp){
_activeExpiry=exp;
// Update active tab styling
var row=document.getElementById('expiry-tabs-row');
if(row) row.querySelectorAll('.exp-tab').forEach(function(b){b.classList.remove('active');});
if(btn) btn.classList.add('active');
loadPositions();
loadPayoff();
}
/* ── Analysis Tab ─────────────────────────────────────────────────────── */
var _CAT_COLOR={DANGER_SHORT:'#E74C3C',DEAD_WEIGHT:'#F5A623',UNDER_PRESSURE:'#E67E22',HEDGE:'#3498DB',WORKING:'#2ECC71'};
var _CAT_EMOJI={DANGER_SHORT:'🔴',DEAD_WEIGHT:'🟡',UNDER_PRESSURE:'🟠',HEDGE:'🔵',WORKING:'🟢'};
var _RISK_COL={LOW:'var(--green)',MODERATE:'var(--amber)',HIGH:'#E67E22',CRITICAL:'var(--red)'};
function showView(name){
document.querySelectorAll('[data-view]').forEach(function(el){el.style.display='none';});
var el=document.getElementById('view-'+name);if(el)el.style.display='block';
document.querySelectorAll('.nav-tab').forEach(function(t){t.classList.remove('active');});
var tab=document.querySelector('.nav-tab[data-target="'+name+'"]');if(tab)tab.classList.add('active');
if(name==='analysis')loadAnalysis();
}
async function loadAnalysis(){
try{
var res=await fetch('/api/analysis').then(function(r){return r.json();});
if(!res.ok||!res.data)return;
renderAnalysis(res.data,res.asOf);
}catch(e){console.error('loadAnalysis:',e);}
}
function renderAnalysis(d,asOf){
var cl=d.classified||[],gk=d.greeks||{},rk=d.risk||{},sc=d.scenarios||[];
var se=function(id,v,c){var e=document.getElementById(id);if(!e)return;e.textContent=v;if(c)e.className='pval '+c;};
se('a-delta',(gk.totalDelta||0).toFixed(1),gk.totalDelta>50?'up':gk.totalDelta<-50?'dn':'fl');
se('a-theta','₹'+(gk.totalTheta||0).toFixed(0),gk.totalTheta>0?'up':'dn');
se('a-vega',(gk.totalVega||0).toFixed(0),'fl');
se('a-risk-lvl',rk.riskLevel||'—',rk.riskLevel==='LOW'?'up':rk.riskLevel==='CRITICAL'?'dn':'fl');
var sub=document.getElementById('a-risk-sub');
if(sub)sub.textContent=((rk.breachedRules||[]).length)+' rule'+(((rk.breachedRules||[]).length)!==1?'s':'')+' breached';
var badges=document.getElementById('a-class-badges');
if(badges){
var cts={};cl.forEach(function(p){cts[p.category]=(cts[p.category]||0)+1;});
badges.innerHTML=Object.entries(cts).map(function(kv){
var col=_CAT_COLOR[kv[0]]||'#8B949E';
return '<span style="background:'+col+'22;color:'+col+';padding:2px 9px;border-radius:9999px;font-size:.65rem;font-weight:600">'+
(_CAT_EMOJI[kv[0]]||'')+' '+kv[0].replace(/_/g,' ')+' \xd7'+kv[1]+'</span>';}).join('');}
var urgent=cl.filter(function(p){return p.urgencyScore>0;}).sort(function(a,b){return b.urgencyScore-a.urgencyScore;});
var aList=document.getElementById('a-action-list');
if(aList){
if(urgent.length>0){
aList.style.display='block';
aList.innerHTML='<div style="font-size:.67rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:8px">Action Required</div>'+
urgent.map(function(p){
var col=_CAT_COLOR[p.category]||'#8B949E';var em=_CAT_EMOJI[p.category]||'';
var msg=p.category==='DANGER_SHORT'?'ITM short with thin time value — closing avoids assignment risk':
p.category==='DEAD_WEIGHT'?'Far OTM long bleeding theta with near-zero value — roll or close':
p.category==='UNDER_PRESSURE'?'Down >50% of premium — monitor and consider stop':'';
return '<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;padding:8px 12px;background:'+col+'11;border:1px solid '+col+'33;border-radius:10px">'+
'<span style="font-size:.85rem">'+em+'</span>'+
'<span style="font-size:.78rem;font-weight:600;color:var(--text);min-width:180px">'+p.symbol+'</span>'+
'<span style="font-size:.68rem;color:var(--text2);flex:1">'+msg+'</span>'+
'<span style="font-size:.65rem;font-family:\'Geist Mono\',monospace;color:var(--text2)">'+p.daysToExpiry.toFixed(0)+'d \xb7 IV '+p.ivPct.toFixed(0)+'%</span>'+
'<span style="background:'+col+';color:#fff;padding:1px 8px;border-radius:9999px;font-size:.65rem;font-weight:700">'+p.urgencyScore+'</span></div>';
}).join('');
}else{aList.style.display='none';}
}
var tbody=document.getElementById('a-class-body');
if(tbody){
if(!cl.length){tbody.innerHTML='<tr><td colspan="8" style="text-align:center;padding:36px;color:var(--text3)">No option positions to analyse</td></tr>';}
else{
tbody.innerHTML=cl.map(function(p){
var col=_CAT_COLOR[p.category]||'#8B949E';
var urg=p.urgencyScore>0
?'<div style="width:'+Math.min(p.urgencyScore,100)+'%;background:'+col+';height:4px;border-radius:2px;margin-bottom:2px"></div><span style="font-size:.6rem;color:var(--text3)">'+p.urgencyScore+'</span>'
:'<span style="font-size:.6rem;color:var(--text3)">—</span>';
return '<tr>'+
'<td><div class="sym">'+p.symbol+'</div><div class="sym-meta">'+p.underlying+' \xb7 '+p.optionType+' '+p.strikePrice.toLocaleString('en-IN')+'</div></td>'+
'<td style="text-align:center"><span style="background:'+col+'22;color:'+col+';padding:2px 8px;border-radius:9999px;font-size:.62rem;font-weight:600">'+(_CAT_EMOJI[p.category]||'')+' '+p.category.replace(/_/g,' ')+'</span></td>'+
'<td class="mono">'+(p.daysToExpiry<1?'<span style="color:var(--red)">'+p.daysToExpiry.toFixed(1)+'</span>':p.daysToExpiry.toFixed(1))+'d</td>'+
'<td class="mono">'+p.ivPct.toFixed(1)+'%</td>'+
'<td class="mono '+(p.positionDelta>0.1?'up':p.positionDelta<-0.1?'dn':'fl')+'">'+p.positionDelta.toFixed(2)+'</td>'+
'<td class="mono '+(p.positionTheta>0?'up':p.positionTheta<0?'dn':'fl')+'">'+p.positionTheta.toFixed(0)+'</td>'+
'<td class="mono '+(p.unrealizedPnl>=0?'up':'dn')+'">'+fmt(p.unrealizedPnl)+'</td>'+
'<td>'+urg+'</td></tr>';}).join('');}}
var rBody=document.getElementById('a-risk-body');
if(rBody){
var rl=rk.riskLevel||'LOW';var rlc=_RISK_COL[rl]||'var(--green)';
var html='<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">'+
'<span style="font-size:1.3rem;font-weight:700;font-family:\'DM Serif Display\',serif;color:'+rlc+'">'+rl+'</span>'+
'<span style="font-size:.72rem;color:var(--text2)">'+(rk.breachedRules||[]).length+' rule(s) breached</span></div>';
if((rk.breachedRules||[]).length){
html+='<div style="margin-bottom:10px">';
(rk.breachedRules||[]).forEach(function(r){html+='<div style="font-size:.75rem;color:var(--text);margin-bottom:4px">⚠️ '+r+'</div>';});
html+='</div>';}
if(rk.hasNakedShorts){
html+='<div style="margin-bottom:8px;padding:8px 12px;background:rgba(231,76,60,.1);border:1px solid rgba(231,76,60,.3);border-radius:10px;font-size:.75rem">'+
'<span style="color:var(--red);font-weight:600">⚠ Naked Shorts</span>: '+
(rk.nakedShortLegs||[]).map(function(l){return l.symbol+' (qty '+l.quantity+')';}).join(', ')+'</div>';}
if(rk.concentrationWarning){html+='<div style="margin-bottom:6px;font-size:.75rem;color:var(--amber)">⚠ '+rk.concentrationWarning+'</div>';}
html+='<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:10px">'+
'<div style="font-size:.72rem;color:var(--text2)">Short CE: <span style="color:var(--text)">₹'+Math.round(rk.netShortCEExposure||0).toLocaleString('en-IN')+'</span></div>'+
'<div style="font-size:.72rem;color:var(--text2)">Short PE: <span style="color:var(--text)">₹'+Math.round(rk.netShortPEExposure||0).toLocaleString('en-IN')+'</span></div>'+
'<div style="font-size:.72rem;color:var(--text2)">Max Theo Loss: <span style="color:var(--red)">₹'+Math.round(rk.maxTheoreticalLoss||0).toLocaleString('en-IN')+'</span></div></div>';
rBody.innerHTML=html;}
var sBody=document.getElementById('a-scen-body');
if(sBody){
if(!sc.length){sBody.innerHTML='<div style="color:var(--text3);font-size:.8rem">No option positions yet</div>';}
else{
sBody.innerHTML=sc.map(function(res){
return '<div style="margin-bottom:14px">'+
'<div style="font-size:.68rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:8px">'+
res.underlying+' &mdash; spot ₹'+Math.round(res.currentSpot).toLocaleString('en-IN')+'</div>'+
'<div style="overflow-x:auto"><table style="min-width:500px"><thead><tr>'+
res.scenarios.map(function(pt){
var c=pt.spotMove===0?'var(--text2)':pt.spotMove>0?'var(--green)':'var(--red)';
return '<th style="color:'+c+'">'+(pt.spotMove>0?'+':'')+pt.spotMove+'</th>';}).join('')+
'</tr></thead><tbody><tr>'+
res.scenarios.map(function(pt){
var c=pt.totalPnl>0?'var(--green)':pt.totalPnl<0?'var(--red)':'var(--text2)';
return '<td class="mono" style="color:'+c+';font-weight:600">'+(pt.totalPnl>=0?'+':'')+'₹'+Math.round(pt.totalPnl).toLocaleString('en-IN')+'</td>';}).join('')+
'</tr></tbody></table></div></div>';}).join('');}}
var asofEl=document.getElementById('a-asof');
if(asofEl&&asOf)asofEl.textContent='Updated: '+toIST(asOf);
}
/* ── End Analysis Tab ─────────────────────────────────────────────────── */
loadConfig();refresh();
setInterval(refresh,60000);
setInterval(loadMarket,15000);
</script>
</body>
</html>