design: favicon, market indices, alert settings, TTE, dark/light toggle
This commit is contained in:
parent
496d192435
commit
1f00d2da41
1 changed files with 233 additions and 600 deletions
|
|
@ -1,628 +1,261 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="dark">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Angel · Position Tracker</title>
|
<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 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">
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
/* ── Design tokens ─────────────────────────────────────────────────── */
|
:root{--coral:#FF6B4A;--amber:#F5A623;--green:#2ECC71;--red:#E74C3C;--tr:.22s cubic-bezier(.4,0,.2,1)}
|
||||||
:root {
|
[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)}
|
||||||
--coral: #FF6B4A;
|
[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)}
|
||||||
--coral-dim: rgba(255,107,74,0.12);
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
--amber: #F5A623;
|
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}
|
||||||
--amber-dim: rgba(245,166,35,0.12);
|
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%)}
|
||||||
--green: #2ECC71;
|
.wrap{max-width:1300px;margin:0 auto;padding:20px;position:relative;z-index:1}
|
||||||
--green-dim: rgba(46,204,113,0.12);
|
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)}
|
||||||
--red: #E74C3C;
|
.nav-l{display:flex;align-items:center;gap:10px}
|
||||||
--red-dim: rgba(231,76,60,0.12);
|
.logo{font-family:'DM Serif Display',serif;font-size:1.15rem;color:var(--coral);letter-spacing:-.02em}
|
||||||
--purple: #9B59B6;
|
.nav-sep{width:1px;height:18px;background:var(--border2)}
|
||||||
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
|
.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}
|
||||||
[data-theme="dark"] {
|
.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)}
|
||||||
--bg: #0D1117;
|
.pill.open{color:var(--green);border-color:rgba(46,204,113,.3);background:rgba(46,204,113,.12)}
|
||||||
--bg2: #161B22;
|
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||||
--bg3: #1C2330;
|
.pill.open .dot,.ldot{animation:blink 2s infinite}
|
||||||
--border: rgba(255,255,255,0.07);
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}
|
||||||
--border2: rgba(255,255,255,0.12);
|
.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}
|
||||||
--text: #E6EDF3;
|
.ibtn:hover{background:var(--bg2);color:var(--text);border-color:var(--coral);transform:translateY(-1px)}
|
||||||
--text2: #8B949E;
|
.g3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:16px}
|
||||||
--text3: #484F58;
|
.g6{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:16px}
|
||||||
--card-bg: rgba(22,27,34,0.95);
|
.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-glow: rgba(255,107,74,0.04);
|
.card:hover{transform:translateY(-1px);box-shadow:var(--shadow-lg)}
|
||||||
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
.card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border)}
|
||||||
--shadow-lg: 0 8px 48px rgba(0,0,0,0.6);
|
.card-title{font-size:.78rem;font-weight:600;letter-spacing:.03em}
|
||||||
--glass: rgba(22,27,34,0.8);
|
.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}
|
||||||
[data-theme="light"] {
|
.pcard.pu{--ca:#2ECC71;--cb:#1abc9c}.pcard.pr{--ca:#3498DB;--cb:#2980b9}.pcard.pt{--ca:#FF6B4A;--cb:#F5A623}
|
||||||
--bg: #FAF7F4;
|
.plbl{font-size:.67rem;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--text2);margin-bottom:8px}
|
||||||
--bg2: #FFFFFF;
|
.pval{font-family:'DM Serif Display',serif;font-size:2.2rem;line-height:1;letter-spacing:-.02em;margin-bottom:6px}
|
||||||
--bg3: #F4EFE9;
|
.pcard.pt .pval{font-size:2.7rem}
|
||||||
--border: rgba(0,0,0,0.06);
|
.psub{font-size:.68rem;color:var(--text3)}
|
||||||
--border2: rgba(0,0,0,0.12);
|
.up{color:var(--green)}.dn{color:var(--red)}.fl{color:var(--text2)}
|
||||||
--text: #1A1410;
|
.mcard{padding:14px 16px}
|
||||||
--text2: #6B6056;
|
.mlbl{font-size:.63rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:6px}
|
||||||
--text3: #B8AFA8;
|
.mprice{font-family:'DM Serif Display',serif;font-size:1.35rem;letter-spacing:-.01em;color:var(--text);line-height:1.1}
|
||||||
--card-bg: rgba(255,255,255,0.95);
|
.mchg{font-size:.68rem;font-family:'Geist Mono',monospace;margin-top:3px}
|
||||||
--card-glow: rgba(255,107,74,0.03);
|
.mstale{font-size:.6rem;color:var(--text3);margin-top:2px}
|
||||||
--shadow: 0 4px 24px rgba(0,0,0,0.08);
|
.scard{padding:14px 16px}
|
||||||
--shadow-lg: 0 8px 48px rgba(0,0,0,0.12);
|
.slbl{font-size:.67rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text2);margin-bottom:5px}
|
||||||
--glass: rgba(255,255,255,0.85);
|
.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}
|
||||||
/* ── Reset & base ──────────────────────────────────────────────────── */
|
.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}
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
.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}
|
||||||
body {
|
.chart-empty{display:none;position:absolute;inset:0;align-items:center;justify-content:center;color:var(--text3);font-size:.78rem}
|
||||||
font-family: 'Geist', sans-serif;
|
.chart-empty.show{display:flex}
|
||||||
background: var(--bg);
|
.live-badge{display:flex;align-items:center;gap:5px;font-size:.67rem;font-weight:600;letter-spacing:.06em;color:var(--green);text-transform:uppercase}
|
||||||
color: var(--text);
|
.ldot{width:5px;height:5px;border-radius:50%;background:var(--green)}
|
||||||
min-height: 100vh;
|
table{width:100%;border-collapse:collapse}
|
||||||
transition: background var(--transition), color var(--transition);
|
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)}
|
||||||
overflow-x: hidden;
|
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)}
|
||||||
/* Background texture */
|
td{padding:11px 18px;font-size:.8rem;text-align:right;color:var(--text)}td:first-child{text-align:left}
|
||||||
body::before {
|
.sym{font-weight:600;font-size:.78rem}.sym-meta{font-size:.65rem;color:var(--text3);margin-top:1px;font-family:'Geist Mono',monospace}
|
||||||
content: '';
|
.tte{font-size:.62rem;color:var(--amber);margin-top:2px;font-family:'Geist Mono',monospace}
|
||||||
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
.mono{font-family:'Geist Mono',monospace;font-size:.78rem}
|
||||||
background-image:
|
.ql{color:var(--green);font-weight:600;font-family:'Geist Mono',monospace}.qs{color:var(--red);font-weight:600;font-family:'Geist Mono',monospace}
|
||||||
radial-gradient(ellipse 80vw 60vh at 10% 0%, rgba(255,107,74,0.06) 0%, transparent 60%),
|
tfoot tr{border-top:2px solid var(--border2);background:var(--bg3)}
|
||||||
radial-gradient(ellipse 60vw 50vh at 90% 100%, rgba(245,166,35,0.04) 0%, transparent 60%);
|
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)}
|
||||||
.wrap { max-width: 1280px; margin: 0 auto; padding: 24px 20px; position: relative; z-index: 1; }
|
.sg{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px}
|
||||||
|
.field{display:flex;flex-direction:column;gap:5px}
|
||||||
/* ── Typography ────────────────────────────────────────────────────── */
|
.field label{font-size:.67rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2)}
|
||||||
.serif { font-family: 'DM Serif Display', serif; }
|
.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}
|
||||||
.mono { font-family: 'Geist Mono', monospace; }
|
.field input:focus{border-color:var(--coral)}
|
||||||
|
.hint{font-size:.62rem;color:var(--text3)}
|
||||||
/* ── Nav ───────────────────────────────────────────────────────────── */
|
.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)}
|
||||||
nav {
|
.btn-save:hover{background:#e55a3a;transform:translateY(-1px)}
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
.save-msg{font-size:.72rem;color:var(--green);display:none;margin-left:10px}
|
||||||
padding: 16px 24px; margin-bottom: 32px;
|
|
||||||
background: var(--glass);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-left { display: flex; align-items: center; gap: 12px; }
|
|
||||||
|
|
||||||
.nav-logo {
|
|
||||||
font-family: 'DM Serif Display', serif;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--coral);
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-divider { width: 1px; height: 20px; background: var(--border2); }
|
|
||||||
|
|
||||||
.nav-subtitle { font-size: 0.75rem; font-weight: 500; color: var(--text2); letter-spacing: 0.06em; text-transform: uppercase; }
|
|
||||||
|
|
||||||
.nav-right { display: flex; align-items: center; gap: 10px; }
|
|
||||||
|
|
||||||
.status-pill {
|
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
padding: 5px 12px; border-radius: 9999px;
|
|
||||||
font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
|
|
||||||
border: 1px solid var(--border2);
|
|
||||||
color: var(--text2);
|
|
||||||
background: var(--bg3);
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
|
||||||
.status-pill.open { color: var(--green); border-color: rgba(46,204,113,0.3); background: var(--green-dim); }
|
|
||||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
||||||
.status-pill.open .status-dot { animation: pulse 2s infinite; }
|
|
||||||
|
|
||||||
@keyframes pulse { 0%,100%{opacity:1; transform:scale(1)} 50%{opacity:0.5; transform:scale(0.8)} }
|
|
||||||
|
|
||||||
.last-updated { font-size: 0.72rem; color: var(--text3); font-family: 'Geist Mono', monospace; }
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: 36px; height: 36px; border-radius: 10px;
|
|
||||||
border: 1px solid var(--border2);
|
|
||||||
background: var(--bg3);
|
|
||||||
color: var(--text2);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
transition: all var(--transition);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.icon-btn:hover { background: var(--bg2); color: var(--text); border-color: var(--coral); transform: translateY(-1px); }
|
|
||||||
.icon-btn:active { transform: translateY(0); }
|
|
||||||
|
|
||||||
/* ── P&L Hero ──────────────────────────────────────────────────────── */
|
|
||||||
.pnl-hero {
|
|
||||||
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pnl-card {
|
|
||||||
padding: 24px 24px 20px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
position: relative; overflow: hidden;
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
|
||||||
.pnl-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
|
|
||||||
|
|
||||||
/* Accent bar at top */
|
|
||||||
.pnl-card::before {
|
|
||||||
content: ''; position: absolute; top: 0; left: 0; right: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg, var(--accent-a), var(--accent-b));
|
|
||||||
border-radius: 20px 20px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subtle glow blob */
|
|
||||||
.pnl-card::after {
|
|
||||||
content: ''; position: absolute; top: -40px; right: -40px;
|
|
||||||
width: 120px; height: 120px; border-radius: 50%;
|
|
||||||
background: var(--accent-a);
|
|
||||||
opacity: 0.08; filter: blur(30px); pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pnl-card.unrealised { --accent-a: #2ECC71; --accent-b: #1abc9c; }
|
|
||||||
.pnl-card.realised { --accent-a: #3498DB; --accent-b: #2980b9; }
|
|
||||||
.pnl-card.total { --accent-a: #FF6B4A; --accent-b: #F5A623; }
|
|
||||||
|
|
||||||
.pnl-label {
|
|
||||||
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase; color: var(--text2); margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pnl-value {
|
|
||||||
font-family: 'DM Serif Display', serif;
|
|
||||||
font-size: 2.4rem; line-height: 1;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
transition: color var(--transition);
|
|
||||||
}
|
|
||||||
.pnl-card.total .pnl-value { font-size: 3rem; }
|
|
||||||
|
|
||||||
.pnl-sub { font-size: 0.72rem; color: var(--text3); }
|
|
||||||
|
|
||||||
.up { color: var(--green); }
|
|
||||||
.down { color: var(--red); }
|
|
||||||
.flat { color: var(--text2); }
|
|
||||||
|
|
||||||
/* ── Stats row ─────────────────────────────────────────────────────── */
|
|
||||||
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
padding: 16px 18px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 14px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
|
||||||
.stat-label { font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); margin-bottom: 6px; }
|
|
||||||
.stat-value { font-family: 'DM Serif Display', serif; font-size: 1.6rem; color: var(--text); }
|
|
||||||
.stat-value.amber { color: var(--amber); }
|
|
||||||
.stat-value.red { color: var(--red); }
|
|
||||||
|
|
||||||
/* ── Chart card ────────────────────────────────────────────────────── */
|
|
||||||
.chart-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-header {
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
padding: 18px 24px 14px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title { font-size: 0.82rem; font-weight: 600; letter-spacing: 0.04em; color: var(--text); }
|
|
||||||
|
|
||||||
.range-btns { display: flex; gap: 4px; }
|
|
||||||
.range-btn {
|
|
||||||
padding: 4px 12px; border-radius: 8px; font-size: 0.72rem; font-weight: 600;
|
|
||||||
border: 1px solid var(--border2); cursor: pointer; background: transparent;
|
|
||||||
color: var(--text2); transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.range-btn:hover { color: var(--text); border-color: var(--coral); }
|
|
||||||
.range-btn.active { background: var(--coral); border-color: var(--coral); color: #fff; }
|
|
||||||
|
|
||||||
.chart-body { padding: 16px 20px 20px; height: 200px; position: relative; }
|
|
||||||
.chart-empty { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; color: var(--text3); font-size: 0.8rem; }
|
|
||||||
.chart-empty.show { display: flex; }
|
|
||||||
|
|
||||||
/* ── Table card ────────────────────────────────────────────────────── */
|
|
||||||
.table-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-header {
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
padding: 18px 24px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-title { font-size: 0.82rem; font-weight: 600; color: var(--text); }
|
|
||||||
.live-badge {
|
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em;
|
|
||||||
color: var(--green); text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
|
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
|
||||||
thead th {
|
|
||||||
padding: 10px 20px; text-align: right;
|
|
||||||
font-size: 0.66rem; font-weight: 600; letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase; color: var(--text3);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
thead th:first-child { text-align: left; }
|
|
||||||
|
|
||||||
tbody tr { transition: background 0.12s; }
|
|
||||||
tbody tr:hover { background: rgba(255,107,74,0.03); }
|
|
||||||
tbody tr + tr { border-top: 1px solid var(--border); }
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 13px 20px; font-size: 0.82rem;
|
|
||||||
text-align: right; color: var(--text);
|
|
||||||
}
|
|
||||||
td:first-child { text-align: left; }
|
|
||||||
|
|
||||||
.sym-name { font-weight: 600; font-size: 0.8rem; color: var(--text); }
|
|
||||||
.sym-meta { font-size: 0.67rem; color: var(--text3); margin-top: 2px; font-family: 'Geist Mono', monospace; }
|
|
||||||
|
|
||||||
.qty-long { color: var(--green); font-weight: 600; font-family: 'Geist Mono', monospace; }
|
|
||||||
.qty-short { color: var(--red); font-weight: 600; font-family: 'Geist Mono', monospace; }
|
|
||||||
|
|
||||||
.mono-val { font-family: 'Geist Mono', monospace; font-size: 0.8rem; }
|
|
||||||
|
|
||||||
tfoot tr { border-top: 2px solid var(--border2); background: var(--bg3); }
|
|
||||||
tfoot td { padding: 12px 20px; font-weight: 600; font-size: 0.82rem; }
|
|
||||||
tfoot td:first-child { font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2); }
|
|
||||||
|
|
||||||
/* ── Alerts card ───────────────────────────────────────────────────── */
|
|
||||||
.alert-row-up { border-left: 3px solid var(--green) !important; }
|
|
||||||
.alert-row-down { border-left: 3px solid var(--red) !important; }
|
|
||||||
|
|
||||||
/* ── Utility ───────────────────────────────────────────────────────── */
|
|
||||||
.section-gap { margin-bottom: 20px; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
|
<nav>
|
||||||
<!-- Nav -->
|
<div class="nav-l">
|
||||||
<nav>
|
<span class="logo">Angel</span>
|
||||||
<div class="nav-left">
|
<div class="nav-sep"></div>
|
||||||
<span class="nav-logo">Angel</span>
|
<span class="nav-sub">Position Tracker</span>
|
||||||
<div class="nav-divider"></div>
|
</div>
|
||||||
<span class="nav-subtitle">Position Tracker</span>
|
<div class="nav-r">
|
||||||
</div>
|
<span class="ts" id="ts">—</span>
|
||||||
<div class="nav-right">
|
<div class="pill" id="mkt-pill"><span class="dot"></span><span id="mkt-lbl">Checking</span></div>
|
||||||
<span class="last-updated" id="last-updated">—</span>
|
<button class="ibtn" id="theme-btn" onclick="toggleTheme()">☀️</button>
|
||||||
<div class="status-pill" id="market-status">
|
<button class="ibtn" onclick="refresh()">↻</button>
|
||||||
<span class="status-dot"></span>
|
</div>
|
||||||
<span id="market-label">Checking</span>
|
</nav>
|
||||||
</div>
|
<div class="g3">
|
||||||
<button class="icon-btn" id="theme-toggle" title="Toggle theme" onclick="toggleTheme()">🌙</button>
|
<div class="card pcard pu"><div class="plbl">Unrealised P&L</div><div class="pval fl" id="v-u">—</div><div class="psub">Open positions MTM</div></div>
|
||||||
<button class="icon-btn" title="Refresh" onclick="refresh()">↺</button>
|
<div class="card pcard pr"><div class="plbl">Realised P&L</div><div class="pval fl" id="v-r">—</div><div class="psub">Closed legs today</div></div>
|
||||||
</div>
|
<div class="card pcard pt"><div class="plbl">Total P&L</div><div class="pval fl" id="v-t">—</div><div class="psub">Unrealised + Realised</div></div>
|
||||||
</nav>
|
</div>
|
||||||
|
<div class="g6" id="market-grid">
|
||||||
<!-- P&L Hero -->
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
<div class="pnl-hero">
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
<div class="pnl-card unrealised">
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
<div class="pnl-label">Unrealised P&L</div>
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
<div class="pnl-value flat" id="val-unrealised">—</div>
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
<div class="pnl-sub">Open positions MTM</div>
|
<div class="card mcard"><div class="mlbl">Loading…</div><div class="mprice">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pnl-card realised">
|
<div class="g3" style="margin-bottom:16px">
|
||||||
<div class="pnl-label">Realised P&L</div>
|
<div class="card scard"><div class="slbl">Open Positions</div><div class="sval" id="s-pos">—</div></div>
|
||||||
<div class="pnl-value flat" id="val-realised">—</div>
|
<div class="card scard"><div class="slbl">Alerts Today</div><div class="sval amber" id="s-alrt">—</div></div>
|
||||||
<div class="pnl-sub">Closed legs today</div>
|
<div class="card scard"><div class="slbl">System</div><div class="sval" id="s-err" style="font-size:.88rem;font-family:'Geist',sans-serif;margin-top:2px;color:var(--green)">Healthy</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pnl-card total">
|
<div class="card" style="margin-bottom:16px">
|
||||||
<div class="pnl-label">Total P&L</div>
|
<div class="card-head">
|
||||||
<div class="pnl-value flat" id="val-total">—</div>
|
<span class="card-title">Intraday P&L Curve</span>
|
||||||
<div class="pnl-sub">Unrealised + Realised</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-wrap"><canvas id="pnl-chart"></canvas><div class="chart-empty" id="chart-empty">No history yet — populates after first poll cycle</div></div>
|
||||||
<!-- Stats -->
|
</div>
|
||||||
<div class="stats-row">
|
<div class="card" style="margin-bottom:16px">
|
||||||
<div class="stat-card">
|
<div class="card-head"><span class="card-title">Option Positions</span><div class="live-badge"><span class="ldot"></span>Live</div></div>
|
||||||
<div class="stat-label">Open Positions</div>
|
<table>
|
||||||
<div class="stat-value" id="stat-positions">—</div>
|
<thead><tr><th>Symbol</th><th>Qty</th><th>LTP</th><th>Avg Cost</th><th>Unrealised</th><th>Realised</th><th>Total P&L</th></tr></thead>
|
||||||
|
<tbody id="pos-body"><tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">Loading…</td></tr></tbody>
|
||||||
|
<tfoot id="pos-foot" style="display:none"><tr><td colspan="4">Portfolio Total</td><td id="f-u" class="mono">—</td><td id="f-r" class="mono">—</td><td id="f-t" class="mono">—</td></tr></tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="margin-bottom:16px">
|
||||||
|
<div class="card-head"><span class="card-title">Alert History</span></div>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Symbol</th><th>P&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 class="card" style="margin-bottom:16px">
|
||||||
|
<div class="card-head">
|
||||||
|
<span class="card-title">Alert Settings</span>
|
||||||
|
<span style="font-size:.68rem;color:var(--text3)">Changes apply immediately</span>
|
||||||
|
</div>
|
||||||
|
<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&L moves this % from anchor</span></div>
|
||||||
|
<div class="field"><label>Min Absolute Move (₹)</label><input id="cfg-abs" type="number" min="50" max="10000" step="50" value="100"/><span class="hint">Guard for near-zero P&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>
|
||||||
<div class="stat-card">
|
<div style="display:flex;align-items:center;margin-bottom:18px">
|
||||||
<div class="stat-label">Alerts Today</div>
|
<button class="btn-save" onclick="saveConfig()">Save Settings</button>
|
||||||
<div class="stat-value amber" id="stat-alerts">—</div>
|
<span class="save-msg" id="save-msg">✓ Saved</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div style="border-top:1px solid var(--border);padding-top:14px">
|
||||||
<div class="stat-label">Last Error</div>
|
<div style="font-size:.68rem;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text2);margin-bottom:10px">Per-position Overrides</div>
|
||||||
<div class="stat-value red" id="stat-error" style="font-size:0.9rem;font-family:'Geist',sans-serif;margin-top:4px">None</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>
|
||||||
|
</div>
|
||||||
<!-- Chart -->
|
</div>
|
||||||
<div class="chart-card">
|
|
||||||
<div class="chart-header">
|
|
||||||
<span class="chart-title">Intraday P&L Curve</span>
|
|
||||||
<div class="range-btns">
|
|
||||||
<button class="range-btn" onclick="loadChart(2)" id="btn-2h">2H</button>
|
|
||||||
<button class="range-btn active" onclick="loadChart(8)" id="btn-8h">8H</button>
|
|
||||||
<button class="range-btn" onclick="loadChart(24)" id="btn-24h">24H</button>
|
|
||||||
<button class="range-btn" onclick="loadChart(72)" id="btn-72h">3D</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-body">
|
|
||||||
<canvas id="pnl-chart"></canvas>
|
|
||||||
<div class="chart-empty" id="chart-empty">No history yet — populates after first poll cycle</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Positions -->
|
|
||||||
<div class="table-card section-gap">
|
|
||||||
<div class="table-header">
|
|
||||||
<span class="table-title">Option Positions</span>
|
|
||||||
<div class="live-badge"><span class="live-dot"></span> Live</div>
|
|
||||||
</div>
|
|
||||||
<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&L</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="pos-tbody">
|
|
||||||
<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text3)">Loading positions…</td></tr>
|
|
||||||
</tbody>
|
|
||||||
<tfoot id="pos-tfoot" style="display:none">
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">Portfolio Total</td>
|
|
||||||
<td id="foot-u" class="mono-val">—</td>
|
|
||||||
<td id="foot-r" class="mono-val">—</td>
|
|
||||||
<td id="foot-t" class="mono-val">—</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alerts -->
|
|
||||||
<div class="table-card">
|
|
||||||
<div class="table-header">
|
|
||||||
<span class="table-title">Alert History</span>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Symbol</th>
|
|
||||||
<th>P&L at Alert</th>
|
|
||||||
<th>Anchor</th>
|
|
||||||
<th>Move</th>
|
|
||||||
<th>LTP</th>
|
|
||||||
<th>Time (IST)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="alert-tbody">
|
|
||||||
<tr><td colspan="6" style="text-align:center;padding:32px;color:var(--text3)">No alerts yet</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- /wrap -->
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── Theme ─────────────────────────────────────────────────────────────────
|
let theme=localStorage.getItem('theme')||'dark';
|
||||||
let currentTheme = localStorage.getItem('theme') || 'dark';
|
document.documentElement.setAttribute('data-theme',theme);
|
||||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
document.getElementById('theme-btn').textContent=theme==='dark'?'\u2600\ufe0f':'\ud83c\udf19';
|
||||||
document.getElementById('theme-toggle').textContent = currentTheme === 'dark' ? '☀️' : '🌙';
|
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});
|
||||||
function toggleTheme() {
|
const cls=n=>n>50?'up':n<-50?'dn':'fl';
|
||||||
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
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'});
|
||||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
function parseTTE(sym){const m=sym.match(/([A-Z]+)(\d{2})(\d{2})(\d{2})(\d+)(CE|PE)$/);if(!m)return null;const exp=new Date('20'+m[2]+'-'+m[4]+'-'+m[3]);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';}
|
||||||
document.getElementById('theme-toggle').textContent = currentTheme === 'dark' ? '☀️' : '🌙';
|
let chartInst=null,curH=8;
|
||||||
localStorage.setItem('theme', currentTheme);
|
async function loadChart(h=8){
|
||||||
if (chartInstance) { chartInstance.destroy(); chartInstance = null; loadChart(currentHours); }
|
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);set('v-t',summary.totalPnl);document.getElementById('s-pos').textContent=summary.openPositions;}
|
||||||
|
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(){
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
const {ok,data=[]}=await fetch('/api/market').then(r=>r.json()).catch(()=>({ok:false,data:[]}));
|
||||||
const fmt = (n, d=0) => {
|
const grid=document.getElementById('market-grid');
|
||||||
if (n == null) return '—';
|
if(!ok||!data.length){grid.innerHTML='<div class="card mcard" style="grid-column:1/-1;text-align:center;padding:14px;color:var(--text3)">Market data unavailable</div>';return;}
|
||||||
const abs = Math.abs(n).toLocaleString('en-IN', {maximumFractionDigits: d});
|
grid.innerHTML=data.map(q=>{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">'+q.label+'</div><div class="mprice">'+price+' <span style="font-size:.62rem;color:var(--text2)">'+q.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</div>':'')+'</div>';}).join('');
|
||||||
return (n >= 0 ? '+₹' : '-₹') + abs;
|
|
||||||
};
|
|
||||||
const pnlCls = n => n > 50 ? 'up' : n < -50 ? 'down' : 'flat';
|
|
||||||
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'});
|
|
||||||
|
|
||||||
// ── Chart ──────────────────────────────────────────────────────────────────
|
|
||||||
let chartInstance = null;
|
|
||||||
let currentHours = 8;
|
|
||||||
|
|
||||||
async function loadChart(h = 8) {
|
|
||||||
currentHours = h;
|
|
||||||
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
const btnMap = {2:'btn-2h', 8:'btn-8h', 24:'btn-24h', 72:'btn-72h'};
|
|
||||||
document.getElementById(btnMap[h])?.classList.add('active');
|
|
||||||
|
|
||||||
const r = await fetch(`/api/pnl-history?hours=${h}`);
|
|
||||||
const { summary, history } = await r.json();
|
|
||||||
|
|
||||||
if (summary) {
|
|
||||||
const set = (id, val) => {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
el.textContent = fmt(val);
|
|
||||||
el.className = 'pnl-value ' + pnlCls(val);
|
|
||||||
};
|
|
||||||
set('val-unrealised', summary.totalUnrealised);
|
|
||||||
set('val-realised', summary.totalRealised);
|
|
||||||
set('val-total', summary.totalPnl);
|
|
||||||
document.getElementById('stat-positions').textContent = summary.openPositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = document.getElementById('pnl-chart');
|
|
||||||
const 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 isDark = currentTheme === 'dark';
|
|
||||||
const gridColor = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)';
|
|
||||||
const textColor = isDark ? '#484F58' : '#B8AFA8';
|
|
||||||
|
|
||||||
const labels = history.map(h => toIST(h.recorded_at));
|
|
||||||
const unrealised = history.map(h => h.total_unrealised);
|
|
||||||
const realised = history.map(h => h.total_realised);
|
|
||||||
const total = history.map(h => h.total_pnl);
|
|
||||||
|
|
||||||
if (chartInstance) chartInstance.destroy();
|
|
||||||
chartInstance = new Chart(canvas, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{ label: 'Total P&L', data: total,
|
|
||||||
borderColor: '#FF6B4A', backgroundColor: 'rgba(255,107,74,0.08)',
|
|
||||||
tension: 0.4, pointRadius: 0, borderWidth: 2, fill: true },
|
|
||||||
{ label: 'Unrealised', data: unrealised,
|
|
||||||
borderColor: '#2ECC71', backgroundColor: 'transparent',
|
|
||||||
tension: 0.4, pointRadius: 0, borderWidth: 1.5 },
|
|
||||||
{ label: 'Realised', data: realised,
|
|
||||||
borderColor: '#3498DB', backgroundColor: 'transparent',
|
|
||||||
tension: 0.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: textColor, boxWidth: 10, font: { size: 11, family: 'Geist' } } },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: isDark ? '#1C2330' : '#fff',
|
|
||||||
borderColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
|
||||||
borderWidth: 1, titleColor: textColor,
|
|
||||||
bodyColor: isDark ? '#E6EDF3' : '#1A1410',
|
|
||||||
padding: 10, cornerRadius: 8,
|
|
||||||
callbacks: {
|
|
||||||
label: ctx => ` ${ctx.dataset.label}: ${ctx.raw >= 0 ? '+' : ''}₹${Math.round(ctx.raw).toLocaleString('en-IN')}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: { ticks: { color: textColor, maxTicksLimit: 8, font: {size:10, family:'Geist Mono'} }, grid: { color: gridColor } },
|
|
||||||
y: { ticks: { color: textColor, font: {size:10, family:'Geist Mono'},
|
|
||||||
callback: v => (v>=0?'+':'')+'₹'+Math.round(v).toLocaleString('en-IN') },
|
|
||||||
grid: { color: gridColor } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
async function loadPositions(){
|
||||||
// ── Positions ──────────────────────────────────────────────────────────────
|
const {data}=await fetch('/api/positions').then(r=>r.json());
|
||||||
async function loadPositions() {
|
const open=data.filter(p=>p.is_closed===0);
|
||||||
const r = await fetch('/api/positions');
|
const tbody=document.getElementById('pos-body'),tfoot=document.getElementById('pos-foot');
|
||||||
const { data } = await r.json();
|
if(!open.length){tbody.innerHTML='<tr><td colspan="7" style="text-align:center;padding:36px;color:var(--text3)">No open positions</td></tr>';tfoot.style.display='none';return;}
|
||||||
const open = data.filter(p => p.is_closed === 0);
|
const sU=open.reduce((s,p)=>s+p.unrealised_pnl,0),sR=open.reduce((s,p)=>s+p.realised_pnl,0),sT=open.reduce((s,p)=>s+p.total_pnl,0);
|
||||||
const tbody = document.getElementById('pos-tbody');
|
tbody.innerHTML=open.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('');
|
||||||
const tfoot = document.getElementById('pos-tfoot');
|
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);
|
||||||
if (!open.length) {
|
document.getElementById('override-body').innerHTML=open.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('');
|
||||||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--text3)">No open positions</td></tr>`;
|
|
||||||
tfoot.style.display = 'none'; return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumU = open.reduce((s,p) => s + p.unrealised_pnl, 0);
|
|
||||||
const sumR = open.reduce((s,p) => s + p.realised_pnl, 0);
|
|
||||||
const sumT = open.reduce((s,p) => s + p.total_pnl, 0);
|
|
||||||
|
|
||||||
tbody.innerHTML = open.map(p => `
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="sym-name">${p.tradingsymbol}</div>
|
|
||||||
<div class="sym-meta">${p.exchange} · ${p.producttype}</div>
|
|
||||||
</td>
|
|
||||||
<td class="${p.netqty < 0 ? 'qty-short' : 'qty-long'}">${p.netqty > 0 ? '+' : ''}${p.netqty}</td>
|
|
||||||
<td class="mono-val">₹${(+p.ltp).toFixed(2)}</td>
|
|
||||||
<td class="mono-val" style="color:var(--text2)">₹${(+p.avg_price).toFixed(2)}</td>
|
|
||||||
<td class="mono-val ${pnlCls(p.unrealised_pnl)}">${fmt(p.unrealised_pnl)}</td>
|
|
||||||
<td class="mono-val ${pnlCls(p.realised_pnl)}">${fmt(p.realised_pnl)}</td>
|
|
||||||
<td class="mono-val ${pnlCls(p.total_pnl)}" style="font-weight:600">${fmt(p.total_pnl)}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
|
|
||||||
tfoot.style.display = '';
|
|
||||||
const sf = (id,v) => {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
el.textContent = fmt(v);
|
|
||||||
el.className = `mono-val ${pnlCls(v)}`;
|
|
||||||
};
|
|
||||||
sf('foot-u', sumU); sf('foot-r', sumR); sf('foot-t', sumT);
|
|
||||||
}
|
}
|
||||||
|
async function loadAlerts(){
|
||||||
// ── Alerts ─────────────────────────────────────────────────────────────────
|
const {data}=await fetch('/api/alerts?limit=20').then(r=>r.json());
|
||||||
async function loadAlerts() {
|
const today=new Date().toISOString().slice(0,10);
|
||||||
const r = await fetch('/api/alerts?limit=20');
|
document.getElementById('s-alrt').textContent=data.filter(a=>a.alerted_at.startsWith(today)).length;
|
||||||
const { data } = await r.json();
|
if(!data.length)return;
|
||||||
const today = new Date().toISOString().slice(0,10);
|
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('');
|
||||||
const todayCount = data.filter(a => a.alerted_at.startsWith(today)).length;
|
|
||||||
document.getElementById('stat-alerts').textContent = todayCount;
|
|
||||||
|
|
||||||
if (!data.length) return;
|
|
||||||
document.getElementById('alert-tbody').innerHTML = data.map(a => `
|
|
||||||
<tr class="${a.direction==='up' ? 'alert-row-up' : 'alert-row-down'}">
|
|
||||||
<td><div class="sym-name">${a.direction==='up'?'▲':'▼'} ${a.tradingsymbol}</div></td>
|
|
||||||
<td class="mono-val ${pnlCls(a.current_pnl)}">${fmt(a.current_pnl)}</td>
|
|
||||||
<td class="mono-val" style="color:var(--text2)">${fmt(a.anchor_pnl)}</td>
|
|
||||||
<td class="mono-val ${pnlCls(a.delta_abs)}">${fmt(a.delta_abs)} <span style="opacity:0.6;font-size:0.75em">(${a.delta_pct>0?'+':''}${(+a.delta_pct).toFixed(1)}%)</span></td>
|
|
||||||
<td class="mono-val">₹${(+a.ltp).toFixed(2)}</td>
|
|
||||||
<td style="color:var(--text3);font-family:'Geist Mono',monospace;font-size:0.75rem">${toIST(a.alerted_at)}</td>
|
|
||||||
</tr>`).join('');
|
|
||||||
}
|
}
|
||||||
|
async function loadHealth(){
|
||||||
// ── Health ─────────────────────────────────────────────────────────────────
|
const d=await fetch('/api/health').then(r=>r.json());
|
||||||
async function loadHealth() {
|
const pill=document.getElementById('mkt-pill');
|
||||||
const r = await fetch('/api/health');
|
document.getElementById('mkt-lbl').textContent=d.marketOpen?'Market Open':'Market Closed';
|
||||||
const d = await r.json();
|
pill.className='pill'+(d.marketOpen?' open':'');
|
||||||
const pill = document.getElementById('market-status');
|
const e=document.getElementById('s-err');
|
||||||
const label = document.getElementById('market-label');
|
if(d.lastError){e.textContent=d.lastError.error.slice(0,30)+'\u2026';e.style.color='var(--red)';}
|
||||||
label.textContent = d.marketOpen ? 'Market Open' : 'Market Closed';
|
else{e.textContent='Healthy';e.style.color='var(--green)';}
|
||||||
pill.className = 'status-pill' + (d.marketOpen ? ' open' : '');
|
document.getElementById('ts').textContent=new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false})+' IST';
|
||||||
document.getElementById('stat-error').textContent = d.lastError ? d.lastError.error.slice(0,30)+'…' : 'None';
|
|
||||||
document.getElementById('last-updated').textContent =
|
|
||||||
new Date().toLocaleTimeString('en-IN',{timeZone:'Asia/Kolkata',hour12:false}) + ' IST';
|
|
||||||
}
|
}
|
||||||
|
async function loadConfig(){
|
||||||
// ── Main loop ──────────────────────────────────────────────────────────────
|
const {global:g}=await fetch('/api/config').then(r=>r.json()).catch(()=>({}));
|
||||||
async function refresh() {
|
if(!g)return;
|
||||||
await Promise.all([loadPositions(), loadAlerts(), loadHealth(), loadChart(currentHours)]);
|
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(){
|
||||||
refresh();
|
const pct=parseFloat(document.getElementById('cfg-pct').value);
|
||||||
setInterval(refresh, 60_000);
|
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()]);}
|
||||||
|
loadConfig();refresh();
|
||||||
|
setInterval(refresh,60000);
|
||||||
|
setInterval(loadMarket,15000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue