topsecrettraders commited on
Commit
5f8ea59
·
verified ·
1 Parent(s): db853e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +196 -440
app.py CHANGED
@@ -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
 
@@ -141,9 +141,9 @@ 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":
@@ -156,7 +156,7 @@ async def fetch_series(
156
  ref_instrument = instrument
157
  if not ref_instrument:
158
  spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
159
- fut_keys = [k for k in instruments if 'FUT' in k]
160
  if spot_keys:
161
  ref_instrument = spot_keys[0]
162
  elif fut_keys:
@@ -165,13 +165,26 @@ async def fetch_series(
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
  rpc_name = "get_advanced_chart_data"
169
  base_params = {
170
  "p_root": root,
171
  "p_expiry": expiry,
172
  "p_ref_instrument": ref_instrument,
173
  "p_atm_range": atm_range,
174
- "p_strike_map": {}
175
  }
176
 
177
  else:
@@ -185,9 +198,7 @@ async def fetch_series(
185
  "p_instrument": instrument
186
  }
187
 
188
- # =========================================================
189
- # CHUNKING: 2-hour chunks fetched in parallel
190
- # =========================================================
191
  async def fetch_chunk(start_dt, end_dt):
192
  params = base_params.copy()
193
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
@@ -214,22 +225,13 @@ async def fetch_series(
214
  if not all_records:
215
  return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
216
 
217
- # =========================================================
218
- # DATA PROCESSING
219
- # =========================================================
220
  df = pd.DataFrame(all_records)
221
 
222
  df.rename(columns={
223
- "tick_ts": "ts",
224
- "b_qty": "B",
225
- "s_qty": "S",
226
- "vol": "V",
227
- "oi": "OI",
228
- "cb": "CB",
229
- "cs": "CS",
230
- "pb": "PB",
231
- "ps": "PS",
232
- "price": "P"
233
  }, inplace=True)
234
 
235
  df['ts'] = pd.to_datetime(df['ts'])
@@ -237,7 +239,6 @@ async def fetch_series(
237
  df.set_index('ts', inplace=True)
238
  df.sort_index(inplace=True)
239
 
240
- # Resample
241
  tf_map = {
242
  "1min": "1min", "3min": "3min", "5min": "5min",
243
  "15min": "15min", "30min": "30min", "1hour": "1h"
@@ -246,28 +247,29 @@ async def fetch_series(
246
 
247
  agg_dict = {
248
  "P": "last", "V": "sum", "OI": "last", "B": "last", "S": "last",
249
- "CB": "last", "CS": "last", "PB": "last", "PS": "last",
250
- "breakdown": "last" # Include the new JSON breakdown
251
  }
252
 
253
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
254
- resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
255
 
256
- response_data = {
257
- "labels": resampled.index.strftime('%H:%M').tolist()
258
- }
 
 
 
 
259
 
260
- for var_key in["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
261
- if var_key in required_vars and var_key in resampled.columns:
262
- response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
263
 
264
- # Send the breakdown array strictly for UI sub-tables
265
- if "breakdown" in resampled.columns:
266
- cleaned_breakdown = []
267
- for x in resampled["breakdown"]:
268
- if isinstance(x, list): cleaned_breakdown.append(x)
269
- else: cleaned_breakdown.append([])
270
- response_data["breakdown"] = cleaned_breakdown
271
 
272
  return JSONResponse(response_data)
273
 
@@ -355,7 +357,6 @@ HTML_TEMPLATE = """
355
 
356
  .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
357
  .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
358
- .tool-btn-small { font-size:10px; padding:2px 6px; height:auto; width:auto; border:1px solid var(--border); }
359
 
360
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
361
  .empty-state {
@@ -391,10 +392,10 @@ HTML_TEMPLATE = """
391
  box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
392
  }
393
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
394
- .custom-dd-opt:last-child { border-bottom: none; }
395
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
396
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
397
 
 
398
  .series-row {
399
  background: var(--bg-panel); padding: 12px; border-radius: 8px;
400
  border: 1px solid var(--border); margin-bottom: 10px;
@@ -402,62 +403,36 @@ HTML_TEMPLATE = """
402
  .series-row-top { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
403
  .series-row-bottom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
404
  .series-row-cond { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px; border: 1px dashed var(--border); }
405
-
406
  .s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-label-true, .s-label-false {
407
  background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
408
  border-radius: 6px; padding: 0 10px; height: 30px; font-size: 12px; transition: 0.2s;
409
  }
410
- .s-form:focus, .s-lbl:focus { border-color: var(--accent); }
411
-
412
- .color-input {
413
- padding: 0; width: 34px; border: 1px solid var(--border); background: var(--bg-input);
414
- height: 30px; cursor: pointer; border-radius: 6px; overflow: hidden;
415
- }
416
- .color-input::-webkit-color-swatch-wrapper { padding: 0; }
417
- .color-input::-webkit-color-swatch { border: none; }
418
- .cond-label { font-size: 10px; color: var(--text-secondary); font-weight: 600; white-space: nowrap; }
419
 
420
- .segmented-control {
421
- display: flex; background: var(--bg-body); border: 1px solid var(--border);
422
- border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
423
- }
424
- .seg-btn {
425
- flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600;
426
- color: var(--text-secondary); cursor: pointer; border: none; background: transparent;
427
- transition: 0.2s; border-radius: 4px;
428
- }
429
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
430
 
431
- .info-icon {
432
- display: inline-flex; align-items: center; justify-content: center;
433
- width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel);
434
- color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help;
435
- border: 1px solid var(--border); margin-left: 8px; transition: 0.2s;
436
- }
437
- .info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
438
 
439
- /* BREAKDOWN TABLE */
440
- .breakdown-table {
441
- width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px;
442
- }
443
- .breakdown-table th {
444
- background: var(--bg-panel); color: var(--text-secondary); padding: 8px 10px;
445
- border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0;
446
- }
447
  .breakdown-table th:first-child { text-align: left; }
448
- .breakdown-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
449
  .breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
450
- .breakdown-table tr:hover > td { background: var(--bg-panel); }
451
  .val-pos { color: #3fb950; }
452
  .val-neg { color: #f85149; }
453
- .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 60vh; border: 1px solid var(--border); border-radius: 6px; }
454
-
455
- /* Sub Table specific styling */
456
- .sub-table { margin: 10px 0; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
457
- .sub-table th { background: var(--bg-body); border-bottom: 1px solid var(--border); padding: 6px 10px; font-size: 10px; }
458
- .sub-table td { padding: 4px 10px; border-bottom: 1px solid var(--border); }
459
- .sub-table tr:last-child td { border-bottom: none; }
460
- .atm-row { background: rgba(47,129,247,0.12); border-left: 3px solid var(--accent); }
 
461
  </style>
462
  </head>
463
  <body>
@@ -530,7 +505,7 @@ HTML_TEMPLATE = """
530
  <label>Chart Mode</label>
531
  <div class="segmented-control" id="cfgModeToggle">
532
  <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
533
- <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode (2N)</button>
534
  </div>
535
  </div>
536
 
@@ -541,17 +516,14 @@ HTML_TEMPLATE = """
541
  <div class="input-arrow">▼</div>
542
  <div id="dd-cfgInst" class="custom-dd"></div>
543
  </div>
544
- <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
545
- * Advanced Mode: select Spot/Future as reference. Aggregates CE/PE strictly N strikes above and N below Spot.
546
- </div>
547
  </div>
548
 
549
  <div id="cfgAdvancedFields" style="display:none;">
550
  <div class="form-section" style="margin-top:12px;">
551
- <label style="color:var(--accent);">N Strikes (ITM/OTM Range)</label>
552
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
553
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
554
- Grabs precisely N strikes below and N strikes above/equal to the reference price. (Total 2N strikes).
555
  </div>
556
  </div>
557
  </div>
@@ -591,7 +563,7 @@ HTML_TEMPLATE = """
591
  <div class="form-section">
592
  <div style="display:flex; align-items:center; margin-bottom:6px;">
593
  <label style="margin:0;">Plot Lines</label>
594
- <span class="info-icon" id="infoIcon" title="Variables: $P=Price, $B=Buy, $S=Sell (Fut/Eq) | $CB=Call Buy, $CS=Call Sell, $PB=Put Buy, $PS=Put Sell (Options)">i</span>
595
  </div>
596
  <div id="cfgSeries"></div>
597
  <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
@@ -666,7 +638,6 @@ function extractRequiredVars(seriesList) {
666
  const CustomDD = {
667
  data: { cfgRoot:[], cfgExp: [], cfgInst: [] },
668
  recent: { cfgRoot:[] },
669
-
670
  init() {
671
  const saved = localStorage.getItem('dc_recent_symbols');
672
  if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
@@ -704,10 +675,8 @@ const CustomDD = {
704
  .filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
705
  .filter(item => item.val.toLowerCase().includes(q));
706
 
707
- if (!filtered.length) {
708
- el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary);">No matches</div>';
709
- return;
710
- }
711
  let html = '', currentGroup = null;
712
  filtered.forEach(item => {
713
  if (item.group && item.group !== currentGroup) {
@@ -738,52 +707,14 @@ const CustomDD = {
738
  // ===================================================
739
  const App = {
740
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
741
-
742
  presets: {
743
- "Basic Flow (Fut/Eq)":[
744
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
745
- { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
746
- { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
747
- ],
748
- "Net Flow (Fut/Eq)":[
749
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
750
- { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
751
- ],
752
- "Call Flow (Options)":[
753
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
754
- { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
755
- { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
756
- ],
757
- "Put Flow (Options)":[
758
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
759
- { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
760
- { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
761
- ],
762
- "Aggregated Flow (Options)":[
763
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
764
- { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
765
- { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
766
- ],
767
- "Net Flow (Options)":[
768
- { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
769
- { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
770
- ],
771
- "Price & Flows (Advanced)":[
772
- { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
773
- { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
774
- { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
775
- ],
776
- "Net Flow (Advanced)":[
777
- { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
778
- { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
779
- ],
780
- "Components (Advanced)":[
781
- { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
782
- { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
783
- { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
784
- { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#da3633', cond: 'none' },
785
- { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#f85149', cond: 'none' }
786
- ]
787
  },
788
 
789
  async init() {
@@ -793,41 +724,25 @@ const App = {
793
  if (savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e) {}
794
 
795
  const saved = localStorage.getItem('dc_pro_v4_state');
796
- if (saved) {
797
- try {
798
- const s = JSON.parse(saved);
799
- this.state = { ...this.state, ...s };
800
- } catch(e) { this.initDefault(); }
801
- } else {
802
- this.initDefault();
803
- }
804
 
805
  const dSel = document.getElementById('globalDate');
806
- if (this.state.date && Array.from(dSel.options).some(o => o.value === this.state.date))
807
- dSel.value = this.state.date;
808
- else if (dSel.options.length)
809
- this.state.date = dSel.options[0].value;
810
 
811
  document.getElementById('globalMode').value = this.state.mode;
812
- this.updateModeUI();
813
- this.renderGrid();
814
- this.startLoop();
815
  },
816
 
817
  initDefault() {
818
- this.state.columns = 3;
819
- this.state.layout = [[], [],[]];
820
- this.state.refreshRate = 15;
821
- for (let i = 0; i < 3; i++) {
822
- this.addChartInternal(i);
823
- this.addChartInternal(i);
824
- }
825
  },
826
 
827
  async loadDates() {
828
  try {
829
- const res = await fetch('/api/dates');
830
- const dates = await res.json();
831
  document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
832
  if (!this.state.date && dates.length) this.state.date = dates[0];
833
  } catch(e) {}
@@ -839,6 +754,7 @@ const App = {
839
  this.state.charts[id] = { config: {}, instance: null, lastData: null };
840
  return id;
841
  },
 
842
  save() {
843
  const cleanCharts = {};
844
  for (let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
@@ -848,57 +764,39 @@ const App = {
848
  refreshRate: this.state.refreshRate, charts: cleanCharts
849
  }));
850
  },
 
851
  reset() { if (confirm('Reset Workspace?')) { localStorage.removeItem('dc_pro_v4_state'); location.reload(); } },
 
852
  setLayout(n) {
853
  const all = this.state.layout.flat();
854
- this.state.columns = n;
855
- this.state.layout = Array.from({ length: n }, () =>[]);
856
  all.forEach((id, i) => this.state.layout[i % n].push(id));
857
  this.renderGrid(); this.save();
858
  },
859
 
860
  renderGrid() {
861
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i + 1) === this.state.columns));
862
- const container = document.getElementById('gridRoot');
863
- container.innerHTML = '';
864
-
865
  this.state.layout.forEach((ids, colIdx) => {
866
- const col = document.createElement('div');
867
- col.className = 'column';
868
- const viewH = window.innerHeight - 52;
869
- const count = ids.length;
870
-
871
  ids.forEach(id => {
872
- if (this.state.charts[id]?.instance) {
873
- this.state.charts[id].instance.destroy();
874
- this.state.charts[id].instance = null;
875
- }
876
  const card = this.createCardDOM(id);
877
  card.style.height = (count > 0 && count <= 3) ? `calc(${viewH / count}px - 1px)` : '300px';
878
  col.appendChild(card);
879
  setTimeout(() => ChartLogic.plot(id), 0);
880
  });
881
-
882
- const btn = document.createElement('button');
883
- btn.className = 'add-chart-btn';
884
- btn.innerText = '+ Add Chart';
885
  btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
886
- col.appendChild(btn);
887
- container.appendChild(col);
888
  });
889
  },
890
 
891
  createCardDOM(id) {
892
- const div = document.createElement('div');
893
- div.className = 'chart-card';
894
- div.id = `card-${id}`;
895
-
896
- const cfg = this.state.charts[id]?.config || {};
897
- const isCfg = cfg && cfg.root;
898
- const modeTag = cfg.chartMode === 'advanced'
899
- ? `<span class="inst-label">2N (±${cfg.atmRange || 5})</span>`
900
- : `<span class="inst-label">${cfg.instrument || ''}</span>`;
901
-
902
  div.innerHTML = `
903
  <div class="card-header">
904
  <div class="card-info">
@@ -940,8 +838,7 @@ const App = {
940
  const loop = () => {
941
  if (this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
942
  setTimeout(loop, (this.state.refreshRate || 15) * 1000);
943
- };
944
- loop();
945
  }
946
  };
947
 
@@ -970,16 +867,9 @@ const ChartLogic = {
970
  method: 'POST',
971
  headers: { 'Content-Type': 'application/json' },
972
  body: JSON.stringify({
973
- date: App.state.date,
974
- root: cfg.root,
975
- expiry: cfg.expiry,
976
- instrument: cfg.instrument || '',
977
- timeframe: cfg.timeframe,
978
- start_time: cfg.start,
979
- end_time: endTime,
980
- mode: cfg.chartMode || 'normal',
981
- atm_range: cfg.atmRange || 5,
982
- required_vars: requiredVars
983
  })
984
  });
985
  const data = await res.json();
@@ -989,8 +879,7 @@ const ChartLogic = {
989
  const b = document.getElementById(`body-${id}`);
990
  if (b) b.innerHTML = `<div class="empty-state" style="opacity:0.8;"><span style="font-size:20px;">⚠️</span><br/>${data.error || 'No Data Found'}</div>`;
991
  }
992
- if (dot) dot.classList.remove('retry');
993
- return;
994
  }
995
 
996
  chart.lastData = data;
@@ -999,15 +888,11 @@ const ChartLogic = {
999
  if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
1000
 
1001
  const datasets = (cfg.series ||[]).map(s => {
1002
- const vals = data.labels.map((_, i) => {
1003
- const vars = {
1004
- P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
1005
- CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i),
1006
- PB: safeVal(data, 'PB', i), PS: safeVal(data, 'PS', i),
1007
- V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
1008
- };
1009
- return evalFormula(s.formula, vars);
1010
- });
1011
 
1012
  const ds = {
1013
  label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0,
@@ -1016,10 +901,8 @@ const ChartLogic = {
1016
 
1017
  if (s.cond && s.cond !== 'none') {
1018
  const thresh = parseFloat(s.thresh) || 0;
1019
- const cTrue = s.color1 || '#238636';
1020
- const cFalse = s.color2 || '#da3633';
1021
  const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
1022
-
1023
  ds.segment = { borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse };
1024
  ds.borderColor = cTrue; ds.backgroundColor = cTrue;
1025
  ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
@@ -1030,24 +913,19 @@ const ChartLogic = {
1030
  }
1031
  return ds;
1032
  });
1033
-
1034
  this.render(id, data.labels, datasets, cfg.series ||[]);
1035
 
1036
  } catch(e) { console.error('ChartLogic.plot error:', e); }
1037
  },
1038
 
1039
  render(id, labels, datasets, seriesConfig) {
1040
- const ctx = document.getElementById(`canvas-${id}`);
1041
- if (!ctx) return;
1042
  const chart = App.state.charts[id];
1043
 
1044
- if (chart.instance && chart.instance.ctx?.canvas !== ctx) {
1045
- chart.instance.destroy(); chart.instance = null;
1046
- }
1047
 
1048
  if (chart.instance) {
1049
- chart.instance.data.labels = labels;
1050
- chart.instance.data.datasets = datasets;
1051
  chart.instance.update('none');
1052
  } else {
1053
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
@@ -1059,15 +937,12 @@ const ChartLogic = {
1059
  plugins: {
1060
  legend: { display: false },
1061
  tooltip: {
1062
- backgroundColor: 'rgba(22,27,34,0.97)',
1063
- titleColor: '#e6edf3', bodyColor: '#8b949e',
1064
  borderColor: '#30363d', borderWidth: 1, padding: 10,
1065
  callbacks: {
1066
  label: function(context) {
1067
- const s = seriesConfig[context.datasetIndex];
1068
- if (!s) return '';
1069
- const v = context.parsed.y;
1070
- const fv = v !== null ? v.toFixed(2) : 'N/A';
1071
  let condText = '';
1072
  if (s.cond && s.cond !== 'none') {
1073
  const thresh = parseFloat(s.thresh) || 0;
@@ -1078,13 +953,11 @@ const ChartLogic = {
1078
  return `${s.label}: ${fv}${condText}`;
1079
  },
1080
  labelColor: function(context) {
1081
- const s = seriesConfig[context.datasetIndex];
1082
- if (!s) return { borderColor: '#fff', backgroundColor: '#fff' };
1083
  let c = s.color1 || '#fff';
1084
  if (s.cond && s.cond !== 'none') {
1085
- const thresh = parseFloat(s.thresh) || 0;
1086
- const v = context.parsed.y;
1087
- c = (s.cond === '>' ? v > thresh : v < thresh) ? (s.color1 || '#238636') : (s.color2 || '#da3633');
1088
  }
1089
  return { borderColor: c, backgroundColor: c };
1090
  }
@@ -1103,62 +976,42 @@ const ChartLogic = {
1103
  };
1104
 
1105
  // ===================================================
1106
- // BREAKDOWN TABLE WITH SUB-TABLES
1107
  // ===================================================
1108
  const Breakdown = {
1109
  open(id) {
1110
  const chart = App.state.charts[id];
1111
- if (!chart || !chart.lastData || !chart.config.series) {
1112
- alert('No data loaded yet. Please wait for chart to load.');
1113
- return;
1114
- }
1115
-
1116
- const data = chart.lastData;
1117
- const cfg = chart.config;
1118
- const series = cfg.series;
1119
-
1120
- // Check if breakdown JSON is present and populated
1121
- const hasBreakdown = data.breakdown && data.breakdown.some(b => b && b.length > 0);
1122
 
 
1123
  const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
1124
  const presentVars = allVarKeys.filter(k => data[k] && data[k].some(v => v && v !== 0));
1125
 
1126
- // Header
1127
  let thead = '<thead><tr><th>Time</th>';
1128
- if (hasBreakdown) thead += '<th style="text-align:center;">Details</th>';
1129
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1130
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1131
  thead += '</tr></thead>';
1132
 
1133
- // Body
1134
  let tbody = '<tbody>';
1135
  data.labels.forEach((lbl, i) => {
1136
  const vars = {
1137
- P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
1138
  CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
1139
- PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
1140
  };
1141
 
1142
- tbody += `<tr><td>${lbl}</td>`;
1143
-
1144
- if (hasBreakdown) {
1145
- const bdArr = data.breakdown[i] ||[];
1146
- if (bdArr.length > 0) {
1147
- tbody += `<td style="text-align:center;"><button class="tool-btn tool-btn-small" onclick="Breakdown.toggleSubRow('${id}', ${i})">▼ Strikes</button></td>`;
1148
- } else {
1149
- tbody += `<td></td>`;
1150
- }
1151
- }
1152
 
1153
  presentVars.forEach(k => {
1154
- const v = vars[k];
1155
- const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1156
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1157
  });
1158
 
1159
  series.forEach(s => {
1160
- const result = evalFormula(s.formula, vars);
1161
- const rv = result !== null ? result : 0;
1162
  let cls = rv > 0 ? 'val-pos' : (rv < 0 ? 'val-neg' : '');
1163
  let condLabel = '';
1164
  if (s.cond && s.cond !== 'none') {
@@ -1170,62 +1023,47 @@ const Breakdown = {
1170
  }
1171
  tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
1172
  });
1173
- tbody += '</tr>';
1174
 
1175
- // Insert hidden Sub-Table if breakdown exists
1176
- if (hasBreakdown && (data.breakdown[i] || []).length > 0) {
1177
- const bd = data.breakdown[i];
1178
- const strikes = {};
1179
- bd.forEach(item => {
1180
- if (!strikes[item.strike]) strikes[item.strike] = { CE_B: 0, CE_S: 0, PE_B: 0, PE_S: 0 };
1181
- if (item.type === 'CE') { strikes[item.strike].CE_B = item.b; strikes[item.strike].CE_S = item.s; }
1182
- if (item.type === 'PE') { strikes[item.strike].PE_B = item.b; strikes[item.strike].PE_S = item.s; }
1183
- });
1184
 
1185
- // Find closest strike to Spot Price
1186
- const spotPrice = vars.P;
1187
- let closestStrike = null;
1188
- let minDiff = Infinity;
1189
- Object.keys(strikes).forEach(k => {
1190
- const diff = Math.abs(parseFloat(k) - spotPrice);
1191
- if (diff < minDiff) { minDiff = diff; closestStrike = k; }
1192
  });
1193
-
1194
- let subHtml = `<div style="padding:4px 10px 14px 10px;">
1195
- <div style="font-size:10px; color:var(--text-secondary); margin-bottom:4px;">STRIKES SELECTED FOR ${lbl} (Spot: ${spotPrice})</div>
1196
- <table class="sub-table" style="width:100%;">
1197
- <tr><th style="text-align:left;">Strike</th><th style="text-align:right;">CE Buy</th><th style="text-align:right;">CE Sell</th><th style="text-align:right;">PE Buy</th><th style="text-align:right;">PE Sell</th></tr>`;
1198
-
1199
- Object.keys(strikes).sort((a,b)=>a-b).forEach(k => {
1200
- const isAtm = (k === closestStrike);
1201
- const rowCls = isAtm ? 'atm-row' : '';
1202
- subHtml += `<tr class="${rowCls}">
1203
- <td style="text-align:left; font-weight:bold; color:var(--text-primary);">${k} ${isAtm ? '<span style="font-size:9px; color:var(--accent); margin-left:4px;">(ATM)</span>' : ''}</td>
1204
- <td class="val-pos" style="text-align:right;">${strikes[k].CE_B.toFixed(2)}</td>
1205
- <td class="val-neg" style="text-align:right;">${strikes[k].CE_S.toFixed(2)}</td>
1206
- <td class="val-pos" style="text-align:right;">${strikes[k].PE_B.toFixed(2)}</td>
1207
- <td class="val-neg" style="text-align:right;">${strikes[k].PE_S.toFixed(2)}</td>
1208
  </tr>`;
1209
  });
1210
  subHtml += `</table></div>`;
1211
-
1212
- const colspan = 1 + 1 + presentVars.length + series.length;
1213
- tbody += `<tr id="subrow-${id}-${i}" style="display:none; background:var(--bg-header);"><td colspan="${colspan}" style="padding:0;">${subHtml}</td></tr>`;
1214
  }
1215
  });
1216
  tbody += '</tbody>';
1217
 
1218
- document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? '2N (±' + (cfg.atmRange || 5) + ')' : (cfg.instrument || '')}`;
1219
  document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1220
  document.getElementById('bdTable').innerHTML = thead + tbody;
1221
  document.getElementById('breakdownModal').style.display = 'flex';
1222
  },
1223
 
1224
- toggleSubRow(chartId, index) {
1225
- const row = document.getElementById(`subrow-${chartId}-${index}`);
1226
- if (row) {
1227
- row.style.display = row.style.display === 'none' ? 'table-row' : 'none';
1228
- }
1229
  }
1230
  };
1231
 
@@ -1242,7 +1080,6 @@ const Config = {
1242
  btns[1].classList.toggle('active', mode === 'advanced');
1243
  document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
1244
  document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1245
- document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1246
  this.updatePresetsDropdown();
1247
  const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
1248
  this.loadPreset(defPreset);
@@ -1253,87 +1090,60 @@ const Config = {
1253
  },
1254
  loadPreset(presetName = null) {
1255
  const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1256
- if (name && App.presets[name]) {
1257
- document.getElementById('cfgPresetsDropdown').value = name;
1258
- this.renderSeries(App.presets[name]);
1259
- }
1260
  },
1261
  savePreset() {
1262
  const name = prompt('Name for this model:', 'Custom Model');
1263
  if (!name) return;
1264
  App.presets[name] = this.extractSeriesFromDOM();
1265
  localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1266
- this.updatePresetsDropdown();
1267
- document.getElementById('cfgPresetsDropdown').value = name;
1268
  },
1269
  deletePreset() {
1270
  const name = document.getElementById('cfgPresetsDropdown').value;
1271
- const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
1272
- 'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
1273
- 'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
1274
  if (locked.includes(name)) return alert('Cannot delete default models.');
1275
- if (confirm(`Delete model '${name}'?`)) {
1276
- delete App.presets[name];
1277
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1278
- this.updatePresetsDropdown();
1279
- this.loadPreset();
1280
- }
1281
  },
1282
 
1283
  async open(id) {
1284
  this.tid = id; this.isGlobal = false;
1285
- document.getElementById('configModal').style.display = 'flex';
1286
- document.getElementById('cfgTitle').innerText = 'Configure Chart';
1287
- document.getElementById('cfgLoading').style.display = 'block';
1288
- document.getElementById('cfgContent').style.display = 'none';
1289
- document.getElementById('cfgGlobalOnly').style.display = 'none';
1290
- document.getElementById('cfgChartOnly').style.display = 'block';
1291
-
1292
- this.updatePresetsDropdown();
1293
 
1294
- CustomDD.clear('cfgRoot');
1295
- const res = await fetch(`/api/roots?date=${App.state.date}`);
1296
- const roots = await res.json();
1297
  CustomDD.set('cfgRoot', roots);
1298
 
1299
  const cfg = App.state.charts[id].config;
1300
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1301
- document.getElementById('cfgStart').value = cfg.start || '09:15';
1302
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
1303
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1304
 
1305
  this.currentMode = cfg.chartMode || 'normal';
1306
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1307
- btns[0].classList.toggle('active', this.currentMode === 'normal');
1308
- btns[1].classList.toggle('active', this.currentMode === 'advanced');
1309
  document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1310
  document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1311
- document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1312
 
1313
  if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
1314
  else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
1315
 
1316
- document.getElementById('cfgRoot').value = cfg.root || '';
1317
- document.getElementById('cfgExp').value = cfg.expiry || '';
1318
- document.getElementById('cfgInst').value = cfg.instrument || '';
1319
-
1320
  if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
1321
-
1322
- document.getElementById('cfgLoading').style.display = 'none';
1323
- document.getElementById('cfgContent').style.display = 'block';
1324
  },
1325
 
1326
  openDefaults() {
1327
- this.isGlobal = true;
1328
- document.getElementById('configModal').style.display = 'flex';
1329
- document.getElementById('cfgTitle').innerText = 'Global Settings';
1330
- document.getElementById('cfgLoading').style.display = 'none';
1331
- document.getElementById('cfgContent').style.display = 'block';
1332
- document.getElementById('cfgGlobalOnly').style.display = 'block';
1333
- document.getElementById('cfgChartOnly').style.display = 'none';
1334
- document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1335
  this.updatePresetsDropdown();
1336
  },
 
1337
  close() { document.getElementById('configModal').style.display = 'none'; },
1338
 
1339
  async onRootChange(preExp, preInst) {
@@ -1343,81 +1153,50 @@ const Config = {
1343
  if (!preInst) document.getElementById('cfgInst').value = '';
1344
  if (!root) return;
1345
 
1346
- const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1347
- const exps = await res.json();
1348
  CustomDD.set('cfgExp', exps);
1349
 
1350
- if (preExp && exps.includes(preExp)) {
1351
- document.getElementById('cfgExp').value = preExp;
1352
- await this.onExpChange(preInst);
1353
- } else if (exps.length > 0) {
1354
  const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
1355
- if (auto.current) {
1356
- document.getElementById('cfgExp').value = auto.current.expiry;
1357
- await this.onExpChange(auto.current.instrument);
1358
- }
1359
  }
1360
  },
1361
 
1362
  async onExpChange(preInst) {
1363
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1364
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1365
- CustomDD.clear('cfgInst');
1366
- if (!root || !exp) return;
1367
-
1368
- const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
1369
- const data = await res.json();
1370
  if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1371
  if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1372
  if (preInst) document.getElementById('cfgInst').value = preInst;
1373
  },
1374
 
1375
- toggleCond(sel) {
1376
- const condGroup = sel.closest('.series-row').querySelector('.series-row-cond');
1377
- if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex';
1378
- },
1379
 
1380
- renderSeries(list) {
1381
- const c = document.getElementById('cfgSeries');
1382
- c.innerHTML = '';
1383
- list.forEach(s => this._appendSeriesRow(c, s));
1384
- },
1385
 
1386
  _appendSeriesRow(container, s = {}) {
1387
- const r = document.createElement('div');
1388
- r.className = 'series-row';
1389
  const showCond = s.cond && s.cond !== 'none';
1390
  r.innerHTML = `
1391
  <div class="series-row-top">
1392
  <input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
1393
  <input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
1394
- <select class="s-axis" style="width:60px;">
1395
- <option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option>
1396
- <option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option>
1397
- </select>
1398
  <button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
1399
  </div>
1400
  <div class="series-row-bottom">
1401
  <input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
1402
- <select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)">
1403
- <option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option>
1404
- <option value=">" ${s.cond === '>' ? 'selected' : ''}>If &gt;</option>
1405
- <option value="<" ${s.cond === '<' ? 'selected' : ''}>If &lt;</option>
1406
- </select>
1407
  </div>
1408
  <div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
1409
- <span class="cond-label">Threshold:</span>
1410
- <input type="number" class="s-thresh" value="${s.thresh || 0}" style="width:65px;">
1411
- <span class="cond-label">True →</span>
1412
- <input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True">
1413
- <input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
1414
- <span class="cond-label">False →</span>
1415
- <input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False">
1416
- <input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
1417
  </div>
1418
  `;
1419
- const solidColor = r.querySelector('.s-color1');
1420
- const condTrueColor = r.querySelector('.s-color1-cond');
1421
  solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
1422
  container.appendChild(r);
1423
  },
@@ -1428,50 +1207,27 @@ const Config = {
1428
  return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
1429
  const cond = r.querySelector('.s-cond')?.value || 'none';
1430
  return {
1431
- formula: r.querySelector('.s-form')?.value || '$P',
1432
- label: r.querySelector('.s-lbl')?.value || 'Line',
1433
- axis: r.querySelector('.s-axis')?.value || 'left',
1434
- color1: r.querySelector('.s-color1-cond')?.value || r.querySelector('.s-color1')?.value || '#2f81f7',
1435
- cond,
1436
- thresh: r.querySelector('.s-thresh')?.value || '0',
1437
- color2: r.querySelector('.s-color2')?.value || '#da3633',
1438
- labelTrue: r.querySelector('.s-label-true')?.value || '',
1439
- labelFalse: r.querySelector('.s-label-false')?.value || ''
1440
  };
1441
  });
1442
  },
1443
 
1444
  saveChartConfig() {
1445
- if (this.isGlobal) {
1446
- App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1447
- App.save();
1448
- this.close();
1449
- return;
1450
- }
1451
-
1452
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1453
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1454
  const inst = document.getElementById('cfgInst').value.toUpperCase();
1455
-
1456
  if (!root || !exp) return alert('Please select a Symbol and Expiry.');
1457
-
1458
- const series = this.extractSeriesFromDOM();
1459
- if (!series.length) return alert('Add at least one plot line.');
1460
 
1461
  App.state.charts[this.tid].config = {
1462
- chartMode: this.currentMode,
1463
- atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1464
- root, expiry: exp, instrument: inst,
1465
- timeframe: document.getElementById('cfgTF').value,
1466
- start: document.getElementById('cfgStart').value,
1467
- end: document.getElementById('cfgEnd').value,
1468
- series
1469
  };
1470
-
1471
- App.save();
1472
- App.renderGrid();
1473
- ChartLogic.plot(this.tid);
1474
- this.close();
1475
  }
1476
  };
1477
 
 
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
 
 
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
  required_vars = list(set(required_vars) | {"P"})
148
 
149
  if mode == "advanced":
 
156
  ref_instrument = instrument
157
  if not ref_instrument:
158
  spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
159
+ fut_keys =[k for k in instruments if 'FUT' in k]
160
  if spot_keys:
161
  ref_instrument = spot_keys[0]
162
  elif fut_keys:
 
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
+ # PROPER STRIKE EXTRACTION USING PYTHON
169
+ p_strike_map = {}
170
+ for inst in instruments:
171
+ if inst.endswith('CE') or inst.endswith('PE'):
172
+ sym_part = inst.split(':')[-1]
173
+ if sym_part.startswith(root):
174
+ remainder = sym_part[len(root):-2] # Remove root and CE/PE
175
+ if len(remainder) >= 5: # Fyers Expiry dates are 5 characters (e.g. 26302, 26MAR)
176
+ strike_str = remainder[5:]
177
+ try:
178
+ p_strike_map[inst] = float(strike_str)
179
+ except: pass
180
+
181
  rpc_name = "get_advanced_chart_data"
182
  base_params = {
183
  "p_root": root,
184
  "p_expiry": expiry,
185
  "p_ref_instrument": ref_instrument,
186
  "p_atm_range": atm_range,
187
+ "p_strike_map": p_strike_map
188
  }
189
 
190
  else:
 
198
  "p_instrument": instrument
199
  }
200
 
201
+ # Chunking requests to DB
 
 
202
  async def fetch_chunk(start_dt, end_dt):
203
  params = base_params.copy()
204
  params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
 
225
  if not all_records:
226
  return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
227
 
228
+ # Data Processing
 
 
229
  df = pd.DataFrame(all_records)
230
 
231
  df.rename(columns={
232
+ "tick_ts": "ts", "b_qty": "B", "s_qty": "S", "vol": "V",
233
+ "oi": "OI", "cb": "CB", "cs": "CS", "pb": "PB", "ps": "PS",
234
+ "price": "P", "details": "details"
 
 
 
 
 
 
 
235
  }, inplace=True)
236
 
237
  df['ts'] = pd.to_datetime(df['ts'])
 
239
  df.set_index('ts', inplace=True)
240
  df.sort_index(inplace=True)
241
 
 
242
  tf_map = {
243
  "1min": "1min", "3min": "3min", "5min": "5min",
244
  "15min": "15min", "30min": "30min", "1hour": "1h"
 
247
 
248
  agg_dict = {
249
  "P": "last", "V": "sum", "OI": "last", "B": "last", "S": "last",
250
+ "CB": "last", "CS": "last", "PB": "last", "PS": "last", "details": "last"
 
251
  }
252
 
253
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
254
+ resampled = df.resample(panda_tf).agg(existing_agg)
255
 
256
+ for col in resampled.columns:
257
+ if col == "details":
258
+ resampled[col] = resampled[col].ffill()
259
+ details_list = [d if isinstance(d, list) else None for d in resampled[col]]
260
+ resampled[col] = details_list
261
+ else:
262
+ resampled[col] = resampled[col].ffill().fillna(0)
263
 
264
+ # Build response payload
265
+ response_data = {"labels": resampled.index.strftime('%H:%M').tolist()}
 
266
 
267
+ for var_key in["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS", "details"]:
268
+ if var_key in resampled.columns and var_key in required_vars + ["details"]:
269
+ if var_key == "details":
270
+ response_data[var_key] = resampled[var_key].tolist()
271
+ else:
272
+ response_data[var_key] =[round(float(v), 2) for v in resampled[var_key].tolist()]
 
273
 
274
  return JSONResponse(response_data)
275
 
 
357
 
358
  .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
359
  .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
 
360
 
361
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
362
  .empty-state {
 
392
  box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
393
  }
394
  .custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
 
395
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
396
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
397
 
398
+ /* SERIES ROWS */
399
  .series-row {
400
  background: var(--bg-panel); padding: 12px; border-radius: 8px;
401
  border: 1px solid var(--border); margin-bottom: 10px;
 
403
  .series-row-top { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
404
  .series-row-bottom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
405
  .series-row-cond { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px; border: 1px dashed var(--border); }
 
406
  .s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-label-true, .s-label-false {
407
  background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
408
  border-radius: 6px; padding: 0 10px; height: 30px; font-size: 12px; transition: 0.2s;
409
  }
410
+ .color-input { padding: 0; width: 34px; border: 1px solid var(--border); background: var(--bg-input); height: 30px; cursor: pointer; border-radius: 6px; overflow: hidden; }
 
 
 
 
 
 
 
 
411
 
412
+ .segmented-control { display: flex; background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px; }
413
+ .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; transition: 0.2s; border-radius: 4px; }
 
 
 
 
 
 
 
414
  .seg-btn.active { background: var(--bg-panel); color: var(--accent); }
415
 
416
+ .info-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel); color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help; border: 1px solid var(--border); margin-left: 8px; transition: 0.2s; }
 
 
 
 
 
 
417
 
418
+ /* BREAKDOWN TABLE STYLES */
419
+ .breakdown-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; }
420
+ .breakdown-table th { background: var(--bg-panel); color: var(--text-secondary); padding: 6px 10px; border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0; }
 
 
 
 
 
421
  .breakdown-table th:first-child { text-align: left; }
422
+ .breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
423
  .breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
424
+ .breakdown-table tr:hover td { background: var(--bg-panel); }
425
  .val-pos { color: #3fb950; }
426
  .val-neg { color: #f85149; }
427
+ .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
428
+
429
+ .expand-btn { cursor: pointer; color: var(--accent); font-weight: 700; padding: 0 4px; border: 1px solid var(--border); border-radius: 4px; background: transparent; margin-right: 6px; }
430
+ .expand-btn:hover { background: rgba(47,129,247,0.1); }
431
+ .sub-table-row { display: none; background: #0a0d12; border-bottom: 2px solid var(--border); }
432
+ .sub-table { width: 100%; border-collapse: collapse; font-size: 10px; margin: 8px 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden;}
433
+ .sub-table th, .sub-table td { padding: 4px 8px; border: 1px solid #1f242b; text-align: right; }
434
+ .sub-table th { background: #161b22; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px;}
435
+ .sub-table-wrapper { padding: 4px 10px 10px 32px; }
436
  </style>
437
  </head>
438
  <body>
 
505
  <label>Chart Mode</label>
506
  <div class="segmented-control" id="cfgModeToggle">
507
  <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
508
+ <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
509
  </div>
510
  </div>
511
 
 
516
  <div class="input-arrow">▼</div>
517
  <div id="dd-cfgInst" class="custom-dd"></div>
518
  </div>
 
 
 
519
  </div>
520
 
521
  <div id="cfgAdvancedFields" style="display:none;">
522
  <div class="form-section" style="margin-top:12px;">
523
+ <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
524
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
525
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
526
+ Strictly Aggregates 1 ATM + N ITM + N OTM options. (e.g., Range 5 = 11 strikes total).
527
  </div>
528
  </div>
529
  </div>
 
563
  <div class="form-section">
564
  <div style="display:flex; align-items:center; margin-bottom:6px;">
565
  <label style="margin:0;">Plot Lines</label>
566
+ <span class="info-icon" title="Variables: $P=Price, $B=Buy, $S=Sell (Fut/Eq) | $CB=Call Buy, $CS=Call Sell, $PB=Put Buy, $PS=Put Sell (Options)">i</span>
567
  </div>
568
  <div id="cfgSeries"></div>
569
  <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
 
638
  const CustomDD = {
639
  data: { cfgRoot:[], cfgExp: [], cfgInst: [] },
640
  recent: { cfgRoot:[] },
 
641
  init() {
642
  const saved = localStorage.getItem('dc_recent_symbols');
643
  if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
 
675
  .filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
676
  .filter(item => item.val.toLowerCase().includes(q));
677
 
678
+ if (!filtered.length) { el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary);">No matches</div>'; return; }
679
+
 
 
680
  let html = '', currentGroup = null;
681
  filtered.forEach(item => {
682
  if (item.group && item.group !== currentGroup) {
 
707
  // ===================================================
708
  const App = {
709
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
 
710
  presets: {
711
+ "Basic Flow (Fut/Eq)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
712
+ "Net Flow (Fut/Eq)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }],
713
+ "Call Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
714
+ "Put Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }],
715
+ "Aggregated Flow (Options)":[{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }],
716
+ "Price & Flows (Advanced)":[{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' }, { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }],
717
+ "Net Flow (Advanced)":[{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' }, { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  },
719
 
720
  async init() {
 
724
  if (savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e) {}
725
 
726
  const saved = localStorage.getItem('dc_pro_v4_state');
727
+ if (saved) { try { const s = JSON.parse(saved); this.state = { ...this.state, ...s }; } catch(e) { this.initDefault(); } }
728
+ else this.initDefault();
 
 
 
 
 
 
729
 
730
  const dSel = document.getElementById('globalDate');
731
+ if (this.state.date && Array.from(dSel.options).some(o => o.value === this.state.date)) dSel.value = this.state.date;
732
+ else if (dSel.options.length) this.state.date = dSel.options[0].value;
 
 
733
 
734
  document.getElementById('globalMode').value = this.state.mode;
735
+ this.updateModeUI(); this.renderGrid(); this.startLoop();
 
 
736
  },
737
 
738
  initDefault() {
739
+ this.state.columns = 3; this.state.layout = [[], [],[]]; this.state.refreshRate = 15;
740
+ for (let i = 0; i < 3; i++) { this.addChartInternal(i); this.addChartInternal(i); }
 
 
 
 
 
741
  },
742
 
743
  async loadDates() {
744
  try {
745
+ const res = await fetch('/api/dates'); const dates = await res.json();
 
746
  document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
747
  if (!this.state.date && dates.length) this.state.date = dates[0];
748
  } catch(e) {}
 
754
  this.state.charts[id] = { config: {}, instance: null, lastData: null };
755
  return id;
756
  },
757
+
758
  save() {
759
  const cleanCharts = {};
760
  for (let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
 
764
  refreshRate: this.state.refreshRate, charts: cleanCharts
765
  }));
766
  },
767
+
768
  reset() { if (confirm('Reset Workspace?')) { localStorage.removeItem('dc_pro_v4_state'); location.reload(); } },
769
+
770
  setLayout(n) {
771
  const all = this.state.layout.flat();
772
+ this.state.columns = n; this.state.layout = Array.from({ length: n }, () => []);
 
773
  all.forEach((id, i) => this.state.layout[i % n].push(id));
774
  this.renderGrid(); this.save();
775
  },
776
 
777
  renderGrid() {
778
  document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i + 1) === this.state.columns));
779
+ const container = document.getElementById('gridRoot'); container.innerHTML = '';
 
 
780
  this.state.layout.forEach((ids, colIdx) => {
781
+ const col = document.createElement('div'); col.className = 'column';
782
+ const viewH = window.innerHeight - 52; const count = ids.length;
 
 
 
783
  ids.forEach(id => {
784
+ if (this.state.charts[id]?.instance) { this.state.charts[id].instance.destroy(); this.state.charts[id].instance = null; }
 
 
 
785
  const card = this.createCardDOM(id);
786
  card.style.height = (count > 0 && count <= 3) ? `calc(${viewH / count}px - 1px)` : '300px';
787
  col.appendChild(card);
788
  setTimeout(() => ChartLogic.plot(id), 0);
789
  });
790
+ const btn = document.createElement('button'); btn.className = 'add-chart-btn'; btn.innerText = '+ Add Chart';
 
 
 
791
  btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
792
+ col.appendChild(btn); container.appendChild(col);
 
793
  });
794
  },
795
 
796
  createCardDOM(id) {
797
+ const div = document.createElement('div'); div.className = 'chart-card'; div.id = `card-${id}`;
798
+ const cfg = this.state.charts[id]?.config || {}; const isCfg = cfg && cfg.root;
799
+ const modeTag = cfg.chartMode === 'advanced' ? `<span class="inst-label">ATM ± ${cfg.atmRange || 5}</span>` : `<span class="inst-label">${cfg.instrument || ''}</span>`;
 
 
 
 
 
 
 
800
  div.innerHTML = `
801
  <div class="card-header">
802
  <div class="card-info">
 
838
  const loop = () => {
839
  if (this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
840
  setTimeout(loop, (this.state.refreshRate || 15) * 1000);
841
+ }; loop();
 
842
  }
843
  };
844
 
 
867
  method: 'POST',
868
  headers: { 'Content-Type': 'application/json' },
869
  body: JSON.stringify({
870
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry, instrument: cfg.instrument || '',
871
+ timeframe: cfg.timeframe, start_time: cfg.start, end_time: endTime,
872
+ mode: cfg.chartMode || 'normal', atm_range: cfg.atmRange || 5, required_vars: requiredVars
 
 
 
 
 
 
 
873
  })
874
  });
875
  const data = await res.json();
 
879
  const b = document.getElementById(`body-${id}`);
880
  if (b) b.innerHTML = `<div class="empty-state" style="opacity:0.8;"><span style="font-size:20px;">⚠️</span><br/>${data.error || 'No Data Found'}</div>`;
881
  }
882
+ if (dot) dot.classList.remove('retry'); return;
 
883
  }
884
 
885
  chart.lastData = data;
 
888
  if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
889
 
890
  const datasets = (cfg.series ||[]).map(s => {
891
+ const vals = data.labels.map((_, i) => evalFormula(s.formula, {
892
+ P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
893
+ CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
894
+ PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
895
+ }));
 
 
 
 
896
 
897
  const ds = {
898
  label: s.label, data: vals, borderWidth: 1.5, pointRadius: 0,
 
901
 
902
  if (s.cond && s.cond !== 'none') {
903
  const thresh = parseFloat(s.thresh) || 0;
904
+ const cTrue = s.color1 || '#238636'; const cFalse = s.color2 || '#da3633';
 
905
  const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
 
906
  ds.segment = { borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse };
907
  ds.borderColor = cTrue; ds.backgroundColor = cTrue;
908
  ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
 
913
  }
914
  return ds;
915
  });
 
916
  this.render(id, data.labels, datasets, cfg.series ||[]);
917
 
918
  } catch(e) { console.error('ChartLogic.plot error:', e); }
919
  },
920
 
921
  render(id, labels, datasets, seriesConfig) {
922
+ const ctx = document.getElementById(`canvas-${id}`); if (!ctx) return;
 
923
  const chart = App.state.charts[id];
924
 
925
+ if (chart.instance && chart.instance.ctx?.canvas !== ctx) { chart.instance.destroy(); chart.instance = null; }
 
 
926
 
927
  if (chart.instance) {
928
+ chart.instance.data.labels = labels; chart.instance.data.datasets = datasets;
 
929
  chart.instance.update('none');
930
  } else {
931
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
 
937
  plugins: {
938
  legend: { display: false },
939
  tooltip: {
940
+ backgroundColor: 'rgba(22,27,34,0.97)', titleColor: '#e6edf3', bodyColor: '#8b949e',
 
941
  borderColor: '#30363d', borderWidth: 1, padding: 10,
942
  callbacks: {
943
  label: function(context) {
944
+ const s = seriesConfig[context.datasetIndex]; if (!s) return '';
945
+ const v = context.parsed.y; const fv = v !== null ? v.toFixed(2) : 'N/A';
 
 
946
  let condText = '';
947
  if (s.cond && s.cond !== 'none') {
948
  const thresh = parseFloat(s.thresh) || 0;
 
953
  return `${s.label}: ${fv}${condText}`;
954
  },
955
  labelColor: function(context) {
956
+ const s = seriesConfig[context.datasetIndex]; if (!s) return { borderColor: '#fff', backgroundColor: '#fff' };
 
957
  let c = s.color1 || '#fff';
958
  if (s.cond && s.cond !== 'none') {
959
+ const thresh = parseFloat(s.thresh) || 0; const v = context.parsed.y;
960
+ const isTrue = s.cond === '>' ? v > thresh : v < thresh; c = isTrue ? (s.color1 || '#238636') : (s.color2 || '#da3633');
 
961
  }
962
  return { borderColor: c, backgroundColor: c };
963
  }
 
976
  };
977
 
978
  // ===================================================
979
+ // BREAKDOWN TABLE
980
  // ===================================================
981
  const Breakdown = {
982
  open(id) {
983
  const chart = App.state.charts[id];
984
+ if (!chart || !chart.lastData || !chart.config.series) return alert('No data loaded yet.');
 
 
 
 
 
 
 
 
 
 
985
 
986
+ const data = chart.lastData; const cfg = chart.config; const series = cfg.series;
987
  const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
988
  const presentVars = allVarKeys.filter(k => data[k] && data[k].some(v => v && v !== 0));
989
 
 
990
  let thead = '<thead><tr><th>Time</th>';
 
991
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
992
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
993
  thead += '</tr></thead>';
994
 
 
995
  let tbody = '<tbody>';
996
  data.labels.forEach((lbl, i) => {
997
  const vars = {
998
+ P: safeVal(data, 'P', i), B: safeVal(data, 'B', i), S: safeVal(data, 'S', i),
999
  CB: safeVal(data, 'CB', i), CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
1000
+ PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i), OI: safeVal(data, 'OI', i)
1001
  };
1002
 
1003
+ const hasDetails = data.details && data.details[i] && data.details[i] !== null;
1004
+ const expBtn = hasDetails ? `<button class="expand-btn" onclick="Breakdown.toggleRow('sub-${id}-${i}')">+</button>` : '';
1005
+
1006
+ tbody += `<tr><td>${expBtn} ${lbl}</td>`;
 
 
 
 
 
 
1007
 
1008
  presentVars.forEach(k => {
1009
+ const v = vars[k]; const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
 
1010
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1011
  });
1012
 
1013
  series.forEach(s => {
1014
+ const rv = evalFormula(s.formula, vars) || 0;
 
1015
  let cls = rv > 0 ? 'val-pos' : (rv < 0 ? 'val-neg' : '');
1016
  let condLabel = '';
1017
  if (s.cond && s.cond !== 'none') {
 
1023
  }
1024
  tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
1025
  });
 
1026
 
1027
+ tbody += '</tr>';
 
 
 
 
 
 
 
 
1028
 
1029
+ // Individual Sub-Calculation Strike Detail Injection
1030
+ if (hasDetails) {
1031
+ const det = data.details[i];
1032
+ const strikesMap = {};
1033
+ det.forEach(d => {
1034
+ if (!strikesMap[d.k]) strikesMap[d.k] = { CE: null, PE: null };
1035
+ strikesMap[d.k][d.t] = d;
1036
  });
1037
+ const sortedStrikes = Object.keys(strikesMap).map(Number).sort((a,b) => a-b);
1038
+
1039
+ let subHtml = `<div class="sub-table-wrapper"><table class="sub-table">
1040
+ <tr><th>STRIKE</th><th>CE BUY</th><th>CE SELL</th><th>PE BUY</th><th>PE SELL</th><th>CE VOL</th><th>PE VOL</th></tr>`;
1041
+
1042
+ sortedStrikes.forEach(k => {
1043
+ const ce = strikesMap[k].CE || {b:0, s:0, v:0};
1044
+ const pe = strikesMap[k].PE || {b:0, s:0, v:0};
1045
+ subHtml += `<tr>
1046
+ <td style="text-align:center; font-weight:bold; color:var(--text-primary); background:#161b22; border-right:1px solid var(--border);">${k}</td>
1047
+ <td class="${ce.b>0?'val-pos':''}">${ce.b}</td><td class="${ce.s>0?'val-neg':''}">${ce.s}</td>
1048
+ <td class="${pe.b>0?'val-pos':''}">${pe.b}</td><td class="${pe.s>0?'val-neg':''}">${pe.s}</td>
1049
+ <td>${ce.v}</td><td>${pe.v}</td>
 
 
1050
  </tr>`;
1051
  });
1052
  subHtml += `</table></div>`;
1053
+ tbody += `<tr id="sub-${id}-${i}" class="sub-table-row"><td colspan="100" style="padding:0;">${subHtml}</td></tr>`;
 
 
1054
  }
1055
  });
1056
  tbody += '</tbody>';
1057
 
1058
+ document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1059
  document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1060
  document.getElementById('bdTable').innerHTML = thead + tbody;
1061
  document.getElementById('breakdownModal').style.display = 'flex';
1062
  },
1063
 
1064
+ toggleRow(rowId) {
1065
+ const el = document.getElementById(rowId);
1066
+ if(el) el.style.display = el.style.display === 'table-row' ? 'none' : 'table-row';
 
 
1067
  }
1068
  };
1069
 
 
1080
  btns[1].classList.toggle('active', mode === 'advanced');
1081
  document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
1082
  document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
 
1083
  this.updatePresetsDropdown();
1084
  const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
1085
  this.loadPreset(defPreset);
 
1090
  },
1091
  loadPreset(presetName = null) {
1092
  const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1093
+ if (name && App.presets[name]) { document.getElementById('cfgPresetsDropdown').value = name; this.renderSeries(App.presets[name]); }
 
 
 
1094
  },
1095
  savePreset() {
1096
  const name = prompt('Name for this model:', 'Custom Model');
1097
  if (!name) return;
1098
  App.presets[name] = this.extractSeriesFromDOM();
1099
  localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1100
+ this.updatePresetsDropdown(); document.getElementById('cfgPresetsDropdown').value = name;
 
1101
  },
1102
  deletePreset() {
1103
  const name = document.getElementById('cfgPresetsDropdown').value;
1104
+ const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)', 'Put Flow (Options)', 'Aggregated Flow (Options)', 'Price & Flows (Advanced)', 'Net Flow (Advanced)'];
 
 
1105
  if (locked.includes(name)) return alert('Cannot delete default models.');
1106
+ if (confirm(`Delete model '${name}'?`)) { delete App.presets[name]; localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets)); this.updatePresetsDropdown(); this.loadPreset(); }
 
 
 
 
 
1107
  },
1108
 
1109
  async open(id) {
1110
  this.tid = id; this.isGlobal = false;
1111
+ document.getElementById('configModal').style.display = 'flex'; document.getElementById('cfgTitle').innerText = 'Configure Chart';
1112
+ document.getElementById('cfgLoading').style.display = 'block'; document.getElementById('cfgContent').style.display = 'none';
1113
+ document.getElementById('cfgGlobalOnly').style.display = 'none'; document.getElementById('cfgChartOnly').style.display = 'block';
 
 
 
 
 
1114
 
1115
+ this.updatePresetsDropdown(); CustomDD.clear('cfgRoot');
1116
+ const res = await fetch(`/api/roots?date=${App.state.date}`); const roots = await res.json();
 
1117
  CustomDD.set('cfgRoot', roots);
1118
 
1119
  const cfg = App.state.charts[id].config;
1120
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1121
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
1122
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
1123
  document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1124
 
1125
  this.currentMode = cfg.chartMode || 'normal';
1126
  const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1127
+ btns[0].classList.toggle('active', this.currentMode === 'normal'); btns[1].classList.toggle('active', this.currentMode === 'advanced');
 
1128
  document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1129
  document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
 
1130
 
1131
  if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
1132
  else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
1133
 
1134
+ document.getElementById('cfgRoot').value = cfg.root || ''; document.getElementById('cfgExp').value = cfg.expiry || ''; document.getElementById('cfgInst').value = cfg.instrument || '';
 
 
 
1135
  if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
1136
+ document.getElementById('cfgLoading').style.display = 'none'; document.getElementById('cfgContent').style.display = 'block';
 
 
1137
  },
1138
 
1139
  openDefaults() {
1140
+ this.isGlobal = true; document.getElementById('configModal').style.display = 'flex';
1141
+ document.getElementById('cfgTitle').innerText = 'Global Settings'; document.getElementById('cfgLoading').style.display = 'none';
1142
+ document.getElementById('cfgContent').style.display = 'block'; document.getElementById('cfgGlobalOnly').style.display = 'block';
1143
+ document.getElementById('cfgChartOnly').style.display = 'none'; document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
 
 
 
 
1144
  this.updatePresetsDropdown();
1145
  },
1146
+
1147
  close() { document.getElementById('configModal').style.display = 'none'; },
1148
 
1149
  async onRootChange(preExp, preInst) {
 
1153
  if (!preInst) document.getElementById('cfgInst').value = '';
1154
  if (!root) return;
1155
 
1156
+ const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`); const exps = await res.json();
 
1157
  CustomDD.set('cfgExp', exps);
1158
 
1159
+ if (preExp && exps.includes(preExp)) { document.getElementById('cfgExp').value = preExp; await this.onExpChange(preInst); }
1160
+ else if (exps.length > 0) {
 
 
1161
  const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
1162
+ if (auto.current) { document.getElementById('cfgExp').value = auto.current.expiry; await this.onExpChange(auto.current.instrument); }
 
 
 
1163
  }
1164
  },
1165
 
1166
  async onExpChange(preInst) {
1167
+ const root = document.getElementById('cfgRoot').value.toUpperCase(); const exp = document.getElementById('cfgExp').value.toUpperCase();
1168
+ CustomDD.clear('cfgInst'); if (!root || !exp) return;
1169
+ const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`); const data = await res.json();
 
 
 
 
1170
  if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1171
  if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1172
  if (preInst) document.getElementById('cfgInst').value = preInst;
1173
  },
1174
 
1175
+ toggleCond(sel) { const condGroup = sel.closest('.series-row').querySelector('.series-row-cond'); if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex'; },
 
 
 
1176
 
1177
+ renderSeries(list) { const c = document.getElementById('cfgSeries'); c.innerHTML = ''; list.forEach(s => this._appendSeriesRow(c, s)); },
 
 
 
 
1178
 
1179
  _appendSeriesRow(container, s = {}) {
1180
+ const r = document.createElement('div'); r.className = 'series-row';
 
1181
  const showCond = s.cond && s.cond !== 'none';
1182
  r.innerHTML = `
1183
  <div class="series-row-top">
1184
  <input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
1185
  <input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
1186
+ <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>
 
 
 
1187
  <button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
1188
  </div>
1189
  <div class="series-row-bottom">
1190
  <input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
1191
+ <select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)"><option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option><option value=">" ${s.cond === '>' ? 'selected' : ''}>If &gt;</option><option value="<" ${s.cond === '<' ? 'selected' : ''}>If &lt;</option></select>
 
 
 
 
1192
  </div>
1193
  <div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
1194
+ <span class="cond-label">Threshold:</span><input type="number" class="s-thresh" value="${s.thresh || 0}" style="width:65px;">
1195
+ <span class="cond-label">True →</span><input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True"><input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
1196
+ <span class="cond-label">False →</span><input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False"><input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
 
 
 
 
 
1197
  </div>
1198
  `;
1199
+ const solidColor = r.querySelector('.s-color1'); const condTrueColor = r.querySelector('.s-color1-cond');
 
1200
  solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
1201
  container.appendChild(r);
1202
  },
 
1207
  return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
1208
  const cond = r.querySelector('.s-cond')?.value || 'none';
1209
  return {
1210
+ formula: r.querySelector('.s-form')?.value || '$P', label: r.querySelector('.s-lbl')?.value || 'Line', axis: r.querySelector('.s-axis')?.value || 'left',
1211
+ color1: r.querySelector('.s-color1-cond')?.value || r.querySelector('.s-color1')?.value || '#2f81f7', cond,
1212
+ thresh: r.querySelector('.s-thresh')?.value || '0', color2: r.querySelector('.s-color2')?.value || '#da3633',
1213
+ labelTrue: r.querySelector('.s-label-true')?.value || '', labelFalse: r.querySelector('.s-label-false')?.value || ''
 
 
 
 
 
1214
  };
1215
  });
1216
  },
1217
 
1218
  saveChartConfig() {
1219
+ if (this.isGlobal) { App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15; App.save(); this.close(); return; }
1220
+ const root = document.getElementById('cfgRoot').value.toUpperCase(); const exp = document.getElementById('cfgExp').value.toUpperCase();
 
 
 
 
 
 
 
1221
  const inst = document.getElementById('cfgInst').value.toUpperCase();
 
1222
  if (!root || !exp) return alert('Please select a Symbol and Expiry.');
1223
+ const series = this.extractSeriesFromDOM(); if (!series.length) return alert('Add at least one plot line.');
 
 
1224
 
1225
  App.state.charts[this.tid].config = {
1226
+ chartMode: this.currentMode, atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1227
+ root, expiry: exp, instrument: inst, timeframe: document.getElementById('cfgTF').value,
1228
+ start: document.getElementById('cfgStart').value, end: document.getElementById('cfgEnd').value, series
 
 
 
 
1229
  };
1230
+ App.save(); App.renderGrid(); ChartLogic.plot(this.tid); this.close();
 
 
 
 
1231
  }
1232
  };
1233