datamk's picture
Upload 2 files
ce7d4d5 verified
Raw
History Blame Contribute Delete
87.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIFTY 50 Directional Forecaster</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome for Premium Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-color: #070913;
--card-bg: rgba(18, 22, 45, 0.4);
--card-border: rgba(255, 255, 255, 0.07);
--card-border-hover: rgba(147, 51, 234, 0.3);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
/* Accent Colors */
--accent-purple: #a855f7;
--accent-blue: #3b82f6;
--accent-emerald: #10b981;
--accent-rose: #f43f5e;
--accent-amber: #f59e0b;
/* Glows */
--glow-purple: rgba(168, 85, 247, 0.15);
--glow-emerald: rgba(16, 185, 129, 0.15);
--glow-rose: rgba(244, 63, 94, 0.15);
--glow-blue: rgba(59, 130, 246, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
/* Custom Scrollbar */
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
background-image:
radial-gradient(at 10% 20%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
radial-gradient(at 90% 80%, rgba(168, 85, 247, 0.15) 0px, transparent 50%),
radial-gradient(at 50% 50%, rgba(18, 22, 45, 0.5) 0px, transparent 80%);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
padding-bottom: 40px;
overflow-x: hidden;
}
h1, h2, h3, h4, .title-font {
font-family: 'Outfit', sans-serif;
}
.container {
max-width: 1440px;
margin: 0 auto;
padding: 0 24px;
}
/* Glassmorphism utility */
.glass-panel {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-panel:hover {
border-color: rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
/* Header design */
header {
padding: 24px 0;
margin-bottom: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(7, 9, 19, 0.6);
backdrop-filter: blur(12px);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.logo-area {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
font-size: 28px;
background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(0 2px 8px rgba(168, 85, 247, 0.4));
}
.logo-title {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(to right, #ffffff, #d1d5db);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.logo-badge {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 6px;
background: rgba(168, 85, 247, 0.15);
color: var(--accent-purple);
border: 1px solid rgba(168, 85, 247, 0.3);
letter-spacing: 0.5px;
}
/* System Info Statuses */
.system-status {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
padding: 6px 12px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-dot.active {
background-color: var(--accent-emerald);
box-shadow: 0 0 8px var(--accent-emerald);
animation: pulse 2s infinite;
}
.status-dot.pending {
background-color: var(--accent-amber);
box-shadow: 0 0 8px var(--accent-amber);
}
.status-dot.inactive {
background-color: var(--accent-rose);
box-shadow: 0 0 8px var(--accent-rose);
}
/* Nav Tabs */
.nav-tabs {
display: flex;
gap: 8px;
padding: 4px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
margin-bottom: 28px;
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-family: 'Outfit', sans-serif;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.25s ease;
}
.tab-btn:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.04);
}
.tab-btn.active {
color: #ffffff;
background: linear-gradient(135deg, rgba(168, 85, 247, 0.25), rgba(59, 130, 246, 0.25));
border: 1px solid rgba(168, 85, 247, 0.3);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
/* Main View Switcher */
.tab-content {
display: none;
animation: fadeIn 0.4s ease;
}
.tab-content.active {
display: block;
}
/* Grid Layouts */
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
/* Prediction Card Section */
.predictions-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.predict-card {
padding: 24px;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 280px;
}
.predict-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
}
.predict-card.up::before { background: linear-gradient(to right, var(--accent-emerald), #34d399); }
.predict-card.down::before { background: linear-gradient(to right, var(--accent-rose), #fb7185); }
.predict-card.pending::before { background: linear-gradient(to right, var(--accent-amber), #fbbf24); }
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.card-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-secondary);
font-weight: 700;
}
.card-badge {
font-size: 11px;
padding: 4px 8px;
border-radius: 6px;
font-weight: 600;
}
.predict-card.up .card-badge { background: rgba(16, 185, 129, 0.1); color: var(--accent-emerald); }
.predict-card.down .card-badge { background: rgba(244, 63, 94, 0.1); color: var(--accent-rose); }
.predict-card.pending .card-badge { background: rgba(245, 158, 11, 0.1); color: var(--accent-amber); }
.signal-display {
text-align: center;
margin: 20px 0;
}
.signal-text {
font-size: 48px;
font-weight: 900;
letter-spacing: -1.5px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.predict-card.up .signal-text {
color: var(--accent-emerald);
text-shadow: 0 0 20px var(--glow-emerald);
}
.predict-card.down .signal-text {
color: var(--accent-rose);
text-shadow: 0 0 20px var(--glow-rose);
}
.predict-card.pending .signal-text {
color: var(--accent-amber);
font-size: 32px;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
font-size: 13px;
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
color: var(--text-secondary);
}
.metric-value {
font-weight: 600;
}
/* Nifty Quote Card widget */
.quote-panel {
padding: 24px;
background: radial-gradient(circle at top right, rgba(59, 130, 246, 0.08), transparent), var(--card-bg);
}
.quote-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.quote-symbol {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.5px;
}
.quote-price-area {
margin-bottom: 20px;
}
.quote-price {
font-size: 36px;
font-weight: 800;
letter-spacing: -1px;
line-height: 1;
}
.quote-change {
font-size: 14px;
font-weight: 600;
margin-top: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.quote-change.up { color: var(--accent-emerald); }
.quote-change.down { color: var(--accent-rose); }
.quote-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
font-size: 13px;
}
.quote-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 8px;
padding: 10px;
}
/* Action hub panel */
.action-panel {
padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.action-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
}
.btn {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--text-primary);
padding: 12px 18px;
border-radius: 10px;
cursor: pointer;
font-family: 'Outfit', sans-serif;
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.2s ease;
}
.btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.15);
}
.btn:active {
transform: translateY(1px);
}
.btn.btn-primary {
background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
border: none;
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.3);
}
.btn.btn-primary:hover {
opacity: 0.9;
box-shadow: 0 6px 20px rgba(168, 85, 247, 0.4);
}
.btn.btn-success {
background: rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.3);
color: var(--accent-emerald);
}
.btn.btn-success:hover {
background: rgba(16, 185, 129, 0.25);
}
.btn.btn-danger {
background: rgba(244, 63, 94, 0.15);
border-color: rgba(244, 63, 94, 0.3);
color: var(--accent-rose);
}
.btn.btn-danger:hover {
background: rgba(244, 63, 94, 0.25);
}
.btn i.spin {
animation: spin 1s linear infinite;
}
/* MFE Panel specific */
.mfe-panel {
padding: 24px;
background: radial-gradient(circle at bottom left, rgba(168, 85, 247, 0.08), transparent), var(--card-bg);
margin-bottom: 24px;
}
.mfe-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.mfe-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 600px) {
.mfe-body {
grid-template-columns: 1fr;
}
}
.mfe-side {
background: rgba(255, 255, 255, 0.01);
border: 1px solid rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 16px;
}
.mfe-side-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 8px;
}
.mfe-pts {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.5px;
}
.mfe-side.up .mfe-pts { color: var(--accent-emerald); }
.mfe-side.down .mfe-pts { color: var(--accent-rose); }
/* Chart container */
.chart-panel {
padding: 24px;
margin-bottom: 24px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.chart-wrapper {
position: relative;
width: 100%;
height: 380px;
}
/* Accuracy Grid */
.accuracy-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.accuracy-card {
padding: 24px;
}
.accuracy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.gauge-area {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 120px;
margin-bottom: 16px;
}
.gauge-number {
position: absolute;
font-size: 32px;
font-weight: 800;
font-family: 'Outfit', sans-serif;
}
.gauge-svg {
transform: rotate(-90deg);
width: 120px;
height: 120px;
}
.gauge-svg circle {
fill: none;
stroke-width: 10;
}
.gauge-track {
stroke: rgba(255, 255, 255, 0.05);
}
.gauge-fill {
stroke: var(--accent-purple);
stroke-linecap: round;
stroke-dasharray: 314.16;
stroke-dashoffset: 314.16;
transition: stroke-dashoffset 1s ease-out;
}
/* Tables & Lists style */
.table-panel {
padding: 24px;
margin-bottom: 24px;
}
.table-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
.table-responsive {
width: 100%;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 13px;
}
th {
color: var(--text-secondary);
font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.5px;
}
td {
padding: 14px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
color: var(--text-primary);
}
tr:hover td {
background: rgba(255, 255, 255, 0.01);
}
.badge-pill {
display: inline-block;
padding: 3px 8px;
border-radius: 99px;
font-size: 11px;
font-weight: 600;
}
.badge-pill.success { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); }
.badge-pill.danger { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); }
.badge-pill.secondary { background: rgba(255, 255, 255, 0.06); color: var(--text-secondary); }
/* Tools Page Tabs and Grids */
.tools-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 1024px) {
.tools-grid {
grid-template-columns: 1fr;
}
}
/* Kotak credentials and snap */
.kotak-panel {
padding: 24px;
}
.kotak-auth-box {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 20px;
margin-top: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
margin-bottom: 6px;
text-transform: uppercase;
}
.form-input {
width: 100%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 12px;
color: #fff;
font-family: inherit;
font-size: 14px;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent-purple);
box-shadow: 0 0 10px rgba(168, 85, 247, 0.25);
}
/* Balances & Grid counters */
.cash-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
margin-top: 16px;
}
.cash-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
padding: 14px;
}
.cash-title {
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 6px;
}
.cash-val {
font-size: 18px;
font-weight: 700;
font-family: 'Outfit', sans-serif;
}
/* Screener style */
.screener-panel {
padding: 24px;
}
.search-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.screener-results {
margin-top: 16px;
animation: fadeIn 0.4s ease;
}
.pro-con-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 16px 0;
}
@media (max-width: 600px) {
.pro-con-grid {
grid-template-columns: 1fr;
}
}
.pro-list, .con-list {
padding: 16px;
border-radius: 12px;
font-size: 12px;
}
.pro-list { background: rgba(16, 185, 129, 0.04); border: 1px solid rgba(16, 185, 129, 0.1); }
.con-list { background: rgba(244, 63, 94, 0.04); border: 1px solid rgba(244, 63, 94, 0.1); }
.pro-list h4 { color: var(--accent-emerald); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.con-list h4 { color: var(--accent-rose); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.bullet-list li {
list-style: none;
margin-bottom: 8px;
position: relative;
padding-left: 14px;
line-height: 1.4;
}
.pro-list li::before { content: '•'; color: var(--accent-emerald); position: absolute; left: 0; }
.con-list li::before { content: '•'; color: var(--accent-rose); position: absolute; left: 0; }
.screener-sec-title {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 24px 0 10px 0;
color: var(--text-secondary);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 6px;
}
/* Settings cog & modal */
.settings-cog {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
width: 38px;
height: 38px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.2s ease;
}
.settings-cog:hover {
color: #fff;
border-color: rgba(255, 255, 255, 0.15);
transform: rotate(30deg);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.modal-overlay.active {
opacity: 1;
pointer-events: all;
}
.modal {
width: 450px;
max-width: 90%;
padding: 28px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 20px;
font-weight: 700;
}
.modal-close {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 20px;
cursor: pointer;
}
.modal-close:hover {
color: #fff;
}
/* Toast notifications */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: rgba(18, 22, 45, 0.9);
border: 1px solid var(--card-border);
backdrop-filter: blur(12px);
padding: 14px 20px;
border-radius: 10px;
color: #fff;
font-size: 13px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
min-width: 280px;
}
.toast.success { border-left: 4px solid var(--accent-emerald); }
.toast.error { border-left: 4px solid var(--accent-rose); }
.toast.info { border-left: 4px solid var(--accent-blue); }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(100%) translateY(0); opacity: 0; }
to { transform: translateX(0) translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<header>
<div class="container header-content">
<div class="logo-area">
<i class="fa-solid fa-chart-line logo-icon"></i>
<div class="logo-title">NIFTY 50 Forecaster</div>
<div class="logo-badge">Live</div>
</div>
<div class="system-status">
<div class="status-badge">
<span id="conn-dot" class="status-dot inactive"></span>
<span id="conn-text">Connecting...</span>
</div>
<div class="status-badge">
<i class="fa-regular fa-clock"></i>
<span id="server-time">--:--:-- IST</span>
</div>
<div class="status-badge">
<i class="fa-solid fa-calendar-day"></i>
<span id="trading-day-text">Trading Day: --</span>
</div>
<button class="settings-cog" id="settings-btn" title="API Settings">
<i class="fa-solid fa-cog"></i>
</button>
</div>
</div>
</header>
<main class="container">
<!-- Main Tabs -->
<nav class="nav-tabs">
<button class="tab-btn active" data-tab="overview">
<i class="fa-solid fa-chart-pie"></i> Overview
</button>
<button class="tab-btn" data-tab="analytics">
<i class="fa-solid fa-chart-column"></i> Performance & Analytics
</button>
<button class="tab-btn" data-tab="tools">
<i class="fa-solid fa-toolbox"></i> Tools (Broker & Screener)
</button>
</nav>
<!-- 1. OVERVIEW TAB CONTENT -->
<div id="overview" class="tab-content active">
<div class="dashboard-grid">
<!-- Left Side: Prediction cards & MFE -->
<div>
<!-- Main Predictions Grid -->
<div class="predictions-container">
<!-- T+5 Card -->
<div class="glass-panel predict-card pending" id="card-t5">
<div class="card-header">
<span class="card-title">T+5 Prediction</span>
<span class="card-badge" id="t5-badge">Pending</span>
</div>
<div class="signal-display">
<div class="signal-text" id="t5-signal">
<i class="fa-solid fa-hourglass-half"></i> PENDING
</div>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Confidence</span>
<span class="metric-value" id="t5-conf">--%</span>
</div>
<div class="metric-row">
<span class="metric-label">Probability (Up)</span>
<span class="metric-value" id="t5-prob">--%</span>
</div>
<div class="metric-row">
<span class="metric-label">Model</span>
<span class="metric-value" id="t5-model">--</span>
</div>
</div>
</div>
<!-- Tomorrow Card -->
<div class="glass-panel predict-card pending" id="card-tomorrow">
<div class="card-header">
<span class="card-title">Tomorrow Prediction</span>
<span class="card-badge" id="tomorrow-badge">Pending</span>
</div>
<div class="signal-display">
<div class="signal-text" id="tomorrow-signal">
<i class="fa-solid fa-hourglass-half"></i> PENDING
</div>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Target Date</span>
<span class="metric-value" id="tomorrow-target-date">--</span>
</div>
<div class="metric-row">
<span class="metric-label">Probability (Up)</span>
<span class="metric-value" id="tomorrow-prob">--%</span>
</div>
<div class="metric-row">
<span class="metric-label">Model</span>
<span class="metric-value" id="tomorrow-model">--</span>
</div>
</div>
</div>
<!-- T+1 Card -->
<div class="glass-panel predict-card pending" id="card-tplus1">
<div class="card-header">
<span class="card-title">T+1 Prediction</span>
<span class="card-badge" id="tplus1-badge">Pending</span>
</div>
<div class="signal-display">
<div class="signal-text" id="tplus1-signal">
<i class="fa-solid fa-hourglass-half"></i> PENDING
</div>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Confidence</span>
<span class="metric-value" id="tplus1-conf">--%</span>
</div>
<div class="metric-row">
<span class="metric-label">Probability (Up)</span>
<span class="metric-value" id="tplus1-prob">--%</span>
</div>
<div class="metric-row">
<span class="metric-label">Model</span>
<span class="metric-value" id="tplus1-model">--</span>
</div>
</div>
</div>
</div>
<!-- MFE Regression Card -->
<div class="glass-panel mfe-panel">
<div class="mfe-header">
<h3 class="title-font"><i class="fa-solid fa-expand"></i> Maximum Favorable Excursion (MFE) Bounds</h3>
<span class="status-badge" id="mfe-date-badge">Session: --</span>
</div>
<div class="mfe-body">
<div class="mfe-side up">
<div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-up"></i> Predicted Day High</div>
<div class="mfe-pts" id="mfe-high-val">+-- pts</div>
<div style="margin-top: 10px;">
<div class="metric-row">
<span class="metric-label">Target Level</span>
<span class="metric-value" id="mfe-high-level">--</span>
</div>
<div class="metric-row">
<span class="metric-label">Actual Day High</span>
<span class="metric-value" id="mfe-high-actual">--</span>
</div>
</div>
</div>
<div class="mfe-side down">
<div class="mfe-side-title"><i class="fa-solid fa-circle-arrow-down"></i> Predicted Day Low</div>
<div class="mfe-pts" id="mfe-low-val">--- pts</div>
<div style="margin-top: 10px;">
<div class="metric-row">
<span class="metric-label">Target Level</span>
<span class="metric-value" id="mfe-low-level">--</span>
</div>
<div class="metric-row">
<span class="metric-label">Actual Day Low</span>
<span class="metric-value" id="mfe-low-actual">--</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Side: Nifty Quote and Action Hub -->
<div>
<!-- Nifty Live Quote panel -->
<div class="glass-panel quote-panel" style="margin-bottom: 24px;">
<div class="quote-header">
<span class="quote-symbol title-font">NIFTY 50 SPOT</span>
<span class="logo-badge" id="quote-source">Source: --</span>
</div>
<div class="quote-price-area">
<div class="quote-price" id="quote-price">------</div>
<div class="quote-change" id="quote-change">-- (--%)</div>
</div>
<div class="quote-grid">
<div class="quote-item">
<div style="color: var(--text-secondary); font-size: 11px;">Open</div>
<div style="font-weight: 700; margin-top: 4px;" id="quote-open">--</div>
</div>
<div class="quote-item">
<div style="color: var(--text-secondary); font-size: 11px;">Prev Close</div>
<div style="font-weight: 700; margin-top: 4px;" id="quote-prev">--</div>
</div>
<div class="quote-item">
<div style="color: var(--text-secondary); font-size: 11px;">Day High</div>
<div style="font-weight: 700; margin-top: 4px;" id="quote-high">--</div>
</div>
<div class="quote-item">
<div style="color: var(--text-secondary); font-size: 11px;">Day Low</div>
<div style="font-weight: 700; margin-top: 4px;" id="quote-low">--</div>
</div>
</div>
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 14px; text-align: right;" id="quote-time">
As of: --
</div>
</div>
<!-- Action Hub panel -->
<div class="glass-panel action-panel">
<h4 class="action-title"><i class="fa-solid fa-gears"></i> Operations Control Hub</h4>
<button class="btn btn-primary" id="btn-keepalive">
<i class="fa-solid fa-heartbeat"></i> Trigger Keepalive Ping
</button>
<button class="btn" id="btn-refresh-first5">
<i class="fa-solid fa-hourglass-start"></i> Refresh First 5-Mins (T+5)
</button>
<button class="btn" id="btn-refresh-daily">
<i class="fa-solid fa-sync"></i> Refresh Daily candles
</button>
<button class="btn" id="btn-refresh-close">
<i class="fa-solid fa-circle-check"></i> Refresh Market Close Data
</button>
<div id="data-staleness" style="font-size: 11px; margin-top: 6px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 8px;">
<div style="font-weight: 700; margin-bottom: 6px; color: var(--text-secondary); text-transform: uppercase;">Stale Checks:</div>
<div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
<span>Daily Data Stale:</span>
<span id="stale-daily" class="badge-pill secondary">--</span>
</div>
<div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
<span>Minutes Data Stale:</span>
<span id="stale-minutes" class="badge-pill secondary">--</span>
</div>
<div style="display:flex; justify-content:space-between; margin-bottom: 4px;">
<span>T+5 Forecast Stale:</span>
<span id="stale-t5" class="badge-pill secondary">--</span>
</div>
<div style="display:flex; justify-content:space-between;">
<span>T+1 Forecast Stale:</span>
<span id="stale-tplus1" class="badge-pill secondary">--</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2. ANALYTICS & BACKTEST TAB CONTENT -->
<div id="analytics" class="tab-content">
<!-- Daily Closes chart -->
<div class="glass-panel chart-panel">
<div class="chart-header">
<h3 class="title-font"><i class="fa-solid fa-chart-line"></i> NIFTY 50 Daily Closes History</h3>
<span class="status-badge" id="chart-sessions-badge">Last 180 Days</span>
</div>
<div class="chart-wrapper">
<canvas id="closesChart"></canvas>
</div>
</div>
<!-- Accuracy Gauges -->
<div class="accuracy-grid">
<!-- T+5 Accuracy -->
<div class="glass-panel accuracy-card">
<div class="accuracy-header">
<h4 class="title-font">T+5 Model Accuracy</h4>
<span class="badge-pill secondary" id="gauge-t5-count">0 Days</span>
</div>
<div class="gauge-area">
<span class="gauge-number" id="gauge-t5-pct">--%</span>
<svg class="gauge-svg">
<circle class="gauge-track" cx="60" cy="60" r="50"></circle>
<circle class="gauge-fill" id="gauge-t5-fill" cx="60" cy="60" r="50"></circle>
</svg>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Live Correct</span>
<span class="metric-value" id="gauge-t5-correct">0</span>
</div>
<div class="metric-row">
<span class="metric-label">Validation Accuracy</span>
<span class="metric-value" id="gauge-t5-val">--%</span>
</div>
</div>
</div>
<!-- Tomorrow Accuracy -->
<div class="glass-panel accuracy-card">
<div class="accuracy-header">
<h4 class="title-font">Tomorrow Model Accuracy</h4>
<span class="badge-pill secondary" id="gauge-tomorrow-count">0 Days</span>
</div>
<div class="gauge-area">
<span class="gauge-number" id="gauge-tomorrow-pct">--%</span>
<svg class="gauge-svg">
<circle class="gauge-track" cx="60" cy="60" r="50"></circle>
<circle class="gauge-fill" id="gauge-tomorrow-fill" cx="60" cy="60" r="50"></circle>
</svg>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Live Correct</span>
<span class="metric-value" id="gauge-tomorrow-correct">0</span>
</div>
<div class="metric-row">
<span class="metric-label">Validation Accuracy</span>
<span class="metric-value" id="gauge-tomorrow-val">--%</span>
</div>
</div>
</div>
<!-- T+1 Accuracy -->
<div class="glass-panel accuracy-card">
<div class="accuracy-header">
<h4 class="title-font">T+1 Model Accuracy</h4>
<span class="badge-pill secondary" id="gauge-tplus1-count">0 Days</span>
</div>
<div class="gauge-area">
<span class="gauge-number" id="gauge-tplus1-pct">--%</span>
<svg class="gauge-svg">
<circle class="gauge-track" cx="60" cy="60" r="50"></circle>
<circle class="gauge-fill" id="gauge-tplus1-fill" cx="60" cy="60" r="50"></circle>
</svg>
</div>
<div>
<div class="metric-row">
<span class="metric-label">Live Correct</span>
<span class="metric-value" id="gauge-tplus1-correct">0</span>
</div>
<div class="metric-row">
<span class="metric-label">Validation Accuracy</span>
<span class="metric-value" id="gauge-tplus1-val">--%</span>
</div>
</div>
</div>
</div>
<!-- Tomorrow Predictions Live Track record -->
<div class="glass-panel table-panel">
<div class="table-title">Tomorrow Forecast Live Track Record (Last 10 Days)</div>
<div class="table-responsive">
<table>
<thead>
<tr>
<th>Date</th>
<th>Input Date</th>
<th>Prediction</th>
<th>Probability (Up)</th>
<th>Actual Direction</th>
<th>Move %</th>
<th>Outcome</th>
<th>Source</th>
</tr>
</thead>
<tbody id="tomorrow-track-record-tbody">
<tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 3. TOOLS TAB CONTENT -->
<div id="tools" class="tab-content">
<div class="tools-grid">
<!-- Left panel: Kotak Neo Broker -->
<div class="glass-panel kotak-panel">
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px;">
<h3 class="title-font"><i class="fa-solid fa-key"></i> Kotak Securities Neo</h3>
<span class="badge-pill secondary" id="kotak-status-badge">Checking...</span>
</div>
<!-- Auth Box if not authenticated -->
<div id="kotak-auth-container" class="kotak-auth-box">
<h4 style="font-size: 13px; font-weight: 700; margin-bottom: 12px; color: var(--accent-purple);">Session Authentication Required</h4>
<div class="form-group">
<label for="totp-input">Enter TOTP Token</label>
<input type="text" id="totp-input" class="form-input" placeholder="6-digit auth code" maxlength="6">
</div>
<button class="btn btn-primary" id="btn-kotak-login" style="width: 100%;">
<i class="fa-solid fa-right-to-bracket"></i> Validate Session Token
</button>
</div>
<!-- Snapshot information if authenticated -->
<div id="kotak-snapshot-container" style="display: none; margin-top: 16px;">
<h4 style="font-size: 13px; font-weight: 700; color: var(--accent-emerald);">Active Trading Portfolio Snap</h4>
<div class="cash-grid">
<div class="cash-card">
<div class="cash-title">Net Value</div>
<div class="cash-val" id="cash-net">--</div>
</div>
<div class="cash-card">
<div class="cash-title">Margin Used</div>
<div class="cash-val" id="cash-margin">--</div>
</div>
<div class="cash-card">
<div class="cash-title">Available Cash</div>
<div class="cash-val" id="cash-avail">--</div>
</div>
<div class="cash-card">
<div class="cash-title">Live P&L</div>
<div class="cash-val" id="cash-pnl">--</div>
</div>
</div>
<!-- Active holdings list -->
<div class="screener-sec-title" style="margin-top: 20px;">Current Stock Holdings</div>
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
<table>
<thead>
<tr>
<th>Symbol</th>
<th>Qty</th>
<th>Cost</th>
<th>Market Val</th>
<th>P&L</th>
</tr>
</thead>
<tbody id="kotak-holdings-tbody">
<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Right panel: Screener Scraper -->
<div class="glass-panel screener-panel">
<h3 class="title-font" style="border-bottom:1px solid rgba(255,255,255,0.05); padding-bottom:12px; margin-bottom:16px;">
<i class="fa-solid fa-search"></i> Fundamental Stock Screener
</h3>
<div class="search-row">
<input type="text" id="screener-query" class="form-input" placeholder="Enter stock symbol (e.g., RELIANCE, TCS)">
<button class="btn btn-primary" id="btn-screener-search">
<i class="fa-solid fa-magnifying-glass"></i> Search
</button>
</div>
<div id="screener-loader" style="display:none; text-align:center; padding: 40px 0;">
<i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: var(--accent-purple);"></i>
<div style="margin-top: 12px; font-size:13px; color: var(--text-secondary);">Fetching data from Screener.in...</div>
</div>
<!-- Search Result container -->
<div id="screener-results" class="screener-results" style="display: none;">
<h4 class="title-font" id="screener-company-name" style="font-size: 20px; font-weight:700;">COMPANY NAME</h4>
<!-- Ratios grid -->
<div class="screener-sec-title">Key Ratios</div>
<div class="quote-grid" id="screener-ratios-grid"></div>
<!-- Pros & Cons -->
<div class="pro-con-grid">
<div class="pro-list">
<h4><i class="fa-solid fa-thumbs-up"></i> Strengths (Pros)</h4>
<ul class="bullet-list" id="screener-pros"></ul>
</div>
<div class="con-list">
<h4><i class="fa-solid fa-thumbs-down"></i> Weaknesses (Cons)</h4>
<ul class="bullet-list" id="screener-cons"></ul>
</div>
</div>
<!-- Growth metrics table -->
<div class="screener-sec-title">Compound Growth Metrics</div>
<div class="table-responsive">
<table>
<thead>
<tr>
<th>Metric</th>
<th>Period</th>
<th>Growth % / Value</th>
</tr>
</thead>
<tbody id="screener-growth-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Settings Modal -->
<div class="modal-overlay" id="settings-modal">
<div class="glass-panel modal">
<div class="modal-header">
<h3 class="modal-title title-font"><i class="fa-solid fa-link"></i> API Endpoint Config</h3>
<button class="modal-close" id="settings-close-btn">&times;</button>
</div>
<div class="form-group">
<label for="backend-url-input">Backend Base URL</label>
<input type="text" id="backend-url-input" class="form-input" placeholder="e.g. https://domain.hf.space">
<small style="display:block; font-size:11px; color:var(--text-secondary); margin-top:6px;">
Leave blank to automatically connect to this webserver's origin.
</small>
</div>
<button class="btn btn-primary" id="btn-save-settings" style="width: 100%;">
<i class="fa-solid fa-save"></i> Save Configuration
</button>
</div>
</div>
<!-- Toast container -->
<div class="toast-container" id="toast-container"></div>
<script>
// Global variables for Client State
let apiBaseUrl = localStorage.getItem('forecast_api_url') || '';
let dashboardData = null;
let chartInstance = null;
// Elements
const connDot = document.getElementById('conn-dot');
const connText = document.getElementById('conn-text');
const serverTimeEl = document.getElementById('server-time');
const tradingDayEl = document.getElementById('trading-day-text');
const settingsModal = document.getElementById('settings-modal');
const backendUrlInput = document.getElementById('backend-url-input');
// Toast notifications logic
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let iconClass = 'fa-circle-info';
if (type === 'success') iconClass = 'fa-circle-check';
if (type === 'error') iconClass = 'fa-circle-exclamation';
toast.innerHTML = `
<i class="fa-solid ${iconClass}"></i>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) reverse forwards';
setTimeout(() => toast.remove(), 300);
}, 4000);
}
// Active URL selector
function getApiUrl(endpoint) {
const base = apiBaseUrl.trim() || window.location.origin;
return `${base.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`;
}
// Settings Modal management
document.getElementById('settings-btn').addEventListener('click', () => {
backendUrlInput.value = apiBaseUrl;
settingsModal.classList.add('active');
});
document.getElementById('settings-close-btn').addEventListener('click', () => {
settingsModal.classList.remove('active');
});
document.getElementById('btn-save-settings').addEventListener('click', () => {
const val = backendUrlInput.value.trim();
apiBaseUrl = val;
localStorage.setItem('forecast_api_url', val);
settingsModal.classList.remove('active');
showToast('API URL configuration updated!', 'success');
fetchDashboard();
});
// Tab Navigation management
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const contentId = btn.getAttribute('data-tab');
document.getElementById(contentId).classList.add('active');
// Render chart when switching to analytics
if (contentId === 'analytics' && dashboardData) {
setTimeout(() => renderChart(dashboardData.charts.daily_closes), 100);
}
});
});
// Parse Float helper
function formatVal(val, decimals = 2, suffix = '') {
if (val === null || val === undefined || isNaN(val)) return '--';
return parseFloat(val).toFixed(decimals) + suffix;
}
// Main Dashboard API fetcher
async function fetchDashboard() {
try {
connDot.className = 'status-dot pending';
connText.textContent = 'Syncing...';
const response = await fetch(getApiUrl('dashboard'));
if (!response.ok) throw new Error(`HTTP Error status: ${response.status}`);
const data = await response.json();
dashboardData = data;
renderDashboard(data);
connDot.className = 'status-dot active';
connText.textContent = 'Connected';
} catch (err) {
console.error(err);
connDot.className = 'status-dot inactive';
connText.textContent = 'Disconnected';
showToast('Failed to fetch dashboard data. Verify your API URL settings.', 'error');
}
}
// UI Renderer
function renderDashboard(data) {
// Header stats
const status = data.data_status || {};
const dateStr = status.server_time_ist ? new Date(status.server_time_ist).toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST' : '--:--:-- IST';
serverTimeEl.textContent = dateStr;
tradingDayEl.textContent = `Trading Day: ${status.is_trading_day ? 'Yes' : 'No'}`;
// 1. T+5 Card
const t5 = data.predictions.t5 || {};
const cardT5 = document.getElementById('card-t5');
const sigT5 = document.getElementById('t5-signal');
cardT5.className = 'glass-panel predict-card ' + (t5.available ? (t5.prediction === 'UP' ? 'up' : 'down') : 'pending');
document.getElementById('t5-badge').textContent = t5.status || 'Pending';
if (t5.available) {
sigT5.innerHTML = t5.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
document.getElementById('t5-conf').textContent = formatVal(t5.confidence * 100, 1, '%');
document.getElementById('t5-prob').textContent = formatVal(t5.prob_up * 100, 1, '%');
document.getElementById('t5-model').textContent = t5.model_name || '--';
} else {
sigT5.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
document.getElementById('t5-conf').textContent = '--%';
document.getElementById('t5-prob').textContent = '--%';
document.getElementById('t5-model').textContent = '--';
}
// 2. Tomorrow Card
const tom = data.predictions.tomorrow || {};
const cardTom = document.getElementById('card-tomorrow');
const sigTom = document.getElementById('tomorrow-signal');
cardTom.className = 'glass-panel predict-card ' + (tom.available ? (tom.prediction === 'UP' ? 'up' : 'down') : 'pending');
document.getElementById('tomorrow-badge').textContent = tom.status || 'Pending';
if (tom.available) {
sigTom.innerHTML = tom.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
document.getElementById('tomorrow-target-date').textContent = tom.target_date || '--';
document.getElementById('tomorrow-prob').textContent = formatVal(tom.prob_up * 100, 1, '%');
document.getElementById('tomorrow-model').textContent = tom.source_model || tom.model_name || '--';
} else {
sigTom.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
document.getElementById('tomorrow-target-date').textContent = '--';
document.getElementById('tomorrow-prob').textContent = '--%';
document.getElementById('tomorrow-model').textContent = '--';
}
// 3. T+1 Card
const tplus = data.predictions.tplus1 || {};
const cardTplus1 = document.getElementById('card-tplus1');
const sigTplus1 = document.getElementById('tplus1-signal');
cardTplus1.className = 'glass-panel predict-card ' + (tplus.available ? (tplus.prediction === 'UP' ? 'up' : 'down') : 'pending');
document.getElementById('tplus1-badge').textContent = tplus.status || 'Pending';
if (tplus.available) {
sigTplus1.innerHTML = tplus.prediction === 'UP' ? '<i class="fa-solid fa-circle-chevron-up"></i> UP' : '<i class="fa-solid fa-circle-chevron-down"></i> DOWN';
document.getElementById('tplus1-conf').textContent = formatVal(tplus.confidence * 100, 1, '%');
document.getElementById('tplus1-prob').textContent = formatVal(tplus.prob_up * 100, 1, '%');
document.getElementById('tplus1-model').textContent = tplus.model_name || '--';
} else {
sigTplus1.innerHTML = '<i class="fa-solid fa-hourglass-half"></i> PENDING';
document.getElementById('tplus1-conf').textContent = '--%';
document.getElementById('tplus1-prob').textContent = '--%';
document.getElementById('tplus1-model').textContent = '--';
}
// 4. MFE Regression Card
const mfe = data.predictions.mfe || {};
const mfeLatest = mfe.latest || {};
const mfeSummary = mfe.summary || {};
document.getElementById('mfe-date-badge').textContent = `Session: ${mfeLatest.input_date || '--'}`;
if (mfe.available && mfeLatest.predicted_up_points !== undefined) {
document.getElementById('mfe-high-val').textContent = `+${formatVal(mfeLatest.predicted_up_points, 1)} pts`;
document.getElementById('mfe-low-val').textContent = `-${formatVal(mfeLatest.predicted_down_points, 1)} pts`;
const f5Close = parseFloat(mfeLatest.first5_close);
document.getElementById('mfe-high-level').textContent = formatVal(f5Close + parseFloat(mfeLatest.predicted_up_points), 1);
document.getElementById('mfe-low-level').textContent = formatVal(f5Close - parseFloat(mfeLatest.predicted_down_points), 1);
// History metrics mapping
const currentHistoryRow = (mfe.history || []).find(r => r.date === mfeLatest.input_date);
if (currentHistoryRow) {
document.getElementById('mfe-high-actual').textContent = formatVal(currentHistoryRow.actual_high, 1);
document.getElementById('mfe-low-actual').textContent = formatVal(currentHistoryRow.actual_low, 1);
} else {
document.getElementById('mfe-high-actual').textContent = '--';
document.getElementById('mfe-low-actual').textContent = '--';
}
} else {
document.getElementById('mfe-high-val').textContent = '+-- pts';
document.getElementById('mfe-low-val').textContent = '--- pts';
document.getElementById('mfe-high-level').textContent = '--';
document.getElementById('mfe-low-level').textContent = '--';
document.getElementById('mfe-high-actual').textContent = '--';
document.getElementById('mfe-low-actual').textContent = '--';
}
// 5. Nifty Spot Live Quote
const quote = data.nifty_quote || {};
const quoteErr = data.nifty_quote_error;
if (quoteErr) {
document.getElementById('quote-price').textContent = 'UNAVAILABLE';
document.getElementById('quote-price').style.fontSize = '24px';
document.getElementById('quote-change').innerHTML = `<i class="fa-solid fa-triangle-exclamation"></i> ${quoteErr.message}`;
document.getElementById('quote-change').className = 'quote-change down';
document.getElementById('quote-source').textContent = 'Error';
document.getElementById('quote-open').textContent = '--';
document.getElementById('quote-prev').textContent = '--';
document.getElementById('quote-high').textContent = '--';
document.getElementById('quote-low').textContent = '--';
document.getElementById('quote-time').textContent = 'Quote refresh failed.';
} else if (quote.last_traded_price !== undefined) {
document.getElementById('quote-price').textContent = formatVal(quote.last_traded_price, 2);
document.getElementById('quote-price').style.fontSize = '36px';
document.getElementById('quote-source').textContent = quote.exchange_segment || 'Index';
const changeVal = parseFloat(quote.change);
const isUp = changeVal >= 0;
document.getElementById('quote-change').innerHTML = `
<i class="fa-solid ${isUp ? 'fa-caret-up' : 'fa-caret-down'}"></i>
${isUp ? '+' : ''}${formatVal(changeVal, 2)} (${isUp ? '+' : ''}${formatVal(quote.change_pct, 2)}%)
`;
document.getElementById('quote-change').className = 'quote-change ' + (isUp ? 'up' : 'down');
document.getElementById('quote-open').textContent = formatVal(quote.open, 2);
document.getElementById('quote-prev').textContent = formatVal(quote.close, 2);
document.getElementById('quote-high').textContent = formatVal(quote.high, 2);
document.getElementById('quote-low').textContent = formatVal(quote.low, 2);
const timeStr = quote.as_of ? new Date(quote.as_of).toLocaleTimeString('en-US') : '--:--';
document.getElementById('quote-time').textContent = `As of: ${timeStr}`;
}
// 6. Action Hub - Stale details mapping
const stale = data.data_status || {};
const formatStaleBadge = (elId, isStale) => {
const el = document.getElementById(elId);
el.textContent = isStale ? 'Stale' : 'Fresh';
el.className = `badge-pill ${isStale ? 'danger' : 'success'}`;
};
formatStaleBadge('stale-daily', stale.daily_stale);
formatStaleBadge('stale-minutes', stale.minutes_stale);
formatStaleBadge('stale-t5', stale.t5_stale);
formatStaleBadge('stale-tplus1', stale.tplus1_stale);
// 7. Accuracy tab gauges update
const liveAcc = data.live_accuracy || {};
// T+5 Gauge
const t5Acc = liveAcc.t5 || {};
renderGauge('gauge-t5', t5Acc.accuracy, t5Acc.total, t5Acc.correct_count, t5.validation_accuracy);
// Tomorrow Gauge
const tomAcc = liveAcc.tomorrow || {};
renderGauge('gauge-tomorrow', tomAcc.accuracy, tomAcc.total, tomAcc.correct_count, tom.validation_accuracy);
// T+1 Gauge
const tplusAcc = liveAcc.tplus1 || {};
renderGauge('gauge-tplus1', tplusAcc.accuracy, tplusAcc.total, tplusAcc.correct_count, tplus.validation_accuracy);
// 8. Tomorrow Live Track Record Table
const trackRecord = data.charts.tomorrow_live_track_record || [];
const trTbody = document.getElementById('tomorrow-track-record-tbody');
if (trackRecord.length > 0) {
trTbody.innerHTML = '';
trackRecord.forEach(row => {
const moveVal = parseFloat(row.actual_move) * 100;
const moveClass = moveVal >= 0 ? 'success' : 'danger';
const correctClass = row.correct ? 'success' : 'danger';
trTbody.innerHTML += `
<tr>
<td><strong>${row.date}</strong></td>
<td>${row.input_date}</td>
<td><span class="badge-pill ${row.prediction === 'UP' ? 'success' : 'danger'}">${row.prediction}</span></td>
<td>${formatVal(row.prob_up * 100, 1, '%')}</td>
<td><span class="badge-pill ${row.actual_direction === 'UP' ? 'success' : 'danger'}">${row.actual_direction}</span></td>
<td><span class="badge-pill ${moveClass}">${moveVal >= 0 ? '+' : ''}${formatVal(moveVal, 2)}%</span></td>
<td><span class="badge-pill ${correctClass}">${row.correct ? 'Correct' : 'Incorrect'}</span></td>
<td><span style="color:var(--text-secondary);">${row.prediction_source || 'Rolling'}</span></td>
</tr>
`;
});
} else {
trTbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: var(--text-secondary);">No historical records found.</td></tr>`;
}
// 9. Kotak credentials/snapshots updates
const kotakStatus = data.predictions.t5.kotak_status || data.kotak_status || {};
const kotakBadge = document.getElementById('kotak-status-badge');
if (kotakStatus.authenticated) {
kotakBadge.textContent = 'Authenticated';
kotakBadge.className = 'badge-pill success';
document.getElementById('kotak-auth-container').style.display = 'none';
document.getElementById('kotak-snapshot-container').style.display = 'block';
// Display Limits & balances
const limits = data.limits_summary || {};
document.getElementById('cash-net').textContent = formatVal(limits.net, 0);
document.getElementById('cash-margin').textContent = formatVal(limits.margin_used, 0);
let cashAvail = '--';
if (limits.net !== null && limits.margin_used !== null) {
cashAvail = limits.net - limits.margin_used;
}
document.getElementById('cash-avail').textContent = formatVal(cashAvail, 0);
const livePnl = (data.summary || {}).live_pnl;
const pnlEl = document.getElementById('cash-pnl');
pnlEl.textContent = formatVal(livePnl, 0);
pnlEl.style.color = livePnl >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)';
// Holdings table mapping
const holdings = data.holdings || [];
const holdingsTbody = document.getElementById('kotak-holdings-tbody');
if (holdings.length > 0) {
holdingsTbody.innerHTML = '';
holdings.forEach(row => {
const pnlVal = parseFloat(row.pnl);
holdingsTbody.innerHTML += `
<tr>
<td><strong>${row.trading_symbol || row.symbol}</strong></td>
<td>${row.quantity}</td>
<td>${formatVal(row.average_price, 1)}</td>
<td>${formatVal(row.market_value, 0)}</td>
<td style="color: ${pnlVal >= 0 ? 'var(--accent-emerald)' : 'var(--accent-rose)'}; font-weight:700;">
${pnlVal >= 0 ? '+' : ''}${formatVal(pnlVal, 0)}
</td>
</tr>
`;
});
} else {
holdingsTbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">No stock holdings.</td></tr>`;
}
} else {
kotakBadge.textContent = 'Unauthorized';
kotakBadge.className = 'badge-pill danger';
document.getElementById('kotak-auth-container').style.display = 'block';
document.getElementById('kotak-snapshot-container').style.display = 'none';
}
}
// Render Gauge Helper
function renderGauge(id, accuracy, total, correctCount, validationAcc) {
const pct = accuracy !== null && accuracy !== undefined ? (accuracy * 100) : null;
document.getElementById(`${id}-pct`).textContent = pct !== null ? `${pct.toFixed(0)}%` : '--%';
document.getElementById(`${id}-count`).textContent = `${total || 0} Sessions`;
document.getElementById(`${id}-correct`).textContent = `${correctCount || 0} / ${total || 0}`;
document.getElementById(`${id}-val`).textContent = formatVal(validationAcc * 100, 1, '%');
const strokeOffset = pct !== null ? (314.16 - (314.16 * pct) / 100) : 314.16;
document.getElementById(`${id}-fill`).style.strokeDashoffset = strokeOffset;
}
// Render History Close line chart with Chart.js
function renderChart(closes) {
if (!closes || closes.length === 0) return;
const ctx = document.getElementById('closesChart').getContext('2d');
const labels = closes.map(r => r.date.split(' ')[0]);
const prices = closes.map(r => r.close);
if (chartInstance) {
chartInstance.destroy();
}
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'NIFTY 50 Close',
data: prices,
borderColor: '#a855f7',
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 6,
tension: 0.1,
fill: true,
backgroundColor: (context) => {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) return null;
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, 'rgba(168, 85, 247, 0.25)');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.00)');
return gradient;
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(7, 9, 19, 0.9)',
titleFont: { family: 'Outfit' },
bodyFont: { family: 'Inter' },
borderColor: 'rgba(255, 255, 255, 0.1)',
borderWidth: 1
}
},
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.03)' },
ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } }
},
y: {
grid: { color: 'rgba(255, 255, 255, 0.03)' },
ticks: { color: '#9ca3af', font: { family: 'Inter', size: 10 } }
}
}
}
});
}
// Operation triggers logic helper
async function runTrigger(btnId, endpoint, method = 'POST', successMsg) {
const btn = document.getElementById(btnId);
const originalHtml = btn.innerHTML;
try {
btn.disabled = true;
btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Processing...`;
const response = await fetch(getApiUrl(endpoint), { method: method });
if (!response.ok) throw new Error(`HTTP Error Status: ${response.status}`);
showToast(successMsg, 'success');
fetchDashboard();
} catch (err) {
console.error(err);
showToast(`Operation failed: ${err.message}`, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalHtml;
}
}
// Action Hub triggers
document.getElementById('btn-keepalive').addEventListener('click', () => {
runTrigger('btn-keepalive', 'cron/keepalive', 'GET', 'Keepalive keep-warm routine triggered!');
});
document.getElementById('btn-refresh-first5').addEventListener('click', () => {
runTrigger('btn-refresh-first5', 'prediction/refresh-first5', 'POST', 'First 5-minutes prediction refreshed!');
});
document.getElementById('btn-refresh-daily').addEventListener('click', () => {
runTrigger('btn-refresh-daily', 'data/refresh-daily', 'POST', 'Daily parquet data refreshed!');
});
document.getElementById('btn-refresh-close').addEventListener('click', () => {
runTrigger('btn-refresh-close', 'data/refresh-market-close', 'POST', 'Market close refresh sequence executed!');
});
// Kotak Securities login action
document.getElementById('btn-kotak-login').addEventListener('click', async () => {
const totpInput = document.getElementById('totp-input');
const totpVal = totpInput.value.trim();
if (!totpVal || totpVal.length < 6) {
showToast('A valid 6-digit TOTP code is required.', 'error');
return;
}
const btn = document.getElementById('btn-kotak-login');
const originalHtml = btn.innerHTML;
try {
btn.disabled = true;
btn.innerHTML = `<i class="fa-solid fa-spinner spin"></i> Authenticating...`;
const response = await fetch(getApiUrl('kotak/auth/totp'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ totp: totpVal })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || `Authentication rejected: status ${response.status}`);
}
showToast('Kotak Neo session authenticated successfully!', 'success');
totpInput.value = '';
fetchDashboard();
} catch (err) {
console.error(err);
showToast(`Session auth failed: ${err.message}`, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
// Fundamental Screener search search logic
document.getElementById('btn-screener-search').addEventListener('click', async () => {
const queryInput = document.getElementById('screener-query');
const query = queryInput.value.trim().toUpperCase();
if (!query) {
showToast('Please specify a ticker symbol (e.g. INFYS, RELIANCE).', 'error');
return;
}
const resultsContainer = document.getElementById('screener-results');
const loader = document.getElementById('screener-loader');
const btn = document.getElementById('btn-screener-search');
try {
btn.disabled = true;
loader.style.display = 'block';
resultsContainer.style.display = 'none';
const response = await fetch(getApiUrl(`info/${query}`));
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || `Server returned error status ${response.status}`);
}
const data = await response.json();
renderScreenerResult(data);
resultsContainer.style.display = 'block';
} catch (err) {
console.error(err);
showToast(`Screener search failed: ${err.message}`, 'error');
} finally {
btn.disabled = false;
loader.style.display = 'none';
}
});
// Search renderer helper
function renderScreenerResult(data) {
document.getElementById('screener-company-name').textContent = data.ticker || 'STOCK ANALYSIS';
// Ratios
const ratiosGrid = document.getElementById('screener-ratios-grid');
ratiosGrid.innerHTML = '';
const metrics = data.key_metrics || {};
for (const [name, val] of Object.entries(metrics)) {
ratiosGrid.innerHTML += `
<div class="quote-item">
<div style="color: var(--text-secondary); font-size: 11px;">${name}</div>
<div style="font-weight: 700; margin-top: 4px; font-size: 14px;">${val}</div>
</div>
`;
}
// Pros / Cons
const prosUl = document.getElementById('screener-pros');
const consUl = document.getElementById('screener-cons');
prosUl.innerHTML = '';
consUl.innerHTML = '';
const pros = data.pros || [];
if (pros.length > 0) {
pros.forEach(item => prosUl.innerHTML += `<li>${item}</li>`);
} else {
prosUl.innerHTML = `<li>No data listed.</li>`;
}
const cons = data.cons || [];
if (cons.length > 0) {
cons.forEach(item => consUl.innerHTML += `<li>${item}</li>`);
} else {
consUl.innerHTML = `<li>No data listed.</li>`;
}
// Growth table
const growthTbody = document.getElementById('screener-growth-tbody');
const growth = data.growth || [];
if (growth.length > 0) {
growthTbody.innerHTML = '';
growth.forEach(row => {
growthTbody.innerHTML += `
<tr>
<td><strong>${row.Metric}</strong></td>
<td>${row.Period}</td>
<td><span class="badge-pill secondary" style="font-size:12px; font-weight:700;">${row.Value}</span></td>
</tr>
`;
});
} else {
growthTbody.innerHTML = `<tr><td colspan="3" style="text-align: center; color: var(--text-secondary);">No growth metrics available.</td></tr>`;
}
}
// Initialize fetching loop and setup timer ticks
fetchDashboard();
setInterval(fetchDashboard, 60000); // dashboard reload once every minute
// Incremental clock tick loop
setInterval(() => {
if (dashboardData && dashboardData.data_status) {
const now = new Date();
serverTimeEl.textContent = now.toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST';
}
}, 1000);
</script>
</body>
</html>