topsecrettraders commited on
Commit
acbc567
·
verified ·
1 Parent(s): bf1edf9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1028 -220
app.py CHANGED
@@ -58,9 +58,10 @@ def get_dates():
58
 
59
  @app.get("/api/roots")
60
  def get_roots(date: str, q: Optional[str] = None):
61
- """Fetch roots. If q is provided, search specifically."""
62
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
63
- endpoint = f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}"
 
64
  if q:
65
  endpoint += f"&root=ilike.*{q.upper()}*"
66
 
@@ -73,7 +74,7 @@ def get_roots(date: str, q: Optional[str] = None):
73
 
74
  @app.get("/api/auto_config")
75
  def get_auto_config(date: str, root: str):
76
- """Smart helper for defaults."""
77
  root = root.upper()
78
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
79
  data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
@@ -116,26 +117,28 @@ def get_auto_config(date: str, root: str):
116
 
117
  @app.get("/api/expiries")
118
  def get_expiries(date: str, root: str):
119
- """Fetch all available expiries for root."""
120
  root = root.upper()
121
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
122
- data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
 
123
  exps = set(row['expiry'] for row in data if row.get('expiry'))
124
  return JSONResponse(sorted(list(exps)))
125
 
126
  @app.get("/api/instruments")
127
  def get_instruments(date: str, root: str, expiry: str):
128
- """Fetch instruments and segregate."""
129
  root = root.upper()
130
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
131
- data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=50")
 
132
  instruments = set()
133
  for row in data:
134
  keys = row.get('data', {}).keys()
135
  instruments.update(keys)
136
-
137
- spot_fut = []
138
- options = []
139
  for i in instruments:
140
  if i.endswith('CE') or i.endswith('PE'):
141
  options.append(i)
@@ -150,10 +153,15 @@ def get_instruments(date: str, root: str, expiry: str):
150
 
151
  @app.post("/api/fetch_series")
152
  def fetch_series(
153
- date: str = Body(...), root: str = Body(...), expiry: str = Body(...),
154
- instrument: str = Body(""), timeframe: str = Body(...),
155
- start_time: str = Body(...), end_time: str = Body(...),
156
- mode: str = Body("normal"), atm_range: int = Body(5)
 
 
 
 
 
157
  ):
158
  root = root.upper()
159
  t_start = f"{date}T{start_time}:00"
@@ -172,6 +180,7 @@ def fetch_series(
172
  try:
173
  base_ts = datetime.fromisoformat(row['minute'])
174
  data_dict = row.get('data') if mode == 'advanced' else { instrument: row.get('inst_data') }
 
175
  if not data_dict: continue
176
 
177
  if mode == 'advanced':
@@ -194,65 +203,114 @@ def fetch_series(
194
  p_base = None
195
  if base_key and base_key in data_dict:
196
  p_arr = data_dict[base_key].get('p',[])
197
- if i < len(p_arr) and p_arr[i] is not None: p_base = float(p_arr[i])
 
198
 
199
  if p_base is None: continue
200
 
201
  atm_strike = min(sorted_strikes, key=lambda x: abs(x - p_base)) if sorted_strikes else None
202
  target_strikes = set()
 
203
  if atm_strike is not None:
204
  atm_idx = sorted_strikes.index(atm_strike)
205
- target_strikes = set(sorted_strikes[max(0, atm_idx - atm_range):min(len(sorted_strikes), atm_idx + atm_range + 1)])
 
 
206
 
207
  cb, cs, pb, ps = 0.0, 0.0, 0.0, 0.0
208
  tot_v, tot_oi = 0.0, 0.0
 
209
  for k, meta in opt_keys.items():
210
  if meta['strike'] in target_strikes:
211
  k_data = data_dict[k]
212
- b_val = float(k_data.get('b', [0]*6)[i]) if i < 6 and k_data.get('b', [0]*6)[i] else 0.0
213
- s_val = float(k_data.get('s', [0]*6)[i]) if i < 6 and k_data.get('s', [0]*6)[i] else 0.0
214
- v_val = float(k_data.get('v', [0]*6)[i]) if i < 6 and k_data.get('v', [0]*6)[i] else 0.0
215
- o_val = float(k_data.get('o', [0]*6)[i]) if i < 6 and k_data.get('o', [0]*6)[i] else 0.0
216
- tot_v += v_val; tot_oi += o_val
217
- if meta['type'] == 'CE': cb += b_val; cs += s_val
218
- else: pb += b_val; ps += s_val
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  records.append({
221
- "ts": base_ts + timedelta(seconds=i*10), "price": p_base,
222
- "buy_qty": cb + pb, "sell_qty": cs + ps, "volume": tot_v, "oi": tot_oi,
 
 
 
 
223
  "cb": cb, "cs": cs, "pb": pb, "ps": ps
224
  })
 
225
  else:
226
  inst_data = data_dict.get(instrument)
227
  if not inst_data: continue
 
 
 
 
 
 
228
  for i in range(6):
229
- p_arr = inst_data.get('p', [])
230
  if i < len(p_arr) and p_arr[i] is not None:
231
  records.append({
232
- "ts": base_ts + timedelta(seconds=i*10), "price": float(p_arr[i]),
233
- "buy_qty": float(inst_data.get('b', [0]*6)[i] or 0),
234
- "sell_qty": float(inst_data.get('s', [0]*6)[i] or 0),
235
- "volume": float(inst_data.get('v', [0]*6)[i] or 0),
236
- "oi": float(inst_data.get('o', [0]*6)[i] or 0),
 
237
  "cb": 0.0, "cs": 0.0, "pb": 0.0, "ps": 0.0
238
  })
239
- except Exception: continue
 
 
 
 
240
 
241
- if not records: return JSONResponse({"error": "No data found", "labels":[], "P":[]})
242
- df = pd.DataFrame(records).set_index('ts')
243
- panda_tf = {"1min": "1min", "3min": "3min", "5min": "5min", "15min": "15min", "30min": "30min", "1hour": "1h"}.get(timeframe, "1min")
244
- resampled = df.resample(panda_tf).agg({
245
- "price": "last", "volume": "last", "oi": "last", "buy_qty": "mean", "sell_qty": "mean",
 
 
 
 
 
 
 
246
  "cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
247
- }).ffill().fillna(0)
248
 
249
- return JSONResponse({
 
 
250
  "labels": resampled.index.strftime('%H:%M').tolist(),
251
- "P": resampled['price'].tolist(), "B": resampled['buy_qty'].tolist(),
252
- "S": resampled['sell_qty'].tolist(), "V": resampled['volume'].tolist(),
253
- "OI": resampled['oi'].tolist(), "CB": resampled['cb'].tolist(),
254
- "CS": resampled['cs'].tolist(), "PB": resampled['pb'].tolist(), "PS": resampled['ps'].tolist()
255
- })
 
 
 
 
 
 
 
 
256
 
257
  # ==========================================
258
  # 5. FRONTEND TEMPLATE
@@ -268,52 +326,166 @@ HTML_TEMPLATE = """
268
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
269
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
270
  <style>
271
- :root { --bg-body: #0d1117; --bg-header: #161b22; --bg-card: #0d1117; --bg-panel: #21262d; --bg-input: #161b22; --border: #30363d; --border-hover: #8b949e; --text-primary: #e6edf3; --text-secondary: #8b949e; --accent: #2f81f7; --success: #238636; --danger: #da3633; --warning: #d29922; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
273
- body { background: var(--bg-body); color: var(--text-primary); font-family: 'Inter', sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px; }
274
- ::-webkit-scrollbar { width: 8px; height: 8px; }
 
 
 
275
  ::-webkit-scrollbar-track { background: var(--bg-body); }
276
- ::-webkit-scrollbar-thumb { background: var(--bg-panel); border-radius: 4px; }
277
- .top-bar { height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0; }
278
- .brand { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; }
 
 
 
 
 
279
  .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
280
  .controls { display: flex; align-items: center; gap: 12px; }
281
- select, button, input { background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary); padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: 0.2s; }
282
- input:focus { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(47,129,247,0.2); }
 
 
 
 
283
  button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
284
  button.primary { background: var(--accent); border-color: var(--accent); color: white; }
 
285
  button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
 
 
286
  .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
287
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
288
- .layout-btn.active { background: var(--bg-panel); color: var(--accent); }
289
- .main-container { flex: 1; display: flex; overflow: auto; }
290
- .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 300px; }
291
- .add-chart-btn { height: 40px; border: none; border-top: 1px dashed var(--border); background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
292
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
293
- .card-header { height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; background: var(--bg-header); }
294
- .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; }
295
- .inst-label { color: var(--accent); font-family: monospace; font-size: 11px; }
 
 
 
 
 
296
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
297
- .status-dot.live { background: #3fb950; animation: pulse 2s infinite; }
298
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
299
- .card-body { flex: 1; position: relative; overflow: hidden; }
300
- .modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; display: none; justify-content: center; align-items: center; }
301
- .modal { background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px; width: 540px; max-width: 95%; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 20px 50px rgba(0,0,0,0.5); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
 
 
303
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
 
 
304
  .input-wrapper { position: relative; width: 100%; }
305
  .input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
306
- .custom-dd { position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px; }
 
 
 
 
307
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
 
308
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
309
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
310
- .series-row { background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px; }
311
- .series-row-top, .series-row-bottom { display: flex; gap: 10px; margin-bottom: 8px; align-items: center; }
312
- .color-input { padding: 0; width: 36px; border: 1px solid var(--border); background: var(--bg-input); height: 32px; cursor: pointer; border-radius: 6px; overflow: hidden; }
313
- .segmented-control { display: flex; background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px; padding: 2px; margin-bottom: 6px; }
314
- .seg-btn { flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600; color: var(--text-secondary); cursor: pointer; border: none; background: transparent; border-radius: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
316
- .info-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel); color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help; border: 1px solid var(--border); margin-left: 8px; }
 
 
 
 
 
 
 
317
  </style>
318
  </head>
319
  <body>
@@ -321,267 +493,903 @@ HTML_TEMPLATE = """
321
  <div class="top-bar">
322
  <div class="brand">DepthChain <span>PRO</span></div>
323
  <div class="controls">
324
- <select id="globalDate" onchange="App.onDateChange()"></select>
325
  <div style="position: relative;">
326
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left: 28px; font-weight: 600;">
327
- <option value="LIVE">LIVE</option><option value="HIST">HISTORY</option>
 
328
  </select>
329
- <div id="modeDot" class="status-dot live" style="position: absolute; left: 10px; top: 12px;"></div>
330
  </div>
 
331
  <div class="layout-toggle">
332
  <button class="layout-btn" onclick="App.setLayout(1)">1</button>
333
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
334
  <button class="layout-btn active" onclick="App.setLayout(3)">3</button>
335
  <button class="layout-btn" onclick="App.setLayout(4)">4</button>
336
  </div>
 
337
  <button class="danger" onclick="App.reset()">Reset</button>
338
  </div>
339
  </div>
340
 
341
  <div class="main-container" id="gridRoot"></div>
342
 
 
343
  <div class="modal-backdrop" id="configModal">
344
  <div class="modal">
345
- <div class="modal-head" style="padding:16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; font-weight:700;">
346
- <span id="cfgTitle">Configure Chart</span><button onclick="Config.close()">✕</button>
 
347
  </div>
348
  <div class="modal-body">
349
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
350
- <div class="form-section">
351
- <label>Symbol</label>
352
- <div class="input-wrapper">
353
- <input type="text" id="cfgRoot" onfocus="CustomDD.open('cfgRoot')" oninput="CustomDD.filter('cfgRoot')" style="width:100%; text-transform: uppercase;" placeholder="Type or select..." autocomplete="off">
354
- <div class="input-arrow">▼</div><div id="dd-cfgRoot" class="custom-dd"></div>
 
 
355
  </div>
 
356
  </div>
357
- <div class="form-section">
358
- <label>Expiry</label>
359
- <div class="input-wrapper">
360
- <input type="text" id="cfgExp" onfocus="CustomDD.open('cfgExp')" oninput="CustomDD.filter('cfgExp')" style="width:100%;" placeholder="Type or select..." autocomplete="off">
361
- <div class="input-arrow">▼</div><div id="dd-cfgExp" class="custom-dd"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  </div>
363
- </div>
364
- </div>
365
- <div class="form-section"><label>Mode</label>
366
- <div class="segmented-control" id="cfgModeToggle">
367
- <button class="seg-btn active" onclick="Config.setMode('normal')">Normal</button>
368
- <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced</button>
369
- </div>
370
- </div>
371
- <div id="cfgNormalFields">
372
- <div class="form-section"><label>Instrument</label>
373
- <div class="input-wrapper">
374
- <input type="text" id="cfgInst" onfocus="CustomDD.open('cfgInst')" oninput="CustomDD.filter('cfgInst')" style="width:100%;" placeholder="Type or select..." autocomplete="off">
375
- <div class="input-arrow">▼</div><div id="dd-cfgInst" class="custom-dd"></div>
376
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  </div>
378
- </div>
379
- <div id="cfgAdvancedFields" style="display:none;">
380
- <div class="form-section"><label style="color:var(--accent);">ATM (+/-) Range</label>
381
- <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
382
- </div>
383
- </div>
384
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
385
- <div class="form-section"><label>Timeframe</label>
386
- <select id="cfgTF" style="width:100%"><option value="1min">1m</option><option value="3min">3m</option><option value="5min">5m</option><option value="15min">15m</option></select>
387
  </div>
388
- <div class="form-section"><label>Range</label><div style="display:flex; gap:8px;"><input type="time" id="cfgStart" value="09:15"><input type="time" id="cfgEnd" value="15:30"></div></div>
389
- </div>
390
- <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px;">
391
- <label>Quick Configurations</label>
392
- <div style="display:flex; gap:8px;">
393
- <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
394
- <button class="primary" onclick="Config.savePreset()">Save</button>
 
 
395
  </div>
396
  </div>
397
- <div class="form-section">
398
- <div style="display:flex; align-items:center;"><label>Plot Series</label><span class="info-icon" id="infoIcon">i</span></div>
399
- <div id="cfgSeries"></div>
400
- <button style="width:100%; border:1px dashed var(--border); background:transparent; font-weight:600; height:32px;" onclick="Config.addSeriesRow()">+ Add Line</button>
401
- </div>
402
  </div>
403
- <div class="modal-foot" style="padding:16px; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:10px;">
404
- <button onclick="Config.close()">Cancel</button><button class="primary" onclick="Config.saveChartConfig()">Save</button>
 
405
  </div>
406
  </div>
407
  </div>
408
 
409
  <script>
 
 
 
 
 
 
 
410
  const CustomDD = {
411
  data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
412
- recent: { cfgRoot: [] },
413
- init() { const saved = localStorage.getItem('dc_recent_symbols'); if(saved) this.recent.cfgRoot = JSON.parse(saved); },
414
- clear(id) { this.data[id] = []; document.getElementById(id).value = ''; },
415
- set(id, items, group = null) {
416
- if(!group) this.data[id] = items.map(i => ({val: i, group: null}));
417
- else this.data[id].push(...items.map(i => ({val: i, group})));
418
- },
419
- open(id) {
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
421
- document.getElementById(id).select();
422
- // FORCE RENDER ALL OPTIONS ON CLICK/FOCUS
 
 
423
  this.render(id, '');
424
  document.getElementById(`dd-${id}`).style.display = 'block';
425
  },
426
- filter(id) {
427
- const val = document.getElementById(id).value.toUpperCase();
428
- if(id === 'cfgRoot' && val.length >= 2) {
429
- fetch(`/api/roots?date=${App.state.date}&q=${val}`).then(r=>r.json()).then(res => {
430
- this.set(id, res); this.render(id, val);
 
 
 
 
 
 
 
 
431
  });
432
- } else {
433
- this.render(id, val);
434
  }
 
 
 
435
  },
 
436
  render(id, query) {
437
  const el = document.getElementById(`dd-${id}`);
438
  const q = query.toLowerCase();
439
- let pool = [...this.data[id]];
 
 
 
 
440
  if(id === 'cfgRoot' && q === '') {
441
- pool = [...this.recent.cfgRoot.map(r => ({val: r, group: 'Recent'})), ...pool];
 
442
  }
443
- const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
444
- .filter(x => x.val.toLowerCase().includes(q));
 
445
 
446
- let html = '', currentGroup = null;
447
- if(!filtered.length) { el.innerHTML = '<div class="custom-dd-opt">No results</div>'; return; }
 
 
 
448
  filtered.forEach(item => {
449
- if(item.group && item.group !== currentGroup) { html += `<div class="custom-dd-group">${item.group}</div>`; currentGroup = item.group; }
 
 
 
450
  html += `<div class="custom-dd-opt" onclick="CustomDD.pick('${id}', '${item.val}')">${item.val}</div>`;
451
  });
452
  el.innerHTML = html;
453
- el.style.display = 'block';
454
  },
 
455
  pick(id, val) {
456
  document.getElementById(id).value = val;
457
  document.getElementById(`dd-${id}`).style.display = 'none';
 
458
  if(id === 'cfgRoot') {
459
- if(!this.recent.cfgRoot.includes(val)) { this.recent.cfgRoot.unshift(val); this.recent.cfgRoot = this.recent.cfgRoot.slice(0,10); localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot)); }
 
 
 
 
 
460
  Config.onRootChange();
461
  }
462
  if(id === 'cfgExp') Config.onExpChange();
463
  }
464
  };
465
 
466
- document.addEventListener('click', (e) => { if(!e.target.closest('.input-wrapper')) document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none'); });
467
-
468
  const App = {
469
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
470
  presets: {
471
- "Model 1": [
472
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
473
  { formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
474
  { formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
475
  ],
476
- "Model 2": [
477
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
478
  { formula: '$CB + $PS - ($PB + $CS)', color1: '#238636', color2: '#da3633', cond: '>', thresh: 0, label: 'Overall', axis: 'right' }
479
  ]
480
  },
 
481
  async init() {
482
- CustomDD.init(); await this.loadDates();
 
 
 
 
 
 
483
  const saved = localStorage.getItem('dc_pro_v3_final');
484
- if(saved) this.state = {...this.state, ...JSON.parse(saved)}; else this.initDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  document.getElementById('globalMode').value = this.state.mode;
486
- this.setLayout(this.state.columns); this.startLoop();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  },
488
- initDefault() { for(let i=0; i<3; i++) { this.addChartInternal(i); this.addChartInternal(i); } },
489
- async loadDates() { const res = await fetch('/api/dates'); const dates = await res.json(); document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join(''); this.state.date = dates[0]; },
490
- addChartInternal(col) { const id = 'c-'+Math.random().toString(36).substr(2,6); if(!this.state.layout[col]) this.state.layout[col]=[]; this.state.layout[col].push(id); this.state.charts[id]={config:{},instance:null}; },
491
- setLayout(n) { const all = this.state.layout.flat(); this.state.columns = n; this.state.layout = Array.from({length:n},()=>[]); all.forEach((id,i) => this.state.layout[i%n].push(id)); this.renderGrid(); this.save(); },
492
- save() { const clean={}; for(let id in this.state.charts) clean[id]={config:this.state.charts[id].config}; localStorage.setItem('dc_pro_v3_final', JSON.stringify({...this.state, charts:clean})); },
493
  renderGrid() {
494
- const root = document.getElementById('gridRoot'); root.innerHTML = '';
 
 
 
 
495
  this.state.layout.forEach((ids, colIdx) => {
496
- const col = document.createElement('div'); col.className = 'column';
 
 
 
 
497
  ids.forEach(id => {
498
- const card = document.createElement('div'); card.className = 'chart-card'; card.style.height = `calc(${(window.innerHeight-52)/ids.length}px - 1px)`;
499
- const cfg = this.state.charts[id].config;
500
- card.innerHTML = `<div class="card-header"><div class="card-info"><div class="status-dot ${cfg.root?'live':''}"></div><span>${cfg.root||'New'}</span></div><div><button onclick="Config.open('${id}')">⚙</button></div></div><div class="card-body" id="body-${id}"><canvas id="canvas-${id}"></canvas></div>`;
501
- col.appendChild(card); setTimeout(()=>ChartLogic.plot(id),0);
 
 
 
 
 
 
 
502
  });
503
- root.appendChild(col);
 
 
 
 
 
 
504
  });
505
  },
506
- reset() { localStorage.removeItem('dc_pro_v3_final'); location.reload(); },
507
- onDateChange() { this.state.date = document.getElementById('globalDate').value; this.save(); this.renderGrid(); },
508
- onModeChange() { this.state.mode = document.getElementById('globalMode').value; this.save(); this.renderGrid(); },
509
- startLoop() { setInterval(() => { if(this.state.mode==='LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true)); }, 15000); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  };
511
 
512
  const ChartLogic = {
513
  async plot(id, isUpdate=false) {
514
- const chart = App.state.charts[id]; if(!chart || !chart.config.root) return;
515
- const res = await fetch('/api/fetch_series', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ ...chart.config, date:App.state.date, end_time:App.state.mode==='LIVE'?new Date().toTimeString().slice(0,5):chart.config.end }) });
516
- const data = await res.json(); if(data.error) return;
517
- const datasets = chart.config.series.map(s => {
518
- const vals = data.labels.map((_, i) => {
519
- const P=data.P[i], V=data.V[i], OI=data.OI[i], B=data.B[i], S=data.S[i], CB=data.CB[i], CS=data.CS[i], PB=data.PB[i], PS=data.PS[i];
520
- try { return new Function('return '+s.formula.replace(/\$CB/g,CB).replace(/\$CS/g,CS).replace(/\$PB/g,PB).replace(/\$PS/g,PS).replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S))(); } catch{ return 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  });
522
- const hoverColor = (ctx) => { const v = ctx.raw; if(s.cond==='>') return v>s.thresh?s.color1:s.color2; if(s.cond==='<') return v<s.thresh?s.color1:s.color2; return s.color1; };
523
- return { label:s.label, data:vals, borderWidth:1.5, pointRadius:0, tension:0.1, yAxisID:s.axis==='left'?'y':'y1', segment:{ borderColor: ctx => hoverColor({raw:ctx.p1.parsed.y}) }, pointHoverBackgroundColor: hoverColor };
524
- });
525
- this.render(id, data.labels, datasets, chart.config.series);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  },
527
- render(id, labels, datasets, series) {
528
- const ctx = document.getElementById(`canvas-${id}`); if(!ctx) return;
529
- if(App.state.charts[id].instance) App.state.charts[id].instance.destroy();
530
- App.state.charts[id].instance = new Chart(ctx, { type:'line', data:{labels, datasets}, options:{ responsive:true, maintainAspectRatio:false, animation:false, plugins:{legend:{display:false}, tooltip:{ callbacks:{ labelColor: c => { const s=series[c.datasetIndex]; const v=c.parsed.y; let clr=s.color1; if(s.cond==='>') clr=v>s.thresh?s.color1:s.color2; if(s.cond==='<') clr=v<s.thresh?s.color1:s.color2; return {borderColor:clr, backgroundColor:clr}; } } } }, scales:{ y:{position:'left'}, y1:{position:'right', grid:{display:false}} } } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  }
532
  };
533
 
534
  const Config = {
535
- tid: null, currentMode: 'normal',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  async open(id) {
537
- this.tid = id; document.getElementById('configModal').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  const cfg = App.state.charts[id].config;
539
- document.getElementById('cfgPresetsDropdown').innerHTML = Object.keys(App.presets).map(k=>`<option>${k}</option>`).join('');
540
- const res = await fetch(`/api/roots?date=${App.state.date}`); const roots = await res.json(); CustomDD.set('cfgRoot', roots);
541
- if(cfg.root) { document.getElementById('cfgRoot').value = cfg.root; await this.onRootChange(cfg.expiry, cfg.instrument); }
542
- this.setMode(cfg.chartMode || 'normal'); if(cfg.series) this.renderSeries(cfg.series); else this.loadPreset("Model 1");
543
- },
544
- setMode(m) {
545
- this.currentMode = m; document.querySelectorAll('#cfgModeToggle .seg-btn').forEach(b => b.classList.toggle('active', b.innerText.toLowerCase()===m));
546
- document.getElementById('cfgNormalFields').style.display = m==='normal'?'block':'none';
547
- document.getElementById('cfgAdvancedFields').style.display = m==='advanced'?'block':'none';
548
- const icon = document.getElementById('infoIcon');
549
- icon.title = m==='advanced' ? "$P,$B,$S,$V,$OI + $CB,$CS,$PB,$PS" : "$P,$B,$S,$V,$OI";
550
- icon.style.color = m==='advanced' ? "var(--accent)" : "var(--text-secondary)";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  async onRootChange(preExp, preInst) {
553
- const root = document.getElementById('cfgRoot').value; if(!root) return;
554
- const exps = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`).then(r=>r.json());
555
- CustomDD.set('cfgExp', exps);
556
- if(preExp) document.getElementById('cfgExp').value = preExp;
557
- await this.onExpChange(preInst);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  },
 
559
  async onExpChange(preInst) {
560
- const root = document.getElementById('cfgRoot').value, exp = document.getElementById('cfgExp').value; if(!root || !exp) return;
561
- const data = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`).then(r=>r.json());
562
  CustomDD.clear('cfgInst');
563
- CustomDD.set('cfgInst', data.spot_fut, 'Futures'); CustomDD.set('cfgInst', data.options, 'Options');
 
 
 
 
 
 
 
 
564
  if(preInst) document.getElementById('cfgInst').value = preInst;
565
  },
566
- loadPreset(name) { const p = App.presets[name || document.getElementById('cfgPresetsDropdown').value]; if(p) this.renderSeries(p); },
567
- savePreset() { const n = prompt("Name:"); if(n) { App.presets[n] = this.extractSeries(); localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets)); } },
 
 
 
 
568
  renderSeries(list) {
569
  const c = document.getElementById('cfgSeries'); c.innerHTML = '';
570
  list.forEach(s => {
571
  const r = document.createElement('div'); r.className = 'series-row';
572
- r.innerHTML = `<div class="series-row-top"><input type="text" value="${s.formula}" class="s-form" style="flex:1;"><input type="text" value="${s.label}" class="s-lbl" style="width:80px;"><select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select></div><div class="series-row-bottom"><input type="color" value="${s.color1}" class="s-color1 color-input"><select class="s-cond" onchange="this.nextElementSibling.style.display=this.value==='none'?'none':'flex'"><option value="none">Solid</option><option value=">" ${s.cond==='>'?'selected':''}>If ></option><option value="<" ${s.cond==='<'?'selected':''}>If <</option></select><div class="s-cond-group" style="display:${s.cond==='none'?'none':'flex'}; gap:5px;"><input type="number" class="s-thresh" value="${s.thresh||0}" style="width:50px;"><input type="color" value="${s.color2||'#ffffff'}" class="s-color2 color-input"></div></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  c.appendChild(r);
574
  });
575
  },
576
- addSeriesRow() { this.renderSeries([...this.extractSeries(), {formula:'$P', label:'New', axis:'left', color1:'#ffffff', cond:'none'}]); },
577
- extractSeries() { return [...document.querySelectorAll('.series-row')].map(r => ({ formula:r.querySelector('.s-form').value, label:r.querySelector('.s-lbl').value, axis:r.querySelector('.s-axis').value, color1:r.querySelector('.s-color1').value, cond:r.querySelector('.s-cond').value, thresh:r.querySelector('.s-thresh').value, color2:r.querySelector('.s-color2').value })); },
578
- saveChartConfig() {
579
- const root = document.getElementById('cfgRoot').value, exp = document.getElementById('cfgExp').value;
580
- if(!root || !exp) return alert("Incomplete");
581
- App.state.charts[this.tid].config = { chartMode:this.currentMode, atmRange:parseInt(document.getElementById('cfgAtmRange').value), root, expiry:exp, instrument:document.getElementById('cfgInst').value, timeframe:document.getElementById('cfgTF').value, start:document.getElementById('cfgStart').value, end:document.getElementById('cfgEnd').value, series:this.extractSeries() };
582
- App.save(); App.renderGrid(); this.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  },
584
- close() { document.getElementById('configModal').style.display = 'none'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  };
586
 
587
  window.onload = () => App.init();
 
58
 
59
  @app.get("/api/roots")
60
  def get_roots(date: str, q: Optional[str] = None):
61
+ """Fetch roots. If q is provided, search specifically for that string."""
62
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
63
+ # Ensure high limit so we don't hit default 1000 truncation
64
+ endpoint = f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}&limit=5000"
65
  if q:
66
  endpoint += f"&root=ilike.*{q.upper()}*"
67
 
 
74
 
75
  @app.get("/api/auto_config")
76
  def get_auto_config(date: str, root: str):
77
+ """Smart helper for defaults (Current/Next expiry)."""
78
  root = root.upper()
79
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
80
  data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
 
117
 
118
  @app.get("/api/expiries")
119
  def get_expiries(date: str, root: str):
120
+ """Fetch LITERALLY all distinct expiries for a specific root."""
121
  root = root.upper()
122
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
123
+ # High limit to guarantee we catch every single expiry traded that day
124
+ data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}&limit=5000")
125
  exps = set(row['expiry'] for row in data if row.get('expiry'))
126
  return JSONResponse(sorted(list(exps)))
127
 
128
  @app.get("/api/instruments")
129
  def get_instruments(date: str, root: str, expiry: str):
130
+ """Fetch instruments and segregate into spot/future vs options."""
131
  root = root.upper()
132
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
133
+ # Limit 5 is enough since 1 row contains ALL instruments for that expiry in the JSON dict
134
+ data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
135
  instruments = set()
136
  for row in data:
137
  keys = row.get('data', {}).keys()
138
  instruments.update(keys)
139
+
140
+ spot_fut =[]
141
+ options =[]
142
  for i in instruments:
143
  if i.endswith('CE') or i.endswith('PE'):
144
  options.append(i)
 
153
 
154
  @app.post("/api/fetch_series")
155
  def fetch_series(
156
+ date: str = Body(...),
157
+ root: str = Body(...),
158
+ expiry: str = Body(...),
159
+ instrument: str = Body(""),
160
+ timeframe: str = Body(...),
161
+ start_time: str = Body(...),
162
+ end_time: str = Body(...),
163
+ mode: str = Body("normal"),
164
+ atm_range: int = Body(5)
165
  ):
166
  root = root.upper()
167
  t_start = f"{date}T{start_time}:00"
 
180
  try:
181
  base_ts = datetime.fromisoformat(row['minute'])
182
  data_dict = row.get('data') if mode == 'advanced' else { instrument: row.get('inst_data') }
183
+
184
  if not data_dict: continue
185
 
186
  if mode == 'advanced':
 
203
  p_base = None
204
  if base_key and base_key in data_dict:
205
  p_arr = data_dict[base_key].get('p',[])
206
+ if i < len(p_arr) and p_arr[i] is not None:
207
+ p_base = float(p_arr[i])
208
 
209
  if p_base is None: continue
210
 
211
  atm_strike = min(sorted_strikes, key=lambda x: abs(x - p_base)) if sorted_strikes else None
212
  target_strikes = set()
213
+
214
  if atm_strike is not None:
215
  atm_idx = sorted_strikes.index(atm_strike)
216
+ start_idx = max(0, atm_idx - atm_range)
217
+ end_idx = min(len(sorted_strikes), atm_idx + atm_range + 1)
218
+ target_strikes = set(sorted_strikes[start_idx:end_idx])
219
 
220
  cb, cs, pb, ps = 0.0, 0.0, 0.0, 0.0
221
  tot_v, tot_oi = 0.0, 0.0
222
+
223
  for k, meta in opt_keys.items():
224
  if meta['strike'] in target_strikes:
225
  k_data = data_dict[k]
226
+ bv = k_data.get('b', [0]*6)
227
+ sv = k_data.get('s', [0]*6)
228
+ vv = k_data.get('v', [0]*6)
229
+ ov = k_data.get('o', [0]*6)
230
+
231
+ b_val = float(bv[i]) if i < len(bv) and bv[i] else 0.0
232
+ s_val = float(sv[i]) if i < len(sv) and sv[i] else 0.0
233
+ v_val = float(vv[i]) if i < len(vv) and vv[i] else 0.0
234
+ o_val = float(ov[i]) if i < len(ov) and ov[i] else 0.0
235
+
236
+ tot_v += v_val
237
+ tot_oi += o_val
238
+
239
+ if meta['type'] == 'CE':
240
+ cb += b_val
241
+ cs += s_val
242
+ else:
243
+ pb += b_val
244
+ ps += s_val
245
 
246
  records.append({
247
+ "ts": base_ts + timedelta(seconds=i*10),
248
+ "price": p_base,
249
+ "buy_qty": cb + pb,
250
+ "sell_qty": cs + ps,
251
+ "volume": tot_v,
252
+ "oi": tot_oi,
253
  "cb": cb, "cs": cs, "pb": pb, "ps": ps
254
  })
255
+
256
  else:
257
  inst_data = data_dict.get(instrument)
258
  if not inst_data: continue
259
+ p_arr = inst_data.get('p', []) or[]
260
+ b_arr = inst_data.get('b',[]) or[]
261
+ s_arr = inst_data.get('s', []) or[]
262
+ v_arr = inst_data.get('v', []) or[]
263
+ o_arr = inst_data.get('o', []) or[]
264
+
265
  for i in range(6):
 
266
  if i < len(p_arr) and p_arr[i] is not None:
267
  records.append({
268
+ "ts": base_ts + timedelta(seconds=i*10),
269
+ "price": float(p_arr[i]),
270
+ "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
271
+ "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
272
+ "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
273
+ "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0,
274
  "cb": 0.0, "cs": 0.0, "pb": 0.0, "ps": 0.0
275
  })
276
+ except Exception:
277
+ continue
278
+
279
+ if not records:
280
+ return JSONResponse({"error": "No data found", "labels":[], "P":[]})
281
 
282
+ df = pd.DataFrame(records)
283
+ df.set_index('ts', inplace=True)
284
+
285
+ tf_map = {
286
+ "1min": "1min", "3min": "3min", "5min": "5min",
287
+ "15min": "15min", "30min": "30min", "1hour": "1h"
288
+ }
289
+ panda_tf = tf_map.get(timeframe, "1min")
290
+
291
+ agg_dict = {
292
+ "price": "last", "volume": "last", "oi": "last",
293
+ "buy_qty": "mean", "sell_qty": "mean",
294
  "cb": "mean", "cs": "mean", "pb": "mean", "ps": "mean"
295
+ }
296
 
297
+ resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
298
+
299
+ response_data = {
300
  "labels": resampled.index.strftime('%H:%M').tolist(),
301
+ "P": resampled['price'].tolist(),
302
+ "B": resampled['buy_qty'].tolist(),
303
+ "S": resampled['sell_qty'].tolist(),
304
+ "V": resampled['volume'].tolist(),
305
+ "OI": resampled['oi'].tolist(),
306
+ "CB": resampled['cb'].tolist(),
307
+ "CS": resampled['cs'].tolist(),
308
+ "PB": resampled['pb'].tolist(),
309
+ "PS": resampled['ps'].tolist()
310
+ }
311
+
312
+ return JSONResponse(response_data)
313
+
314
 
315
  # ==========================================
316
  # 5. FRONTEND TEMPLATE
 
326
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
327
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
328
  <style>
329
+ /* --- THEME --- */
330
+ :root {
331
+ --bg-body: #0d1117;
332
+ --bg-header: #161b22;
333
+ --bg-card: #0d1117;
334
+ --bg-panel: #21262d;
335
+ --bg-input: #161b22;
336
+ --border: #30363d;
337
+ --border-hover: #8b949e;
338
+ --text-primary: #e6edf3;
339
+ --text-secondary: #8b949e;
340
+ --accent: #2f81f7;
341
+ --success: #238636;
342
+ --danger: #da3633;
343
+ --warning: #d29922;
344
+ --font-main: 'Inter', sans-serif;
345
+ --font-mono: 'JetBrains Mono', monospace;
346
+ }
347
  * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
348
+ body {
349
+ background: var(--bg-body); color: var(--text-primary); font-family: var(--font-main);
350
+ display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px;
351
+ }
352
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
353
  ::-webkit-scrollbar-track { background: var(--bg-body); }
354
+ ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
355
+
356
+ /* HEADER */
357
+ .top-bar {
358
+ height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
359
+ display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
360
+ }
361
+ .brand { font-weight: 700; font-size: 15px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
362
  .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
363
  .controls { display: flex; align-items: center; gap: 12px; }
364
+
365
+ select, button, input {
366
+ background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
367
+ padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: 0.2s;
368
+ }
369
+ select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(47,129,247,0.2); }
370
  button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
371
  button.primary { background: var(--accent); border-color: var(--accent); color: white; }
372
+ button.primary:hover { background: #58a6ff; }
373
  button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
374
+ button.danger:hover { background: rgba(218, 54, 51, 0.1); }
375
+
376
  .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
377
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
378
+ .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
379
+
380
+ /* GRID */
381
+ .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
382
+ .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
383
+ .column:last-child { border-right: none; }
384
+
385
+ .add-chart-btn {
386
+ height: 40px; border: none; border-top: 1px dashed var(--border);
387
+ background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600;
388
+ }
389
+ .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
390
+
391
+ /* CARD */
392
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
393
+ .card-header {
394
+ height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
395
+ padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
396
+ }
397
+ .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
398
+ .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
399
+ .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
400
+
401
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
402
+ .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
403
+ .status-dot.hist { background: var(--accent); }
404
+ .status-dot.retry { background: var(--warning); }
405
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
406
+
407
+ .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
408
+ .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
409
+
410
+ .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
411
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); background: radial-gradient(circle, var(--bg-panel) 0%, var(--bg-card) 100%); }
412
+
413
+ /* MODAL */
414
+ .modal-backdrop {
415
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7);
416
+ z-index: 100; display: none; justify-content: center; align-items: center;
417
+ }
418
+ .modal {
419
+ background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px;
420
+ width: 540px; max-width: 95%; max-height: 85vh; display: flex; flex-direction: column;
421
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5);
422
+ }
423
+ .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
424
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
425
+ .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
426
+
427
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
428
+
429
+ /* CUSTOM DROPDOWN UI */
430
  .input-wrapper { position: relative; width: 100%; }
431
  .input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
432
+ .custom-dd {
433
+ position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-panel); border: 1px solid var(--border);
434
+ border-radius: 6px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none;
435
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
436
+ }
437
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
438
+ .custom-dd-opt:last-child { border-bottom: none; }
439
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
440
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
441
+
442
+ /* SERIES ROW STRUCTURE (POLISHED UI) */
443
+ .series-row {
444
+ background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px;
445
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
446
+ }
447
+ .series-row-top { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
448
+ .series-row-bottom { display: flex; gap: 10px; align-items: center; }
449
+
450
+ .s-form, .s-lbl, .s-axis, .s-cond, .s-thresh {
451
+ background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
452
+ border-radius: 6px; padding: 0 10px; height: 32px; font-size: 12px; transition: 0.2s;
453
+ }
454
+ .s-form:focus, .s-lbl:focus, .s-axis:focus, .s-cond:focus, .s-thresh:focus {
455
+ border-color: var(--accent); box-shadow: 0 0 0 1px rgba(47, 129, 247, 0.2);
456
+ }
457
+
458
+ .color-input {
459
+ padding: 0; width: 36px; border: 1px solid var(--border); background: var(--bg-input);
460
+ height: 32px; cursor: pointer; border-radius: 6px; overflow: hidden;
461
+ }
462
+ .color-input::-webkit-color-swatch-wrapper { padding: 0; }
463
+ .color-input::-webkit-color-swatch { border: none; }
464
+
465
+ .s-cond-group {
466
+ display: flex; gap: 8px; align-items: center; background: rgba(0,0,0,0.2);
467
+ padding: 4px 10px; border-radius: 6px; border: 1px dashed var(--border); flex: 1;
468
+ }
469
+
470
+ /* UI ENHANCEMENTS */
471
+ .segmented-control {
472
+ display: flex; background: var(--bg-body); border: 1px solid var(--border);
473
+ border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
474
+ }
475
+ .seg-btn {
476
+ flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600;
477
+ color: var(--text-secondary); cursor: pointer; border: none; background: transparent;
478
+ transition: 0.2s; border-radius: 4px;
479
+ }
480
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
481
+
482
+ .info-icon {
483
+ display: inline-flex; align-items: center; justify-content: center;
484
+ width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel);
485
+ color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help;
486
+ border: 1px solid var(--border); margin-left: 8px; transition: 0.2s;
487
+ }
488
+ .info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
489
  </style>
490
  </head>
491
  <body>
 
493
  <div class="top-bar">
494
  <div class="brand">DepthChain <span>PRO</span></div>
495
  <div class="controls">
496
+ <select id="globalDate" onchange="App.onDateChange()" style="min-width: 140px;"></select>
497
  <div style="position: relative;">
498
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left: 28px; font-weight: 600;">
499
+ <option value="LIVE">LIVE</option>
500
+ <option value="HIST">HISTORY</option>
501
  </select>
502
+ <div id="modeDot" class="status-dot live" style="position: absolute; left: 10px; top: 12px; pointer-events: none;"></div>
503
  </div>
504
+ <div style="width:1px; height: 20px; background: var(--border);"></div>
505
  <div class="layout-toggle">
506
  <button class="layout-btn" onclick="App.setLayout(1)">1</button>
507
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
508
  <button class="layout-btn active" onclick="App.setLayout(3)">3</button>
509
  <button class="layout-btn" onclick="App.setLayout(4)">4</button>
510
  </div>
511
+ <button onclick="Config.openDefaults()">Settings</button>
512
  <button class="danger" onclick="App.reset()">Reset</button>
513
  </div>
514
  </div>
515
 
516
  <div class="main-container" id="gridRoot"></div>
517
 
518
+ <!-- CONFIG MODAL -->
519
  <div class="modal-backdrop" id="configModal">
520
  <div class="modal">
521
+ <div class="modal-head">
522
+ <span id="cfgTitle">Configure Chart</span>
523
+ <button class="tool-btn" onclick="Config.close()">✕</button>
524
  </div>
525
  <div class="modal-body">
526
+ <div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading...</div>
527
+ <div id="cfgContent">
528
+
529
+ <!-- Global Settings Mode -->
530
+ <div id="cfgGlobalOnly" style="display:none;">
531
+ <div class="form-section">
532
+ <label>Refresh Rate (Seconds)</label>
533
+ <input type="number" id="cfgRefresh" value="15" min="5">
534
  </div>
535
+ <div style="height:1px; background:var(--border); margin: 16px 0;"></div>
536
  </div>
537
+
538
+ <!-- Single Chart Config Mode -->
539
+ <div id="cfgChartOnly">
540
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
541
+ <div class="form-section">
542
+ <label>Symbol</label>
543
+ <div class="input-wrapper">
544
+ <input type="text" id="cfgRoot" onfocus="CustomDD.open('cfgRoot')" oninput="CustomDD.filter('cfgRoot')" style="width:100%; text-transform: uppercase;" placeholder="Type or select..." autocomplete="off">
545
+ <div class="input-arrow">▼</div>
546
+ <div id="dd-cfgRoot" class="custom-dd"></div>
547
+ </div>
548
+ </div>
549
+ <div class="form-section">
550
+ <label>Expiry</label>
551
+ <div class="input-wrapper">
552
+ <input type="text" id="cfgExp" onfocus="CustomDD.open('cfgExp')" oninput="CustomDD.filter('cfgExp')" style="width:100%; text-transform: uppercase;" placeholder="Type or select..." autocomplete="off">
553
+ <div class="input-arrow">▼</div>
554
+ <div id="dd-cfgExp" class="custom-dd"></div>
555
+ </div>
556
+ </div>
557
  </div>
558
+
559
+ <div class="form-section" style="margin-top: 8px;">
560
+ <label>Mode</label>
561
+ <div class="segmented-control" id="cfgModeToggle">
562
+ <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
563
+ <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
564
+ </div>
 
 
 
 
 
 
565
  </div>
566
+
567
+ <!-- Normal Mode Fields -->
568
+ <div id="cfgNormalFields">
569
+ <div class="form-section">
570
+ <label>Instrument</label>
571
+ <div class="input-wrapper">
572
+ <input type="text" id="cfgInst" onfocus="CustomDD.open('cfgInst')" oninput="CustomDD.filter('cfgInst')" style="width:100%; text-transform: uppercase;" placeholder="Type or select..." autocomplete="off">
573
+ <div class="input-arrow">▼</div>
574
+ <div id="dd-cfgInst" class="custom-dd"></div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <!-- Advanced Mode Fields -->
580
+ <div id="cfgAdvancedFields" style="display:none;">
581
+ <div class="form-section">
582
+ <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
583
+ <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
584
+ <div style="font-size:10px; color:var(--text-secondary); margin-top:4px; line-height: 1.4;">Calculates aggregate pressure dynamically across ATM plus/minus this many strikes.</div>
585
+ </div>
586
+ </div>
587
+
588
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:12px;">
589
+ <div class="form-section">
590
+ <label>Timeframe</label>
591
+ <select id="cfgTF" style="width:100%">
592
+ <option value="1min">1 Minute</option>
593
+ <option value="3min">3 Minutes</option>
594
+ <option value="5min">5 Minutes</option>
595
+ <option value="15min">15 Minutes</option>
596
+ <option value="30min">30 Minutes</option>
597
+ <option value="1hour">1 Hour</option>
598
+ </select>
599
+ </div>
600
+ <div class="form-section">
601
+ <label>Range (HH:MM)</label>
602
+ <div style="display:flex; gap:8px;">
603
+ <input type="time" id="cfgStart" value="09:15">
604
+ <input type="time" id="cfgEnd" value="15:30">
605
+ </div>
606
+ </div>
607
+ </div>
608
+ <div style="height:1px; background:var(--border); margin: 12px 0 8px 0;"></div>
609
  </div>
610
+
611
+ <!-- PRESET MANAGER -->
612
+ <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom: 12px;">
613
+ <label>Quick Configurations</label>
614
+ <div style="display:flex; gap:8px;">
615
+ <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
616
+ <button class="primary" onclick="Config.savePreset()" title="Save Current Lines as Configuration">Save</button>
617
+ <button class="danger" onclick="Config.deletePreset()" title="Delete Selected Model" style="width: 32px; padding:0;"></button>
618
+ </div>
619
  </div>
620
+
621
+ <!-- Common Series Config -->
622
+ <div class="form-section">
623
+ <div style="display:flex; align-items:center; margin-bottom: 6px;">
624
+ <label style="margin:0;">Plot Series</label>
625
+ <span class="info-icon" id="infoIcon">i</span>
626
+ </div>
627
+ <div id="cfgSeries"></div>
628
+ <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
629
  </div>
630
  </div>
 
 
 
 
 
631
  </div>
632
+ <div class="modal-foot">
633
+ <button style="background:transparent; border:1px solid var(--border); color:var(--text-primary);" onclick="Config.close()">Cancel</button>
634
+ <button class="primary" onclick="Config.saveChartConfig()">Save Configuration</button>
635
  </div>
636
  </div>
637
  </div>
638
 
639
  <script>
640
+ // --- CUSTOM SEARCHABLE DROPDOWN LOGIC ---
641
+ document.addEventListener('click', (e) => {
642
+ if (!e.target.closest('.input-wrapper')) {
643
+ document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
644
+ }
645
+ });
646
+
647
  const CustomDD = {
648
  data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
649
+ recent: { cfgRoot:[] }, // Persistent memory for symbols
650
+
651
+ init() {
652
+ const saved = localStorage.getItem('dc_recent_symbols');
653
+ if(saved) this.recent.cfgRoot = JSON.parse(saved);
654
+ },
655
+
656
+ clear(id) {
657
+ this.data[id] =[];
658
+ document.getElementById(id).value = '';
659
+ },
660
+
661
+ set(id, items, groupName = null) {
662
+ if (!groupName) {
663
+ this.data[id] = items.map(i => ({val: i, group: null}));
664
+ } else {
665
+ this.data[id].push(...items.map(i => ({val: i, group: groupName})));
666
+ }
667
+ },
668
+
669
+ async open(id) {
670
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
671
+ const inp = document.getElementById(id);
672
+ inp.select();
673
+
674
+ // SHOW ALL options immediately when focused, ignoring current input text visually
675
  this.render(id, '');
676
  document.getElementById(`dd-${id}`).style.display = 'block';
677
  },
678
+
679
+ async filter(id) {
680
+ const query = document.getElementById(id).value.toUpperCase();
681
+
682
+ // Targeted backend search for symbols to minimize egress, but append to cache
683
+ if(id === 'cfgRoot' && query.length >= 2) {
684
+ const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
685
+ const results = await res.json();
686
+
687
+ // Add newly found items to our local cache so they don't disappear
688
+ const existing = this.data[id].map(x => x.val);
689
+ results.forEach(r => {
690
+ if(!existing.includes(r)) this.data[id].push({val: r, group: null});
691
  });
 
 
692
  }
693
+
694
+ this.render(id, query);
695
+ document.getElementById(`dd-${id}`).style.display = 'block';
696
  },
697
+
698
  render(id, query) {
699
  const el = document.getElementById(`dd-${id}`);
700
  const q = query.toLowerCase();
701
+ let html = '';
702
+ let currentGroup = null;
703
+
704
+ // Add Recent Symbols to the top of Root list if no specific query
705
+ let pool =[...this.data[id]];
706
  if(id === 'cfgRoot' && q === '') {
707
+ const recentItems = this.recent.cfgRoot.map(r => ({val: r, group: 'Recent Search'}));
708
+ pool = [...recentItems, ...pool];
709
  }
710
+
711
+ const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i) // Unique
712
+ .filter(item => item.val.toLowerCase().includes(q));
713
 
714
+ if (filtered.length === 0) {
715
+ el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary); cursor:default;">No matches found</div>';
716
+ return;
717
+ }
718
+
719
  filtered.forEach(item => {
720
+ if (item.group && item.group !== currentGroup) {
721
+ html += `<div class="custom-dd-group">${item.group}</div>`;
722
+ currentGroup = item.group;
723
+ }
724
  html += `<div class="custom-dd-opt" onclick="CustomDD.pick('${id}', '${item.val}')">${item.val}</div>`;
725
  });
726
  el.innerHTML = html;
 
727
  },
728
+
729
  pick(id, val) {
730
  document.getElementById(id).value = val;
731
  document.getElementById(`dd-${id}`).style.display = 'none';
732
+
733
  if(id === 'cfgRoot') {
734
+ // Save to recent
735
+ if(!this.recent.cfgRoot.includes(val)) {
736
+ this.recent.cfgRoot.unshift(val);
737
+ if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
738
+ localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot));
739
+ }
740
  Config.onRootChange();
741
  }
742
  if(id === 'cfgExp') Config.onExpChange();
743
  }
744
  };
745
 
 
 
746
  const App = {
747
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
748
  presets: {
749
+ "Model 1":[
750
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
751
  { formula: '$CB + $PS', color1: '#238636', cond: 'none', label: 'Buy', axis: 'right' },
752
  { formula: '$CS + $PB', color1: '#da3633', cond: 'none', label: 'Sell', axis: 'right' }
753
  ],
754
+ "Model 2":[
755
  { formula: '$P', color1: '#2f81f7', cond: 'none', label: 'Price', axis: 'left' },
756
  { formula: '$CB + $PS - ($PB + $CS)', color1: '#238636', color2: '#da3633', cond: '>', thresh: 0, label: 'Overall', axis: 'right' }
757
  ]
758
  },
759
+
760
  async init() {
761
+ CustomDD.init();
762
+ await this.loadDates();
763
+ const savedPresets = localStorage.getItem('dc_pro_v3_presets');
764
+ if(savedPresets) {
765
+ try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e){}
766
+ }
767
+
768
  const saved = localStorage.getItem('dc_pro_v3_final');
769
+ if (saved) {
770
+ try {
771
+ const s = JSON.parse(saved);
772
+ this.state = { ...this.state, ...s };
773
+ if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
774
+ } catch(e) { this.initDefault(); }
775
+ } else {
776
+ this.initDefault();
777
+ }
778
+
779
+ const dSel = document.getElementById('globalDate');
780
+ if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
781
+ else if(dSel.options.length) this.state.date = dSel.options[0].value;
782
+
783
  document.getElementById('globalMode').value = this.state.mode;
784
+ this.updateModeUI();
785
+ this.renderGrid();
786
+ this.startLoop();
787
+ },
788
+
789
+ initDefault() {
790
+ this.state.columns = 3;
791
+ this.state.layout = [[], [],[]];
792
+ this.state.refreshRate = 15;
793
+ for(let i=0; i<3; i++) {
794
+ this.addChartInternal(i);
795
+ this.addChartInternal(i);
796
+ }
797
+ },
798
+
799
+ async loadDates() {
800
+ try {
801
+ const res = await fetch('/api/dates');
802
+ const dates = await res.json();
803
+ document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
804
+ if(!this.state.date && dates.length) this.state.date = dates[0];
805
+ } catch(e) {}
806
+ },
807
+
808
+ addChartInternal(colIdx) {
809
+ const id = 'c-' + Math.random().toString(36).substr(2, 6);
810
+ this.state.layout[colIdx].push(id);
811
+ this.state.charts[id] = { config: {}, instance: null };
812
+ return id;
813
+ },
814
+
815
+ save() {
816
+ const cleanCharts = {};
817
+ for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
818
+
819
+ localStorage.setItem('dc_pro_v3_final', JSON.stringify({
820
+ date: this.state.date,
821
+ mode: this.state.mode,
822
+ columns: this.state.columns,
823
+ layout: this.state.layout,
824
+ refreshRate: this.state.refreshRate,
825
+ charts: cleanCharts
826
+ }));
827
+ },
828
+
829
+ reset() {
830
+ if(confirm("Reset Workspace?")) {
831
+ localStorage.removeItem('dc_pro_v3_final');
832
+ location.reload();
833
+ }
834
+ },
835
+
836
+ setLayout(n) {
837
+ const all = this.state.layout.flat();
838
+ this.state.columns = n;
839
+ this.state.layout = Array.from({length: n}, () =>[]);
840
+ all.forEach((id, i) => this.state.layout[i % n].push(id));
841
+ this.renderGrid();
842
+ this.save();
843
  },
844
+
 
 
 
 
845
  renderGrid() {
846
+ document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
847
+
848
+ const container = document.getElementById('gridRoot');
849
+ container.innerHTML = '';
850
+
851
  this.state.layout.forEach((ids, colIdx) => {
852
+ const col = document.createElement('div');
853
+ col.className = 'column';
854
+ const count = ids.length;
855
+ const viewH = window.innerHeight - 52;
856
+
857
  ids.forEach(id => {
858
+ if(this.state.charts[id] && this.state.charts[id].instance) {
859
+ this.state.charts[id].instance.destroy();
860
+ this.state.charts[id].instance = null;
861
+ }
862
+
863
+ const card = this.createCardDOM(id);
864
+ if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
865
+ else card.style.height = '300px';
866
+
867
+ col.appendChild(card);
868
+ setTimeout(() => ChartLogic.plot(id), 0);
869
  });
870
+
871
+ const btn = document.createElement('button');
872
+ btn.className = 'add-chart-btn';
873
+ btn.innerText = '+ Add Chart';
874
+ btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
875
+ col.appendChild(btn);
876
+ container.appendChild(col);
877
  });
878
  },
879
+
880
+ createCardDOM(id) {
881
+ const div = document.createElement('div');
882
+ div.className = 'chart-card';
883
+ div.id = `card-${id}`;
884
+
885
+ const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
886
+ const isCfg = cfg && cfg.root;
887
+ const advancedLabel = cfg.chartMode === 'advanced' ? `<span class="inst-label">ATM +/- ${cfg.atmRange}</span>` : `<span class="inst-label">${cfg.instrument || ''}</span>`;
888
+
889
+ div.innerHTML = `
890
+ <div class="card-header">
891
+ <div class="card-info">
892
+ <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
893
+ <span>${isCfg ? cfg.root : 'New Chart'}</span>
894
+ ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
895
+ </div>
896
+ <div class="card-tools">
897
+ <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
898
+ <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
899
+ </div>
900
+ </div>
901
+ <div class="card-body" id="body-${id}">
902
+ ${!isCfg ? `
903
+ <div class="empty-state">
904
+ <div style="font-size:24px; opacity:0.3;">📊</div>
905
+ <button style="background:transparent; border:1px solid var(--border); padding:5px 10px; border-radius:4px; color:var(--text-secondary); margin-top:8px;" onclick="Config.open('${id}')">Configure</button>
906
+ </div>
907
+ ` : `<canvas id="canvas-${id}"></canvas>`}
908
+ </div>
909
+ `;
910
+ return div;
911
+ },
912
+
913
+ removeChart(id) {
914
+ if(!confirm("Delete?")) return;
915
+ this.state.layout = this.state.layout.map(c => c.filter(x => x !== id));
916
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
917
+ delete this.state.charts[id];
918
+ this.renderGrid();
919
+ this.save();
920
+ },
921
+
922
+ onDateChange() {
923
+ this.state.date = document.getElementById('globalDate').value;
924
+ const today = new Date().toISOString().split('T')[0];
925
+ if(this.state.date === today && this.state.mode !== 'LIVE') {
926
+ this.state.mode = 'LIVE';
927
+ document.getElementById('globalMode').value = 'LIVE';
928
+ this.updateModeUI();
929
+ }
930
+ this.save();
931
+ this.refreshAll();
932
+ },
933
+
934
+ onModeChange() {
935
+ this.state.mode = document.getElementById('globalMode').value;
936
+ this.updateModeUI();
937
+ this.save();
938
+ this.refreshAll();
939
+ },
940
+
941
+ updateModeUI() {
942
+ document.getElementById('modeDot').className = `status-dot ${this.state.mode==='LIVE'?'live':'hist'}`;
943
+ },
944
+
945
+ refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
946
+
947
+ startLoop() {
948
+ const loop = () => {
949
+ if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
950
+ const rate = (this.state.refreshRate || 15) * 1000;
951
+ setTimeout(loop, rate);
952
+ };
953
+ loop();
954
+ }
955
  };
956
 
957
  const ChartLogic = {
958
  async plot(id, isUpdate=false) {
959
+ const chart = App.state.charts[id];
960
+ if(!chart || !chart.config.root) return;
961
+
962
+ const cfg = chart.config;
963
+ const dot = document.getElementById(`dot-${id}`);
964
+ if(isUpdate && dot) dot.classList.add('retry');
965
+
966
+ let endTime = cfg.end;
967
+ if (App.state.mode === 'LIVE') {
968
+ const now = new Date();
969
+ endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
970
+ }
971
+
972
+ try {
973
+ const res = await fetch('/api/fetch_series', {
974
+ method: 'POST',
975
+ headers: {'Content-Type': 'application/json'},
976
+ body: JSON.stringify({
977
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry,
978
+ instrument: cfg.instrument || "", timeframe: cfg.timeframe,
979
+ start_time: cfg.start, end_time: endTime,
980
+ mode: cfg.chartMode || 'normal',
981
+ atm_range: cfg.atmRange || 5
982
+ })
983
  });
984
+ const data = await res.json();
985
+
986
+ if(data.error || !data.labels || !data.labels.length) {
987
+ if(App.state.mode !== 'LIVE') {
988
+ const b = document.getElementById(`body-${id}`);
989
+ if(b) b.innerHTML = '<div class="empty-state" style="opacity:0.5">No Data</div>';
990
+ }
991
+ if(dot) dot.classList.remove('retry');
992
+ return;
993
+ }
994
+
995
+ const body = document.getElementById(`body-${id}`);
996
+ if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
997
+
998
+ if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
999
+
1000
+ const datasets = cfg.series.map(s => {
1001
+ const vals = data.labels.map((_, i) => {
1002
+ const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
1003
+ const CB=data.CB[i]||0, CS=data.CS[i]||0, PB=data.PB[i]||0, PS=data.PS[i]||0;
1004
+
1005
+ let f = s.formula
1006
+ .replace(/\$CB/g, CB).replace(/\$CS/g, CS)
1007
+ .replace(/\$PB/g, PB).replace(/\$PS/g, PS)
1008
+ .replace(/\$OI/g, OI).replace(/\$P/g, P)
1009
+ .replace(/\$V/g, V).replace(/\$B/g, B).replace(/\$S/g, S);
1010
+
1011
+ try { return new Function('return '+f)(); } catch{ return null; }
1012
+ });
1013
+
1014
+ let dataset = {
1015
+ label: s.label, data: vals,
1016
+ borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
1017
+ };
1018
+
1019
+ if(s.cond && s.cond !== 'none') {
1020
+ let thresh = parseFloat(s.thresh) || 0;
1021
+ let cTrue = s.color1;
1022
+ let cFalse = s.color2 || s.color1;
1023
+
1024
+ dataset.segment = {
1025
+ borderColor: ctx => {
1026
+ let v = ctx.p1.parsed.y;
1027
+ if(s.cond === '>') return v > thresh ? cTrue : cFalse;
1028
+ if(s.cond === '<') return v < thresh ? cTrue : cFalse;
1029
+ return cTrue;
1030
+ }
1031
+ };
1032
+ dataset.borderColor = cTrue;
1033
+ dataset.backgroundColor = cTrue;
1034
+
1035
+ const hoverColorCallback = (ctx) => {
1036
+ let v = ctx.raw;
1037
+ if (v === undefined || v === null) return cTrue;
1038
+ if(s.cond === '>') return v > thresh ? cTrue : cFalse;
1039
+ if(s.cond === '<') return v < thresh ? cTrue : cFalse;
1040
+ return cTrue;
1041
+ };
1042
+ dataset.pointHoverBackgroundColor = hoverColorCallback;
1043
+ dataset.pointHoverBorderColor = hoverColorCallback;
1044
+
1045
+ } else {
1046
+ dataset.borderColor = s.color1;
1047
+ dataset.backgroundColor = s.color1;
1048
+ dataset.pointHoverBackgroundColor = s.color1;
1049
+ dataset.pointHoverBorderColor = s.color1;
1050
+ }
1051
+
1052
+ return dataset;
1053
+ });
1054
+
1055
+ this.render(id, data.labels, datasets, cfg.series);
1056
+
1057
+ } catch(e) { console.error(e); }
1058
  },
1059
+
1060
+ render(id, labels, datasets, seriesConfig) {
1061
+ const ctx = document.getElementById(`canvas-${id}`);
1062
+ if(!ctx) return;
1063
+
1064
+ if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
1065
+ App.state.charts[id].instance.destroy();
1066
+ App.state.charts[id].instance = null;
1067
+ }
1068
+
1069
+ if(App.state.charts[id].instance) {
1070
+ App.state.charts[id].instance.data.labels = labels;
1071
+ App.state.charts[id].instance.data.datasets = datasets;
1072
+ App.state.charts[id].instance.update('none');
1073
+ } else {
1074
+ const hasRight = datasets.some(d => d.yAxisID === 'y1');
1075
+ App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
1076
+ type: 'line',
1077
+ data: { labels, datasets },
1078
+ options: {
1079
+ responsive: true, maintainAspectRatio: false, animation: false,
1080
+ interaction: { mode: 'index', intersect: false },
1081
+ plugins: {
1082
+ legend: { display: false },
1083
+ tooltip: {
1084
+ backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor: '#30363d', borderWidth: 1,
1085
+ callbacks: {
1086
+ labelColor: function(context) {
1087
+ let s = seriesConfig[context.datasetIndex];
1088
+ let v = context.parsed.y;
1089
+ let c = s.color1;
1090
+ if(s.cond && s.cond !== 'none') {
1091
+ let thresh = parseFloat(s.thresh) || 0;
1092
+ let cTrue = s.color1;
1093
+ let cFalse = s.color2 || s.color1;
1094
+ if(s.cond === '>') c = v > thresh ? cTrue : cFalse;
1095
+ if(s.cond === '<') c = v < thresh ? cTrue : cFalse;
1096
+ }
1097
+ return { borderColor: c, backgroundColor: c };
1098
+ }
1099
+ }
1100
+ }
1101
+ },
1102
+ scales: {
1103
+ x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
1104
+ y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
1105
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
1106
+ }
1107
+ }
1108
+ });
1109
+ }
1110
  }
1111
  };
1112
 
1113
  const Config = {
1114
+ tid: null, isGlobal: false, currentMode: 'normal',
1115
+
1116
+ setMode(mode) {
1117
+ this.currentMode = mode;
1118
+ const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1119
+ btns[0].classList.toggle('active', mode === 'normal');
1120
+ btns[1].classList.toggle('active', mode === 'advanced');
1121
+
1122
+ document.getElementById('cfgNormalFields').style.display = mode === 'normal' ? 'block' : 'none';
1123
+ document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
1124
+
1125
+ this.updateInfoTooltip(mode);
1126
+ this.updatePresetsDropdown();
1127
+ this.loadPreset("Model 1"); // Model 1 is now the default
1128
+ },
1129
+
1130
+ updateInfoTooltip(mode) {
1131
+ const icon = document.getElementById('infoIcon');
1132
+ if (!icon) return;
1133
+ if (mode === 'advanced') {
1134
+ icon.title = "Standard Variables:\n$P = Price\n$B = Total Buy\n$S = Total Sell\n$V = Volume\n$OI = Open Interest\n\nAdvanced Mode Only:\n$CB = Call Buy\n$CS = Call Sell\n$PB = Put Buy\n$PS = Put Sell";
1135
+ icon.style.color = "var(--accent)";
1136
+ icon.style.borderColor = "var(--accent)";
1137
+ } else {
1138
+ icon.title = "Standard Variables:\n$P = Price\n$B = Total Buy\n$S = Total Sell\n$V = Volume\n$OI = Open Interest";
1139
+ icon.style.color = "var(--text-secondary)";
1140
+ icon.style.borderColor = "var(--border)";
1141
+ }
1142
+ },
1143
+
1144
+ updatePresetsDropdown() {
1145
+ const sel = document.getElementById('cfgPresetsDropdown');
1146
+ sel.innerHTML = Object.keys(App.presets).map(k => `<option value="${k}">${k}</option>`).join('');
1147
+ },
1148
+
1149
+ loadPreset(presetName = null) {
1150
+ const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1151
+ if(name && App.presets[name]) {
1152
+ document.getElementById('cfgPresetsDropdown').value = name;
1153
+ this.renderSeries(App.presets[name]);
1154
+ }
1155
+ },
1156
+
1157
+ savePreset() {
1158
+ const name = prompt("Enter a name for this Quick Configuration:", "Custom Model");
1159
+ if(!name) return;
1160
+ const series = this.extractSeriesFromDOM();
1161
+ App.presets[name] = series;
1162
+ localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
1163
+ this.updatePresetsDropdown();
1164
+ document.getElementById('cfgPresetsDropdown').value = name;
1165
+ },
1166
+
1167
+ deletePreset() {
1168
+ const name = document.getElementById('cfgPresetsDropdown').value;
1169
+ if(name.startsWith('Model 1') || name.startsWith('Model 2')) return alert("Cannot delete default models.");
1170
+ if(confirm(`Delete model '${name}'?`)) {
1171
+ delete App.presets[name];
1172
+ localStorage.setItem('dc_pro_v3_presets', JSON.stringify(App.presets));
1173
+ this.updatePresetsDropdown();
1174
+ this.loadPreset();
1175
+ }
1176
+ },
1177
+
1178
  async open(id) {
1179
+ this.tid = id; this.isGlobal = false;
1180
+ document.getElementById('configModal').style.display = 'flex';
1181
+ document.getElementById('cfgTitle').innerText = 'Configure Chart';
1182
+ document.getElementById('cfgLoading').style.display = 'block';
1183
+ document.getElementById('cfgContent').style.display = 'none';
1184
+
1185
+ document.getElementById('cfgGlobalOnly').style.display = 'none';
1186
+ document.getElementById('cfgChartOnly').style.display = 'block';
1187
+
1188
+ this.updatePresetsDropdown();
1189
+
1190
+ // Load existing roots once
1191
+ CustomDD.clear('cfgRoot');
1192
+ const res = await fetch(`/api/roots?date=${App.state.date}`);
1193
+ const roots = await res.json();
1194
+ CustomDD.set('cfgRoot', roots);
1195
+
1196
  const cfg = App.state.charts[id].config;
1197
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1198
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
1199
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
1200
+
1201
+ this.currentMode = cfg.chartMode || 'normal';
1202
+ document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1203
+
1204
+ // Setup initial UI states without forcing preset reset if series exist
1205
+ const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1206
+ btns[0].classList.toggle('active', this.currentMode === 'normal');
1207
+ btns[1].classList.toggle('active', this.currentMode === 'advanced');
1208
+ document.getElementById('cfgNormalFields').style.display = this.currentMode === 'normal' ? 'block' : 'none';
1209
+ document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1210
+
1211
+ this.updateInfoTooltip(this.currentMode);
1212
+
1213
+ // Override defaults if there is already a saved config
1214
+ if (cfg.series && cfg.series.length > 0) {
1215
+ this.renderSeries(cfg.series);
1216
+ } else {
1217
+ this.loadPreset("Model 1");
1218
+ }
1219
+
1220
+ document.getElementById('cfgRoot').value = cfg.root || '';
1221
+ document.getElementById('cfgExp').value = cfg.expiry || '';
1222
+ document.getElementById('cfgInst').value = cfg.instrument || '';
1223
+
1224
+ if(cfg.root) { await this.onRootChange(cfg.expiry, cfg.instrument); }
1225
+ document.getElementById('cfgLoading').style.display = 'none';
1226
+ document.getElementById('cfgContent').style.display = 'block';
1227
  },
1228
+
1229
+ openDefaults() {
1230
+ this.isGlobal = true;
1231
+ document.getElementById('configModal').style.display = 'flex';
1232
+ document.getElementById('cfgTitle').innerText = 'Global Settings';
1233
+ document.getElementById('cfgLoading').style.display = 'none';
1234
+ document.getElementById('cfgContent').style.display = 'block';
1235
+
1236
+ document.getElementById('cfgGlobalOnly').style.display = 'block';
1237
+ document.getElementById('cfgChartOnly').style.display = 'none';
1238
+ document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1239
+
1240
+ this.updatePresetsDropdown();
1241
+ this.loadPreset("Model 1");
1242
+ },
1243
+
1244
+ close() { document.getElementById('configModal').style.display = 'none'; },
1245
+
1246
  async onRootChange(preExp, preInst) {
1247
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1248
+ CustomDD.clear('cfgExp');
1249
+ CustomDD.clear('cfgInst');
1250
+
1251
+ if (!preExp) document.getElementById('cfgExp').value = '';
1252
+ if (!preInst) document.getElementById('cfgInst').value = '';
1253
+
1254
+ if (!root) return;
1255
+
1256
+ // Fetches ALL available expiries for the root
1257
+ const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1258
+ const exps = await res.json();
1259
+ CustomDD.set('cfgExp', exps);
1260
+
1261
+ if(preExp && exps.includes(preExp)) {
1262
+ document.getElementById('cfgExp').value = preExp;
1263
+ await this.onExpChange(preInst);
1264
+ }
1265
+ else if (exps.length > 0) {
1266
+ const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
1267
+ if(auto.current) {
1268
+ document.getElementById('cfgExp').value = auto.current.expiry;
1269
+ await this.onExpChange(auto.current.instrument);
1270
+ }
1271
+ }
1272
  },
1273
+
1274
  async onExpChange(preInst) {
1275
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1276
+ const exp = document.getElementById('cfgExp').value.toUpperCase();
1277
  CustomDD.clear('cfgInst');
1278
+
1279
+ if (!root || !exp) return;
1280
+
1281
+ const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
1282
+ const data = await res.json();
1283
+
1284
+ if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1285
+ if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1286
+
1287
  if(preInst) document.getElementById('cfgInst').value = preInst;
1288
  },
1289
+
1290
+ toggleCond(sel) {
1291
+ const group = sel.nextElementSibling;
1292
+ group.style.display = sel.value === 'none' ? 'none' : 'flex';
1293
+ },
1294
+
1295
  renderSeries(list) {
1296
  const c = document.getElementById('cfgSeries'); c.innerHTML = '';
1297
  list.forEach(s => {
1298
  const r = document.createElement('div'); r.className = 'series-row';
1299
+ const showCond = s.cond && s.cond !== 'none';
1300
+ r.innerHTML = `
1301
+ <div class="series-row-top">
1302
+ <input type="text" value="${s.formula}" class="s-form" placeholder="Formula (e.g. $P)" style="flex:1;">
1303
+ <input type="text" value="${s.label}" class="s-lbl" placeholder="Label" style="width:100px;">
1304
+ <select class="s-axis" style="width:60px;"><option value="left" ${s.axis==='left'?'selected':''}>Left</option><option value="right" ${s.axis==='right'?'selected':''}>Right</option></select>
1305
+ <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1306
+ </div>
1307
+ <div class="series-row-bottom">
1308
+ <input type="color" value="${s.color1}" class="color-input s-color1" title="Color">
1309
+ <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1310
+ <option value="none" ${s.cond==='none'||!s.cond?'selected':''}>Solid</option>
1311
+ <option value=">" ${s.cond==='>'?'selected':''}>If &gt;</option>
1312
+ <option value="<" ${s.cond==='<'?'selected':''}>If &lt;</option>
1313
+ </select>
1314
+ <div class="s-cond-group" style="display:${showCond?'flex':'none'};">
1315
+ <input type="number" class="s-thresh" value="${s.thresh||0}" style="width:70px;">
1316
+ <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">Else:</span>
1317
+ <input type="color" value="${s.color2||'#ffffff'}" class="color-input s-color2" title="Color 2">
1318
+ </div>
1319
+ </div>
1320
+ `;
1321
  c.appendChild(r);
1322
  });
1323
  },
1324
+
1325
+ addSeriesRow() {
1326
+ const r = document.createElement('div'); r.className = 'series-row';
1327
+ r.innerHTML = `
1328
+ <div class="series-row-top">
1329
+ <input type="text" value="$P" class="s-form" placeholder="Formula (e.g. $P)" style="flex:1;">
1330
+ <input type="text" value="New" class="s-lbl" placeholder="Label" style="width:100px;">
1331
+ <select class="s-axis" style="width:60px;"><option value="left">Left</option><option value="right">Right</option></select>
1332
+ <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1333
+ </div>
1334
+ <div class="series-row-bottom">
1335
+ <input type="color" value="#ffffff" class="color-input s-color1" title="Color">
1336
+ <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1337
+ <option value="none" selected>Solid</option>
1338
+ <option value=">">If &gt;</option>
1339
+ <option value="<">If &lt;</option>
1340
+ </select>
1341
+ <div class="s-cond-group" style="display:none;">
1342
+ <input type="number" class="s-thresh" value="0" style="width:70px;">
1343
+ <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">Else:</span>
1344
+ <input type="color" value="#ffffff" class="color-input s-color2" title="Color 2">
1345
+ </div>
1346
+ </div>
1347
+ `;
1348
+ document.getElementById('cfgSeries').appendChild(r);
1349
+ },
1350
+
1351
+ extractSeriesFromDOM() {
1352
+ const rows = document.querySelectorAll('#cfgSeries .series-row');
1353
+ return Array.from(rows).map(r => ({
1354
+ formula: r.querySelector('.s-form').value,
1355
+ label: r.querySelector('.s-lbl').value,
1356
+ axis: r.querySelector('.s-axis').value,
1357
+ color1: r.querySelector('.s-color1').value,
1358
+ cond: r.querySelector('.s-cond').value,
1359
+ thresh: r.querySelector('.s-thresh').value,
1360
+ color2: r.querySelector('.s-color2').value
1361
+ }));
1362
  },
1363
+
1364
+ saveChartConfig() {
1365
+ const series = this.extractSeriesFromDOM();
1366
+
1367
+ if(this.isGlobal) {
1368
+ App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1369
+ App.save();
1370
+ }
1371
+ else {
1372
+ const root = document.getElementById('cfgRoot').value.toUpperCase();
1373
+ const exp = document.getElementById('cfgExp').value.toUpperCase();
1374
+ const inst = document.getElementById('cfgInst').value.toUpperCase();
1375
+
1376
+ if(!root || !exp) return alert("Incomplete Details. Please type or select valid options.");
1377
+
1378
+ App.state.charts[this.tid].config = {
1379
+ chartMode: this.currentMode,
1380
+ atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1381
+ root, expiry: exp, instrument: inst,
1382
+ timeframe: document.getElementById('cfgTF').value,
1383
+ start: document.getElementById('cfgStart').value,
1384
+ end: document.getElementById('cfgEnd').value,
1385
+ series
1386
+ };
1387
+ App.save();
1388
+ App.renderGrid();
1389
+ ChartLogic.plot(this.tid);
1390
+ }
1391
+ this.close();
1392
+ }
1393
  };
1394
 
1395
  window.onload = () => App.init();