topsecrettraders commited on
Commit
8f655df
·
verified ·
1 Parent(s): 6a81743

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +394 -567
app.py CHANGED
@@ -5,7 +5,7 @@ import asyncio
5
  from datetime import timedelta
6
  from fastapi import FastAPI, Body
7
  from fastapi.responses import HTMLResponse, JSONResponse
8
- from typing import Optional
9
 
10
  # ==========================================
11
  # 1. CONFIGURATION
@@ -73,7 +73,6 @@ def get_auto_config(date: str, root: str):
73
  root = root.upper()
74
  exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
75
  all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
76
-
77
  valid_expiries = [e for e in all_expiries if e >= date]
78
  if not valid_expiries: valid_expiries = all_expiries
79
 
@@ -89,6 +88,7 @@ def get_auto_config(date: str, root: str):
89
  if len(valid_expiries) > 0:
90
  exp_c = valid_expiries[0]
91
  response["current"] = {"expiry": exp_c, "instrument": find_fut_rpc(exp_c)}
 
92
  if len(valid_expiries) > 1:
93
  exp_n = valid_expiries[1]
94
  response["next"] = {"expiry": exp_n, "instrument": find_fut_rpc(exp_n)}
@@ -104,17 +104,14 @@ def get_expiries(date: str, root: str):
104
  def get_instruments(date: str, root: str, expiry: str):
105
  data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root.upper(), "p_expiry": expiry})
106
  instruments = [row['instrument_name'] for row in data]
107
-
108
- spot_fut =[]
109
- options =[]
110
  for i in instruments:
111
  if i.endswith('CE') or i.endswith('PE'): options.append(i)
112
  else: spot_fut.append(i)
113
-
114
  return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
115
 
116
  # ==========================================
117
- # 4. API ROUTES - DATA FETCHING
118
  # ==========================================
119
 
120
  @app.post("/api/fetch_series")
@@ -128,46 +125,49 @@ async def fetch_series(
128
  end_time: str = Body(...),
129
  mode: str = Body("normal"),
130
  atm_range: int = Body(5),
131
- req_vars: list = Body(["P", "CB", "CS", "PB", "PS"])
132
  ):
133
  root = root.upper()
134
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
135
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  if mode == "advanced":
138
  inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
139
  instruments = [r['instrument_name'] for r in inst_data]
140
- if not instruments:
141
- return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P":[]})
142
 
143
  ref_instrument = instrument
144
  if not ref_instrument:
145
  spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
146
- fut_keys = [k for k in instruments if 'FUT' in k]
147
  if spot_keys: ref_instrument = spot_keys[0]
148
  elif fut_keys: ref_instrument = fut_keys[0]
149
  else: ref_instrument = instruments[0]
150
 
151
  rpc_name = "get_advanced_chart_data"
152
  base_params = {
153
- "p_root": root,
154
- "p_expiry": expiry,
155
- "p_ref_instrument": ref_instrument,
156
- "p_atm_range": atm_range,
157
- "p_req_vars": req_vars
158
  }
159
  else:
160
- if not instrument:
161
- return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
162
  rpc_name = "get_normal_chart_data"
163
- base_params = {
164
- "p_root": root,
165
- "p_expiry": expiry,
166
- "p_instrument": instrument,
167
- "p_req_vars": req_vars
168
- }
169
 
170
- # CHUNKING LOGIC: Bypass DB Limits (Concurrent 2hr fetch)
171
  async def fetch_chunk(start_dt, end_dt):
172
  params = base_params.copy()
173
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
@@ -175,7 +175,7 @@ async def fetch_series(
175
  loop = asyncio.get_event_loop()
176
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
177
 
178
- tasks =[]
179
  curr = t_start_dt
180
  chunk_hours = 2
181
 
@@ -187,38 +187,42 @@ async def fetch_series(
187
 
188
  chunk_results = await asyncio.gather(*tasks)
189
 
190
- all_records =[]
191
  for res in chunk_results:
192
  if isinstance(res, list): all_records.extend(res)
193
 
194
- if not all_records:
195
- return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
196
 
197
- # =========================================================
198
- # DATA PROCESSING: Last Valid Value Logic
199
- # =========================================================
200
  df = pd.DataFrame(all_records)
201
- df.rename(columns={"tick_ts": "ts"}, inplace=True)
202
  df['ts'] = pd.to_datetime(df['ts'])
203
  df.drop_duplicates(subset=['ts'], inplace=True)
204
  df.set_index('ts', inplace=True)
205
  df.sort_index(inplace=True)
206
 
207
- tf_map = {
208
- "1min": "1min", "3min": "3min", "5min": "5min",
209
- "15min": "15min", "30min": "30min", "1hour": "1h"
210
- }
211
  panda_tf = tf_map.get(timeframe, "1min")
212
 
213
- # Strictly take the "last" value to replicate the end-of-candle snapshot!
214
- agg_dict = {col: "last" for col in df.columns}
 
 
 
215
  resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
216
 
217
- # Standardize uppercase to frontend
218
  response_data = {"labels": resampled.index.strftime('%H:%M').tolist()}
219
- for col in["p", "b", "s", "v", "oi", "cb", "cs", "pb", "ps"]:
220
- if col in resampled.columns: response_data[col.upper()] = resampled[col].tolist()
221
- else: response_data[col.upper()] =[]
 
 
 
 
 
 
 
 
222
 
223
  return JSONResponse(response_data)
224
 
@@ -237,6 +241,7 @@ HTML_TEMPLATE = """
237
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
238
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
239
  <style>
 
240
  :root {
241
  --bg-body: #0d1117; --bg-header: #161b22; --bg-card: #0d1117;
242
  --bg-panel: #21262d; --bg-input: #161b22; --border: #30363d;
@@ -248,79 +253,83 @@ HTML_TEMPLATE = """
248
  body { background: var(--bg-body); color: var(--text-primary); font-family: var(--font-main); display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px; }
249
  ::-webkit-scrollbar { width: 8px; height: 8px; }
250
  ::-webkit-scrollbar-track { background: var(--bg-body); }
251
- ::-webkit-scrollbar-thumb { background: var(--bg-panel); border-radius: 4px; }
252
 
 
253
  .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; }
254
- .brand { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 8px; }
255
  .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
256
  .controls { display: flex; align-items: center; gap: 12px; }
257
 
258
  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; }
259
- select:focus, input:focus { border-color: var(--accent); }
260
- button:hover { background: var(--bg-panel); }
261
  button.primary { background: var(--accent); border-color: var(--accent); color: white; }
262
  button.primary:hover { background: #58a6ff; }
263
  button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
 
264
 
265
  .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
266
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
267
- .layout-btn.active { background: var(--bg-panel); color: var(--accent); }
268
 
 
269
  .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
270
  .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
271
  .add-chart-btn { height: 40px; border: none; border-top: 1px dashed var(--border); background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600; }
272
  .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
273
 
 
274
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
275
  .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); flex-shrink: 0; }
276
- .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; }
277
  .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
 
278
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
279
  .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
280
- .status-dot.hist { background: var(--accent); }
281
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
282
 
283
- .tool-btn { width: 26px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; display:inline-flex; align-items:center; justify-content:center; }
284
  .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
285
-
286
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
287
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); text-align: center; }
288
 
 
289
  .modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100; display: none; justify-content: center; align-items: center; }
290
- .modal { background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px; width: 560px; max-width: 95%; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 20px 50px rgba(0,0,0,0.5); }
291
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
292
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
293
- .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
294
 
295
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
296
 
 
 
 
 
 
 
 
 
297
  .input-wrapper { position: relative; width: 100%; }
298
- .input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
299
- .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; }
300
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
301
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
302
- .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); }
303
 
 
304
  .series-row { background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px; }
305
- .series-row-top, .series-row-bottom { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
306
- .series-row-bottom { margin-bottom: 0; }
307
-
308
- .s-form, .s-lbl, .s-lbl1, .s-lbl2, .s-thresh { background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; padding: 0 10px; height: 32px; font-size: 12px; }
309
- .color-input { padding: 0; width: 32px; border: 1px solid var(--border); background: var(--bg-input); height: 32px; cursor: pointer; border-radius: 6px; overflow: hidden; }
310
  .color-input::-webkit-color-swatch-wrapper { padding: 0; }
311
  .color-input::-webkit-color-swatch { border: none; }
 
312
 
313
- .s-cond-group { display: flex; gap: 6px; align-items: center; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 6px; border: 1px dashed var(--border); flex: 1; }
314
- .s-cond-solid { display: flex; gap: 6px; align-items: center; flex: 1; }
315
-
316
- .segmented-control { display: flex; background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px; }
317
  .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; }
318
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
319
-
320
- .ins-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; }
321
- .ins-table th { background: var(--bg-panel); color: var(--text-secondary); padding: 8px; text-align: right; position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); }
322
- .ins-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); text-align: right; }
323
- .ins-table tr:hover { background: rgba(255,255,255,0.05); }
324
  </style>
325
  </head>
326
  <body>
@@ -338,10 +347,9 @@ HTML_TEMPLATE = """
338
  </div>
339
  <div style="width:1px; height: 20px; background: var(--border);"></div>
340
  <div class="layout-toggle">
341
- <button class="layout-btn" onclick="App.setLayout(1)">1</button>
342
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
343
- <button class="layout-btn active" onclick="App.setLayout(3)">3</button>
344
- <button class="layout-btn" onclick="App.setLayout(4)">4</button>
345
  </div>
346
  <button onclick="Config.openDefaults()">Settings</button>
347
  <button class="danger" onclick="App.reset()">Reset</button>
@@ -350,6 +358,21 @@ HTML_TEMPLATE = """
350
 
351
  <div class="main-container" id="gridRoot"></div>
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  <!-- CONFIG MODAL -->
354
  <div class="modal-backdrop" id="configModal">
355
  <div class="modal">
@@ -374,14 +397,14 @@ HTML_TEMPLATE = """
374
  <label>Symbol</label>
375
  <div class="input-wrapper">
376
  <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">
377
- <div class="custom-dd" id="dd-cfgRoot"></div>
378
  </div>
379
  </div>
380
  <div class="form-section">
381
  <label>Expiry</label>
382
  <div class="input-wrapper">
383
  <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">
384
- <div class="custom-dd" id="dd-cfgExp"></div>
385
  </div>
386
  </div>
387
  </div>
@@ -390,7 +413,7 @@ HTML_TEMPLATE = """
390
  <label>Mode</label>
391
  <div class="segmented-control" id="cfgModeToggle">
392
  <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
393
- <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced ATM Mode</button>
394
  </div>
395
  </div>
396
 
@@ -398,14 +421,14 @@ HTML_TEMPLATE = """
398
  <label id="lblInst">Instrument</label>
399
  <div class="input-wrapper">
400
  <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">
401
- <div class="custom-dd" id="dd-cfgInst"></div>
402
  </div>
403
  </div>
404
 
405
  <div id="cfgAdvancedFields" style="display:none;">
406
  <div class="form-section" style="margin-top:12px;">
407
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
408
- <input type="number" id="cfgAtmRange" value="5" min="1" style="width:100%;">
409
  </div>
410
  </div>
411
 
@@ -413,32 +436,35 @@ HTML_TEMPLATE = """
413
  <div class="form-section">
414
  <label>Timeframe</label>
415
  <select id="cfgTF" style="width:100%">
416
- <option value="1min">1 Minute</option>
417
- <option value="3min">3 Minutes</option>
418
- <option value="5min">5 Minutes</option>
419
- <option value="15min">15 Minutes</option>
420
  </select>
421
  </div>
422
  <div class="form-section">
423
- <label>Range</label>
424
  <div style="display:flex; gap:8px;">
425
- <input type="time" id="cfgStart" value="09:15">
426
- <input type="time" id="cfgEnd" value="15:30">
427
  </div>
428
  </div>
429
  </div>
 
430
  </div>
431
 
432
- <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; margin-top: 12px;">
433
- <label>Quick Configurations</label>
 
434
  <div style="display:flex; gap:8px;">
 
 
 
 
 
435
  <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
436
- <button class="primary" onclick="Config.savePreset()">Save</button>
437
  </div>
438
  </div>
439
 
440
- <div class="form-section" style="margin-top:12px;">
441
- <label>Plot Series <span style="font-size:10px; text-transform:none; color:var(--text-secondary);">(Supports CB, CS, PB, PS, B, S, P, V, OI)</span></label>
442
  <div id="cfgSeries"></div>
443
  <button style="width:100%; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
444
  </div>
@@ -446,607 +472,418 @@ HTML_TEMPLATE = """
446
  </div>
447
  <div class="modal-foot">
448
  <button style="background:transparent; border:1px solid var(--border); color:var(--text-primary);" onclick="Config.close()">Cancel</button>
449
- <button class="primary" onclick="Config.saveChartConfig()">Save</button>
450
- </div>
451
- </div>
452
- </div>
453
-
454
- <!-- INSPECTION MODAL -->
455
- <div class="modal-backdrop" id="inspectionModal">
456
- <div class="modal" style="width: 800px; max-width: 95%;">
457
- <div class="modal-head">
458
- <span>Calculation Breakdown</span>
459
- <button class="tool-btn" onclick="document.getElementById('inspectionModal').style.display='none'">✕</button>
460
- </div>
461
- <div class="modal-body" style="padding:0; max-height: 60vh;">
462
- <table class="ins-table" id="inspectionTable"></table>
463
  </div>
464
  </div>
465
  </div>
466
 
467
  <script>
468
- document.addEventListener('click', (e) => {
469
- if (!e.target.closest('.input-wrapper')) {
470
- document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  }
472
- });
473
 
474
  const CustomDD = {
475
  data: { cfgRoot: [], cfgExp:[], cfgInst:[] },
476
  recent: { cfgRoot:[] },
477
-
478
- init() {
479
- const saved = localStorage.getItem('dc_recent_symbols');
480
- if(saved) this.recent.cfgRoot = JSON.parse(saved);
481
- },
482
  clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
483
- set(id, items, groupName = null) {
484
- if (!groupName) this.data[id] = items.map(i => ({val: i, group: null}));
485
- else this.data[id].push(...items.map(i => ({val: i, group: groupName})));
486
- },
487
- async open(id) {
488
- document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
489
- document.getElementById(id).select();
490
- this.render(id, '');
491
- document.getElementById(`dd-${id}`).style.display = 'block';
492
  },
 
493
  async filter(id) {
494
- const query = document.getElementById(id).value.toUpperCase();
495
- if(id === 'cfgRoot' && query.length >= 2) {
496
- const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
497
  this.set(id, await res.json());
498
  }
499
- this.render(id, query);
500
- document.getElementById(`dd-${id}`).style.display = 'block';
501
  },
502
- render(id, query) {
503
- const el = document.getElementById(`dd-${id}`);
504
- let html = ''; let currentGroup = null; let pool = [...this.data[id]];
505
-
506
- if(id === 'cfgRoot' && query === '') pool =[...this.recent.cfgRoot.map(r => ({val: r, group: 'Recent'})), ...pool];
507
-
508
- const filtered = pool.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
509
- .filter(item => item.val.toLowerCase().includes(query.toLowerCase()));
510
-
511
- if (!filtered.length) { el.innerHTML = '<div class="custom-dd-opt">No matches</div>'; return; }
512
-
513
- filtered.forEach(item => {
514
- if (item.group && item.group !== currentGroup) {
515
- html += `<div class="custom-dd-group">${item.group}</div>`; currentGroup = item.group;
516
- }
517
- html += `<div class="custom-dd-opt" onclick="CustomDD.pick('${id}', '${item.val}')">${item.val}</div>`;
518
  });
519
  el.innerHTML = html;
520
  },
521
  pick(id, val) {
522
- document.getElementById(id).value = val;
523
- document.getElementById(`dd-${id}`).style.display = 'none';
524
- if(id === 'cfgRoot') {
525
- if(!this.recent.cfgRoot.includes(val)) {
526
- this.recent.cfgRoot.unshift(val);
527
- if(this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
528
- localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot));
529
- }
530
  Config.onRootChange();
531
  }
532
- if(id === 'cfgExp') Config.onExpChange();
533
  }
534
  };
535
 
536
  const App = {
537
- state: { date: null, mode: 'LIVE', columns: 3, layout: [[],[],[]], charts: {}, refreshRate: 15 },
538
- presets: {
539
- "Normal (Call Flow)":[
540
- { formula: "P", color1: "#2f81f7", cond: "none", label: "Price", axis: "left" },
541
- { formula: "CB - CS", color1: "#238636", color2: "#da3633", cond: ">", thresh: "0", lbl1: "Net Buy", lbl2: "Net Sell", label: "Call Net", axis: "right" }
542
- ],
543
- "Normal (Put Flow)":[
544
- { formula: "P", color1: "#2f81f7", cond: "none", label: "Price", axis: "left" },
545
- { formula: "PB - PS", color1: "#da3633", color2: "#238636", cond: ">", thresh: "0", lbl1: "Net Buy", lbl2: "Net Sell", label: "Put Net", axis: "right" }
546
- ],
547
- "Advanced Combined Flow":[
548
- { formula: "P", color1: "#2f81f7", cond: "none", label: "Spot", axis: "left" },
549
- { formula: "CB + PS - (PB + CS)", color1: "#238636", color2: "#da3633", cond: ">", thresh: "0", lbl1: "Bullish", lbl2: "Bearish", label: "Combined Flow", axis: "right" }
550
- ]
551
- },
552
-
553
  async init() {
554
- CustomDD.init();
555
- await this.loadDates();
556
- const savedPresets = localStorage.getItem('dc_pro_v4_presets');
557
- if(savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e){}
558
-
559
- const saved = localStorage.getItem('dc_pro_v4_final');
560
- if (saved) {
561
- try {
562
- const s = JSON.parse(saved);
563
- this.state = { ...this.state, ...s };
564
- if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
565
- } catch(e) { this.initDefault(); }
566
- } else this.initDefault();
567
 
568
  const dSel = document.getElementById('globalDate');
569
  if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
570
  else if(dSel.options.length) this.state.date = dSel.options[0].value;
571
 
572
- document.getElementById('globalMode').value = this.state.mode;
573
- this.updateModeUI(); this.renderGrid(); this.startLoop();
574
- },
575
-
576
- initDefault() {
577
- this.state.columns = 3; this.state.layout = [[], [],[]]; this.state.refreshRate = 15;
578
- for(let i=0; i<3; i++) { this.addChartInternal(i); this.addChartInternal(i); }
579
  },
580
-
581
  async loadDates() {
582
- try {
583
- const res = await fetch('/api/dates'); const dates = await res.json();
584
- document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
585
  if(!this.state.date && dates.length) this.state.date = dates[0];
586
- } catch(e) {}
587
  },
588
-
589
- addChartInternal(colIdx) {
590
- const id = 'c-' + Math.random().toString(36).substr(2, 6);
591
- this.state.layout[colIdx].push(id);
592
- this.state.charts[id] = { config: {}, instance: null, lastData: null };
593
- return id;
594
- },
595
-
596
  save() {
597
- const cleanCharts = {};
598
- for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
599
- localStorage.setItem('dc_pro_v4_final', JSON.stringify({ ...this.state, charts: cleanCharts }));
600
  },
601
-
602
- reset() { if(confirm("Reset Workspace?")) { localStorage.removeItem('dc_pro_v4_final'); location.reload(); } },
603
-
604
  setLayout(n) {
605
- const all = this.state.layout.flat();
606
- this.state.columns = n; this.state.layout = Array.from({length: n}, () =>[]);
607
- all.forEach((id, i) => this.state.layout[i % n].push(id));
608
- this.renderGrid(); this.save();
609
  },
610
-
611
  renderGrid() {
612
- document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
613
- const container = document.getElementById('gridRoot'); container.innerHTML = '';
614
-
615
  this.state.layout.forEach((ids, colIdx) => {
616
- const col = document.createElement('div'); col.className = 'column';
617
- const count = ids.length; const viewH = window.innerHeight - 52;
618
-
619
  ids.forEach(id => {
620
- if(this.state.charts[id] && this.state.charts[id].instance) {
621
- this.state.charts[id].instance.destroy(); this.state.charts[id].instance = null;
622
- }
623
- const card = this.createCardDOM(id);
624
- if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
625
- else card.style.height = '300px';
626
-
 
 
 
 
 
 
 
 
 
 
627
  col.appendChild(card);
628
- setTimeout(() => ChartLogic.plot(id), 0);
629
  });
630
-
631
- const btn = document.createElement('button'); btn.className = 'add-chart-btn'; btn.innerText = '+ Add Chart';
632
- btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
633
- col.appendChild(btn); container.appendChild(col);
634
  });
635
  },
636
-
637
- createCardDOM(id) {
638
- const div = document.createElement('div'); div.className = 'chart-card'; div.id = `card-${id}`;
639
- const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
640
- const isCfg = cfg && cfg.root;
641
- const advancedLabel = cfg.chartMode === 'advanced' ? `<span class="inst-label">ATM +/- ${cfg.atmRange}</span>` : `<span class="inst-label">${cfg.instrument || ''}</span>`;
642
-
643
- div.innerHTML = `
644
- <div class="card-header">
645
- <div class="card-info">
646
- <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
647
- <span>${isCfg ? cfg.root : 'New Chart'}</span>
648
- ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
649
- </div>
650
- <div class="card-tools">
651
- ${isCfg ? `<button class="tool-btn" onclick="ChartLogic.showInspection('${id}')" title="Data Table">🔍</button>` : ''}
652
- <button class="tool-btn" onclick="Config.open('${id}')" title="Configure">⚙</button>
653
- <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
654
- </div>
655
- </div>
656
- <div class="card-body" id="body-${id}">
657
- ${!isCfg ? `
658
- <div class="empty-state"><div style="font-size:24px; opacity:0.3;">📊</div><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></div>
659
- ` : `<canvas id="canvas-${id}"></canvas>`}
660
- </div>
661
- `;
662
- return div;
663
- },
664
-
665
- removeChart(id) {
666
- if(!confirm("Delete?")) return;
667
- this.state.layout = this.state.layout.map(c => c.filter(x => x !== id));
668
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
669
- delete this.state.charts[id];
670
- this.renderGrid(); this.save();
671
- },
672
-
673
- onDateChange() {
674
- this.state.date = document.getElementById('globalDate').value;
675
- const today = new Date().toISOString().split('T')[0];
676
- if(this.state.date === today && this.state.mode !== 'LIVE') {
677
- this.state.mode = 'LIVE'; document.getElementById('globalMode').value = 'LIVE'; this.updateModeUI();
678
- }
679
- this.save(); this.refreshAll();
680
- },
681
- onModeChange() { this.state.mode = document.getElementById('globalMode').value; this.updateModeUI(); this.save(); this.refreshAll(); },
682
- updateModeUI() { document.getElementById('modeDot').className = `status-dot ${this.state.mode==='LIVE'?'live':'hist'}`; },
683
- refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
684
-
685
  startLoop() {
686
- const loop = () => {
687
- if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
688
- setTimeout(loop, (this.state.refreshRate || 15) * 1000);
689
- };
690
- loop();
691
  }
692
  };
693
 
694
  const ChartLogic = {
695
  async plot(id, isUpdate=false) {
696
- const chart = App.state.charts[id];
697
- if(!chart || !chart.config.root) return;
698
-
699
  const cfg = chart.config;
700
- const dot = document.getElementById(`dot-${id}`);
701
- if(isUpdate && dot) dot.classList.add('retry');
702
-
703
  let endTime = cfg.end;
704
- if (App.state.mode === 'LIVE') {
705
- const now = new Date();
706
- endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
707
- }
708
-
709
- // Dynamically extract variables to request
710
- const allFormulas = cfg.series.map(s => s.formula.toUpperCase()).join(' ');
711
- const reqVars = [];['P','B','S','V','OI','CB','CS','PB','PS'].forEach(v => {
712
- if (new RegExp('\\b' + v + '\\b').test(allFormulas)) reqVars.push(v);
713
- });
714
- if (!reqVars.includes('P')) reqVars.push('P');
715
 
716
  try {
 
 
 
717
  const res = await fetch('/api/fetch_series', {
718
  method: 'POST', headers: {'Content-Type': 'application/json'},
719
  body: JSON.stringify({
720
- date: App.state.date, root: cfg.root, expiry: cfg.expiry,
721
- instrument: cfg.instrument || "", timeframe: cfg.timeframe,
722
- start_time: cfg.start, end_time: endTime,
723
- mode: cfg.chartMode || 'normal', atm_range: cfg.atmRange || 5,
724
- req_vars: reqVars
725
  })
726
  });
727
  const data = await res.json();
728
-
729
- if(data.error || !data.labels || !data.labels.length) {
730
- if(App.state.mode !== 'LIVE') {
731
- const b = document.getElementById(`body-${id}`);
732
- if(b) b.innerHTML = `<div class="empty-state" style="opacity:0.8"><span style="font-size:20px;">⚠️</span><br/>${data.error || "No Data"}</div>`;
733
- }
734
- if(dot) dot.classList.remove('retry');
735
- return;
736
- }
737
 
738
- // Save data for inspection modal
739
- chart.lastData = data;
740
-
741
- const body = document.getElementById(`body-${id}`);
742
- if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
743
- if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
744
 
745
  const datasets = cfg.series.map(s => {
746
  const vals = data.labels.map((_, i) => {
747
- let d = {
748
- P: data.P?.[i]||0, B: data.B?.[i]||0, S: data.S?.[i]||0,
749
- V: data.V?.[i]||0, OI: data.OI?.[i]||0, CB: data.CB?.[i]||0,
750
- CS: data.CS?.[i]||0, PB: data.PB?.[i]||0, PS: data.PS?.[i]||0
751
- };
752
- return this.evaluate(s.formula, d);
 
 
 
753
  });
754
 
755
- let dataset = {
756
- label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0,
757
- hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
758
- };
759
-
760
- if(s.cond && s.cond !== 'none') {
761
- let thresh = parseFloat(s.thresh) || 0;
762
- let cTrue = s.color1; let cFalse = s.color2 || s.color1;
763
-
764
- dataset.segment = {
765
- borderColor: ctx => {
766
- let v = ctx.p1.parsed.y;
767
- if(s.cond === '>') return v > thresh ? cTrue : cFalse;
768
- if(s.cond === '<') return v < thresh ? cTrue : cFalse;
769
- return cTrue;
770
- }
771
- };
772
- dataset.borderColor = cTrue; dataset.backgroundColor = cTrue;
773
-
774
- const hoverColor = (ctx) => {
775
- let v = ctx.raw;
776
- if(s.cond === '>') return v > thresh ? cTrue : cFalse;
777
- if(s.cond === '<') return v < thresh ? cTrue : cFalse;
778
- return cTrue;
779
- };
780
- dataset.pointHoverBackgroundColor = hoverColor;
781
- dataset.pointHoverBorderColor = hoverColor;
782
- } else {
783
- dataset.borderColor = s.color1; dataset.backgroundColor = s.color1;
784
- dataset.pointHoverBackgroundColor = s.color1; dataset.pointHoverBorderColor = s.color1;
785
- }
786
  return dataset;
787
  });
788
 
789
  this.render(id, data.labels, datasets, cfg.series);
790
- } catch(e) { console.error(e); }
791
  },
792
-
793
- evaluate(formula, vars) {
794
- try {
795
- let exp = formula.toUpperCase()
796
- .replace(/\bCB\b/g, vars.CB).replace(/\bCS\b/g, vars.CS)
797
- .replace(/\bPB\b/g, vars.PB).replace(/\bPS\b/g, vars.PS)
798
- .replace(/\bOI\b/g, vars.OI).replace(/\bP\b/g, vars.P)
799
- .replace(/\bV\b/g, vars.V).replace(/\bB\b/g, vars.B).replace(/\bS\b/g, vars.S);
800
- return new Function('return '+exp)();
801
- } catch { return null; }
802
- },
803
-
804
- render(id, labels, datasets, seriesConfig) {
805
- const ctx = document.getElementById(`canvas-${id}`);
806
- if(!ctx) return;
807
-
808
- if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
809
- App.state.charts[id].instance.destroy(); App.state.charts[id].instance = null;
810
- }
811
-
812
  if(App.state.charts[id].instance) {
813
  App.state.charts[id].instance.data.labels = labels;
814
  App.state.charts[id].instance.data.datasets = datasets;
815
  App.state.charts[id].instance.update('none');
816
  } else {
817
- const hasRight = datasets.some(d => d.yAxisID === 'y1');
818
- App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
819
  type: 'line', data: { labels, datasets },
820
  options: {
821
- responsive: true, maintainAspectRatio: false, animation: false,
822
- interaction: { mode: 'index', intersect: false },
823
  plugins: {
824
  legend: { display: false },
825
  tooltip: {
826
- backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor: '#30363d', borderWidth: 1,
827
  callbacks: {
828
- label: function(context) {
829
- let s = seriesConfig[context.datasetIndex];
830
- let v = context.parsed.y;
831
- let lbl = s.label;
832
- if(s.cond && s.cond !== 'none') {
833
- let thresh = parseFloat(s.thresh) || 0;
834
- let isTrue = s.cond === '>' ? (v > thresh) : (v < thresh);
835
- if(isTrue && s.lbl1) lbl = s.lbl1;
836
- if(!isTrue && s.lbl2) lbl = s.lbl2;
837
  }
838
- return `${lbl}: ${v.toLocaleString()}`;
839
  },
840
- labelColor: function(context) {
841
- let s = seriesConfig[context.datasetIndex];
842
- let v = context.parsed.y;
843
- let c = s.color1;
844
- if(s.cond && s.cond !== 'none') {
845
- let thresh = parseFloat(s.thresh) || 0;
846
- if(s.cond === '>') c = v > thresh ? s.color1 : (s.color2 || s.color1);
847
- if(s.cond === '<') c = v < thresh ? s.color1 : (s.color2 || s.color1);
848
- }
849
- return { borderColor: c, backgroundColor: c };
850
  }
851
  }
852
  }
853
  },
854
  scales: {
855
- x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
856
- y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
857
- y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
858
  }
859
  }
860
  });
861
  }
862
- },
863
-
864
- showInspection(id) {
865
- const chart = App.state.charts[id];
866
- if(!chart || !chart.lastData) return alert("No data available.");
867
- const d = chart.lastData;
868
- const cfg = chart.config;
869
-
870
- // Define Columns
871
- const cols =['Time', 'Spot/Ref'];
872
- const reqVars = Object.keys(d).filter(k => k!=='labels' && k!=='P' && k!=='error' && d[k].length>0);
873
- cols.push(...reqVars);
874
- cfg.series.forEach((s,i) => cols.push(`F${i+1} (${s.label})`));
875
-
876
- let html = `<thead><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr></thead><tbody>`;
877
 
878
- d.labels.forEach((time, i) => {
879
- let rowVars = { P: d.P?.[i]||0, B: d.B?.[i]||0, S: d.S?.[i]||0, V: d.V?.[i]||0, OI: d.OI?.[i]||0, CB: d.CB?.[i]||0, CS: d.CS?.[i]||0, PB: d.PB?.[i]||0, PS: d.PS?.[i]||0 };
 
 
 
 
 
 
 
 
 
 
880
 
881
- let cells = `<td>${time}</td><td style="color:var(--accent); font-weight:bold;">${rowVars.P.toLocaleString()}</td>`;
882
- reqVars.forEach(v => cells += `<td>${rowVars[v].toLocaleString()}</td>`);
883
-
884
- cfg.series.forEach(s => {
885
- let val = this.evaluate(s.formula, rowVars);
886
- let color = "inherit";
887
- if(s.cond && s.cond !== 'none') {
888
- let thresh = parseFloat(s.thresh)||0;
889
- color = (s.cond === '>' ? val>thresh : val<thresh) ? s.color1 : (s.color2||s.color1);
890
- }
891
- cells += `<td style="color:${color}; font-weight:bold;">${val ? val.toLocaleString() : '0'}</td>`;
892
  });
893
- html += `<tr>${cells}</tr>`;
894
- });
895
- html += `</tbody>`;
896
-
897
- document.getElementById('inspectionTable').innerHTML = html;
898
- document.getElementById('inspectionModal').style.display = 'flex';
899
- }
900
- };
901
 
902
  const Config = {
903
- tid: null, isGlobal: false, currentMode: 'normal',
904
 
905
- setMode(mode) {
906
- this.currentMode = mode;
907
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
908
- btns[0].classList.toggle('active', mode === 'normal');
909
- btns[1].classList.toggle('active', mode === 'advanced');
 
910
 
911
- document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
912
- document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut';
913
  this.updatePresetsDropdown();
914
- this.loadPreset(mode === 'advanced' ? "Advanced Combined Flow" : "Normal (Call Flow)");
915
  },
916
 
917
  updatePresetsDropdown() {
918
- document.getElementById('cfgPresetsDropdown').innerHTML = Object.keys(App.presets).map(k => `<option value="${k}">${k}</option>`).join('');
 
 
919
  },
920
 
921
- loadPreset(presetName = null) {
922
- const name = presetName || document.getElementById('cfgPresetsDropdown').value;
923
- if(name && App.presets[name]) {
924
- document.getElementById('cfgPresetsDropdown').value = name;
925
- this.renderSeries(App.presets[name]);
 
926
  }
927
  },
928
 
929
- savePreset() {
930
- const name = prompt("Name this Setup:", "Custom Flow");
931
- if(!name) return;
932
- App.presets[name] = this.extractSeriesFromDOM();
933
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
934
- this.updatePresetsDropdown(); document.getElementById('cfgPresetsDropdown').value = name;
935
- },
936
-
937
  async open(id) {
938
  this.tid = id; this.isGlobal = false;
939
  document.getElementById('configModal').style.display = 'flex';
940
- document.getElementById('cfgTitle').innerText = 'Configure Chart';
941
- document.getElementById('cfgLoading').style.display = 'block';
942
- document.getElementById('cfgContent').style.display = 'none';
943
-
944
  document.getElementById('cfgGlobalOnly').style.display = 'none';
945
  document.getElementById('cfgChartOnly').style.display = 'block';
946
 
947
- this.updatePresetsDropdown();
948
  CustomDD.clear('cfgRoot');
949
- const res = await fetch(`/api/roots?date=${App.state.date}`);
950
- CustomDD.set('cfgRoot', await res.json());
951
 
952
  const cfg = App.state.charts[id].config;
953
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
954
  document.getElementById('cfgStart').value = cfg.start || '09:15';
955
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
956
-
957
- this.currentMode = cfg.chartMode || 'normal';
958
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
959
- this.setMode(this.currentMode);
960
-
961
- if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
 
962
 
963
  document.getElementById('cfgRoot').value = cfg.root || '';
964
- document.getElementById('cfgExp').value = cfg.expiry || '';
965
- document.getElementById('cfgInst').value = cfg.instrument || '';
966
-
967
  if(cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
968
- document.getElementById('cfgLoading').style.display = 'none';
969
- document.getElementById('cfgContent').style.display = 'block';
970
  },
971
 
972
  openDefaults() {
973
  this.isGlobal = true;
974
  document.getElementById('configModal').style.display = 'flex';
975
- document.getElementById('cfgTitle').innerText = 'Global Settings';
976
  document.getElementById('cfgGlobalOnly').style.display = 'block';
977
  document.getElementById('cfgChartOnly').style.display = 'none';
978
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
979
  },
980
-
981
  close() { document.getElementById('configModal').style.display = 'none'; },
982
 
983
- async onRootChange(preExp, preInst) {
984
  const root = document.getElementById('cfgRoot').value.toUpperCase();
985
  CustomDD.clear('cfgExp'); CustomDD.clear('cfgInst');
986
- if (!preExp) document.getElementById('cfgExp').value = '';
987
- if (!preInst) document.getElementById('cfgInst').value = '';
988
- if (!root) return;
989
-
990
- const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
991
- const exps = await res.json();
992
  CustomDD.set('cfgExp', exps);
993
-
994
- if(preExp && exps.includes(preExp)) {
995
- document.getElementById('cfgExp').value = preExp; await this.onExpChange(preInst);
996
- } else if (exps.length > 0) {
997
- const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
998
- if(auto.current) { document.getElementById('cfgExp').value = auto.current.expiry; await this.onExpChange(auto.current.instrument); }
999
- }
1000
  },
1001
 
1002
- async onExpChange(preInst) {
1003
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1004
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1005
  CustomDD.clear('cfgInst');
1006
- if (!root || !exp) return;
1007
-
1008
  const data = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`).then(r=>r.json());
1009
- if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1010
  if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1011
- if(preInst) document.getElementById('cfgInst').value = preInst;
1012
  },
1013
 
1014
- toggleCond(sel) {
1015
- const solidGrp = sel.nextElementSibling;
1016
- const condGrp = solidGrp.nextElementSibling;
1017
- if(sel.value === 'none') { solidGrp.style.display = 'flex'; condGrp.style.display = 'none'; }
1018
- else { solidGrp.style.display = 'none'; condGrp.style.display = 'flex'; }
1019
- },
1020
 
1021
  renderSeries(list) {
1022
  const c = document.getElementById('cfgSeries'); c.innerHTML = '';
1023
  list.forEach(s => {
1024
  const r = document.createElement('div'); r.className = 'series-row';
1025
- const showCond = s.cond && s.cond !== 'none';
1026
  r.innerHTML = `
1027
  <div class="series-row-top">
1028
- <input type="text" value="${s.formula}" class="s-form" placeholder="e.g. CB+PS" style="flex:1;">
1029
- <select class="s-axis" style="width:70px;"><option value="left" ${s.axis==='left'?'selected':''}>Left</option><option value="right" ${s.axis==='right'?'selected':''}>Right</option></select>
1030
- <button class="tool-btn" style="color:var(--danger); background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()"></button>
 
1031
  </div>
1032
  <div class="series-row-bottom">
 
1033
  <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1034
- <option value="none" ${!showCond?'selected':''}>Solid</option>
1035
  <option value=">" ${s.cond==='>'?'selected':''}>If &gt;</option>
1036
  <option value="<" ${s.cond==='<'?'selected':''}>If &lt;</option>
1037
  </select>
1038
-
1039
- <div class="s-cond-solid" style="display:${showCond?'none':'flex'};">
1040
- <input type="text" value="${s.label}" class="s-lbl" placeholder="Label">
1041
- <input type="color" value="${s.color1}" class="color-input s-color1" title="Color">
1042
- </div>
1043
-
1044
- <div class="s-cond-group" style="display:${showCond?'flex':'none'};">
1045
  <input type="number" class="s-thresh" value="${s.thresh||0}" style="width:50px;">
1046
- <input type="text" value="${s.lbl1||''}" class="s-lbl1" placeholder="True" style="width:65px;">
1047
- <input type="color" value="${s.color1}" class="color-input s-color1-cond" title="True Color">
1048
- <input type="text" value="${s.lbl2||''}" class="s-lbl2" placeholder="False" style="width:65px;">
1049
- <input type="color" value="${s.color2||'#ffffff'}" class="color-input s-color2" title="False Color">
 
1050
  </div>
1051
  </div>
1052
  `;
@@ -1054,50 +891,40 @@ const Config = {
1054
  });
1055
  },
1056
 
1057
- addSeriesRow() {
1058
- this.renderSeries([...this.extractSeriesFromDOM(), { formula: "CB", color1: "#2f81f7", cond: "none", label: "New Line", axis: "right" }]);
1059
- },
1060
-
1061
- extractSeriesFromDOM() {
1062
- return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
1063
- const cond = r.querySelector('.s-cond').value;
1064
- const isCond = cond !== 'none';
1065
- return {
1066
- formula: r.querySelector('.s-form').value.toUpperCase(),
1067
- axis: r.querySelector('.s-axis').value,
1068
- cond: cond,
1069
- label: isCond ? 'Dynamic' : r.querySelector('.s-lbl').value,
1070
- color1: isCond ? r.querySelector('.s-color1-cond').value : r.querySelector('.s-color1').value,
1071
- thresh: isCond ? r.querySelector('.s-thresh').value : "0",
1072
- lbl1: isCond ? r.querySelector('.s-lbl1').value : "",
1073
- lbl2: isCond ? r.querySelector('.s-lbl2').value : "",
1074
- color2: isCond ? r.querySelector('.s-color2').value : "#ffffff"
1075
- };
1076
- });
1077
  },
1078
 
1079
  saveChartConfig() {
1080
- if(this.isGlobal) {
1081
- App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15; App.save();
1082
- } else {
1083
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1084
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1085
- if(!root || !exp) return alert("Incomplete Details.");
1086
-
1087
- App.state.charts[this.tid].config = {
1088
- chartMode: this.currentMode,
1089
- atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1090
- root, expiry: exp, instrument: document.getElementById('cfgInst').value.toUpperCase(),
1091
- timeframe: document.getElementById('cfgTF').value,
1092
- start: document.getElementById('cfgStart').value, end: document.getElementById('cfgEnd').value,
1093
- series: this.extractSeriesFromDOM()
1094
- };
1095
- App.save(); App.renderGrid(); ChartLogic.plot(this.tid);
1096
- }
1097
- this.close();
1098
  }
1099
  };
1100
 
 
1101
  window.onload = () => App.init();
1102
  </script>
1103
  </body>
 
5
  from datetime import timedelta
6
  from fastapi import FastAPI, Body
7
  from fastapi.responses import HTMLResponse, JSONResponse
8
+ from typing import Optional, List
9
 
10
  # ==========================================
11
  # 1. CONFIGURATION
 
73
  root = root.upper()
74
  exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
75
  all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
 
76
  valid_expiries = [e for e in all_expiries if e >= date]
77
  if not valid_expiries: valid_expiries = all_expiries
78
 
 
88
  if len(valid_expiries) > 0:
89
  exp_c = valid_expiries[0]
90
  response["current"] = {"expiry": exp_c, "instrument": find_fut_rpc(exp_c)}
91
+
92
  if len(valid_expiries) > 1:
93
  exp_n = valid_expiries[1]
94
  response["next"] = {"expiry": exp_n, "instrument": find_fut_rpc(exp_n)}
 
104
  def get_instruments(date: str, root: str, expiry: str):
105
  data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root.upper(), "p_expiry": expiry})
106
  instruments = [row['instrument_name'] for row in data]
107
+ spot_fut, options = [], []
 
 
108
  for i in instruments:
109
  if i.endswith('CE') or i.endswith('PE'): options.append(i)
110
  else: spot_fut.append(i)
 
111
  return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
112
 
113
  # ==========================================
114
+ # 4. API ROUTES - DATA FETCHING & EGRESS OPTIMIZATION
115
  # ==========================================
116
 
117
  @app.post("/api/fetch_series")
 
125
  end_time: str = Body(...),
126
  mode: str = Body("normal"),
127
  atm_range: int = Body(5),
128
+ formulas: List[str] = Body([])
129
  ):
130
  root = root.upper()
131
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
132
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
133
+
134
+ # Parse Formulas to ONLY return requested data (Huge EGRESS Saver)
135
+ req_vars = set()
136
+ for f in formulas:
137
+ if "$CB" in f: req_vars.add("cb")
138
+ if "$CS" in f: req_vars.add("cs")
139
+ if "$PB" in f: req_vars.add("pb")
140
+ if "$PS" in f: req_vars.add("ps")
141
+ if "$B" in f: req_vars.add("buy_qty")
142
+ if "$S" in f: req_vars.add("sell_qty")
143
+ if "$V" in f: req_vars.add("volume")
144
+ if "$OI" in f: req_vars.add("oi")
145
+ req_vars.add("price") # Always mandatory
146
 
147
  if mode == "advanced":
148
  inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
149
  instruments = [r['instrument_name'] for r in inst_data]
150
+ if not instruments: return JSONResponse({"error": f"No data found for expiry {expiry}", "labels":[]})
 
151
 
152
  ref_instrument = instrument
153
  if not ref_instrument:
154
  spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
155
+ fut_keys =[k for k in instruments if 'FUT' in k]
156
  if spot_keys: ref_instrument = spot_keys[0]
157
  elif fut_keys: ref_instrument = fut_keys[0]
158
  else: ref_instrument = instruments[0]
159
 
160
  rpc_name = "get_advanced_chart_data"
161
  base_params = {
162
+ "p_root": root, "p_expiry": expiry, "p_ref_instrument": ref_instrument,
163
+ "p_atm_range": atm_range, "p_strike_map": {}
 
 
 
164
  }
165
  else:
166
+ if not instrument: return JSONResponse({"error": "No Instrument Selected.", "labels": []})
 
167
  rpc_name = "get_normal_chart_data"
168
+ base_params = {"p_root": root, "p_expiry": expiry, "p_instrument": instrument}
 
 
 
 
 
169
 
170
+ # Parallel Database Chunking
171
  async def fetch_chunk(start_dt, end_dt):
172
  params = base_params.copy()
173
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
 
175
  loop = asyncio.get_event_loop()
176
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
177
 
178
+ tasks = []
179
  curr = t_start_dt
180
  chunk_hours = 2
181
 
 
187
 
188
  chunk_results = await asyncio.gather(*tasks)
189
 
190
+ all_records = []
191
  for res in chunk_results:
192
  if isinstance(res, list): all_records.extend(res)
193
 
194
+ if not all_records: return JSONResponse({"error": "No valid data found in range.", "labels":[]})
 
195
 
196
+ # Pandas Processing using .last() for Depth Tracking
 
 
197
  df = pd.DataFrame(all_records)
198
+ df.rename(columns={"tick_ts": "ts", "b_qty": "buy_qty", "s_qty": "sell_qty", "vol": "volume"}, inplace=True)
199
  df['ts'] = pd.to_datetime(df['ts'])
200
  df.drop_duplicates(subset=['ts'], inplace=True)
201
  df.set_index('ts', inplace=True)
202
  df.sort_index(inplace=True)
203
 
204
+ tf_map = {"1min": "1min", "3min": "3min", "5min": "5min", "15min": "15min", "30min": "30min", "1hour": "1h"}
 
 
 
205
  panda_tf = tf_map.get(timeframe, "1min")
206
 
207
+ # Using Last Valid values for pure depth aggregation
208
+ agg_dict = {}
209
+ for col in req_vars:
210
+ if col in df.columns: agg_dict[col] = "last"
211
+
212
  resampled = df.resample(panda_tf).agg(agg_dict).ffill().fillna(0)
213
 
 
214
  response_data = {"labels": resampled.index.strftime('%H:%M').tolist()}
215
+
216
+ # Return ONLY mapped variables directly
217
+ if "price" in resampled: response_data["P"] = resampled["price"].tolist()
218
+ if "buy_qty" in resampled: response_data["B"] = resampled["buy_qty"].tolist()
219
+ if "sell_qty" in resampled: response_data["S"] = resampled["sell_qty"].tolist()
220
+ if "volume" in resampled: response_data["V"] = resampled["volume"].tolist()
221
+ if "oi" in resampled: response_data["OI"] = resampled["oi"].tolist()
222
+ if "cb" in resampled: response_data["CB"] = resampled["cb"].tolist()
223
+ if "cs" in resampled: response_data["CS"] = resampled["cs"].tolist()
224
+ if "pb" in resampled: response_data["PB"] = resampled["pb"].tolist()
225
+ if "ps" in resampled: response_data["PS"] = resampled["ps"].tolist()
226
 
227
  return JSONResponse(response_data)
228
 
 
241
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
242
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
243
  <style>
244
+ /* --- THEME --- */
245
  :root {
246
  --bg-body: #0d1117; --bg-header: #161b22; --bg-card: #0d1117;
247
  --bg-panel: #21262d; --bg-input: #161b22; --border: #30363d;
 
253
  body { background: var(--bg-body); color: var(--text-primary); font-family: var(--font-main); display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px; }
254
  ::-webkit-scrollbar { width: 8px; height: 8px; }
255
  ::-webkit-scrollbar-track { background: var(--bg-body); }
256
+ ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
257
 
258
+ /* HEADER */
259
  .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; }
260
+ .brand { font-weight: 700; font-size: 15px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
261
  .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
262
  .controls { display: flex; align-items: center; gap: 12px; }
263
 
264
  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; }
265
+ select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(47,129,247,0.2); }
266
+ button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
267
  button.primary { background: var(--accent); border-color: var(--accent); color: white; }
268
  button.primary:hover { background: #58a6ff; }
269
  button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
270
+ button.danger:hover { background: rgba(218, 54, 51, 0.1); }
271
 
272
  .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
273
  .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
274
+ .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
275
 
276
+ /* GRID */
277
  .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
278
  .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
279
  .add-chart-btn { height: 40px; border: none; border-top: 1px dashed var(--border); background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600; }
280
  .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
281
 
282
+ /* CARD */
283
  .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
284
  .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); flex-shrink: 0; }
285
+ .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
286
  .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
287
+
288
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
289
  .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
 
290
  @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
291
 
292
+ .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
293
  .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
 
294
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
295
  .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); text-align: center; }
296
 
297
+ /* MODALS */
298
  .modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100; display: none; justify-content: center; align-items: center; }
299
+ .modal { background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px; width: 620px; max-width: 95%; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 20px 50px rgba(0,0,0,0.5); }
300
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
301
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
302
+ .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
303
 
304
  .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
305
 
306
+ /* DATA TABLE MODAL */
307
+ .table-wrap { overflow-y: auto; max-height: 400px; border: 1px solid var(--border); border-radius: 6px; }
308
+ .data-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; text-align: right; }
309
+ .data-table th, .data-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); }
310
+ .data-table th { background: var(--bg-panel); position: sticky; top: 0; color: var(--text-secondary); font-weight: bold; }
311
+ .data-table tr:hover { background: var(--bg-input); }
312
+
313
+ /* DROPDOWN */
314
  .input-wrapper { position: relative; width: 100%; }
315
+ .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; margin-top: 4px; }
 
316
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
317
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
318
+ .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
319
 
320
+ /* SERIES CONFIG ROW */
321
  .series-row { background: var(--bg-panel); padding: 12px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 10px; }
322
+ .series-row-top { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
323
+ .series-row-bottom { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
324
+ .s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-text { background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; padding: 0 10px; height: 32px; font-size: 12px; }
325
+ .color-input { padding: 0; width: 36px; border: 1px solid var(--border); background: var(--bg-input); height: 32px; cursor: pointer; border-radius: 6px; overflow: hidden; }
 
326
  .color-input::-webkit-color-swatch-wrapper { padding: 0; }
327
  .color-input::-webkit-color-swatch { border: none; }
328
+ .s-cond-group { display: flex; gap: 8px; align-items: center; background: rgba(0,0,0,0.2); padding: 6px 10px; border-radius: 6px; border: 1px dashed var(--border); flex: 1; flex-wrap:wrap;}
329
 
330
+ .segmented-control { display: flex; background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
 
 
 
331
  .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; }
332
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
 
 
 
 
 
333
  </style>
334
  </head>
335
  <body>
 
347
  </div>
348
  <div style="width:1px; height: 20px; background: var(--border);"></div>
349
  <div class="layout-toggle">
350
+ <button class="layout-btn active" onclick="App.setLayout(1)">1</button>
351
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
352
+ <button class="layout-btn" onclick="App.setLayout(3)">3</button>
 
353
  </div>
354
  <button onclick="Config.openDefaults()">Settings</button>
355
  <button class="danger" onclick="App.reset()">Reset</button>
 
358
 
359
  <div class="main-container" id="gridRoot"></div>
360
 
361
+ <!-- CALCULATION BREAKDOWN MODAL -->
362
+ <div class="modal-backdrop" id="breakdownModal">
363
+ <div class="modal">
364
+ <div class="modal-head">
365
+ <span>Calculation Breakdown (Data Inspector)</span>
366
+ <button class="tool-btn" onclick="Breakdown.close()">✕</button>
367
+ </div>
368
+ <div class="modal-body">
369
+ <div class="table-wrap">
370
+ <table class="data-table" id="breakdownTable"></table>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
  <!-- CONFIG MODAL -->
377
  <div class="modal-backdrop" id="configModal">
378
  <div class="modal">
 
397
  <label>Symbol</label>
398
  <div class="input-wrapper">
399
  <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">
400
+ <div id="dd-cfgRoot" class="custom-dd"></div>
401
  </div>
402
  </div>
403
  <div class="form-section">
404
  <label>Expiry</label>
405
  <div class="input-wrapper">
406
  <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">
407
+ <div id="dd-cfgExp" class="custom-dd"></div>
408
  </div>
409
  </div>
410
  </div>
 
413
  <label>Mode</label>
414
  <div class="segmented-control" id="cfgModeToggle">
415
  <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
416
+ <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
417
  </div>
418
  </div>
419
 
 
421
  <label id="lblInst">Instrument</label>
422
  <div class="input-wrapper">
423
  <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">
424
+ <div id="dd-cfgInst" class="custom-dd"></div>
425
  </div>
426
  </div>
427
 
428
  <div id="cfgAdvancedFields" style="display:none;">
429
  <div class="form-section" style="margin-top:12px;">
430
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
431
+ <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
432
  </div>
433
  </div>
434
 
 
436
  <div class="form-section">
437
  <label>Timeframe</label>
438
  <select id="cfgTF" style="width:100%">
439
+ <option value="1min">1 Minute</option><option value="3min">3 Minutes</option>
440
+ <option value="5min">5 Minutes</option><option value="15min">15 Minutes</option>
 
 
441
  </select>
442
  </div>
443
  <div class="form-section">
444
+ <label>Range (HH:MM)</label>
445
  <div style="display:flex; gap:8px;">
446
+ <input type="time" id="cfgStart" value="09:15"><input type="time" id="cfgEnd" value="15:30">
 
447
  </div>
448
  </div>
449
  </div>
450
+ <div style="height:1px; background:var(--border); margin: 12px 0;"></div>
451
  </div>
452
 
453
+ <!-- PRESETS -->
454
+ <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom: 12px;">
455
+ <label>Pre-built Models</label>
456
  <div style="display:flex; gap:8px;">
457
+ <select id="cfgPresetsGroup" onchange="Config.updatePresetsDropdown()" style="flex:1;">
458
+ <option value="Normal - Options">Normal - Options</option>
459
+ <option value="Normal - Futures">Normal - Futures / Eq</option>
460
+ <option value="Advanced - Options">Advanced - Options</option>
461
+ </select>
462
  <select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
 
463
  </div>
464
  </div>
465
 
466
+ <div class="form-section">
467
+ <label>Plot Series</label>
468
  <div id="cfgSeries"></div>
469
  <button style="width:100%; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
470
  </div>
 
472
  </div>
473
  <div class="modal-foot">
474
  <button style="background:transparent; border:1px solid var(--border); color:var(--text-primary);" onclick="Config.close()">Cancel</button>
475
+ <button class="primary" onclick="Config.saveChartConfig()">Save Configuration</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  </div>
477
  </div>
478
  </div>
479
 
480
  <script>
481
+ // --- CORE PRESETS DEFINITION ---
482
+ const PRESETS = {
483
+ "Normal - Futures": {
484
+ "Basic Flow": [
485
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
486
+ { formula: '$B', label: 'Buy Qty', axis: 'right', cond: 'none', color1: '#238636' },
487
+ { formula: '$S', label: 'Sell Qty', axis: 'right', cond: 'none', color1: '#da3633' }
488
+ ],
489
+ "Net Flow": [
490
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
491
+ { formula: '$B - $S', label: 'Net Flow', axis: 'right', cond: '>', thresh: '0', color1: '#238636', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
492
+ ]
493
+ },
494
+ "Normal - Options": {
495
+ "Call Flow": [
496
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
497
+ { formula: '$CB', label: 'Call Buy', axis: 'right', cond: 'none', color1: '#238636' },
498
+ { formula: '$CS', label: 'Call Sell', axis: 'right', cond: 'none', color1: '#da3633' }
499
+ ],
500
+ "Put Flow": [
501
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
502
+ { formula: '$PB', label: 'Put Buy', axis: 'right', cond: 'none', color1: '#238636' },
503
+ { formula: '$PS', label: 'Put Sell', axis: 'right', cond: 'none', color1: '#da3633' }
504
+ ],
505
+ "Aggregated Flow": [
506
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
507
+ { formula: '$CB + $PS', label: 'Total Buy Side', axis: 'right', cond: 'none', color1: '#238636' },
508
+ { formula: '$PB + $CS', label: 'Total Sell Side', axis: 'right', cond: 'none', color1: '#da3633' }
509
+ ],
510
+ "Overall Net Flow": [
511
+ { formula: '$P', label: 'Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
512
+ { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', cond: '>', thresh: '0', color1: '#238636', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
513
+ ]
514
+ },
515
+ "Advanced - Options": {
516
+ "Price & Flows": [
517
+ { formula: '$P', label: 'Spot Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
518
+ { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', cond: 'none', color1: '#238636' },
519
+ { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', cond: 'none', color1: '#da3633' }
520
+ ],
521
+ "Overall Net Flow": [
522
+ { formula: '$P', label: 'Spot Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
523
+ { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', cond: '>', thresh: '0', color1: '#238636', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
524
+ ],
525
+ "Components": [
526
+ { formula: '$P', label: 'Spot Price', axis: 'left', cond: 'none', color1: '#2f81f7' },
527
+ { formula: '$CB', label: 'Call Buy', axis: 'right', cond: 'none', color1: '#238636' },
528
+ { formula: '$CS', label: 'Call Sell', axis: 'right', cond: 'none', color1: '#3fb950' },
529
+ { formula: '$PB', label: 'Put Buy', axis: 'right', cond: 'none', color1: '#da3633' },
530
+ { formula: '$PS', label: 'Put Sell', axis: 'right', cond: 'none', color1: '#f85149' }
531
+ ]
532
  }
533
+ };
534
 
535
  const CustomDD = {
536
  data: { cfgRoot: [], cfgExp:[], cfgInst:[] },
537
  recent: { cfgRoot:[] },
538
+ init() { const saved = localStorage.getItem('dc_recent_syms'); if(saved) this.recent.cfgRoot = JSON.parse(saved); },
 
 
 
 
539
  clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
540
+ set(id, items, group=null) {
541
+ if(!group) this.data[id] = items.map(i=>({val:i, group:null}));
542
+ else this.data[id].push(...items.map(i=>({val:i, group})));
 
 
 
 
 
 
543
  },
544
+ async open(id) { document.querySelectorAll('.custom-dd').forEach(el=>el.style.display='none'); document.getElementById(id).select(); this.render(id,''); document.getElementById(`dd-${id}`).style.display='block'; },
545
  async filter(id) {
546
+ const q = document.getElementById(id).value.toUpperCase();
547
+ if(id==='cfgRoot' && q.length>=2) {
548
+ const res = await fetch(`/api/roots?date=${App.state.date}&q=${q}`);
549
  this.set(id, await res.json());
550
  }
551
+ this.render(id, q); document.getElementById(`dd-${id}`).style.display='block';
 
552
  },
553
+ render(id, q) {
554
+ const el = document.getElementById(`dd-${id}`); q=q.toLowerCase();
555
+ let html='', curGrp=null; let pool = [...this.data[id]];
556
+ if(id==='cfgRoot' && q==='') pool =[...this.recent.cfgRoot.map(r=>({val:r,group:'Recent'})), ...pool];
557
+ const f = pool.filter((v,i,a)=>a.findIndex(t=>t.val===v.val)===i).filter(item=>item.val.toLowerCase().includes(q));
558
+ if(f.length===0){ el.innerHTML='<div class="custom-dd-opt" style="color:var(--text-secondary)">No matches</div>'; return; }
559
+ f.forEach(item => {
560
+ if(item.group && item.group!==curGrp) { html+=`<div class="custom-dd-group">${item.group}</div>`; curGrp=item.group; }
561
+ html += `<div class="custom-dd-opt" onclick="CustomDD.pick('${id}','${item.val}')">${item.val}</div>`;
 
 
 
 
 
 
 
562
  });
563
  el.innerHTML = html;
564
  },
565
  pick(id, val) {
566
+ document.getElementById(id).value = val; document.getElementById(`dd-${id}`).style.display='none';
567
+ if(id==='cfgRoot') {
568
+ if(!this.recent.cfgRoot.includes(val)) { this.recent.cfgRoot.unshift(val); if(this.recent.cfgRoot.length>10) this.recent.cfgRoot.pop(); localStorage.setItem('dc_recent_syms', JSON.stringify(this.recent.cfgRoot)); }
 
 
 
 
 
569
  Config.onRootChange();
570
  }
571
+ if(id==='cfgExp') Config.onExpChange();
572
  }
573
  };
574
 
575
  const App = {
576
+ state: { date: null, mode: 'LIVE', columns: 1, layout: [[],[],[]], charts: {}, refreshRate: 15 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  async init() {
578
+ CustomDD.init(); await this.loadDates();
579
+ const saved = localStorage.getItem('dc_pro_v4');
580
+ if(saved) { try { this.state = { ...this.state, ...JSON.parse(saved) }; } catch(e){ this.initDef(); } }
581
+ else this.initDef();
 
 
 
 
 
 
 
 
 
582
 
583
  const dSel = document.getElementById('globalDate');
584
  if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
585
  else if(dSel.options.length) this.state.date = dSel.options[0].value;
586
 
587
+ document.getElementById('globalMode').value = this.state.mode; this.updateModeUI();
588
+ this.renderGrid(); this.startLoop();
 
 
 
 
 
589
  },
590
+ initDef() { this.state.columns=1; this.state.layout=[[]]; this.addChartInternal(0); },
591
  async loadDates() {
592
+ try { const res = await fetch('/api/dates'); const dates = await res.json();
593
+ document.getElementById('globalDate').innerHTML = dates.map(d=>`<option value="${d}">${d}</option>`).join('');
 
594
  if(!this.state.date && dates.length) this.state.date = dates[0];
595
+ } catch(e){}
596
  },
597
+ addChartInternal(col) { const id = 'c-'+Math.random().toString(36).substr(2,6); this.state.layout[col].push(id); this.state.charts[id]={config:{},instance:null,lastData:null}; return id; },
 
 
 
 
 
 
 
598
  save() {
599
+ const cc={}; for(let id in this.state.charts) cc[id]={config:this.state.charts[id].config};
600
+ localStorage.setItem('dc_pro_v4', JSON.stringify({...this.state, charts:cc}));
 
601
  },
602
+ reset() { if(confirm("Reset Workspace?")) { localStorage.removeItem('dc_pro_v4'); location.reload(); } },
 
 
603
  setLayout(n) {
604
+ const all = this.state.layout.flat(); this.state.columns=n; this.state.layout=Array.from({length:n},()=>[]);
605
+ all.forEach((id,i)=>this.state.layout[i%n].push(id)); this.renderGrid(); this.save();
 
 
606
  },
 
607
  renderGrid() {
608
+ document.querySelectorAll('.layout-btn').forEach((b,i)=>b.classList.toggle('active', (i+1)===this.state.columns));
609
+ const c = document.getElementById('gridRoot'); c.innerHTML='';
 
610
  this.state.layout.forEach((ids, colIdx) => {
611
+ const col = document.createElement('div'); col.className='column';
612
+ const vH = window.innerHeight-52; const h = ids.length<=2 ? `calc(${vH/ids.length}px - 1px)` : '350px';
 
613
  ids.forEach(id => {
614
+ const card = document.createElement('div'); card.className='chart-card'; card.style.height=h;
615
+ const cfg = this.state.charts[id]?.config||{};
616
+ const lbl = cfg.chartMode==='advanced' ? `ATM ±${cfg.atmRange}` : (cfg.instrument||'');
617
+ card.innerHTML = `
618
+ <div class="card-header">
619
+ <div class="card-info"><div class="status-dot ${cfg.root?(this.state.mode==='LIVE'?'live':''):''}" id="dot-${id}"></div>
620
+ <span>${cfg.root||'New Chart'}</span> <span class="inst-label">${lbl}</span></div>
621
+ <div class="card-tools">
622
+ <button class="tool-btn" onclick="Breakdown.open('${id}')" title="Inspect Breakdown">🔍</button>
623
+ <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
624
+ <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
625
+ </div>
626
+ </div>
627
+ <div class="card-body" id="body-${id}">
628
+ ${!cfg.root ? `<div class="empty-state"><button onclick="Config.open('${id}')">Configure</button></div>` : `<canvas id="canvas-${id}"></canvas>`}
629
+ </div>
630
+ `;
631
  col.appendChild(card);
632
+ if(cfg.root) setTimeout(()=>ChartLogic.plot(id),0);
633
  });
634
+ const btn=document.createElement('button'); btn.className='add-chart-btn'; btn.innerText='+ Add Chart';
635
+ btn.onclick=()=>{ this.addChartInternal(colIdx); this.renderGrid(); this.save(); }; col.appendChild(btn); c.appendChild(col);
 
 
636
  });
637
  },
638
+ removeChart(id) { if(!confirm("Delete?"))return; this.state.layout=this.state.layout.map(c=>c.filter(x=>x!==id)); if(this.state.charts[id].instance) this.state.charts[id].instance.destroy(); delete this.state.charts[id]; this.renderGrid(); this.save(); },
639
+ onDateChange() { this.state.date=document.getElementById('globalDate').value; this.save(); Object.keys(this.state.charts).forEach(id=>ChartLogic.plot(id)); },
640
+ onModeChange() { this.state.mode=document.getElementById('globalMode').value; this.updateModeUI(); this.save(); Object.keys(this.state.charts).forEach(id=>ChartLogic.plot(id)); },
641
+ updateModeUI() { document.getElementById('modeDot').className=`status-dot ${this.state.mode==='LIVE'?'live':''}`; },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  startLoop() {
643
+ const loop=()=>{
644
+ if(this.state.mode==='LIVE') Object.keys(this.state.charts).forEach(id=>ChartLogic.plot(id, true));
645
+ setTimeout(loop, (this.state.refreshRate||15)*1000);
646
+ }; loop();
 
647
  }
648
  };
649
 
650
  const ChartLogic = {
651
  async plot(id, isUpdate=false) {
652
+ const chart = App.state.charts[id]; if(!chart||!chart.config.root) return;
 
 
653
  const cfg = chart.config;
 
 
 
654
  let endTime = cfg.end;
655
+ if(App.state.mode==='LIVE') { const d=new Date(); endTime=d.getHours().toString().padStart(2,'0')+":"+d.getMinutes().toString().padStart(2,'0'); }
 
 
 
 
 
 
 
 
 
 
656
 
657
  try {
658
+ // Egress Optimization: Extract formulas directly from config
659
+ const requestedFormulas = cfg.series.map(s => s.formula);
660
+
661
  const res = await fetch('/api/fetch_series', {
662
  method: 'POST', headers: {'Content-Type': 'application/json'},
663
  body: JSON.stringify({
664
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry, instrument: cfg.instrument||"",
665
+ timeframe: cfg.timeframe, start_time: cfg.start, end_time: endTime,
666
+ mode: cfg.chartMode||'normal', atm_range: cfg.atmRange||5,
667
+ formulas: requestedFormulas
 
668
  })
669
  });
670
  const data = await res.json();
671
+ chart.lastData = data; // Store for Breakdown Modal
 
 
 
 
 
 
 
 
672
 
673
+ if(data.error||!data.labels||!data.labels.length) return;
 
 
 
 
 
674
 
675
  const datasets = cfg.series.map(s => {
676
  const vals = data.labels.map((_, i) => {
677
+ 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;
678
+ const CB=data.CB?.[i]||0, CS=data.CS?.[i]||0, PB=data.PB?.[i]||0, PS=data.PS?.[i]||0;
679
+
680
+ let f = s.formula
681
+ .replace(/\$CB/g, str(CB)).replace(/\$CS/g, str(CS))
682
+ .replace(/\$PB/g, str(PB)).replace(/\$PS/g, str(PS))
683
+ .replace(/\$OI/g, str(OI)).replace(/\$P/g, str(P))
684
+ .replace(/\$V/g, str(V)).replace(/\$B/g, str(B)).replace(/\$S/g, str(S));
685
+ try { return new Function('return '+f)(); } catch{ return 0; }
686
  });
687
 
688
+ let dataset = { label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1' };
689
+ if(s.cond && s.cond!=='none') {
690
+ let th = parseFloat(s.thresh)||0;
691
+ dataset.segment = { borderColor: ctx => { let v=ctx.p1.parsed.y; return (s.cond==='>'?v>th:v<th) ? s.color1 : (s.color2||s.color1); } };
692
+ } else dataset.borderColor = s.color1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  return dataset;
694
  });
695
 
696
  this.render(id, data.labels, datasets, cfg.series);
697
+ } catch(e){}
698
  },
699
+ render(id, labels, datasets, sCfg) {
700
+ const ctx = document.getElementById(`canvas-${id}`); if(!ctx)return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  if(App.state.charts[id].instance) {
702
  App.state.charts[id].instance.data.labels = labels;
703
  App.state.charts[id].instance.data.datasets = datasets;
704
  App.state.charts[id].instance.update('none');
705
  } else {
706
+ const hasR = datasets.some(d=>d.yAxisID==='y1');
707
+ App.state.charts[id].instance = new Chart(ctx, {
708
  type: 'line', data: { labels, datasets },
709
  options: {
710
+ responsive: true, maintainAspectRatio: false, animation: false, interaction: { mode: 'index', intersect: false },
 
711
  plugins: {
712
  legend: { display: false },
713
  tooltip: {
 
714
  callbacks: {
715
+ label: function(c) {
716
+ let s = sCfg[c.datasetIndex], v = c.parsed.y, lbl = s.label;
717
+ if(s.cond && s.cond!=='none') {
718
+ let isT = (s.cond==='>') ? v > parseFloat(s.thresh||0) : v < parseFloat(s.thresh||0);
719
+ if(isT && s.labelTrue) lbl = s.labelTrue;
720
+ if(!isT && s.labelFalse) lbl = s.labelFalse;
 
 
 
721
  }
722
+ return `${lbl}: ${v}`;
723
  },
724
+ labelColor: function(c) {
725
+ let s = sCfg[c.datasetIndex], v = c.parsed.y, cl = s.color1;
726
+ if(s.cond && s.cond!=='none') cl = ((s.cond==='>') ? v > parseFloat(s.thresh||0) : v < parseFloat(s.thresh||0)) ? s.color1 : (s.color2||s.color1);
727
+ return { borderColor: cl, backgroundColor: cl };
 
 
 
 
 
 
728
  }
729
  }
730
  }
731
  },
732
  scales: {
733
+ x: { ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
734
+ y: { position:'left', ticks:{color:'#8b949e', font:{size:10}} },
735
+ y1: { display:hasR, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
736
  }
737
  }
738
  });
739
  }
740
+ }
741
+ };
742
+
743
+ const Breakdown = {
744
+ open(id) {
745
+ const d = App.state.charts[id].lastData;
746
+ const sCfg = App.state.charts[id].config.series;
747
+ if(!d || !d.labels) return alert("No data to inspect.");
 
 
 
 
 
 
 
748
 
749
+ let html = `<tr><th>Time</th><th>Price</th>`;
750
+ if(d.B) html += `<th>B</th>`; if(d.S) html += `<th>S</th>`;
751
+ if(d.CB) html += `<th>CB</th>`; if(d.CS) html += `<th>CS</th>`;
752
+ if(d.PB) html += `<th>PB</th>`; if(d.PS) html += `<th>PS</th>`;
753
+ sCfg.forEach(s => html += `<th>${s.label}</th>`);
754
+ html += `</tr>`;
755
+
756
+ for(let i=0; i<d.labels.length; i++) {
757
+ html += `<tr><td>${d.labels[i]}</td><td>${d.P?.[i]||0}</td>`;
758
+ if(d.B) html += `<td>${d.B[i]}</td>`; if(d.S) html += `<td>${d.S[i]}</td>`;
759
+ if(d.CB) html += `<td>${d.CB[i]}</td>`; if(d.CS) html += `<td>${d.CS[i]}</td>`;
760
+ if(d.PB) html += `<td>${d.PB[i]}</td>`; if(d.PS) html += `<td>${d.PS[i]}</td>`;
761
 
762
+ sCfg.forEach(s => {
763
+ const P=d.P?.[i]||0, B=d.B?.[i]||0, S=d.S?.[i]||0;
764
+ const CB=d.CB?.[i]||0, CS=d.CS?.[i]||0, PB=d.PB?.[i]||0, PS=d.PS?.[i]||0;
765
+ let f = s.formula.replace(/\$CB/g, str(CB)).replace(/\$CS/g, str(CS)).replace(/\$PB/g, str(PB)).replace(/\$PS/g, str(PS)).replace(/\$P/g, str(P)).replace(/\$B/g, str(B)).replace(/\$S/g, str(S));
766
+ try { html += `<td style="color:var(--accent); font-weight:bold;">${new Function('return '+f)()}</td>`; } catch{ html+=`<td>0</td>`; }
 
 
 
 
 
 
767
  });
768
+ html += `</tr>`;
769
+ }
770
+ document.getElementById('breakdownTable').innerHTML = html;
771
+ document.getElementById('breakdownModal').style.display = 'flex';
772
+ },
773
+ close() { document.getElementById('breakdownModal').style.display = 'none'; }
774
+ }
 
775
 
776
  const Config = {
777
+ tid: null, isGlobal: false,
778
 
779
+ setMode(m) {
 
780
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
781
+ btns[0].classList.toggle('active', m === 'normal');
782
+ btns[1].classList.toggle('active', m === 'advanced');
783
+ document.getElementById('cfgAdvancedFields').style.display = m==='advanced'?'block':'none';
784
 
785
+ let defGrp = m==='advanced' ? "Advanced - Options" : "Normal - Options";
786
+ document.getElementById('cfgPresetsGroup').value = defGrp;
787
  this.updatePresetsDropdown();
788
+ this.loadPreset(Object.keys(PRESETS[defGrp])[0]);
789
  },
790
 
791
  updatePresetsDropdown() {
792
+ const grp = document.getElementById('cfgPresetsGroup').value;
793
+ const sel = document.getElementById('cfgPresetsDropdown');
794
+ sel.innerHTML = Object.keys(PRESETS[grp]).map(k => `<option value="${k}">${k}</option>`).join('');
795
  },
796
 
797
+ loadPreset(name=null) {
798
+ const grp = document.getElementById('cfgPresetsGroup').value;
799
+ const n = name || document.getElementById('cfgPresetsDropdown').value;
800
+ if(n && PRESETS[grp][n]) {
801
+ document.getElementById('cfgPresetsDropdown').value = n;
802
+ this.renderSeries(PRESETS[grp][n]);
803
  }
804
  },
805
 
 
 
 
 
 
 
 
 
806
  async open(id) {
807
  this.tid = id; this.isGlobal = false;
808
  document.getElementById('configModal').style.display = 'flex';
 
 
 
 
809
  document.getElementById('cfgGlobalOnly').style.display = 'none';
810
  document.getElementById('cfgChartOnly').style.display = 'block';
811
 
 
812
  CustomDD.clear('cfgRoot');
813
+ CustomDD.set('cfgRoot', await fetch(`/api/roots?date=${App.state.date}`).then(r=>r.json()));
 
814
 
815
  const cfg = App.state.charts[id].config;
816
+ document.getElementById('cfgTF').value = cfg.timeframe || '5min';
817
  document.getElementById('cfgStart').value = cfg.start || '09:15';
818
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
 
 
819
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
820
+
821
+ let m = cfg.chartMode || 'normal';
822
+ this.setMode(m);
823
+ if(cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
824
 
825
  document.getElementById('cfgRoot').value = cfg.root || '';
 
 
 
826
  if(cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
 
 
827
  },
828
 
829
  openDefaults() {
830
  this.isGlobal = true;
831
  document.getElementById('configModal').style.display = 'flex';
 
832
  document.getElementById('cfgGlobalOnly').style.display = 'block';
833
  document.getElementById('cfgChartOnly').style.display = 'none';
834
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
835
  },
 
836
  close() { document.getElementById('configModal').style.display = 'none'; },
837
 
838
+ async onRootChange(pE, pI) {
839
  const root = document.getElementById('cfgRoot').value.toUpperCase();
840
  CustomDD.clear('cfgExp'); CustomDD.clear('cfgInst');
841
+ if(!root) return;
842
+ const exps = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`).then(r=>r.json());
 
 
 
 
843
  CustomDD.set('cfgExp', exps);
844
+ if(pE && exps.includes(pE)) { document.getElementById('cfgExp').value = pE; await this.onExpChange(pI); }
845
+ else if(exps.length>0) { document.getElementById('cfgExp').value = exps[0]; await this.onExpChange(); }
 
 
 
 
 
846
  },
847
 
848
+ async onExpChange(pI) {
849
+ const root = document.getElementById('cfgRoot').value;
850
+ const exp = document.getElementById('cfgExp').value;
851
  CustomDD.clear('cfgInst');
852
+ if(!root || !exp) return;
 
853
  const data = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`).then(r=>r.json());
854
+ if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Spot');
855
  if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
856
+ if(pI) document.getElementById('cfgInst').value = pI;
857
  },
858
 
859
+ toggleCond(sel) { sel.nextElementSibling.style.display = sel.value==='none'?'none':'flex'; },
 
 
 
 
 
860
 
861
  renderSeries(list) {
862
  const c = document.getElementById('cfgSeries'); c.innerHTML = '';
863
  list.forEach(s => {
864
  const r = document.createElement('div'); r.className = 'series-row';
865
+ const showC = s.cond && s.cond !== 'none';
866
  r.innerHTML = `
867
  <div class="series-row-top">
868
+ <input type="text" value="${s.formula}" class="s-form" placeholder="$P, $CB, $PS..." style="flex:1;">
869
+ <input type="text" value="${s.label}" class="s-lbl" placeholder="Label" style="width:100px;">
870
+ <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>
871
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.parentElement.remove()">✕</button>
872
  </div>
873
  <div class="series-row-bottom">
874
+ <input type="color" value="${s.color1}" class="color-input s-color1" title="Color">
875
  <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
876
+ <option value="none" ${s.cond==='none'||!s.cond?'selected':''}>Solid</option>
877
  <option value=">" ${s.cond==='>'?'selected':''}>If &gt;</option>
878
  <option value="<" ${s.cond==='<'?'selected':''}>If &lt;</option>
879
  </select>
880
+ <div class="s-cond-group" style="display:${showC?'flex':'none'};">
 
 
 
 
 
 
881
  <input type="number" class="s-thresh" value="${s.thresh||0}" style="width:50px;">
882
+ <input type="color" value="${s.color1}" class="color-input s-ctrue" title="True Color">
883
+ <input type="text" class="s-ltrue s-text" value="${s.labelTrue||'BUY'}" placeholder="Label True" style="width:80px;">
884
+ <span style="color:#666">Else</span>
885
+ <input type="color" value="${s.color2||'#ffffff'}" class="color-input s-cfalse" title="False Color">
886
+ <input type="text" class="s-lfalse s-text" value="${s.labelFalse||'SELL'}" placeholder="Label False" style="width:80px;">
887
  </div>
888
  </div>
889
  `;
 
891
  });
892
  },
893
 
894
+ extract() {
895
+ return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => ({
896
+ formula: r.querySelector('.s-form').value.toUpperCase(),
897
+ label: r.querySelector('.s-lbl').value,
898
+ axis: r.querySelector('.s-axis').value,
899
+ color1: r.querySelector('.s-color1').value,
900
+ cond: r.querySelector('.s-cond').value,
901
+ thresh: r.querySelector('.s-thresh').value,
902
+ colorTrue: r.querySelector('.s-ctrue') ? r.querySelector('.s-ctrue').value : null,
903
+ color2: r.querySelector('.s-cfalse') ? r.querySelector('.s-cfalse').value : null,
904
+ labelTrue: r.querySelector('.s-ltrue') ? r.querySelector('.s-ltrue').value : null,
905
+ labelFalse: r.querySelector('.s-lfalse') ? r.querySelector('.s-lfalse').value : null
906
+ }));
 
 
 
 
 
 
 
907
  },
908
 
909
  saveChartConfig() {
910
+ if(this.isGlobal) { App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value)||15; App.save(); this.close(); return; }
911
+ const rt = document.getElementById('cfgRoot').value;
912
+ const ex = document.getElementById('cfgExp').value;
913
+ if(!rt||!ex) return alert("Select Symbol and Expiry");
914
+
915
+ App.state.charts[this.tid].config = {
916
+ chartMode: document.querySelector('#cfgModeToggle .active').innerText.includes('Advanced') ? 'advanced' : 'normal',
917
+ atmRange: parseInt(document.getElementById('cfgAtmRange').value)||5,
918
+ root: rt, expiry: ex, instrument: document.getElementById('cfgInst').value,
919
+ timeframe: document.getElementById('cfgTF').value,
920
+ start: document.getElementById('cfgStart').value, end: document.getElementById('cfgEnd').value,
921
+ series: this.extract()
922
+ };
923
+ App.save(); App.renderGrid(); ChartLogic.plot(this.tid); this.close();
 
 
 
 
924
  }
925
  };
926
 
927
+ function str(v) { return v!==null && v!==undefined ? v.toString() : "0"; }
928
  window.onload = () => App.init();
929
  </script>
930
  </body>