topsecrettraders commited on
Commit
7ac5d89
·
verified ·
1 Parent(s): e663207

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +724 -848
app.py CHANGED
@@ -48,8 +48,14 @@ def home():
48
 
49
  @app.get("/api/dates")
50
  def get_dates():
51
- """Fetch distinct dates."""
52
- data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "50000"})
 
 
 
 
 
 
53
  dates = set()
54
  for row in data:
55
  if row.get('minute'):
@@ -158,6 +164,7 @@ def fetch_series(
158
  inst_data = row['data'].get(instrument)
159
  if not inst_data: continue
160
 
 
161
  p_arr = inst_data.get('p', []) or []
162
  b_arr = inst_data.get('b', []) or []
163
  s_arr = inst_data.get('s', []) or []
@@ -204,7 +211,7 @@ def fetch_series(
204
 
205
 
206
  # ==========================================
207
- # 5. FRONTEND TEMPLATE (NEW UI)
208
  # ==========================================
209
 
210
  HTML_TEMPLATE = """
@@ -214,335 +221,264 @@ HTML_TEMPLATE = """
214
  <meta charset="UTF-8">
215
  <title>DepthChain PRO</title>
216
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
 
217
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
218
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
219
  <style>
220
- /* --- THEME CONFIGURATION --- */
221
  :root {
222
- --bg-app: #09090b; /* Zinc 950 */
223
- --bg-header: #121214; /* Slightly lighter than body */
224
- --bg-card: #0d1117;
225
- --bg-input: #1c1c1f;
226
- --border: #27272a;
227
- --border-hover: #3f3f46;
228
 
229
- --text-primary: #e4e4e7;
230
- --text-secondary: #a1a1aa;
231
- --text-muted: #71717a;
232
 
233
- --accent: #3b82f6; /* Blue 500 */
234
- --accent-glow: rgba(59, 130, 246, 0.2);
235
- --success: #22c55e;
236
- --danger: #ef4444;
237
- --warning: #eab308;
238
 
239
- --font-sans: 'Inter', sans-serif;
240
  --font-mono: 'JetBrains Mono', monospace;
 
 
 
241
  }
242
 
243
- /* --- RESET & BASE --- */
244
- * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
 
245
  ::-webkit-scrollbar { width: 6px; height: 6px; }
246
  ::-webkit-scrollbar-track { background: transparent; }
247
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
248
- ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
249
-
250
- body {
251
- background-color: var(--bg-app);
252
- color: var(--text-primary);
253
- font-family: var(--font-sans);
254
- height: 100vh;
255
- display: flex;
256
- flex-direction: column;
257
- overflow: hidden;
258
- font-size: 13px;
259
  }
260
 
261
- /* --- TOP BAR --- */
262
- .top-bar {
263
- height: 48px;
264
- background: var(--bg-header);
265
  border-bottom: 1px solid var(--border);
266
- display: flex;
267
- align-items: center;
268
- justify-content: space-between;
269
- padding: 0 16px;
270
- flex-shrink: 0;
271
- user-select: none;
272
  }
273
 
274
- .brand {
275
- font-weight: 600; font-size: 15px; letter-spacing: -0.02em;
276
- display:flex; align-items:center; gap:8px;
277
- color: var(--text-primary);
278
- }
279
- .brand span { color: var(--accent); }
280
-
281
- .controls-area { display: flex; align-items: center; gap: 12px; }
282
- .separator { width: 1px; height: 20px; background: var(--border); }
283
-
284
- /* Custom Select inputs */
285
- .custom-select-wrapper { position: relative; display: inline-block; }
286
- .custom-select {
287
- appearance: none; -webkit-appearance: none;
288
- background: var(--bg-input);
289
- border: 1px solid var(--border);
290
- color: var(--text-primary);
291
- padding: 6px 30px 6px 10px;
292
- border-radius: 6px;
293
- font-family: var(--font-sans);
294
- font-size: 12px;
295
- cursor: pointer;
296
- transition: all 0.15s ease;
297
- min-width: 130px;
298
- }
299
- .custom-select:hover { border-color: var(--text-secondary); }
300
- .custom-select-arrow {
301
- position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
302
- pointer-events: none; color: var(--text-secondary); font-size: 10px;
303
- }
304
 
305
- /* Mode Switcher (Toggle) */
306
- .mode-toggle {
307
- display: flex; background: var(--bg-input);
308
- border: 1px solid var(--border); border-radius: 6px;
309
- overflow: hidden; padding: 2px;
310
- }
311
- .mode-btn {
312
- padding: 4px 10px; font-size: 11px; font-weight: 600; cursor: pointer;
313
- border-radius: 4px; transition: 0.2s; color: var(--text-secondary);
314
  }
315
- .mode-btn.active {
316
- background: var(--border-hover); color: var(--text-primary);
 
 
 
 
 
317
  }
318
- .mode-btn.active.live { color: var(--success); }
319
-
320
- /* Layout Buttons */
321
- .layout-group { display: flex; background: var(--bg-input); border-radius: 6px; padding: 2px; border: 1px solid var(--border); }
322
- .layout-btn {
323
- width: 28px; height: 24px; display: flex; align-items: center; justify-content: center;
324
- font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary);
325
- cursor: pointer; border-radius: 4px; transition: 0.15s; font-weight:600;
326
  }
327
- .layout-btn:hover { color: var(--text-primary); }
328
- .layout-btn.active { background: var(--accent); color: white; }
329
-
330
- /* Action Buttons */
331
- .btn {
332
- background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
333
- padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer;
334
- transition: 0.15s;
 
 
 
 
335
  }
336
- .btn:hover { border-color: var(--text-secondary); background: var(--border); }
337
- .btn.primary { background: var(--text-primary); color: var(--bg-app); border:none; font-weight: 600; }
338
- .btn.primary:hover { opacity: 0.9; }
339
- .btn.danger { color: var(--danger); border-color: var(--border); }
340
- .btn.danger:hover { background: rgba(239, 68, 68, 0.1); border-color: var(--danger); }
341
-
342
- /* --- MAIN GRID --- */
343
- .app-container {
344
- flex: 1; display: flex; width: 100%; height: calc(100vh - 48px);
345
- overflow: hidden; /* Main app doesn't scroll, columns do */
346
  }
347
 
348
  .column {
349
- flex: 1; display: flex; flex-direction: column;
 
350
  border-right: 1px solid var(--border);
351
- min-width: 300px;
352
- overflow-y: auto; /* Independent scrolling */
353
  overflow-x: hidden;
354
- position: relative;
355
  }
356
  .column:last-child { border-right: none; }
357
 
358
- .col-footer {
359
- padding: 10px; border-top: 1px solid var(--border);
360
- background: var(--bg-header); text-align: center;
361
- margin-top: auto; /* Push to bottom if content small */
362
- }
363
- .add-chart-btn {
364
- background: transparent; border: 1px dashed var(--border); color: var(--text-muted);
365
- width: 100%; padding: 8px; border-radius: 6px; font-size: 11px; cursor: pointer;
366
- transition: 0.2s;
367
- }
368
- .add-chart-btn:hover { border-color: var(--text-secondary); color: var(--text-primary); }
369
-
370
  /* --- CHART CARD --- */
371
  .chart-card {
372
- display: flex; flex-direction: column;
373
- border-bottom: 1px solid var(--border);
374
  background: var(--bg-card);
 
 
375
  position: relative;
376
- /* Height is dynamic */
377
  }
378
 
379
- .card-header {
380
- height: 32px; display: flex; align-items: center; justify-content: space-between;
381
- padding: 0 10px; border-bottom: 1px solid var(--border);
382
- background: var(--bg-header); user-select: none;
383
  }
384
-
385
- .card-info { display: flex; align-items: center; gap: 8px; font-size: 11px; }
386
- .status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); transition: 0.3s; }
387
- .status-dot.live { background: var(--success); box-shadow: 0 0 6px rgba(34, 197, 94, 0.4); animation: pulse 2s infinite; }
388
- .status-dot.hist { background: var(--accent); }
389
- .status-dot.retry { background: var(--warning); animation: blink 0.5s infinite; }
390
-
391
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
392
- @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
393
-
394
- .symbol-text { font-weight: 600; color: var(--text-primary); }
395
- .meta-text { color: var(--text-secondary); font-family: var(--font-mono); font-size: 10px; }
396
-
397
- .card-tools { display: flex; gap: 4px; }
398
- .tool-btn {
399
- background: transparent; border: none; color: var(--text-muted);
400
- width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
401
- display: flex; align-items: center; justify-content: center; font-size: 14px;
402
  }
403
- .tool-btn:hover { background: var(--border); color: var(--text-primary); }
404
- .tool-btn.close:hover { color: var(--danger); background: rgba(239, 68, 68, 0.1); }
405
 
406
- .card-content { flex: 1; position: relative; overflow: hidden; }
 
407
 
408
  /* Empty State */
409
- .empty-state {
410
- height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
411
- background: linear-gradient(180deg, var(--bg-card) 0%, rgba(0,0,0,0.4) 100%);
 
412
  }
413
- .config-btn-subtle {
414
- background: transparent; border: 1px solid var(--border);
415
- color: var(--text-secondary); padding: 8px 16px; border-radius: 20px;
416
- font-size: 11px; font-weight: 500; cursor: pointer; transition: 0.2s;
417
- display: flex; align-items: center; gap: 6px;
418
  }
419
- .config-btn-subtle:hover { border-color: var(--accent); color: var(--accent); background: rgba(59,130,246,0.05); }
 
 
 
 
 
 
 
 
 
420
 
421
  /* --- MODAL --- */
422
- .overlay {
423
- position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
424
- background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
425
- z-index: 100; display: none; align-items: center; justify-content: center;
426
  }
427
  .modal {
428
- background: var(--bg-header); border: 1px solid var(--border);
429
- width: 420px; border-radius: 8px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
430
  display: flex; flex-direction: column; overflow: hidden;
431
- animation: slideUp 0.2s ease-out;
432
  }
433
- @keyframes slideUp { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
434
 
435
- .modal-header {
436
- padding: 16px; border-bottom: 1px solid var(--border);
437
- display: flex; justify-content: space-between; align-items: center; font-weight: 600;
438
- }
439
- .modal-body { padding: 16px; max-height: 70vh; overflow-y: auto; }
440
- .modal-footer {
441
- padding: 12px 16px; border-top: 1px solid var(--border);
442
- background: var(--bg-input); display: flex; justify-content: flex-end; gap: 8px;
443
- }
444
 
445
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
446
- .form-group label { display: block; font-size: 10px; color: var(--text-secondary); margin-bottom: 4px; font-weight: 600; text-transform: uppercase;}
447
- .form-control {
448
- width: 100%; background: var(--bg-app); border: 1px solid var(--border);
449
- color: var(--text-primary); padding: 8px; border-radius: 4px; font-size: 12px;
450
- }
451
- .form-control:focus { border-color: var(--accent); }
452
 
453
  /* Series List */
454
- .series-list { display: flex; flex-direction: column; gap: 6px; }
455
- .series-item { display: grid; grid-template-columns: 30px 1fr 1fr 50px 24px; gap: 6px; align-items: center; }
456
- input[type="color"] { border: none; background: none; padding: 0; height: 28px; width: 100%; cursor: pointer; }
457
 
458
  /* Toast */
459
  .toast {
460
- position: fixed; bottom: 24px; right: 24px;
461
- background: var(--bg-header); border: 1px solid var(--border);
462
- color: var(--text-primary); padding: 10px 16px; border-radius: 6px;
463
- font-size: 12px; font-weight: 500;
464
- box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3);
465
- transform: translateY(100px); transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
466
- z-index: 200; display: flex; align-items: center; gap: 8px;
467
  }
468
- .toast.show { transform: translateY(0); }
469
- .toast-icon { color: var(--success); }
470
 
471
  </style>
472
  </head>
473
  <body>
474
 
475
- <!-- HEADER -->
476
- <div class="top-bar">
477
- <div class="brand">
478
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
479
- DepthChain <span>PRO</span>
480
- </div>
481
-
482
- <div class="controls-area">
483
- <!-- Date Selector -->
484
- <div class="custom-select-wrapper">
485
- <select id="globalDate" class="custom-select" onchange="GlobalApp.onDateChange()"></select>
486
- <div class="custom-select-arrow">▼</div>
487
- </div>
488
-
489
- <div class="separator"></div>
490
 
491
- <!-- Mode Toggle -->
492
- <div class="mode-toggle">
493
- <div id="modeHist" class="mode-btn" onclick="GlobalApp.setMode('HIST')">HISTORICAL</div>
494
- <div id="modeLive" class="mode-btn" onclick="GlobalApp.setMode('LIVE')">LIVE</div>
495
- </div>
496
 
497
- <div class="separator"></div>
498
 
499
- <!-- Layout -->
500
- <div class="layout-group">
501
- <div class="layout-btn" onclick="GlobalApp.setLayout(1)" title="1 Col">1</div>
502
- <div class="layout-btn" onclick="GlobalApp.setLayout(2)" title="2 Cols">2</div>
503
- <div class="layout-btn" onclick="GlobalApp.setLayout(3)" title="3 Cols">3</div>
504
- <div class="layout-btn" onclick="GlobalApp.setLayout(4)" title="4 Cols">4</div>
505
  </div>
506
 
507
- <div class="separator"></div>
508
-
509
- <button class="btn" onclick="ConfigModal.openDefaults()">Settings</button>
510
- <button class="btn danger" onclick="GlobalApp.resetApp()">Reset</button>
511
  </div>
512
- </div>
513
 
514
- <!-- MAIN APP GRID -->
515
- <div class="app-container" id="gridContainer">
516
- <!-- Columns injected here -->
517
  </div>
518
 
519
- <!-- CONFIG MODAL -->
520
- <div class="overlay" id="configOverlay">
521
  <div class="modal">
522
- <div class="modal-header">
523
- <span id="cfgModalTitle">Configure Chart</span>
524
  <button class="tool-btn" onclick="ConfigModal.close()">✕</button>
525
  </div>
526
- <div class="modal-body">
527
  <div id="cfgForm">
528
- <div class="form-row">
529
- <div class="form-group">
530
  <label>Symbol</label>
531
- <select id="cfgRoot" class="form-control" onchange="ConfigModal.onRootChange()"></select>
532
  </div>
533
- <div class="form-group">
534
  <label>Expiry</label>
535
- <select id="cfgExp" class="form-control" onchange="ConfigModal.onExpChange()"></select>
536
  </div>
537
  </div>
538
- <div class="form-group" style="margin-bottom:12px;">
539
  <label>Instrument</label>
540
- <select id="cfgInst" class="form-control"></select>
541
  </div>
542
- <div class="form-row">
543
- <div class="form-group">
544
  <label>Timeframe</label>
545
- <select id="cfgTF" class="form-control">
546
  <option value="1min">1 Minute</option>
547
  <option value="3min">3 Minutes</option>
548
  <option value="5min">5 Minutes</option>
@@ -551,422 +487,380 @@ HTML_TEMPLATE = """
551
  <option value="1hour">1 Hour</option>
552
  </select>
553
  </div>
554
- <div class="form-group">
555
- <label>Time Range</label>
556
- <div style="display:flex; gap:6px;">
557
- <input type="time" id="cfgStart" class="form-control" value="09:15">
558
- <input type="time" id="cfgEnd" class="form-control" value="15:30">
559
  </div>
560
  </div>
561
  </div>
562
-
563
- <div style="height:1px; background:var(--border); margin:16px 0;"></div>
564
-
565
- <div class="form-group">
566
- <label>Plot Series ($P, $V, $OI, $B, $S)</label>
567
- <div id="cfgSeriesList" class="series-list"></div>
568
- <button class="add-chart-btn" style="margin-top:8px;" onclick="ConfigModal.addSeriesRow()">+ Add Plot Line</button>
569
  </div>
570
  </div>
571
  </div>
572
- <div class="modal-footer">
573
- <button class="btn" onclick="ConfigModal.close()">Cancel</button>
574
- <button class="btn primary" onclick="ConfigModal.save()">Save Configuration</button>
575
  </div>
576
  </div>
577
  </div>
578
 
579
- <div id="toast" class="toast">
580
- <span class="toast-icon">✓</span>
581
- <span id="toastMsg">Settings Saved</span>
582
  </div>
583
 
584
  <script>
585
- /** --- CORE STATE --- */
586
- const GlobalApp = {
587
- state: {
588
- date: '',
589
- mode: 'HIST',
590
- columns: 3,
591
- // layout: [ [id1, id2], [id3], [] ]
592
- layout: [[], [], []],
593
- // charts: { id: { col:0, config:{}, instance:Chart, cachedData: {} } }
594
- charts: {}
595
- },
596
- defaults: [
597
- { formula: '$P', color: '#3b82f6', label: 'Price', axis: 'left' },
598
- { formula: '$B', color: '#22c55e', label: 'Buy Qty', axis: 'right' },
599
- { formula: '$S', color: '#ef4444', label: 'Sell Qty', axis: 'right' }
600
- ],
601
-
602
- async init() {
603
- await this.loadDates();
604
- this.loadState();
605
-
606
- // Auto Live Check
607
- const today = new Date().toISOString().split('T')[0];
608
- if(this.state.date === today) {
609
- this.setMode('LIVE', false); // No auto-save yet to prevent overwrite on refresh logic
610
- } else {
611
- this.setMode(this.state.mode, false);
612
- }
613
-
614
- this.updateLayoutBtns();
615
- },
616
-
617
- async loadDates() {
618
- try {
619
- const res = await fetch('/api/dates');
620
- const dates = await res.json();
621
- const sel = document.getElementById('globalDate');
622
- sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
623
- if(dates.length) this.state.date = dates[0];
624
- } catch(e) { console.error(e); }
625
- },
626
-
627
- // --- LAYOUT LOGIC ---
628
- setLayout(cols) {
629
- if(cols === this.state.columns) return;
630
-
631
- // Gather all IDs
632
- const allCharts = this.state.layout.flat();
633
-
634
- // Rebuild Layout Array
635
- this.state.columns = cols;
636
- this.state.layout = Array.from({length: cols}, () => []);
637
-
638
- allCharts.forEach((id, i) => {
639
- const colIdx = i % cols;
640
- this.state.layout[colIdx].push(id);
641
- if(this.state.charts[id]) this.state.charts[id].col = colIdx;
642
- });
643
-
644
- this.updateLayoutBtns();
645
- this.renderGrid(); // This will re-mount DOM
646
- this.saveState();
647
- },
648
-
649
- updateLayoutBtns() {
650
- document.querySelectorAll('.layout-btn').forEach((b, i) => {
651
- b.classList.toggle('active', (i+1) === this.state.columns);
652
- });
653
- },
654
-
655
- renderGrid() {
656
- const container = document.getElementById('gridContainer');
657
- container.innerHTML = '';
658
-
659
- for(let i=0; i<this.state.columns; i++) {
660
- const colDiv = document.createElement('div');
661
- colDiv.className = 'column';
662
- colDiv.id = `col-${i}`;
663
-
664
- const chartIds = this.state.layout[i] || [];
665
 
666
- chartIds.forEach(id => {
667
- colDiv.appendChild(this.createCardDOM(id));
668
- // Post-render: Re-attach chart logic
669
- // We use setTimeout to allow DOM reflow
670
- setTimeout(() => ChartLogic.render(id), 0);
671
- });
672
-
673
- // Footer with Add Button
674
- const footer = document.createElement('div');
675
- footer.className = 'col-footer';
676
- footer.innerHTML = `<button class="add-chart-btn" onclick="GlobalApp.addChart(${i})">+ ADD CHART</button>`;
677
- colDiv.appendChild(footer);
678
 
679
- container.appendChild(colDiv);
 
 
 
 
 
 
 
 
 
 
 
680
  }
681
- this.adjustHeights();
682
- },
683
-
684
- adjustHeights() {
685
- // Logic:
686
- // Max 4 visible charts per column.
687
- // If <= 4, split available height equally (min 25%).
688
- // If > 4, stick to 25% height and allow column scroll.
689
-
690
- // Available height = 100vh - 48px (header)
691
- // But we are inside .column which handles scroll.
692
- // We set height via style on the card.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
 
694
- for(let i=0; i<this.state.columns; i++) {
695
- const ids = this.state.layout[i];
696
- const count = ids.length;
697
- if(count === 0) continue;
 
 
698
 
699
- let heightPct = 100 / count;
700
- if(count > 4) heightPct = 25;
701
-
702
- // If only 1 chart, it takes full height minus footer approx
703
- // We use flex grow logic via CSS usually, but let's enforce min-heights
704
-
705
- ids.forEach(id => {
706
- const card = document.getElementById(`card-${id}`);
707
- if(card) {
708
- // Enforce a minimum pixel height to look good
709
- card.style.height = `max(${heightPct}%, 250px)`;
710
- // If it's the only one, make it fill the space
711
- if(count === 1) card.style.height = 'calc(100vh - 100px)'; // rough calc
712
- else if(count <= 4) card.style.height = `calc((100vh - 90px) / ${count})`;
713
- }
714
- });
715
- }
716
- },
717
 
718
- // --- CHART MANAGEMENT ---
719
- addChart(colIdx) {
720
- const id = 'c_' + Date.now() + Math.random().toString(36).substr(2,4);
721
- this.state.charts[id] = { col: colIdx, config: {}, instance: null, cachedData: null };
722
-
723
- if(!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
724
- this.state.layout[colIdx].push(id);
725
-
726
- // Insert into DOM immediately to avoid full re-render
727
- const colDiv = document.getElementById(`col-${colIdx}`);
728
- const footer = colDiv.querySelector('.col-footer');
729
- colDiv.insertBefore(this.createCardDOM(id), footer);
730
-
731
- this.adjustHeights();
732
- this.saveState();
733
- },
734
-
735
- createCardDOM(id) {
736
- const div = document.createElement('div');
737
- div.className = 'chart-card';
738
- div.id = `card-${id}`;
739
- div.innerHTML = `
740
- <div class="card-header">
741
- <div class="card-info" id="info-${id}">
742
- <div class="status-dot"></div>
743
- <span class="symbol-text">New Chart</span>
744
- </div>
745
- <div class="card-tools">
746
- <button class="tool-btn" onclick="ConfigModal.open('${id}')">⚙</button>
747
- <button class="tool-btn close" onclick="GlobalApp.removeChart('${id}')">✕</button>
748
- </div>
749
  </div>
750
- <div class="card-content" id="body-${id}">
751
- <div class="empty-state">
752
- <button class="config-btn-subtle" onclick="ConfigModal.open('${id}')">
753
- <span>+</span> Configure Symbol
754
- </button>
755
- </div>
756
  </div>
757
- `;
758
- return div;
759
- },
760
-
761
- removeChart(id) {
762
- if(!confirm("Remove this chart?")) return;
763
- ChartLogic.destroy(id);
764
-
765
- const colIdx = this.state.charts[id].col;
766
- this.state.layout[colIdx] = this.state.layout[colIdx].filter(x => x !== id);
767
- delete this.state.charts[id];
768
-
769
- document.getElementById(`card-${id}`).remove();
770
- this.adjustHeights();
771
- this.saveState();
772
- },
773
-
774
- // --- CONTROLS ---
775
- onDateChange() {
776
- this.state.date = document.getElementById('globalDate').value;
777
- const today = new Date().toISOString().split('T')[0];
778
- if(this.state.date === today && this.state.mode !== 'LIVE') this.setMode('LIVE');
779
- else this.refreshAll();
780
- this.saveState();
781
- },
782
-
783
- setMode(m, save=true) {
784
- this.state.mode = m;
785
- document.getElementById('modeHist').classList.toggle('active', m==='HIST');
786
- const liveBtn = document.getElementById('modeLive');
787
- liveBtn.classList.toggle('active', m==='LIVE');
788
- liveBtn.classList.toggle('live', m==='LIVE'); // Green Text
 
 
 
 
 
 
 
 
 
 
 
789
 
790
- if(m==='LIVE') ChartLogic.startAllLive();
791
- else ChartLogic.stopAllLive();
 
792
 
793
- if(save) this.saveState();
794
- },
795
-
796
- refreshAll() {
797
- Object.keys(this.state.charts).forEach(id => {
798
- if(this.state.charts[id].config.root) ChartLogic.plot(id);
799
- });
800
- },
801
-
802
- resetApp() {
803
- if(confirm("Reset entire workspace?")) {
804
- localStorage.removeItem('DC_PRO_V5');
805
- location.reload();
806
- }
807
- },
808
-
809
- // --- PERSISTENCE ---
810
- saveState() {
811
- const simpleLayout = this.state.layout.map(col => {
812
- return col.map(id => ({ id, config: this.state.charts[id].config }));
813
  });
814
- const data = {
815
- date: this.state.date,
816
- mode: this.state.mode,
817
- columns: this.state.columns,
818
- layout: simpleLayout
819
- };
820
- localStorage.setItem('DC_PRO_V5', JSON.stringify(data));
821
- },
822
-
823
- loadState() {
824
- try {
825
- const raw = localStorage.getItem('DC_PRO_V5');
826
- if(!raw) { this.initDefault(); return; }
827
- const data = JSON.parse(raw);
828
-
829
- // Date handling: try to stick to saved date, but if invalid/old, might default to latest loaded
830
- // For now, let's use the loaded latest date unless user explicitly saved one?
831
- // Actually, prioritizing the latest DB date is usually better for UX on reload.
832
- // We keep this.state.date as is (from loadDates), unless we want to force saved.
833
- if(data.date) document.getElementById('globalDate').value = data.date;
834
- this.state.date = document.getElementById('globalDate').value;
835
-
836
- this.state.mode = data.mode || 'HIST';
837
- this.state.columns = data.columns || 3;
838
-
839
- // Hydrate Charts
840
- this.state.layout = [[],[],[],[]]; // Max 4 cols supported in UI
841
- data.layout.forEach((col, idx) => {
842
- if(idx >= 4) return;
843
- col.forEach(item => {
844
- this.state.charts[item.id] = {
845
- col: idx,
846
- config: item.config,
847
- instance: null,
848
- cachedData: null
849
- };
850
- this.state.layout[idx].push(item.id);
851
- });
852
- });
853
-
854
- this.renderGrid();
855
-
856
- } catch(e) { console.error(e); this.initDefault(); }
857
- },
858
-
859
- initDefault() {
860
- this.state.layout = [[], [], []];
861
- this.state.columns = 3;
862
- // Add 1 empty chart per col
863
- for(let i=0; i<3; i++) this.addChart(i);
864
  }
865
- };
866
-
867
- /** --- CHART LOGIC --- */
868
- const ChartLogic = {
869
- async plot(id, isUpdate=false) {
870
- const chartObj = GlobalApp.state.charts[id];
871
- if(!chartObj || !chartObj.config.root) return;
872
- const cfg = chartObj.config;
873
-
874
- // UI Updates
875
- const infoEl = document.getElementById(`info-${id}`);
876
- if(infoEl) {
877
- const dotClass = GlobalApp.state.mode === 'LIVE' ? (isUpdate ? 'retry' : 'live') : 'hist';
878
- infoEl.innerHTML = `
879
- <div class="status-dot ${dotClass}"></div>
880
- <span class="symbol-text">${cfg.root}</span>
881
- <span class="meta-text">${cfg.expiry} · ${cfg.timeframe}</span>
882
- `;
883
- }
884
-
885
- // Prepare Container
886
- const body = document.getElementById(`body-${id}`);
887
- if(!body.querySelector('canvas')) {
888
- body.innerHTML = `<div style="width:100%; height:100%; position:relative;"><canvas id="canvas-${id}"></canvas></div>`;
889
- }
890
-
891
- // Calc Time
892
- let endTime = cfg.end;
893
- if(GlobalApp.state.mode === 'LIVE') {
894
- const now = new Date();
895
- endTime = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
896
- }
897
 
898
- try {
899
- const res = await fetch('/api/fetch_series', {
900
- method: 'POST',
901
- headers: {'Content-Type': 'application/json'},
902
- body: JSON.stringify({
903
- date: GlobalApp.state.date,
904
- root: cfg.root,
905
- expiry: cfg.expiry,
906
- instrument: cfg.instrument,
907
- timeframe: cfg.timeframe,
908
- start_time: cfg.start,
909
- end_time: endTime
910
- })
911
- });
912
- const data = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
 
914
- if(data.error || !data.labels || !data.labels.length) {
915
- if(GlobalApp.state.mode !== 'LIVE') {
916
- body.innerHTML = '<div class="empty-state"><span style="color:var(--text-muted)">No Data Available</span></div>';
917
- }
918
- return;
919
- }
920
 
921
- // Cache Data for Layout Switching
922
- chartObj.cachedData = data;
923
-
924
- // Process Series
925
- const datasets = this.processDatasets(cfg.series, data);
926
- this.renderCanvas(id, data.labels, datasets);
927
-
928
- // Fix Dot if it was 'retry'
929
- if(infoEl) infoEl.querySelector('.status-dot').className = `status-dot ${GlobalApp.state.mode==='LIVE'?'live':'hist'}`;
930
-
931
- } catch(e) { console.error(e); }
932
- },
933
-
934
- // Render from Cache or Fetch
935
- render(id) {
936
- const chartObj = GlobalApp.state.charts[id];
937
- if(!chartObj || !chartObj.config.root) return;
938
-
939
- // If we have cached data, use it instantly (layout switch optimization)
940
- if(chartObj.cachedData) {
941
- // UI Restore
942
- const body = document.getElementById(`body-${id}`);
943
- if(!body.querySelector('canvas')) {
944
- body.innerHTML = `<div style="width:100%; height:100%; position:relative;"><canvas id="canvas-${id}"></canvas></div>`;
945
- }
946
- const datasets = this.processDatasets(chartObj.config.series, chartObj.cachedData);
947
- this.renderCanvas(id, chartObj.cachedData.labels, datasets);
948
-
949
- // Update Title
950
- const infoEl = document.getElementById(`info-${id}`);
951
- if(infoEl) {
952
- const cfg = chartObj.config;
953
- const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
954
- infoEl.innerHTML = `
955
- <div class="status-dot ${dotClass}"></div>
956
- <span class="symbol-text">${cfg.root}</span>
957
- <span class="meta-text">${cfg.expiry} · ${cfg.timeframe}</span>
958
- `;
959
  }
960
- } else {
961
- this.plot(id);
962
  }
963
 
964
- // Restart live loop if needed
965
- if(GlobalApp.state.mode === 'LIVE') this.startLive(id);
966
- },
967
 
968
- processDatasets(seriesCfg, data) {
969
- return seriesCfg.map(s => {
970
  const vals = data.labels.map((_, i) => {
971
  const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
972
  let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
@@ -974,233 +868,215 @@ HTML_TEMPLATE = """
974
  });
975
  return {
976
  label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
977
- borderWidth: 1.5, pointRadius: 0, hitRadius: 10, hoverRadius: 4, tension: 0.1,
978
  yAxisID: s.axis==='left'?'y':'y1'
979
  };
980
  });
981
- },
982
 
983
- renderCanvas(id, labels, datasets) {
984
- const ctx = document.getElementById(`canvas-${id}`);
985
- if(!ctx) return;
986
- const cData = GlobalApp.state.charts[id];
987
-
988
- if(cData.instance) {
989
- cData.instance.data.labels = labels;
990
- cData.instance.data.datasets = datasets;
991
- cData.instance.update('none');
992
- } else {
993
- const hasRight = datasets.some(d => d.yAxisID === 'y1');
994
- cData.instance = new Chart(ctx.getContext('2d'), {
995
- type: 'line',
996
- data: { labels, datasets },
997
- options: {
998
- responsive: true, maintainAspectRatio: false, animation: false,
999
- interaction: { mode: 'index', intersect: false },
1000
- plugins: { legend: { display: false } },
1001
- scales: {
1002
- x: { grid: { color: '#27272a', tickLength: 0 }, ticks: { color: '#71717a', maxTicksLimit: 6 } },
1003
- y: { type: 'linear', display: true, position: 'left', grid: { color: '#27272a' }, ticks: { color: '#71717a' } },
1004
- y1: { type: 'linear', display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#71717a' } }
1005
- }
 
 
 
 
 
 
 
1006
  }
1007
- });
1008
- }
1009
- },
1010
-
1011
- destroy(id) {
1012
- const c = GlobalApp.state.charts[id];
1013
- this.stopLive(id);
1014
- if(c.instance) c.instance.destroy();
1015
- c.cachedData = null;
1016
- },
1017
-
1018
- startLive(id) {
1019
- this.stopLive(id);
1020
- const c = GlobalApp.state.charts[id];
1021
- c.interval = setInterval(() => {
1022
- if(c.config.root) this.plot(id, true);
1023
- }, 15000);
1024
- },
1025
-
1026
- stopLive(id) {
1027
- const c = GlobalApp.state.charts[id];
1028
- if(c && c.interval) { clearInterval(c.interval); c.interval = null; }
1029
- },
1030
-
1031
- startAllLive() { Object.keys(GlobalApp.state.charts).forEach(id => this.startLive(id)); },
1032
- stopAllLive() { Object.keys(GlobalApp.state.charts).forEach(id => this.stopLive(id)); }
1033
- };
1034
-
1035
- /** --- CONFIG MODAL --- */
1036
- const ConfigModal = {
1037
- currId: null,
1038
- isGlobal: false,
1039
-
1040
- async open(id) {
1041
- this.currId = id;
1042
- this.isGlobal = false;
1043
- document.getElementById('cfgModalTitle').innerText = "Configure Symbol";
1044
- document.getElementById('configOverlay').style.display = 'flex';
1045
-
1046
- const cfg = GlobalApp.state.charts[id].config;
1047
- await this.loadRoots(cfg.root);
1048
-
1049
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1050
- document.getElementById('cfgStart').value = cfg.start || '09:15';
1051
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
1052
-
1053
- this.renderSeries(cfg.series || GlobalApp.defaults);
1054
-
1055
- if(cfg.root) {
1056
- document.getElementById('cfgRoot').value = cfg.root;
1057
- await this.onRootChange(cfg.expiry, cfg.instrument);
1058
- }
1059
- },
1060
-
1061
- openDefaults() {
1062
- this.isGlobal = true;
1063
- document.getElementById('cfgModalTitle').innerText = "Default Settings";
1064
- document.getElementById('configOverlay').style.display = 'flex';
1065
- this.renderSeries(GlobalApp.defaults);
1066
- },
1067
-
1068
- close() { document.getElementById('configOverlay').style.display = 'none'; },
1069
-
1070
- async loadRoots(pre) {
1071
- const sel = document.getElementById('cfgRoot');
1072
- sel.innerHTML = '<option>Loading...</option>';
1073
- try {
1074
- const d = GlobalApp.state.date;
1075
- const res = await fetch(`/api/roots?date=${d}`);
1076
- const roots = await res.json();
1077
- sel.innerHTML = `<option value="">Select...</option>` + roots.map(r=>`<option>${r}</option>`).join('');
1078
- if(pre && roots.includes(pre)) sel.value = pre;
1079
- } catch(e){ sel.innerHTML='<option>Error</option>'; }
1080
- },
1081
-
1082
- async onRootChange(preExp, preInst) {
1083
- const root = document.getElementById('cfgRoot').value;
1084
- const d = GlobalApp.state.date;
1085
- const eSel = document.getElementById('cfgExp');
1086
- eSel.innerHTML = '<option>Loading...</option>';
1087
-
1088
- const res = await fetch(`/api/expiries?date=${d}&root=${root}`);
1089
- const exps = await res.json();
1090
- eSel.innerHTML = `<option value="">Select...</option>` + exps.map(e=>`<option>${e}</option>`).join('');
1091
-
1092
- if(preExp && exps.includes(preExp)) {
1093
- eSel.value = preExp;
1094
- await this.onExpChange(preInst);
1095
- } else {
1096
- const auto = await fetch(`/api/auto_config?date=${d}&root=${root}`).then(r=>r.json());
1097
- if(auto.current) {
1098
- eSel.value = auto.current.expiry;
1099
- await this.onExpChange(auto.current.instrument);
1100
  }
1101
- }
1102
- },
1103
-
1104
- async onExpChange(preInst) {
1105
- const root = document.getElementById('cfgRoot').value;
1106
- const exp = document.getElementById('cfgExp').value;
1107
- const d = GlobalApp.state.date;
1108
- const iSel = document.getElementById('cfgInst');
1109
- iSel.innerHTML = '<option>Loading...</option>';
1110
- const res = await fetch(`/api/instruments?date=${d}&root=${root}&expiry=${exp}`);
1111
- const insts = await res.json();
1112
- iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
1113
- if(preInst && insts.includes(preInst)) iSel.value = preInst;
1114
- },
1115
-
1116
- renderSeries(list) {
1117
- const div = document.getElementById('cfgSeriesList');
1118
- div.innerHTML = '';
1119
- list.forEach(s => {
1120
- const el = document.createElement('div');
1121
- el.className = 'series-item';
1122
- el.innerHTML = `
1123
- <input type="color" value="${s.color}" class="s-color">
1124
- <input type="text" value="${s.formula}" class="form-control s-formula" placeholder="$P">
1125
- <input type="text" value="${s.label}" class="form-control s-label" placeholder="Label">
1126
- <div class="custom-select-wrapper" style="width:100%">
1127
- <select class="custom-select s-axis" style="min-width:0; width:100%; padding:4px;">
1128
- <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1129
- <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1130
- </select>
1131
- </div>
1132
- <button class="tool-btn close" onclick="this.parentElement.remove()">✕</button>
1133
- `;
1134
- div.appendChild(el);
1135
  });
1136
- },
1137
-
1138
- addSeriesRow() {
1139
- const div = document.getElementById('cfgSeriesList');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1140
  const el = document.createElement('div');
1141
- el.className = 'series-item';
1142
  el.innerHTML = `
1143
- <input type="color" value="#ffffff" class="s-color">
1144
- <input type="text" value="$P" class="form-control s-formula">
1145
- <input type="text" value="New" class="form-control s-label">
1146
- <div class="custom-select-wrapper" style="width:100%">
1147
- <select class="custom-select s-axis" style="min-width:0; width:100%; padding:4px;">
1148
- <option value="left">L</option><option value="right">R</option>
1149
- </select>
1150
- </div>
1151
- <button class="tool-btn close" onclick="this.parentElement.remove()">✕</button>
1152
  `;
1153
  div.appendChild(el);
1154
- },
1155
-
1156
- save() {
1157
- const rows = document.querySelectorAll('#cfgSeriesList .series-item');
1158
- const series = Array.from(rows).map(r => ({
1159
- color: r.querySelector('.s-color').value,
1160
- formula: r.querySelector('.s-formula').value,
1161
- label: r.querySelector('.s-label').value,
1162
- axis: r.querySelector('.s-axis').value
1163
- }));
1164
-
1165
- if(this.isGlobal) {
1166
- GlobalApp.defaults = series;
1167
- this.showToast("Defaults updated");
1168
- } else {
1169
- const root = document.getElementById('cfgRoot').value;
1170
- const exp = document.getElementById('cfgExp').value;
1171
- const inst = document.getElementById('cfgInst').value;
1172
-
1173
- if(!root || !exp || !inst) { this.showToast("Incomplete details!", true); return; }
1174
-
1175
- const config = {
1176
- root, expiry: exp, instrument: inst,
1177
- timeframe: document.getElementById('cfgTF').value,
1178
- start: document.getElementById('cfgStart').value,
1179
- end: document.getElementById('cfgEnd').value,
1180
- series: series
1181
- };
1182
-
1183
- GlobalApp.state.charts[this.currId].config = config;
1184
- GlobalApp.saveState();
1185
- ChartLogic.plot(this.currId);
1186
- if(GlobalApp.state.mode === 'LIVE') ChartLogic.startLive(this.currId);
1187
- this.showToast("Chart configured");
1188
- }
1189
- this.close();
1190
- },
1191
-
1192
- showToast(msg, isError=false) {
1193
- const t = document.getElementById('toast');
1194
- t.querySelector('#toastMsg').innerText = msg;
1195
- t.querySelector('.toast-icon').innerText = isError ? '!' : '✓';
1196
- t.querySelector('.toast-icon').style.color = isError ? 'var(--danger)' : 'var(--success)';
1197
- t.classList.add('show');
1198
- setTimeout(() => t.classList.remove('show'), 2000);
 
 
1199
  }
1200
- };
1201
-
1202
- window.onload = () => GlobalApp.init();
1203
-
 
1204
  </script>
1205
  </body>
1206
  </html>
 
48
 
49
  @app.get("/api/dates")
50
  def get_dates():
51
+ """
52
+ Fetch distinct dates.
53
+ Increased limit to 100,000 to scan through dense 10s data
54
+ to find older dates.
55
+ """
56
+ # 10s data is dense. 1 day ~ 3750 minutes ~ 22500 rows.
57
+ # 100k rows gives ~4-5 days history scan.
58
+ data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "100000"})
59
  dates = set()
60
  for row in data:
61
  if row.get('minute'):
 
164
  inst_data = row['data'].get(instrument)
165
  if not inst_data: continue
166
 
167
+ # Extract buckets (0s to 50s)
168
  p_arr = inst_data.get('p', []) or []
169
  b_arr = inst_data.get('b', []) or []
170
  s_arr = inst_data.get('s', []) or []
 
211
 
212
 
213
  # ==========================================
214
+ # 5. FRONTEND TEMPLATE (High Aesthetic)
215
  # ==========================================
216
 
217
  HTML_TEMPLATE = """
 
221
  <meta charset="UTF-8">
222
  <title>DepthChain PRO</title>
223
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
224
+ <!-- Fonts -->
225
+ <link rel="preconnect" href="https://fonts.googleapis.com">
226
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
227
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
228
+
229
  <style>
230
+ /* --- DESIGN SYSTEM --- */
231
  :root {
232
+ --bg-app: #050505;
233
+ --bg-panel: #111111;
234
+ --bg-card: #0A0A0A;
235
+ --border: #222222;
236
+ --border-hover: #333333;
 
237
 
238
+ --text-main: #EAEAEA;
239
+ --text-muted: #666666;
240
+ --text-accent: #3399FF;
241
 
242
+ --accent: #007AFF;
243
+ --accent-glow: rgba(0, 122, 255, 0.2);
244
+ --danger: #FF3B30;
245
+ --success: #34C759;
246
+ --warning: #FFCC00;
247
 
248
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
249
  --font-mono: 'JetBrains Mono', monospace;
250
+
251
+ --radius: 8px;
252
+ --header-h: 52px;
253
  }
254
 
255
+ * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; outline: none; }
256
+
257
+ /* SCROLLBARS */
258
  ::-webkit-scrollbar { width: 6px; height: 6px; }
259
  ::-webkit-scrollbar-track { background: transparent; }
260
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
261
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
262
+
263
+ body {
264
+ background: var(--bg-app); color: var(--text-main); font-family: var(--font-main);
265
+ height: 100vh; overflow: hidden; display: flex; flex-direction: column;
 
 
 
 
 
 
266
  }
267
 
268
+ /* --- HEADER --- */
269
+ header {
270
+ height: var(--header-h); background: rgba(17, 17, 17, 0.8);
271
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
272
  border-bottom: 1px solid var(--border);
273
+ display: flex; align-items: center; justify-content: space-between;
274
+ padding: 0 16px; z-index: 100;
 
 
 
 
275
  }
276
 
277
+ .logo { font-weight: 600; font-size: 14px; letter-spacing: -0.02em; color: var(--text-main); }
278
+ .logo span { color: var(--accent); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
+ .controls { display: flex; gap: 12px; align-items: center; }
281
+
282
+ /* Custom Inputs */
283
+ select, button {
284
+ background: #1A1A1A; border: 1px solid var(--border); color: var(--text-main);
285
+ padding: 0 12px; height: 28px; border-radius: 6px; font-size: 12px;
286
+ font-family: var(--font-main); transition: all 0.2s ease; cursor: pointer;
 
 
287
  }
288
+ select:hover, button:hover { border-color: var(--border-hover); background: #222; }
289
+ select:focus { border-color: var(--accent); }
290
+
291
+ /* Segmented Control for Layout */
292
+ .segmented {
293
+ background: #1A1A1A; border: 1px solid var(--border);
294
+ border-radius: 6px; padding: 2px; display: flex; gap: 2px; height: 28px;
295
  }
296
+ .seg-btn {
297
+ background: transparent; border: none; color: var(--text-muted);
298
+ width: 32px; height: 100%; border-radius: 4px; font-weight: 600; font-size: 11px;
299
+ display: flex; align-items: center; justify-content: center; padding: 0;
 
 
 
 
300
  }
301
+ .seg-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.05); }
302
+ .seg-btn.active { background: #333; color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.5); }
303
+
304
+ .btn-blue { color: var(--accent); font-weight: 500; border-color: var(--border); }
305
+ .btn-blue:hover { border-color: var(--accent); background: var(--accent-glow); }
306
+
307
+ .btn-red { color: var(--danger); opacity: 0.7; }
308
+ .btn-red:hover { opacity: 1; border-color: var(--danger); }
309
+
310
+ /* Mode Switcher */
311
+ .mode-switch {
312
+ font-size: 11px; font-weight: 600; padding: 0 10px; display: flex; align-items: center; gap: 6px;
313
  }
314
+ .mode-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted); transition: 0.3s; }
315
+ .mode-switch.live .mode-dot { background: var(--success); box-shadow: 0 0 6px var(--success); }
316
+ .mode-switch.live { border-color: var(--success); color: #fff; background: rgba(52, 199, 89, 0.1); }
317
+
318
+ /* --- MAIN LAYOUT --- */
319
+ .grid-container {
320
+ flex: 1; display: flex; overflow: hidden; /* No scroll on container */
321
+ padding: 0; gap: 0;
 
 
322
  }
323
 
324
  .column {
325
+ flex: 1;
326
+ display: flex; flex-direction: column;
327
  border-right: 1px solid var(--border);
328
+ overflow-y: auto; /* Independent Scroll */
 
329
  overflow-x: hidden;
330
+ min-width: 0; /* Prevents flex blowout */
331
  }
332
  .column:last-child { border-right: none; }
333
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  /* --- CHART CARD --- */
335
  .chart-card {
 
 
336
  background: var(--bg-card);
337
+ border-bottom: 1px solid var(--border);
338
+ display: flex; flex-direction: column;
339
  position: relative;
340
+ transition: height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
341
  }
342
 
343
+ .card-head {
344
+ height: 36px; display: flex; align-items: center; justify-content: space-between;
345
+ padding: 0 12px; font-size: 11px; border-bottom: 1px solid rgba(255,255,255,0.03);
346
+ flex-shrink: 0;
347
  }
348
+
349
+ .card-info { display: flex; align-items: center; gap: 8px; font-weight: 500; }
350
+ .ci-symbol { color: var(--text-main); font-weight: 600; }
351
+ .ci-meta { color: var(--text-muted); font-family: var(--font-mono); }
352
+
353
+ .card-tools { display: flex; gap: 4px; opacity: 0; transition: 0.2s; }
354
+ .chart-card:hover .card-tools { opacity: 1; }
355
+
356
+ .tool-btn {
357
+ width: 24px; height: 24px; border: none; background: transparent;
358
+ color: var(--text-muted); border-radius: 4px; display: flex; align-items: center; justify-content: center;
 
 
 
 
 
 
 
359
  }
360
+ .tool-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.1); }
 
361
 
362
+ .card-body { flex: 1; position: relative; overflow: hidden; }
363
+ .card-overlay { position: absolute; inset: 0; display:flex; justify-content: center; align-items: center; z-index: 10; pointer-events: none;}
364
 
365
  /* Empty State */
366
+ .empty-placeholder {
367
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
368
+ height: 100%; width: 100%; color: var(--text-muted); gap: 12px;
369
+ cursor: pointer; transition: 0.2s;
370
  }
371
+ .empty-placeholder:hover { background: rgba(255,255,255,0.02); color: var(--text-main); }
372
+ .add-icon {
373
+ width: 32px; height: 32px; border-radius: 50%; border: 1px dashed var(--text-muted);
374
+ display: flex; align-items: center; justify-content: center; font-size: 16px;
 
375
  }
376
+ .empty-placeholder:hover .add-icon { border-color: var(--text-main); border-style: solid; }
377
+
378
+ /* Add Button in Column */
379
+ .col-add {
380
+ height: 40px; display: flex; align-items: center; justify-content: center;
381
+ border-top: 1px solid var(--border); color: var(--text-muted);
382
+ font-size: 11px; font-weight: 500; cursor: pointer; flex-shrink: 0;
383
+ background: var(--bg-panel); transition: 0.2s;
384
+ }
385
+ .col-add:hover { background: #1a1a1a; color: var(--text-main); }
386
 
387
  /* --- MODAL --- */
388
+ .modal-backdrop {
389
+ position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
390
+ z-index: 200; display: none; align-items: center; justify-content: center;
 
391
  }
392
  .modal {
393
+ background: #111; border: 1px solid var(--border); border-radius: 12px;
394
+ width: 420px; box-shadow: 0 24px 48px rgba(0,0,0,0.8);
395
  display: flex; flex-direction: column; overflow: hidden;
396
+ animation: modalPop 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
397
  }
398
+ @keyframes modalPop { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
399
 
400
+ .m-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 13px; }
401
+ .m-body { padding: 16px; display: flex; flex-direction: column; gap: 16px; max-height: 70vh; overflow-y: auto; }
402
+ .m-foot { padding: 12px 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; background: #0e0e0e; }
 
 
 
 
 
 
403
 
404
+ .f-group label { display: block; font-size: 10px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px; font-weight: 600; }
405
+ .f-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
406
+ .inp { width: 100%; background: #050505; border: 1px solid var(--border); padding: 6px 10px; border-radius: 6px; color: #fff; font-size: 12px; }
407
+ .inp:focus { border-color: var(--accent); }
 
 
 
408
 
409
  /* Series List */
410
+ .s-item { display: grid; grid-template-columns: 24px 1fr 1fr 40px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
411
+ .s-color { padding: 0; width: 24px; height: 24px; border: none; border-radius: 4px; overflow: hidden; cursor: pointer; }
 
412
 
413
  /* Toast */
414
  .toast {
415
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
416
+ background: #222; border: 1px solid var(--border); padding: 8px 16px; border-radius: 20px;
417
+ font-size: 12px; font-weight: 500; color: #fff; opacity: 0; pointer-events: none; transition: 0.3s;
418
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 500; display: flex; align-items: center; gap: 8px;
 
 
 
419
  }
420
+ .toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
421
+ .t-icon { color: var(--success); }
422
 
423
  </style>
424
  </head>
425
  <body>
426
 
427
+ <header>
428
+ <div class="logo">DepthChain <span>PRO</span></div>
429
+
430
+ <div class="controls">
431
+ <select id="globalDate" onchange="App.onDateChange()"></select>
 
 
 
 
 
 
 
 
 
 
432
 
433
+ <button id="modeBtn" class="mode-switch" onclick="App.toggleMode()">
434
+ <div class="mode-dot"></div> <span id="modeText">HISTORICAL</span>
435
+ </button>
 
 
436
 
437
+ <div style="width:1px; height:16px; background:var(--border); margin:0 4px;"></div>
438
 
439
+ <div class="segmented">
440
+ <button class="seg-btn" onclick="App.setLayout(1)">1</button>
441
+ <button class="seg-btn" onclick="App.setLayout(2)">2</button>
442
+ <button class="seg-btn active" onclick="App.setLayout(3)">3</button>
443
+ <button class="seg-btn" onclick="App.setLayout(4)">4</button>
 
444
  </div>
445
 
446
+ <button class="btn-blue" onclick="ConfigModal.openDefaults()">Settings</button>
447
+ <button class="btn-red" onclick="App.reset()">Reset</button>
 
 
448
  </div>
449
+ </header>
450
 
451
+ <div class="grid-container" id="grid">
452
+ <!-- Columns and charts injected here -->
 
453
  </div>
454
 
455
+ <!-- Config Modal -->
456
+ <div class="modal-backdrop" id="modalOverlay">
457
  <div class="modal">
458
+ <div class="m-head">
459
+ <span id="mTitle">Configure Chart</span>
460
  <button class="tool-btn" onclick="ConfigModal.close()">✕</button>
461
  </div>
462
+ <div class="m-body">
463
  <div id="cfgForm">
464
+ <div class="f-row">
465
+ <div class="f-group">
466
  <label>Symbol</label>
467
+ <select id="cfgRoot" class="inp" onchange="ConfigModal.onRootChange()"></select>
468
  </div>
469
+ <div class="f-group">
470
  <label>Expiry</label>
471
+ <select id="cfgExp" class="inp" onchange="ConfigModal.onExpChange()"></select>
472
  </div>
473
  </div>
474
+ <div class="f-group">
475
  <label>Instrument</label>
476
+ <select id="cfgInst" class="inp"></select>
477
  </div>
478
+ <div class="f-row">
479
+ <div class="f-group">
480
  <label>Timeframe</label>
481
+ <select id="cfgTF" class="inp">
482
  <option value="1min">1 Minute</option>
483
  <option value="3min">3 Minutes</option>
484
  <option value="5min">5 Minutes</option>
 
487
  <option value="1hour">1 Hour</option>
488
  </select>
489
  </div>
490
+ <div class="f-group">
491
+ <label>Range (Start - End)</label>
492
+ <div style="display:flex; gap:4px;">
493
+ <input type="time" id="cfgStart" value="09:15" class="inp">
494
+ <input type="time" id="cfgEnd" value="15:30" class="inp">
495
  </div>
496
  </div>
497
  </div>
498
+
499
+ <div style="height:1px; background:var(--border); margin:10px 0;"></div>
500
+
501
+ <div class="f-group">
502
+ <label>Series ($P, $B, $S, $V, $OI)</label>
503
+ <div id="cfgSeries"></div>
504
+ <button class="inp" style="margin-top:4px; text-align:center;" onclick="ConfigModal.addRow()">+ Add Plot Line</button>
505
  </div>
506
  </div>
507
  </div>
508
+ <div class="m-foot">
509
+ <button class="inp" style="width:auto;" onclick="ConfigModal.close()">Cancel</button>
510
+ <button class="inp" style="width:auto; background:var(--accent); border-color:var(--accent);" onclick="ConfigModal.save()">Save</button>
511
  </div>
512
  </div>
513
  </div>
514
 
515
+ <div class="toast" id="toast">
516
+ <span class="t-icon">✓</span> <span id="toastMsg">Saved</span>
 
517
  </div>
518
 
519
  <script>
520
+ /**
521
+ * --- APP CONTROLLER ---
522
+ * Handles Layout, DOM Caching (Instant Switch), and Global State
523
+ */
524
+ const App = {
525
+ state: {
526
+ date: '',
527
+ mode: 'HIST',
528
+ columns: 3,
529
+ layout: [[], [], []], // Array of arrays of IDs
530
+ charts: {} // ID -> { config: {}, instance: ChartObj, interval: null }
531
+ },
532
+ domCache: {}, // Map<ID, HTMLElement> - Keeps chart elements alive in memory
533
+
534
+ defaults: [
535
+ { formula: '$P', color: '#007AFF', label: 'Price', axis: 'left' },
536
+ { formula: '$B', color: '#34C759', label: 'Buy', axis: 'right' },
537
+ { formula: '$S', color: '#FF3B30', label: 'Sell', axis: 'right' }
538
+ ],
539
+
540
+ async init() {
541
+ await this.loadDates();
542
+ this.loadState();
543
+
544
+ // Auto-Live Check
545
+ const today = new Date().toISOString().split('T')[0];
546
+ if(this.state.date === today && this.state.mode !== 'LIVE') this.toggleMode();
547
+
548
+ this.renderGrid();
549
+ this.startLoops();
550
+ },
551
+
552
+ async loadDates() {
553
+ try {
554
+ const res = await fetch('/api/dates');
555
+ const dates = await res.json();
556
+ const sel = document.getElementById('globalDate');
557
+ sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
558
+ if(dates.length) this.state.date = dates[0];
559
+ } catch(e) { console.error(e); }
560
+ },
561
+
562
+ loadState() {
563
+ try {
564
+ const raw = localStorage.getItem('dc_pro_v5');
565
+ if(raw) {
566
+ const s = JSON.parse(raw);
567
+ this.state.date = s.date || this.state.date;
568
+ document.getElementById('globalDate').value = this.state.date;
569
+ this.state.columns = s.columns || 3;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
+ // Restore Mode
572
+ if(s.mode === 'LIVE') {
573
+ this.state.mode = 'LIVE';
574
+ this.updateModeUI();
575
+ }
 
 
 
 
 
 
 
576
 
577
+ // Restore Layout & Configs
578
+ this.state.layout = [];
579
+ s.layout.forEach((colIds, cIdx) => {
580
+ this.state.layout[cIdx] = [];
581
+ colIds.forEach(item => {
582
+ const id = item.id;
583
+ this.state.layout[cIdx].push(id);
584
+ this.state.charts[id] = { config: item.config, instance: null, interval: null };
585
+ });
586
+ });
587
+ } else {
588
+ this.resetLayoutState();
589
  }
590
+ } catch(e) { this.resetLayoutState(); }
591
+ },
592
+
593
+ resetLayoutState() {
594
+ this.state.layout = [[],[],[]];
595
+ this.state.columns = 3;
596
+ // Init 3x2 empty
597
+ for(let i=0; i<3; i++) {
598
+ this.addChartToState(i);
599
+ this.addChartToState(i);
600
+ }
601
+ },
602
+
603
+ saveState() {
604
+ const minLayout = this.state.layout.map(col => col.map(id => ({
605
+ id: id, config: this.state.charts[id].config
606
+ })));
607
+ localStorage.setItem('dc_pro_v5', JSON.stringify({
608
+ date: this.state.date,
609
+ mode: this.state.mode,
610
+ columns: this.state.columns,
611
+ layout: minLayout
612
+ }));
613
+ },
614
+
615
+ addChartToState(colIdx) {
616
+ const id = 'c_' + Date.now() + Math.random().toString(36).substr(2,4);
617
+ this.state.charts[id] = { config: {}, instance: null, interval: null };
618
+ if(!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
619
+ this.state.layout[colIdx].push(id);
620
+ return id;
621
+ },
622
+
623
+ // --- RENDERING (The Magic Part) ---
624
+ setLayout(n) {
625
+ if(n === this.state.columns) return;
626
+
627
+ // Flatten all charts
628
+ const allIds = this.state.layout.flat();
629
+ this.state.columns = n;
630
+
631
+ // Update UI Tabs
632
+ document.querySelectorAll('.seg-btn').forEach((b, i) => {
633
+ b.classList.toggle('active', (i+1) === n);
634
+ });
635
+
636
+ // Redistribute
637
+ this.state.layout = Array.from({length: n}, () => []);
638
+ allIds.forEach((id, i) => {
639
+ this.state.layout[i % n].push(id);
640
+ });
641
+
642
+ this.renderGrid();
643
+ this.saveState();
644
+ },
645
+
646
+ renderGrid() {
647
+ const container = document.getElementById('grid');
648
+ // We do NOT clear innerHTML because we want to preserve DOM nodes (Canvas context)
649
+ // Instead, we diff or just append cached nodes to new columns.
650
+
651
+ container.innerHTML = ''; // Reset container structure
652
+
653
+ for(let i=0; i<this.state.columns; i++) {
654
+ const colDiv = document.createElement('div');
655
+ colDiv.className = 'column';
656
+ colDiv.id = `col-${i}`;
657
+
658
+ const ids = this.state.layout[i] || [];
659
+ ids.forEach(id => {
660
+ const el = this.getOrBuilderChartDOM(id);
661
+ colDiv.appendChild(el);
662
+ });
663
 
664
+ // Add button
665
+ const btn = document.createElement('div');
666
+ btn.className = 'col-add';
667
+ btn.innerHTML = '+ Add Chart';
668
+ btn.onclick = () => this.addChartBtn(i);
669
+ colDiv.appendChild(btn);
670
 
671
+ container.appendChild(colDiv);
672
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
+ this.calcHeights();
675
+ // Trigger resize for charts to fit new widths
676
+ setTimeout(() => Object.values(this.state.charts).forEach(c => c.instance && c.instance.resize()), 50);
677
+ },
678
+
679
+ getOrBuilderChartDOM(id) {
680
+ if(this.domCache[id]) return this.domCache[id];
681
+
682
+ // Build New
683
+ const div = document.createElement('div');
684
+ div.className = 'chart-card';
685
+ div.id = id;
686
+ div.innerHTML = `
687
+ <div class="card-head">
688
+ <div class="card-info">
689
+ <div class="mode-dot" id="dot-${id}"></div>
690
+ <span class="ci-symbol" id="sym-${id}">Empty</span>
691
+ <span class="ci-meta" id="meta-${id}"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  </div>
693
+ <div class="card-tools">
694
+ <button class="tool-btn" onclick="ConfigModal.open('${id}')">⚙</button>
695
+ <button class="tool-btn" onclick="App.removeChart('${id}')">✕</button>
 
 
 
696
  </div>
697
+ </div>
698
+ <div class="card-body" id="body-${id}">
699
+ <div class="empty-placeholder" onclick="ConfigModal.open('${id}')">
700
+ <div class="add-icon">+</div>
701
+ <span>Configure Chart</span>
702
+ </div>
703
+ </div>
704
+ `;
705
+
706
+ this.domCache[id] = div;
707
+
708
+ // If config exists, plot it (restore)
709
+ const cfg = this.state.charts[id].config;
710
+ if(cfg && cfg.root) ChartLogic.plot(id);
711
+
712
+ return div;
713
+ },
714
+
715
+ addChartBtn(colIdx) {
716
+ const id = this.addChartToState(colIdx);
717
+ this.renderGrid(); // Re-render to place it
718
+ this.saveState();
719
+ },
720
+
721
+ removeChart(id) {
722
+ ChartLogic.stopLive(id);
723
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
724
+ delete this.state.charts[id];
725
+ delete this.domCache[id];
726
+
727
+ // Remove from layout array
728
+ this.state.layout = this.state.layout.map(col => col.filter(x => x !== id));
729
+ this.renderGrid();
730
+ this.saveState();
731
+ },
732
+
733
+ calcHeights() {
734
+ const headerH = 52;
735
+ const totalH = window.innerHeight - headerH;
736
+
737
+ this.state.layout.forEach((ids, colIdx) => {
738
+ const count = ids.length;
739
+ if(count === 0) return;
740
 
741
+ // Max 4 visible rule
742
+ let hPx = totalH / count;
743
+ if(count > 4) hPx = totalH / 4; // Scroll logic kicks in via CSS
744
 
745
+ ids.forEach(id => {
746
+ if(this.domCache[id]) {
747
+ this.domCache[id].style.height = `${hPx}px`;
748
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  });
750
+ });
751
+ },
752
+
753
+ // --- GLOBAL ACTIONS ---
754
+ onDateChange() {
755
+ this.state.date = document.getElementById('globalDate').value;
756
+ const today = new Date().toISOString().split('T')[0];
757
+ if(this.state.date === today && this.state.mode !== 'LIVE') this.toggleMode();
758
+
759
+ this.saveState();
760
+ // Refresh all
761
+ Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
762
+ this.startLoops();
763
+ },
764
+
765
+ toggleMode() {
766
+ this.state.mode = (this.state.mode === 'LIVE') ? 'HIST' : 'LIVE';
767
+ this.updateModeUI();
768
+ this.saveState();
769
+ this.startLoops();
770
+ },
771
+
772
+ updateModeUI() {
773
+ const btn = document.getElementById('modeBtn');
774
+ const txt = document.getElementById('modeText');
775
+ if(this.state.mode === 'LIVE') {
776
+ btn.classList.add('live');
777
+ txt.innerText = 'LIVE';
778
+ } else {
779
+ btn.classList.remove('live');
780
+ txt.innerText = 'HISTORICAL';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  }
782
+ },
783
+
784
+ startLoops() {
785
+ const isLive = (this.state.mode === 'LIVE');
786
+ Object.keys(this.state.charts).forEach(id => {
787
+ if(isLive) ChartLogic.startLive(id);
788
+ else ChartLogic.stopLive(id);
789
+ });
790
+ },
791
+
792
+ reset() {
793
+ if(confirm("Reset Everything?")) {
794
+ localStorage.removeItem('dc_pro_v5');
795
+ location.reload();
796
+ }
797
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
 
799
+ toast(msg) {
800
+ const t = document.getElementById('toast');
801
+ document.getElementById('toastMsg').innerText = msg;
802
+ t.classList.add('visible');
803
+ setTimeout(() => t.classList.remove('visible'), 2000);
804
+ }
805
+ };
806
+
807
+ window.onresize = () => App.calcHeights();
808
+
809
+ /**
810
+ * --- CHART LOGIC ---
811
+ */
812
+ const ChartLogic = {
813
+ async plot(id, isUpdate=false) {
814
+ const c = App.state.charts[id];
815
+ const cfg = c.config;
816
+ if(!cfg || !cfg.root) return;
817
+
818
+ // UI Updates
819
+ const card = App.domCache[id];
820
+ if(!card) return; // Should not happen
821
+
822
+ card.querySelector('#sym-'+id).innerText = cfg.root;
823
+ card.querySelector('#meta-'+id).innerText = `${cfg.expiry} • ${cfg.instrument} • ${cfg.timeframe}`;
824
+
825
+ const dot = card.querySelector('#dot-'+id);
826
+ dot.style.background = isUpdate ? 'var(--warning)' : (App.state.mode==='LIVE' ? 'var(--success)' : 'var(--accent)');
827
+
828
+ // Canvas Setup
829
+ const body = card.querySelector('#body-'+id);
830
+ if(!body.querySelector('canvas')) {
831
+ body.innerHTML = `<div style="position:relative; width:100%; height:100%"><canvas id="can-${id}"></canvas></div>`;
832
+ }
833
 
834
+ let endTime = cfg.end;
835
+ if(App.state.mode === 'LIVE') {
836
+ const n = new Date();
837
+ endTime = `${String(n.getHours()).padStart(2,'0')}:${String(n.getMinutes()).padStart(2,'0')}`;
838
+ }
 
839
 
840
+ try {
841
+ const res = await fetch('/api/fetch_series', {
842
+ method: 'POST',
843
+ headers: {'Content-Type':'application/json'},
844
+ body: JSON.stringify({
845
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry,
846
+ instrument: cfg.instrument, timeframe: cfg.timeframe,
847
+ start_time: cfg.start, end_time: endTime
848
+ })
849
+ });
850
+ const data = await res.json();
851
+
852
+ if(data.error || !data.labels || !data.labels.length) {
853
+ if(App.state.mode !== 'LIVE') {
854
+ body.innerHTML = `<div class="empty-placeholder" onclick="ConfigModal.open('${id}')"><span>No Data</span></div>`;
855
+ if(c.instance) c.instance.destroy();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  }
857
+ return;
 
858
  }
859
 
860
+ // Restore live dot color
861
+ if(App.state.mode==='LIVE') dot.style.background = 'var(--success)';
 
862
 
863
+ const datasets = cfg.series.map(s => {
 
864
  const vals = data.labels.map((_, i) => {
865
  const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
866
  let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
 
868
  });
869
  return {
870
  label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
871
+ borderWidth: 1.5, pointRadius: 0, hoverRadius: 4, tension: 0.1,
872
  yAxisID: s.axis==='left'?'y':'y1'
873
  };
874
  });
 
875
 
876
+ this.renderCanvas(id, data.labels, datasets);
877
+
878
+ } catch(e) { console.error(e); }
879
+ },
880
+
881
+ renderCanvas(id, labels, datasets) {
882
+ const ctx = document.getElementById(`can-${id}`);
883
+ if(!ctx) return;
884
+ const c = App.state.charts[id];
885
+
886
+ if(c.instance) {
887
+ c.instance.data.labels = labels;
888
+ c.instance.data.datasets = datasets;
889
+ c.instance.update('none');
890
+ } else {
891
+ const hasRight = datasets.some(d => d.yAxisID === 'y1');
892
+ c.instance = new Chart(ctx.getContext('2d'), {
893
+ type: 'line',
894
+ data: { labels, datasets },
895
+ options: {
896
+ responsive: true, maintainAspectRatio: false, animation: false,
897
+ interaction: { mode: 'index', intersect: false },
898
+ plugins: {
899
+ legend: { display:false },
900
+ tooltip: { backgroundColor:'rgba(10,10,10,0.95)', titleColor:'#fff', bodyColor:'#ccc', borderColor:'#333', borderWidth:1 }
901
+ },
902
+ scales: {
903
+ x: { grid:{color:'#222', tickLength:0}, ticks:{color:'#666', maxTicksLimit:8} },
904
+ y: { type:'linear', display:true, position:'left', grid:{color:'#222'}, ticks:{color:'#666'} },
905
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#666'} }
906
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  });
909
+ }
910
+ },
911
+
912
+ startLive(id) {
913
+ this.stopLive(id);
914
+ const c = App.state.charts[id];
915
+ if(!c.config.root) return;
916
+
917
+ this.plot(id);
918
+ c.interval = setInterval(() => this.plot(id, true), 15000);
919
+ },
920
+
921
+ stopLive(id) {
922
+ const c = App.state.charts[id];
923
+ if(c && c.interval) { clearInterval(c.interval); c.interval = null; }
924
+ }
925
+ };
926
+
927
+ /**
928
+ * --- CONFIG MODAL ---
929
+ */
930
+ const ConfigModal = {
931
+ currId: null,
932
+ isGlobal: false,
933
+
934
+ async open(id) {
935
+ this.currId = id;
936
+ this.isGlobal = false;
937
+ document.getElementById('mTitle').innerText = 'Configure Chart';
938
+ document.getElementById('modalOverlay').style.display = 'flex';
939
+ document.getElementById('cfgForm').style.display = 'block';
940
+
941
+ const cfg = App.state.charts[id].config;
942
+ await this.loadRoots(cfg.root);
943
+
944
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
945
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
946
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
947
+
948
+ this.renderSeries(cfg.series || App.defaults);
949
+
950
+ if(cfg.root) {
951
+ document.getElementById('cfgRoot').value = cfg.root;
952
+ await this.onRootChange(cfg.expiry, cfg.instrument);
953
+ }
954
+ },
955
+
956
+ openDefaults() {
957
+ this.isGlobal = true;
958
+ document.getElementById('mTitle').innerText = 'Global Defaults';
959
+ document.getElementById('modalOverlay').style.display = 'flex';
960
+ this.renderSeries(App.defaults);
961
+ },
962
+
963
+ close() { document.getElementById('modalOverlay').style.display = 'none'; },
964
+
965
+ async loadRoots(pre) {
966
+ const sel = document.getElementById('cfgRoot');
967
+ sel.innerHTML = '<option>Loading...</option>';
968
+ try {
969
+ const d = App.state.date;
970
+ const res = await fetch(`/api/roots?date=${d}`);
971
+ const data = await res.json();
972
+ sel.innerHTML = `<option value="">-- Select --</option>` + data.map(r=>`<option>${r}</option>`).join('');
973
+ if(pre && data.includes(pre)) sel.value = pre;
974
+ } catch(e){ sel.innerHTML = '<option>Error</option>'; }
975
+ },
976
+
977
+ async onRootChange(preExp, preInst) {
978
+ const root = document.getElementById('cfgRoot').value;
979
+ const d = App.state.date;
980
+ const eSel = document.getElementById('cfgExp');
981
+ eSel.innerHTML = '<option>Loading...</option>';
982
+
983
+ const res = await fetch(`/api/expiries?date=${d}&root=${root}`);
984
+ const data = await res.json();
985
+ eSel.innerHTML = `<option value="">-- Select --</option>` + data.map(e=>`<option>${e}</option>`).join('');
986
+
987
+ if(preExp && data.includes(preExp)) {
988
+ eSel.value = preExp;
989
+ await this.onExpChange(preInst);
990
+ } else {
991
+ // Auto
992
+ const auto = await fetch(`/api/auto_config?date=${d}&root=${root}`).then(r=>r.json());
993
+ if(auto.current) {
994
+ eSel.value = auto.current.expiry;
995
+ await this.onExpChange(auto.current.instrument);
996
+ }
997
+ }
998
+ },
999
+
1000
+ async onExpChange(preInst) {
1001
+ const root = document.getElementById('cfgRoot').value;
1002
+ const exp = document.getElementById('cfgExp').value;
1003
+ const d = App.state.date;
1004
+ const iSel = document.getElementById('cfgInst');
1005
+ iSel.innerHTML = '<option>Loading...</option>';
1006
+
1007
+ const res = await fetch(`/api/instruments?date=${d}&root=${root}&expiry=${exp}`);
1008
+ const data = await res.json();
1009
+ iSel.innerHTML = data.map(i=>`<option>${i}</option>`).join('');
1010
+ if(preInst && data.includes(preInst)) iSel.value = preInst;
1011
+ },
1012
+
1013
+ renderSeries(list) {
1014
+ const div = document.getElementById('cfgSeries');
1015
+ div.innerHTML = '';
1016
+ list.forEach(s => {
1017
  const el = document.createElement('div');
1018
+ el.className = 's-item';
1019
  el.innerHTML = `
1020
+ <input type="color" class="s-color" value="${s.color}">
1021
+ <input type="text" class="inp" value="${s.formula}" placeholder="$P">
1022
+ <input type="text" class="inp" value="${s.label}" placeholder="Label">
1023
+ <select class="inp" style="padding:0 4px;"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1024
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
 
 
 
 
1025
  `;
1026
  div.appendChild(el);
1027
+ });
1028
+ },
1029
+
1030
+ addRow() {
1031
+ const div = document.getElementById('cfgSeries');
1032
+ const el = document.createElement('div');
1033
+ el.className = 's-item';
1034
+ el.innerHTML = `
1035
+ <input type="color" class="s-color" value="#ffffff">
1036
+ <input type="text" class="inp" value="$P">
1037
+ <input type="text" class="inp" value="New">
1038
+ <select class="inp" style="padding:0 4px;"><option value="left">L</option><option value="right">R</option></select>
1039
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1040
+ `;
1041
+ div.appendChild(el);
1042
+ },
1043
+
1044
+ save() {
1045
+ const rows = document.querySelectorAll('#cfgSeries .s-item');
1046
+ const series = Array.from(rows).map(r => ({
1047
+ color: r.querySelector('.s-color').value,
1048
+ formula: r.querySelector('input[type=text]').value, // first text input
1049
+ label: r.querySelectorAll('input[type=text]')[1].value, // second
1050
+ axis: r.querySelector('select').value
1051
+ }));
1052
+
1053
+ if(this.isGlobal) {
1054
+ App.defaults = series;
1055
+ } else {
1056
+ const root = document.getElementById('cfgRoot').value;
1057
+ const exp = document.getElementById('cfgExp').value;
1058
+ const inst = document.getElementById('cfgInst').value;
1059
+
1060
+ if(!root || !exp || !inst) { App.toast('Details incomplete'); return; }
1061
+
1062
+ const config = {
1063
+ root, expiry: exp, instrument: inst,
1064
+ timeframe: document.getElementById('cfgTF').value,
1065
+ start: document.getElementById('cfgStart').value,
1066
+ end: document.getElementById('cfgEnd').value,
1067
+ series: series
1068
+ };
1069
+
1070
+ App.state.charts[this.currId].config = config;
1071
+ App.saveState();
1072
+ ChartLogic.plot(this.currId);
1073
+ if(App.state.mode === 'LIVE') ChartLogic.startLive(this.currId);
1074
  }
1075
+
1076
+ App.toast('Configuration Saved');
1077
+ this.close();
1078
+ }
1079
+ };
1080
  </script>
1081
  </body>
1082
  </html>