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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +859 -585
app.py CHANGED
@@ -48,8 +48,9 @@ def home():
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,9 +88,9 @@ def get_auto_config(date: str, root: str):
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,6 +113,7 @@ def get_auto_config(date: str, root: str):
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,6 +121,7 @@ def get_expiries(date: str, root: str):
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,7 +160,7 @@ def fetch_series(
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,7 +181,7 @@ def fetch_series(
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,7 +207,7 @@ def fetch_series(
204
 
205
 
206
  # ==========================================
207
- # 5. FRONTEND TEMPLATE (High Fidelity UI)
208
  # ==========================================
209
 
210
  HTML_TEMPLATE = """
@@ -212,448 +215,516 @@ 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>
556
- <option value="15min">15 Minutes</option>
557
- <option value="30min">30 Minutes</option>
558
- <option value="1hour">1 Hour</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() {
@@ -661,345 +732,518 @@ const App = {
661
  const res = await fetch('/api/dates');
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,69 +1251,99 @@ const Config = {
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>
 
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
  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
 
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
 
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
  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
  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
 
208
 
209
  # ==========================================
210
+ # 5. FRONTEND TEMPLATE (World-Class Refactor)
211
  # ==========================================
212
 
213
  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 rel="preconnect" href="https://fonts.googleapis.com">
221
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
222
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
223
  <style>
224
+ /* --- WORLD CLASS THEME --- */
225
  :root {
226
+ --bg-body: #0d1117; /* GitHub Dimmed Background */
227
+ --bg-header: rgba(22, 27, 34, 0.85); /* Translucent Header */
228
+ --bg-card: #161b22; /* Card Background */
229
+ --bg-input: #010409; /* Dark Input Background */
230
+ --border: #30363d; /* Subtle Border */
231
+ --border-hover: #6e7681;
 
232
 
233
+ --text-primary: #e6edf3;
234
+ --text-secondary: #8b949e;
 
235
 
236
+ --accent: #2f81f7;
237
+ --accent-glow: rgba(47, 129, 247, 0.2);
238
+ --success: #238636;
239
+ --danger: #f85149;
240
+ --warning: #d29922;
241
+
242
+ --shadow-card: 0 4px 12px rgba(0,0,0,0.5);
243
+ --radius-md: 8px;
244
+ --radius-sm: 6px;
245
  }
246
 
247
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; -webkit-tap-highlight-color: transparent; }
248
 
249
+ body {
250
+ background: var(--bg-body);
251
+ color: var(--text-primary);
252
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
253
+ display: flex;
254
+ flex-direction: column;
255
+ height: 100vh;
256
+ overflow: hidden; /* Body doesn't scroll, columns do */
257
+ font-size: 13px;
258
  }
259
 
260
+ /* --- SCROLLBARS --- */
261
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
262
+ ::-webkit-scrollbar-track { background: transparent; }
263
+ ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
264
+ ::-webkit-scrollbar-thumb:hover { background: #586069; }
265
+
266
+ /* --- TOP BAR --- */
267
+ .top-bar {
268
+ height: 56px;
269
+ background: var(--bg-header);
270
+ backdrop-filter: blur(12px);
271
+ -webkit-backdrop-filter: blur(12px);
272
  border-bottom: 1px solid var(--border);
273
  display: flex;
274
  align-items: center;
275
  justify-content: space-between;
276
  padding: 0 20px;
277
  z-index: 100;
278
+ flex-shrink: 0;
279
  }
280
+
281
+ .logo {
282
+ font-weight: 700;
283
+ font-size: 16px;
284
+ letter-spacing: -0.5px;
285
+ color: var(--text-primary);
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 8px;
289
+ }
290
+ .logo span {
291
+ background: var(--accent);
292
+ color: white;
293
+ font-size: 10px;
294
+ padding: 2px 6px;
295
+ border-radius: 12px;
296
+ font-weight: 600;
297
  }
 
298
 
299
+ .controls { display: flex; gap: 12px; align-items: center; }
300
 
301
+ /* Custom Select Styling */
302
+ .select-wrapper { position: relative; }
303
+ select {
304
+ appearance: none;
305
  background: var(--bg-input);
 
306
  border: 1px solid var(--border);
307
+ color: var(--text-primary);
308
+ padding: 6px 30px 6px 12px;
309
+ border-radius: var(--radius-sm);
310
+ font-size: 12px;
311
+ font-family: inherit;
312
  cursor: pointer;
313
+ transition: all 0.2s;
314
+ min-width: 120px;
315
+ }
316
+ select:hover { border-color: var(--border-hover); }
317
+ select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
318
+ .select-chevron {
319
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
320
+ pointer-events: none; color: var(--text-secondary); font-size: 10px;
321
  }
 
322
 
323
+ /* Buttons */
324
+ button {
325
+ background: var(--bg-card);
 
 
 
 
326
  border: 1px solid var(--border);
327
+ color: var(--text-primary);
328
+ padding: 6px 12px;
329
+ border-radius: var(--radius-sm);
 
 
 
330
  font-size: 12px;
331
  font-weight: 500;
 
332
  cursor: pointer;
333
  transition: all 0.2s;
334
  }
335
+ button:hover { border-color: var(--text-secondary); background: #21262d; }
336
+
337
+ .btn-primary { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
338
+ .btn-primary:hover { background: #2c76e3; border-color: #2c76e3; }
339
+
340
+ .btn-danger { color: var(--danger); border-color: var(--border); }
341
+ .btn-danger:hover { background: rgba(248, 81, 73, 0.1); border-color: var(--danger); }
342
+
343
+ .btn-ghost {
344
+ background: transparent;
345
+ border: 1px solid var(--text-secondary);
346
+ color: var(--text-secondary);
347
+ opacity: 0.7;
348
+ }
349
+ .btn-ghost:hover {
350
+ opacity: 1;
351
+ border-color: var(--accent);
352
+ color: var(--accent);
353
+ background: rgba(47, 129, 247, 0.05);
354
  }
355
 
356
+ /* --- LAYOUT SELECTOR --- */
357
+ .layout-group {
358
+ display: flex;
359
+ background: var(--bg-input);
360
+ border: 1px solid var(--border);
361
+ border-radius: var(--radius-sm);
362
+ padding: 2px;
363
  }
364
+ .layout-btn {
365
+ background: transparent; border: none; padding: 4px 10px;
366
+ color: var(--text-secondary); border-radius: 4px;
367
+ }
368
+ .layout-btn:hover { background: transparent; color: var(--text-primary); }
369
+ .layout-btn.active { background: var(--border); color: var(--text-primary); }
370
 
371
+ /* --- MAIN GRID --- */
372
+ .main-content {
373
  flex: 1;
374
+ display: flex; /* Flex rows for columns */
375
+ overflow: hidden; /* Prevent body scroll */
376
+ padding: 0;
 
 
377
  gap: 1px; /* Divider look */
378
+ background: var(--border);
379
  }
380
 
381
+ .column {
382
+ flex: 1;
383
  display: flex;
384
  flex-direction: column;
385
+ gap: 16px;
386
  overflow-y: auto; /* INDEPENDENT SCROLL */
387
+ background: var(--bg-body);
388
+ padding: 16px;
389
+ min-width: 320px;
 
 
390
  }
 
391
 
392
+ .column-add-btn {
393
+ padding: 20px;
394
+ border: 1px dashed var(--border);
395
+ color: var(--text-secondary);
396
+ text-align: center;
397
+ border-radius: var(--radius-md);
398
+ cursor: pointer;
399
+ margin-top: auto;
400
+ transition: 0.2s;
401
+ font-size: 13px;
402
+ }
403
+ .column-add-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(47, 129, 247, 0.03); }
404
 
405
  /* --- CHART CARD --- */
406
  .chart-card {
407
+ background: var(--bg-card);
408
  border: 1px solid var(--border);
409
+ border-radius: var(--radius-md);
410
+ height: 380px;
411
+ min-height: 380px;
412
  display: flex;
413
  flex-direction: column;
414
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
415
  position: relative;
 
 
 
 
 
416
  transition: border-color 0.2s;
417
  }
418
  .chart-card:hover { border-color: var(--border-hover); }
419
 
420
+ .card-header {
421
+ height: 40px;
422
+ border-bottom: 1px solid var(--border);
 
423
  display: flex;
424
  align-items: center;
425
  justify-content: space-between;
426
+ padding: 0 12px;
427
+ background: rgba(255,255,255,0.01);
428
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
429
  }
430
 
431
+ .card-title {
432
+ font-weight: 600;
433
+ font-size: 12px;
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 8px;
437
+ color: var(--text-primary);
438
+ }
439
+ .card-meta {
440
+ color: var(--text-secondary);
441
+ font-family: 'JetBrains Mono', monospace;
442
+ font-size: 11px;
443
+ opacity: 0.8;
444
+ margin-left: 6px;
445
+ }
446
 
447
+ .icon-btn {
448
+ padding: 4px;
449
+ border: none;
450
+ background: transparent;
451
+ color: var(--text-secondary);
452
+ font-size: 14px;
453
+ border-radius: 4px;
454
  }
455
+ .icon-btn:hover { background: var(--border); color: var(--text-primary); }
456
 
457
+ .card-body {
458
+ flex: 1;
459
+ position: relative;
460
+ padding: 8px 8px 4px 4px; /* Slight padding for chart labels */
461
+ }
462
 
463
  .empty-state {
464
+ display: flex;
465
+ flex-direction: column;
466
+ align-items: center;
467
+ justify-content: center;
468
+ height: 100%;
469
+ color: var(--text-secondary);
470
+ gap: 12px;
471
  }
 
 
 
 
 
472
 
473
+ /* --- STATUS DOTS --- */
474
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
475
+ .status-dot.live { background: var(--success); box-shadow: 0 0 8px var(--success); animation: pulse 2s infinite; }
476
+ .status-dot.hist { background: var(--accent); }
477
+ .status-dot.retry { background: var(--warning); }
478
+ .status-dot.offline { background: var(--border); }
479
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
480
+
481
+ /* --- TOAST --- */
482
+ .toast-container {
483
+ position: fixed; bottom: 24px; right: 24px; z-index: 2000;
484
+ display: flex; flex-direction: column; gap: 10px;
485
+ }
486
+ .toast {
487
+ background: var(--bg-card); border: 1px solid var(--border);
488
+ color: var(--text-primary); padding: 12px 16px;
489
+ border-radius: var(--radius-md); font-weight: 500; font-size: 13px;
490
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
491
+ display: flex; align-items: center; gap: 10px;
492
+ animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
493
  }
494
+ .toast-success { border-left: 3px solid var(--success); }
495
+ .toast-info { border-left: 3px solid var(--accent); }
496
+ @keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
497
 
498
  /* --- MODAL --- */
499
  .modal-overlay {
500
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
501
+ background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); z-index: 1000;
502
+ display: none; justify-content: center; align-items: center;
503
+ opacity: 0; transition: opacity 0.2s;
504
  }
505
+ .modal-overlay.active { opacity: 1; }
506
+
507
+ .modal {
508
+ background: #1c2128; border: 1px solid var(--border); border-radius: 12px;
509
+ width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
510
+ box-shadow: 0 24px 48px rgba(0,0,0,0.6);
511
+ transform: scale(0.95); transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
 
512
  }
513
+ .modal-overlay.active .modal { transform: scale(1); }
514
+
515
+ .modal-head {
516
+ padding: 16px 20px; border-bottom: 1px solid var(--border);
517
+ font-weight: 600; font-size: 14px;
518
+ display: flex; justify-content: space-between; align-items: center;
519
  }
520
+ .modal-body { padding: 20px; overflow-y: auto; }
521
+ .modal-foot { padding: 16px 20px; border-top: 1px solid var(--border); text-align: right; background: rgba(0,0,0,0.2); border-radius: 0 0 12px 12px; }
522
 
523
+ .form-group { margin-bottom: 16px; }
524
+ .form-group label { display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
525
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
526
 
527
+ input[type="text"], input[type="time"], input[type="number"] {
528
+ width: 100%; background: var(--bg-input); border: 1px solid var(--border);
529
+ color: var(--text-primary); padding: 8px 10px; border-radius: var(--radius-sm);
530
  }
531
+ input:focus { border-color: var(--accent); }
532
 
533
+ /* Series List Styling */
534
+ .series-row {
535
+ display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px;
536
+ gap: 8px; margin-bottom: 8px; align-items: center;
537
  }
538
+ .color-picker {
539
+ width: 100%; height: 32px; padding: 0; border: none;
540
+ background: none; cursor: pointer;
 
 
 
 
 
 
 
 
 
541
  }
 
 
542
 
543
  </style>
544
  </head>
545
  <body>
546
 
547
  <!-- HEADER -->
548
+ <div class="top-bar">
549
+ <div class="logo">DepthChain <span>PRO</span></div>
 
 
550
 
551
  <div class="controls">
552
  <!-- Date Selector -->
553
+ <div class="select-wrapper">
554
+ <select id="globalDate" onchange="GlobalApp.onDateChange()"></select>
555
+ <span class="select-chevron">▼</span>
556
+ </div>
557
 
558
+ <!-- Mode Toggle -->
559
+ <div class="layout-group">
560
+ <button class="layout-btn active" id="modeHist" onclick="GlobalApp.setMode('HIST')">History</button>
561
+ <button class="layout-btn" id="modeLive" onclick="GlobalApp.setMode('LIVE')">Live</button>
562
  </div>
563
+
564
+ <div style="width: 1px; height: 20px; background: var(--border); margin: 0 8px;"></div>
565
 
566
  <!-- Layout Selector -->
567
+ <div class="layout-group">
568
+ <button class="layout-btn" id="lay1" onclick="GlobalApp.setLayout(1)">1</button>
569
+ <button class="layout-btn" id="lay2" onclick="GlobalApp.setLayout(2)">2</button>
570
+ <button class="layout-btn active" id="lay3" onclick="GlobalApp.setLayout(3)">3</button>
571
+ <button class="layout-btn" id="lay4" onclick="GlobalApp.setLayout(4)">4</button>
572
+ </div>
573
+
574
+ <button onclick="ConfigModal.openDefaults()">Global Defaults</button>
575
+ <button class="btn-danger" onclick="GlobalApp.resetApp()">Reset</button>
576
  </div>
577
+ </div>
578
 
579
+ <!-- GRID CONTAINER -->
580
+ <div class="main-content" id="gridContainer">
581
+ <!-- Columns injected via JS -->
582
  </div>
583
 
584
+ <!-- TOAST CONTAINER -->
585
+ <div class="toast-container" id="toastContainer"></div>
586
+
587
+ <!-- MODALS -->
588
+ <div class="modal-overlay" id="configModalOverlay">
589
+ <div class="modal">
590
+ <div class="modal-head">
591
+ <span id="cfgModalTitle">Configure Chart</span>
592
+ <button class="icon-btn" onclick="ConfigModal.close()">✕</button>
593
  </div>
594
+ <div class="modal-body">
595
+ <!-- Loading State -->
596
+ <div id="cfgLoader" style="display:none; text-align:center; padding:20px; color:var(--text-secondary);">Loading metadata...</div>
597
+
598
+ <!-- Config Form -->
599
  <div id="cfgForm">
600
  <div class="form-row">
601
  <div class="form-group">
602
+ <label>Symbol (Root)</label>
603
+ <div class="select-wrapper">
604
+ <select id="cfgRoot" onchange="ConfigModal.onRootChange()" style="width:100%"></select>
605
+ <span class="select-chevron">▼</span>
606
+ </div>
607
  </div>
608
  <div class="form-group">
609
+ <label>Expiry</label>
610
+ <div class="select-wrapper">
611
+ <select id="cfgExp" onchange="ConfigModal.onExpChange()" style="width:100%"></select>
612
+ <span class="select-chevron">▼</span>
613
+ </div>
614
  </div>
615
  </div>
616
 
617
  <div class="form-group">
618
+ <label>Instrument</label>
619
+ <div class="select-wrapper">
620
+ <select id="cfgInst" style="width:100%"></select>
621
+ <span class="select-chevron">▼</span>
622
+ </div>
623
  </div>
624
+
625
  <div class="form-row">
626
  <div class="form-group">
627
+ <label>Timeframe</label>
628
+ <div class="select-wrapper">
629
+ <select id="cfgTF" style="width:100%">
630
+ <option value="1min">1 Minute</option>
631
+ <option value="3min">3 Minutes</option>
632
+ <option value="5min">5 Minutes</option>
633
+ <option value="15min">15 Minutes</option>
634
+ <option value="30min">30 Minutes</option>
635
+ <option value="1hour">1 Hour</option>
636
+ </select>
637
+ <span class="select-chevron">▼</span>
638
+ </div>
639
  </div>
640
  <div class="form-group">
641
+ <label>Range (HH:MM)</label>
642
+ <div style="display:flex; gap:8px;">
643
+ <input type="time" id="cfgStart" value="09:15">
644
+ <input type="time" id="cfgEnd" value="15:30">
645
  </div>
646
  </div>
647
  </div>
648
 
649
+ <hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
650
+
651
+ <div class="form-group">
652
+ <label>Plot Lines (Vars: $P, $B, $S, $V, $OI)</label>
653
+ <div id="cfgSeriesList"></div>
654
+ <button class="btn-ghost" style="width:100%; margin-top:8px; border-style:dashed;" onclick="ConfigModal.addSeriesRow()">+ Add Line</button>
655
  </div>
656
  </div>
657
  </div>
658
+ <div class="modal-foot">
659
+ <button onclick="ConfigModal.close()" style="background:transparent; border:none; margin-right:10px;">Cancel</button>
660
+ <button class="btn-primary" onclick="ConfigModal.save()">Save Configuration</button>
661
  </div>
662
  </div>
663
  </div>
664
 
665
+ <!-- RESTORE SESSION POPUP -->
666
+ <div class="modal-overlay" id="restoreModal">
667
+ <div class="modal" style="width: 360px;">
668
+ <div class="modal-head">Restore Session?</div>
669
+ <div class="modal-body" style="color: var(--text-secondary);">
670
+ We found a saved layout from your previous visit. Would you like to restore it?
671
+ </div>
672
+ <div class="modal-foot">
673
+ <button class="btn-danger" onclick="GlobalApp.initNew()" style="margin-right:8px;">Start Fresh</button>
674
+ <button class="btn-primary" onclick="GlobalApp.loadFromStorage()">Restore</button>
675
+ </div>
676
+ </div>
677
  </div>
678
 
679
  <script>
680
  /**
681
+ * --- UTILS ---
682
+ */
683
+ const Toast = {
684
+ show(msg, type='info') {
685
+ const container = document.getElementById('toastContainer');
686
+ const el = document.createElement('div');
687
+ el.className = `toast toast-${type}`;
688
+ el.innerHTML = `<span>${msg}</span>`;
689
+ container.appendChild(el);
690
+ setTimeout(() => {
691
+ el.style.opacity = '0';
692
+ el.style.transform = 'translateY(10px)';
693
+ setTimeout(() => el.remove(), 300);
694
+ }, 3000);
695
+ }
696
+ };
697
+
698
+ /**
699
+ * --- GLOBAL STATE MANAGER ---
700
  */
701
+ const GlobalApp = {
702
  state: {
703
+ date: '',
704
+ mode: 'HIST',
705
+ columns: 3, // Default to 3
706
+ charts: {}, // ID -> { config: {}, instance: ChartJS, interval: null, lastData: null, col: int }
707
+ layout: [] // Array of Arrays [[id1, id2], [id3], [id4]]
708
  },
709
+
 
710
  defaults: [
711
+ { formula: '$P', color: '#58a6ff', label: 'Price', axis: 'left' },
712
+ { formula: '$B', color: '#238636', label: 'Buy Qty', axis: 'right' },
713
+ { formula: '$S', color: '#da3633', label: 'Sell Qty', axis: 'right' }
714
  ],
715
 
716
  async init() {
717
+ // Load dates first
718
  await this.loadDates();
719
 
720
+ // Check local storage
721
+ if (localStorage.getItem('dc_state_v3')) {
722
+ const modal = document.getElementById('restoreModal');
723
+ modal.style.display = 'flex';
724
+ setTimeout(() => modal.classList.add('active'), 10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  } else {
726
+ this.initNew();
727
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  },
729
 
730
  async loadDates() {
 
732
  const res = await fetch('/api/dates');
733
  const dates = await res.json();
734
  const sel = document.getElementById('globalDate');
735
+
736
+ // Populating ALL dates as requested
737
  sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
738
+
739
+ if(dates.length) {
740
+ this.state.date = dates[0];
741
+ sel.value = dates[0];
742
+ }
743
+ } catch(e) { console.error("Dates error", e); Toast.show("Failed to load dates", "error"); }
744
  },
745
 
746
+ initNew() {
747
+ this.closeRestoreModal();
748
+ this.setLayout(3); // Default 3 cols
749
+ // Add one chart per column initially
750
+ this.addChart(0);
751
+ this.addChart(1);
752
+ this.addChart(2);
753
  },
754
 
755
+ closeRestoreModal() {
756
+ const modal = document.getElementById('restoreModal');
757
+ modal.classList.remove('active');
758
+ setTimeout(() => modal.style.display = 'none', 200);
 
 
759
  },
760
 
761
+ loadFromStorage() {
762
+ try {
763
+ const raw = localStorage.getItem('dc_state_v3');
764
+ const saved = JSON.parse(raw);
765
+
766
+ this.closeRestoreModal();
767
+
768
+ // Restore Global Settings
769
+ this.state.date = saved.date;
770
+ this.state.mode = saved.mode;
771
+ this.state.columns = saved.columns;
772
+
773
+ // Update UI Controls
774
+ const dateSel = document.getElementById('globalDate');
775
+ if(dateSel.querySelector(`option[value="${saved.date}"]`)) {
776
+ dateSel.value = saved.date;
777
+ }
778
+ this.updateModeUI();
779
+ this.updateLayoutUI();
780
+
781
+ // Rebuild Layout
782
+ this.setupGridDOM(); // Create columns
783
+
784
+ // Add Charts
785
+ saved.chartList.forEach(item => {
786
+ this.addChart(item.col, item.id, item.config);
787
+ });
788
+
789
+ Toast.show("Session Restored", "success");
790
+
791
+ } catch(e) {
792
+ console.error("Load failed", e);
793
+ Toast.show("Save file corrupted, starting new.", "error");
794
+ this.initNew();
795
+ }
796
+ },
797
+
798
+ saveToStorage() {
799
+ // Flatten layout for storage
800
+ const chartList = [];
801
+ Object.keys(this.state.charts).forEach(id => {
802
+ chartList.push({
803
+ id: id,
804
+ col: this.state.charts[id].col,
805
+ config: this.state.charts[id].config
806
+ });
807
+ });
808
+
809
+ const dump = {
810
+ date: this.state.date,
811
+ mode: this.state.mode,
812
+ columns: this.state.columns,
813
+ chartList: chartList
814
+ };
815
+ localStorage.setItem('dc_state_v3', JSON.stringify(dump));
816
  },
817
 
818
+ resetApp() {
819
+ if(!confirm("Are you sure you want to reset everything?")) return;
820
+ localStorage.removeItem('dc_state_v3');
821
+ location.reload();
822
  },
823
 
824
+ // --- LAYOUT LOGIC (PERSISTENCE) ---
825
+ setLayout(cols) {
826
+ if (cols === this.state.columns && document.getElementById('col-0')) return; // No change
 
827
 
828
+ this.state.columns = cols;
829
+ this.updateLayoutUI();
830
+
831
+ // 1. Get all existing chart elements
832
+ const existingCards = [];
833
+ Object.keys(this.state.charts).forEach(id => {
834
+ const el = document.getElementById(`card-${id}`);
835
+ if (el) existingCards.push({ id, el });
836
+ });
837
+
838
+ // 2. Setup New Grid Columns
839
+ this.setupGridDOM();
840
+
841
+ // 3. Re-distribute existing charts
842
+ existingCards.forEach((item, index) => {
843
+ const newColIdx = index % cols; // Round robin distribution
844
+ const colDiv = document.getElementById(`col-${newColIdx}`);
845
+ const addBtn = colDiv.querySelector('.column-add-btn');
846
 
847
+ // Move DOM Element (Persists Canvas State if handled correctly, though resize might be needed)
848
+ colDiv.insertBefore(item.el, addBtn);
 
 
 
849
 
850
+ // Update Internal State
851
+ this.state.charts[item.id].col = newColIdx;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
 
853
+ // Trigger Resize on ChartJS instance just in case
854
+ const inst = this.state.charts[item.id].instance;
855
+ if(inst) inst.resize();
856
  });
857
+
858
+ this.saveToStorage();
859
  },
860
 
861
+ setupGridDOM() {
862
+ const container = document.getElementById('gridContainer');
863
+ container.innerHTML = ''; // Clear container
864
+
865
+ for (let i = 0; i < this.state.columns; i++) {
866
+ const colDiv = document.createElement('div');
867
+ colDiv.className = 'column';
868
+ colDiv.id = `col-${i}`;
869
+
870
+ const addBtn = document.createElement('div');
871
+ addBtn.className = 'column-add-btn';
872
+ addBtn.innerHTML = '+ Add Chart';
873
+ addBtn.onclick = () => this.addChart(i);
874
+
875
+ colDiv.appendChild(addBtn);
876
+ container.appendChild(colDiv);
877
+ }
878
+ },
879
+
880
+ updateLayoutUI() {
881
+ document.querySelectorAll('.layout-btn').forEach(b => b.classList.remove('active'));
882
+ const btn = document.getElementById(`lay${this.state.columns}`);
883
+ if(btn) btn.classList.add('active');
884
+ },
885
+
886
+ // --- CHART MANAGEMENT ---
887
+ addChart(colIdx, forceId = null, forceConfig = null) {
888
+ const id = forceId || 'c_' + Date.now() + Math.random().toString(36).substr(2,5);
889
+
890
+ // Init State
891
+ if (!this.state.charts[id]) {
892
+ this.state.charts[id] = {
893
+ col: colIdx,
894
+ config: forceConfig || {},
895
+ instance: null,
896
+ interval: null,
897
+ lastData: null
898
+ };
899
+ }
900
+
901
+ // Render UI
902
+ const colDiv = document.getElementById(`col-${colIdx}`);
903
+ const btn = colDiv.querySelector('.column-add-btn');
904
+ const el = this.createChartElement(id);
905
+ colDiv.insertBefore(el, btn);
906
+
907
+ // If config exists (Restore or moved), plot from memory or fetch
908
+ if (forceConfig && forceConfig.root) {
909
+ // Delay slightly to ensure DOM is painted for Canvas
910
+ setTimeout(() => ChartLogic.plot(id), 0);
911
+ }
912
+
913
+ if (!forceId) this.saveToStorage();
914
+ },
915
+
916
+ removeChart(id) {
917
+ // Confirmation is annoying, maybe just do it? Or subtle check.
918
+ if(!confirm("Remove this chart?")) return;
919
+
920
+ ChartLogic.stopLive(id);
921
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
922
+
923
+ delete this.state.charts[id];
924
+
925
+ const el = document.getElementById(`card-${id}`);
926
+ if(el) el.remove();
927
+
928
+ this.saveToStorage();
929
+ },
930
+
931
+ createChartElement(id) {
932
  const div = document.createElement('div');
933
  div.className = 'chart-card';
934
+ div.id = `card-${id}`;
935
  div.innerHTML = `
936
+ <div class="card-header">
937
+ <div class="card-title" id="title-${id}">
938
+ <span class="status-dot offline"></span>
939
+ <span>New Chart</span>
940
  </div>
941
+ <div class="controls" style="gap:4px">
942
+ <button class="icon-btn" onclick="ConfigModal.open('${id}')">⚙</button>
943
+ <button class="icon-btn" onclick="GlobalApp.removeChart('${id}')">✕</button>
944
  </div>
945
  </div>
946
+ <div class="card-body" id="body-${id}">
947
  <div class="empty-state">
948
+ <p style="font-weight:500;">Not Configured</p>
949
+ <button class="btn-ghost" onclick="ConfigModal.open('${id}')">Configure Chart</button>
950
  </div>
951
  </div>
952
  `;
953
  return div;
954
  },
955
 
956
+ // --- GLOBAL CONTROLS ---
957
+ onDateChange() {
958
+ this.state.date = document.getElementById('globalDate').value;
959
+ this.saveToStorage();
960
+ // Reload all charts
961
+ Object.keys(this.state.charts).forEach(id => {
962
+ this.state.charts[id].lastData = null; // Clear cache
963
+ ChartLogic.plot(id);
964
+ });
965
+ Toast.show(`Date changed to ${this.state.date}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
966
  },
967
 
968
+ setMode(mode) {
969
+ this.state.mode = mode;
970
+ this.updateModeUI();
971
+ this.saveToStorage();
972
+
973
+ Object.keys(this.state.charts).forEach(id => {
974
+ if(mode === 'LIVE') ChartLogic.startLive(id);
975
+ else ChartLogic.stopLive(id);
976
+ });
977
+ Toast.show(`Switched to ${mode === 'LIVE' ? 'Live Auto-Refresh' : 'Historical Mode'}`);
978
  },
979
 
980
+ updateModeUI() {
981
+ document.getElementById('modeHist').classList.toggle('active', this.state.mode === 'HIST');
982
+ document.getElementById('modeLive').classList.toggle('active', this.state.mode === 'LIVE');
 
 
983
  }
984
  };
985
 
986
  /**
987
+ * --- CHART LOGIC & DATA ---
988
  */
989
+ const ChartLogic = {
990
+
991
+ async plot(id, isUpdate=false) {
992
+ const chartData = GlobalApp.state.charts[id];
993
+ if (!chartData) return;
994
+ const cfg = chartData.config;
995
+
996
+ if (!cfg || !cfg.root) return; // Not configured
997
 
998
+ // UI Header Update
999
+ const titleEl = document.querySelector(`#card-${id} .card-title`);
1000
+ const dotClass = GlobalApp.state.mode === 'LIVE' ? 'live' : 'hist';
1001
  if(titleEl) {
1002
  titleEl.innerHTML = `
1003
+ <span class="status-dot ${dotClass}"></span>
1004
+ ${cfg.root} <span style="color:var(--accent); margin-left:4px;">${cfg.expiry}</span>
1005
+ <span class="card-meta">${cfg.instrument} · ${cfg.timeframe}</span>
1006
  `;
1007
  }
1008
 
1009
+ // Ensure DOM exists
1010
+ const body = document.getElementById(`body-${id}`);
1011
+ if (!body) return;
 
 
1012
 
1013
+ // Create Canvas if missing
1014
+ if (!body.querySelector('canvas')) {
1015
+ body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
 
1016
  }
1017
 
1018
  try {
1019
+ let data;
 
 
 
 
 
 
 
 
 
 
 
 
 
1020
 
1021
+ // Use Cache if simple redraw and not live update
1022
+ if (!isUpdate && chartData.lastData) {
1023
+ data = chartData.lastData;
1024
+ } else {
1025
+ // Fetch
1026
+ if(isUpdate && titleEl) titleEl.querySelector('.status-dot').classList.add('retry');
1027
+
1028
+ const res = await fetch('/api/fetch_series', {
1029
+ method: 'POST',
1030
+ headers: {'Content-Type': 'application/json'},
1031
+ body: JSON.stringify({
1032
+ date: GlobalApp.state.date,
1033
+ root: cfg.root,
1034
+ expiry: cfg.expiry,
1035
+ instrument: cfg.instrument,
1036
+ timeframe: cfg.timeframe,
1037
+ start_time: cfg.start,
1038
+ end_time: cfg.end
1039
+ })
1040
+ });
1041
+ data = await res.json();
1042
+
1043
+ // Update Cache
1044
+ if (data.labels && data.labels.length > 0) {
1045
+ chartData.lastData = data;
1046
+ }
1047
  }
1048
 
1049
+ if(data.error || !data.labels || data.labels.length === 0) {
1050
+ if(GlobalApp.state.mode !== 'LIVE') {
1051
+ if(chartData.instance) chartData.instance.destroy();
1052
+ chartData.instance = null;
1053
+ body.innerHTML = '<div class="empty-state">No Data Available</div>';
1054
+ }
1055
  return;
1056
  }
1057
 
1058
+ // Success Visuals
1059
+ if(titleEl) titleEl.querySelector('.status-dot').className = `status-dot ${dotClass}`;
1060
+
1061
  // Process Data
1062
+ const datasets = cfg.series.map(s => {
1063
  const computed = data.labels.map((_, i) => {
1064
+ const P = data.P[i] || 0, V = data.V[i] || 0, OI = data.OI[i] || 0;
1065
+ const B = data.B[i] || 0, S = data.S[i] || 0;
1066
+ let f = s.formula
1067
+ .replace(/\$OI/g, OI).replace(/\$P/g, P).replace(/\$V/g, V)
1068
+ .replace(/\$B/g, B).replace(/\$S/g, S);
1069
+ try { return new Function('return ' + f)(); } catch { return null; }
1070
  });
1071
+
1072
  return {
1073
+ label: s.label,
1074
+ data: computed,
1075
+ borderColor: s.color,
1076
+ backgroundColor: s.color,
1077
+ borderWidth: 1.5,
1078
+ pointRadius: 0,
1079
+ pointHoverRadius: 4,
1080
+ yAxisID: s.axis === 'left' ? 'y' : 'y1',
1081
+ tension: 0.1
1082
  };
1083
  });
1084
 
1085
+ this.renderCanvas(id, data.labels, datasets);
 
1086
 
1087
+ } catch (e) {
1088
+ console.error("Plot Error", e);
1089
+ }
1090
  },
1091
 
1092
+ renderCanvas(id, labels, datasets) {
1093
+ const ctx = document.getElementById(`canvas-${id}`);
1094
+ if (!ctx) return;
1095
+
1096
+ const chartData = GlobalApp.state.charts[id];
1097
 
1098
+ // If chart exists, update data (prevent flicker)
1099
+ if (chartData.instance) {
1100
+ chartData.instance.data.labels = labels;
1101
+ chartData.instance.data.datasets = datasets;
1102
+ chartData.instance.update('none');
1103
  } else {
1104
+ // Create New
1105
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
1106
+ Chart.defaults.color = '#8b949e';
1107
+ chartData.instance = new Chart(ctx.getContext('2d'), {
1108
  type: 'line',
1109
  data: { labels, datasets },
1110
  options: {
1111
+ responsive: true,
1112
+ maintainAspectRatio: false,
1113
  animation: false,
1114
  interaction: { mode: 'index', intersect: false },
1115
+ plugins: {
1116
+ legend: { labels: { usePointStyle: true, boxWidth: 6 } },
1117
+ tooltip: {
1118
+ backgroundColor: 'rgba(22, 27, 34, 0.95)',
1119
+ titleColor:'#e6edf3',
1120
+ bodyColor:'#e6edf3',
1121
+ borderColor:'#30363d',
1122
+ borderWidth:1,
1123
+ padding: 10,
1124
+ cornerRadius: 6
1125
+ }
1126
  },
1127
  scales: {
1128
+ x: {
1129
+ grid: { color: '#30363d', tickLength: 0, drawBorder: false, lineWidth: 0.5 },
1130
+ ticks: { maxTicksLimit: 8, maxRotation: 0 }
1131
+ },
1132
+ y: {
1133
+ type: 'linear', display: true, position: 'left',
1134
+ grid: { color: '#30363d', borderDash: [2, 2], drawBorder: false },
1135
+ },
1136
+ y1: {
1137
+ type: 'linear', display: hasRight, position: 'right',
1138
+ grid: { display: false, drawBorder: false }
1139
+ }
1140
  }
1141
  }
1142
  });
1143
  }
1144
  },
1145
 
1146
+ startLive(id) {
1147
+ this.stopLive(id);
1148
+ this.plot(id);
1149
+ GlobalApp.state.charts[id].interval = setInterval(() => {
1150
+ this.plot(id, true);
1151
+ }, 15000);
 
 
 
1152
  },
1153
 
1154
+ stopLive(id) {
1155
+ const c = GlobalApp.state.charts[id];
1156
+ if(c && c.interval) {
1157
+ clearInterval(c.interval);
1158
+ c.interval = null;
1159
+ }
1160
  }
1161
  };
1162
 
1163
  /**
1164
+ * --- CONFIGURATION MODAL ---
1165
  */
1166
+ const ConfigModal = {
1167
  currentId: null,
1168
+ isGlobal: false,
1169
+
1170
  async open(id) {
1171
  this.currentId = id;
1172
+ this.isGlobal = false;
1173
+
1174
+ const modal = document.querySelector('.modal');
1175
+ document.getElementById('cfgModalTitle').textContent = "Configure Chart";
1176
+ document.getElementById('configModalOverlay').style.display = 'flex';
1177
+ // Trigger Animation
1178
+ setTimeout(() => document.getElementById('configModalOverlay').classList.add('active'), 10);
1179
 
1180
+ document.getElementById('cfgForm').style.display = 'block';
1181
+ document.getElementById('cfgLoader').style.display = 'none';
1182
+
1183
+ const c = GlobalApp.state.charts[id].config;
1184
 
1185
+ await this.loadRoots(c.root);
 
1186
 
1187
+ document.getElementById('cfgTF').value = c.timeframe || '1min';
1188
+ document.getElementById('cfgStart').value = c.start || '09:15';
1189
+ document.getElementById('cfgEnd').value = c.end || '15:30';
1190
 
1191
+ const series = c.series && c.series.length ? c.series : GlobalApp.defaults;
 
1192
  this.renderSeries(series);
1193
+
1194
+ if(c.root) {
1195
+ document.getElementById('cfgRoot').value = c.root;
1196
+ await this.onRootChange(c.expiry, c.instrument);
1197
  }
1198
  },
1199
 
1200
  openDefaults() {
1201
+ this.isGlobal = true;
1202
+ document.getElementById('cfgModalTitle').textContent = "Global Default Series";
1203
+ document.getElementById('configModalOverlay').style.display = 'flex';
1204
+ setTimeout(() => document.getElementById('configModalOverlay').classList.add('active'), 10);
1205
+
1206
+ document.getElementById('cfgForm').style.display = 'block';
1207
+ // Hacky: hide other fields via CSS or just ignore them. For now, we leave them but they won't save.
1208
+ this.renderSeries(GlobalApp.defaults);
1209
  },
1210
 
1211
+ close() {
1212
+ const overlay = document.getElementById('configModalOverlay');
1213
+ overlay.classList.remove('active');
1214
+ setTimeout(() => overlay.style.display = 'none', 200);
1215
+ },
1216
 
1217
+ async loadRoots(preSelect) {
1218
+ const rSel = document.getElementById('cfgRoot');
1219
+ rSel.innerHTML = '<option>Loading...</option>';
1220
+ try {
1221
+ const date = GlobalApp.state.date;
1222
+ const res = await fetch(`/api/roots?date=${date}`);
1223
+ const roots = await res.json();
1224
+ rSel.innerHTML = `<option value="">-- Select Symbol --</option>` + roots.map(r => `<option>${r}</option>`).join('');
1225
+ if(preSelect && roots.includes(preSelect)) rSel.value = preSelect;
1226
+ } catch(e) { rSel.innerHTML = '<option>Error</option>'; }
1227
  },
1228
 
1229
  async onRootChange(preExp, preInst) {
1230
  const root = document.getElementById('cfgRoot').value;
1231
+ const date = GlobalApp.state.date;
1232
+ const eSel = document.getElementById('cfgExp');
1233
+ eSel.innerHTML = '<option>Loading...</option>';
1234
+
1235
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
1236
+ const exps = await res.json();
1237
+ eSel.innerHTML = `<option value="">-- Select Expiry --</option>` + exps.map(e => `<option>${e}</option>`).join('');
1238
 
1239
+ if(preExp && exps.includes(preExp)) {
1240
+ eSel.value = preExp;
1241
+ await this.onExpChange(preInst);
1242
  } else {
1243
+ const auto = await fetch(`/api/auto_config?date=${date}&root=${root}`).then(r=>r.json());
 
1244
  if(auto.current) {
1245
+ eSel.value = auto.current.expiry;
1246
+ await this.onExpChange(auto.current.instrument);
1247
  }
1248
  }
1249
  },
 
1251
  async onExpChange(preInst) {
1252
  const root = document.getElementById('cfgRoot').value;
1253
  const exp = document.getElementById('cfgExp').value;
1254
+ const date = GlobalApp.state.date;
1255
+ const iSel = document.getElementById('cfgInst');
1256
+
1257
+ iSel.innerHTML = '<option>Loading...</option>';
1258
+ const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
1259
+ const insts = await res.json();
1260
+ iSel.innerHTML = `<option value="">-- Select Instrument --</option>` + insts.map(i => `<option>${i}</option>`).join('');
1261
+
1262
+ if(preInst && insts.includes(preInst)) iSel.value = preInst;
1263
+ else if(insts.length > 0) iSel.value = insts[0];
1264
  },
1265
 
1266
  renderSeries(list) {
1267
+ const container = document.getElementById('cfgSeriesList');
1268
+ container.innerHTML = '';
1269
+ list.forEach(s => {
1270
+ const div = document.createElement('div');
1271
+ div.className = 'series-row';
1272
+ div.innerHTML = `
1273
+ <input type="color" value="${s.color}" class="color-picker s-color">
1274
+ <input type="text" value="${s.formula}" class="s-formula" placeholder="$P">
1275
+ <input type="text" value="${s.label}" class="s-label" placeholder="Label">
1276
+ <div class="select-wrapper">
1277
+ <select class="s-axis" style="min-width:50px; padding-right:15px;">
1278
+ <option value="left" ${s.axis==='left'?'selected':''}>L</option>
1279
+ <option value="right" ${s.axis==='right'?'selected':''}>R</option>
1280
+ </select>
1281
+ </div>
1282
+ <button class="icon-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1283
+ `;
1284
+ container.appendChild(div);
1285
+ });
1286
  },
1287
 
1288
+ addSeriesRow() {
1289
+ const container = document.getElementById('cfgSeriesList');
1290
+ const div = document.createElement('div');
1291
+ div.className = 'series-row';
1292
+ div.innerHTML = `
1293
+ <input type="color" value="#ffffff" class="color-picker s-color">
1294
+ <input type="text" value="$P" class="s-formula">
1295
+ <input type="text" value="New" class="s-label">
1296
+ <div class="select-wrapper">
1297
+ <select class="s-axis" style="min-width:50px; padding-right:15px;">
1298
+ <option value="left">L</option><option value="right">R</option>
1299
+ </select>
1300
+ </div>
1301
+ <button class="icon-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1302
  `;
1303
+ container.appendChild(div);
1304
+ div.scrollIntoView({ behavior: 'smooth' });
1305
  },
1306
 
1307
  save() {
1308
+ const rows = document.querySelectorAll('#cfgSeriesList .series-row');
1309
+ const newSeries = Array.from(rows).map(r => ({
1310
+ color: r.querySelector('.s-color').value,
1311
+ formula: r.querySelector('.s-formula').value,
1312
+ label: r.querySelector('.s-label').value,
1313
+ axis: r.querySelector('.s-axis').value
 
 
 
 
 
1314
  }));
1315
 
1316
+ if(this.isGlobal) {
1317
+ GlobalApp.defaults = newSeries;
1318
+ Toast.show("Global Defaults Updated", "success");
1319
+ } else {
1320
+ const root = document.getElementById('cfgRoot').value;
1321
+ const exp = document.getElementById('cfgExp').value;
1322
+ const inst = document.getElementById('cfgInst').value;
1323
+
1324
+ if(!root || !exp || !inst) { Toast.show("Please select all symbol details", "warning"); return; }
1325
+
1326
+ const config = {
1327
+ root, expiry: exp, instrument: inst,
1328
+ timeframe: document.getElementById('cfgTF').value,
1329
+ start: document.getElementById('cfgStart').value,
1330
+ end: document.getElementById('cfgEnd').value,
1331
+ series: newSeries
1332
+ };
1333
+
1334
+ GlobalApp.state.charts[this.currentId].config = config;
1335
+ // Clear cache to force redraw with new config
1336
+ GlobalApp.state.charts[this.currentId].lastData = null;
1337
+
1338
+ GlobalApp.saveToStorage();
1339
+ ChartLogic.plot(this.currentId);
1340
+ Toast.show("Chart Configured", "success");
1341
+ }
1342
  this.close();
 
1343
  }
1344
  };
1345
 
1346
+ window.onload = () => GlobalApp.init();
1347
 
1348
  </script>
1349
  </body>