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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +602 -607
app.py CHANGED
@@ -49,6 +49,7 @@ def home():
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,7 +206,7 @@ def fetch_series(
205
 
206
 
207
  # ==========================================
208
- # 5. FRONTEND TEMPLATE (REVISED UI)
209
  # ==========================================
210
 
211
  HTML_TEMPLATE = """
@@ -213,252 +214,314 @@ 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 --- */
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;
229
- --text-secondary: #8b949e;
230
- --accent: #2f81f7;
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 {
358
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
359
- background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 500;
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
 
393
  </style>
394
  </head>
395
  <body>
396
 
397
- <!-- HEADER -->
398
- <div class="top-bar">
399
- <div class="logo">DepthChain <span>PRO</span></div>
 
 
 
 
 
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>
431
 
432
- <!-- CONFIG MODAL -->
433
- <div class="modal-overlay" id="configModalOverlay">
434
  <div class="modal">
435
- <div class="modal-head">
436
- <span id="cfgModalTitle">Configure Chart</span>
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
-
453
- <div class="form-group">
454
  <label>Instrument</label>
455
- <select id="cfgInst" style="width:100%"></select>
456
  </div>
457
 
458
- <div class="form-row">
459
- <div class="form-group">
460
  <label>Timeframe</label>
461
- <select id="cfgTF" style="width:100%">
462
  <option value="1min">1 Minute</option>
463
  <option value="3min">3 Minutes</option>
464
  <option value="5min">5 Minutes</option>
@@ -467,89 +530,61 @@ HTML_TEMPLATE = """
467
  <option value="1hour">1 Hour</option>
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>
519
-
520
  /**
521
- * --- GLOBAL STATE MANAGER ---
522
  */
523
- const GlobalApp = {
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
  ],
537
 
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() {
@@ -558,287 +593,270 @@ const GlobalApp = {
558
  const dates = await res.json();
559
  const sel = document.getElementById('globalDate');
560
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
561
- if(dates.length) this.state.date = dates[0];
562
- } catch(e) { console.error("Dates error", e); }
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,
623
- mode: this.state.mode,
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() {
656
  const container = document.getElementById('gridContainer');
657
  container.innerHTML = '';
 
658
 
659
- for (let i = 0; i < this.state.columns; i++) {
660
  const colDiv = document.createElement('div');
661
- colDiv.className = 'column';
662
  colDiv.id = `col-${i}`;
663
-
664
- // 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 {
@@ -846,36 +864,24 @@ const ChartLogic = {
846
  method: 'POST',
847
  headers: {'Content-Type': 'application/json'},
848
  body: JSON.stringify({
849
- date: GlobalApp.state.date,
850
  root: cfg.root,
851
  expiry: cfg.expiry,
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;
@@ -895,208 +901,190 @@ const ChartLogic = {
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
- });
940
- }
941
- },
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
 
970
  /**
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() {
@@ -1109,14 +1097,14 @@ const ConfigModal = {
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,
@@ -1126,16 +1114,23 @@ const ConfigModal = {
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>
1141
  </body>
 
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
 
207
 
208
  # ==========================================
209
+ # 5. FRONTEND TEMPLATE (REFINED UI)
210
  # ==========================================
211
 
212
  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
  <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">
538
  </div>
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
  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
 
862
  try {
 
864
  method: 'POST',
865
  headers: {'Content-Type': 'application/json'},
866
  body: JSON.stringify({
867
+ date: App.state.date,
868
  root: cfg.root,
869
  expiry: cfg.expiry,
870
  instrument: cfg.instrument,
871
  timeframe: cfg.timeframe,
872
  start_time: cfg.start,
873
+ end_time: endTime
874
  })
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;
 
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) {
1029
  const root = document.getElementById('cfgRoot').value;
 
1030
  const eSel = document.getElementById('cfgExp');
1031
  eSel.innerHTML = '<option>Loading...</option>';
1032
 
1033
+ const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1034
  const exps = await res.json();
1035
+ eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e=>`<option>${e}</option>`).join('');
1036
+
1037
  if(preExp && exps.includes(preExp)) {
1038
  eSel.value = preExp;
1039
  await this.onExpChange(preInst);
1040
  } else {
1041
+ // Auto Config
1042
+ const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
1043
+ if(auto.current) {
1044
+ eSel.value = auto.current.expiry;
1045
+ await this.onExpChange(auto.current.instrument);
1046
+ }
1047
  }
1048
  },
1049
 
1050
  async onExpChange(preInst) {
1051
  const root = document.getElementById('cfgRoot').value;
1052
  const exp = document.getElementById('cfgExp').value;
 
1053
  const iSel = document.getElementById('cfgInst');
1054
  iSel.innerHTML = '<option>Loading...</option>';
1055
 
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() {
 
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,
 
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>