topsecrettraders commited on
Commit
d0548f0
·
verified ·
1 Parent(s): ce0cce3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +625 -607
app.py CHANGED
@@ -48,9 +48,8 @@ def home():
48
 
49
  @app.get("/api/dates")
50
  def get_dates():
51
- """
52
- Fetch distinct dates.
53
- """
54
  data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "50000"})
55
  dates = set()
56
  for row in data:
@@ -88,9 +87,9 @@ def get_auto_config(date: str, root: str):
88
  expiry_map[exp] = list(existing.union(new_keys))
89
 
90
  all_expiries = sorted(expiry_map.keys())
 
91
  valid_expiries = [e for e in all_expiries if e >= date]
92
- if not valid_expiries:
93
- valid_expiries = all_expiries
94
 
95
  response = {"expiries": all_expiries, "current": None, "next": None}
96
 
@@ -113,7 +112,6 @@ def get_auto_config(date: str, root: str):
113
 
114
  @app.get("/api/expiries")
115
  def get_expiries(date: str, root: str):
116
- """Fetch distinct expiries."""
117
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
118
  data = sb_get(f"/history_buckets_upstox?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
119
  exps = set(row['expiry'] for row in data if row.get('expiry'))
@@ -121,7 +119,6 @@ def get_expiries(date: str, root: str):
121
 
122
  @app.get("/api/instruments")
123
  def get_instruments(date: str, root: str, expiry: str):
124
- """Fetch instruments."""
125
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
126
  data = sb_get(f"/history_buckets_upstox?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
127
  instruments = set()
@@ -160,7 +157,7 @@ def fetch_series(
160
  inst_data = row['data'].get(instrument)
161
  if not inst_data: continue
162
 
163
- # Extract buckets (0s to 50s)
164
  p_arr = inst_data.get('p', []) or []
165
  b_arr = inst_data.get('b', []) or []
166
  s_arr = inst_data.get('s', []) or []
@@ -181,7 +178,7 @@ def fetch_series(
181
  continue
182
 
183
  if not records:
184
- return JSONResponse({"error": "No data found", "labels": [], "P": []})
185
 
186
  df = pd.DataFrame(records)
187
  df.set_index('ts', inplace=True)
@@ -207,7 +204,7 @@ def fetch_series(
207
 
208
 
209
  # ==========================================
210
- # 5. FRONTEND TEMPLATE (NEW UI)
211
  # ==========================================
212
 
213
  HTML_TEMPLATE = """
@@ -215,212 +212,344 @@ HTML_TEMPLATE = """
215
  <html lang="en">
216
  <head>
217
  <meta charset="UTF-8">
218
- <title>DepthChain Live Pro</title>
219
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
220
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
 
221
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
222
  <style>
223
- /* --- THEME --- */
224
  :root {
225
- --bg-body: #0d1117;
226
- --bg-header: #161b22;
227
- --bg-card: #010409;
228
- --bg-panel: #21262d;
229
- --border: #30363d;
230
- --text-primary: #e6edf3;
231
- --text-secondary: #8b949e;
232
- --accent: #2f81f7;
233
- --success: #238636;
234
- --danger: #da3633;
235
- --warning: #d29922;
 
 
 
 
 
 
 
236
  }
237
- * { box-sizing: border-box; margin: 0; padding: 0; }
238
- body {
239
- background: var(--bg-body); color: var(--text-primary);
240
- font-family: 'Inter', sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden;
 
 
 
 
 
 
 
 
241
  }
242
 
243
- /* --- TOP BAR --- */
244
- .top-bar {
245
- height: 50px; background: var(--bg-header); border-bottom: 1px solid var(--border);
246
- display: flex; align-items: center; justify-content: space-between; padding: 0 15px; flex-shrink: 0;
 
 
 
 
 
 
 
247
  }
248
- .logo { font-weight: 800; font-size: 16px; margin-right: 20px; color: var(--text-primary); }
249
- .logo span { color: var(--accent); }
250
-
251
- .controls { display: flex; gap: 10px; align-items: center; }
252
-
253
- select, button, input {
254
- background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary);
255
- padding: 5px 10px; border-radius: 4px; font-size: 12px; font-family: inherit; outline: none; cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
- select:focus, input:focus { border-color: var(--accent); }
258
- button:hover { background: var(--border); }
259
- .btn-accent { background: var(--accent); color: white; border: none; font-weight: 600; }
260
- .btn-accent:hover { background: #58a6ff; }
261
- .btn-danger { color: var(--danger); border-color: var(--border); }
262
- .btn-danger:hover { background: rgba(218, 54, 51, 0.1); border-color: var(--danger); }
263
-
264
- .layout-popover { position: relative; }
265
- .layout-menu {
266
- position: absolute; top: 35px; right: 0; background: var(--bg-header);
267
- border: 1px solid var(--border); border-radius: 6px; padding: 10px;
268
- display: none; grid-template-columns: 1fr; gap: 5px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.5);
269
- width: 150px;
270
  }
271
- .layout-menu.active { display: grid; }
272
- .layout-option {
273
- padding: 8px; text-align: center; border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
274
  }
275
- .layout-option:hover { background: var(--bg-panel); border-color: var(--text-secondary); }
276
 
277
- /* --- MAIN GRID --- */
278
- .main-content { flex: 1; overflow: hidden; display: flex; padding: 5px; gap: 5px; }
 
 
 
 
279
 
280
- .column {
281
- flex: 1; display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
282
- padding: 5px; min-width: 300px;
 
 
 
 
 
 
 
 
 
283
  }
284
- .column-add-btn {
285
- padding: 10px; border: 1px dashed var(--border); color: var(--text-secondary);
286
- text-align: center; border-radius: 6px; cursor: pointer; margin-top: auto; font-size: 12px;
 
 
 
 
 
 
 
287
  }
288
- .column-add-btn:hover { border-color: var(--text-primary); color: var(--text-primary); }
 
 
 
 
 
 
289
 
290
  /* --- CHART CARD --- */
291
  .chart-card {
292
- background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
293
- display: flex; flex-direction: column; height: 400px; min-height: 400px; position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
- .card-header {
296
- height: 35px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
297
- padding: 0 10px; background: var(--bg-header); font-size: 12px;
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
- .card-title { font-weight: 600; display: flex; gap: 8px; align-items: center; }
300
- .card-meta { color: var(--text-secondary); font-family: 'JetBrains Mono', monospace; font-size: 11px; }
301
- .card-actions { display: flex; gap: 5px; }
302
- .icon-btn { padding: 2px 6px; font-size: 14px; border: none; background: transparent; color: var(--text-secondary); }
303
- .icon-btn:hover { color: var(--text-primary); background: var(--border); }
304
 
305
- .card-body { flex: 1; position: relative; padding: 5px; overflow: hidden; }
 
 
306
  .empty-state {
307
  display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
308
- color: var(--text-secondary); font-size: 13px; gap: 10px;
309
  }
310
-
311
- /* --- STATUS DOTS --- */
312
- .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--text-secondary); }
313
- .status-dot.live { background: var(--success); box-shadow: 0 0 5px var(--success); animation: pulse 2s infinite; }
314
- .status-dot.retry { background: var(--warning); }
315
- .status-dot.hist { background: var(--accent); }
316
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  /* --- MODAL --- */
319
  .modal-overlay {
320
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
321
- background: rgba(0,0,0,0.7); backdrop-filter: blur(2px); z-index: 500;
322
- display: none; justify-content: center; align-items: center;
323
  }
324
- .modal {
325
- background: var(--bg-header); border: 1px solid var(--border); border-radius: 8px;
326
- width: 500px; max-width: 95%; max-height: 90vh; display: flex; flex-direction: column;
327
- box-shadow: 0 20px 40px rgba(0,0,0,0.5);
 
 
 
 
328
  }
329
- .modal-head { padding: 15px; border-bottom: 1px solid var(--border); font-weight: 700; display: flex; justify-content: space-between; }
330
- .modal-body { padding: 20px; overflow-y: auto; }
331
- .modal-foot { padding: 15px; border-top: 1px solid var(--border); text-align: right; }
332
-
 
 
 
333
  .form-group { margin-bottom: 15px; }
334
- .form-group label { display: block; margin-bottom: 5px; color: var(--text-secondary); font-size: 11px; text-transform: uppercase; }
335
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  /* Series List */
338
- .series-row { display: grid; grid-template-columns: 30px 1fr 1fr 60px 20px; gap: 5px; margin-bottom: 5px; align-items: center; }
339
- .series-row input[type="color"] { padding: 0; height: 30px; border: none; background: none; }
340
 
341
  /* Toast */
342
  .toast {
343
- position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: white;
344
- padding: 10px 20px; border-radius: 4px; font-weight: 600; font-size: 13px;
345
- transform: translateY(100px); transition: transform 0.3s; z-index: 1000;
 
346
  }
347
- .toast.show { transform: translateY(0); }
 
348
 
349
  </style>
350
  </head>
351
  <body>
352
 
353
  <!-- HEADER -->
354
- <div class="top-bar">
355
- <div class="logo">DepthChain <span>PRO</span></div>
 
 
356
 
357
  <div class="controls">
358
- <!-- Date Selection -->
359
- <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
360
 
361
- <!-- Mode Selection -->
362
- <select id="globalMode" onchange="GlobalApp.onModeChange()">
363
- <option value="HIST">Historical</option>
364
- <option value="LIVE">Live (Auto Refresh)</option>
365
- </select>
366
-
367
- <div style="width: 1px; height: 20px; background: var(--border); margin: 0 10px;"></div>
368
 
369
  <!-- Layout Selector -->
370
- <div class="layout-popover">
371
- <button onclick="document.getElementById('layoutMenu').classList.toggle('active')">
372
- Layout / Columns
373
- </button>
374
- <div class="layout-menu" id="layoutMenu">
375
- <div class="layout-option" onclick="GlobalApp.setLayout(1)">1 Column</div>
376
- <div class="layout-option" onclick="GlobalApp.setLayout(2)">2 Columns</div>
377
- <div class="layout-option" onclick="GlobalApp.setLayout(3)">3 Columns</div>
378
- </div>
379
- </div>
380
 
381
- <button onclick="ConfigModal.openDefaults()">Global Defaults</button>
382
- <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset App</button>
383
  </div>
384
- </div>
385
 
386
- <!-- GRID CONTAINER -->
387
- <div class="main-content" id="gridContainer">
388
- <!-- Columns injected via JS -->
389
  </div>
390
 
391
- <!-- MODALS -->
392
- <div class="modal-overlay" id="configModalOverlay">
393
- <div class="modal">
394
- <div class="modal-head">
395
- <span id="cfgModalTitle">Configure Chart</span>
396
- <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
397
  </div>
398
- <div class="modal-body">
399
- <!-- Loading State -->
400
- <div id="cfgLoader" style="display:none; text-align:center; padding:20px;">Loading...</div>
401
-
402
- <!-- Config Form -->
403
  <div id="cfgForm">
404
  <div class="form-row">
405
  <div class="form-group">
406
- <label>Symbol (Root)</label>
407
- <select id="cfgRoot" onchange="ConfigModal.onRootChange()"></select>
408
  </div>
409
  <div class="form-group">
410
- <label>Expiry</label>
411
- <select id="cfgExp" onchange="ConfigModal.onExpChange()"></select>
412
  </div>
413
  </div>
414
 
415
  <div class="form-group">
416
- <label>Instrument</label>
417
- <select id="cfgInst"></select>
418
  </div>
419
-
420
  <div class="form-row">
421
  <div class="form-group">
422
- <label>Timeframe</label>
423
- <select id="cfgTF">
424
  <option value="1min">1 Minute</option>
425
  <option value="3min">3 Minutes</option>
426
  <option value="5min">5 Minutes</option>
@@ -430,76 +559,101 @@ HTML_TEMPLATE = """
430
  </select>
431
  </div>
432
  <div class="form-group">
433
- <label>Time Range</label>
434
  <div style="display:flex; gap:5px;">
435
- <input type="time" id="cfgStart" value="09:15">
436
- <input type="time" id="cfgEnd" value="15:30">
437
  </div>
438
  </div>
439
  </div>
440
 
441
- <hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
442
-
443
- <div class="form-group">
444
- <label>Plot Lines (Variables: $P, $B, $S, $V, $OI)</label>
445
- <div id="cfgSeriesList"></div>
446
- <button style="width:100%; margin-top:5px;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
447
  </div>
448
  </div>
449
  </div>
450
- <div class="modal-foot">
451
- <button onclick="ConfigModal.close()">Cancel</button>
452
- <button class="btn-accent" onclick="ConfigModal.save()">Save Configuration</button>
453
  </div>
454
  </div>
455
  </div>
456
 
457
- <!-- RESTORE SESSION POPUP -->
458
- <div class="modal-overlay" id="restoreModal" style="display:none;">
459
- <div class="modal" style="width: 350px;">
460
- <div class="modal-head">Restore Session?</div>
461
- <div class="modal-body">
462
- We found a saved layout from your previous visit.
463
- </div>
464
- <div class="modal-foot">
465
- <button class="btn-danger" onclick="GlobalApp.initNew()">Start Fresh</button>
466
- <button class="btn-accent" onclick="GlobalApp.loadFromStorage()">Restore</button>
467
- </div>
468
- </div>
469
  </div>
470
 
471
- <div id="toast" class="toast">Settings Saved</div>
472
-
473
  <script>
474
-
475
  /**
476
- * --- GLOBAL STATE MANAGER ---
477
  */
478
- const GlobalApp = {
479
  state: {
480
- date: '',
481
- mode: 'HIST', // HIST or LIVE
482
- columns: 1,
483
- layout: [[], [], []], // 3 potential columns of chart IDs
484
- charts: {} // ID -> { config: {}, instance: ChartJS, interval: null }
485
  },
486
-
 
487
  defaults: [
488
- { formula: '$P', color: '#58a6ff', label: 'Price', axis: 'left' },
489
- { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
490
- { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
491
  ],
492
 
493
  async init() {
494
- // Load dates first
495
  await this.loadDates();
496
 
497
- // Check local storage
498
- if (localStorage.getItem('dc_state')) {
499
- document.getElementById('restoreModal').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  } else {
501
- this.initNew();
502
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  },
504
 
505
  async loadDates() {
@@ -508,459 +662,344 @@ const GlobalApp = {
508
  const dates = await res.json();
509
  const sel = document.getElementById('globalDate');
510
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
511
- if(dates.length) this.state.date = dates[0];
512
- } catch(e) { console.error("Dates error", e); }
513
- },
514
-
515
- initNew() {
516
- document.getElementById('restoreModal').style.display = 'none';
517
- this.setLayout(2); // Default 2 cols
518
- this.addChart(0); // 1 chart in col 1
519
- this.addChart(1); // 1 chart in col 2
520
  },
521
 
522
- loadFromStorage() {
523
- try {
524
- const raw = localStorage.getItem('dc_state');
525
- const saved = JSON.parse(raw);
526
-
527
- document.getElementById('restoreModal').style.display = 'none';
528
- document.getElementById('globalDate').value = saved.date;
529
- document.getElementById('globalMode').value = saved.mode;
530
-
531
- this.state.date = saved.date;
532
- this.state.mode = saved.mode;
533
-
534
- // Rebuild Layout
535
- this.setLayout(saved.columns);
536
-
537
- // Rebuild Charts
538
- saved.layout.forEach((colCharts, colIdx) => {
539
- colCharts.forEach(chartData => {
540
- this.addChart(colIdx, chartData.id, chartData.config);
541
- });
542
- });
543
-
544
- } catch(e) {
545
- console.error("Load failed", e);
546
- this.initNew();
547
- }
548
  },
549
 
550
- saveToStorage() {
551
- const minimalLayout = [[], [], []];
552
- this.state.layout.forEach((col, idx) => {
553
- minimalLayout[idx] = col.map(id => ({
554
- id: id,
555
- config: this.state.charts[id].config
556
- }));
557
- });
558
-
559
- const dump = {
560
- date: this.state.date,
561
- mode: this.state.mode,
562
- columns: this.state.columns,
563
- layout: minimalLayout
564
- };
565
- localStorage.setItem('dc_state', JSON.stringify(dump));
566
- this.showToast("Layout Saved");
567
- },
568
-
569
- resetApp() {
570
- if(!confirm("Clear all settings and reset?")) return;
571
- localStorage.removeItem('dc_state');
572
- location.reload();
573
  },
574
 
575
- showToast(msg) {
576
- const t = document.getElementById('toast');
577
- t.textContent = msg;
578
- t.classList.add('show');
579
- setTimeout(() => t.classList.remove('show'), 2000);
580
- },
581
-
582
- // --- LAYOUT LOGIC ---
583
- setLayout(cols) {
584
- this.state.columns = cols;
585
- document.getElementById('layoutMenu').classList.remove('active');
586
- this.renderGrid();
587
  },
588
 
589
- renderGrid() {
590
- const container = document.getElementById('gridContainer');
591
- container.innerHTML = '';
592
-
593
- for (let i = 0; i < this.state.columns; i++) {
594
- const colDiv = document.createElement('div');
595
- colDiv.className = 'column';
596
- colDiv.id = `col-${i}`;
597
-
598
- // Append existing charts for this column from state
599
- const chartIds = this.state.layout[i] || [];
600
- chartIds.forEach(id => {
601
- const el = this.createChartElement(id);
602
- colDiv.appendChild(el);
603
- // Re-initialize chart if needed (canvas context is lost on re-append)
604
- setTimeout(() => ChartLogic.render(id), 0);
605
- });
606
-
607
- // Add button
608
- const addBtn = document.createElement('div');
609
- addBtn.className = 'column-add-btn';
610
- addBtn.innerHTML = '+ Add Chart';
611
- addBtn.onclick = () => this.addChart(i);
612
- colDiv.appendChild(addBtn);
613
-
614
- container.appendChild(colDiv);
615
- }
616
  },
617
 
618
- addChart(colIdx, forceId = null, forceConfig = null) {
619
- const id = forceId || 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
 
 
620
 
621
- // Init State
622
- this.state.charts[id] = {
623
- col: colIdx,
624
- config: forceConfig || {}, // Empty config means "Not Configured"
625
- instance: null,
626
- interval: null,
627
- lastFetch: null
628
- };
629
-
630
- // Add to Layout Arrays
631
- if (!this.state.layout[colIdx]) this.state.layout[colIdx] = [];
632
- // prevent duplicate push if restoring
633
- if (!this.state.layout[colIdx].includes(id)) {
634
- this.state.layout[colIdx].push(id);
635
- }
636
-
637
- // Render UI
638
- const colDiv = document.getElementById(`col-${colIdx}`);
639
- const btn = colDiv.querySelector('.column-add-btn');
640
- const el = this.createChartElement(id);
641
- colDiv.insertBefore(el, btn);
642
-
643
- // If config exists, try to plot
644
- if (forceConfig && forceConfig.root) {
645
- ChartLogic.plot(id);
646
  }
647
 
648
- this.saveToStorage();
649
- },
650
-
651
- removeChart(id) {
652
- if(!confirm("Remove chart?")) return;
653
-
654
- // Cleanup Intervals
655
- ChartLogic.stopLive(id);
656
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
657
-
658
- // Remove from Layout
659
- const colIdx = this.state.charts[id].col;
660
- this.state.layout[colIdx] = this.state.layout[colIdx].filter(x => x !== id);
661
- delete this.state.charts[id];
662
-
663
- // Remove DOM
664
- document.getElementById(`card-${id}`).remove();
665
- this.saveToStorage();
666
  },
667
 
668
- createChartElement(id) {
 
669
  const div = document.createElement('div');
670
  div.className = 'chart-card';
671
- div.id = `card-${id}`;
672
  div.innerHTML = `
673
- <div class="card-header">
674
- <div class="card-title" id="title-${id}">
675
- <span class="status-dot"></span>
676
- <span>New Chart</span>
677
  </div>
678
- <div class="card-actions">
679
- <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
680
- <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
681
  </div>
682
  </div>
683
- <div class="card-body" id="body-${id}">
684
  <div class="empty-state">
685
- <p>Not Configured</p>
686
- <button onclick="ConfigModal.open('${id}')">Configure</button>
687
  </div>
688
  </div>
689
  `;
690
  return div;
691
  },
692
 
693
- // --- GLOBAL CONTROLS ---
694
- onDateChange() {
695
- this.state.date = document.getElementById('globalDate').value;
696
- this.saveToStorage();
697
- // Reload all charts
698
- Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id));
 
 
 
 
 
699
  },
700
 
701
- onModeChange() {
702
- this.state.mode = document.getElementById('globalMode').value;
703
- this.saveToStorage();
704
- // Toggle intervals
705
- Object.keys(this.state.charts).forEach(id => {
706
- if(this.state.mode === 'LIVE') ChartLogic.startLive(id);
707
- else ChartLogic.stopLive(id);
708
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
  }
710
  };
711
 
712
  /**
713
- * --- CHART LOGIC & DATA ---
714
  */
715
- const ChartLogic = {
716
-
717
- async plot(id, isUpdate=false) {
718
- const chartData = GlobalApp.state.charts[id];
719
- const cfg = chartData.config;
720
-
721
- if (!cfg || !cfg.root) return; // Not configured
722
 
723
  // UI Update
724
- const titleEl = document.querySelector(`#card-${id} .card-title`);
725
- const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
726
- titleEl.innerHTML = `
727
- <span class="status-dot ${dotClass}"></span>
728
- ${cfg.root} <span style="color:var(--accent);">${cfg.expiry}</span>
729
- <span class="card-meta">${cfg.instrument} | ${cfg.timeframe}</span>
730
- `;
 
 
731
 
732
- // Ensure Canvas
733
- const body = document.getElementById(`body-${id}`);
734
- if (!body.querySelector('canvas')) {
735
- body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
736
  }
737
 
738
- // Prepare Status Indicator for Live
739
- if(isUpdate) titleEl.querySelector('.status-dot').classList.add('retry');
 
 
 
740
 
741
  try {
742
  const res = await fetch('/api/fetch_series', {
743
  method: 'POST',
744
  headers: {'Content-Type': 'application/json'},
745
  body: JSON.stringify({
746
- date: GlobalApp.state.date,
747
- root: cfg.root,
748
- expiry: cfg.expiry,
749
- instrument: cfg.instrument,
750
- timeframe: cfg.timeframe,
751
- start_time: cfg.start,
752
- end_time: cfg.end
753
  })
754
  });
755
  const data = await res.json();
756
-
757
- // Handle "Live but no data yet" or "Empty"
758
- if(data.error || !data.labels || data.labels.length === 0) {
759
- if(GlobalApp.state.mode === 'LIVE') {
760
- // Keep old data, just indicate retry
761
- titleEl.querySelector('.status-dot').className = 'status-dot retry';
762
- return;
763
- } else {
764
- // Historical empty
765
- if(chartData.instance) chartData.instance.destroy();
766
- body.innerHTML = '<div class="empty-state">No Data Found</div>';
767
- return;
768
- }
769
  }
770
 
771
- // Success: Update Dot
772
- titleEl.querySelector('.status-dot').className = `status-dot ${dotClass}`;
 
 
 
773
 
774
- // Compute Datasets
775
- const datasets = cfg.series.map(s => {
776
  const computed = data.labels.map((_, i) => {
777
- const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
778
- const B = data.B[i] || 0, S = data.S[i] || 0;
779
- let f = s.formula
780
- .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
781
- .replace(/\$B/g, B).replace(/\$S/g, S);
782
- try { return new Function('return ' + f)(); } catch { return null; }
783
  });
784
-
785
  return {
786
- label: s.label,
787
- data: computed,
788
- borderColor: s.color,
789
- backgroundColor: s.color,
790
- borderWidth: 1.5,
791
- pointRadius: 0,
792
- pointHoverRadius: 4,
793
- yAxisID: s.axis === 'left' ? 'y' : 'y1',
794
- tension: 0.1
795
  };
796
  });
797
 
798
- this.renderCanvas(id, data.labels, datasets);
 
799
 
800
- } catch (e) {
801
- console.error("Plot Error", e);
802
- }
803
  },
804
 
805
- renderCanvas(id, labels, datasets) {
806
- const ctx = document.getElementById(`canvas-${id}`);
807
- if (!ctx) return;
808
-
809
- const chartData = GlobalApp.state.charts[id];
810
 
811
- // If chart exists, update data (prevent flicker)
812
- if (chartData.instance) {
813
- chartData.instance.data.labels = labels;
814
- chartData.instance.data.datasets = datasets;
815
- chartData.instance.update('none'); // 'none' mode for performance
816
  } else {
817
- // Create New
818
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
819
- chartData.instance = new Chart(ctx.getContext('2d'), {
820
  type: 'line',
821
  data: { labels, datasets },
822
  options: {
823
- responsive: true,
824
- maintainAspectRatio: false,
825
  animation: false,
826
  interaction: { mode: 'index', intersect: false },
827
- plugins: {
828
- legend: { labels: { color: '#8b949e', font: {size:10} } },
829
- tooltip: { backgroundColor: '#161b22', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor:'#30363d', borderWidth:1 }
830
  },
831
  scales: {
832
- x: { grid: { color: '#30363d', tickLength: 0 }, ticks: { color: '#8b949e', maxTicksLimit: 10 } },
833
- y: { type: 'linear', display: true, position: 'left', grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
834
- y1: { type: 'linear', display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#8b949e' } }
835
  }
836
  }
837
  });
838
  }
839
  },
840
 
841
- // --- LIVE MODE ---
842
- startLive(id) {
843
- this.stopLive(id); // Safety clear
844
- // Check if date is today (simple check)
845
- const selected = GlobalApp.state.date;
846
- const today = new Date().toISOString().split('T')[0];
847
-
848
- // In real app, might compare accurately. Assuming user selects correct date or backend handles it.
849
- // We just poll regardless if Mode is Live.
850
-
851
- // Initial Plot
852
- this.plot(id);
853
-
854
- // Interval
855
- GlobalApp.state.charts[id].interval = setInterval(() => {
856
- this.plot(id, true);
857
- }, 15000); // 15 seconds
858
- },
859
-
860
- stopLive(id) {
861
- const c = GlobalApp.state.charts[id];
862
- if(c && c.interval) {
863
- clearInterval(c.interval);
864
- c.interval = null;
865
  }
866
  },
867
 
868
- // Re-render chart instance if DOM moved
869
- render(id) {
870
- const c = GlobalApp.state.charts[id];
871
- if(c.config && c.config.root) {
872
- if(c.instance) c.instance.destroy();
873
- c.instance = null;
874
- this.plot(id);
875
- if(GlobalApp.state.mode === 'LIVE') this.startLive(id);
876
- }
877
  }
878
  };
879
 
880
  /**
881
- * --- CONFIGURATION MODAL ---
882
  */
883
- const ConfigModal = {
884
  currentId: null,
885
- isGlobal: false,
886
-
887
  async open(id) {
888
  this.currentId = id;
889
- this.isGlobal = false;
890
- document.getElementById('cfgModalTitle').textContent = "Configure Chart";
891
- document.getElementById('configModalOverlay').style.display = 'flex';
892
- document.getElementById('cfgForm').style.display = 'block';
893
- document.getElementById('cfgLoader').style.display = 'none';
894
-
895
- // Pre-fill
896
- const c = GlobalApp.state.charts[id].config;
897
 
898
- // Load Roots
899
- await this.loadRoots(c.root);
900
 
901
- document.getElementById('cfgTF').value = c.timeframe || '1min';
902
- document.getElementById('cfgStart').value = c.start || '09:15';
903
- document.getElementById('cfgEnd').value = c.end || '15:30';
904
 
905
  // Series
906
- const series = c.series && c.series.length ? c.series : GlobalApp.defaults;
907
  this.renderSeries(series);
908
-
909
- // If saved state exists, trigger cascades
910
- if(c.root) {
911
- document.getElementById('cfgRoot').value = c.root;
912
- await this.onRootChange(c.expiry, c.instrument);
913
  }
914
  },
915
 
916
  openDefaults() {
917
- this.isGlobal = true;
918
- document.getElementById('cfgModalTitle').textContent = "Global Default Series";
919
- document.getElementById('configModalOverlay').style.display = 'flex';
920
- // Hide specific fields
921
- document.getElementById('cfgForm').style.display = 'block';
922
- // Actually, we just want series editing.
923
- // Quick hack: hide form rows, show series only.
924
- // Ideally separate modal, but re-using for brevity.
925
- this.renderSeries(GlobalApp.defaults);
926
  },
927
 
928
- close() {
929
- document.getElementById('configModalOverlay').style.display = 'none';
930
- },
931
 
932
- async loadRoots(preSelect) {
933
- const rSel = document.getElementById('cfgRoot');
934
- rSel.innerHTML = '<option>Loading...</option>';
935
- try {
936
- const date = GlobalApp.state.date;
937
- const res = await fetch(`/api/roots?date=${date}`);
938
- const roots = await res.json();
939
- rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r => `<option>${r}</option>`).join('');
940
- if(preSelect && roots.includes(preSelect)) rSel.value = preSelect;
941
- } catch(e) { rSel.innerHTML = '<option>Error</option>'; }
942
  },
943
 
944
  async onRootChange(preExp, preInst) {
945
  const root = document.getElementById('cfgRoot').value;
946
- const date = GlobalApp.state.date;
947
- const eSel = document.getElementById('cfgExp');
948
- eSel.innerHTML = '<option>Loading...</option>';
949
-
950
- const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
951
- const exps = await res.json();
952
- eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e => `<option>${e}</option>`).join('');
953
 
954
- if(preExp && exps.includes(preExp)) {
955
- eSel.value = preExp;
956
- await this.onExpChange(preInst);
957
  } else {
958
- // Auto select current?
959
- // Fetch auto config for smart defaults
960
- const auto = await fetch(`/api/auto_config?date=${date}&root=${root}`).then(r=>r.json());
961
  if(auto.current) {
962
- eSel.value = auto.current.expiry;
963
- await this.onExpChange(auto.current.instrument);
964
  }
965
  }
966
  },
@@ -968,90 +1007,69 @@ const ConfigModal = {
968
  async onExpChange(preInst) {
969
  const root = document.getElementById('cfgRoot').value;
970
  const exp = document.getElementById('cfgExp').value;
971
- const date = GlobalApp.state.date;
972
- const iSel = document.getElementById('cfgInst');
973
-
974
- iSel.innerHTML = '<option>Loading...</option>';
975
- const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
976
- const insts = await res.json();
977
- iSel.innerHTML = `<option value="">-- Select --</option>` + insts.map(i => `<option>${i}</option>`).join('');
978
-
979
- if(preInst && insts.includes(preInst)) iSel.value = preInst;
980
- else if(insts.length > 0) iSel.value = insts[0]; // Select first default
981
  },
982
 
983
  renderSeries(list) {
984
- const container = document.getElementById('cfgSeriesList');
985
- container.innerHTML = '';
986
- list.forEach(s => {
987
- const div = document.createElement('div');
988
- div.className = 'series-row';
989
- div.innerHTML = `
990
- <input type="color" value="${s.color}" class="s-color">
991
- <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
992
- <input type="text" value="${s.label}" class="s-label" placeholder="Label">
993
- <select class="s-axis">
994
- <option value="left" ${s.axis==='left'?'selected':''}>L</option>
995
- <option value="right" ${s.axis==='right'?'selected':''}>R</option>
996
- </select>
997
- <button class="btn-danger" style="padding:0;" onclick="this.parentElement.remove()">✕</button>
998
- `;
999
- container.appendChild(div);
1000
- });
1001
  },
1002
 
1003
- addSeriesRow() {
1004
- const container = document.getElementById('cfgSeriesList');
1005
- const div = document.createElement('div');
1006
- div.className = 'series-row';
1007
- div.innerHTML = `
1008
- <input type="color" value="#ffffff" class="s-color">
1009
- <input type="text" value="$P" class="s-formula">
1010
- <input type="text" value="New" class="s-label">
1011
- <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1012
- <button class="btn-danger" style="padding:0;" onclick="this.parentElement.remove()">✕</button>
 
 
 
1013
  `;
1014
- container.appendChild(div);
1015
  },
1016
 
1017
  save() {
1018
- // Collect Series
1019
- const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1020
- const newSeries = Array.from(rows).map(r => ({
1021
- color: r.querySelector('.s-color').value,
1022
- formula: r.querySelector('.s-formula').value,
1023
- label: r.querySelector('.s-label').value,
1024
- axis: r.querySelector('.s-axis').value
 
 
 
 
1025
  }));
1026
 
1027
- if(this.isGlobal) {
1028
- GlobalApp.defaults = newSeries;
1029
- GlobalApp.showToast("Defaults Updated");
1030
- } else {
1031
- const root = document.getElementById('cfgRoot').value;
1032
- const exp = document.getElementById('cfgExp').value;
1033
- const inst = document.getElementById('cfgInst').value;
1034
-
1035
- if(!root || !exp || !inst) { alert("Select Symbol details"); return; }
1036
-
1037
- const config = {
1038
- root, expiry: exp, instrument: inst,
1039
- timeframe: document.getElementById('cfgTF').value,
1040
- start: document.getElementById('cfgStart').value,
1041
- end: document.getElementById('cfgEnd').value,
1042
- series: newSeries
1043
- };
1044
-
1045
- GlobalApp.state.charts[this.currentId].config = config;
1046
- GlobalApp.saveToStorage();
1047
- ChartLogic.render(this.currentId); // Re-plot
1048
- }
1049
  this.close();
 
1050
  }
1051
  };
1052
 
1053
- // Start App
1054
- window.onload = () => GlobalApp.init();
1055
 
1056
  </script>
1057
  </body>
 
48
 
49
  @app.get("/api/dates")
50
  def get_dates():
51
+ """Fetch distinct dates."""
52
+ # Fetch ample rows to ensure we cover multiple days
 
53
  data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "50000"})
54
  dates = set()
55
  for row in data:
 
87
  expiry_map[exp] = list(existing.union(new_keys))
88
 
89
  all_expiries = sorted(expiry_map.keys())
90
+ # Allow looking at past expiries if viewing history, but generally prefer forward
91
  valid_expiries = [e for e in all_expiries if e >= date]
92
+ if not valid_expiries: valid_expiries = all_expiries
 
93
 
94
  response = {"expiries": all_expiries, "current": None, "next": None}
95
 
 
112
 
113
  @app.get("/api/expiries")
114
  def get_expiries(date: str, root: str):
 
115
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
116
  data = sb_get(f"/history_buckets_upstox?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
117
  exps = set(row['expiry'] for row in data if row.get('expiry'))
 
119
 
120
  @app.get("/api/instruments")
121
  def get_instruments(date: str, root: str, expiry: str):
 
122
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
123
  data = sb_get(f"/history_buckets_upstox?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
124
  instruments = set()
 
157
  inst_data = row['data'].get(instrument)
158
  if not inst_data: continue
159
 
160
+ # Extract buckets
161
  p_arr = inst_data.get('p', []) or []
162
  b_arr = inst_data.get('b', []) or []
163
  s_arr = inst_data.get('s', []) or []
 
178
  continue
179
 
180
  if not records:
181
+ return JSONResponse({"error": "No data", "labels": [], "P": []})
182
 
183
  df = pd.DataFrame(records)
184
  df.set_index('ts', inplace=True)
 
204
 
205
 
206
  # ==========================================
207
+ # 5. FRONTEND TEMPLATE (High Fidelity UI)
208
  # ==========================================
209
 
210
  HTML_TEMPLATE = """
 
212
  <html lang="en">
213
  <head>
214
  <meta charset="UTF-8">
215
+ <title>DepthChain PRO</title>
216
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
217
+ <!-- Inter Font for Apple-like Aesthetic -->
218
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
219
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
220
  <style>
 
221
  :root {
222
+ /* --- COLOR PALETTE (Dark, Elegant) --- */
223
+ --bg-app: #000000;
224
+ --bg-panel: #161618; /* Card Background */
225
+ --bg-header: #0a0a0a; /* Header */
226
+ --bg-input: #252528;
227
+ --border: #2C2C2E;
228
+ --border-hover: #3A3A3C;
229
+
230
+ --text-main: #F5F5F7;
231
+ --text-dim: #86868B;
232
+ --text-accent: #0A84FF; /* iOS Blue */
233
+
234
+ --success: #30D158;
235
+ --danger: #FF453A;
236
+ --warning: #FFD60A;
237
+
238
+ --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
239
+ --font-mono: 'JetBrains Mono', monospace;
240
  }
241
+
242
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
243
+
244
+ body {
245
+ background-color: var(--bg-app);
246
+ color: var(--text-main);
247
+ font-family: var(--font-ui);
248
+ height: 100vh;
249
+ display: flex;
250
+ flex-direction: column;
251
+ overflow: hidden;
252
+ -webkit-font-smoothing: antialiased;
253
  }
254
 
255
+ /* --- HEADER --- */
256
+ header {
257
+ height: 52px;
258
+ background: rgba(10, 10, 10, 0.8);
259
+ backdrop-filter: blur(20px);
260
+ border-bottom: 1px solid var(--border);
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: space-between;
264
+ padding: 0 20px;
265
+ z-index: 100;
266
  }
267
+
268
+ .brand {
269
+ font-size: 15px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-main);
270
+ display: flex; align-items: center; gap: 6px;
271
+ }
272
+ .brand span { color: var(--text-accent); }
273
+
274
+ .controls { display: flex; align-items: center; gap: 12px; }
275
+
276
+ /* Custom Selects */
277
+ .styled-select {
278
+ background: var(--bg-input);
279
+ color: var(--text-main);
280
+ border: 1px solid var(--border);
281
+ padding: 6px 12px;
282
+ border-radius: 6px;
283
+ font-size: 13px;
284
+ font-family: var(--font-ui);
285
+ cursor: pointer;
286
+ transition: 0.2s;
287
+ appearance: none; /* Hide default arrow */
288
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2386868B%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
289
+ background-repeat: no-repeat;
290
+ background-position: right 10px center;
291
+ background-size: 8px;
292
+ padding-right: 30px;
293
  }
294
+ .styled-select:hover { border-color: var(--text-dim); }
295
+
296
+ /* Segmented Control for Live/Hist */
297
+ .segmented-control {
298
+ background: var(--bg-input);
299
+ padding: 2px;
300
+ border-radius: 7px;
301
+ display: flex;
302
+ gap: 2px;
303
+ border: 1px solid var(--border);
 
 
 
304
  }
305
+ .seg-btn {
306
+ background: transparent;
307
+ color: var(--text-dim);
308
+ border: none;
309
+ padding: 4px 12px;
310
+ font-size: 12px;
311
+ font-weight: 500;
312
+ border-radius: 5px;
313
+ cursor: pointer;
314
+ transition: all 0.2s;
315
+ }
316
+ .seg-btn.active {
317
+ background: #3A3A3C;
318
+ color: var(--text-main);
319
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
320
  }
 
321
 
322
+ /* Buttons */
323
+ .btn-ghost {
324
+ background: transparent; border: 1px solid transparent; color: var(--text-dim);
325
+ padding: 6px 12px; font-size: 13px; border-radius: 6px; cursor: pointer; transition: 0.2s;
326
+ }
327
+ .btn-ghost:hover { background: var(--bg-input); color: var(--text-main); }
328
 
329
+ .btn-reset { color: var(--danger); }
330
+ .btn-reset:hover { background: rgba(255, 69, 58, 0.1); }
331
+
332
+ /* --- LAYOUT GRID --- */
333
+ .app-grid {
334
+ flex: 1;
335
+ display: grid;
336
+ /* Columns set via JS */
337
+ grid-auto-flow: column;
338
+ overflow: hidden; /* No global scroll, columns scroll individually */
339
+ background: var(--bg-app);
340
+ gap: 1px; /* Divider look */
341
  }
342
+
343
+ .chart-column {
344
+ display: flex;
345
+ flex-direction: column;
346
+ overflow-y: auto; /* INDEPENDENT SCROLL */
347
+ background: var(--bg-app);
348
+ padding: 8px;
349
+ gap: 8px;
350
+ border-right: 1px solid var(--border);
351
+ min-width: 0;
352
  }
353
+ .chart-column:last-child { border-right: none; }
354
+
355
+ /* Custom Scrollbar */
356
+ .chart-column::-webkit-scrollbar { width: 6px; }
357
+ .chart-column::-webkit-scrollbar-track { background: transparent; }
358
+ .chart-column::-webkit-scrollbar-thumb { background: #3A3A3C; border-radius: 3px; }
359
+ .chart-column::-webkit-scrollbar-thumb:hover { background: #555; }
360
 
361
  /* --- CHART CARD --- */
362
  .chart-card {
363
+ background: var(--bg-panel);
364
+ border: 1px solid var(--border);
365
+ border-radius: 8px;
366
+ display: flex;
367
+ flex-direction: column;
368
+ position: relative;
369
+ /* SIZING LOGIC */
370
+ flex-grow: 1;
371
+ min-height: 25vh; /* Ensure 4 fit nicely (100/4), if more, they stack */
372
+ flex-shrink: 0;
373
+ overflow: hidden;
374
+ transition: border-color 0.2s;
375
+ }
376
+ .chart-card:hover { border-color: var(--border-hover); }
377
+
378
+ /* Card Header */
379
+ .cc-header {
380
+ height: 36px;
381
+ padding: 0 10px;
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: space-between;
385
+ border-bottom: 1px solid var(--border);
386
+ background: rgba(255,255,255,0.02);
387
  }
388
+
389
+ .cc-title { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 500; }
390
+ .cc-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); transition: 0.3s; }
391
+ .cc-dot.live { background: var(--success); box-shadow: 0 0 6px rgba(48, 209, 88, 0.4); }
392
+ .cc-dot.hist { background: var(--text-accent); }
393
+
394
+ .cc-meta { color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; margin-left: 5px; }
395
+
396
+ .cc-actions { opacity: 0; transition: 0.2s; display: flex; gap: 4px; }
397
+ .chart-card:hover .cc-actions { opacity: 1; }
398
+
399
+ .icon-btn {
400
+ background: transparent; border: none; color: var(--text-dim);
401
+ cursor: pointer; padding: 4px; border-radius: 4px; font-size: 14px;
402
  }
403
+ .icon-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.1); }
 
 
 
 
404
 
405
+ /* Card Body */
406
+ .cc-body { flex: 1; position: relative; width: 100%; height: 100%; min-height: 0; }
407
+
408
  .empty-state {
409
  display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;
410
+ color: var(--text-dim); gap: 10px;
411
  }
412
+ .btn-conf-subtle {
413
+ background: transparent; border: 1px dashed var(--border);
414
+ color: var(--text-dim); padding: 8px 16px; border-radius: 6px; font-size: 12px; cursor: pointer;
415
+ }
416
+ .btn-conf-subtle:hover { border-color: var(--text-accent); color: var(--text-accent); }
417
+
418
+ /* Add Chart Button at bottom of column */
419
+ .add-chart-btn {
420
+ padding: 12px;
421
+ border: 1px dashed var(--border);
422
+ border-radius: 8px;
423
+ color: var(--text-dim);
424
+ text-align: center;
425
+ cursor: pointer;
426
+ font-size: 12px;
427
+ margin-top: 5px;
428
+ flex-shrink: 0;
429
+ }
430
+ .add-chart-btn:hover { background: rgba(255,255,255,0.03); color: var(--text-main); border-color: var(--text-dim); }
431
 
432
  /* --- MODAL --- */
433
  .modal-overlay {
434
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
435
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
436
+ z-index: 200; display: none; justify-content: center; align-items: center;
437
  }
438
+ .modal-box {
439
+ background: #1c1c1e;
440
+ width: 450px;
441
+ border: 1px solid var(--border);
442
+ border-radius: 12px;
443
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
444
+ display: flex; flex-direction: column;
445
+ overflow: hidden;
446
  }
447
+ .m-head {
448
+ padding: 15px 20px; border-bottom: 1px solid var(--border);
449
+ font-weight: 600; font-size: 14px; display: flex; justify-content: space-between; align-items: center;
450
+ }
451
+ .m-body { padding: 20px; max-height: 70vh; overflow-y: auto; }
452
+ .m-foot { padding: 15px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: #222; }
453
+
454
  .form-group { margin-bottom: 15px; }
455
+ .form-label { display: block; font-size: 11px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; letter-spacing: 0.5px; }
456
  .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
457
+
458
+ .modal-input {
459
+ width: 100%; background: #2c2c2e; border: 1px solid transparent;
460
+ color: white; padding: 8px 10px; border-radius: 6px; font-size: 13px; font-family: var(--font-ui);
461
+ }
462
+ .modal-input:focus { border-color: var(--text-accent); background: #1c1c1e; }
463
+
464
+ .btn-primary {
465
+ background: var(--text-accent); color: white; border: none; padding: 8px 16px;
466
+ border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer;
467
+ }
468
+ .btn-primary:hover { filter: brightness(1.1); }
469
 
470
  /* Series List */
471
+ .series-row { display: grid; grid-template-columns: 30px 1fr 1fr 40px 20px; gap: 8px; margin-bottom: 8px; align-items: center; }
472
+ .color-well { width: 100%; height: 28px; border: none; background: none; padding: 0; cursor: pointer; }
473
 
474
  /* Toast */
475
  .toast {
476
+ position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%) translateY(100px);
477
+ background: #333; color: white; padding: 10px 20px; border-radius: 20px;
478
+ font-size: 13px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); transition: 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
479
+ z-index: 1000; display: flex; align-items: center; gap: 8px; opacity: 0; pointer-events: none;
480
  }
481
+ .toast.visible { transform: translateX(-50%) translateY(0); opacity: 1; }
482
+ .toast-icon { color: var(--success); }
483
 
484
  </style>
485
  </head>
486
  <body>
487
 
488
  <!-- HEADER -->
489
+ <header>
490
+ <div class="brand">
491
+ DepthChain <span>PRO</span>
492
+ </div>
493
 
494
  <div class="controls">
495
+ <!-- Date Selector -->
496
+ <select id="globalDate" class="styled-select" onchange="App.onDateChange()"></select>
497
 
498
+ <!-- Live/Hist Toggle -->
499
+ <div class="segmented-control">
500
+ <button class="seg-btn" id="modeHist" onclick="App.setMode('HIST')">History</button>
501
+ <button class="seg-btn active" id="modeLive" onclick="App.setMode('LIVE')">Live</button>
502
+ </div>
503
+
504
+ <div style="width:1px; height:16px; background:var(--border); margin:0 5px;"></div>
505
 
506
  <!-- Layout Selector -->
507
+ <select class="styled-select" id="layoutSel" onchange="App.changeLayout(this.value)">
508
+ <option value="1">1 Column</option>
509
+ <option value="2">2 Columns</option>
510
+ <option value="3" selected>3 Columns</option>
511
+ <option value="4">4 Columns</option>
512
+ </select>
 
 
 
 
513
 
514
+ <button class="btn-ghost" onclick="Config.openDefaults()">Settings</button>
515
+ <button class="btn-ghost btn-reset" onclick="App.reset()">Reset</button>
516
  </div>
517
+ </header>
518
 
519
+ <!-- MAIN APP GRID -->
520
+ <div id="appGrid" class="app-grid">
521
+ <!-- Columns injected here -->
522
  </div>
523
 
524
+ <!-- CONFIG MODAL -->
525
+ <div class="modal-overlay" id="configModal">
526
+ <div class="modal-box">
527
+ <div class="m-head">
528
+ <span id="modalTitle">Configure Chart</span>
529
+ <button class="icon-btn" onclick="Config.close()">✕</button>
530
  </div>
531
+ <div class="m-body">
 
 
 
 
532
  <div id="cfgForm">
533
  <div class="form-row">
534
  <div class="form-group">
535
+ <label class="form-label">Symbol</label>
536
+ <select id="cfgRoot" class="modal-input" onchange="Config.onRootChange()"></select>
537
  </div>
538
  <div class="form-group">
539
+ <label class="form-label">Expiry</label>
540
+ <select id="cfgExp" class="modal-input" onchange="Config.onExpChange()"></select>
541
  </div>
542
  </div>
543
 
544
  <div class="form-group">
545
+ <label class="form-label">Instrument</label>
546
+ <select id="cfgInst" class="modal-input"></select>
547
  </div>
548
+
549
  <div class="form-row">
550
  <div class="form-group">
551
+ <label class="form-label">Timeframe</label>
552
+ <select id="cfgTF" class="modal-input">
553
  <option value="1min">1 Minute</option>
554
  <option value="3min">3 Minutes</option>
555
  <option value="5min">5 Minutes</option>
 
559
  </select>
560
  </div>
561
  <div class="form-group">
562
+ <label class="form-label">Time Range</label>
563
  <div style="display:flex; gap:5px;">
564
+ <input type="time" id="cfgStart" class="modal-input" value="09:15">
565
+ <input type="time" id="cfgEnd" class="modal-input" value="15:30">
566
  </div>
567
  </div>
568
  </div>
569
 
570
+ <div class="form-group" style="margin-top:10px; padding-top:10px; border-top:1px solid var(--border);">
571
+ <label class="form-label">Plots ($P, $B, $S, $V, $OI)</label>
572
+ <div id="cfgSeries"></div>
573
+ <button class="btn-ghost" style="width:100%; text-align:center; font-size:12px; margin-top:5px;" onclick="Config.addSeries()">+ Add Plot Line</button>
 
 
574
  </div>
575
  </div>
576
  </div>
577
+ <div class="m-foot">
578
+ <button class="btn-ghost" onclick="Config.close()">Cancel</button>
579
+ <button class="btn-primary" onclick="Config.save()">Apply Changes</button>
580
  </div>
581
  </div>
582
  </div>
583
 
584
+ <!-- TOAST -->
585
+ <div id="toast" class="toast">
586
+ <span class="toast-icon">✓</span> <span id="toastMsg">Saved</span>
 
 
 
 
 
 
 
 
 
587
  </div>
588
 
 
 
589
  <script>
 
590
  /**
591
+ * --- CORE APP LOGIC ---
592
  */
593
+ const App = {
594
  state: {
595
+ date: null,
596
+ mode: 'LIVE',
597
+ colCount: 3,
598
+ charts: [] // Flat array of Chart Objects {id, config, domId}
 
599
  },
600
+
601
+ // Default Plot Configuration
602
  defaults: [
603
+ { formula: '$P', color: '#0A84FF', label: 'Price', axis: 'left' },
604
+ { formula: '$B', color: '#30D158', label: 'Buy Qty', axis: 'right' },
605
+ { formula: '$S', color: '#FF453A', label: 'Sell Qty', axis: 'right' }
606
  ],
607
 
608
  async init() {
 
609
  await this.loadDates();
610
 
611
+ // Restore State
612
+ const saved = localStorage.getItem('dc_pro_v5');
613
+ if(saved) {
614
+ try {
615
+ const s = JSON.parse(saved);
616
+ this.state.date = s.date;
617
+ this.state.mode = s.mode || 'LIVE';
618
+ this.state.colCount = s.colCount || 3;
619
+ // Rehydrate charts (configs only)
620
+ this.state.charts = s.charts.map(c => ({
621
+ id: c.id,
622
+ config: c.config,
623
+ domId: `card-${c.id}`,
624
+ instance: null,
625
+ interval: null
626
+ }));
627
+
628
+ document.getElementById('layoutSel').value = this.state.colCount;
629
+ if(this.state.date) document.getElementById('globalDate').value = this.state.date;
630
+ this.updateModeUI();
631
+
632
+ } catch(e) { console.error("Restore failed", e); this.initDefault(); }
633
  } else {
634
+ this.initDefault();
635
  }
636
+
637
+ // Render Initial Grid
638
+ this.renderLayout();
639
+ },
640
+
641
+ initDefault() {
642
+ this.state.colCount = 3;
643
+ this.state.charts = [
644
+ this.createChartObj(), this.createChartObj(), this.createChartObj(),
645
+ this.createChartObj(), this.createChartObj(), this.createChartObj()
646
+ ];
647
+ },
648
+
649
+ createChartObj() {
650
+ return {
651
+ id: 'c'+Date.now()+Math.random().toString(16).slice(2),
652
+ config: null, // Empty
653
+ domId: null,
654
+ instance: null,
655
+ interval: null
656
+ };
657
  },
658
 
659
  async loadDates() {
 
662
  const dates = await res.json();
663
  const sel = document.getElementById('globalDate');
664
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
665
+ if(dates.length && !this.state.date) this.state.date = dates[0];
666
+ // Ensure select shows the value
667
+ if(this.state.date) sel.value = this.state.date;
668
+ } catch(e) { console.error(e); }
 
 
 
 
 
669
  },
670
 
671
+ updateModeUI() {
672
+ document.getElementById('modeLive').className = this.state.mode === 'LIVE' ? 'seg-btn active' : 'seg-btn';
673
+ document.getElementById('modeHist').className = this.state.mode === 'HIST' ? 'seg-btn active' : 'seg-btn';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  },
675
 
676
+ setMode(m) {
677
+ this.state.mode = m;
678
+ this.updateModeUI();
679
+ this.save();
680
+ // Trigger all charts to update intervals
681
+ this.state.charts.forEach(c => ChartController.manageInterval(c));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  },
683
 
684
+ onDateChange() {
685
+ this.state.date = document.getElementById('globalDate').value;
686
+ this.save();
687
+ // Reload all
688
+ this.state.charts.forEach(c => ChartController.plot(c));
 
 
 
 
 
 
 
689
  },
690
 
691
+ changeLayout(val) {
692
+ this.state.colCount = parseInt(val);
693
+ this.save();
694
+ this.renderLayout(); // Re-distribute DOM nodes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  },
696
 
697
+ // --- LAYOUT ENGINE ---
698
+ renderLayout() {
699
+ const grid = document.getElementById('appGrid');
700
+ grid.style.gridTemplateColumns = `repeat(${this.state.colCount}, 1fr)`;
701
 
702
+ // Create Columns
703
+ const columns = [];
704
+ grid.innerHTML = '';
705
+ for(let i=0; i<this.state.colCount; i++) {
706
+ const col = document.createElement('div');
707
+ col.className = 'chart-column';
708
+ col.id = `col-${i}`;
709
+
710
+ // Add "Add Chart" button at bottom
711
+ const btn = document.createElement('div');
712
+ btn.className = 'add-chart-btn';
713
+ btn.innerText = '+ Add Chart';
714
+ btn.onclick = () => App.addChartToCol(i);
715
+
716
+ // Store ref for appending
717
+ col.appendChild(btn);
718
+ columns.push(col);
719
+ grid.appendChild(col);
 
 
 
 
 
 
 
720
  }
721
 
722
+ // Distribute Charts into Columns
723
+ this.state.charts.forEach((c, index) => {
724
+ const targetColIndex = index % this.state.colCount;
725
+ const targetCol = columns[targetColIndex];
726
+ const addBtn = targetCol.lastChild;
727
+
728
+ // Check if DOM element exists
729
+ let card = document.getElementById(c.domId);
730
+ if(!card) {
731
+ // Create new Card DOM
732
+ card = this.createCardDOM(c);
733
+ }
734
+ // Move into new column (before the button)
735
+ targetCol.insertBefore(card, addBtn);
736
+
737
+ // Re-render canvas if it was lost (unlikely with appendChild move, but safe check)
738
+ if(c.config && !c.instance) ChartController.plot(c);
739
+ });
740
  },
741
 
742
+ createCardDOM(c) {
743
+ c.domId = `card-${c.id}`;
744
  const div = document.createElement('div');
745
  div.className = 'chart-card';
746
+ div.id = c.domId;
747
  div.innerHTML = `
748
+ <div class="cc-header">
749
+ <div class="cc-title" id="title-${c.id}">
750
+ <span class="cc-dot"></span> New Chart
 
751
  </div>
752
+ <div class="cc-actions">
753
+ <button class="icon-btn" onclick="Config.open('${c.id}')">⚙</button>
754
+ <button class="icon-btn" onclick="App.removeChart('${c.id}')">✕</button>
755
  </div>
756
  </div>
757
+ <div class="cc-body" id="body-${c.id}">
758
  <div class="empty-state">
759
+ <button class="btn-conf-subtle" onclick="Config.open('${c.id}')">Configure Chart</button>
 
760
  </div>
761
  </div>
762
  `;
763
  return div;
764
  },
765
 
766
+ addChartToCol(colIndex) {
767
+ // We create a new chart.
768
+ // To maintain "order", we technically append to the list.
769
+ // But the layout logic distributes round-robin.
770
+ // To force it into a specific column visually is hard with round-robin.
771
+ // SIMPLIFICATION: We just add to the end of the global list, and re-render.
772
+ // It will appear in the next available slot.
773
+ const c = this.createChartObj();
774
+ this.state.charts.push(c);
775
+ this.save();
776
+ this.renderLayout();
777
  },
778
 
779
+ removeChart(id) {
780
+ if(!confirm("Remove this chart?")) return;
781
+ const idx = this.state.charts.findIndex(x => x.id === id);
782
+ if(idx > -1) {
783
+ const c = this.state.charts[idx];
784
+ ChartController.cleanup(c);
785
+ document.getElementById(c.domId).remove();
786
+ this.state.charts.splice(idx, 1);
787
+ this.save();
788
+ this.renderLayout(); // Re-balance
789
+ }
790
+ },
791
+
792
+ reset() {
793
+ if(confirm("Reset entire workspace?")) {
794
+ localStorage.removeItem('dc_pro_v5');
795
+ location.reload();
796
+ }
797
+ },
798
+
799
+ save() {
800
+ const minimal = {
801
+ date: this.state.date,
802
+ mode: this.state.mode,
803
+ colCount: this.state.colCount,
804
+ charts: this.state.charts.map(c => ({ id: c.id, config: c.config }))
805
+ };
806
+ localStorage.setItem('dc_pro_v5', JSON.stringify(minimal));
807
+ },
808
+
809
+ toast(msg) {
810
+ const t = document.getElementById('toast');
811
+ document.getElementById('toastMsg').innerText = msg;
812
+ t.classList.add('visible');
813
+ setTimeout(() => t.classList.remove('visible'), 2000);
814
  }
815
  };
816
 
817
  /**
818
+ * --- CHART CONTROLLER ---
819
  */
820
+ const ChartController = {
821
+ async plot(c, isUpdate=false) {
822
+ if(!c.config) return;
 
 
 
 
823
 
824
  // UI Update
825
+ const titleEl = document.querySelector(`#title-${c.id}`);
826
+ const dot = c.config ? (App.state.mode==='LIVE' ? 'live' : 'hist') : '';
827
+ if(titleEl) {
828
+ titleEl.innerHTML = `
829
+ <span class="cc-dot ${dot}" id="dot-${c.id}"></span>
830
+ ${c.config.root} <span style="color:var(--text-accent)">${c.config.expiry}</span>
831
+ <span class="cc-meta">${c.config.instrument} • ${c.config.timeframe}</span>
832
+ `;
833
+ }
834
 
835
+ // DOM Check
836
+ const body = document.getElementById(`body-${c.id}`);
837
+ if(!body.querySelector('canvas')) {
838
+ body.innerHTML = `<canvas id="canvas-${c.id}"></canvas>`;
839
  }
840
 
841
+ // Indicate loading if update
842
+ if(isUpdate) {
843
+ const d = document.getElementById(`dot-${c.id}`);
844
+ if(d) d.style.opacity = '0.5';
845
+ }
846
 
847
  try {
848
  const res = await fetch('/api/fetch_series', {
849
  method: 'POST',
850
  headers: {'Content-Type': 'application/json'},
851
  body: JSON.stringify({
852
+ date: App.state.date,
853
+ root: c.config.root,
854
+ expiry: c.config.expiry,
855
+ instrument: c.config.instrument,
856
+ timeframe: c.config.timeframe,
857
+ start_time: c.config.start,
858
+ end_time: c.config.end
859
  })
860
  });
861
  const data = await res.json();
862
+
863
+ if(isUpdate) {
864
+ const d = document.getElementById(`dot-${c.id}`);
865
+ if(d) d.style.opacity = '1';
 
 
 
 
 
 
 
 
 
866
  }
867
 
868
+ if(data.error || !data.labels || !data.labels.length) {
869
+ if(App.state.mode === 'LIVE' && isUpdate) return; // Keep old data
870
+ if(!isUpdate) body.innerHTML = '<div class="empty-state">No Data Available</div>';
871
+ return;
872
+ }
873
 
874
+ // Process Data
875
+ const datasets = c.config.series.map(s => {
876
  const computed = data.labels.map((_, i) => {
877
+ const P=data.P[i]||0, B=data.B[i]||0, S=data.S[i]||0, V=data.V[i]||0, OI=data.OI[i]||0;
878
+ const formula = s.formula.replace(/\$P/g,P).replace(/\$B/g,B).replace(/\$S/g,S).replace(/\$V/g,V).replace(/\$OI/g,OI);
879
+ try { return new Function('return '+formula)(); } catch{ return null; }
 
 
 
880
  });
 
881
  return {
882
+ label: s.label, data: computed, borderColor: s.color, backgroundColor: s.color,
883
+ borderWidth: 1.5, pointRadius: 0, hoverRadius: 4, tension: 0.1,
884
+ yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
 
 
885
  };
886
  });
887
 
888
+ this.renderCanvas(c, data.labels, datasets);
889
+ this.manageInterval(c);
890
 
891
+ } catch(e) { console.error(e); }
 
 
892
  },
893
 
894
+ renderCanvas(c, labels, datasets) {
895
+ const ctx = document.getElementById(`canvas-${c.id}`);
896
+ if(!ctx) return;
 
 
897
 
898
+ if(c.instance) {
899
+ c.instance.data.labels = labels;
900
+ c.instance.data.datasets = datasets;
901
+ c.instance.update('none');
 
902
  } else {
 
903
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
904
+ c.instance = new Chart(ctx.getContext('2d'), {
905
  type: 'line',
906
  data: { labels, datasets },
907
  options: {
908
+ responsive: true, maintainAspectRatio: false,
 
909
  animation: false,
910
  interaction: { mode: 'index', intersect: false },
911
+ plugins: {
912
+ legend: { display: false },
913
+ tooltip: { backgroundColor: '#1c1c1e', titleColor:'#fff', bodyColor:'#ccc', borderColor:'#333', borderWidth:1 }
914
  },
915
  scales: {
916
+ x: { grid:{color:'#222', tickLength:0}, ticks:{color:'#666', maxTicksLimit:8} },
917
+ y: { type:'linear', display:true, position:'left', grid:{color:'#222'}, ticks:{color:'#666'} },
918
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#666'} }
919
  }
920
  }
921
  });
922
  }
923
  },
924
 
925
+ manageInterval(c) {
926
+ if(c.interval) clearInterval(c.interval);
927
+ if(App.state.mode === 'LIVE' && c.config) {
928
+ // Check if today
929
+ const today = new Date().toISOString().split('T')[0];
930
+ if(App.state.date === today) {
931
+ c.interval = setInterval(() => this.plot(c, true), 15000);
932
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
  }
934
  },
935
 
936
+ cleanup(c) {
937
+ if(c.interval) clearInterval(c.interval);
938
+ if(c.instance) c.instance.destroy();
 
 
 
 
 
 
939
  }
940
  };
941
 
942
  /**
943
+ * --- CONFIG MODAL ---
944
  */
945
+ const Config = {
946
  currentId: null,
947
+
 
948
  async open(id) {
949
  this.currentId = id;
950
+ document.getElementById('configModal').style.display = 'flex';
951
+
952
+ // Find config or use defaults
953
+ const c = App.state.charts.find(x => x.id === id);
954
+ const cfg = c.config || {};
 
 
 
955
 
956
+ // Load roots
957
+ await this.loadRoots(cfg.root);
958
 
959
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
960
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
961
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
962
 
963
  // Series
964
+ const series = cfg.series || App.defaults;
965
  this.renderSeries(series);
966
+
967
+ if(cfg.root) {
968
+ document.getElementById('cfgRoot').value = cfg.root;
969
+ await this.onRootChange(cfg.expiry, cfg.instrument);
 
970
  }
971
  },
972
 
973
  openDefaults() {
974
+ alert("Global Defaults editing coming soon. Edit individual charts for now.");
 
 
 
 
 
 
 
 
975
  },
976
 
977
+ close() { document.getElementById('configModal').style.display = 'none'; },
 
 
978
 
979
+ async loadRoots(pre) {
980
+ const sel = document.getElementById('cfgRoot');
981
+ sel.innerHTML = '<option>Loading...</option>';
982
+ const roots = await fetch(`/api/roots?date=${App.state.date}`).then(r=>r.json());
983
+ sel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
984
+ if(pre) sel.value = pre;
 
 
 
 
985
  },
986
 
987
  async onRootChange(preExp, preInst) {
988
  const root = document.getElementById('cfgRoot').value;
989
+ const sel = document.getElementById('cfgExp');
990
+ sel.innerHTML = '<option>Loading...</option>';
991
+ const exps = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`).then(r=>r.json());
992
+ sel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e=>`<option>${e}</option>`).join('');
 
 
 
993
 
994
+ if(preExp) {
995
+ sel.value = preExp;
996
+ this.onExpChange(preInst);
997
  } else {
998
+ // Auto logic
999
+ const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
 
1000
  if(auto.current) {
1001
+ sel.value = auto.current.expiry;
1002
+ this.onExpChange(auto.current.instrument);
1003
  }
1004
  }
1005
  },
 
1007
  async onExpChange(preInst) {
1008
  const root = document.getElementById('cfgRoot').value;
1009
  const exp = document.getElementById('cfgExp').value;
1010
+ const sel = document.getElementById('cfgInst');
1011
+ sel.innerHTML = '<option>Loading...</option>';
1012
+ const insts = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`).then(r=>r.json());
1013
+ sel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
1014
+ if(preInst) sel.value = preInst;
 
 
 
 
 
1015
  },
1016
 
1017
  renderSeries(list) {
1018
+ const div = document.getElementById('cfgSeries');
1019
+ div.innerHTML = '';
1020
+ list.forEach(s => this.addSeriesUI(s));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1021
  },
1022
 
1023
+ addSeries() {
1024
+ this.addSeriesUI({ color:'#ffffff', formula:'$P', label:'New', axis:'left' });
1025
+ },
1026
+
1027
+ addSeriesUI(s) {
1028
+ const el = document.createElement('div');
1029
+ el.className = 'series-row';
1030
+ el.innerHTML = `
1031
+ <input type="color" class="color-well s-col" value="${s.color}">
1032
+ <input type="text" class="modal-input s-for" value="${s.formula}" placeholder="$P">
1033
+ <input type="text" class="modal-input s-lab" value="${s.label}" placeholder="Label">
1034
+ <select class="modal-input s-ax" style="padding:4px;"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1035
+ <button class="icon-btn" onclick="this.parentElement.remove()" style="color:var(--danger)">✕</button>
1036
  `;
1037
+ document.getElementById('cfgSeries').appendChild(el);
1038
  },
1039
 
1040
  save() {
1041
+ const root = document.getElementById('cfgRoot').value;
1042
+ const exp = document.getElementById('cfgExp').value;
1043
+ const inst = document.getElementById('cfgInst').value;
1044
+
1045
+ if(!root || !exp || !inst) { App.toast("Missing Fields"); return; }
1046
+
1047
+ const series = Array.from(document.querySelectorAll('.series-row')).map(row => ({
1048
+ color: row.querySelector('.s-col').value,
1049
+ formula: row.querySelector('.s-for').value,
1050
+ label: row.querySelector('.s-lab').value,
1051
+ axis: row.querySelector('.s-ax').value
1052
  }));
1053
 
1054
+ const config = {
1055
+ root, expiry: exp, instrument: inst,
1056
+ timeframe: document.getElementById('cfgTF').value,
1057
+ start: document.getElementById('cfgStart').value,
1058
+ end: document.getElementById('cfgEnd').value,
1059
+ series
1060
+ };
1061
+
1062
+ const chart = App.state.charts.find(x => x.id === this.currentId);
1063
+ chart.config = config;
1064
+
1065
+ App.save();
1066
+ ChartController.plot(chart);
 
 
 
 
 
 
 
 
 
1067
  this.close();
1068
+ App.toast("Configuration Applied");
1069
  }
1070
  };
1071
 
1072
+ window.onload = () => App.init();
 
1073
 
1074
  </script>
1075
  </body>