topsecrettraders commited on
Commit
733fbb4
·
verified ·
1 Parent(s): c6ec707

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +462 -415
app.py CHANGED
@@ -205,7 +205,7 @@ def fetch_series(
205
 
206
 
207
  # ==========================================
208
- # 5. FRONTEND TEMPLATE (NEW UI)
209
  # ==========================================
210
 
211
  HTML_TEMPLATE = """
@@ -222,7 +222,7 @@ HTML_TEMPLATE = """
222
  :root {
223
  --bg-body: #0d1117;
224
  --bg-header: #161b22;
225
- --bg-card: #0d1117;
226
  --bg-panel: #21262d;
227
  --border: #30363d;
228
  --text-primary: #e6edf3;
@@ -231,114 +231,127 @@ HTML_TEMPLATE = """
231
  --success: #238636;
232
  --danger: #da3633;
233
  --warning: #d29922;
 
 
234
  }
235
- * { box-sizing: border-box; margin: 0; padding: 0; }
236
-
237
- /* SCROLLBAR */
238
- ::-webkit-scrollbar { width: 8px; height: 8px; }
239
- ::-webkit-scrollbar-track { background: var(--bg-body); }
240
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
241
- ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
242
 
 
 
243
  body {
244
  background: var(--bg-body); color: var(--text-primary);
245
- font-family: 'Inter', sans-serif;
246
- display: flex; flex-direction: column;
247
- height: 100vh; overflow: hidden;
248
  }
249
 
250
  /* --- TOP BAR --- */
251
  .top-bar {
252
- height: 50px; background: var(--bg-header); border-bottom: 1px solid var(--border);
253
- display: flex; align-items: center; justify-content: space-between; padding: 0 15px; flex-shrink: 0;
254
  z-index: 50;
255
  }
256
- .logo { font-weight: 800; font-size: 16px; margin-right: 20px; color: var(--text-primary); }
257
  .logo span { color: var(--accent); }
258
 
259
- .controls { display: flex; gap: 10px; align-items: center; }
260
 
261
  select, button, input {
262
  background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary);
263
- padding: 5px 12px; border-radius: 4px; font-size: 12px; font-family: inherit; outline: none; cursor: pointer;
264
- height: 30px; display: flex; align-items: center;
265
  }
 
266
  select:focus, input:focus { border-color: var(--accent); }
267
- button:hover { background: var(--border); }
268
 
269
  .btn-accent { background: var(--accent); color: white; border: none; font-weight: 600; }
270
  .btn-accent:hover { background: #58a6ff; }
 
271
  .btn-danger { color: var(--danger); border-color: var(--border); background: transparent; }
272
  .btn-danger:hover { background: rgba(218, 54, 51, 0.1); border-color: var(--danger); }
273
 
274
- .layout-btn-group { display: flex; gap: 2px; }
275
- .layout-btn { width: 30px; justify-content: center; font-weight: bold; background: var(--bg-card); }
276
- .layout-btn.active { background: var(--border); color: var(--accent); }
277
-
278
  /* --- MAIN GRID CONTAINER --- */
279
- /* This is the scrolling area. Columns are placed inside here. */
280
- .main-content {
281
- flex: 1;
282
- display: flex;
283
- overflow-y: auto; /* PAGE SCROLL */
284
  overflow-x: hidden;
285
- padding: 0;
 
 
 
 
 
286
  }
287
 
288
- .column {
289
- flex: 1;
290
- display: flex;
291
- flex-direction: column;
292
- min-width: 0;
293
- border-right: 1px solid var(--border);
294
- /* Height is determined by content, so it scrolls with the page */
295
- }
296
- .column:last-child { border-right: none; }
297
 
298
- .column-add-btn {
299
- padding: 10px; border-top: 1px solid var(--border); color: var(--text-secondary);
300
- text-align: center; cursor: pointer; font-size: 12px; font-weight: 600;
301
- background: var(--bg-header); flex-shrink: 0; height: 40px;
 
 
 
302
  }
303
- .column-add-btn:hover { color: var(--text-primary); background: var(--bg-panel); }
304
 
305
  /* --- CHART CARD --- */
306
  .chart-card {
307
  background: var(--bg-card);
308
- border-bottom: 1px solid var(--border);
 
309
  display: flex; flex-direction: column;
310
  position: relative;
311
- /* Height is set dynamically by JS */
312
- transition: height 0.3s ease;
 
 
313
  }
314
-
 
315
  .card-header {
316
- height: 35px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
317
- padding: 0 10px; background: var(--bg-header); font-size: 12px; flex-shrink: 0;
 
318
  }
319
  .card-title { font-weight: 600; display: flex; gap: 8px; align-items: center; }
320
  .card-meta { color: var(--text-secondary); font-family: 'JetBrains Mono', monospace; font-size: 11px; }
 
321
 
322
- .card-actions { display: flex; gap: 5px; }
323
- .icon-btn { padding: 4px; height: 24px; width: 24px; justify-content: center; border: none; background: transparent; color: var(--text-secondary); }
324
- .icon-btn:hover { color: var(--text-primary); background: var(--border); }
 
 
 
 
 
 
325
 
326
- .card-body { flex: 1; position: relative; padding: 0; overflow: hidden; display: flex; flex-direction: column; }
327
-
328
- /* Empty State */
329
  .empty-state {
330
  display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
331
- color: var(--text-secondary); font-size: 13px; gap: 10px;
332
  background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-card) 70%);
333
  }
334
- .empty-state button { height: auto; padding: 8px 16px; font-size: 13px; }
 
 
 
 
 
 
 
 
335
 
336
  /* --- STATUS DOTS --- */
337
- .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--text-secondary); }
338
- .status-dot.live { background: #00ff00; box-shadow: 0 0 8px #00ff00; animation: pulse 1.5s infinite; }
339
- .status-dot.retry { background: var(--warning); animation: pulse 0.5s infinite; }
340
  .status-dot.hist { background: var(--accent); }
341
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
 
342
 
343
  /* --- MODAL --- */
344
  .modal-overlay {
@@ -347,30 +360,33 @@ HTML_TEMPLATE = """
347
  display: none; justify-content: center; align-items: center;
348
  }
349
  .modal {
350
- background: var(--bg-header); border: 1px solid var(--border); border-radius: 8px;
351
  width: 500px; max-width: 95%; max-height: 90vh; display: flex; flex-direction: column;
352
  box-shadow: 0 20px 50px rgba(0,0,0,0.6);
353
  }
354
- .modal-head { padding: 15px; border-bottom: 1px solid var(--border); font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
355
- .modal-body { padding: 20px; overflow-y: auto; }
356
- .modal-foot { padding: 15px; border-top: 1px solid var(--border); text-align: right; display:flex; justify-content:flex-end; gap:10px;}
357
-
358
- .form-group { margin-bottom: 15px; }
359
- .form-group label { display: block; margin-bottom: 5px; color: var(--text-secondary); font-size: 11px; text-transform: uppercase; font-weight:600; }
360
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
361
 
362
- input[type="time"] { width: 100%; }
 
363
 
364
  /* Series List */
365
- .series-row { display: grid; grid-template-columns: 35px 1fr 1fr 60px 30px; gap: 5px; margin-bottom: 5px; align-items: center; }
366
- .series-row input[type="color"] { padding: 0; height: 30px; border: none; background: none; width:100%; cursor:pointer;}
367
- .series-row button { justify-content: center; }
 
 
 
 
 
368
 
369
  /* Toast */
370
  .toast {
371
- position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: white;
372
- padding: 10px 20px; border-radius: 4px; font-weight: 600; font-size: 13px;
373
- transform: translateY(100px); transition: transform 0.3s; z-index: 1000;
 
374
  }
375
  .toast.show { transform: translateY(0); }
376
 
@@ -384,30 +400,31 @@ HTML_TEMPLATE = """
384
 
385
  <div class="controls">
386
  <!-- Date Selection -->
387
- <select id="globalDate" onchange="GlobalApp.onDateChange()" style="min-width:120px;"></select>
388
 
389
  <!-- Mode Selection -->
390
- <select id="globalMode" onchange="GlobalApp.onModeChange()" style="font-weight:bold;">
391
- <option value="HIST">HISTORICAL</option>
392
- <option value="LIVE">LIVE 🟢</option>
393
  </select>
394
 
395
- <div style="width: 1px; height: 20px; background: var(--border); margin: 0 5px;"></div>
396
-
397
- <!-- Layout Selector Buttons -->
398
- <div class="layout-btn-group">
399
- <button class="layout-btn" onclick="GlobalApp.setLayout(1)" title="1 Column">1</button>
400
- <button class="layout-btn" onclick="GlobalApp.setLayout(2)" title="2 Columns">2</button>
401
- <button class="layout-btn" onclick="GlobalApp.setLayout(3)" title="3 Columns">3</button>
402
- <button class="layout-btn" onclick="GlobalApp.setLayout(4)" title="4 Columns">4</button>
403
- </div>
 
404
 
405
  <button onclick="ConfigModal.openDefaults()">Settings</button>
406
  <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset</button>
407
  </div>
408
  </div>
409
 
410
- <!-- MAIN GRID CONTAINER -->
411
  <div class="main-content" id="gridContainer">
412
  <!-- Columns injected via JS -->
413
  </div>
@@ -420,15 +437,16 @@ HTML_TEMPLATE = """
420
  <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
421
  </div>
422
  <div class="modal-body">
 
423
  <div id="cfgForm">
424
  <div class="form-row">
425
  <div class="form-group">
426
  <label>Symbol (Root)</label>
427
- <select id="cfgRoot" onchange="ConfigModal.onRootChange()" style="width:100%"></select>
428
  </div>
429
  <div class="form-group">
430
  <label>Expiry</label>
431
- <select id="cfgExp" onchange="ConfigModal.onExpChange()" style="width:100%"></select>
432
  </div>
433
  </div>
434
 
@@ -450,30 +468,51 @@ HTML_TEMPLATE = """
450
  </select>
451
  </div>
452
  <div class="form-group">
453
- <label>Range (HH:MM)</label>
454
- <div style="display:flex; gap:5px;">
455
- <input type="time" id="cfgStart" value="09:15">
456
- <input type="time" id="cfgEnd" value="15:30">
457
  </div>
458
  </div>
459
  </div>
460
 
461
- <hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
462
-
463
- <div class="form-group">
464
- <label>Plot Lines (Variables: $P, $B, $S, $V, $OI)</label>
 
 
 
 
 
 
 
465
  <div id="cfgSeriesList"></div>
466
- <button style="width:100%; margin-top:5px; height:auto; padding:5px;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
467
  </div>
468
  </div>
469
  </div>
470
  <div class="modal-foot">
471
- <button onclick="ConfigModal.close()">Cancel</button>
472
  <button class="btn-accent" onclick="ConfigModal.save()">Save Configuration</button>
473
  </div>
474
  </div>
475
  </div>
476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  <div id="toast" class="toast">Settings Saved</div>
478
 
479
  <script>
@@ -485,13 +524,13 @@ const GlobalApp = {
485
  state: {
486
  date: '',
487
  mode: 'HIST',
488
- columns: 3,
489
- layout: [[], [], []], // Array of arrays of chart IDs
490
- charts: {} // ID -> { col: 0, config: {}, instance: ChartJS, interval: null }
491
  },
492
 
493
  defaults: [
494
- { formula: '$P', color: '#58a6ff', label: 'Price', axis: 'left' },
495
  { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
496
  { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
497
  ],
@@ -499,22 +538,18 @@ const GlobalApp = {
499
  async init() {
500
  await this.loadDates();
501
 
502
- if (localStorage.getItem('dc_pro_state')) {
503
- this.loadFromStorage();
504
- } else {
505
- this.initNew();
506
- }
507
-
508
- // Auto Live Mode Logic
509
  const today = new Date().toISOString().split('T')[0];
510
- if (this.state.date === today) {
511
  this.state.mode = 'LIVE';
512
  document.getElementById('globalMode').value = 'LIVE';
513
  }
514
 
515
- this.updateLayoutUI();
516
- // Start Loops if Live
517
- this.checkMode();
 
 
518
  },
519
 
520
  async loadDates() {
@@ -528,77 +563,60 @@ const GlobalApp = {
528
  },
529
 
530
  initNew() {
531
- // Default 3x2 Grid (3 Columns, 2 Empty Charts each)
532
- this.state.layout = [[], [], []];
533
- this.state.columns = 3;
534
-
535
- for(let c=0; c<3; c++) {
536
- this.addChartInternal(c, null);
537
- this.addChartInternal(c, null);
538
- }
539
- this.renderGrid();
540
- },
541
-
542
- // Adds chart to state, does NOT render immediately (called by init or add btn)
543
- addChartInternal(colIdx, savedData) {
544
- const id = savedData ? savedData.id : 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
545
- const config = savedData ? savedData.config : {};
546
-
547
- this.state.charts[id] = {
548
- col: colIdx,
549
- config: config,
550
- instance: null,
551
- interval: null
552
- };
553
-
554
- if (!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
555
- // Prevent dupes
556
- if (!this.state.layout[colIdx].includes(id)) {
557
- this.state.layout[colIdx].push(id);
558
- }
559
- return id;
560
  },
561
 
562
  loadFromStorage() {
563
  try {
564
- const raw = localStorage.getItem('dc_pro_state');
565
  const saved = JSON.parse(raw);
566
- this.state.date = saved.date || this.state.date; // Prefer saved date? Or default to latest? Let's use saved if valid.
567
- document.getElementById('globalDate').value = this.state.date;
 
 
 
 
 
 
 
568
 
569
- this.state.mode = saved.mode || 'HIST';
 
 
 
 
 
 
570
  document.getElementById('globalMode').value = this.state.mode;
571
 
572
- this.state.columns = saved.columns || 3;
573
- this.state.layout = [];
574
 
575
- // Reconstruct
576
- saved.layout.forEach((colCharts, cIdx) => {
577
- this.state.layout[cIdx] = [];
578
- colCharts.forEach(cData => {
579
- this.addChartInternal(cIdx, cData);
580
  });
581
  });
582
- this.renderGrid();
583
- // Re-plot valid charts
584
- setTimeout(() => {
585
- Object.keys(this.state.charts).forEach(id => {
586
- if(this.state.charts[id].config.root) ChartLogic.plot(id);
587
- });
588
- }, 100);
589
 
590
- } catch(e) { this.initNew(); }
 
 
 
591
  },
592
 
593
  saveToStorage() {
594
- const minimalLayout = [];
595
- for(let i=0; i<this.state.columns; i++) {
596
- const col = this.state.layout[i] || [];
597
- minimalLayout.push(col.map(id => ({
598
  id: id,
599
  config: this.state.charts[id].config
600
- })));
601
- }
602
 
603
  const dump = {
604
  date: this.state.date,
@@ -606,38 +624,32 @@ const GlobalApp = {
606
  columns: this.state.columns,
607
  layout: minimalLayout
608
  };
609
- localStorage.setItem('dc_pro_state', JSON.stringify(dump));
610
  },
611
 
612
  resetApp() {
613
- if(!confirm("Reset layout and settings?")) return;
614
- localStorage.removeItem('dc_pro_state');
615
  location.reload();
616
  },
617
 
618
- // --- LAYOUT & RENDERING ---
 
 
 
 
 
 
 
619
  setLayout(cols) {
620
- // Redistribute charts if reducing columns
621
- const allCharts = this.state.layout.flat();
622
  this.state.columns = cols;
623
- this.state.layout = Array.from({length: cols}, () => []);
624
-
625
- // Distribute round-robin
626
- allCharts.forEach((id, i) => {
627
- const col = i % cols;
628
- this.state.layout[col].push(id);
629
- this.state.charts[id].col = col;
630
- });
631
-
632
- this.updateLayoutUI();
633
  this.renderGrid();
634
- this.saveToStorage();
635
- },
636
-
637
- updateLayoutUI() {
638
- document.querySelectorAll('.layout-btn').forEach((b, i) => {
639
- b.classList.toggle('active', (i+1) === this.state.columns);
640
- });
641
  },
642
 
643
  renderGrid() {
@@ -648,189 +660,185 @@ const GlobalApp = {
648
  const colDiv = document.createElement('div');
649
  colDiv.className = 'column';
650
  colDiv.id = `col-${i}`;
651
-
 
652
  const chartIds = this.state.layout[i] || [];
653
 
 
 
 
 
 
 
 
654
  chartIds.forEach(id => {
655
- colDiv.appendChild(this.createChartDOM(id));
 
 
 
656
  });
657
 
658
- // Add "+" Button at bottom of column
659
  const addBtn = document.createElement('div');
660
- addBtn.className = 'column-add-btn';
661
- addBtn.innerHTML = '+ ADD CHART';
662
- addBtn.onclick = () => this.addChartBtn(i);
663
  colDiv.appendChild(addBtn);
664
 
665
  container.appendChild(colDiv);
666
  }
 
667
 
668
- this.recalculateHeights();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  },
670
 
671
- createChartDOM(id) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  const div = document.createElement('div');
673
  div.className = 'chart-card';
674
  div.id = `card-${id}`;
675
-
676
- // Initial HTML
 
 
 
 
 
 
 
 
 
 
 
 
677
  div.innerHTML = `
678
  <div class="card-header">
679
- <div class="card-title">
680
  <span class="status-dot"></span>
681
  <span>New Chart</span>
682
  </div>
683
  <div class="card-actions">
684
  <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
685
- <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
686
  </div>
687
  </div>
688
  <div class="card-body" id="body-${id}">
689
  <div class="empty-state">
690
- <button class="btn-accent" onclick="ConfigModal.open('${id}')">Configure Chart</button>
 
691
  </div>
692
  </div>
693
  `;
694
  return div;
695
  },
696
 
697
- addChartBtn(colIdx) {
698
- const id = this.addChartInternal(colIdx, null);
699
- const colDiv = document.getElementById(`col-${colIdx}`);
700
- const btn = colDiv.querySelector('.column-add-btn');
701
- colDiv.insertBefore(this.createChartDOM(id), btn);
702
- this.recalculateHeights();
703
- this.saveToStorage();
704
- },
705
-
706
- removeChart(id) {
707
- if(!confirm("Delete chart?")) return;
708
-
709
- ChartLogic.stopLive(id);
710
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
711
-
712
- const cIdx = this.state.charts[id].col;
713
- this.state.layout[cIdx] = this.state.layout[cIdx].filter(x => x !== id);
714
- delete this.state.charts[id];
715
-
716
- document.getElementById(`card-${id}`).remove();
717
- this.recalculateHeights();
718
- this.saveToStorage();
719
- },
720
-
721
- // --- DYNAMIC HEIGHT CALCULATION ---
722
- recalculateHeights() {
723
- // Logic:
724
- // Viewport Height (minus header) = H
725
- // Charts in column = N
726
- // Height per chart = H / N
727
- // Constraint: Max 4 charts visible. So Min Height = H / 4.
728
-
729
- const headerH = 50;
730
- const viewportH = window.innerHeight - headerH;
731
-
732
- for (let i = 0; i < this.state.columns; i++) {
733
- const ids = this.state.layout[i];
734
- const count = ids.length;
735
- if(count === 0) continue;
736
-
737
- // Calculate height
738
- // If count <= 4, split equally (100%, 50%, 33%, 25%)
739
- // If count > 4, keep at 25% (scrolling happens via main container)
740
- let pct = 100 / count;
741
- if (count > 4) pct = 25; // Floor at 25%
742
-
743
- ids.forEach(id => {
744
- const card = document.getElementById(`card-${id}`);
745
- if(card) {
746
- card.style.height = `max(${pct}%, ${viewportH/4}px)`;
747
- // Fallback pixel min-height just in case
748
- if(count === 1) card.style.height = '100%';
749
- }
750
- });
751
-
752
- // Note: Since .column uses flex display but no fixed height,
753
- // giving the children % heights relative to the column works if the column stretches.
754
- // But here the Column is inside a scrolling container.
755
- // Better approach: Set Pixel Height based on calculation.
756
-
757
- const pxHeight = Math.max(viewportH / count, viewportH / 4);
758
-
759
- ids.forEach(id => {
760
- const card = document.getElementById(`card-${id}`);
761
- if(card) {
762
- // Subtract small buffer for borders
763
- card.style.height = (count === 1) ? `${viewportH}px` : `${pxHeight}px`;
764
- }
765
- });
766
- }
767
- },
768
-
769
- // --- GLOBAL CONTROLS ---
770
  onDateChange() {
771
  this.state.date = document.getElementById('globalDate').value;
 
772
  const today = new Date().toISOString().split('T')[0];
773
-
774
- // Auto switch mode check
775
- if(this.state.date === today && this.state.mode !== 'LIVE') {
776
  this.state.mode = 'LIVE';
777
  document.getElementById('globalMode').value = 'LIVE';
778
- }
779
-
780
  this.saveToStorage();
781
- this.checkMode();
782
  Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
783
  },
784
 
785
  onModeChange() {
786
  this.state.mode = document.getElementById('globalMode').value;
787
  this.saveToStorage();
788
- this.checkMode();
789
- },
790
-
791
- checkMode() {
792
- const isLive = (this.state.mode === 'LIVE');
793
  Object.keys(this.state.charts).forEach(id => {
794
- if(isLive) ChartLogic.startLive(id);
795
  else ChartLogic.stopLive(id);
796
  });
797
  }
798
  };
799
 
800
- // Hook resize to recalc heights
801
- window.onresize = () => GlobalApp.recalculateHeights();
802
-
803
  /**
804
  * --- CHART LOGIC ---
805
  */
806
  const ChartLogic = {
807
  async plot(id, isUpdate=false) {
808
  const chartData = GlobalApp.state.charts[id];
 
809
  const cfg = chartData.config;
810
- if (!cfg || !cfg.root) return;
811
 
812
- // Update Header UI
813
- const card = document.getElementById(`card-${id}`);
814
- const titleEl = card.querySelector('.card-title');
815
- const dotType = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
816
 
 
817
  titleEl.innerHTML = `
818
- <span class="status-dot ${isUpdate ? 'retry' : dotType}"></span>
819
- ${cfg.root} <span style="color:var(--accent)">${cfg.expiry}</span>
820
- <span class="card-meta">${cfg.instrument} • ${cfg.timeframe}</span>
821
  `;
822
 
823
- // Create Canvas if needed
824
  const body = document.getElementById(`body-${id}`);
825
- if(!body.querySelector('canvas')) {
826
- body.innerHTML = `<div style="position:relative; height:100%; width:100%;"><canvas id="canvas-${id}"></canvas></div>`;
827
  }
828
 
829
- // Live End Time Logic
830
- let endTime = cfg.end;
831
- if(GlobalApp.state.mode === 'LIVE') {
832
- const now = new Date();
833
- endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
834
  }
835
 
836
  try {
@@ -844,69 +852,88 @@ const ChartLogic = {
844
  instrument: cfg.instrument,
845
  timeframe: cfg.timeframe,
846
  start_time: cfg.start,
847
- end_time: endTime
848
  })
849
  });
850
  const data = await res.json();
851
 
852
- if(data.error || !data.labels || !data.labels.length) {
 
853
  if(GlobalApp.state.mode === 'LIVE') {
854
- // Retry mode, keep old chart
855
- return;
 
856
  } else {
857
- body.innerHTML = '<div class="empty-state">No Data Found</div>';
858
- if(chartData.instance) { chartData.instance.destroy(); chartData.instance = null; }
859
  return;
860
  }
861
  }
862
 
863
- // Restore Dot
864
- titleEl.querySelector('.status-dot').className = `status-dot ${dotType}`;
 
 
865
 
866
- // Process Formulas
867
  const datasets = cfg.series.map(s => {
868
- const vals = data.labels.map((_, i) => {
869
- 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;
870
- let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
871
- try { return new Function('return '+f)(); } catch{ return null; }
 
 
 
872
  });
 
873
  return {
874
- label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
875
- borderWidth: 1.5, pointRadius: 0, hoverRadius: 4, tension: 0.1,
876
- yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
 
 
877
  };
878
  });
879
 
880
  this.renderCanvas(id, data.labels, datasets);
881
 
882
- } catch(e) { console.error(e); }
 
 
883
  },
884
 
885
  renderCanvas(id, labels, datasets) {
886
  const ctx = document.getElementById(`canvas-${id}`);
887
- if(!ctx) return;
888
- const cData = GlobalApp.state.charts[id];
 
889
 
890
- if(cData.instance) {
891
- cData.instance.data.labels = labels;
892
- cData.instance.data.datasets = datasets;
893
- cData.instance.update('none');
894
  } else {
895
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
896
- cData.instance = new Chart(ctx.getContext('2d'), {
897
  type: 'line',
898
  data: { labels, datasets },
899
  options: {
900
- responsive: true, maintainAspectRatio: false, animation: false,
 
 
901
  interaction: { mode: 'index', intersect: false },
902
- plugins: {
903
- legend: { display:false },
904
- tooltip: { backgroundColor:'rgba(13,17,23,0.9)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor:'#30363d', borderWidth:1 }
905
  },
906
  scales: {
907
- x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8} },
908
- y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e'} },
909
- y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e'} }
910
  }
911
  }
912
  });
@@ -915,17 +942,28 @@ const ChartLogic = {
915
 
916
  startLive(id) {
917
  this.stopLive(id);
918
- const c = GlobalApp.state.charts[id];
919
- // Plot immediately then interval
920
- if(c.config.root) this.plot(id);
921
- c.interval = setInterval(() => {
922
- if(c.config.root) this.plot(id, true);
923
  }, 15000);
924
  },
925
 
926
  stopLive(id) {
927
  const c = GlobalApp.state.charts[id];
928
- if(c && c.interval) { clearInterval(c.interval); c.interval = null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
929
  }
930
  };
931
 
@@ -933,125 +971,137 @@ const ChartLogic = {
933
  * --- CONFIG MODAL ---
934
  */
935
  const ConfigModal = {
936
- currId: null,
937
  isGlobal: false,
938
 
939
  async open(id) {
940
- this.currId = id;
941
  this.isGlobal = false;
 
942
  document.getElementById('configModalOverlay').style.display = 'flex';
943
- document.getElementById('cfgModalTitle').innerText = "Configure Chart";
944
  document.getElementById('cfgForm').style.display = 'block';
945
 
946
- const cfg = GlobalApp.state.charts[id].config;
947
- await this.loadRoots(cfg.root);
948
 
949
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
950
- document.getElementById('cfgStart').value = cfg.start || '09:15';
951
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
952
 
953
- this.renderSeries(cfg.series || GlobalApp.defaults);
954
-
955
- if(cfg.root) {
956
- document.getElementById('cfgRoot').value = cfg.root;
957
- await this.onRootChange(cfg.expiry, cfg.instrument);
 
958
  }
959
  },
960
 
961
  openDefaults() {
962
  this.isGlobal = true;
 
963
  document.getElementById('configModalOverlay').style.display = 'flex';
964
- document.getElementById('cfgModalTitle').innerText = "Global Default Series";
965
- // Hide top part
966
- document.getElementById('cfgForm').style.display = 'block';
967
- // We really should hide inputs but for simplicity keeping logic
968
  this.renderSeries(GlobalApp.defaults);
969
  },
970
 
971
  close() { document.getElementById('configModalOverlay').style.display = 'none'; },
972
 
973
- async loadRoots(pre) {
974
- const sel = document.getElementById('cfgRoot');
975
- sel.innerHTML = '<option>Loading...</option>';
976
  try {
977
- const d = GlobalApp.state.date;
978
- const res = await fetch(`/api/roots?date=${d}`);
979
  const roots = await res.json();
980
- sel.innerHTML = `<option value="">-- Select Symbol --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
981
- if(pre && roots.includes(pre)) sel.value = pre;
982
- } catch(e){ sel.innerHTML='<option>Error</option>'; }
983
  },
984
 
985
  async onRootChange(preExp, preInst) {
986
  const root = document.getElementById('cfgRoot').value;
987
- const d = GlobalApp.state.date;
988
  const eSel = document.getElementById('cfgExp');
989
  eSel.innerHTML = '<option>Loading...</option>';
990
-
991
- const res = await fetch(`/api/expiries?date=${d}&root=${root}`);
992
  const exps = await res.json();
993
- eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e=>`<option>${e}</option>`).join('');
994
-
995
  if(preExp && exps.includes(preExp)) {
996
  eSel.value = preExp;
997
  await this.onExpChange(preInst);
998
  } else {
999
- // Auto defaults
1000
- const auto = await fetch(`/api/auto_config?date=${d}&root=${root}`).then(r=>r.json());
1001
- if(auto.current) {
1002
- eSel.value = auto.current.expiry;
1003
- await this.onExpChange(auto.current.instrument);
1004
- }
1005
  }
1006
  },
1007
 
1008
  async onExpChange(preInst) {
1009
  const root = document.getElementById('cfgRoot').value;
1010
  const exp = document.getElementById('cfgExp').value;
1011
- const d = GlobalApp.state.date;
1012
  const iSel = document.getElementById('cfgInst');
1013
  iSel.innerHTML = '<option>Loading...</option>';
1014
-
1015
- const res = await fetch(`/api/instruments?date=${d}&root=${root}&expiry=${exp}`);
1016
  const insts = await res.json();
1017
- iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
 
1018
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
 
1019
  },
1020
 
1021
  renderSeries(list) {
1022
- const div = document.getElementById('cfgSeriesList');
1023
- div.innerHTML = '';
1024
  list.forEach(s => {
1025
- const r = document.createElement('div');
1026
- r.className = 'series-row';
1027
- r.innerHTML = `
1028
  <input type="color" value="${s.color}" class="s-color">
1029
  <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
1030
  <input type="text" value="${s.label}" class="s-label" placeholder="Label">
1031
- <select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1032
- <button class="btn-danger" onclick="this.parentElement.remove()"></button>
 
 
 
1033
  `;
1034
- div.appendChild(r);
1035
  });
1036
  },
1037
 
1038
  addSeriesRow() {
1039
- const div = document.getElementById('cfgSeriesList');
1040
- const r = document.createElement('div');
1041
- r.className = 'series-row';
1042
- r.innerHTML = `
1043
  <input type="color" value="#ffffff" class="s-color">
1044
  <input type="text" value="$P" class="s-formula">
1045
  <input type="text" value="New" class="s-label">
1046
  <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1047
- <button class="btn-danger" onclick="this.parentElement.remove()">✕</button>
1048
  `;
1049
- div.appendChild(r);
 
 
 
 
 
 
 
 
1050
  },
1051
 
1052
  save() {
1053
  const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1054
- const series = Array.from(rows).map(r => ({
1055
  color: r.querySelector('.s-color').value,
1056
  formula: r.querySelector('.s-formula').value,
1057
  label: r.querySelector('.s-label').value,
@@ -1059,35 +1109,32 @@ const ConfigModal = {
1059
  }));
1060
 
1061
  if(this.isGlobal) {
1062
- GlobalApp.defaults = series;
1063
- document.getElementById('toast').innerText = "Defaults Saved";
1064
  } else {
1065
  const root = document.getElementById('cfgRoot').value;
1066
  const exp = document.getElementById('cfgExp').value;
1067
  const inst = document.getElementById('cfgInst').value;
1068
- if(!root || !exp || !inst) { alert("Incomplete details"); return; }
1069
 
1070
- const cfg = {
 
 
1071
  root, expiry: exp, instrument: inst,
1072
  timeframe: document.getElementById('cfgTF').value,
1073
  start: document.getElementById('cfgStart').value,
1074
  end: document.getElementById('cfgEnd').value,
1075
- series: series
1076
  };
1077
-
1078
- GlobalApp.state.charts[this.currId].config = cfg;
1079
  GlobalApp.saveToStorage();
1080
- ChartLogic.plot(this.currId);
1081
- if(GlobalApp.state.mode === 'LIVE') ChartLogic.startLive(this.currId);
1082
  }
1083
-
1084
- document.getElementById('toast').classList.add('show');
1085
- setTimeout(()=>document.getElementById('toast').classList.remove('show'), 2000);
1086
  this.close();
1087
  }
1088
  };
1089
 
1090
- // Init
1091
  window.onload = () => GlobalApp.init();
1092
 
1093
  </script>
 
205
 
206
 
207
  # ==========================================
208
+ # 5. FRONTEND TEMPLATE (REVISED UI)
209
  # ==========================================
210
 
211
  HTML_TEMPLATE = """
 
222
  :root {
223
  --bg-body: #0d1117;
224
  --bg-header: #161b22;
225
+ --bg-card: #010409;
226
  --bg-panel: #21262d;
227
  --border: #30363d;
228
  --text-primary: #e6edf3;
 
231
  --success: #238636;
232
  --danger: #da3633;
233
  --warning: #d29922;
234
+
235
+ --header-h: 55px;
236
  }
 
 
 
 
 
 
 
237
 
238
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
239
+
240
  body {
241
  background: var(--bg-body); color: var(--text-primary);
242
+ font-family: 'Inter', sans-serif; display: flex; flex-direction: column;
243
+ height: 100vh; overflow: hidden; /* Prevent body scroll, handle in main-content */
 
244
  }
245
 
246
  /* --- TOP BAR --- */
247
  .top-bar {
248
+ height: var(--header-h); background: var(--bg-header); border-bottom: 1px solid var(--border);
249
+ display: flex; align-items: center; justify-content: space-between; padding: 0 20px; flex-shrink: 0;
250
  z-index: 50;
251
  }
252
+ .logo { font-weight: 800; font-size: 18px; color: var(--text-primary); display: flex; align-items: center; gap: 8px;}
253
  .logo span { color: var(--accent); }
254
 
255
+ .controls { display: flex; gap: 12px; align-items: center; }
256
 
257
  select, button, input {
258
  background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary);
259
+ padding: 6px 12px; border-radius: 6px; font-size: 13px; font-family: inherit; cursor: pointer;
260
+ transition: 0.2s;
261
  }
262
+ select:hover, button:hover { border-color: var(--text-secondary); }
263
  select:focus, input:focus { border-color: var(--accent); }
 
264
 
265
  .btn-accent { background: var(--accent); color: white; border: none; font-weight: 600; }
266
  .btn-accent:hover { background: #58a6ff; }
267
+
268
  .btn-danger { color: var(--danger); border-color: var(--border); background: transparent; }
269
  .btn-danger:hover { background: rgba(218, 54, 51, 0.1); border-color: var(--danger); }
270
 
 
 
 
 
271
  /* --- MAIN GRID CONTAINER --- */
272
+ .main-content {
273
+ flex: 1;
274
+ /* GLOBAL SCROLLBAR HERE */
275
+ overflow-y: auto;
 
276
  overflow-x: hidden;
277
+ display: grid;
278
+ padding: 10px;
279
+ gap: 10px;
280
+ /* Columns will be set via JS: grid-template-columns: repeat(N, 1fr) */
281
+ height: calc(100vh - var(--header-h));
282
+ align-content: start; /* Don't stretch rows if content is small */
283
  }
284
 
285
+ /* Custom Scrollbar */
286
+ .main-content::-webkit-scrollbar { width: 8px; }
287
+ .main-content::-webkit-scrollbar-track { background: var(--bg-body); }
288
+ .main-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
289
+ .main-content::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
 
 
 
 
290
 
291
+ /* --- COLUMN --- */
292
+ .column {
293
+ display: flex;
294
+ flex-direction: column;
295
+ gap: 10px;
296
+ min-height: 100%; /* Fill the grid cell */
297
+ /* No internal overflow */
298
  }
 
299
 
300
  /* --- CHART CARD --- */
301
  .chart-card {
302
  background: var(--bg-card);
303
+ border: 1px solid var(--border);
304
+ border-radius: 8px;
305
  display: flex; flex-direction: column;
306
  position: relative;
307
+ overflow: hidden;
308
+ transition: box-shadow 0.2s;
309
+ /* Height is dynamic, set via inline style or flex */
310
+ width: 100%;
311
  }
312
+ .chart-card:hover { box-shadow: 0 0 0 1px var(--border); }
313
+
314
  .card-header {
315
+ height: 40px; border-bottom: 1px solid var(--border);
316
+ display: flex; align-items: center; justify-content: space-between;
317
+ padding: 0 12px; background: var(--bg-header); font-size: 12px;
318
  }
319
  .card-title { font-weight: 600; display: flex; gap: 8px; align-items: center; }
320
  .card-meta { color: var(--text-secondary); font-family: 'JetBrains Mono', monospace; font-size: 11px; }
321
+ .card-actions { display: flex; gap: 4px; }
322
 
323
+ .icon-btn {
324
+ padding: 4px; font-size: 14px; border: none; background: transparent;
325
+ color: var(--text-secondary); width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
326
+ }
327
+ .icon-btn:hover { color: var(--text-primary); background: var(--border); border-radius: 4px; }
328
+ .icon-btn.danger:hover { color: var(--danger); background: rgba(218, 54, 51, 0.2); }
329
+
330
+ .card-body { flex: 1; position: relative; padding: 2px; }
331
+ .chart-canvas { width: 100%; height: 100%; }
332
 
 
 
 
333
  .empty-state {
334
  display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
335
+ color: var(--text-secondary); font-size: 13px; gap: 15px;
336
  background: radial-gradient(circle at center, var(--bg-panel) 0%, var(--bg-card) 70%);
337
  }
338
+
339
+ /* Add Button inside Column */
340
+ .col-add-btn {
341
+ padding: 12px; border: 1px dashed var(--border); color: var(--text-secondary);
342
+ text-align: center; border-radius: 8px; cursor: pointer; font-size: 12px; font-weight: 600;
343
+ transition: 0.2s; background: rgba(22, 27, 34, 0.5);
344
+ flex-shrink: 0; /* Prevent shrinking */
345
+ }
346
+ .col-add-btn:hover { border-color: var(--text-primary); color: var(--text-primary); background: var(--bg-panel); }
347
 
348
  /* --- STATUS DOTS --- */
349
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--text-secondary); transition: 0.3s;}
350
+ .status-dot.live { background: #00ff00; box-shadow: 0 0 8px rgba(0,255,0,0.4); animation: pulse 2s infinite; }
351
+ .status-dot.retry { background: var(--warning); animation: flash 0.5s infinite; }
352
  .status-dot.hist { background: var(--accent); }
353
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
354
+ @keyframes flash { 0%,100% { opacity: 1; } 50% { opacity: 0.2; } }
355
 
356
  /* --- MODAL --- */
357
  .modal-overlay {
 
360
  display: none; justify-content: center; align-items: center;
361
  }
362
  .modal {
363
+ background: var(--bg-header); border: 1px solid var(--border); border-radius: 12px;
364
  width: 500px; max-width: 95%; max-height: 90vh; display: flex; flex-direction: column;
365
  box-shadow: 0 20px 50px rgba(0,0,0,0.6);
366
  }
367
+ .modal-head { padding: 15px 20px; border-bottom: 1px solid var(--border); font-weight: 700; display: flex; justify-content: space-between; align-items: center;}
368
+ .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px;}
369
+ .modal-foot { padding: 15px 20px; border-top: 1px solid var(--border); text-align: right; background: var(--bg-card); border-radius: 0 0 12px 12px;}
 
 
 
 
370
 
371
+ .form-group label { display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 11px; text-transform: uppercase; font-weight: 600;}
372
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
373
 
374
  /* Series List */
375
+ .series-row { display: grid; grid-template-columns: 35px 1fr 1fr 60px 30px; gap: 8px; margin-bottom: 8px; align-items: center; }
376
+ .series-row input[type="color"] { padding: 0; height: 32px; border: none; background: none; cursor: pointer;}
377
+
378
+ .pill-btn {
379
+ background: var(--bg-panel); border: 1px solid var(--border); border-radius: 20px;
380
+ padding: 4px 10px; font-size: 11px; cursor: pointer; display: inline-block; margin-right: 5px; margin-bottom: 5px;
381
+ }
382
+ .pill-btn:hover { border-color: var(--accent); color: var(--accent); }
383
 
384
  /* Toast */
385
  .toast {
386
+ position: fixed; bottom: 30px; right: 30px; background: var(--accent); color: white;
387
+ padding: 12px 24px; border-radius: 8px; font-weight: 600; font-size: 13px;
388
+ transform: translateY(100px); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 1000;
389
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
390
  }
391
  .toast.show { transform: translateY(0); }
392
 
 
400
 
401
  <div class="controls">
402
  <!-- Date Selection -->
403
+ <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
404
 
405
  <!-- Mode Selection -->
406
+ <select id="globalMode" onchange="GlobalApp.onModeChange()">
407
+ <option value="HIST">Historical</option>
408
+ <option value="LIVE">Live (Auto Refresh)</option>
409
  </select>
410
 
411
+ <div style="width: 1px; height: 24px; background: var(--border); margin: 0 5px;"></div>
412
+
413
+ <!-- Layout Selector -->
414
+ <label style="font-size:12px; color:var(--text-secondary); font-weight:600;">COLS:</label>
415
+ <select id="colSelect" onchange="GlobalApp.setLayout(this.value)">
416
+ <option value="1">1</option>
417
+ <option value="2">2</option>
418
+ <option value="3">3</option>
419
+ <option value="4">4</option>
420
+ </select>
421
 
422
  <button onclick="ConfigModal.openDefaults()">Settings</button>
423
  <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset</button>
424
  </div>
425
  </div>
426
 
427
+ <!-- GRID CONTAINER -->
428
  <div class="main-content" id="gridContainer">
429
  <!-- Columns injected via JS -->
430
  </div>
 
437
  <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
438
  </div>
439
  <div class="modal-body">
440
+
441
  <div id="cfgForm">
442
  <div class="form-row">
443
  <div class="form-group">
444
  <label>Symbol (Root)</label>
445
+ <select id="cfgRoot" onchange="ConfigModal.onRootChange()"></select>
446
  </div>
447
  <div class="form-group">
448
  <label>Expiry</label>
449
+ <select id="cfgExp" onchange="ConfigModal.onExpChange()"></select>
450
  </div>
451
  </div>
452
 
 
468
  </select>
469
  </div>
470
  <div class="form-group">
471
+ <label>Time Range (HH:MM)</label>
472
+ <div style="display:flex; gap:10px;">
473
+ <input type="time" id="cfgStart" value="09:15" style="flex:1">
474
+ <input type="time" id="cfgEnd" value="15:30" style="flex:1">
475
  </div>
476
  </div>
477
  </div>
478
 
479
+ <div style="border-top:1px solid var(--border); padding-top:15px; margin-top:5px;">
480
+ <label style="margin-bottom:10px; display:block; color:var(--text-secondary); font-size:11px; font-weight:700;">PLOT FORMULAS</label>
481
+
482
+ <div style="margin-bottom:10px;">
483
+ <span class="pill-btn" onclick="ConfigModal.insertVar('$P')">$P (Price)</span>
484
+ <span class="pill-btn" onclick="ConfigModal.insertVar('$B')">$B (Buy)</span>
485
+ <span class="pill-btn" onclick="ConfigModal.insertVar('$S')">$S (Sell)</span>
486
+ <span class="pill-btn" onclick="ConfigModal.insertVar('$V')">$V (Vol)</span>
487
+ <span class="pill-btn" onclick="ConfigModal.insertVar('$OI')">$OI</span>
488
+ </div>
489
+
490
  <div id="cfgSeriesList"></div>
491
+ <button class="btn-accent" style="width:100%; margin-top:10px; padding:8px;" onclick="ConfigModal.addSeriesRow()">+ Add Plot Line</button>
492
  </div>
493
  </div>
494
  </div>
495
  <div class="modal-foot">
496
+ <button onclick="ConfigModal.close()" style="margin-right:10px;">Cancel</button>
497
  <button class="btn-accent" onclick="ConfigModal.save()">Save Configuration</button>
498
  </div>
499
  </div>
500
  </div>
501
 
502
+ <!-- RESTORE PROMPT -->
503
+ <div class="modal-overlay" id="restoreModal" style="display:none;">
504
+ <div class="modal" style="width: 360px;">
505
+ <div class="modal-head">Welcome Back</div>
506
+ <div class="modal-body" style="text-align:center; padding:30px 20px;">
507
+ <p style="color:var(--text-secondary);">We found a saved layout from your previous session.</p>
508
+ </div>
509
+ <div class="modal-foot">
510
+ <button class="btn-danger" onclick="GlobalApp.initNew()">Reset Fresh</button>
511
+ <button class="btn-accent" onclick="GlobalApp.loadFromStorage()">Restore Layout</button>
512
+ </div>
513
+ </div>
514
+ </div>
515
+
516
  <div id="toast" class="toast">Settings Saved</div>
517
 
518
  <script>
 
524
  state: {
525
  date: '',
526
  mode: 'HIST',
527
+ columns: 1,
528
+ layout: [[], [], [], []], // Max 4 cols supported in logic
529
+ charts: {} // ID -> { col:0, config: {}, instance: ChartJS, interval: null }
530
  },
531
 
532
  defaults: [
533
+ { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
534
  { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
535
  { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
536
  ],
 
538
  async init() {
539
  await this.loadDates();
540
 
541
+ // Auto-detect Live Date
 
 
 
 
 
 
542
  const today = new Date().toISOString().split('T')[0];
543
+ if(this.state.date === today) {
544
  this.state.mode = 'LIVE';
545
  document.getElementById('globalMode').value = 'LIVE';
546
  }
547
 
548
+ if (localStorage.getItem('dc_pro_v3')) {
549
+ document.getElementById('restoreModal').style.display = 'flex';
550
+ } else {
551
+ this.initNew();
552
+ }
553
  },
554
 
555
  async loadDates() {
 
563
  },
564
 
565
  initNew() {
566
+ document.getElementById('restoreModal').style.display = 'none';
567
+ this.setLayout(2); // Default 2 cols
568
+ // Init with 2 empty charts
569
+ this.addChart(0);
570
+ this.addChart(1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  },
572
 
573
  loadFromStorage() {
574
  try {
575
+ const raw = localStorage.getItem('dc_pro_v3');
576
  const saved = JSON.parse(raw);
577
+
578
+ document.getElementById('restoreModal').style.display = 'none';
579
+
580
+ // Restore Date if valid, else keep latest
581
+ const dSel = document.getElementById('globalDate');
582
+ if(Array.from(dSel.options).some(o => o.value === saved.date)) {
583
+ dSel.value = saved.date;
584
+ this.state.date = saved.date;
585
+ }
586
 
587
+ // If date matches today, force live, else restore saved mode
588
+ const today = new Date().toISOString().split('T')[0];
589
+ if(this.state.date === today) {
590
+ this.state.mode = 'LIVE';
591
+ } else {
592
+ this.state.mode = saved.mode || 'HIST';
593
+ }
594
  document.getElementById('globalMode').value = this.state.mode;
595
 
596
+ // Restore Layout
597
+ this.setLayout(saved.columns);
598
 
599
+ // Restore Charts
600
+ saved.layout.forEach((colCharts, colIdx) => {
601
+ colCharts.forEach(chartData => {
602
+ this.addChart(colIdx, chartData.id, chartData.config);
 
603
  });
604
  });
 
 
 
 
 
 
 
605
 
606
+ } catch(e) {
607
+ console.error("Load failed", e);
608
+ this.initNew();
609
+ }
610
  },
611
 
612
  saveToStorage() {
613
+ // Minify layout for storage
614
+ const minimalLayout = this.state.layout.map(col => {
615
+ return col.map(id => ({
 
616
  id: id,
617
  config: this.state.charts[id].config
618
+ }));
619
+ });
620
 
621
  const dump = {
622
  date: this.state.date,
 
624
  columns: this.state.columns,
625
  layout: minimalLayout
626
  };
627
+ localStorage.setItem('dc_pro_v3', JSON.stringify(dump));
628
  },
629
 
630
  resetApp() {
631
+ if(!confirm("Reset everything to fresh state?")) return;
632
+ localStorage.removeItem('dc_pro_v3');
633
  location.reload();
634
  },
635
 
636
+ showToast(msg) {
637
+ const t = document.getElementById('toast');
638
+ t.textContent = msg;
639
+ t.classList.add('show');
640
+ setTimeout(() => t.classList.remove('show'), 2000);
641
+ },
642
+
643
+ // --- LAYOUT LOGIC ---
644
  setLayout(cols) {
645
+ cols = parseInt(cols);
 
646
  this.state.columns = cols;
647
+ document.getElementById('colSelect').value = cols;
648
+
649
+ const container = document.getElementById('gridContainer');
650
+ container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
651
+
 
 
 
 
 
652
  this.renderGrid();
 
 
 
 
 
 
 
653
  },
654
 
655
  renderGrid() {
 
660
  const colDiv = document.createElement('div');
661
  colDiv.className = 'column';
662
  colDiv.id = `col-${i}`;
663
+
664
+ // Get charts for this column
665
  const chartIds = this.state.layout[i] || [];
666
 
667
+ // Dynamic Height Logic:
668
+ // If charts <= 4, they fit to screen (flex:1)
669
+ // If charts > 4, they get fixed height (300px) and column scrolls (if body allowed, but here body is global scroll)
670
+
671
+ // Actually, we want the *Chart Cards* to size dynamically.
672
+ // We apply specific styles to the charts based on the count in this column.
673
+
674
  chartIds.forEach(id => {
675
+ const el = this.createChartElement(id, chartIds.length);
676
+ colDiv.appendChild(el);
677
+ // Defer render to ensure DOM presence
678
+ setTimeout(() => ChartLogic.render(id), 10);
679
  });
680
 
681
+ // Add Button
682
  const addBtn = document.createElement('div');
683
+ addBtn.className = 'col-add-btn';
684
+ addBtn.innerHTML = '+ Add Chart';
685
+ addBtn.onclick = () => this.addChart(i);
686
  colDiv.appendChild(addBtn);
687
 
688
  container.appendChild(colDiv);
689
  }
690
+ },
691
 
692
+ addChart(colIdx, forceId = null, forceConfig = null) {
693
+ const id = forceId || 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
694
+
695
+ // Register in State
696
+ this.state.charts[id] = {
697
+ col: colIdx,
698
+ config: forceConfig || {},
699
+ instance: null,
700
+ interval: null
701
+ };
702
+
703
+ if (!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
704
+ if (!this.state.layout[colIdx].includes(id)) {
705
+ this.state.layout[colIdx].push(id);
706
+ }
707
+
708
+ // Re-render the specific column to recalculate heights
709
+ this.renderColumn(colIdx);
710
+
711
+ // Auto-plot if configured
712
+ if (forceConfig && forceConfig.root) {
713
+ setTimeout(() => ChartLogic.plot(id), 50);
714
+ }
715
+
716
+ this.saveToStorage();
717
  },
718
 
719
+ removeChart(id) {
720
+ if(!confirm("Remove this chart?")) return;
721
+
722
+ ChartLogic.stopLive(id);
723
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
724
+
725
+ const colIdx = this.state.charts[id].col;
726
+ this.state.layout[colIdx] = this.state.layout[colIdx].filter(x => x !== id);
727
+ delete this.state.charts[id];
728
+
729
+ this.renderColumn(colIdx);
730
+ this.saveToStorage();
731
+ },
732
+
733
+ renderColumn(colIdx) {
734
+ const colDiv = document.getElementById(`col-${colIdx}`);
735
+ // If col doesn't exist (switching layouts), just full render
736
+ if(!colDiv) { this.renderGrid(); return; }
737
+
738
+ colDiv.innerHTML = '';
739
+ const chartIds = this.state.layout[colIdx];
740
+
741
+ chartIds.forEach(id => {
742
+ colDiv.appendChild(this.createChartElement(id, chartIds.length));
743
+ setTimeout(() => ChartLogic.render(id), 10);
744
+ });
745
+
746
+ const addBtn = document.createElement('div');
747
+ addBtn.className = 'col-add-btn';
748
+ addBtn.innerHTML = '+ Add Chart';
749
+ addBtn.onclick = () => this.addChart(colIdx);
750
+ colDiv.appendChild(addBtn);
751
+ },
752
+
753
+ createChartElement(id, totalInCol) {
754
  const div = document.createElement('div');
755
  div.className = 'chart-card';
756
  div.id = `card-${id}`;
757
+
758
+ // Dynamic Height Calculation
759
+ // Viewport - Header (55) - Padding/Gap (~20)
760
+ // If total <= 4, calculate percent height to fill screen exactly
761
+ if (totalInCol <= 4 && totalInCol > 0) {
762
+ // Flex grow to fill the column container which is 100% height
763
+ div.style.flex = "1";
764
+ div.style.minHeight = "0"; // allow shrink
765
+ } else {
766
+ // More than 4, fix height to 300px, creating scroll
767
+ div.style.flex = "none";
768
+ div.style.height = "300px";
769
+ }
770
+
771
  div.innerHTML = `
772
  <div class="card-header">
773
+ <div class="card-title" id="title-${id}">
774
  <span class="status-dot"></span>
775
  <span>New Chart</span>
776
  </div>
777
  <div class="card-actions">
778
  <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
779
+ <button class="icon-btn danger" onclick="GlobalApp.removeChart('${id}')">✕</button>
780
  </div>
781
  </div>
782
  <div class="card-body" id="body-${id}">
783
  <div class="empty-state">
784
+ <p>Select a Symbol</p>
785
+ <button class="btn-accent" onclick="ConfigModal.open('${id}')">Configure</button>
786
  </div>
787
  </div>
788
  `;
789
  return div;
790
  },
791
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  onDateChange() {
793
  this.state.date = document.getElementById('globalDate').value;
794
+ // Auto-check live
795
  const today = new Date().toISOString().split('T')[0];
796
+ if(this.state.date === today) {
 
 
797
  this.state.mode = 'LIVE';
798
  document.getElementById('globalMode').value = 'LIVE';
799
+ }
 
800
  this.saveToStorage();
 
801
  Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
802
  },
803
 
804
  onModeChange() {
805
  this.state.mode = document.getElementById('globalMode').value;
806
  this.saveToStorage();
 
 
 
 
 
807
  Object.keys(this.state.charts).forEach(id => {
808
+ if(this.state.mode === 'LIVE') ChartLogic.startLive(id);
809
  else ChartLogic.stopLive(id);
810
  });
811
  }
812
  };
813
 
 
 
 
814
  /**
815
  * --- CHART LOGIC ---
816
  */
817
  const ChartLogic = {
818
  async plot(id, isUpdate=false) {
819
  const chartData = GlobalApp.state.charts[id];
820
+ if (!chartData || !chartData.config || !chartData.config.root) return;
821
  const cfg = chartData.config;
 
822
 
823
+ // UI Update
824
+ const titleEl = document.querySelector(`#card-${id} .card-title`);
825
+ if(!titleEl) return;
 
826
 
827
+ const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
828
  titleEl.innerHTML = `
829
+ <span class="status-dot ${dotClass}" id="dot-${id}"></span>
830
+ ${cfg.root} <span style="color:var(--accent); margin-left:5px;">${cfg.expiry}</span>
831
+ <span class="card-meta" style="margin-left:5px;">${cfg.instrument} • ${cfg.timeframe}</span>
832
  `;
833
 
 
834
  const body = document.getElementById(`body-${id}`);
835
+ if (!body.querySelector('canvas')) {
836
+ body.innerHTML = `<div class="chart-canvas"><canvas id="canvas-${id}"></canvas></div>`;
837
  }
838
 
839
+ if(isUpdate) {
840
+ const dot = document.getElementById(`dot-${id}`);
841
+ if(dot) dot.classList.add('retry');
 
 
842
  }
843
 
844
  try {
 
852
  instrument: cfg.instrument,
853
  timeframe: cfg.timeframe,
854
  start_time: cfg.start,
855
+ end_time: cfg.end
856
  })
857
  });
858
  const data = await res.json();
859
 
860
+ // Handle No Data
861
+ if(data.error || !data.labels || data.labels.length === 0) {
862
  if(GlobalApp.state.mode === 'LIVE') {
863
+ // Keep old data, remove retry class after a bit
864
+ console.log(`[${id}] No new data`);
865
+ return;
866
  } else {
867
+ if(chartData.instance) chartData.instance.destroy();
868
+ body.innerHTML = '<div class="empty-state">No Data Available</div>';
869
  return;
870
  }
871
  }
872
 
873
+ if(isUpdate) {
874
+ const dot = document.getElementById(`dot-${id}`);
875
+ if(dot) dot.classList.remove('retry');
876
+ }
877
 
878
+ // Compute Datasets
879
  const datasets = cfg.series.map(s => {
880
+ const computed = data.labels.map((_, i) => {
881
+ const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
882
+ const B = data.B[i] || 0, S = data.S[i] || 0;
883
+ let f = s.formula
884
+ .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
885
+ .replace(/\$B/g, B).replace(/\$S/g, S);
886
+ try { return new Function('return ' + f)(); } catch { return null; }
887
  });
888
+
889
  return {
890
+ label: s.label,
891
+ data: computed,
892
+ borderColor: s.color,
893
+ backgroundColor: s.color,
894
+ borderWidth: 1.5,
895
+ pointRadius: 0,
896
+ pointHoverRadius: 4,
897
+ yAxisID: s.axis === 'left' ? 'y' : 'y1',
898
+ tension: 0.1
899
  };
900
  });
901
 
902
  this.renderCanvas(id, data.labels, datasets);
903
 
904
+ } catch (e) {
905
+ console.error("Plot Error", e);
906
+ }
907
  },
908
 
909
  renderCanvas(id, labels, datasets) {
910
  const ctx = document.getElementById(`canvas-${id}`);
911
+ if (!ctx) return;
912
+
913
+ const chartData = GlobalApp.state.charts[id];
914
 
915
+ if (chartData.instance) {
916
+ chartData.instance.data.labels = labels;
917
+ chartData.instance.data.datasets = datasets;
918
+ chartData.instance.update('none');
919
  } else {
920
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
921
+ chartData.instance = new Chart(ctx.getContext('2d'), {
922
  type: 'line',
923
  data: { labels, datasets },
924
  options: {
925
+ responsive: true,
926
+ maintainAspectRatio: false,
927
+ animation: false,
928
  interaction: { mode: 'index', intersect: false },
929
+ plugins: {
930
+ legend: { labels: { color: '#8b949e', font: {size:10} } },
931
+ tooltip: { backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor:'#30363d', borderWidth:1 }
932
  },
933
  scales: {
934
+ x: { grid: { color: '#30363d', tickLength: 0 }, ticks: { color: '#8b949e', maxTicksLimit: 8 } },
935
+ y: { type: 'linear', display: true, position: 'left', grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
936
+ y1: { type: 'linear', display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#8b949e' } }
937
  }
938
  }
939
  });
 
942
 
943
  startLive(id) {
944
  this.stopLive(id);
945
+ this.plot(id);
946
+ GlobalApp.state.charts[id].interval = setInterval(() => {
947
+ this.plot(id, true);
 
 
948
  }, 15000);
949
  },
950
 
951
  stopLive(id) {
952
  const c = GlobalApp.state.charts[id];
953
+ if(c && c.interval) {
954
+ clearInterval(c.interval);
955
+ c.interval = null;
956
+ }
957
+ },
958
+
959
+ render(id) {
960
+ const c = GlobalApp.state.charts[id];
961
+ if(c.config && c.config.root) {
962
+ if(c.instance) c.instance.destroy();
963
+ c.instance = null;
964
+ this.plot(id);
965
+ if(GlobalApp.state.mode === 'LIVE') this.startLive(id);
966
+ }
967
  }
968
  };
969
 
 
971
  * --- CONFIG MODAL ---
972
  */
973
  const ConfigModal = {
974
+ currentId: null,
975
  isGlobal: false,
976
 
977
  async open(id) {
978
+ this.currentId = id;
979
  this.isGlobal = false;
980
+ document.getElementById('cfgModalTitle').textContent = "Configure Chart";
981
  document.getElementById('configModalOverlay').style.display = 'flex';
 
982
  document.getElementById('cfgForm').style.display = 'block';
983
 
984
+ const c = GlobalApp.state.charts[id].config;
985
+ await this.loadRoots(c.root);
986
 
987
+ document.getElementById('cfgTF').value = c.timeframe || '1min';
988
+ document.getElementById('cfgStart').value = c.start || '09:15';
989
+ document.getElementById('cfgEnd').value = c.end || '15:30';
990
 
991
+ const series = c.series && c.series.length ? c.series : GlobalApp.defaults;
992
+ this.renderSeries(series);
993
+
994
+ if(c.root) {
995
+ document.getElementById('cfgRoot').value = c.root;
996
+ await this.onRootChange(c.expiry, c.instrument);
997
  }
998
  },
999
 
1000
  openDefaults() {
1001
  this.isGlobal = true;
1002
+ document.getElementById('cfgModalTitle').textContent = "Global Settings";
1003
  document.getElementById('configModalOverlay').style.display = 'flex';
1004
+ // Hack: keep form visible but maybe ignore inputs. Just series list matters.
 
 
 
1005
  this.renderSeries(GlobalApp.defaults);
1006
  },
1007
 
1008
  close() { document.getElementById('configModalOverlay').style.display = 'none'; },
1009
 
1010
+ async loadRoots(preSelect) {
1011
+ const rSel = document.getElementById('cfgRoot');
1012
+ rSel.innerHTML = '<option>Loading...</option>';
1013
  try {
1014
+ const date = GlobalApp.state.date;
1015
+ const res = await fetch(`/api/roots?date=${date}`);
1016
  const roots = await res.json();
1017
+ rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r => `<option>${r}</option>`).join('');
1018
+ if(preSelect && roots.includes(preSelect)) rSel.value = preSelect;
1019
+ } catch(e) { rSel.innerHTML = '<option>Error</option>'; }
1020
  },
1021
 
1022
  async onRootChange(preExp, preInst) {
1023
  const root = document.getElementById('cfgRoot').value;
1024
+ const date = GlobalApp.state.date;
1025
  const eSel = document.getElementById('cfgExp');
1026
  eSel.innerHTML = '<option>Loading...</option>';
1027
+
1028
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
1029
  const exps = await res.json();
1030
+ eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e => `<option>${e}</option>`).join('');
1031
+
1032
  if(preExp && exps.includes(preExp)) {
1033
  eSel.value = preExp;
1034
  await this.onExpChange(preInst);
1035
  } else {
1036
+ // Smart Auto Select
1037
+ const auto = await fetch(`/api/auto_config?date=${date}&root=${root}`).then(r=>r.json());
1038
+ if(auto.current) {
1039
+ eSel.value = auto.current.expiry;
1040
+ await this.onExpChange(auto.current.instrument);
1041
+ }
1042
  }
1043
  },
1044
 
1045
  async onExpChange(preInst) {
1046
  const root = document.getElementById('cfgRoot').value;
1047
  const exp = document.getElementById('cfgExp').value;
1048
+ const date = GlobalApp.state.date;
1049
  const iSel = document.getElementById('cfgInst');
1050
  iSel.innerHTML = '<option>Loading...</option>';
1051
+
1052
+ const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
1053
  const insts = await res.json();
1054
+ iSel.innerHTML = `<option value="">-- Select --</option>` + insts.map(i => `<option>${i}</option>`).join('');
1055
+
1056
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
1057
+ else if(insts.length > 0) iSel.value = insts[0];
1058
  },
1059
 
1060
  renderSeries(list) {
1061
+ const container = document.getElementById('cfgSeriesList');
1062
+ container.innerHTML = '';
1063
  list.forEach(s => {
1064
+ const div = document.createElement('div');
1065
+ div.className = 'series-row';
1066
+ div.innerHTML = `
1067
  <input type="color" value="${s.color}" class="s-color">
1068
  <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
1069
  <input type="text" value="${s.label}" class="s-label" placeholder="Label">
1070
+ <select class="s-axis">
1071
+ <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1072
+ <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1073
+ </select>
1074
+ <button class="icon-btn danger" onclick="this.parentElement.remove()">✕</button>
1075
  `;
1076
+ container.appendChild(div);
1077
  });
1078
  },
1079
 
1080
  addSeriesRow() {
1081
+ const container = document.getElementById('cfgSeriesList');
1082
+ const div = document.createElement('div');
1083
+ div.className = 'series-row';
1084
+ div.innerHTML = `
1085
  <input type="color" value="#ffffff" class="s-color">
1086
  <input type="text" value="$P" class="s-formula">
1087
  <input type="text" value="New" class="s-label">
1088
  <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1089
+ <button class="icon-btn danger" onclick="this.parentElement.remove()">✕</button>
1090
  `;
1091
+ container.appendChild(div);
1092
+ },
1093
+
1094
+ insertVar(v) {
1095
+ const inputs = document.querySelectorAll('.s-formula');
1096
+ if(inputs.length > 0) {
1097
+ const last = inputs[inputs.length-1];
1098
+ last.value = last.value + v;
1099
+ }
1100
  },
1101
 
1102
  save() {
1103
  const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1104
+ const newSeries = Array.from(rows).map(r => ({
1105
  color: r.querySelector('.s-color').value,
1106
  formula: r.querySelector('.s-formula').value,
1107
  label: r.querySelector('.s-label').value,
 
1109
  }));
1110
 
1111
  if(this.isGlobal) {
1112
+ GlobalApp.defaults = newSeries;
1113
+ GlobalApp.showToast("Defaults Updated");
1114
  } else {
1115
  const root = document.getElementById('cfgRoot').value;
1116
  const exp = document.getElementById('cfgExp').value;
1117
  const inst = document.getElementById('cfgInst').value;
 
1118
 
1119
+ if(!root || !exp || !inst) { alert("Incomplete Selection"); return; }
1120
+
1121
+ const config = {
1122
  root, expiry: exp, instrument: inst,
1123
  timeframe: document.getElementById('cfgTF').value,
1124
  start: document.getElementById('cfgStart').value,
1125
  end: document.getElementById('cfgEnd').value,
1126
+ series: newSeries
1127
  };
1128
+
1129
+ GlobalApp.state.charts[this.currentId].config = config;
1130
  GlobalApp.saveToStorage();
1131
+ ChartLogic.render(this.currentId);
1132
+ GlobalApp.showToast("Chart Updated");
1133
  }
 
 
 
1134
  this.close();
1135
  }
1136
  };
1137
 
 
1138
  window.onload = () => GlobalApp.init();
1139
 
1140
  </script>