topsecrettraders commited on
Commit
9869df2
·
verified ·
1 Parent(s): 43ce35a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +533 -595
app.py CHANGED
@@ -49,7 +49,6 @@ def home():
49
  @app.get("/api/dates")
50
  def get_dates():
51
  """Fetch distinct dates."""
52
- # Fetch enough rows to ensure we get distinct days
53
  data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "50000"})
54
  dates = set()
55
  for row in data:
@@ -206,7 +205,7 @@ def fetch_series(
206
 
207
 
208
  # ==========================================
209
- # 5. FRONTEND TEMPLATE (REFINED UI)
210
  # ==========================================
211
 
212
  HTML_TEMPLATE = """
@@ -214,314 +213,280 @@ HTML_TEMPLATE = """
214
  <html lang="en">
215
  <head>
216
  <meta charset="UTF-8">
217
- <title>DepthChain Pro</title>
218
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
219
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
220
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
221
  <style>
222
- /* --- MODERN VARIABLES --- */
223
  :root {
224
- --bg-app: #0e0e10;
225
- --bg-panel: #18181b;
226
- --bg-card: #1f1f23;
227
- --bg-input: #121214;
228
- --border: #2d2d32;
229
- --border-hover: #4b4b52;
230
 
231
- --text-primary: #efeff1;
232
- --text-secondary: #adadb8;
233
- --text-dim: #70707b;
234
 
235
- --accent: #5865F2;
236
- --accent-hover: #4752C4;
237
 
238
- --success: #2dc937;
239
- --danger: #cc3232;
240
- --warning: #e7b416;
 
 
241
 
242
- --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
243
  --font-mono: 'JetBrains Mono', monospace;
244
  }
245
 
246
- /* --- RESET & BASE --- */
247
  * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
248
- ::-webkit-scrollbar { width: 6px; height: 6px; }
249
- ::-webkit-scrollbar-track { background: transparent; }
250
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
251
- ::-webkit-scrollbar-thumb:hover { background: #52525b; }
252
-
253
- body {
254
- background-color: var(--bg-app);
255
- color: var(--text-primary);
256
- font-family: var(--font-ui);
257
- height: 100vh;
258
- display: flex;
259
- flex-direction: column;
260
- overflow: hidden;
261
  font-size: 13px;
262
  }
263
 
 
 
 
 
 
 
 
264
  /* --- HEADER --- */
265
- .app-header {
266
- height: 48px;
267
- background: rgba(24, 24, 27, 0.85);
268
- backdrop-filter: blur(8px);
269
- border-bottom: 1px solid var(--border);
270
- display: flex;
271
- align-items: center;
272
- justify-content: space-between;
273
- padding: 0 16px;
274
- z-index: 100;
275
  }
276
-
277
- .logo { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; color: var(--text-primary); }
278
- .logo span { color: var(--accent); }
279
- .logo .badge {
280
- font-size: 9px; background: var(--border); padding: 2px 6px;
281
- border-radius: 4px; color: var(--text-secondary); letter-spacing: 0.5px;
282
  }
 
283
 
284
  .controls { display: flex; align-items: center; gap: 12px; }
285
 
286
- /* UI Components */
287
- .ui-select, .ui-btn {
288
- background: var(--bg-input);
289
- border: 1px solid var(--border);
290
- color: var(--text-primary);
291
- padding: 6px 12px;
292
- border-radius: 6px;
293
- font-size: 12px;
294
- font-family: var(--font-ui);
295
- cursor: pointer;
296
- transition: all 0.2s ease;
297
  }
298
- .ui-select:hover, .ui-btn:hover { border-color: var(--border-hover); background: var(--bg-card); }
299
- .ui-select:focus { border-color: var(--accent); }
300
-
301
- .ui-btn.primary { background: var(--accent); color: white; border-color: transparent; font-weight: 600; }
302
- .ui-btn.primary:hover { background: var(--accent-hover); }
303
 
304
- .ui-btn.danger { color: var(--danger); border-color: var(--border); }
305
- .ui-btn.danger:hover { background: rgba(204, 50, 50, 0.1); border-color: var(--danger); }
306
-
307
- .mode-toggle {
308
- display: flex; align-items: center; gap: 6px; padding: 6px 12px;
309
- background: var(--bg-input); border: 1px solid var(--border);
310
- border-radius: 6px; cursor: pointer;
 
 
311
  }
312
- .mode-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); transition: 0.3s; }
313
- .mode-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
314
-
315
- .mode-toggle.active { border-color: var(--success); background: rgba(45, 201, 55, 0.05); }
316
- .mode-toggle.active .mode-dot { background: var(--success); box-shadow: 0 0 6px var(--success); }
317
- .mode-toggle.active .mode-label { color: var(--text-primary); }
318
 
319
- /* --- GRID SYSTEM --- */
320
- .workspace {
 
321
  flex: 1;
322
- display: flex;
323
- overflow: hidden; /* Main container does not scroll, columns do */
324
- gap: 1px;
325
- background: var(--bg-app);
326
  }
327
 
328
- .grid-column {
329
  flex: 1;
330
  display: flex;
331
  flex-direction: column;
332
- overflow-y: auto; /* Columns scroll independently if full */
333
- gap: 6px;
334
- padding: 6px;
335
- border-right: 1px solid transparent;
336
- min-width: 320px;
337
  }
338
- /* Aesthetic border between columns */
339
- .grid-column:not(:last-child) { border-right-color: rgba(255,255,255,0.03); }
 
 
 
 
 
 
 
340
 
341
  /* --- CHART CARD --- */
342
  .chart-card {
343
  background: var(--bg-card);
344
- border: 1px solid var(--border);
345
- border-radius: 8px;
346
- display: flex;
347
- flex-direction: column;
348
- position: relative;
349
- /* Dynamic sizing magic */
350
- flex-grow: 1;
351
- min-height: 350px; /* If too many, it scrolls. If few, it expands. */
352
- transition: border-color 0.2s;
353
- }
354
- .chart-card:hover { border-color: var(--border-hover); box-shadow: 0 4px 20px rgba(0,0,0,0.2); }
355
-
356
- .cc-header {
357
- height: 40px;
358
- padding: 0 12px;
359
  border-bottom: 1px solid var(--border);
360
- display: flex;
361
- align-items: center;
362
- justify-content: space-between;
363
- background: rgba(255,255,255,0.02);
364
- border-radius: 8px 8px 0 0;
365
  }
366
 
367
- .cc-title { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text-primary); }
368
- .cc-title span { color: var(--accent); font-family: var(--font-mono); }
369
- .cc-meta { font-size: 11px; color: var(--text-dim); margin-left: 8px; background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; }
 
 
 
 
 
 
370
 
371
- .cc-actions { display: flex; gap: 4px; opacity: 0.6; transition: 0.2s; }
372
- .chart-card:hover .cc-actions { opacity: 1; }
 
 
 
 
 
 
 
 
 
 
 
373
 
374
- .icon-btn {
375
- width: 28px; height: 28px;
376
- display: flex; align-items: center; justify-content: center;
377
- border: none; background: transparent;
378
- color: var(--text-secondary); border-radius: 4px; cursor: pointer;
379
  }
380
- .icon-btn:hover { background: var(--border); color: var(--text-primary); }
381
- .icon-btn.red:hover { background: rgba(204,50,50,0.2); color: var(--danger); }
382
-
383
- .cc-body { flex: 1; position: relative; width: 100%; height: 100%; min-height: 0; }
384
 
385
  /* Empty State */
386
  .empty-state {
387
- height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
388
- color: var(--text-dim); gap: 12px;
389
- }
390
- .btn-config {
391
- background: transparent; border: 1px dashed var(--border);
392
- color: var(--text-secondary); padding: 8px 20px; border-radius: 6px;
393
- font-size: 12px; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 6px;
394
  }
395
- .btn-config:hover { border-color: var(--text-secondary); color: var(--text-primary); background: rgba(255,255,255,0.02); }
396
-
397
- /* Add Button in Column */
398
- .col-add-btn {
399
- padding: 12px; border: 1px dashed var(--border); border-radius: 8px;
400
- color: var(--text-dim); text-align: center; cursor: pointer; font-size: 12px; font-weight: 500;
401
- margin-top: auto; flex-shrink: 0;
402
  }
403
- .col-add-btn:hover { border-color: var(--text-secondary); color: var(--text-secondary); background: rgba(255,255,255,0.01); }
404
 
405
- /* --- MODAL --- */
406
- .modal-overlay {
407
- position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
408
- z-index: 500; display: none; align-items: center; justify-content: center;
 
409
  }
410
  .modal {
411
- background: var(--bg-panel); border: 1px solid var(--border); border-radius: 12px;
412
  width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
413
- box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
414
  }
415
- .modal-header {
416
- padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;
 
 
 
417
  }
418
- .modal-title { font-weight: 700; font-size: 14px; }
419
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
420
- .modal-footer {
421
- padding: 16px 20px; border-top: 1px solid var(--border); background: var(--bg-card);
422
- border-radius: 0 0 12px 12px; display: flex; justify-content: flex-end; gap: 10px;
423
  }
424
 
425
- .input-group label { display: block; font-size: 11px; text-transform: uppercase; color: var(--text-dim); margin-bottom: 6px; font-weight: 600; }
426
- .input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
427
- input, select {
428
- width: 100%; background: var(--bg-input); border: 1px solid var(--border);
429
- color: var(--text-primary); padding: 8px; border-radius: 6px; font-family: inherit; font-size: 13px;
430
- }
431
- input:focus, select:focus { border-color: var(--accent); }
432
 
433
  /* Series List */
 
434
  .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; }
435
- .series-row input[type="color"] { padding: 0; height: 32px; border: none; background: none; cursor: pointer; }
436
- .remove-series { color: var(--text-dim); cursor: pointer; text-align: center; font-weight: bold; }
437
- .remove-series:hover { color: var(--danger); }
438
-
439
  /* Toast */
440
  .toast {
441
- position: fixed; bottom: 24px; right: 24px; background: var(--bg-panel); border: 1px solid var(--success);
442
- color: var(--text-primary); padding: 12px 20px; border-radius: 8px; font-weight: 500;
443
- box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5); transform: translateY(100px); transition: 0.3s cubic-bezier(0.16, 1, 0.3, 1); opacity: 0;
444
- display: flex; align-items: center; gap: 10px; z-index: 1000;
445
  }
446
- .toast.show { transform: translateY(0); opacity: 1; }
447
- .toast::before { content: ''; display: block; width: 8px; height: 8px; background: var(--success); border-radius: 50%; }
448
 
449
  </style>
450
  </head>
451
  <body>
452
 
453
- <!-- TOP BAR -->
454
- <header class="app-header">
455
- <div class="logo">
456
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
457
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
458
- </svg>
459
- DepthChain <span>PRO</span> <div class="badge">V3.0</div>
460
  </div>
461
 
462
  <div class="controls">
463
- <!-- Date Selector -->
464
- <select id="globalDate" class="ui-select" onchange="App.onDateChange()"></select>
465
 
466
- <!-- Live Mode Toggle -->
467
- <div id="modeToggle" class="mode-toggle" onclick="App.toggleMode()">
468
- <div class="mode-dot"></div>
469
- <div class="mode-label" id="modeLabel">HISTORY</div>
 
 
 
470
  </div>
471
 
472
- <div style="width:1px; height:20px; background:var(--border);"></div>
473
 
474
- <!-- Layout Controls -->
475
- <select class="ui-select" id="layoutSelect" onchange="App.setLayout(this.value)">
476
- <option value="1">1 Column</option>
477
- <option value="2">2 Columns</option>
478
- <option value="3" selected>3 Columns</option>
479
- <option value="4">4 Columns</option>
480
- </select>
481
 
482
- <button class="ui-btn" onclick="ConfigModal.openDefaults()">Settings</button>
483
- <button class="ui-btn danger" onclick="App.resetAll()">Reset</button>
484
  </div>
485
- </header>
486
 
487
- <!-- MAIN WORKSPACE -->
488
- <div class="workspace" id="gridContainer">
489
  <!-- Columns injected by JS -->
490
  </div>
491
 
492
- <!-- CONFIGURATION MODAL -->
493
- <div class="modal-overlay" id="configModal">
494
  <div class="modal">
495
- <div class="modal-header">
496
- <div class="modal-title" id="modalTitle">Configure Chart</div>
497
- <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
498
  </div>
499
  <div class="modal-body">
500
- <div id="cfgLoading" style="text-align:center; padding:20px; display:none; color:var(--text-dim);">
501
- Fetching metadata...
502
- </div>
503
-
504
  <div id="cfgContent">
505
- <div class="input-row">
506
- <div class="input-group">
507
  <label>Symbol</label>
508
- <select id="cfgRoot" onchange="ConfigModal.onRootChange()"></select>
509
  </div>
510
- <div class="input-group">
511
  <label>Expiry</label>
512
- <select id="cfgExp" onchange="ConfigModal.onExpChange()"></select>
513
  </div>
514
  </div>
515
-
516
- <div class="input-group">
517
  <label>Instrument</label>
518
- <select id="cfgInst"></select>
519
  </div>
520
 
521
- <div class="input-row">
522
- <div class="input-group">
523
  <label>Timeframe</label>
524
- <select id="cfgTF">
525
  <option value="1min">1 Minute</option>
526
  <option value="3min">3 Minutes</option>
527
  <option value="5min">5 Minutes</option>
@@ -530,8 +495,8 @@ HTML_TEMPLATE = """
530
  <option value="1hour">1 Hour</option>
531
  </select>
532
  </div>
533
- <div class="input-group">
534
- <label>Range (HH:MM)</label>
535
  <div style="display:flex; gap:8px;">
536
  <input type="time" id="cfgStart" value="09:15">
537
  <input type="time" id="cfgEnd" value="15:30">
@@ -539,52 +504,92 @@ HTML_TEMPLATE = """
539
  </div>
540
  </div>
541
 
542
- <div style="border-top:1px solid var(--border); margin:8px 0;"></div>
543
 
544
- <div class="input-group">
545
- <label>Data Series ($P, $B, $S, $V, $OI)</label>
546
- <div id="cfgSeriesList" style="display:flex; flex-direction:column; gap:8px;"></div>
547
- <button class="ui-btn" style="width:100%; margin-top:8px;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
548
  </div>
549
  </div>
550
  </div>
551
- <div class="modal-footer">
552
- <button class="ui-btn" onclick="ConfigModal.close()">Cancel</button>
553
- <button class="ui-btn primary" onclick="ConfigModal.save()">Apply Changes</button>
554
  </div>
555
  </div>
556
  </div>
557
 
558
- <!-- TOAST NOTIFICATION -->
559
- <div class="toast" id="toast">Settings Saved</div>
 
560
 
561
  <script>
562
  /**
563
- * --- APP STATE & LOGIC ---
564
  */
565
  const App = {
566
  state: {
567
- date: '',
568
- isLive: false,
569
  columns: 3,
570
- // layout[colIndex] = [chartId1, chartId2, ...]
571
- layout: [[], [], []],
572
- charts: {} // id -> { config: {}, instance: Chart }
573
  },
574
-
575
  defaults: [
576
- { formula: '$P', color: '#5865F2', label: 'Price', axis: 'left' },
577
- { formula: '$B', color: '#2dc937', label: 'Buy Qty', axis: 'right' },
578
- { formula: '$S', color: '#cc3232', label: 'Sell Qty', axis: 'right' }
579
  ],
580
 
581
  async init() {
582
  await this.loadDates();
583
- this.loadState(); // Restore from local storage
584
- this.renderGrid();
585
 
586
- // Refresh Loop for Live Mode
587
- setInterval(() => this.liveTick(), 15000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  },
589
 
590
  async loadDates() {
@@ -593,269 +598,211 @@ const App = {
593
  const dates = await res.json();
594
  const sel = document.getElementById('globalDate');
595
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
596
-
597
- // Default to most recent
598
- if(dates.length > 0) this.state.date = dates[0];
599
-
600
- // Check if Today
601
- this.checkDateIsToday();
602
- } catch(e) { console.error("Date fetch error", e); }
603
  },
604
 
605
- checkDateIsToday() {
606
- const today = new Date().toISOString().split('T')[0];
607
- const selected = document.getElementById('globalDate').value;
608
- const toggle = document.getElementById('modeToggle');
609
- const label = document.getElementById('modeLabel');
610
-
611
- if(selected === today) {
612
- this.state.isLive = true;
613
- toggle.classList.add('active');
614
- label.innerText = "LIVE VIEW";
615
- } else {
616
- this.state.isLive = false;
617
- toggle.classList.remove('active');
618
- label.innerText = "HISTORY";
619
- }
620
- this.state.date = selected;
621
  },
622
 
623
- onDateChange() {
624
- this.checkDateIsToday();
625
- this.saveState();
626
- this.refreshAllCharts();
 
 
 
 
 
 
 
 
 
 
627
  },
628
 
629
- toggleMode() {
630
- // Allow manual toggle, though date usually dictates it
631
- this.state.isLive = !this.state.isLive;
632
- const toggle = document.getElementById('modeToggle');
633
- const label = document.getElementById('modeLabel');
634
-
635
- if(this.state.isLive) {
636
- toggle.classList.add('active');
637
- label.innerText = "LIVE VIEW";
638
- this.refreshAllCharts();
639
- } else {
640
- toggle.classList.remove('active');
641
- label.innerText = "HISTORY";
642
  }
643
  },
644
 
645
- setLayout(cols) {
646
- const newCols = parseInt(cols);
647
- if(newCols === this.state.columns) return;
648
-
649
- // Flatten existing charts
650
- const allCharts = this.state.layout.flat();
 
651
 
652
- // Re-distribute
653
- const newLayout = Array.from({length: newCols}, () => []);
654
- allCharts.forEach((id, index) => {
655
- newLayout[index % newCols].push(id);
656
- });
657
-
658
- // Ensure minimum 1 chart per column if empty (optional, but good for UX)
659
- // For now, let's keep it clean. User can add.
660
-
661
- this.state.columns = newCols;
662
- this.state.layout = newLayout;
663
- this.saveState();
664
  this.renderGrid();
 
665
  },
666
 
667
  renderGrid() {
668
- const container = document.getElementById('gridContainer');
669
- container.innerHTML = '';
670
- document.getElementById('layoutSelect').value = this.state.columns;
 
671
 
672
- for(let i=0; i < this.state.columns; i++) {
673
- const colDiv = document.createElement('div');
674
- colDiv.className = 'grid-column';
675
- colDiv.id = `col-${i}`;
676
 
677
- // Add existing charts
678
- const chartIds = this.state.layout[i] || [];
 
 
 
 
 
 
 
 
 
 
679
  chartIds.forEach(id => {
680
- colDiv.appendChild(this.createChartDOM(id));
681
- // Defer plot to allow DOM insertion
682
- setTimeout(() => ChartEngine.plot(id), 50);
 
 
 
 
 
 
 
 
 
 
683
  });
684
 
685
  // Add Button
686
- const btn = document.createElement('div');
687
- btn.className = 'col-add-btn';
688
- btn.innerHTML = '+ Add Chart';
689
- btn.onclick = () => this.addChart(i);
690
- colDiv.appendChild(btn);
691
-
692
- container.appendChild(colDiv);
693
- }
694
- },
695
-
696
- addChart(colIndex) {
697
- const id = 'c-' + Date.now() + Math.random().toString(36).substr(2,4);
698
- this.state.charts[id] = { config: null, instance: null };
699
-
700
- if(!this.state.layout[colIndex]) this.state.layout[colIndex] = [];
701
- this.state.layout[colIndex].push(id);
702
-
703
- const colDiv = document.getElementById(`col-${colIndex}`);
704
- const btn = colDiv.querySelector('.col-add-btn');
705
- colDiv.insertBefore(this.createChartDOM(id), btn);
706
-
707
- this.saveState();
708
- },
709
 
710
- removeChart(id) {
711
- if(!confirm("Remove this chart?")) return;
712
-
713
- // Find column
714
- let colIndex = -1;
715
- this.state.layout.forEach((ids, idx) => {
716
- if(ids.includes(id)) colIndex = idx;
717
  });
718
-
719
- if(colIndex > -1) {
720
- this.state.layout[colIndex] = this.state.layout[colIndex].filter(x => x !== id);
721
- }
722
-
723
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
724
- delete this.state.charts[id];
725
-
726
- document.getElementById(id).remove();
727
- this.saveState();
728
  },
729
 
730
- createChartDOM(id) {
731
  const div = document.createElement('div');
732
  div.className = 'chart-card';
733
- div.id = id;
734
 
735
- const chartData = this.state.charts[id];
736
- const isConfigured = chartData && chartData.config;
737
-
738
- if(!isConfigured) {
739
- div.innerHTML = `
740
- <div class="empty-state">
741
- <button class="btn-config" onclick="ConfigModal.open('${id}')">
742
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
743
- Configure Symbol
744
- </button>
745
  </div>
746
- <div class="cc-actions" style="position:absolute; top:8px; right:8px;">
747
- <button class="icon-btn red" onclick="App.removeChart('${id}')"></button>
 
748
  </div>
749
- `;
750
- } else {
751
- const cfg = chartData.config;
752
- div.innerHTML = `
753
- <div class="cc-header">
754
- <div class="cc-title">
755
- <span>${cfg.root}</span>
756
- ${cfg.expiry}
757
- <div class="cc-meta">${cfg.instrument} • ${cfg.timeframe}</div>
758
- </div>
759
- <div class="cc-actions">
760
- <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
761
- <button class="icon-btn red" onclick="App.removeChart('${id}')">✕</button>
762
  </div>
763
- </div>
764
- <div class="cc-body">
765
- <canvas id="canvas-${id}"></canvas>
766
- </div>
767
- `;
768
- }
769
  return div;
770
  },
771
 
772
- refreshAllCharts() {
773
- const allIds = this.state.layout.flat();
774
- allIds.forEach(id => ChartEngine.plot(id, true));
 
 
 
 
 
 
775
  },
776
 
777
- liveTick() {
778
- if(this.state.isLive) {
779
- this.refreshAllCharts();
 
 
 
 
 
 
780
  }
 
 
781
  },
782
 
783
- saveState() {
784
- const minimalLayout = this.state.layout;
785
- const minimalCharts = {};
786
- Object.keys(this.state.charts).forEach(id => {
787
- minimalCharts[id] = { config: this.state.charts[id].config };
788
- });
789
-
790
- const data = {
791
- date: this.state.date,
792
- columns: this.state.columns,
793
- layout: minimalLayout,
794
- charts: minimalCharts
795
- };
796
- localStorage.setItem('DepthChainV3', JSON.stringify(data));
797
  },
798
 
799
- loadState() {
800
- try {
801
- const raw = localStorage.getItem('DepthChainV3');
802
- if(raw) {
803
- const data = JSON.parse(raw);
804
- if(data.columns) this.state.columns = data.columns;
805
- if(data.layout) this.state.layout = data.layout;
806
- // Merge charts logic to handle instances
807
- if(data.charts) {
808
- Object.keys(data.charts).forEach(id => {
809
- this.state.charts[id] = { config: data.charts[id].config, instance: null };
810
- });
811
- }
812
- // Check if date from storage is valid, else keep default
813
- if(data.date) {
814
- const sel = document.getElementById('globalDate');
815
- // Only set if option exists
816
- if([...sel.options].some(o => o.value === data.date)) {
817
- sel.value = data.date;
818
- this.state.date = data.date;
819
- }
820
- }
821
- } else {
822
- // Initialize default Layout: 3 Columns, 1 empty chart in each
823
- this.addChart(0);
824
- this.addChart(1);
825
- this.addChart(2);
826
- }
827
- } catch(e) { console.error("Load state error", e); }
828
  },
829
 
830
- resetAll() {
831
- if(confirm("Reset entire workspace?")) {
832
- localStorage.removeItem('DepthChainV3');
833
- location.reload();
834
- }
835
  },
836
 
837
- showToast(msg) {
838
- const t = document.getElementById('toast');
839
- t.innerText = msg;
840
- t.classList.add('show');
841
- setTimeout(() => t.classList.remove('show'), 2000);
 
 
842
  }
843
  };
844
 
845
  /**
846
- * --- CHART ENGINE ---
847
  */
848
- const ChartEngine = {
849
- async plot(id, isUpdate = false) {
850
- const chartObj = App.state.charts[id];
851
- if(!chartObj || !chartObj.config) return;
852
-
853
- const cfg = chartObj.config;
854
 
855
- // Live Mode Time override
 
 
 
 
 
 
856
  let endTime = cfg.end;
857
- if(App.state.isLive) {
858
  const now = new Date();
 
859
  endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
860
  }
861
 
@@ -875,154 +822,143 @@ const ChartEngine = {
875
  });
876
  const data = await res.json();
877
 
878
- if(data.error || !data.labels || data.labels.length === 0) {
879
- // Keep old data if live, else show error?
880
- // For smooth UX, we just don't update if live fetch fails/empty
 
 
 
 
 
 
881
  return;
882
  }
883
 
884
- // Calculations
 
 
 
 
 
 
 
 
 
 
 
885
  const datasets = cfg.series.map(s => {
886
- const computed = data.labels.map((_, i) => {
887
- const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
888
- const B = data.B[i] || 0, S = data.S[i] || 0;
889
- let f = s.formula
890
- .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
891
- .replace(/\$B/g, B).replace(/\$S/g, S);
892
- try { return new Function('return ' + f)(); } catch { return null; }
893
  });
894
-
895
  return {
896
- label: s.label,
897
- data: computed,
898
- borderColor: s.color,
899
- backgroundColor: s.color,
900
- borderWidth: 1.5,
901
- pointRadius: 0,
902
- pointHoverRadius: 4,
903
- yAxisID: s.axis === 'left' ? 'y' : 'y1',
904
- tension: 0.2
905
  };
906
  });
907
 
908
- const ctx = document.getElementById(`canvas-${id}`);
909
- if(!ctx) return;
910
-
911
- // Reuse Instance or Create New
912
- if(chartObj.instance) {
913
- chartObj.instance.data.labels = data.labels;
914
- chartObj.instance.data.datasets = datasets;
915
- chartObj.instance.update('none'); // No animation for performance
916
- } else {
917
- const hasRight = datasets.some(d => d.yAxisID === 'y1');
918
- chartObj.instance = new Chart(ctx.getContext('2d'), {
919
- type: 'line',
920
- data: { labels: data.labels, datasets },
921
- options: {
922
- responsive: true,
923
- maintainAspectRatio: false,
924
- animation: false,
925
- interaction: { mode: 'index', intersect: false },
926
- plugins: {
927
- legend: { display: false },
928
- tooltip: {
929
- backgroundColor: 'rgba(24, 24, 27, 0.9)',
930
- titleColor: '#fff',
931
- bodyColor: '#ccc',
932
- borderColor: '#333',
933
- borderWidth: 1,
934
- padding: 10
935
- }
936
- },
937
- scales: {
938
- x: {
939
- grid: { color: '#2d2d32', tickLength: 0 },
940
- ticks: { color: '#70707b', maxTicksLimit: 8, font:{size:10} }
941
- },
942
- y: {
943
- type: 'linear', display: true, position: 'left',
944
- grid: { color: '#2d2d32' },
945
- ticks: { color: '#70707b', font:{size:10} }
946
- },
947
- y1: {
948
- type: 'linear', display: hasRight, position: 'right',
949
- grid: { display: false },
950
- ticks: { color: '#70707b', font:{size:10} }
951
- }
952
  }
 
 
 
 
 
953
  }
954
- });
955
- }
956
-
957
- } catch(e) {
958
- console.error(e);
959
  }
960
  }
961
  };
962
 
963
  /**
964
- * --- CONFIGURATION MODAL ---
965
  */
966
- const ConfigModal = {
967
  targetId: null,
968
  isGlobal: false,
969
 
970
  async open(id) {
971
  this.targetId = id;
972
  this.isGlobal = false;
973
- document.getElementById('modalTitle').innerText = "Configure Symbol";
974
  document.getElementById('configModal').style.display = 'flex';
 
 
 
975
  document.getElementById('cfgLoading').style.display = 'block';
976
  document.getElementById('cfgContent').style.display = 'none';
977
 
978
- // Load Roots
979
- await this.loadRoots();
980
-
981
- // Fill Current Config
982
- const chart = App.state.charts[id];
983
- const cfg = chart.config || {};
984
 
985
- if(cfg.root) {
986
- document.getElementById('cfgRoot').value = cfg.root;
987
- await this.onRootChange(cfg.expiry, cfg.instrument);
988
- }
989
-
990
  document.getElementById('cfgTF').value = cfg.timeframe || '1min';
991
  document.getElementById('cfgStart').value = cfg.start || '09:15';
992
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
993
 
994
- // Series
995
- const series = cfg.series || JSON.parse(JSON.stringify(App.defaults));
996
- this.renderSeriesList(series);
997
 
 
 
 
 
 
 
 
998
  document.getElementById('cfgLoading').style.display = 'none';
999
  document.getElementById('cfgContent').style.display = 'block';
1000
  },
1001
 
1002
  openDefaults() {
1003
  this.isGlobal = true;
1004
- document.getElementById('modalTitle').innerText = "Global Default Settings";
1005
  document.getElementById('configModal').style.display = 'flex';
 
1006
  document.getElementById('cfgLoading').style.display = 'none';
1007
  document.getElementById('cfgContent').style.display = 'block';
1008
 
1009
- // Hide symbol specific fields for global config (Visual hack)
1010
- // Ideally we structure HTML differently, but this works for single file
1011
- // For now, we show series list only essentially.
1012
-
1013
- this.renderSeriesList(App.defaults);
1014
  },
1015
 
1016
  close() { document.getElementById('configModal').style.display = 'none'; },
1017
 
1018
- async loadRoots() {
1019
- const sel = document.getElementById('cfgRoot');
1020
- sel.innerHTML = '<option>Loading...</option>';
1021
- try {
1022
- const res = await fetch(`/api/roots?date=${App.state.date}`);
1023
- const roots = await res.json();
1024
- sel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
1025
- } catch(e) { sel.innerHTML = '<option>Error</option>'; }
1026
  },
1027
 
1028
  async onRootChange(preExp, preInst) {
@@ -1056,82 +992,84 @@ const ConfigModal = {
1056
  const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
1057
  const insts = await res.json();
1058
  iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
1059
-
1060
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
1061
  },
1062
 
1063
- renderSeriesList(series) {
1064
- const cont = document.getElementById('cfgSeriesList');
1065
- cont.innerHTML = '';
1066
- series.forEach(s => this.addSeriesRowUI(s));
 
 
 
 
 
 
 
 
 
 
 
1067
  },
1068
 
1069
  addSeriesRow() {
1070
- this.addSeriesRowUI({formula:'$P', color:'#ffffff', label:'New', axis:'left'});
1071
- },
1072
-
1073
- addSeriesRowUI(s) {
1074
- const cont = document.getElementById('cfgSeriesList');
1075
- const div = document.createElement('div');
1076
- div.className = 'series-row';
1077
- div.innerHTML = `
1078
- <input type="color" value="${s.color}" class="s-color">
1079
- <input type="text" value="${s.formula}" class="s-formula">
1080
- <input type="text" value="${s.label}" class="s-label">
1081
- <select class="s-axis">
1082
- <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1083
- <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1084
- </select>
1085
- <div class="remove-series" onclick="this.parentElement.remove()">✕</div>
1086
  `;
1087
- cont.appendChild(div);
1088
  },
1089
 
1090
  save() {
1091
- const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1092
- const newSeries = Array.from(rows).map(r => ({
1093
  color: r.querySelector('.s-color').value,
1094
- formula: r.querySelector('.s-formula').value,
1095
- label: r.querySelector('.s-label').value,
1096
  axis: r.querySelector('.s-axis').value
1097
  }));
1098
 
1099
  if(this.isGlobal) {
1100
- App.defaults = newSeries;
1101
- App.showToast("Global Defaults Saved");
1102
  } else {
1103
  const root = document.getElementById('cfgRoot').value;
1104
  const exp = document.getElementById('cfgExp').value;
1105
  const inst = document.getElementById('cfgInst').value;
1106
 
1107
- if(!root || !exp || !inst) { alert("Please complete symbol selection."); return; }
1108
 
1109
  const config = {
1110
  root, expiry: exp, instrument: inst,
1111
  timeframe: document.getElementById('cfgTF').value,
1112
  start: document.getElementById('cfgStart').value,
1113
  end: document.getElementById('cfgEnd').value,
1114
- series: newSeries
1115
  };
1116
 
1117
  App.state.charts[this.targetId].config = config;
1118
- // Update UI card (Rebuild DOM)
1119
- const oldCard = document.getElementById(this.targetId);
1120
- const newCard = App.createChartDOM(this.targetId);
1121
- oldCard.replaceWith(newCard);
1122
-
1123
- App.saveState();
1124
- // Plot
1125
- setTimeout(() => ChartEngine.plot(this.targetId), 50);
1126
- App.showToast("Chart Configured");
1127
  }
1128
  this.close();
 
 
 
 
 
 
 
1129
  }
1130
  };
1131
 
1132
- // Start
1133
  window.onload = () => App.init();
1134
-
1135
  </script>
1136
  </body>
1137
  </html>
 
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:
 
205
 
206
 
207
  # ==========================================
208
+ # 5. FRONTEND TEMPLATE (NEW UI)
209
  # ==========================================
210
 
211
  HTML_TEMPLATE = """
 
213
  <html lang="en">
214
  <head>
215
  <meta charset="UTF-8">
216
+ <title>DepthChain PRO</title>
217
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
219
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
220
  <style>
221
+ /* --- THEME & RESET --- */
222
  :root {
223
+ /* Apple/Github Dark Theme Vibe */
224
+ --bg-body: #0d1117;
225
+ --bg-header: #161b22;
226
+ --bg-card: #0d1117;
227
+ --bg-panel: #21262d;
228
+ --bg-input: #161b22;
229
 
230
+ --border: #30363d;
231
+ --border-hover: #8b949e;
 
232
 
233
+ --text-primary: #e6edf3;
234
+ --text-secondary: #8b949e;
235
 
236
+ --accent: #2f81f7;
237
+ --accent-hover: #58a6ff;
238
+ --success: #238636;
239
+ --danger: #da3633;
240
+ --warning: #d29922;
241
 
242
+ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
243
  --font-mono: 'JetBrains Mono', monospace;
244
  }
245
 
 
246
  * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
247
+
248
+ body {
249
+ background: var(--bg-body);
250
+ color: var(--text-primary);
251
+ font-family: var(--font-main);
252
+ display: flex; flex-direction: column;
253
+ height: 100vh; overflow: hidden;
 
 
 
 
 
 
254
  font-size: 13px;
255
  }
256
 
257
+ /* --- SCROLLBAR --- */
258
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
259
+ ::-webkit-scrollbar-corner { background: var(--bg-body); }
260
+ ::-webkit-scrollbar-track { background: var(--bg-body); }
261
+ ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
262
+ ::-webkit-scrollbar-thumb:hover { background: var(--border); }
263
+
264
  /* --- HEADER --- */
265
+ .top-bar {
266
+ height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
267
+ display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
268
+ z-index: 50; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
 
 
 
 
 
 
269
  }
270
+
271
+ .brand {
272
+ font-weight: 700; font-size: 15px; color: var(--text-primary);
273
+ display: flex; align-items: center; gap: 8px; letter-spacing: -0.3px;
 
 
274
  }
275
+ .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
276
 
277
  .controls { display: flex; align-items: center; gap: 12px; }
278
 
279
+ /* Form Elements */
280
+ select, button, input {
281
+ background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
282
+ padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; font-family: inherit;
283
+ transition: all 0.15s ease; cursor: pointer; display: flex; align-items: center; justify-content: center;
 
 
 
 
 
 
284
  }
285
+ select:hover, button:hover, input:hover { border-color: var(--border-hover); background: var(--bg-panel); }
286
+ select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.2); }
 
 
 
287
 
288
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
289
+ button.primary:hover { background: var(--accent-hover); }
290
+ button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
291
+ button.danger:hover { border-color: var(--danger); background: rgba(218, 54, 51, 0.1); }
292
+
293
+ .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; height: 32px; align-items: center;}
294
+ .layout-btn {
295
+ width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary);
296
+ border-radius: 4px; font-weight: 700; font-size: 11px; padding: 0;
297
  }
298
+ .layout-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
299
+ .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
 
 
 
 
300
 
301
+ /* --- MAIN GRID LAYOUT --- */
302
+ /* Page Scroll Container */
303
+ .main-container {
304
  flex: 1;
305
+ display: flex; /* Row direction */
306
+ overflow-y: auto; /* Vertical scroll for the WHOLE page if needed */
307
+ overflow-x: hidden;
 
308
  }
309
 
310
+ .column {
311
  flex: 1;
312
  display: flex;
313
  flex-direction: column;
314
+ border-right: 1px solid var(--border);
315
+ min-width: 0; /* Flexbox fix */
316
+ /* Height is determined by children. If children exceed viewport, this column grows, causing main-container to scroll. */
 
 
317
  }
318
+ .column:last-child { border-right: none; }
319
+
320
+ .add-chart-btn {
321
+ height: 40px; border: none; border-top: 1px dashed var(--border);
322
+ background: transparent; color: var(--text-secondary); width: 100%;
323
+ font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
324
+ transition: 0.2s; flex-shrink: 0;
325
+ }
326
+ .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
327
 
328
  /* --- CHART CARD --- */
329
  .chart-card {
330
  background: var(--bg-card);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  border-bottom: 1px solid var(--border);
332
+ display: flex; flex-direction: column;
333
+ position: relative;
334
+ /* Height is set dynamically by JS */
 
 
335
  }
336
 
337
+ .card-header {
338
+ height: 38px; border-bottom: 1px solid var(--border);
339
+ display: flex; align-items: center; justify-content: space-between;
340
+ padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
341
+ }
342
+
343
+ .card-info { display: flex; gap: 8px; align-items: center; font-size: 12px; font-weight: 600; }
344
+ .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
345
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); transition: 0.3s; }
346
 
347
+ /* Status States */
348
+ .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
349
+ .status-dot.hist { background: var(--accent); }
350
+ .status-dot.retry { background: var(--warning); animation: flash 0.5s infinite; }
351
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
352
+ @keyframes flash { 0% { opacity: 1; } 50% { opacity: 0.2; } 100% { opacity: 1; } }
353
+
354
+ .card-tools { display: flex; gap: 4px; }
355
+ .tool-btn {
356
+ width: 24px; height: 24px; border: none; background: transparent;
357
+ color: var(--text-secondary); border-radius: 4px; display: flex; justify-content: center; align-items: center;
358
+ }
359
+ .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
360
 
361
+ .card-body {
362
+ flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden;
363
+ display: flex; flex-direction: column;
 
 
364
  }
 
 
 
 
365
 
366
  /* Empty State */
367
  .empty-state {
368
+ display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
369
+ color: var(--text-secondary); gap: 12px;
370
+ background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-card) 100%);
 
 
 
 
371
  }
372
+ .btn-ghost {
373
+ background: transparent; border: 1px solid var(--border); color: var(--text-primary);
374
+ padding: 6px 16px; font-size: 12px; opacity: 0.7;
 
 
 
 
375
  }
376
+ .btn-ghost:hover { opacity: 1; border-color: var(--text-secondary); background: var(--bg-panel); }
377
 
378
+ /* --- MODALS --- */
379
+ .modal-backdrop {
380
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
381
+ background: rgba(0,0,0,0.7); backdrop-filter: blur(2px); z-index: 100;
382
+ display: none; justify-content: center; align-items: center;
383
  }
384
  .modal {
385
+ background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px;
386
  width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
387
+ box-shadow: 0 24px 48px rgba(0,0,0,0.6); animation: slideUp 0.2s ease-out;
388
  }
389
+ @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
390
+
391
+ .modal-head {
392
+ padding: 16px; border-bottom: 1px solid var(--border);
393
+ display: flex; justify-content: space-between; align-items: center; font-weight: 700; font-size: 14px;
394
  }
 
395
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
396
+ .modal-foot {
397
+ padding: 16px; border-top: 1px solid var(--border);
398
+ display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); border-radius: 0 0 10px 10px;
399
  }
400
 
401
+ .form-section { display: flex; flex-direction: column; gap: 6px; }
402
+ .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; letter-spacing: 0.5px; }
403
+ .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
 
 
 
 
404
 
405
  /* Series List */
406
+ .series-list { display: flex; flex-direction: column; gap: 6px; }
407
  .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; }
408
+ .color-input { padding: 0; width: 100%; border: none; background: none; height: 28px; cursor: pointer; }
409
+
 
 
410
  /* Toast */
411
  .toast {
412
+ position: fixed; bottom: 24px; right: 24px; background: var(--bg-header); border: 1px solid var(--success);
413
+ color: var(--success); padding: 12px 24px; border-radius: 6px; font-weight: 600;
414
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5); transform: translateY(100px); transition: 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
415
+ display: flex; align-items: center; gap: 8px;
416
  }
417
+ .toast.show { transform: translateY(0); }
 
418
 
419
  </style>
420
  </head>
421
  <body>
422
 
423
+ <div class="top-bar">
424
+ <div class="brand">
425
+ DepthChain <span>PRO</span>
 
 
 
 
426
  </div>
427
 
428
  <div class="controls">
429
+ <!-- Date -->
430
+ <select id="globalDate" onchange="App.onDateChange()" style="min-width: 140px;"></select>
431
 
432
+ <!-- Live Toggle -->
433
+ <div style="position: relative;">
434
+ <select id="globalMode" onchange="App.onModeChange()" style="padding-left: 28px; font-weight: 600;">
435
+ <option value="LIVE">LIVE</option>
436
+ <option value="HIST">HISTORY</option>
437
+ </select>
438
+ <div id="modeDot" class="status-dot live" style="position: absolute; left: 10px; top: 12px; pointer-events: none;"></div>
439
  </div>
440
 
441
+ <div style="width:1px; height: 20px; background: var(--border);"></div>
442
 
443
+ <!-- Layout -->
444
+ <div class="layout-toggle">
445
+ <button class="layout-btn" onclick="App.setLayout(1)" title="1 Column">1</button>
446
+ <button class="layout-btn" onclick="App.setLayout(2)" title="2 Columns">2</button>
447
+ <button class="layout-btn active" onclick="App.setLayout(3)" title="3 Columns">3</button>
448
+ <button class="layout-btn" onclick="App.setLayout(4)" title="4 Columns">4</button>
449
+ </div>
450
 
451
+ <button onclick="Config.openDefaults()">Settings</button>
452
+ <button class="danger" onclick="App.reset()">Reset</button>
453
  </div>
454
+ </div>
455
 
456
+ <div class="main-container" id="gridRoot">
 
457
  <!-- Columns injected by JS -->
458
  </div>
459
 
460
+ <!-- CONFIG MODAL -->
461
+ <div class="modal-backdrop" id="configModal">
462
  <div class="modal">
463
+ <div class="modal-head">
464
+ <span id="cfgTitle">Configure Chart</span>
465
+ <button class="tool-btn" onclick="Config.close()">✕</button>
466
  </div>
467
  <div class="modal-body">
468
+ <div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading Metadata...</div>
 
 
 
469
  <div id="cfgContent">
470
+ <div class="row-2">
471
+ <div class="form-section">
472
  <label>Symbol</label>
473
+ <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
474
  </div>
475
+ <div class="form-section">
476
  <label>Expiry</label>
477
+ <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
478
  </div>
479
  </div>
480
+
481
+ <div class="form-section">
482
  <label>Instrument</label>
483
+ <select id="cfgInst" style="width:100%"></select>
484
  </div>
485
 
486
+ <div class="row-2">
487
+ <div class="form-section">
488
  <label>Timeframe</label>
489
+ <select id="cfgTF" style="width:100%">
490
  <option value="1min">1 Minute</option>
491
  <option value="3min">3 Minutes</option>
492
  <option value="5min">5 Minutes</option>
 
495
  <option value="1hour">1 Hour</option>
496
  </select>
497
  </div>
498
+ <div class="form-section">
499
+ <label>Time Range</label>
500
  <div style="display:flex; gap:8px;">
501
  <input type="time" id="cfgStart" value="09:15">
502
  <input type="time" id="cfgEnd" value="15:30">
 
504
  </div>
505
  </div>
506
 
507
+ <div style="height:1px; background:var(--border); margin: 8px 0;"></div>
508
 
509
+ <div class="form-section">
510
+ <label>Plot Series ($P, $B, $S, $V, $OI)</label>
511
+ <div class="series-list" id="cfgSeries"></div>
512
+ <button class="btn-ghost" style="width:100%; margin-top:8px;" onclick="Config.addSeriesRow()">+ Add Line</button>
513
  </div>
514
  </div>
515
  </div>
516
+ <div class="modal-foot">
517
+ <button class="btn-ghost" onclick="Config.close()">Cancel</button>
518
+ <button class="primary" onclick="Config.save()">Save Configuration</button>
519
  </div>
520
  </div>
521
  </div>
522
 
523
+ <div id="toast" class="toast">
524
+ <span style="font-size:16px;">✓</span> <span id="toastMsg">Saved</span>
525
+ </div>
526
 
527
  <script>
528
  /**
529
+ * --- APP CONTROLLER ---
530
  */
531
  const App = {
532
  state: {
533
+ date: null,
534
+ mode: 'LIVE',
535
  columns: 3,
536
+ // Layout is array of arrays containing chart IDs: [[id1, id2], [id3], ...]
537
+ layout: [[], [], []],
538
+ charts: {} // id -> { config: {}, instance: ChartObj, interval: null }
539
  },
540
+
541
  defaults: [
542
+ { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
543
+ { formula: '$B', color: '#238636', label: 'Buy', axis: 'right' },
544
+ { formula: '$S', color: '#da3633', label: 'Sell', axis: 'right' }
545
  ],
546
 
547
  async init() {
548
  await this.loadDates();
 
 
549
 
550
+ // Restore State
551
+ const saved = localStorage.getItem('dc_pro_v3');
552
+ if (saved) {
553
+ try {
554
+ const s = JSON.parse(saved);
555
+ this.state.date = s.date || this.state.date;
556
+ this.state.mode = s.mode || 'LIVE';
557
+ this.state.columns = s.columns || 3;
558
+
559
+ // Rehydrate Layout
560
+ this.state.layout = Array.from({length: this.state.columns}, () => []);
561
+ s.layout.flat().forEach((cData, i) => {
562
+ // Redistribute flattened list to current columns
563
+ const colIdx = i % this.state.columns;
564
+ const id = cData.id;
565
+ this.state.layout[colIdx].push(id);
566
+ this.state.charts[id] = { config: cData.config, instance: null, interval: null };
567
+ });
568
+ } catch(e) { this.initDefault(); }
569
+ } else {
570
+ this.initDefault();
571
+ }
572
+
573
+ // Sync UI
574
+ const dSel = document.getElementById('globalDate');
575
+ if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) {
576
+ dSel.value = this.state.date;
577
+ } else if (dSel.options.length > 0) {
578
+ this.state.date = dSel.options[0].value; // fallback
579
+ }
580
+
581
+ document.getElementById('globalMode').value = this.state.mode;
582
+ this.updateModeUI();
583
+
584
+ this.renderGrid();
585
+ this.startLoop();
586
+ },
587
+
588
+ initDefault() {
589
+ this.state.columns = 3;
590
+ this.state.layout = [[], [], []];
591
+ // 3 cols, 1 empty chart each to start
592
+ for(let i=0; i<3; i++) this.addChartInternal(i);
593
  },
594
 
595
  async loadDates() {
 
598
  const dates = await res.json();
599
  const sel = document.getElementById('globalDate');
600
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
601
+ if(!this.state.date && dates.length) this.state.date = dates[0];
602
+ } catch(e) { console.error(e); }
 
 
 
 
 
603
  },
604
 
605
+ // Adds to state, doesn't render
606
+ addChartInternal(colIdx) {
607
+ const id = 'c-' + Math.random().toString(36).substr(2, 6);
608
+ this.state.layout[colIdx].push(id);
609
+ this.state.charts[id] = { config: {}, instance: null, interval: null };
610
+ return id;
 
 
 
 
 
 
 
 
 
 
611
  },
612
 
613
+ save() {
614
+ // Flatten layout for storage
615
+ const flat = this.state.layout.flat().map(id => ({
616
+ id: id,
617
+ config: this.state.charts[id].config
618
+ }));
619
+
620
+ const dump = {
621
+ date: this.state.date,
622
+ mode: this.state.mode,
623
+ columns: this.state.columns,
624
+ layout: flat // We store flat, reconstruct on load
625
+ };
626
+ localStorage.setItem('dc_pro_v3', JSON.stringify(dump));
627
  },
628
 
629
+ reset() {
630
+ if(confirm("Reset entire workspace?")) {
631
+ localStorage.removeItem('dc_pro_v3');
632
+ location.reload();
 
 
 
 
 
 
 
 
 
633
  }
634
  },
635
 
636
+ // --- GRID RENDERING ---
637
+ setLayout(n) {
638
+ // Redistribute charts
639
+ const all = this.state.layout.flat();
640
+ this.state.columns = n;
641
+ this.state.layout = Array.from({length: n}, () => []);
642
+ all.forEach((id, i) => this.state.layout[i % n].push(id));
643
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  this.renderGrid();
645
+ this.save();
646
  },
647
 
648
  renderGrid() {
649
+ // Update Layout Buttons
650
+ document.querySelectorAll('.layout-btn').forEach((b, i) => {
651
+ b.classList.toggle('active', (i+1) === this.state.columns);
652
+ });
653
 
654
+ const container = document.getElementById('gridRoot');
655
+ container.innerHTML = '';
 
 
656
 
657
+ this.state.layout.forEach((chartIds, colIdx) => {
658
+ const col = document.createElement('div');
659
+ col.className = 'column';
660
+ col.id = `col-${colIdx}`;
661
+
662
+ // Calculate height logic
663
+ // Header is ~52px. Viewport ~ 100vh.
664
+ // If charts <= 3, divide height equally (fit screen).
665
+ // If charts > 3, use fixed height (scroll page).
666
+ const count = chartIds.length;
667
+ const viewH = window.innerHeight - 52;
668
+
669
  chartIds.forEach(id => {
670
+ const card = this.createCardDOM(id);
671
+ // Dynamic Height Styling
672
+ if (count <= 3 && count > 0) {
673
+ // Fit to screen (minus borders approx)
674
+ card.style.height = `calc(${viewH / count}px - 1px)`;
675
+ } else {
676
+ // Scroll mode
677
+ card.style.height = `300px`;
678
+ }
679
+ col.appendChild(card);
680
+
681
+ // Plot if configured
682
+ setTimeout(() => ChartLogic.plot(id), 0);
683
  });
684
 
685
  // Add Button
686
+ const addBtn = document.createElement('button');
687
+ addBtn.className = 'add-chart-btn';
688
+ addBtn.innerText = '+ Add Chart';
689
+ addBtn.onclick = () => {
690
+ const newId = this.addChartInternal(colIdx);
691
+ this.renderGrid(); // Re-render to recalc heights
692
+ this.save();
693
+ };
694
+ col.appendChild(addBtn);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
+ container.appendChild(col);
 
 
 
 
 
 
697
  });
 
 
 
 
 
 
 
 
 
 
698
  },
699
 
700
+ createCardDOM(id) {
701
  const div = document.createElement('div');
702
  div.className = 'chart-card';
703
+ div.id = `card-${id}`;
704
 
705
+ const cfg = this.state.charts[id].config;
706
+ const isConfigured = cfg && cfg.root;
707
+
708
+ div.innerHTML = `
709
+ <div class="card-header">
710
+ <div class="card-info">
711
+ <div class="status-dot ${isConfigured ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
712
+ <span>${isConfigured ? cfg.root : 'New Chart'}</span>
713
+ ${isConfigured ? `<span class="card-meta">${cfg.expiry}</span>` : ''}
 
714
  </div>
715
+ <div class="card-tools">
716
+ <button class="tool-btn" onclick="Config.open('${id}')"></button>
717
+ <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
718
  </div>
719
+ </div>
720
+ <div class="card-body" id="body-${id}">
721
+ ${!isConfigured ? `
722
+ <div class="empty-state">
723
+ <div style="font-size:32px; opacity:0.2;">📊</div>
724
+ <button class="btn-ghost" onclick="Config.open('${id}')">Configure Chart</button>
 
 
 
 
 
 
 
725
  </div>
726
+ ` : `<canvas id="canvas-${id}"></canvas>`}
727
+ </div>
728
+ `;
 
 
 
729
  return div;
730
  },
731
 
732
+ removeChart(id) {
733
+ if(!confirm("Remove chart?")) return;
734
+ // Find and remove
735
+ this.state.layout = this.state.layout.map(col => col.filter(x => x !== id));
736
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
737
+ delete this.state.charts[id];
738
+
739
+ this.renderGrid();
740
+ this.save();
741
  },
742
 
743
+ // --- CONTROLS ---
744
+ onDateChange() {
745
+ this.state.date = document.getElementById('globalDate').value;
746
+ const today = new Date().toISOString().split('T')[0];
747
+ // Auto live if today
748
+ if (this.state.date === today && this.state.mode !== 'LIVE') {
749
+ this.state.mode = 'LIVE';
750
+ document.getElementById('globalMode').value = 'LIVE';
751
+ this.updateModeUI();
752
  }
753
+ this.save();
754
+ this.refreshAll();
755
  },
756
 
757
+ onModeChange() {
758
+ this.state.mode = document.getElementById('globalMode').value;
759
+ this.updateModeUI();
760
+ this.save();
761
+ this.refreshAll();
 
 
 
 
 
 
 
 
 
762
  },
763
 
764
+ updateModeUI() {
765
+ const dot = document.getElementById('modeDot');
766
+ if(this.state.mode === 'LIVE') {
767
+ dot.className = 'status-dot live';
768
+ } else {
769
+ dot.className = 'status-dot hist';
770
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  },
772
 
773
+ refreshAll() {
774
+ Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
 
 
 
775
  },
776
 
777
+ startLoop() {
778
+ // Global 15s timer for LIVE mode
779
+ setInterval(() => {
780
+ if(this.state.mode === 'LIVE') {
781
+ Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
782
+ }
783
+ }, 15000);
784
  }
785
  };
786
 
787
  /**
788
+ * --- CHART LOGIC ---
789
  */
790
+ const ChartLogic = {
791
+ async plot(id, isUpdate=false) {
792
+ const chart = App.state.charts[id];
793
+ if(!chart || !chart.config.root) return;
 
 
794
 
795
+ const cfg = chart.config;
796
+ const dot = document.getElementById(`dot-${id}`);
797
+
798
+ // Live UI Feedback
799
+ if(isUpdate && dot) dot.classList.add('retry');
800
+
801
+ // Logic for "End Time" in Live Mode
802
  let endTime = cfg.end;
803
+ if (App.state.mode === 'LIVE') {
804
  const now = new Date();
805
+ // Just use current time HH:MM
806
  endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
807
  }
808
 
 
822
  });
823
  const data = await res.json();
824
 
825
+ // Handle No Data
826
+ if(data.error || !data.labels || !data.labels.length) {
827
+ if(App.state.mode !== 'LIVE') {
828
+ // Show message if history
829
+ if(chart.instance) chart.instance.destroy();
830
+ document.getElementById(`body-${id}`).innerHTML =
831
+ '<div class="empty-state" style="opacity:0.5">No Data</div>';
832
+ }
833
+ if(dot) dot.classList.remove('retry');
834
  return;
835
  }
836
 
837
+ // Ensure Canvas Exists
838
+ const body = document.getElementById(`body-${id}`);
839
+ if(!body.querySelector('canvas')) {
840
+ body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
841
+ }
842
+
843
+ // Restore Dot
844
+ if(dot) {
845
+ dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
846
+ }
847
+
848
+ // Map Data
849
  const datasets = cfg.series.map(s => {
850
+ const vals = data.labels.map((_, i) => {
851
+ 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;
852
+ let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
853
+ try { return new Function('return '+f)(); } catch{ return null; }
 
 
 
854
  });
 
855
  return {
856
+ label: s.label, data: vals,
857
+ borderColor: s.color, backgroundColor: s.color,
858
+ borderWidth: 1.5, pointRadius: 0, hitRadius: 10,
859
+ tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
 
860
  };
861
  });
862
 
863
+ this.render(id, data.labels, datasets);
864
+
865
+ } catch(e) { console.error(e); }
866
+ },
867
+
868
+ render(id, labels, datasets) {
869
+ const ctx = document.getElementById(`canvas-${id}`);
870
+ if(!ctx) return;
871
+ const cData = App.state.charts[id];
872
+
873
+ if(cData.instance) {
874
+ cData.instance.data.labels = labels;
875
+ cData.instance.data.datasets = datasets;
876
+ cData.instance.update('none'); // Silent update
877
+ } else {
878
+ const hasRight = datasets.some(d => d.yAxisID === 'y1');
879
+ cData.instance = new Chart(ctx.getContext('2d'), {
880
+ type: 'line',
881
+ data: { labels, datasets },
882
+ options: {
883
+ responsive: true, maintainAspectRatio: false, animation: false,
884
+ interaction: { mode: 'index', intersect: false },
885
+ plugins: {
886
+ legend: { display: false },
887
+ tooltip: {
888
+ backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3',
889
+ borderColor: '#30363d', borderWidth: 1, padding: 10, cornerRadius: 6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  }
891
+ },
892
+ scales: {
893
+ x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
894
+ y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
895
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
896
  }
897
+ }
898
+ });
 
 
 
899
  }
900
  }
901
  };
902
 
903
  /**
904
+ * --- CONFIG MODAL ---
905
  */
906
+ const Config = {
907
  targetId: null,
908
  isGlobal: false,
909
 
910
  async open(id) {
911
  this.targetId = id;
912
  this.isGlobal = false;
 
913
  document.getElementById('configModal').style.display = 'flex';
914
+ document.getElementById('cfgTitle').innerText = 'Configure Chart';
915
+
916
+ // Show Loading
917
  document.getElementById('cfgLoading').style.display = 'block';
918
  document.getElementById('cfgContent').style.display = 'none';
919
 
920
+ // Load Roots if not loaded
921
+ const roots = await this.fetchRoots();
922
+ const rSel = document.getElementById('cfgRoot');
923
+ rSel.innerHTML = `<option value="">-- Select Symbol --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
 
 
924
 
925
+ // Pre-fill
926
+ const cfg = App.state.charts[id].config;
 
 
 
927
  document.getElementById('cfgTF').value = cfg.timeframe || '1min';
928
  document.getElementById('cfgStart').value = cfg.start || '09:15';
929
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
930
 
931
+ // Load series
932
+ this.renderSeries(cfg.series || App.defaults);
 
933
 
934
+ // Cascades
935
+ if(cfg.root) {
936
+ rSel.value = cfg.root;
937
+ await this.onRootChange(cfg.expiry, cfg.instrument);
938
+ }
939
+
940
+ // Show Content
941
  document.getElementById('cfgLoading').style.display = 'none';
942
  document.getElementById('cfgContent').style.display = 'block';
943
  },
944
 
945
  openDefaults() {
946
  this.isGlobal = true;
 
947
  document.getElementById('configModal').style.display = 'flex';
948
+ document.getElementById('cfgTitle').innerText = 'Global Settings';
949
  document.getElementById('cfgLoading').style.display = 'none';
950
  document.getElementById('cfgContent').style.display = 'block';
951
 
952
+ // Hide symbol stuff for global
953
+ // (Simplified: just render series list, ignoring top inputs visually or logically)
954
+ this.renderSeries(App.defaults);
 
 
955
  },
956
 
957
  close() { document.getElementById('configModal').style.display = 'none'; },
958
 
959
+ async fetchRoots() {
960
+ const res = await fetch(`/api/roots?date=${App.state.date}`);
961
+ return await res.json();
 
 
 
 
 
962
  },
963
 
964
  async onRootChange(preExp, preInst) {
 
992
  const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
993
  const insts = await res.json();
994
  iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
 
995
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
996
  },
997
 
998
+ renderSeries(list) {
999
+ const c = document.getElementById('cfgSeries');
1000
+ c.innerHTML = '';
1001
+ list.forEach(s => {
1002
+ const row = document.createElement('div');
1003
+ row.className = 'series-row';
1004
+ row.innerHTML = `
1005
+ <input type="color" value="${s.color}" class="color-input s-color">
1006
+ <input type="text" value="${s.formula}" class="s-form" placeholder="$P">
1007
+ <input type="text" value="${s.label}" class="s-lbl" placeholder="Label">
1008
+ <select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1009
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1010
+ `;
1011
+ c.appendChild(row);
1012
+ });
1013
  },
1014
 
1015
  addSeriesRow() {
1016
+ const row = document.createElement('div');
1017
+ row.className = 'series-row';
1018
+ row.innerHTML = `
1019
+ <input type="color" value="#ffffff" class="color-input s-color">
1020
+ <input type="text" value="$P" class="s-form">
1021
+ <input type="text" value="New" class="s-lbl">
1022
+ <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1023
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
 
 
 
 
 
 
 
 
1024
  `;
1025
+ document.getElementById('cfgSeries').appendChild(row);
1026
  },
1027
 
1028
  save() {
1029
+ const rows = document.querySelectorAll('#cfgSeries .series-row');
1030
+ const series = Array.from(rows).map(r => ({
1031
  color: r.querySelector('.s-color').value,
1032
+ formula: r.querySelector('.s-form').value,
1033
+ label: r.querySelector('.s-lbl').value,
1034
  axis: r.querySelector('.s-axis').value
1035
  }));
1036
 
1037
  if(this.isGlobal) {
1038
+ App.defaults = series;
1039
+ this.showToast("Defaults Saved");
1040
  } else {
1041
  const root = document.getElementById('cfgRoot').value;
1042
  const exp = document.getElementById('cfgExp').value;
1043
  const inst = document.getElementById('cfgInst').value;
1044
 
1045
+ if(!root || !exp || !inst) { alert("Please select Symbol, Expiry and Instrument"); return; }
1046
 
1047
  const config = {
1048
  root, expiry: exp, instrument: inst,
1049
  timeframe: document.getElementById('cfgTF').value,
1050
  start: document.getElementById('cfgStart').value,
1051
  end: document.getElementById('cfgEnd').value,
1052
+ series: series
1053
  };
1054
 
1055
  App.state.charts[this.targetId].config = config;
1056
+ App.save();
1057
+ App.renderGrid(); // Re-render to show title updates
1058
+ ChartLogic.plot(this.targetId);
1059
+ this.showToast("Chart Configured");
 
 
 
 
 
1060
  }
1061
  this.close();
1062
+ },
1063
+
1064
+ showToast(msg) {
1065
+ const t = document.getElementById('toast');
1066
+ document.getElementById('toastMsg').innerText = msg;
1067
+ t.classList.add('show');
1068
+ setTimeout(() => t.classList.remove('show'), 2000);
1069
  }
1070
  };
1071
 
 
1072
  window.onload = () => App.init();
 
1073
  </script>
1074
  </body>
1075
  </html>