| <!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> |
| |
| <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"> |
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| |
| <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-purple: #a855f7; |
| --accent-blue: #3b82f6; |
| --accent-emerald: #10b981; |
| --accent-rose: #f43f5e; |
| --accent-amber: #f59e0b; |
| |
| |
| --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; |
| } |
| |
| |
| *::-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; |
| } |
| |
| |
| .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 { |
| 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-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 { |
| 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); |
| } |
| |
| |
| .tab-content { |
| display: none; |
| animation: fadeIn 0.4s ease; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| |
| .dashboard-grid { |
| display: grid; |
| grid-template-columns: 2fr 1fr; |
| gap: 24px; |
| margin-bottom: 24px; |
| } |
| |
| @media (max-width: 1024px) { |
| .dashboard-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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-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 { |
| 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-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 { |
| 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; |
| } |
| |
| |
| .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-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 24px; |
| } |
| |
| @media (max-width: 1024px) { |
| .tools-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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-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 { |
| 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-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); } |
| |
| |
| @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"> |
| |
| <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> |
|
|
| |
| <div id="overview" class="tab-content active"> |
| <div class="dashboard-grid"> |
| |
| <div> |
| |
| <div class="predictions-container"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="analytics" class="tab-content"> |
| |
| <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> |
|
|
| |
| <div class="accuracy-grid"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="tools" class="tab-content"> |
| <div class="tools-grid"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| |
| <div class="screener-sec-title">Key Ratios</div> |
| <div class="quote-grid" id="screener-ratios-grid"></div> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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">×</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> |
|
|
| |
| <div class="toast-container" id="toast-container"></div> |
|
|
| <script> |
| |
| let apiBaseUrl = localStorage.getItem('forecast_api_url') || ''; |
| let dashboardData = null; |
| let chartInstance = null; |
| |
| |
| 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'); |
| |
| |
| 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); |
| } |
| |
| |
| function getApiUrl(endpoint) { |
| const base = apiBaseUrl.trim() || window.location.origin; |
| return `${base.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`; |
| } |
| |
| |
| 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(); |
| }); |
| |
| |
| 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'); |
| |
| |
| if (contentId === 'analytics' && dashboardData) { |
| setTimeout(() => renderChart(dashboardData.charts.daily_closes), 100); |
| } |
| }); |
| }); |
| |
| |
| function formatVal(val, decimals = 2, suffix = '') { |
| if (val === null || val === undefined || isNaN(val)) return '--'; |
| return parseFloat(val).toFixed(decimals) + suffix; |
| } |
| |
| |
| 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'); |
| } |
| } |
| |
| |
| function renderDashboard(data) { |
| |
| 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'}`; |
| |
| |
| 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 = '--'; |
| } |
| |
| |
| 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 = '--'; |
| } |
| |
| |
| 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 = '--'; |
| } |
| |
| |
| 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); |
| |
| |
| 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 = '--'; |
| } |
| |
| |
| 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}`; |
| } |
| |
| |
| 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); |
| |
| |
| const liveAcc = data.live_accuracy || {}; |
| |
| |
| const t5Acc = liveAcc.t5 || {}; |
| renderGauge('gauge-t5', t5Acc.accuracy, t5Acc.total, t5Acc.correct_count, t5.validation_accuracy); |
| |
| |
| const tomAcc = liveAcc.tomorrow || {}; |
| renderGauge('gauge-tomorrow', tomAcc.accuracy, tomAcc.total, tomAcc.correct_count, tom.validation_accuracy); |
| |
| |
| const tplusAcc = liveAcc.tplus1 || {}; |
| renderGauge('gauge-tplus1', tplusAcc.accuracy, tplusAcc.total, tplusAcc.correct_count, tplus.validation_accuracy); |
| |
| |
| 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>`; |
| } |
| |
| |
| 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'; |
| |
| |
| 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)'; |
| |
| |
| 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'; |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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 } } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| 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; |
| } |
| } |
| |
| |
| 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!'); |
| }); |
| |
| |
| 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; |
| } |
| }); |
| |
| |
| 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'; |
| } |
| }); |
| |
| |
| function renderScreenerResult(data) { |
| document.getElementById('screener-company-name').textContent = data.ticker || 'STOCK ANALYSIS'; |
| |
| |
| 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> |
| `; |
| } |
| |
| |
| 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>`; |
| } |
| |
| |
| 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>`; |
| } |
| } |
| |
| |
| fetchDashboard(); |
| setInterval(fetchDashboard, 60000); |
| |
| |
| setInterval(() => { |
| if (dashboardData && dashboardData.data_status) { |
| const now = new Date(); |
| serverTimeEl.textContent = now.toLocaleTimeString('en-US', {timeZone: 'Asia/Kolkata'}) + ' IST'; |
| } |
| }, 1000); |
| </script> |
| </body> |
| </html> |
|
|