topsecrettraders commited on
Commit
0358315
·
verified ·
1 Parent(s): 238c5c0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +291 -78
app.py CHANGED
@@ -3,6 +3,7 @@ import requests
3
  import uvicorn
4
  import json
5
  import asyncio
 
6
  from datetime import datetime, timedelta
7
  from fastapi import FastAPI, Body, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse
@@ -35,7 +36,7 @@ def sb_get(endpoint: str, params: dict = None):
35
  return r.json()
36
  except Exception as e:
37
  print(f"DB Error ({endpoint}): {e}")
38
- return []
39
 
40
  # ==========================================
41
  # 3. API ROUTES - METADATA
@@ -70,7 +71,6 @@ def get_roots(date: str):
70
  def get_auto_config(date: str, root: str):
71
  """Smart helper for defaults (Current/Next expiry)."""
72
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
73
- # Fetch small limit just to inspect structure
74
  data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
75
 
76
  expiry_map = {}
@@ -119,7 +119,7 @@ def get_expiries(date: str, root: str):
119
 
120
  @app.get("/api/instruments")
121
  def get_instruments(date: str, root: str, expiry: str):
122
- """Fetch instruments. Fetches small limit to populate dropdown."""
123
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
124
  data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
125
  instruments = set()
@@ -129,7 +129,7 @@ def get_instruments(date: str, root: str, expiry: str):
129
  return JSONResponse(sorted(list(instruments)))
130
 
131
  # ==========================================
132
- # 4. API ROUTES - DATA FETCHING (OPTIMIZED)
133
  # ==========================================
134
 
135
  @app.post("/api/fetch_series")
@@ -137,58 +137,149 @@ def fetch_series(
137
  date: str = Body(...),
138
  root: str = Body(...),
139
  expiry: str = Body(...),
140
- instrument: str = Body(...),
141
  timeframe: str = Body(...),
142
  start_time: str = Body(...),
143
- end_time: str = Body(...)
 
 
144
  ):
145
  """
146
- Fetches raw 10s data using Postgres JSON filtering to save bandwidth.
 
147
  """
148
  t_start = f"{date}T{start_time}:00"
149
  t_end = f"{date}T{end_time}:59"
150
 
151
- # CRITICAL: Postgres JSON Filtering
152
- # Syntax: inst_data:data->"{instrument}"
153
- # This aliases the result to 'inst_data' and extracts ONLY that specific key
154
- # Double quotes around {instrument} handle spaces like "Nifty 50"
155
- select_query = f'minute,inst_data:data->"{instrument}"'
156
 
157
- url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
158
-
159
- rows = sb_get(url)
160
-
161
- records = []
162
- for row in rows:
163
- try:
164
- base_ts = datetime.fromisoformat(row['minute'])
165
- # The data is now in the alias 'inst_data', not 'data'
166
- inst_data = row.get('inst_data')
167
-
168
- if not inst_data: continue
169
-
170
- # Extract buckets (0s to 50s)
171
- p_arr = inst_data.get('p', []) or []
172
- b_arr = inst_data.get('b', []) or []
173
- s_arr = inst_data.get('s', []) or []
174
- v_arr = inst_data.get('v', []) or []
175
- o_arr = inst_data.get('o', []) or []
176
-
177
- for i in range(6):
178
- if i < len(p_arr) and p_arr[i] is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  records.append({
180
  "ts": base_ts + timedelta(seconds=i*10),
181
- "price": float(p_arr[i]),
182
- "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
183
- "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
184
- "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
185
- "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0
 
 
186
  })
187
- except Exception:
188
- continue
 
189
 
190
  if not records:
191
- return JSONResponse({"error": "No data found", "labels": [], "P": []})
192
 
193
  df = pd.DataFrame(records)
194
  df.set_index('ts', inplace=True)
@@ -201,14 +292,27 @@ def fetch_series(
201
 
202
  resampled = df.resample(panda_tf).median().dropna()
203
 
204
- response_data = {
205
- "labels": resampled.index.strftime('%H:%M').tolist(),
206
- "P": resampled['price'].tolist(),
207
- "B": resampled['buy_qty'].tolist(),
208
- "S": resampled['sell_qty'].tolist(),
209
- "V": resampled['volume'].tolist(),
210
- "OI": resampled['oi'].tolist()
211
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  return JSONResponse(response_data)
214
 
@@ -296,7 +400,7 @@ HTML_TEMPLATE = """
296
  padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
297
  }
298
  .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
299
- .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
300
  .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
301
 
302
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
@@ -329,6 +433,21 @@ HTML_TEMPLATE = """
329
  .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
330
  .color-input { padding: 0; width: 100%; border: none; background: none; height: 28px; cursor: pointer; }
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  </style>
333
  </head>
334
  <body>
@@ -380,7 +499,7 @@ HTML_TEMPLATE = """
380
 
381
  <!-- Single Chart Config Mode -->
382
  <div id="cfgChartOnly">
383
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
384
  <div class="form-section">
385
  <label>Symbol</label>
386
  <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
@@ -390,10 +509,30 @@ HTML_TEMPLATE = """
390
  <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
391
  </div>
392
  </div>
393
- <div class="form-section">
394
- <label>Instrument</label>
395
- <select id="cfgInst" style="width:100%"></select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  </div>
 
397
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
398
  <div class="form-section">
399
  <label>Timeframe</label>
@@ -414,12 +553,30 @@ HTML_TEMPLATE = """
414
  </div>
415
  </div>
416
  </div>
417
- <div style="height:1px; background:var(--border); margin: 8px 0;"></div>
418
  </div>
419
 
420
  <!-- Common Series Config -->
421
  <div class="form-section">
422
- <label>Plot Series</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  <div id="cfgSeries"></div>
424
  <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary);" onclick="Config.addSeriesRow()">+ Add Line</button>
425
  </div>
@@ -434,8 +591,8 @@ HTML_TEMPLATE = """
434
 
435
  <script>
436
  const App = {
437
- state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
438
- defaults: [
439
  { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
440
  { formula: '$B', color: '#238636', label: 'Buy', axis: 'right' },
441
  { formula: '$S', color: '#da3633', label: 'Sell', axis: 'right' }
@@ -448,14 +605,12 @@ const App = {
448
  try {
449
  const s = JSON.parse(saved);
450
  this.state = { ...this.state, ...s };
451
- // Ensure layout matches columns
452
  if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
453
  } catch(e) { this.initDefault(); }
454
  } else {
455
  this.initDefault();
456
  }
457
 
458
- // Sync Date UI
459
  const dSel = document.getElementById('globalDate');
460
  if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
461
  else if(dSel.options.length) this.state.date = dSel.options[0].value;
@@ -468,9 +623,8 @@ const App = {
468
 
469
  initDefault() {
470
  this.state.columns = 3;
471
- this.state.layout = [[], [], []];
472
  this.state.refreshRate = 15;
473
- // 3x2 Grid (3 Cols, 2 Rows = 6 Charts)
474
  for(let i=0; i<3; i++) {
475
  this.addChartInternal(i);
476
  this.addChartInternal(i);
@@ -494,7 +648,6 @@ const App = {
494
  },
495
 
496
  save() {
497
- // Strip non-serializable data
498
  const cleanCharts = {};
499
  for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
500
 
@@ -518,18 +671,17 @@ const App = {
518
  setLayout(n) {
519
  const all = this.state.layout.flat();
520
  this.state.columns = n;
521
- this.state.layout = Array.from({length: n}, () => []);
522
  all.forEach((id, i) => this.state.layout[i % n].push(id));
523
  this.renderGrid();
524
  this.save();
525
  },
526
 
527
  renderGrid() {
528
- // Fix Layout Buttons
529
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
530
 
531
  const container = document.getElementById('gridRoot');
532
- container.innerHTML = ''; // Wipes DOM
533
 
534
  this.state.layout.forEach((ids, colIdx) => {
535
  const col = document.createElement('div');
@@ -538,15 +690,12 @@ const App = {
538
  const viewH = window.innerHeight - 52;
539
 
540
  ids.forEach(id => {
541
- // IMPORTANT: Since we wiped DOM, old instances are dead.
542
- // We MUST nullify them so plot() creates new ones.
543
  if(this.state.charts[id] && this.state.charts[id].instance) {
544
  this.state.charts[id].instance.destroy();
545
  this.state.charts[id].instance = null;
546
  }
547
 
548
  const card = this.createCardDOM(id);
549
- // 50/50 split if 2 charts, 33% if 3, else fixed
550
  if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
551
  else card.style.height = '300px';
552
 
@@ -571,12 +720,17 @@ const App = {
571
  const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
572
  const isCfg = cfg && cfg.root;
573
 
 
 
 
 
 
574
  div.innerHTML = `
575
  <div class="card-header">
576
  <div class="card-info">
577
  <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
578
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
579
- ${isCfg ? `<span class="inst-label">${cfg.instrument}</span> <span class="card-meta">${cfg.expiry}</span>` : ''}
580
  </div>
581
  <div class="card-tools">
582
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
@@ -660,8 +814,10 @@ const ChartLogic = {
660
  headers: {'Content-Type': 'application/json'},
661
  body: JSON.stringify({
662
  date: App.state.date, root: cfg.root, expiry: cfg.expiry,
663
- instrument: cfg.instrument, timeframe: cfg.timeframe,
664
- start_time: cfg.start, end_time: endTime
 
 
665
  })
666
  });
667
  const data = await res.json();
@@ -682,8 +838,16 @@ const ChartLogic = {
682
 
683
  const datasets = cfg.series.map(s => {
684
  const vals = data.labels.map((_, i) => {
685
- 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;
686
- let f = s.formula.replace(/\$OI/g,OI).replace(/\$P/g,P).replace(/\$V/g,V).replace(/\$B/g,B).replace(/\$S/g,S);
 
 
 
 
 
 
 
 
687
  try { return new Function('return '+f)(); } catch{ return null; }
688
  });
689
  return {
@@ -701,7 +865,6 @@ const ChartLogic = {
701
  const ctx = document.getElementById(`canvas-${id}`);
702
  if(!ctx) return;
703
 
704
- // Ensure we aren't updating a dead instance (Double check)
705
  if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
706
  App.state.charts[id].instance.destroy();
707
  App.state.charts[id].instance = null;
@@ -752,6 +915,12 @@ const Config = {
752
  rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
753
 
754
  const cfg = App.state.charts[id].config;
 
 
 
 
 
 
755
  document.getElementById('cfgTF').value = cfg.timeframe || '1min';
756
  document.getElementById('cfgStart').value = cfg.start || '09:15';
757
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
@@ -778,6 +947,28 @@ const Config = {
778
 
779
  close() { document.getElementById('configModal').style.display = 'none'; },
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  async onRootChange(preExp, preInst) {
782
  const root = document.getElementById('cfgRoot').value;
783
  const eSel = document.getElementById('cfgExp');
@@ -798,9 +989,27 @@ const Config = {
798
  const exp = document.getElementById('cfgExp').value;
799
  const iSel = document.getElementById('cfgInst');
800
  iSel.innerHTML = '<option>Loading...</option>';
 
801
  const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
802
  const insts = await res.json();
803
- iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
805
  },
806
 
@@ -846,11 +1055,15 @@ const Config = {
846
  else {
847
  const root = document.getElementById('cfgRoot').value;
848
  const exp = document.getElementById('cfgExp').value;
 
849
  const inst = document.getElementById('cfgInst').value;
850
- if(!root || !exp || !inst) return alert("Incomplete Details");
 
 
851
 
852
  App.state.charts[this.tid].config = {
853
  root, expiry: exp, instrument: inst,
 
854
  timeframe: document.getElementById('cfgTF').value,
855
  start: document.getElementById('cfgStart').value,
856
  end: document.getElementById('cfgEnd').value,
 
3
  import uvicorn
4
  import json
5
  import asyncio
6
+ import re
7
  from datetime import datetime, timedelta
8
  from fastapi import FastAPI, Body, HTTPException
9
  from fastapi.responses import HTMLResponse, JSONResponse
 
36
  return r.json()
37
  except Exception as e:
38
  print(f"DB Error ({endpoint}): {e}")
39
+ return[]
40
 
41
  # ==========================================
42
  # 3. API ROUTES - METADATA
 
71
  def get_auto_config(date: str, root: str):
72
  """Smart helper for defaults (Current/Next expiry)."""
73
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
 
74
  data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
75
 
76
  expiry_map = {}
 
119
 
120
  @app.get("/api/instruments")
121
  def get_instruments(date: str, root: str, expiry: str):
122
+ """Fetch instruments."""
123
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
124
  data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
125
  instruments = set()
 
129
  return JSONResponse(sorted(list(instruments)))
130
 
131
  # ==========================================
132
+ # 4. API ROUTES - DATA FETCHING
133
  # ==========================================
134
 
135
  @app.post("/api/fetch_series")
 
137
  date: str = Body(...),
138
  root: str = Body(...),
139
  expiry: str = Body(...),
140
+ instrument: str = Body(""),
141
  timeframe: str = Body(...),
142
  start_time: str = Body(...),
143
+ end_time: str = Body(...),
144
+ adv_mode: bool = Body(False),
145
+ atm_range: int = Body(5)
146
  ):
147
  """
148
+ Fetches raw 10s data. Supports normal (optimized Postgres JSON filtering)
149
+ and advanced mode (aggregated ATM +/- Options dynamics).
150
  """
151
  t_start = f"{date}T{start_time}:00"
152
  t_end = f"{date}T{end_time}:59"
153
 
154
+ records =[]
 
 
 
 
155
 
156
+ if not adv_mode:
157
+ # --- NORMAL MODE (Highly Optimized Postgres Select) ---
158
+ select_query = f'minute,inst_data:data->"{instrument}"'
159
+ url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
160
+ rows = sb_get(url)
161
+
162
+ for row in rows:
163
+ try:
164
+ base_ts = datetime.fromisoformat(row['minute'])
165
+ inst_data = row.get('inst_data')
166
+
167
+ if not inst_data: continue
168
+
169
+ p_arr = inst_data.get('p', []) or []
170
+ b_arr = inst_data.get('b', []) or[]
171
+ s_arr = inst_data.get('s', []) or[]
172
+ v_arr = inst_data.get('v', []) or []
173
+ o_arr = inst_data.get('o',[]) or[]
174
+
175
+ for i in range(6):
176
+ if i < len(p_arr) and p_arr[i] is not None:
177
+ records.append({
178
+ "ts": base_ts + timedelta(seconds=i*10),
179
+ "price": float(p_arr[i]),
180
+ "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
181
+ "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
182
+ "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
183
+ "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0
184
+ })
185
+ except Exception:
186
+ continue
187
+
188
+ else:
189
+ # --- ADVANCED MODE (Full JSON Fetch & Custom Multi-Strike Aggregation) ---
190
+ select_query = 'minute,data'
191
+ url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
192
+ rows = sb_get(url)
193
+
194
+ last_p_ref = 0.0
195
+
196
+ for row in rows:
197
+ try:
198
+ base_ts = datetime.fromisoformat(row['minute'])
199
+ data_dict = row.get('data', {})
200
+ if not data_dict: continue
201
+
202
+ # Identify reference instrument (Index, Equity or Future)
203
+ ref_inst = None
204
+ for inst in data_dict.keys():
205
+ if "-EQ" in inst or "-INDEX" in inst or "FUT" in inst:
206
+ ref_inst = inst
207
+ break
208
+
209
+ # Pre-parse option instruments to map strikes to CE/PE
210
+ options_meta = {}
211
+ strikes_set = set()
212
+ for inst in data_dict.keys():
213
+ match = re.search(r'(\d+)(CE|PE)$', inst)
214
+ if match:
215
+ strike = int(match.group(1))
216
+ opt_type = match.group(2)
217
+ strikes_set.add(strike)
218
+ if strike not in options_meta:
219
+ options_meta[strike] = {}
220
+ options_meta[strike][opt_type] = inst
221
+
222
+ sorted_strikes = sorted(list(strikes_set))
223
+ if not sorted_strikes: continue
224
+
225
+ # Process all 6 x 10sec slots inside the minute
226
+ for i in range(6):
227
+ p_ref = last_p_ref
228
+ if ref_inst and ref_inst in data_dict:
229
+ p_arr = data_dict[ref_inst].get('p', [])
230
+ if i < len(p_arr) and p_arr[i] is not None:
231
+ p_ref = float(p_arr[i])
232
+ last_p_ref = p_ref
233
+
234
+ if p_ref == 0.0:
235
+ continue
236
+
237
+ # Dynamic ATM calculation per tick
238
+ atm_strike = min(sorted_strikes, key=lambda x: abs(x - p_ref))
239
+ atm_idx = sorted_strikes.index(atm_strike)
240
+
241
+ start_idx = max(0, atm_idx - atm_range)
242
+ end_idx = min(len(sorted_strikes) - 1, atm_idx + atm_range)
243
+ selected_strikes = sorted_strikes[start_idx : end_idx + 1]
244
+
245
+ cb_tot, cs_tot, pb_tot, ps_tot = 0.0, 0.0, 0.0, 0.0
246
+ v_tot, o_tot = 0.0, 0.0
247
+
248
+ for s in selected_strikes:
249
+ # Call Accumulation
250
+ ce_inst = options_meta[s].get('CE')
251
+ if ce_inst:
252
+ ce_data = data_dict[ce_inst]
253
+ if i < len(ce_data.get('b',[])) and ce_data['b'][i] is not None: cb_tot += float(ce_data['b'][i])
254
+ if i < len(ce_data.get('s', [])) and ce_data['s'][i] is not None: cs_tot += float(ce_data['s'][i])
255
+ if i < len(ce_data.get('v', [])) and ce_data['v'][i] is not None: v_tot += float(ce_data['v'][i])
256
+ if i < len(ce_data.get('o', [])) and ce_data['o'][i] is not None: o_tot += float(ce_data['o'][i])
257
+
258
+ # Put Accumulation
259
+ pe_inst = options_meta[s].get('PE')
260
+ if pe_inst:
261
+ pe_data = data_dict[pe_inst]
262
+ if i < len(pe_data.get('b', [])) and pe_data['b'][i] is not None: pb_tot += float(pe_data['b'][i])
263
+ if i < len(pe_data.get('s',[])) and pe_data['s'][i] is not None: ps_tot += float(pe_data['s'][i])
264
+ if i < len(pe_data.get('v', [])) and pe_data['v'][i] is not None: v_tot += float(pe_data['v'][i])
265
+ if i < len(pe_data.get('o', [])) and pe_data['o'][i] is not None: o_tot += float(pe_data['o'][i])
266
+
267
  records.append({
268
  "ts": base_ts + timedelta(seconds=i*10),
269
+ "price": p_ref,
270
+ "cb": cb_tot,
271
+ "cs": cs_tot,
272
+ "pb": pb_tot,
273
+ "ps": ps_tot,
274
+ "v": v_tot,
275
+ "o": o_tot
276
  })
277
+ except Exception as e:
278
+ print(f"Error parsing advanced row: {e}")
279
+ continue
280
 
281
  if not records:
282
+ return JSONResponse({"error": "No data found", "labels": [], "P":[]})
283
 
284
  df = pd.DataFrame(records)
285
  df.set_index('ts', inplace=True)
 
292
 
293
  resampled = df.resample(panda_tf).median().dropna()
294
 
295
+ # Return formatted JSON properly matching active mode
296
+ if not adv_mode:
297
+ response_data = {
298
+ "labels": resampled.index.strftime('%H:%M').tolist(),
299
+ "P": resampled['price'].tolist(),
300
+ "B": resampled['buy_qty'].tolist(),
301
+ "S": resampled['sell_qty'].tolist(),
302
+ "V": resampled['volume'].tolist(),
303
+ "OI": resampled['oi'].tolist()
304
+ }
305
+ else:
306
+ response_data = {
307
+ "labels": resampled.index.strftime('%H:%M').tolist(),
308
+ "P": resampled['price'].tolist(),
309
+ "CB": resampled['cb'].tolist(),
310
+ "CS": resampled['cs'].tolist(),
311
+ "PB": resampled['pb'].tolist(),
312
+ "PS": resampled['ps'].tolist(),
313
+ "V": resampled['v'].tolist(),
314
+ "OI": resampled['o'].tolist()
315
+ }
316
 
317
  return JSONResponse(response_data)
318
 
 
400
  padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
401
  }
402
  .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
403
+ .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; background:rgba(47,129,247,0.1); padding:2px 6px; border-radius:4px; }
404
  .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
405
 
406
  .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
 
433
  .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
434
  .color-input { padding: 0; width: 100%; border: none; background: none; height: 28px; cursor: pointer; }
435
 
436
+ /* TOOLTIP & INFO ICON */
437
+ .tooltip-container { position: relative; display: inline-flex; align-items: center; cursor: help; margin-left:8px; }
438
+ .tooltip-icon {
439
+ width: 14px; height: 14px; border-radius: 50%; background: var(--border-hover);
440
+ color: #fff; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold;
441
+ }
442
+ .tooltip-content {
443
+ visibility: hidden; width: 220px; background: var(--bg-panel); color: var(--text-primary); text-align: left;
444
+ border: 1px solid var(--border); border-radius: 6px; padding: 12px; position: absolute; z-index: 10;
445
+ bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity 0.2s;
446
+ font-size: 11px; font-weight: normal; font-family: var(--font-mono); box-shadow: 0 4px 12px rgba(0,0,0,0.5);
447
+ text-transform: none; line-height: 1.4;
448
+ }
449
+ .tooltip-container:hover .tooltip-content { visibility: visible; opacity: 1; }
450
+ optgroup { background: var(--bg-panel); color: var(--text-primary); font-weight: bold; }
451
  </style>
452
  </head>
453
  <body>
 
499
 
500
  <!-- Single Chart Config Mode -->
501
  <div id="cfgChartOnly">
502
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px;">
503
  <div class="form-section">
504
  <label>Symbol</label>
505
  <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
 
509
  <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
510
  </div>
511
  </div>
512
+
513
+ <!-- Mode Toggle & Instrument / ATM Options -->
514
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; padding: 12px; border: 1px solid var(--border); border-radius: 6px; background:rgba(255,255,255,0.02);">
515
+ <div class="form-section" style="margin-bottom:0;">
516
+ <label>Analysis Mode</label>
517
+ <select id="cfgMode" onchange="Config.onChartModeChange()" style="width:100%">
518
+ <option value="normal">Normal (Single)</option>
519
+ <option value="advanced">Advanced (ATM Options)</option>
520
+ </select>
521
+ </div>
522
+
523
+ <!-- Normal Mode: Instrument Selector -->
524
+ <div class="form-section" id="instBox" style="margin-bottom:0;">
525
+ <label>Instrument</label>
526
+ <select id="cfgInst" style="width:100%"></select>
527
+ </div>
528
+
529
+ <!-- Advanced Mode: ATM Range Input -->
530
+ <div class="form-section" id="atmBox" style="margin-bottom:0; display:none;">
531
+ <label>ATM +/- Strikes</label>
532
+ <input type="number" id="cfgAtm" value="5" min="1" max="25" style="width:100%">
533
+ </div>
534
  </div>
535
+
536
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
537
  <div class="form-section">
538
  <label>Timeframe</label>
 
553
  </div>
554
  </div>
555
  </div>
556
+ <div style="height:1px; background:var(--border); margin: 12px 0;"></div>
557
  </div>
558
 
559
  <!-- Common Series Config -->
560
  <div class="form-section">
561
+ <label style="display:flex; align-items:center;">
562
+ Plot Series Formulas
563
+ <div class="tooltip-container">
564
+ <div class="tooltip-icon">i</div>
565
+ <div class="tooltip-content">
566
+ <strong style="color:var(--accent)">Global Variables:</strong><br>
567
+ $P = Price (Spot/Future)<br>
568
+ $V = Total Volume<br>
569
+ $OI = Total Open Interest<br>
570
+ $B = Total Buy Qty<br>
571
+ $S = Total Sell Qty<br><br>
572
+ <strong style="color:var(--warning)">Advanced Mode Only:</strong><br>
573
+ $CB = Call Buy Qty<br>
574
+ $CS = Call Sell Qty<br>
575
+ $PB = Put Buy Qty<br>
576
+ $PS = Put Sell Qty
577
+ </div>
578
+ </div>
579
+ </label>
580
  <div id="cfgSeries"></div>
581
  <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary);" onclick="Config.addSeriesRow()">+ Add Line</button>
582
  </div>
 
591
 
592
  <script>
593
  const App = {
594
+ state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
595
+ defaults:[
596
  { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
597
  { formula: '$B', color: '#238636', label: 'Buy', axis: 'right' },
598
  { formula: '$S', color: '#da3633', label: 'Sell', axis: 'right' }
 
605
  try {
606
  const s = JSON.parse(saved);
607
  this.state = { ...this.state, ...s };
 
608
  if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
609
  } catch(e) { this.initDefault(); }
610
  } else {
611
  this.initDefault();
612
  }
613
 
 
614
  const dSel = document.getElementById('globalDate');
615
  if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
616
  else if(dSel.options.length) this.state.date = dSel.options[0].value;
 
623
 
624
  initDefault() {
625
  this.state.columns = 3;
626
+ this.state.layout = [[], [],[]];
627
  this.state.refreshRate = 15;
 
628
  for(let i=0; i<3; i++) {
629
  this.addChartInternal(i);
630
  this.addChartInternal(i);
 
648
  },
649
 
650
  save() {
 
651
  const cleanCharts = {};
652
  for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
653
 
 
671
  setLayout(n) {
672
  const all = this.state.layout.flat();
673
  this.state.columns = n;
674
+ this.state.layout = Array.from({length: n}, () =>[]);
675
  all.forEach((id, i) => this.state.layout[i % n].push(id));
676
  this.renderGrid();
677
  this.save();
678
  },
679
 
680
  renderGrid() {
 
681
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
682
 
683
  const container = document.getElementById('gridRoot');
684
+ container.innerHTML = '';
685
 
686
  this.state.layout.forEach((ids, colIdx) => {
687
  const col = document.createElement('div');
 
690
  const viewH = window.innerHeight - 52;
691
 
692
  ids.forEach(id => {
 
 
693
  if(this.state.charts[id] && this.state.charts[id].instance) {
694
  this.state.charts[id].instance.destroy();
695
  this.state.charts[id].instance = null;
696
  }
697
 
698
  const card = this.createCardDOM(id);
 
699
  if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
700
  else card.style.height = '300px';
701
 
 
720
  const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
721
  const isCfg = cfg && cfg.root;
722
 
723
+ let displayInst = cfg.instrument;
724
+ if(cfg.mode === 'advanced') {
725
+ displayInst = `ADV: ATM ±${cfg.atm_range}`;
726
+ }
727
+
728
  div.innerHTML = `
729
  <div class="card-header">
730
  <div class="card-info">
731
  <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
732
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
733
+ ${isCfg ? `<span class="inst-label">${displayInst}</span> <span class="card-meta">${cfg.expiry}</span>` : ''}
734
  </div>
735
  <div class="card-tools">
736
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
 
814
  headers: {'Content-Type': 'application/json'},
815
  body: JSON.stringify({
816
  date: App.state.date, root: cfg.root, expiry: cfg.expiry,
817
+ instrument: cfg.instrument || "", timeframe: cfg.timeframe,
818
+ start_time: cfg.start, end_time: endTime,
819
+ adv_mode: cfg.mode === 'advanced',
820
+ atm_range: cfg.atm_range || 5
821
  })
822
  });
823
  const data = await res.json();
 
838
 
839
  const datasets = cfg.series.map(s => {
840
  const vals = data.labels.map((_, i) => {
841
+ let f = s.formula
842
+ .replace(/\$OI/g, data.OI ? (data.OI[i]||0) : 0)
843
+ .replace(/\$CB/g, data.CB ? (data.CB[i]||0) : 0)
844
+ .replace(/\$CS/g, data.CS ? (data.CS[i]||0) : 0)
845
+ .replace(/\$PB/g, data.PB ? (data.PB[i]||0) : 0)
846
+ .replace(/\$PS/g, data.PS ? (data.PS[i]||0) : 0)
847
+ .replace(/\$P/g, data.P ? (data.P[i]||0) : 0)
848
+ .replace(/\$V/g, data.V ? (data.V[i]||0) : 0)
849
+ .replace(/\$B/g, data.B ? (data.B[i]||0) : 0)
850
+ .replace(/\$S/g, data.S ? (data.S[i]||0) : 0);
851
  try { return new Function('return '+f)(); } catch{ return null; }
852
  });
853
  return {
 
865
  const ctx = document.getElementById(`canvas-${id}`);
866
  if(!ctx) return;
867
 
 
868
  if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
869
  App.state.charts[id].instance.destroy();
870
  App.state.charts[id].instance = null;
 
915
  rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
916
 
917
  const cfg = App.state.charts[id].config;
918
+
919
+ // Mode Setting
920
+ document.getElementById('cfgMode').value = cfg.mode || 'normal';
921
+ document.getElementById('cfgAtm').value = cfg.atm_range || 5;
922
+ this.onChartModeChange(false); // Update UI visibility, but don't reset series formulas
923
+
924
  document.getElementById('cfgTF').value = cfg.timeframe || '1min';
925
  document.getElementById('cfgStart').value = cfg.start || '09:15';
926
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
 
947
 
948
  close() { document.getElementById('configModal').style.display = 'none'; },
949
 
950
+ onChartModeChange(resetSeries = true) {
951
+ const mode = document.getElementById('cfgMode').value;
952
+ if(mode === 'advanced') {
953
+ document.getElementById('instBox').style.display = 'none';
954
+ document.getElementById('atmBox').style.display = 'block';
955
+ if(resetSeries) {
956
+ // Default formula mapping as requested by user
957
+ this.renderSeries([
958
+ { formula: '$CB + $PS', color: '#238636', label: 'Buyers', axis: 'left' },
959
+ { formula: '$CS + $PB', color: '#da3633', label: 'Sellers', axis: 'left' },
960
+ { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'right' }
961
+ ]);
962
+ }
963
+ } else {
964
+ document.getElementById('instBox').style.display = 'block';
965
+ document.getElementById('atmBox').style.display = 'none';
966
+ if(resetSeries) {
967
+ this.renderSeries(App.defaults);
968
+ }
969
+ }
970
+ },
971
+
972
  async onRootChange(preExp, preInst) {
973
  const root = document.getElementById('cfgRoot').value;
974
  const eSel = document.getElementById('cfgExp');
 
989
  const exp = document.getElementById('cfgExp').value;
990
  const iSel = document.getElementById('cfgInst');
991
  iSel.innerHTML = '<option>Loading...</option>';
992
+
993
  const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
994
  const insts = await res.json();
995
+
996
+ // Split instruments neatly for Normal Mode
997
+ const futEq = insts.filter(i => !i.endswith('CE') && !i.endswith('PE'));
998
+ const opts = insts.filter(i => i.endswith('CE') || i.endswith('PE'));
999
+
1000
+ let html = '<option value="">-- Select --</option>';
1001
+ if(futEq.length) {
1002
+ html += '<optgroup label="Futures & Equity">';
1003
+ html += futEq.map(i => `<option>${i}</option>`).join('');
1004
+ html += '</optgroup>';
1005
+ }
1006
+ if(opts.length) {
1007
+ html += '<optgroup label="Options">';
1008
+ html += opts.map(i => `<option>${i}</option>`).join('');
1009
+ html += '</optgroup>';
1010
+ }
1011
+
1012
+ iSel.innerHTML = html;
1013
  if(preInst && insts.includes(preInst)) iSel.value = preInst;
1014
  },
1015
 
 
1055
  else {
1056
  const root = document.getElementById('cfgRoot').value;
1057
  const exp = document.getElementById('cfgExp').value;
1058
+ const mode = document.getElementById('cfgMode').value;
1059
  const inst = document.getElementById('cfgInst').value;
1060
+ const atm = parseInt(document.getElementById('cfgAtm').value) || 5;
1061
+
1062
+ if(!root || !exp || (mode === 'normal' && !inst)) return alert("Incomplete Details");
1063
 
1064
  App.state.charts[this.tid].config = {
1065
  root, expiry: exp, instrument: inst,
1066
+ mode: mode, atm_range: atm,
1067
  timeframe: document.getElementById('cfgTF').value,
1068
  start: document.getElementById('cfgStart').value,
1069
  end: document.getElementById('cfgEnd').value,