topsecrettraders commited on
Commit
762b8d3
·
verified ·
1 Parent(s): cdd364d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -123
app.py CHANGED
@@ -32,7 +32,7 @@ def sb_get(endpoint: str, params: dict = None):
32
  return r.json()
33
  except Exception as e:
34
  print(f"DB Error GET ({endpoint}): {e}")
35
- return[]
36
 
37
  def sb_rpc(func_name: str, params: dict = None):
38
  if params is None:
@@ -44,7 +44,7 @@ def sb_rpc(func_name: str, params: dict = None):
44
  return r.json()
45
  except Exception as e:
46
  print(f"DB Error RPC ({func_name}): {e}")
47
- return[]
48
 
49
  # ==========================================
50
  # 3. API ROUTES - METADATA
@@ -74,7 +74,7 @@ def get_auto_config(date: str, root: str):
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:
79
  valid_expiries = all_expiries
80
 
@@ -110,8 +110,8 @@ def get_instruments(date: str, root: str, expiry: str):
110
  data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
111
  instruments = [row['instrument_name'] for row in data]
112
 
113
- spot_fut =[]
114
- options =[]
115
  for i in instruments:
116
  if i.endswith('CE') or i.endswith('PE'):
117
  options.append(i)
@@ -141,9 +141,11 @@ async def fetch_series(
141
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
142
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
143
 
 
144
  if required_vars is None:
145
- required_vars =["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
146
 
 
147
  required_vars = list(set(required_vars) | {"P"})
148
 
149
  if mode == "advanced":
@@ -151,7 +153,7 @@ async def fetch_series(
151
  instruments = [r['instrument_name'] for r in inst_data]
152
 
153
  if not instruments:
154
- return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P":[]})
155
 
156
  ref_instrument = instrument
157
  if not ref_instrument:
@@ -162,19 +164,8 @@ async def fetch_series(
162
  elif fut_keys:
163
  ref_instrument = fut_keys[0]
164
  else:
165
- others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
166
  ref_instrument = others[0] if others else instruments[0]
167
-
168
- # Build Accurate Strike Map locally using Fyers nomenclature length rules
169
- strike_map = {}
170
- for inst in instruments:
171
- if inst.endswith('CE') or inst.endswith('PE'):
172
- try:
173
- prefix = inst.split(':')[1] if ':' in inst else inst
174
- s = prefix[len(root):-2]
175
- if len(s) > 5: strike_map[inst] = float(s[5:])
176
- except:
177
- pass
178
 
179
  rpc_name = "get_advanced_chart_data"
180
  base_params = {
@@ -182,12 +173,12 @@ async def fetch_series(
182
  "p_expiry": expiry,
183
  "p_ref_instrument": ref_instrument,
184
  "p_atm_range": atm_range,
185
- "p_strike_map": strike_map
186
  }
187
 
188
  else:
189
  if not instrument:
190
- return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
191
 
192
  rpc_name = "get_normal_chart_data"
193
  base_params = {
@@ -197,10 +188,8 @@ async def fetch_series(
197
  }
198
 
199
  # =========================================================
200
- # CHUNKING: Shorter chunking for heavy DB loads (Advanced mode)
201
  # =========================================================
202
- chunk_delta = timedelta(hours=1, minutes=45) if mode == "advanced" else timedelta(hours=2)
203
-
204
  async def fetch_chunk(start_dt, end_dt):
205
  params = base_params.copy()
206
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
@@ -208,24 +197,24 @@ async def fetch_series(
208
  loop = asyncio.get_event_loop()
209
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
210
 
211
- tasks =[]
212
  curr = t_start_dt
213
  while curr <= t_end_dt:
214
- nxt = curr + chunk_delta - timedelta(seconds=1)
215
  if nxt > t_end_dt:
216
  nxt = t_end_dt
217
  tasks.append(fetch_chunk(curr, nxt))
218
- curr += chunk_delta
219
 
220
  chunk_results = await asyncio.gather(*tasks)
221
 
222
- all_records =[]
223
  for res in chunk_results:
224
  if isinstance(res, list):
225
  all_records.extend(res)
226
 
227
  if not all_records:
228
- return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
229
 
230
  # =========================================================
231
  # DATA PROCESSING
@@ -250,44 +239,114 @@ async def fetch_series(
250
  df.set_index('ts', inplace=True)
251
  df.sort_index(inplace=True)
252
 
 
253
  tf_map = {
254
  "1min": "1min", "3min": "3min", "5min": "5min",
255
  "15min": "15min", "30min": "30min", "1hour": "1h"
256
  }
257
  panda_tf = tf_map.get(timeframe, "1min")
258
 
 
 
259
  agg_dict = {
260
  "P": "last",
261
- "V": "last", # Fyers Volume is cumulative -> Must extract last snapshot
262
- "OI": "last",
263
- "B": "last",
264
- "S": "last",
265
- "CB": "last",
266
- "CS": "last",
267
- "PB": "last",
268
- "PS": "last",
269
- "details": "last"
270
  }
 
 
 
 
 
 
 
 
271
 
272
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
273
  resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
274
 
 
275
  response_data = {
276
  "labels": resampled.index.strftime('%H:%M').tolist()
277
  }
278
 
279
- for var_key in["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS", "details"]:
280
  if var_key in required_vars and var_key in resampled.columns:
281
- if var_key == "details":
282
- response_data[var_key] = resampled[var_key].tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  else:
284
- response_data[var_key] =[round(float(v), 2) for v in resampled[var_key].tolist()]
 
285
 
286
  return JSONResponse(response_data)
287
 
288
 
289
  # ==========================================
290
- # 5. FRONTEND TEMPLATE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  # ==========================================
292
 
293
  HTML_TEMPLATE = """
@@ -390,6 +449,7 @@ HTML_TEMPLATE = """
390
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
391
  }
392
  .modal.wide { width: 820px; }
 
393
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
394
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
395
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
@@ -464,9 +524,86 @@ HTML_TEMPLATE = """
464
  .breakdown-table th:first-child { text-align: left; }
465
  .breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
466
  .breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
 
467
  .val-pos { color: #3fb950; }
468
  .val-neg { color: #f85149; }
469
  .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  </style>
471
  </head>
472
  <body>
@@ -478,7 +615,7 @@ HTML_TEMPLATE = """
478
  <div style="position:relative;">
479
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
480
  <option value="LIVE">LIVE</option>
481
- <option value="HIST">HISTORY</option>
482
  </select>
483
  <div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
484
  </div>
@@ -560,7 +697,7 @@ HTML_TEMPLATE = """
560
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
561
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
562
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
563
- Aggregates all CE/PE within ATM ± this many strikes.
564
  </div>
565
  </div>
566
  </div>
@@ -616,17 +753,26 @@ HTML_TEMPLATE = """
616
  </div>
617
  </div>
618
 
619
- <!-- ===== BREAKDOWN MODAL ===== -->
620
  <div class="modal-backdrop" id="breakdownModal">
621
- <div class="modal wide">
622
  <div class="modal-head">
623
  <span id="bdTitle">Calculation Breakdown</span>
624
  <button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
625
  </div>
626
  <div class="modal-body" style="padding:12px;">
627
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
628
- <div class="breakdown-scroll">
629
- <table class="breakdown-table" id="bdTable"></table>
 
 
 
 
 
 
 
 
 
630
  </div>
631
  </div>
632
  </div>
@@ -655,14 +801,16 @@ function evalFormula(formula, vars) {
655
  try { return new Function('return ' + f)(); } catch { return null; }
656
  }
657
 
 
658
  function extractRequiredVars(seriesList) {
659
- const vars = new Set(['P']);
660
  seriesList.forEach(s => {
661
  const f = s.formula || '';
662
  if (f.includes('$CB')) vars.add('CB');
663
  if (f.includes('$CS')) vars.add('CS');
664
  if (f.includes('$PB')) vars.add('PB');
665
  if (f.includes('$PS')) vars.add('PS');
 
666
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
667
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
668
  if (f.includes('$V')) vars.add('V');
@@ -675,15 +823,15 @@ function extractRequiredVars(seriesList) {
675
  // CUSTOM DROPDOWN
676
  // ===================================================
677
  const CustomDD = {
678
- data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
679
- recent: { cfgRoot:[] },
680
 
681
  init() {
682
  const saved = localStorage.getItem('dc_recent_symbols');
683
  if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
684
  },
685
 
686
- clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
687
 
688
  set(id, items, groupName = null) {
689
  if (!groupName)
@@ -756,47 +904,51 @@ const CustomDD = {
756
  // APP STATE
757
  // ===================================================
758
  const App = {
759
- state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
760
 
 
761
  presets: {
762
- "Basic Flow (Fut/Eq)":[
 
763
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
764
  { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
765
  { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
766
  ],
767
- "Net Flow (Fut/Eq)":[
768
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
769
  { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
770
  ],
771
- "Call Flow (Options)":[
 
772
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
773
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
774
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
775
  ],
776
- "Put Flow (Options)":[
777
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
778
  { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
779
  { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
780
  ],
781
- "Aggregated Flow (Options)":[
782
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
783
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
784
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
785
  ],
786
- "Net Flow (Options)":[
787
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
788
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
789
  ],
790
- "Price & Flows (Advanced)":[
 
791
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
792
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
793
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
794
  ],
795
- "Net Flow (Advanced)":[
796
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
797
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
798
  ],
799
- "Components (Advanced)":[
800
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
801
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
802
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
@@ -835,7 +987,7 @@ const App = {
835
 
836
  initDefault() {
837
  this.state.columns = 3;
838
- this.state.layout = [[], [],[]];
839
  this.state.refreshRate = 15;
840
  for (let i = 0; i < 3; i++) {
841
  this.addChartInternal(i);
@@ -880,7 +1032,7 @@ const App = {
880
  setLayout(n) {
881
  const all = this.state.layout.flat();
882
  this.state.columns = n;
883
- this.state.layout = Array.from({ length: n }, () =>[]);
884
  all.forEach((id, i) => this.state.layout[i % n].push(id));
885
  this.renderGrid(); this.save();
886
  },
@@ -999,10 +1151,8 @@ const ChartLogic = {
999
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1000
  }
1001
 
1002
- const requiredVars = extractRequiredVars(cfg.series ||[]);
1003
- if (cfg.chartMode === 'advanced') {
1004
- requiredVars.push('details');
1005
- }
1006
 
1007
  try {
1008
  const res = await fetch('/api/fetch_series', {
@@ -1032,13 +1182,14 @@ const ChartLogic = {
1032
  return;
1033
  }
1034
 
 
1035
  chart.lastData = data;
1036
 
1037
  const body = document.getElementById(`body-${id}`);
1038
  if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1039
  if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
1040
 
1041
- const datasets = (cfg.series ||[]).map(s => {
1042
  const vals = data.labels.map((_, i) => {
1043
  const vars = {
1044
  P: safeVal(data, 'P', i),
@@ -1087,7 +1238,7 @@ const ChartLogic = {
1087
  return ds;
1088
  });
1089
 
1090
- this.render(id, data.labels, datasets, cfg.series ||[]);
1091
 
1092
  } catch(e) { console.error('ChartLogic.plot error:', e); }
1093
  },
@@ -1175,9 +1326,22 @@ const ChartLogic = {
1175
  };
1176
 
1177
  // ===================================================
1178
- // BREAKDOWN TABLE
1179
  // ===================================================
1180
  const Breakdown = {
 
 
 
 
 
 
 
 
 
 
 
 
 
1181
  open(id) {
1182
  const chart = App.state.charts[id];
1183
  if (!chart || !chart.lastData || !chart.config.series) {
@@ -1188,19 +1352,49 @@ const Breakdown = {
1188
  const data = chart.lastData;
1189
  const cfg = chart.config;
1190
  const series = cfg.series;
1191
- const hasDetails = data.details && data.details.some(d => d);
1192
 
1193
- const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1194
  const presentVars = allVarKeys.filter(k =>
1195
  data[k] && data[k].some(v => v && v !== 0)
1196
  );
1197
 
 
1198
  let thead = '<thead><tr><th>Time</th>';
1199
- if (hasDetails) thead += '<th>ATM Details</th>';
 
 
 
1200
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1201
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1202
  thead += '</tr></thead>';
1203
 
 
1204
  let tbody = '<tbody>';
1205
  data.labels.forEach((lbl, i) => {
1206
  const vars = {
@@ -1211,25 +1405,22 @@ const Breakdown = {
1211
  OI: safeVal(data, 'OI', i)
1212
  };
1213
 
1214
- let detailsBtn = '';
1215
- if (hasDetails) {
1216
- const det = data.details[i];
1217
- if (det && det.atm) {
1218
- const detId = `det-${id}-${i}`;
1219
- detailsBtn = `<td><button class="tool-btn" style="width:auto; padding:0 6px; font-size:10px; font-weight:bold; color:var(--accent);" onclick="Breakdown.toggleDetails('${detId}')">ATM: ${det.atm}</button></td>`;
1220
- } else {
1221
- detailsBtn = `<td>-</td>`;
1222
- }
1223
  }
1224
 
1225
- tbody += `<tr><td>${lbl}</td>${detailsBtn}`;
1226
-
1227
  presentVars.forEach(k => {
1228
  const v = vars[k];
1229
  const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1230
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1231
  });
1232
 
 
1233
  series.forEach(s => {
1234
  const result = evalFormula(s.formula, vars);
1235
  const rv = result !== null ? result : 0;
@@ -1244,49 +1435,146 @@ const Breakdown = {
1244
  }
1245
  tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
1246
  });
 
1247
  tbody += '</tr>';
 
 
1248
 
1249
- if (hasDetails) {
1250
- const det = data.details[i];
1251
- if (det && det.strikes) {
1252
- let subTable = `<table style="width:100%; border-left:2px solid var(--accent); margin-left:10px; background:var(--bg-body); border-radius:4px; font-size:10px;">
1253
- <tr style="color:var(--text-secondary);"><th style="text-align:left;">Strike</th><th>CB</th><th>CS</th><th>PB</th><th>PS</th></tr>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1254
 
1255
- const strikes = Object.keys(det.strikes).map(Number).sort((a,b) => a-b);
1256
- strikes.forEach(st => {
1257
- const rowData = det.strikes[st];
1258
- subTable += `<tr style="border-bottom: 1px solid var(--border);">
1259
- <td style="text-align:left; color:${st === det.atm ? 'var(--accent)' : 'inherit'}; font-weight:${st === det.atm ? 'bold' : 'normal'}">${st}${st === det.atm ? ' (ATM)' : ''}</td>
1260
- <td class="${rowData.cb > 0 ? 'val-pos':''}">${rowData.cb.toFixed(2)}</td>
1261
- <td class="${rowData.cs > 0 ? 'val-neg':''}">${rowData.cs.toFixed(2)}</td>
1262
- <td class="${rowData.pb > 0 ? 'val-pos':''}">${rowData.pb.toFixed(2)}</td>
1263
- <td class="${rowData.ps > 0 ? 'val-neg':''}">${rowData.ps.toFixed(2)}</td>
1264
- </tr>`;
1265
- });
1266
- subTable += `</table>`;
 
 
 
 
1267
 
1268
- const colCount = 1 + (hasDetails ? 1 : 0) + presentVars.length + series.length;
1269
- tbody += `<tr id="det-${id}-${i}" style="display:none; background:rgba(0,0,0,0.2);">
1270
- <td colspan="${colCount}" style="padding:10px;">
1271
- ${subTable}
1272
- </td>
1273
- </tr>`;
1274
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1275
  }
1276
-
 
 
1277
  });
1278
- tbody += '</tbody>';
1279
-
1280
- document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1281
- document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1282
- document.getElementById('bdTable').innerHTML = thead + tbody;
1283
- document.getElementById('breakdownModal').style.display = 'flex';
1284
  },
1285
-
1286
- toggleDetails(rowId) {
1287
- const row = document.getElementById(rowId);
1288
- if (row.style.display === 'none') row.style.display = 'table-row';
1289
- else row.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
1290
  }
1291
  };
1292
 
@@ -1305,6 +1593,7 @@ const Config = {
1305
  document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1306
  document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1307
  this.updatePresetsDropdown();
 
1308
  const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
1309
  this.loadPreset(defPreset);
1310
  },
@@ -1333,7 +1622,7 @@ const Config = {
1333
 
1334
  deletePreset() {
1335
  const name = document.getElementById('cfgPresetsDropdown').value;
1336
- const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
1337
  'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
1338
  'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
1339
  if (locked.includes(name)) return alert('Cannot delete default models.');
@@ -1482,6 +1771,7 @@ const Config = {
1482
  <input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
1483
  </div>
1484
  `;
 
1485
  const solidColor = r.querySelector('.s-color1');
1486
  const condTrueColor = r.querySelector('.s-color1-cond');
1487
  solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
 
32
  return r.json()
33
  except Exception as e:
34
  print(f"DB Error GET ({endpoint}): {e}")
35
+ return []
36
 
37
  def sb_rpc(func_name: str, params: dict = None):
38
  if params is None:
 
44
  return r.json()
45
  except Exception as e:
46
  print(f"DB Error RPC ({func_name}): {e}")
47
+ return []
48
 
49
  # ==========================================
50
  # 3. API ROUTES - METADATA
 
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:
79
  valid_expiries = all_expiries
80
 
 
110
  data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
111
  instruments = [row['instrument_name'] for row in data]
112
 
113
+ spot_fut = []
114
+ options = []
115
  for i in instruments:
116
  if i.endswith('CE') or i.endswith('PE'):
117
  options.append(i)
 
141
  t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
142
  t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
143
 
144
+ # Default: all variables if not specified
145
  if required_vars is None:
146
+ required_vars = ["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
147
 
148
+ # Always include P (price)
149
  required_vars = list(set(required_vars) | {"P"})
150
 
151
  if mode == "advanced":
 
153
  instruments = [r['instrument_name'] for r in inst_data]
154
 
155
  if not instruments:
156
+ return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P": []})
157
 
158
  ref_instrument = instrument
159
  if not ref_instrument:
 
164
  elif fut_keys:
165
  ref_instrument = fut_keys[0]
166
  else:
167
+ others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
168
  ref_instrument = others[0] if others else instruments[0]
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  rpc_name = "get_advanced_chart_data"
171
  base_params = {
 
173
  "p_expiry": expiry,
174
  "p_ref_instrument": ref_instrument,
175
  "p_atm_range": atm_range,
176
+ "p_strike_map": {}
177
  }
178
 
179
  else:
180
  if not instrument:
181
+ return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P": []})
182
 
183
  rpc_name = "get_normal_chart_data"
184
  base_params = {
 
188
  }
189
 
190
  # =========================================================
191
+ # CHUNKING: 2-hour chunks fetched in parallel
192
  # =========================================================
 
 
193
  async def fetch_chunk(start_dt, end_dt):
194
  params = base_params.copy()
195
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
 
197
  loop = asyncio.get_event_loop()
198
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
199
 
200
+ tasks = []
201
  curr = t_start_dt
202
  while curr <= t_end_dt:
203
+ nxt = curr + timedelta(hours=2) - timedelta(seconds=1)
204
  if nxt > t_end_dt:
205
  nxt = t_end_dt
206
  tasks.append(fetch_chunk(curr, nxt))
207
+ curr += timedelta(hours=2)
208
 
209
  chunk_results = await asyncio.gather(*tasks)
210
 
211
+ all_records = []
212
  for res in chunk_results:
213
  if isinstance(res, list):
214
  all_records.extend(res)
215
 
216
  if not all_records:
217
+ return JSONResponse({"error": "No valid data found in range.", "labels": [], "P": []})
218
 
219
  # =========================================================
220
  # DATA PROCESSING
 
239
  df.set_index('ts', inplace=True)
240
  df.sort_index(inplace=True)
241
 
242
+ # Resample
243
  tf_map = {
244
  "1min": "1min", "3min": "3min", "5min": "5min",
245
  "15min": "15min", "30min": "30min", "1hour": "1h"
246
  }
247
  panda_tf = tf_map.get(timeframe, "1min")
248
 
249
+ # KEY FIX: Use 'last' for quantity fields — these are cumulative running totals.
250
+ # The LAST value in any period is the most current/accurate snapshot.
251
  agg_dict = {
252
  "P": "last",
253
+ "V": "sum", # Volume: summing new volume per candle makes sense
254
+ "OI": "last", # OI: last value
255
+ "B": "last", # Buy qty cumulative total → last value
256
+ "S": "last", # Sell qty cumulative total → last value
257
+ "CB": "last", # Call Buy → last value
258
+ "CS": "last", # Call Sell → last value
259
+ "PB": "last", # Put Buy → last value
260
+ "PS": "last", # Put Sell → last value
 
261
  }
262
+
263
+ # Include ATM strike and breakdown data if available (for advanced mode)
264
+ if "atm_strike" in df.columns:
265
+ agg_dict["atm_strike"] = "last"
266
+ if "included_strikes" in df.columns:
267
+ agg_dict["included_strikes"] = "last"
268
+ if "breakdown_json" in df.columns:
269
+ agg_dict["breakdown_json"] = "last"
270
 
271
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
272
  resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
273
 
274
+ # Build response: only include requested variables (reduces egress)
275
  response_data = {
276
  "labels": resampled.index.strftime('%H:%M').tolist()
277
  }
278
 
279
+ for var_key in ["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
280
  if var_key in required_vars and var_key in resampled.columns:
281
+ response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
282
+ # Omit entirely if not requested — JS handles missing keys gracefully
283
+
284
+ # Include ATM strike and breakdown data for advanced mode
285
+ if "atm_strike" in resampled.columns:
286
+ response_data["atm_strike"] = [round(float(v), 2) if pd.notna(v) else None for v in resampled["atm_strike"].tolist()]
287
+ if "included_strikes" in resampled.columns:
288
+ response_data["included_strikes"] = resampled["included_strikes"].tolist()
289
+ if "breakdown_json" in resampled.columns:
290
+ # Convert JSONB records to list of dicts
291
+ breakdown_data = []
292
+ for bd in resampled["breakdown_json"].tolist():
293
+ if pd.notna(bd) and bd:
294
+ if isinstance(bd, str):
295
+ import json
296
+ try:
297
+ breakdown_data.append(json.loads(bd))
298
+ except:
299
+ breakdown_data.append([])
300
+ else:
301
+ breakdown_data.append(bd)
302
  else:
303
+ breakdown_data.append([])
304
+ response_data["breakdown_json"] = breakdown_data
305
 
306
  return JSONResponse(response_data)
307
 
308
 
309
  # ==========================================
310
+ # 5. NEW API: STRIKE BREAKDOWN FOR SPECIFIC TIMESTAMP
311
+ # ==========================================
312
+
313
+ @app.post("/api/strike_breakdown")
314
+ async def get_strike_breakdown(
315
+ date: str = Body(...),
316
+ root: str = Body(...),
317
+ expiry: str = Body(...),
318
+ ref_instrument: str = Body(...),
319
+ atm_range: int = Body(5),
320
+ timestamp: str = Body(...)
321
+ ):
322
+ """
323
+ Get detailed strike breakdown for a specific timestamp.
324
+ Shows all strikes with their ITM/OTM status and individual values.
325
+ """
326
+ root = root.upper()
327
+
328
+ # Parse timestamp
329
+ ts_dt = pd.to_datetime(f"{date} {timestamp}")
330
+
331
+ result = sb_rpc("get_strike_breakdown", {
332
+ "p_root": root,
333
+ "p_expiry": expiry,
334
+ "p_timestamp": ts_dt.strftime("%Y-%m-%d %H:%M:%S"),
335
+ "p_ref_instrument": ref_instrument,
336
+ "p_atm_range": atm_range
337
+ })
338
+
339
+ if not result:
340
+ return JSONResponse({"error": "No breakdown data found", "strikes": []})
341
+
342
+ return JSONResponse({
343
+ "timestamp": timestamp,
344
+ "strikes": result
345
+ })
346
+
347
+
348
+ # ==========================================
349
+ # 6. FRONTEND TEMPLATE
350
  # ==========================================
351
 
352
  HTML_TEMPLATE = """
 
449
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
450
  }
451
  .modal.wide { width: 820px; }
452
+ .modal.extra-wide { width: 95%; max-width: 1400px; }
453
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
454
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
455
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
 
524
  .breakdown-table th:first-child { text-align: left; }
525
  .breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
526
  .breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
527
+ .breakdown-table tr:hover td { background: var(--bg-panel); }
528
  .val-pos { color: #3fb950; }
529
  .val-neg { color: #f85149; }
530
  .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
531
+
532
+ /* STRIKE BREAKDOWN STYLES */
533
+ .strike-row {
534
+ display: flex; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border);
535
+ align-items: center; font-family: var(--font-mono); font-size: 11px;
536
+ }
537
+ .strike-row:hover { background: var(--bg-panel); }
538
+ .strike-row.itm { background: rgba(47,129,247,0.05); }
539
+ .strike-row.otm { background: rgba(0,0,0,0); }
540
+ .strike-row.atm { background: rgba(35,134,54,0.1); border-left: 3px solid var(--success); }
541
+ .strike-row.not-included { opacity: 0.4; }
542
+ .strike-label {
543
+ min-width: 100px; display: flex; gap: 6px; align-items: center;
544
+ }
545
+ .strike-badge {
546
+ font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600;
547
+ }
548
+ .badge-itm { background: var(--accent); color: white; }
549
+ .badge-otm { background: var(--bg-panel); color: var(--text-secondary); border: 1px solid var(--border); }
550
+ .badge-atm { background: var(--success); color: white; }
551
+ .strike-values {
552
+ display: flex; gap: 16px; flex: 1; justify-content: flex-end;
553
+ }
554
+ .strike-value-group {
555
+ display: flex; flex-direction: column; min-width: 60px;
556
+ }
557
+ .strike-value-label { font-size: 9px; color: var(--text-secondary); }
558
+ .strike-value-num { font-weight: 600; }
559
+
560
+ /* MINUTE BREAKDOWN ACCORDION */
561
+ .minute-row {
562
+ border-bottom: 1px solid var(--border);
563
+ }
564
+ .minute-header {
565
+ display: flex; align-items: center; padding: 8px 12px; cursor: pointer;
566
+ background: var(--bg-card); transition: background 0.2s;
567
+ }
568
+ .minute-header:hover { background: var(--bg-panel); }
569
+ .minute-header.expanded { background: var(--bg-panel); border-bottom: 1px solid var(--border); }
570
+ .minute-time { font-weight: 600; min-width: 60px; }
571
+ .minute-atm { color: var(--accent); font-family: var(--font-mono); min-width: 100px; }
572
+ .minute-summary { flex: 1; display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px; }
573
+ .expand-icon {
574
+ width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
575
+ color: var(--text-secondary); transition: transform 0.2s;
576
+ }
577
+ .expand-icon.expanded { transform: rotate(90deg); }
578
+ .minute-detail {
579
+ display: none; padding: 12px; background: var(--bg-body);
580
+ border-bottom: 1px solid var(--border);
581
+ }
582
+ .minute-detail.visible { display: block; }
583
+
584
+ /* STRIKE DETAIL GRID */
585
+ .strike-detail-grid {
586
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
587
+ gap: 8px;
588
+ }
589
+ .strike-detail-card {
590
+ background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px;
591
+ padding: 10px; font-family: var(--font-mono); font-size: 11px;
592
+ }
593
+ .strike-detail-card.ce { border-left: 3px solid #238636; }
594
+ .strike-detail-card.pe { border-left: 3px solid #da3633; }
595
+ .strike-detail-card.itm { background: rgba(47,129,247,0.08); }
596
+ .strike-detail-card.otm { background: transparent; }
597
+ .strike-detail-card.atm { background: rgba(35,134,54,0.1); }
598
+ .strike-detail-header {
599
+ display: flex; justify-content: space-between; margin-bottom: 8px;
600
+ font-weight: 600;
601
+ }
602
+ .strike-detail-values {
603
+ display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
604
+ }
605
+ .detail-val { color: var(--text-secondary); }
606
+ .detail-num { text-align: right; }
607
  </style>
608
  </head>
609
  <body>
 
615
  <div style="position:relative;">
616
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
617
  <option value="LIVE">LIVE</option>
618
+ <option value="HISTORY">HISTORY</option>
619
  </select>
620
  <div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
621
  </div>
 
697
  <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
698
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
699
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
700
+ Aggregates all CE/PE within ATM ± N strikes. Total 2N strikes included.
701
  </div>
702
  </div>
703
  </div>
 
753
  </div>
754
  </div>
755
 
756
+ <!-- ===== BREAKDOWN MODAL (ENHANCED) ===== -->
757
  <div class="modal-backdrop" id="breakdownModal">
758
+ <div class="modal extra-wide">
759
  <div class="modal-head">
760
  <span id="bdTitle">Calculation Breakdown</span>
761
  <button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
762
  </div>
763
  <div class="modal-body" style="padding:12px;">
764
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
765
+ <div style="display:flex; gap:8px; margin-bottom:12px;">
766
+ <button id="bdTabSummary" class="primary" style="font-size:11px;" onclick="Breakdown.showTab('summary')">Summary</button>
767
+ <button id="bdTabDetail" style="font-size:11px; background:var(--bg-panel);" onclick="Breakdown.showTab('detail')">Strike Detail</button>
768
+ </div>
769
+ <div id="bdSummaryTab">
770
+ <div class="breakdown-scroll">
771
+ <table class="breakdown-table" id="bdTable"></table>
772
+ </div>
773
+ </div>
774
+ <div id="bdDetailTab" style="display:none;">
775
+ <div class="breakdown-scroll" id="bdDetailContent"></div>
776
  </div>
777
  </div>
778
  </div>
 
801
  try { return new Function('return ' + f)(); } catch { return null; }
802
  }
803
 
804
+ // Extract which variables are used in a list of series formulas
805
  function extractRequiredVars(seriesList) {
806
+ const vars = new Set(['P']); // Always need price
807
  seriesList.forEach(s => {
808
  const f = s.formula || '';
809
  if (f.includes('$CB')) vars.add('CB');
810
  if (f.includes('$CS')) vars.add('CS');
811
  if (f.includes('$PB')) vars.add('PB');
812
  if (f.includes('$PS')) vars.add('PS');
813
+ // Check $B / $S but NOT $CB/$CS/$PB/$PS (already handled above)
814
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
815
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
816
  if (f.includes('$V')) vars.add('V');
 
823
  // CUSTOM DROPDOWN
824
  // ===================================================
825
  const CustomDD = {
826
+ data: { cfgRoot: [], cfgExp: [], cfgInst: [] },
827
+ recent: { cfgRoot: [] },
828
 
829
  init() {
830
  const saved = localStorage.getItem('dc_recent_symbols');
831
  if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
832
  },
833
 
834
+ clear(id) { this.data[id] = []; document.getElementById(id).value = ''; },
835
 
836
  set(id, items, groupName = null) {
837
  if (!groupName)
 
904
  // APP STATE
905
  // ===================================================
906
  const App = {
907
+ state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
908
 
909
+ // ---- DEFAULT PRESETS (organized by use case) ----
910
  presets: {
911
+ // --- Normal: Futures / Equity / Spot ---
912
+ "Basic Flow (Fut/Eq)": [
913
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
914
  { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
915
  { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
916
  ],
917
+ "Net Flow (Fut/Eq)": [
918
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
919
  { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
920
  ],
921
+ // --- Normal: Options ---
922
+ "Call Flow (Options)": [
923
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
924
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
925
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
926
  ],
927
+ "Put Flow (Options)": [
928
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
929
  { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
930
  { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
931
  ],
932
+ "Aggregated Flow (Options)": [
933
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
934
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
935
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
936
  ],
937
+ "Net Flow (Options)": [
938
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
939
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
940
  ],
941
+ // --- Advanced: Options (aggregated across ATM range) ---
942
+ "Price & Flows (Advanced)": [
943
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
944
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
945
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
946
  ],
947
+ "Net Flow (Advanced)": [
948
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
949
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
950
  ],
951
+ "Components (Advanced)": [
952
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
953
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
954
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
 
987
 
988
  initDefault() {
989
  this.state.columns = 3;
990
+ this.state.layout = [[], [], []];
991
  this.state.refreshRate = 15;
992
  for (let i = 0; i < 3; i++) {
993
  this.addChartInternal(i);
 
1032
  setLayout(n) {
1033
  const all = this.state.layout.flat();
1034
  this.state.columns = n;
1035
+ this.state.layout = Array.from({ length: n }, () => []);
1036
  all.forEach((id, i) => this.state.layout[i % n].push(id));
1037
  this.renderGrid(); this.save();
1038
  },
 
1151
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1152
  }
1153
 
1154
+ // Determine which variables are actually needed by the series formulas
1155
+ const requiredVars = extractRequiredVars(cfg.series || []);
 
 
1156
 
1157
  try {
1158
  const res = await fetch('/api/fetch_series', {
 
1182
  return;
1183
  }
1184
 
1185
+ // Store data for breakdown table
1186
  chart.lastData = data;
1187
 
1188
  const body = document.getElementById(`body-${id}`);
1189
  if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1190
  if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
1191
 
1192
+ const datasets = (cfg.series || []).map(s => {
1193
  const vals = data.labels.map((_, i) => {
1194
  const vars = {
1195
  P: safeVal(data, 'P', i),
 
1238
  return ds;
1239
  });
1240
 
1241
+ this.render(id, data.labels, datasets, cfg.series || []);
1242
 
1243
  } catch(e) { console.error('ChartLogic.plot error:', e); }
1244
  },
 
1326
  };
1327
 
1328
  // ===================================================
1329
+ // BREAKDOWN TABLE (ENHANCED)
1330
  // ===================================================
1331
  const Breakdown = {
1332
+ currentTab: 'summary',
1333
+ expandedMinutes: new Set(),
1334
+
1335
+ showTab(tab) {
1336
+ this.currentTab = tab;
1337
+ document.getElementById('bdTabSummary').className = tab === 'summary' ? 'primary' : '';
1338
+ document.getElementById('bdTabSummary').style.background = tab === 'summary' ? 'var(--accent)' : 'var(--bg-panel)';
1339
+ document.getElementById('bdTabDetail').className = tab === 'detail' ? 'primary' : '';
1340
+ document.getElementById('bdTabDetail').style.background = tab === 'detail' ? 'var(--accent)' : 'var(--bg-panel)';
1341
+ document.getElementById('bdSummaryTab').style.display = tab === 'summary' ? 'block' : 'none';
1342
+ document.getElementById('bdDetailTab').style.display = tab === 'detail' ? 'block' : 'none';
1343
+ },
1344
+
1345
  open(id) {
1346
  const chart = App.state.charts[id];
1347
  if (!chart || !chart.lastData || !chart.config.series) {
 
1352
  const data = chart.lastData;
1353
  const cfg = chart.config;
1354
  const series = cfg.series;
 
1355
 
1356
+ // Build summary table
1357
+ this.buildSummaryTable(data, cfg, series);
1358
+
1359
+ // Build detail view if breakdown data available
1360
+ if (data.breakdown_json && data.breakdown_json.length > 0) {
1361
+ this.buildDetailView(data, cfg);
1362
+ } else {
1363
+ document.getElementById('bdDetailContent').innerHTML = `
1364
+ <div style="text-align:center; padding:40px; color:var(--text-secondary);">
1365
+ <div style="font-size:24px; opacity:0.5;">📊</div>
1366
+ <div style="margin-top:12px;">Strike breakdown data available only in Advanced Mode</div>
1367
+ <div style="font-size:11px; margin-top:8px;">Switch to Advanced Mode to see per-strike details</div>
1368
+ </div>
1369
+ `;
1370
+ }
1371
+
1372
+ document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1373
+ document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1374
+ document.getElementById('breakdownModal').style.display = 'flex';
1375
+
1376
+ // Default to summary tab
1377
+ this.showTab('summary');
1378
+ },
1379
+
1380
+ buildSummaryTable(data, cfg, series) {
1381
+ // Determine which raw variables to show in table
1382
+ const allVarKeys = ['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
1383
  const presentVars = allVarKeys.filter(k =>
1384
  data[k] && data[k].some(v => v && v !== 0)
1385
  );
1386
 
1387
+ // Build header
1388
  let thead = '<thead><tr><th>Time</th>';
1389
+ // Add ATM strike column if available
1390
+ if (data.atm_strike) {
1391
+ thead += '<th>ATM</th>';
1392
+ }
1393
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1394
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1395
  thead += '</tr></thead>';
1396
 
1397
+ // Build rows
1398
  let tbody = '<tbody>';
1399
  data.labels.forEach((lbl, i) => {
1400
  const vars = {
 
1405
  OI: safeVal(data, 'OI', i)
1406
  };
1407
 
1408
+ tbody += `<tr><td>${lbl}</td>`;
1409
+
1410
+ // ATM strike if available
1411
+ if (data.atm_strike) {
1412
+ const atm = data.atm_strike[i];
1413
+ tbody += `<td style="color:var(--accent); font-weight:600;">${atm ? atm.toFixed(0) : '-'}</td>`;
 
 
 
1414
  }
1415
 
1416
+ // Raw variable values
 
1417
  presentVars.forEach(k => {
1418
  const v = vars[k];
1419
  const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1420
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1421
  });
1422
 
1423
+ // Calculated formula values
1424
  series.forEach(s => {
1425
  const result = evalFormula(s.formula, vars);
1426
  const rv = result !== null ? result : 0;
 
1435
  }
1436
  tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
1437
  });
1438
+
1439
  tbody += '</tr>';
1440
+ });
1441
+ tbody += '</tbody>';
1442
 
1443
+ document.getElementById('bdTable').innerHTML = thead + tbody;
1444
+ },
1445
+
1446
+ buildDetailView(data, cfg) {
1447
+ const atmRange = cfg.atmRange || 5;
1448
+
1449
+ let html = '<div style="margin-bottom:12px; padding:12px; background:var(--bg-panel); border-radius:6px; border:1px solid var(--border);">';
1450
+ html += `<div style="font-size:12px; color:var(--text-secondary);">`;
1451
+ html += `<strong style="color:var(--accent);">ATM ± ${atmRange}</strong> = ${atmRange} ITM + ${atmRange} OTM strikes = <strong>${atmRange * 2}</strong> total strikes per side (CE & PE)`;
1452
+ html += `<br><span style="font-size:10px;">ITM for CALLS = strikes below ATM | ITM for PUTS = strikes above ATM</span>`;
1453
+ html += `</div></div>`;
1454
+
1455
+ html += '<div class="minute-list">';
1456
+
1457
+ data.labels.forEach((lbl, i) => {
1458
+ const breakdown = data.breakdown_json ? data.breakdown_json[i] : null;
1459
+ const atmStrike = data.atm_strike ? data.atm_strike[i] : null;
1460
+ const includedStrikes = data.included_strikes ? data.included_strikes[i] : '';
1461
+
1462
+ // Calculate aggregates for this minute
1463
+ const vars = {
1464
+ P: safeVal(data, 'P', i), B: safeVal(data, 'B', i),
1465
+ S: safeVal(data, 'S', i), CB: safeVal(data, 'CB', i),
1466
+ CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
1467
+ PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i),
1468
+ OI: safeVal(data, 'OI', i)
1469
+ };
1470
+
1471
+ const minuteId = `minute-${i}`;
1472
+ const isExpanded = this.expandedMinutes.has(minuteId);
1473
+
1474
+ html += `<div class="minute-row">`;
1475
+ html += `<div class="minute-header ${isExpanded ? 'expanded' : ''}" onclick="Breakdown.toggleMinute('${minuteId}')">`;
1476
+ html += `<div class="expand-icon ${isExpanded ? 'expanded' : ''}">▶</div>`;
1477
+ html += `<div class="minute-time">${lbl}</div>`;
1478
+ if (atmStrike) {
1479
+ html += `<div class="minute-atm">ATM: ${atmStrike.toFixed(0)}</div>`;
1480
+ }
1481
+ html += `<div class="minute-summary">`;
1482
+ html += `<span style="color:var(--success);">CB: ${vars.CB.toFixed(0)}</span>`;
1483
+ html += `<span style="color:var(--danger);">CS: ${vars.CS.toFixed(0)}</span>`;
1484
+ html += `<span style="color:var(--success);">PB: ${vars.PB.toFixed(0)}</span>`;
1485
+ html += `<span style="color:var(--danger);">PS: ${vars.PS.toFixed(0)}</span>`;
1486
+ html += `</div>`;
1487
+ html += `</div>`; // minute-header
1488
+
1489
+ html += `<div class="minute-detail ${isExpanded ? 'visible' : ''}" id="${minuteId}">`;
1490
+
1491
+ if (breakdown && breakdown.length > 0) {
1492
+ // Group by strike
1493
+ const strikesMap = {};
1494
+ breakdown.forEach(s => {
1495
+ if (!strikesMap[s.strike]) {
1496
+ strikesMap[s.strike] = { CE: null, PE: null };
1497
+ }
1498
+ strikesMap[s.strike][s.type] = s;
1499
+ });
1500
+
1501
+ const strikes = Object.keys(strikesMap).sort((a, b) => b - a); // Descending
1502
+
1503
+ html += `<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;">`;
1504
+ html += `Strikes included: ${includedStrikes || 'N/A'}`;
1505
+ html += `</div>`;
1506
+
1507
+ html += `<div class="strike-detail-grid">`;
1508
+
1509
+ strikes.forEach(strike => {
1510
+ const ceData = strikesMap[strike].CE;
1511
+ const peData = strikesMap[strike].PE;
1512
 
1513
+ if (ceData && ceData.included) {
1514
+ const cls = ceData.moneyness === 'ITM' ? 'itm' : 'otm';
1515
+ const atmClass = Math.abs(ceData.strike - atmStrike) < 1 ? 'atm' : '';
1516
+ html += `<div class="strike-detail-card ce ${cls} ${atmClass}">`;
1517
+ html += `<div class="strike-detail-header">`;
1518
+ html += `<span>${ceData.strike} CE</span>`;
1519
+ html += `<span class="strike-badge ${ceData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${ceData.moneyness}</span>`;
1520
+ html += `</div>`;
1521
+ html += `<div class="strike-detail-values">`;
1522
+ html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(ceData.b || 0).toFixed(0)}</span>`;
1523
+ html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(ceData.s || 0).toFixed(0)}</span>`;
1524
+ html += `<span class="detail-val">Vol:</span><span class="detail-num">${(ceData.v || 0).toFixed(0)}</span>`;
1525
+ html += `<span class="detail-val">Price:</span><span class="detail-num">${(ceData.p || 0).toFixed(2)}</span>`;
1526
+ html += `</div>`;
1527
+ html += `</div>`;
1528
+ }
1529
 
1530
+ if (peData && peData.included) {
1531
+ const cls = peData.moneyness === 'ITM' ? 'itm' : 'otm';
1532
+ const atmClass = Math.abs(peData.strike - atmStrike) < 1 ? 'atm' : '';
1533
+ html += `<div class="strike-detail-card pe ${cls} ${atmClass}">`;
1534
+ html += `<div class="strike-detail-header">`;
1535
+ html += `<span>${peData.strike} PE</span>`;
1536
+ html += `<span class="strike-badge ${peData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${peData.moneyness}</span>`;
1537
+ html += `</div>`;
1538
+ html += `<div class="strike-detail-values">`;
1539
+ html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(peData.b || 0).toFixed(0)}</span>`;
1540
+ html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(peData.s || 0).toFixed(0)}</span>`;
1541
+ html += `<span class="detail-val">Vol:</span><span class="detail-num">${(peData.v || 0).toFixed(0)}</span>`;
1542
+ html += `<span class="detail-val">Price:</span><span class="detail-num">${(peData.p || 0).toFixed(2)}</span>`;
1543
+ html += `</div>`;
1544
+ html += `</div>`;
1545
+ }
1546
+ });
1547
+
1548
+ html += `</div>`; // strike-detail-grid
1549
+ } else {
1550
+ html += `<div style="color:var(--text-secondary); text-align:center; padding:20px;">No strike breakdown data for this minute</div>`;
1551
  }
1552
+
1553
+ html += `</div>`; // minute-detail
1554
+ html += `</div>`; // minute-row
1555
  });
1556
+
1557
+ html += '</div>'; // minute-list
1558
+
1559
+ document.getElementById('bdDetailContent').innerHTML = html;
 
 
1560
  },
1561
+
1562
+ toggleMinute(minuteId) {
1563
+ const detail = document.getElementById(minuteId);
1564
+ const header = detail.previousElementSibling;
1565
+ const icon = header.querySelector('.expand-icon');
1566
+
1567
+ if (this.expandedMinutes.has(minuteId)) {
1568
+ this.expandedMinutes.delete(minuteId);
1569
+ detail.classList.remove('visible');
1570
+ header.classList.remove('expanded');
1571
+ icon.classList.remove('expanded');
1572
+ } else {
1573
+ this.expandedMinutes.add(minuteId);
1574
+ detail.classList.add('visible');
1575
+ header.classList.add('expanded');
1576
+ icon.classList.add('expanded');
1577
+ }
1578
  }
1579
  };
1580
 
 
1593
  document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1594
  document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1595
  this.updatePresetsDropdown();
1596
+ // Auto-load a relevant default preset when mode switches
1597
  const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
1598
  this.loadPreset(defPreset);
1599
  },
 
1622
 
1623
  deletePreset() {
1624
  const name = document.getElementById('cfgPresetsDropdown').value;
1625
+ const locked = ['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
1626
  'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
1627
  'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
1628
  if (locked.includes(name)) return alert('Cannot delete default models.');
 
1771
  <input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
1772
  </div>
1773
  `;
1774
+ // Keep s-color1 in sync with the condition row color
1775
  const solidColor = r.querySelector('.s-color1');
1776
  const condTrueColor = r.querySelector('.s-color1-cond');
1777
  solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });