topsecrettraders commited on
Commit
db853e1
·
verified ·
1 Parent(s): 9cecb90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -514
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
 
@@ -111,7 +111,7 @@ def get_instruments(date: str, root: str, expiry: str):
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,11 +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
- # 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,22 +151,21 @@ async def fetch_series(
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:
160
- spot_keys = [k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
161
  fut_keys = [k for k in instruments if 'FUT' in k]
162
  if spot_keys:
163
  ref_instrument = spot_keys[0]
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
- # Use the new function with breakdown
171
- rpc_name = "get_advanced_chart_data_with_breakdown"
172
  base_params = {
173
  "p_root": root,
174
  "p_expiry": expiry,
@@ -179,7 +176,7 @@ async def fetch_series(
179
 
180
  else:
181
  if not instrument:
182
- return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P": []})
183
 
184
  rpc_name = "get_normal_chart_data"
185
  base_params = {
@@ -198,7 +195,7 @@ async def fetch_series(
198
  loop = asyncio.get_event_loop()
199
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
200
 
201
- tasks = []
202
  curr = t_start_dt
203
  while curr <= t_end_dt:
204
  nxt = curr + timedelta(hours=2) - timedelta(seconds=1)
@@ -209,13 +206,13 @@ async def fetch_series(
209
 
210
  chunk_results = await asyncio.gather(*tasks)
211
 
212
- all_records = []
213
  for res in chunk_results:
214
  if isinstance(res, list):
215
  all_records.extend(res)
216
 
217
  if not all_records:
218
- return JSONResponse({"error": "No valid data found in range.", "labels": [], "P": []})
219
 
220
  # =========================================================
221
  # DATA PROCESSING
@@ -247,94 +244,30 @@ async def fetch_series(
247
  }
248
  panda_tf = tf_map.get(timeframe, "1min")
249
 
250
- # KEY FIX: Use 'last' for quantity fields — these are cumulative running totals.
251
- # The LAST value in any period is the most current/accurate snapshot.
252
  agg_dict = {
253
- "P": "last",
254
- "V": "sum", # Volume: summing new volume per candle makes sense
255
- "OI": "last", # OI: last value
256
- "B": "last", # Buy qty cumulative total → last value
257
- "S": "last", # Sell qty cumulative total → last value
258
- "CB": "last", # Call Buy → last value
259
- "CS": "last", # Call Sell → last value
260
- "PB": "last", # Put Buy → last value
261
- "PS": "last", # Put Sell → last value
262
  }
263
 
264
- # Handle optional columns from advanced mode
265
- if 'atm_strike' in df.columns:
266
- agg_dict['atm_strike'] = 'last'
267
- if 'strike_details' in df.columns:
268
- agg_dict['strike_details'] = 'last'
269
- if 'range_info' in df.columns:
270
- agg_dict['range_info'] = 'last'
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
- # Build response: only include requested variables (reduces egress)
276
  response_data = {
277
  "labels": resampled.index.strftime('%H:%M').tolist()
278
  }
279
 
280
- for var_key in ["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
281
  if var_key in required_vars and var_key in resampled.columns:
282
  response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
283
- # Omit entirely if not requested — JS handles missing keys gracefully
284
-
285
- # Include breakdown data for advanced mode
286
- if mode == "advanced":
287
- # Add ATM strike for each bar
288
- if 'atm_strike' in resampled.columns:
289
- response_data['atm_strike'] = [round(float(v), 2) if v else 0 for v in resampled['atm_strike'].tolist()]
290
-
291
- # Add per-bar breakdown data
292
- if 'strike_details' in resampled.columns:
293
- response_data['strike_details'] = resampled['strike_details'].tolist()
294
-
295
- if 'range_info' in resampled.columns:
296
- response_data['range_info'] = resampled['range_info'].tolist()
297
-
298
- # Build comprehensive strike breakdown per minute
299
- breakdown_per_minute = []
300
- for idx, row in resampled.iterrows():
301
- minute_data = {
302
- "time": idx.strftime('%H:%M'),
303
- "spot_price": round(float(row.get('P', 0)), 2) if row.get('P') else 0,
304
- "atm_strike": round(float(row.get('atm_strike', 0)), 2) if row.get('atm_strike') else 0,
305
- "cb": round(float(row.get('CB', 0)), 2) if row.get('CB') else 0,
306
- "cs": round(float(row.get('CS', 0)), 2) if row.get('CS') else 0,
307
- "pb": round(float(row.get('PB', 0)), 2) if row.get('PB') else 0,
308
- "ps": round(float(row.get('PS', 0)), 2) if row.get('PS') else 0,
309
- "strikes": []
310
- }
311
 
312
- # Process strike details if available
313
- strike_details = row.get('strike_details')
314
- if strike_details and isinstance(strike_details, list):
315
- for sd in strike_details:
316
- if isinstance(sd, dict):
317
- minute_data["strikes"].append({
318
- "strike": sd.get('strike', 0),
319
- "type": sd.get('type', ''),
320
- "instrument": sd.get('instrument', ''),
321
- "buy": sd.get('buy', 0),
322
- "sell": sd.get('sell', 0),
323
- "vol": sd.get('vol', 0),
324
- "oi": sd.get('oi', 0),
325
- "ltp": sd.get('ltp', 0),
326
- "call_status": sd.get('call_status'),
327
- "put_status": sd.get('put_status')
328
- })
329
-
330
- breakdown_per_minute.append(minute_data)
331
-
332
- response_data['breakdown_per_minute'] = breakdown_per_minute
333
-
334
- # Add range info if available (just the last one as it's constant)
335
- range_info = resampled['range_info'].iloc[-1] if 'range_info' in resampled.columns and len(resampled) > 0 else None
336
- if range_info:
337
- response_data['atm_range_info'] = range_info
338
 
339
  return JSONResponse(response_data)
340
 
@@ -422,6 +355,7 @@ HTML_TEMPLATE = """
422
 
423
  .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
424
  .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
 
425
 
426
  .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
427
  .empty-state {
@@ -442,7 +376,7 @@ HTML_TEMPLATE = """
442
  width: 560px; max-width: 95%; max-height: 88vh; display: flex; flex-direction: column;
443
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
444
  }
445
- .modal.wide { width: 950px; }
446
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
447
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
448
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
@@ -461,7 +395,6 @@ HTML_TEMPLATE = """
461
  .custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
462
  .custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
463
 
464
- /* SERIES ROWS */
465
  .series-row {
466
  background: var(--bg-panel); padding: 12px; border-radius: 8px;
467
  border: 1px solid var(--border); margin-bottom: 10px;
@@ -482,10 +415,8 @@ HTML_TEMPLATE = """
482
  }
483
  .color-input::-webkit-color-swatch-wrapper { padding: 0; }
484
  .color-input::-webkit-color-swatch { border: none; }
485
-
486
  .cond-label { font-size: 10px; color: var(--text-secondary); font-weight: 600; white-space: nowrap; }
487
 
488
- /* SEGMENTED */
489
  .segmented-control {
490
  display: flex; background: var(--bg-body); border: 1px solid var(--border);
491
  border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
@@ -507,111 +438,26 @@ HTML_TEMPLATE = """
507
 
508
  /* BREAKDOWN TABLE */
509
  .breakdown-table {
510
- width: 100%; border-collapse: collapse; font-family: var(--font-mono);
511
- font-size: 11px;
512
  }
513
  .breakdown-table th {
514
- background: var(--bg-panel); color: var(--text-secondary); padding: 6px 10px;
515
  border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0;
516
  }
517
  .breakdown-table th:first-child { text-align: left; }
518
- .breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
519
  .breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
520
- .breakdown-table tr:hover td { background: var(--bg-panel); }
521
  .val-pos { color: #3fb950; }
522
  .val-neg { color: #f85149; }
523
- .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
524
-
525
- /* STRIKE BREAKDOWN STYLES */
526
- .strike-section {
527
- margin-top: 10px;
528
- border: 1px solid var(--border);
529
- border-radius: 8px;
530
- overflow: hidden;
531
- }
532
- .strike-header {
533
- background: var(--bg-panel);
534
- padding: 8px 12px;
535
- font-weight: 700;
536
- font-size: 11px;
537
- cursor: pointer;
538
- display: flex;
539
- justify-content: space-between;
540
- align-items: center;
541
- border-bottom: 1px solid var(--border);
542
- }
543
- .strike-header:hover { background: var(--bg-input); }
544
- .strike-header .arrow { transition: transform 0.2s; }
545
- .strike-header.collapsed .arrow { transform: rotate(-90deg); }
546
- .strike-content {
547
- max-height: 400px;
548
- overflow-y: auto;
549
- transition: max-height 0.3s;
550
- }
551
- .strike-content.collapsed { max-height: 0; overflow: hidden; }
552
- .strike-table {
553
- width: 100%;
554
- border-collapse: collapse;
555
- font-family: var(--font-mono);
556
- font-size: 10px;
557
- }
558
- .strike-table th {
559
- background: var(--bg-body);
560
- padding: 6px 8px;
561
- text-align: right;
562
- border-bottom: 1px solid var(--border);
563
- color: var(--text-secondary);
564
- font-weight: 600;
565
- }
566
- .strike-table th:first-child { text-align: left; }
567
- .strike-table td {
568
- padding: 5px 8px;
569
- text-align: right;
570
- border-bottom: 1px solid var(--border);
571
- }
572
- .strike-table td:first-child { text-align: left; }
573
- .strike-table .ce-row { background: rgba(47,129,247,0.05); }
574
- .strike-table .pe-row { background: rgba(218,54,51,0.05); }
575
- .strike-table .atm-strike { background: rgba(210,153,34,0.15); font-weight: 700; }
576
- .strike-table .itm { color: var(--success); }
577
- .strike-table .otm { color: var(--text-secondary); }
578
-
579
- .minute-accordion {
580
- border: 1px solid var(--border);
581
- border-radius: 6px;
582
- margin-bottom: 8px;
583
- overflow: hidden;
584
- }
585
- .minute-header {
586
- background: var(--bg-panel);
587
- padding: 10px 12px;
588
- cursor: pointer;
589
- display: grid;
590
- grid-template-columns: 60px 1fr 1fr 1fr 1fr 1fr;
591
- gap: 10px;
592
- align-items: center;
593
- font-weight: 600;
594
- font-size: 11px;
595
- }
596
- .minute-header:hover { background: var(--bg-input); }
597
- .minute-detail {
598
- background: var(--bg-card);
599
- padding: 12px;
600
- display: none;
601
- border-top: 1px solid var(--border);
602
- }
603
- .minute-detail.show { display: block; }
604
-
605
- .range-info-box {
606
- background: var(--bg-panel);
607
- border: 1px solid var(--accent);
608
- border-radius: 6px;
609
- padding: 10px 14px;
610
- margin-bottom: 12px;
611
- font-size: 11px;
612
- }
613
- .range-info-box .label { color: var(--text-secondary); }
614
- .range-info-box .value { color: var(--accent); font-weight: 700; font-family: var(--font-mono); }
615
  </style>
616
  </head>
617
  <body>
@@ -684,7 +530,7 @@ HTML_TEMPLATE = """
684
  <label>Chart Mode</label>
685
  <div class="segmented-control" id="cfgModeToggle">
686
  <button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
687
- <button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
688
  </div>
689
  </div>
690
 
@@ -696,19 +542,16 @@ HTML_TEMPLATE = """
696
  <div id="dd-cfgInst" class="custom-dd"></div>
697
  </div>
698
  <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
699
- * Advanced Mode: select the Spot/Future as price reference. All CE/PE within ATM ± range will be aggregated.
700
  </div>
701
  </div>
702
 
703
  <div id="cfgAdvancedFields" style="display:none;">
704
  <div class="form-section" style="margin-top:12px;">
705
- <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
706
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
707
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
708
- <strong>Corrected Logic:</strong><br>
709
- • For Calls: N ITM (below ATM) + N OTM (ATM and above) = 2N strikes<br>
710
- • For Puts: N ITM (ATM and above) + N OTM (below ATM) = 2N strikes<br>
711
- • Both CE and PE at same strike are always included together.
712
  </div>
713
  </div>
714
  </div>
@@ -736,7 +579,6 @@ HTML_TEMPLATE = """
736
  <div style="height:1px; background:var(--border); margin:12px 0 8px;"></div>
737
  </div>
738
 
739
- <!-- PRESET MANAGER -->
740
  <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom:12px;">
741
  <label>Quick Models</label>
742
  <div style="display:flex; gap:8px;">
@@ -746,7 +588,6 @@ HTML_TEMPLATE = """
746
  </div>
747
  </div>
748
 
749
- <!-- SERIES CONFIG -->
750
  <div class="form-section">
751
  <div style="display:flex; align-items:center; margin-bottom:6px;">
752
  <label style="margin:0;">Plot Lines</label>
@@ -773,24 +614,8 @@ HTML_TEMPLATE = """
773
  </div>
774
  <div class="modal-body" style="padding:12px;">
775
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
776
- <div id="bdRangeInfo" style="display:none;"></div>
777
-
778
- <!-- TABS -->
779
- <div style="display:flex; gap:8px; margin-bottom:12px;">
780
- <button id="tabSummary" class="primary" onclick="Breakdown.showTab('summary')">Summary</button>
781
- <button id="tabStrikes" onclick="Breakdown.showTab('strikes')">Strike Breakdown</button>
782
- </div>
783
-
784
- <!-- SUMMARY TAB -->
785
- <div id="bdSummaryTab">
786
- <div class="breakdown-scroll">
787
- <table class="breakdown-table" id="bdTable"></table>
788
- </div>
789
- </div>
790
-
791
- <!-- STRIKES TAB -->
792
- <div id="bdStrikesTab" style="display:none;">
793
- <div class="breakdown-scroll" id="bdStrikesContent"></div>
794
  </div>
795
  </div>
796
  </div>
@@ -819,16 +644,14 @@ function evalFormula(formula, vars) {
819
  try { return new Function('return ' + f)(); } catch { return null; }
820
  }
821
 
822
- // Extract which variables are used in a list of series formulas
823
  function extractRequiredVars(seriesList) {
824
- const vars = new Set(['P']); // Always need price
825
  seriesList.forEach(s => {
826
  const f = s.formula || '';
827
  if (f.includes('$CB')) vars.add('CB');
828
  if (f.includes('$CS')) vars.add('CS');
829
  if (f.includes('$PB')) vars.add('PB');
830
  if (f.includes('$PS')) vars.add('PS');
831
- // Check $B / $S but NOT $CB/$CS/$PB/$PS (already handled above)
832
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
833
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
834
  if (f.includes('$V')) vars.add('V');
@@ -841,30 +664,24 @@ function extractRequiredVars(seriesList) {
841
  // CUSTOM DROPDOWN
842
  // ===================================================
843
  const CustomDD = {
844
- data: { cfgRoot: [], cfgExp: [], cfgInst: [] },
845
- recent: { cfgRoot: [] },
846
 
847
  init() {
848
  const saved = localStorage.getItem('dc_recent_symbols');
849
  if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
850
  },
851
-
852
- clear(id) { this.data[id] = []; document.getElementById(id).value = ''; },
853
-
854
  set(id, items, groupName = null) {
855
- if (!groupName)
856
- this.data[id] = items.map(i => ({ val: i, group: null }));
857
- else
858
- this.data[id].push(...items.map(i => ({ val: i, group: groupName })));
859
  },
860
-
861
  async open(id) {
862
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
863
  document.getElementById(id).select();
864
  this.render(id, '');
865
  document.getElementById(`dd-${id}`).style.display = 'block';
866
  },
867
-
868
  async filter(id) {
869
  const query = document.getElementById(id).value.toUpperCase();
870
  if (id === 'cfgRoot' && query.length >= 2) {
@@ -875,7 +692,6 @@ const CustomDD = {
875
  this.render(id, query);
876
  document.getElementById(`dd-${id}`).style.display = 'block';
877
  },
878
-
879
  render(id, query) {
880
  const el = document.getElementById(`dd-${id}`);
881
  const q = query.toLowerCase();
@@ -902,7 +718,6 @@ const CustomDD = {
902
  });
903
  el.innerHTML = html;
904
  },
905
-
906
  pick(id, val) {
907
  document.getElementById(id).value = val;
908
  document.getElementById(`dd-${id}`).style.display = 'none';
@@ -922,51 +737,47 @@ const CustomDD = {
922
  // APP STATE
923
  // ===================================================
924
  const App = {
925
- state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
926
 
927
- // ---- DEFAULT PRESETS (organized by use case) ----
928
  presets: {
929
- // --- Normal: Futures / Equity / Spot ---
930
- "Basic Flow (Fut/Eq)": [
931
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
932
  { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
933
  { formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
934
  ],
935
- "Net Flow (Fut/Eq)": [
936
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
937
  { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
938
  ],
939
- // --- Normal: Options ---
940
- "Call Flow (Options)": [
941
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
942
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
943
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
944
  ],
945
- "Put Flow (Options)": [
946
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
947
  { formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
948
  { formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
949
  ],
950
- "Aggregated Flow (Options)": [
951
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
952
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
953
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
954
  ],
955
- "Net Flow (Options)": [
956
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
957
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
958
  ],
959
- // --- Advanced: Options (aggregated across ATM range) ---
960
- "Price & Flows (Advanced)": [
961
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
962
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
963
  { formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
964
  ],
965
- "Net Flow (Advanced)": [
966
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
967
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
968
  ],
969
- "Components (Advanced)": [
970
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
971
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
972
  { formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
@@ -1005,7 +816,7 @@ const App = {
1005
 
1006
  initDefault() {
1007
  this.state.columns = 3;
1008
- this.state.layout = [[], [], []];
1009
  this.state.refreshRate = 15;
1010
  for (let i = 0; i < 3; i++) {
1011
  this.addChartInternal(i);
@@ -1028,29 +839,20 @@ const App = {
1028
  this.state.charts[id] = { config: {}, instance: null, lastData: null };
1029
  return id;
1030
  },
1031
-
1032
  save() {
1033
  const cleanCharts = {};
1034
- for (let id in this.state.charts)
1035
- cleanCharts[id] = { config: this.state.charts[id].config };
1036
  localStorage.setItem('dc_pro_v4_state', JSON.stringify({
1037
  date: this.state.date, mode: this.state.mode,
1038
  columns: this.state.columns, layout: this.state.layout,
1039
  refreshRate: this.state.refreshRate, charts: cleanCharts
1040
  }));
1041
  },
1042
-
1043
- reset() {
1044
- if (confirm('Reset Workspace?')) {
1045
- localStorage.removeItem('dc_pro_v4_state');
1046
- location.reload();
1047
- }
1048
- },
1049
-
1050
  setLayout(n) {
1051
  const all = this.state.layout.flat();
1052
  this.state.columns = n;
1053
- this.state.layout = Array.from({ length: n }, () => []);
1054
  all.forEach((id, i) => this.state.layout[i % n].push(id));
1055
  this.renderGrid(); this.save();
1056
  },
@@ -1094,7 +896,7 @@ const App = {
1094
  const cfg = this.state.charts[id]?.config || {};
1095
  const isCfg = cfg && cfg.root;
1096
  const modeTag = cfg.chartMode === 'advanced'
1097
- ? `<span class="inst-label">ATM ± ${cfg.atmRange || 5}</span>`
1098
  : `<span class="inst-label">${cfg.instrument || ''}</span>`;
1099
 
1100
  div.innerHTML = `
@@ -1129,17 +931,9 @@ const App = {
1129
  this.renderGrid(); this.save();
1130
  },
1131
 
1132
- onDateChange() {
1133
- this.state.date = document.getElementById('globalDate').value;
1134
- this.save(); this.refreshAll();
1135
- },
1136
- onModeChange() {
1137
- this.state.mode = document.getElementById('globalMode').value;
1138
- this.updateModeUI(); this.save(); this.refreshAll();
1139
- },
1140
- updateModeUI() {
1141
- document.getElementById('modeDot').className = `status-dot ${this.state.mode === 'LIVE' ? 'live' : 'hist'}`;
1142
- },
1143
  refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
1144
 
1145
  startLoop() {
@@ -1169,8 +963,7 @@ const ChartLogic = {
1169
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1170
  }
1171
 
1172
- // Determine which variables are actually needed by the series formulas
1173
- const requiredVars = extractRequiredVars(cfg.series || []);
1174
 
1175
  try {
1176
  const res = await fetch('/api/fetch_series', {
@@ -1200,37 +993,25 @@ const ChartLogic = {
1200
  return;
1201
  }
1202
 
1203
- // Store data for breakdown table
1204
  chart.lastData = data;
1205
-
1206
  const body = document.getElementById(`body-${id}`);
1207
  if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1208
  if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
1209
 
1210
- const datasets = (cfg.series || []).map(s => {
1211
  const vals = data.labels.map((_, i) => {
1212
  const vars = {
1213
- P: safeVal(data, 'P', i),
1214
- B: safeVal(data, 'B', i),
1215
- S: safeVal(data, 'S', i),
1216
- CB: safeVal(data, 'CB', i),
1217
- CS: safeVal(data, 'CS', i),
1218
- PB: safeVal(data, 'PB', i),
1219
- PS: safeVal(data, 'PS', i),
1220
- V: safeVal(data, 'V', i),
1221
- OI: safeVal(data, 'OI', i)
1222
  };
1223
  return evalFormula(s.formula, vars);
1224
  });
1225
 
1226
  const ds = {
1227
- label: s.label,
1228
- data: vals,
1229
- borderWidth: 1.5,
1230
- pointRadius: 0,
1231
- hitRadius: 10,
1232
- tension: 0.1,
1233
- yAxisID: s.axis === 'left' ? 'y' : 'y1'
1234
  };
1235
 
1236
  if (s.cond && s.cond !== 'none') {
@@ -1239,24 +1020,18 @@ const ChartLogic = {
1239
  const cFalse = s.color2 || '#da3633';
1240
  const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
1241
 
1242
- ds.segment = {
1243
- borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse
1244
- };
1245
- ds.borderColor = cTrue;
1246
- ds.backgroundColor = cTrue;
1247
  ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
1248
  ds.pointHoverBorderColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
1249
  } else {
1250
- ds.borderColor = s.color1 || '#2f81f7';
1251
- ds.backgroundColor = s.color1 || '#2f81f7';
1252
- ds.pointHoverBackgroundColor = s.color1 || '#2f81f7';
1253
- ds.pointHoverBorderColor = s.color1 || '#2f81f7';
1254
  }
1255
-
1256
  return ds;
1257
  });
1258
 
1259
- this.render(id, data.labels, datasets, cfg.series || []);
1260
 
1261
  } catch(e) { console.error('ChartLogic.plot error:', e); }
1262
  },
@@ -1264,12 +1039,10 @@ const ChartLogic = {
1264
  render(id, labels, datasets, seriesConfig) {
1265
  const ctx = document.getElementById(`canvas-${id}`);
1266
  if (!ctx) return;
1267
-
1268
  const chart = App.state.charts[id];
1269
 
1270
  if (chart.instance && chart.instance.ctx?.canvas !== ctx) {
1271
- chart.instance.destroy();
1272
- chart.instance = null;
1273
  }
1274
 
1275
  if (chart.instance) {
@@ -1279,8 +1052,7 @@ const ChartLogic = {
1279
  } else {
1280
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
1281
  chart.instance = new Chart(ctx.getContext('2d'), {
1282
- type: 'line',
1283
- data: { labels, datasets },
1284
  options: {
1285
  responsive: true, maintainAspectRatio: false, animation: false,
1286
  interaction: { mode: 'index', intersect: false },
@@ -1289,8 +1061,7 @@ const ChartLogic = {
1289
  tooltip: {
1290
  backgroundColor: 'rgba(22,27,34,0.97)',
1291
  titleColor: '#e6edf3', bodyColor: '#8b949e',
1292
- borderColor: '#30363d', borderWidth: 1,
1293
- padding: 10,
1294
  callbacks: {
1295
  label: function(context) {
1296
  const s = seriesConfig[context.datasetIndex];
@@ -1313,8 +1084,7 @@ const ChartLogic = {
1313
  if (s.cond && s.cond !== 'none') {
1314
  const thresh = parseFloat(s.thresh) || 0;
1315
  const v = context.parsed.y;
1316
- const isTrue = s.cond === '>' ? v > thresh : v < thresh;
1317
- c = isTrue ? (s.color1 || '#238636') : (s.color2 || '#da3633');
1318
  }
1319
  return { borderColor: c, backgroundColor: c };
1320
  }
@@ -1322,20 +1092,9 @@ const ChartLogic = {
1322
  }
1323
  },
1324
  scales: {
1325
- x: {
1326
- grid: { color: '#30363d', tickLength: 0 },
1327
- ticks: { color: '#8b949e', maxTicksLimit: 8, font: { size: 10 } }
1328
- },
1329
- y: {
1330
- type: 'linear', display: true, position: 'left',
1331
- grid: { color: '#30363d' },
1332
- ticks: { color: '#8b949e', font: { size: 10 } }
1333
- },
1334
- y1: {
1335
- type: 'linear', display: hasRight, position: 'right',
1336
- grid: { display: false },
1337
- ticks: { color: '#8b949e', font: { size: 10 } }
1338
- }
1339
  }
1340
  }
1341
  });
@@ -1344,21 +1103,9 @@ const ChartLogic = {
1344
  };
1345
 
1346
  // ===================================================
1347
- // BREAKDOWN TABLE
1348
  // ===================================================
1349
  const Breakdown = {
1350
- currentTab: 'summary',
1351
-
1352
- showTab(tab) {
1353
- this.currentTab = tab;
1354
- document.getElementById('tabSummary').classList.toggle('primary', tab === 'summary');
1355
- document.getElementById('tabStrikes').classList.toggle('primary', tab === 'strikes');
1356
- document.getElementById('tabSummary').classList.toggle('', tab !== 'summary');
1357
- document.getElementById('tabStrikes').classList.toggle('', tab !== 'strikes');
1358
- document.getElementById('bdSummaryTab').style.display = tab === 'summary' ? 'block' : 'none';
1359
- document.getElementById('bdStrikesTab').style.display = tab === 'strikes' ? 'block' : 'none';
1360
- },
1361
-
1362
  open(id) {
1363
  const chart = App.state.charts[id];
1364
  if (!chart || !chart.lastData || !chart.config.series) {
@@ -1369,53 +1116,46 @@ const Breakdown = {
1369
  const data = chart.lastData;
1370
  const cfg = chart.config;
1371
  const series = cfg.series;
1372
- const isAdvanced = cfg.chartMode === 'advanced';
1373
-
1374
- // Reset to summary tab
1375
- this.showTab('summary');
1376
 
1377
- // Determine which raw variables to show in table
1378
- const allVarKeys = ['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
1379
- const presentVars = allVarKeys.filter(k =>
1380
- data[k] && data[k].some(v => v && v !== 0)
1381
- );
1382
 
1383
- // Build header
1384
  let thead = '<thead><tr><th>Time</th>';
1385
- if (isAdvanced && data.atm_strike) {
1386
- thead += '<th>ATM</th>';
1387
- }
1388
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1389
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1390
  thead += '</tr></thead>';
1391
 
1392
- // Build rows
1393
  let tbody = '<tbody>';
1394
  data.labels.forEach((lbl, i) => {
1395
  const vars = {
1396
- P: safeVal(data, 'P', i), B: safeVal(data, 'B', i),
1397
- S: safeVal(data, 'S', i), CB: safeVal(data, 'CB', i),
1398
- CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
1399
- PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i),
1400
- OI: safeVal(data, 'OI', i)
1401
  };
1402
 
1403
  tbody += `<tr><td>${lbl}</td>`;
1404
-
1405
- // Add ATM strike column for advanced mode
1406
- if (isAdvanced && data.atm_strike) {
1407
- const atm = data.atm_strike[i] || 0;
1408
- tbody += `<td style="color:var(--warning); font-weight:700;">${atm.toFixed(0)}</td>`;
 
 
 
1409
  }
1410
 
1411
- // Raw variable values
1412
  presentVars.forEach(k => {
1413
  const v = vars[k];
1414
  const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1415
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1416
  });
1417
 
1418
- // Calculated formula values
1419
  series.forEach(s => {
1420
  const result = evalFormula(s.formula, vars);
1421
  const rv = result !== null ? result : 0;
@@ -1430,151 +1170,62 @@ const Breakdown = {
1430
  }
1431
  tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
1432
  });
1433
-
1434
  tbody += '</tr>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1435
  });
1436
  tbody += '</tbody>';
1437
 
1438
- document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${isAdvanced ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1439
  document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1440
  document.getElementById('bdTable').innerHTML = thead + tbody;
1441
-
1442
- // Build Strike Breakdown Tab (only for advanced mode with breakdown data)
1443
- if (isAdvanced && data.breakdown_per_minute) {
1444
- this.buildStrikeBreakdown(data, cfg);
1445
- document.getElementById('tabStrikes').style.display = 'inline-block';
1446
- } else {
1447
- document.getElementById('tabStrikes').style.display = 'none';
1448
- }
1449
-
1450
- // Show range info if available
1451
- if (isAdvanced && data.atm_range_info) {
1452
- const info = data.atm_range_info;
1453
- document.getElementById('bdRangeInfo').innerHTML = `
1454
- <div class="range-info-box">
1455
- <span class="label">ATM Strike:</span> <span class="value">${info.atm_strike || 'N/A'}</span> &nbsp;
1456
- <span class="label">Spot Price:</span> <span class="value">${info.spot_price || 'N/A'}</span> &nbsp;
1457
- <span class="label">Range:</span> <span class="value">±${info.atm_range || cfg.atmRange || 5}</span> &nbsp;
1458
- <span class="label">Total Strikes:</span> <span class="value">${info.total_strikes_in_range || '2N'}</span>
1459
- </div>
1460
- `;
1461
- document.getElementById('bdRangeInfo').style.display = 'block';
1462
- } else {
1463
- document.getElementById('bdRangeInfo').style.display = 'none';
1464
- }
1465
-
1466
  document.getElementById('breakdownModal').style.display = 'flex';
1467
  },
1468
 
1469
- buildStrikeBreakdown(data, cfg) {
1470
- const breakdown = data.breakdown_per_minute;
1471
- if (!breakdown || !breakdown.length) {
1472
- document.getElementById('bdStrikesContent').innerHTML = '<div style="padding:20px; text-align:center; color:var(--text-secondary);">No strike breakdown data available</div>';
1473
- return;
1474
  }
1475
-
1476
- let html = '';
1477
-
1478
- breakdown.forEach((minuteData, idx) => {
1479
- const strikes = minuteData.strikes || [];
1480
- if (!strikes.length) return;
1481
-
1482
- // Group strikes by strike price
1483
- const strikeGroups = {};
1484
- strikes.forEach(s => {
1485
- if (!strikeGroups[s.strike]) {
1486
- strikeGroups[s.strike] = { strike: s.strike, ce: null, pe: null };
1487
- }
1488
- if (s.type === 'CE') strikeGroups[s.strike].ce = s;
1489
- else if (s.type === 'PE') strikeGroups[s.strike].pe = s;
1490
- });
1491
-
1492
- const sortedStrikes = Object.values(strikeGroups).sort((a, b) => a.strike - b.strike);
1493
- const atmStrike = minuteData.atm_strike || 0;
1494
-
1495
- html += `
1496
- <div class="minute-accordion">
1497
- <div class="minute-header" onclick="Breakdown.toggleMinute(this)">
1498
- <span style="font-weight:700; color:var(--accent);">${minuteData.time}</span>
1499
- <span>ATM: <span style="color:var(--warning);">${atmStrike.toFixed(0)}</span></span>
1500
- <span>CB: <span class="val-pos">${minuteData.cb.toFixed(0)}</span></span>
1501
- <span>CS: <span class="val-neg">${minuteData.cs.toFixed(0)}</span></span>
1502
- <span>PB: <span class="val-pos">${minuteData.pb.toFixed(0)}</span></span>
1503
- <span>PS: <span class="val-neg">${minuteData.ps.toFixed(0)}</span></span>
1504
- </div>
1505
- <div class="minute-detail">
1506
- <table class="strike-table">
1507
- <thead>
1508
- <tr>
1509
- <th>Strike</th>
1510
- <th colspan="4" style="text-align:center; background:rgba(47,129,247,0.2);">CALL (CE)</th>
1511
- <th colspan="4" style="text-align:center; background:rgba(218,54,51,0.2);">PUT (PE)</th>
1512
- </tr>
1513
- <tr>
1514
- <th></th>
1515
- <th style="background:rgba(47,129,247,0.1);">Buy</th>
1516
- <th style="background:rgba(47,129,247,0.1);">Sell</th>
1517
- <th style="background:rgba(47,129,247,0.1);">LTP</th>
1518
- <th style="background:rgba(47,129,247,0.1);">Status</th>
1519
- <th style="background:rgba(218,54,51,0.1);">Buy</th>
1520
- <th style="background:rgba(218,54,51,0.1);">Sell</th>
1521
- <th style="background:rgba(218,54,51,0.1);">LTP</th>
1522
- <th style="background:rgba(218,54,51,0.1);">Status</th>
1523
- </tr>
1524
- </thead>
1525
- <tbody>
1526
- `;
1527
-
1528
- sortedStrikes.forEach(sg => {
1529
- const isAtm = Math.abs(sg.strike - atmStrike) < 1;
1530
- const rowClass = isAtm ? 'atm-strike' : '';
1531
-
1532
- html += `<tr class="${rowClass}">`;
1533
- html += `<td style="font-weight:${isAtm ? '700' : '400'}; color:${isAtm ? 'var(--warning)' : 'var(--text-primary)'};">${sg.strike.toFixed(0)}</td>`;
1534
-
1535
- // CE columns
1536
- if (sg.ce) {
1537
- const ceItmClass = sg.ce.call_status === 'ITM' ? 'itm' : 'otm';
1538
- html += `
1539
- <td class="val-pos">${sg.ce.buy.toFixed(0)}</td>
1540
- <td class="val-neg">${sg.ce.sell.toFixed(0)}</td>
1541
- <td>${sg.ce.ltp.toFixed(2)}</td>
1542
- <td class="${ceItmClass}">${sg.ce.call_status || '-'}</td>
1543
- `;
1544
- } else {
1545
- html += `<td>-</td><td>-</td><td>-</td><td>-</td>`;
1546
- }
1547
-
1548
- // PE columns
1549
- if (sg.pe) {
1550
- const peItmClass = sg.pe.put_status === 'ITM' ? 'itm' : 'otm';
1551
- html += `
1552
- <td class="val-pos">${sg.pe.buy.toFixed(0)}</td>
1553
- <td class="val-neg">${sg.pe.sell.toFixed(0)}</td>
1554
- <td>${sg.pe.ltp.toFixed(2)}</td>
1555
- <td class="${peItmClass}">${sg.pe.put_status || '-'}</td>
1556
- `;
1557
- } else {
1558
- html += `<td>-</td><td>-</td><td>-</td><td>-</td>`;
1559
- }
1560
-
1561
- html += '</tr>';
1562
- });
1563
-
1564
- html += `
1565
- </tbody>
1566
- </table>
1567
- </div>
1568
- </div>
1569
- `;
1570
- });
1571
-
1572
- document.getElementById('bdStrikesContent').innerHTML = html || '<div style="padding:20px; text-align:center; color:var(--text-secondary);">No strike breakdown data available</div>';
1573
- },
1574
-
1575
- toggleMinute(header) {
1576
- const detail = header.nextElementSibling;
1577
- detail.classList.toggle('show');
1578
  }
1579
  };
1580
 
@@ -1593,16 +1244,13 @@ const Config = {
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
  },
1600
-
1601
  updatePresetsDropdown() {
1602
  const sel = document.getElementById('cfgPresetsDropdown');
1603
  sel.innerHTML = Object.keys(App.presets).map(k => `<option value="${k}">${k}</option>`).join('');
1604
  },
1605
-
1606
  loadPreset(presetName = null) {
1607
  const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1608
  if (name && App.presets[name]) {
@@ -1610,7 +1258,6 @@ const Config = {
1610
  this.renderSeries(App.presets[name]);
1611
  }
1612
  },
1613
-
1614
  savePreset() {
1615
  const name = prompt('Name for this model:', 'Custom Model');
1616
  if (!name) return;
@@ -1619,10 +1266,9 @@ const Config = {
1619
  this.updatePresetsDropdown();
1620
  document.getElementById('cfgPresetsDropdown').value = name;
1621
  },
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.');
@@ -1688,7 +1334,6 @@ const Config = {
1688
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1689
  this.updatePresetsDropdown();
1690
  },
1691
-
1692
  close() { document.getElementById('configModal').style.display = 'none'; },
1693
 
1694
  async onRootChange(preExp, preInst) {
@@ -1771,16 +1416,13 @@ const Config = {
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; });
1778
  container.appendChild(r);
1779
  },
1780
 
1781
- addSeriesRow() {
1782
- this._appendSeriesRow(document.getElementById('cfgSeries'));
1783
- },
1784
 
1785
  extractSeriesFromDOM() {
1786
  return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
 
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
 
 
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
  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
  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:
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:
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
+ rpc_name = "get_advanced_chart_data"
 
169
  base_params = {
170
  "p_root": root,
171
  "p_expiry": expiry,
 
176
 
177
  else:
178
  if not instrument:
179
+ return JSONResponse({"error": "No Instrument Selected.", "labels":[], "P":[]})
180
 
181
  rpc_name = "get_normal_chart_data"
182
  base_params = {
 
195
  loop = asyncio.get_event_loop()
196
  return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
197
 
198
+ tasks =[]
199
  curr = t_start_dt
200
  while curr <= t_end_dt:
201
  nxt = curr + timedelta(hours=2) - timedelta(seconds=1)
 
206
 
207
  chunk_results = await asyncio.gather(*tasks)
208
 
209
+ all_records =[]
210
  for res in chunk_results:
211
  if isinstance(res, list):
212
  all_records.extend(res)
213
 
214
  if not all_records:
215
+ return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
216
 
217
  # =========================================================
218
  # DATA PROCESSING
 
244
  }
245
  panda_tf = tf_map.get(timeframe, "1min")
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
 
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 {
 
376
  width: 560px; max-width: 95%; max-height: 88vh; display: flex; flex-direction: column;
377
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
378
  }
379
+ .modal.wide { width: 880px; }
380
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
381
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
382
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
 
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;
 
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;
 
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
  <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
 
 
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>
 
579
  <div style="height:1px; background:var(--border); margin:12px 0 8px;"></div>
580
  </div>
581
 
 
582
  <div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom:12px;">
583
  <label>Quick Models</label>
584
  <div style="display:flex; gap:8px;">
 
588
  </div>
589
  </div>
590
 
 
591
  <div class="form-section">
592
  <div style="display:flex; align-items:center; margin-bottom:6px;">
593
  <label style="margin:0;">Plot Lines</label>
 
614
  </div>
615
  <div class="modal-body" style="padding:12px;">
616
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
617
+ <div class="breakdown-scroll">
618
+ <table class="breakdown-table" id="bdTable"></table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  </div>
620
  </div>
621
  </div>
 
644
  try { return new Function('return ' + f)(); } catch { return null; }
645
  }
646
 
 
647
  function extractRequiredVars(seriesList) {
648
+ const vars = new Set(['P']);
649
  seriesList.forEach(s => {
650
  const f = s.formula || '';
651
  if (f.includes('$CB')) vars.add('CB');
652
  if (f.includes('$CS')) vars.add('CS');
653
  if (f.includes('$PB')) vars.add('PB');
654
  if (f.includes('$PS')) vars.add('PS');
 
655
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
656
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
657
  if (f.includes('$V')) vars.add('V');
 
664
  // CUSTOM DROPDOWN
665
  // ===================================================
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){}
673
  },
674
+ clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
 
 
675
  set(id, items, groupName = null) {
676
+ if (!groupName) this.data[id] = items.map(i => ({ val: i, group: null }));
677
+ else this.data[id].push(...items.map(i => ({ val: i, group: groupName })));
 
 
678
  },
 
679
  async open(id) {
680
  document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
681
  document.getElementById(id).select();
682
  this.render(id, '');
683
  document.getElementById(`dd-${id}`).style.display = 'block';
684
  },
 
685
  async filter(id) {
686
  const query = document.getElementById(id).value.toUpperCase();
687
  if (id === 'cfgRoot' && query.length >= 2) {
 
692
  this.render(id, query);
693
  document.getElementById(`dd-${id}`).style.display = 'block';
694
  },
 
695
  render(id, query) {
696
  const el = document.getElementById(`dd-${id}`);
697
  const q = query.toLowerCase();
 
718
  });
719
  el.innerHTML = html;
720
  },
 
721
  pick(id, val) {
722
  document.getElementById(id).value = val;
723
  document.getElementById(`dd-${id}`).style.display = 'none';
 
737
  // APP STATE
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' },
 
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);
 
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 };
 
845
  localStorage.setItem('dc_pro_v4_state', JSON.stringify({
846
  date: this.state.date, mode: this.state.mode,
847
  columns: this.state.columns, layout: this.state.layout,
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
  },
 
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 = `
 
931
  this.renderGrid(); this.save();
932
  },
933
 
934
+ onDateChange() { this.state.date = document.getElementById('globalDate').value; this.save(); this.refreshAll(); },
935
+ onModeChange() { this.state.mode = document.getElementById('globalMode').value; this.updateModeUI(); this.save(); this.refreshAll(); },
936
+ updateModeUI() { document.getElementById('modeDot').className = `status-dot ${this.state.mode === 'LIVE' ? 'live' : 'hist'}`; },
 
 
 
 
 
 
 
 
937
  refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
938
 
939
  startLoop() {
 
963
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
964
  }
965
 
966
+ const requiredVars = extractRequiredVars(cfg.series ||[]);
 
967
 
968
  try {
969
  const res = await fetch('/api/fetch_series', {
 
993
  return;
994
  }
995
 
 
996
  chart.lastData = data;
 
997
  const body = document.getElementById(`body-${id}`);
998
  if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
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,
1014
+ hitRadius: 10, tension: 0.1, yAxisID: s.axis === 'left' ? 'y' : 'y1'
 
 
 
 
 
1015
  };
1016
 
1017
  if (s.cond && s.cond !== 'none') {
 
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;
1026
  ds.pointHoverBorderColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
1027
  } else {
1028
+ ds.borderColor = s.color1 || '#2f81f7'; ds.backgroundColor = s.color1 || '#2f81f7';
1029
+ ds.pointHoverBackgroundColor = s.color1 || '#2f81f7'; ds.pointHoverBorderColor = s.color1 || '#2f81f7';
 
 
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
  },
 
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) {
 
1052
  } else {
1053
  const hasRight = datasets.some(d => d.yAxisID === 'y1');
1054
  chart.instance = new Chart(ctx.getContext('2d'), {
1055
+ type: 'line', data: { labels, datasets },
 
1056
  options: {
1057
  responsive: true, maintainAspectRatio: false, animation: false,
1058
  interaction: { mode: 'index', intersect: 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];
 
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
  }
 
1092
  }
1093
  },
1094
  scales: {
1095
+ x: { grid: { color: '#30363d', tickLength: 0 }, ticks: { color: '#8b949e', maxTicksLimit: 8, font: { size: 10 } } },
1096
+ y: { type: 'linear', display: true, position: 'left', grid: { color: '#30363d' }, ticks: { color: '#8b949e', font: { size: 10 } } },
1097
+ y1: { type: 'linear', display: hasRight, position: 'right', grid: { display: false }, ticks: { color: '#8b949e', font: { size: 10 } } }
 
 
 
 
 
 
 
 
 
 
 
1098
  }
1099
  }
1100
  });
 
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) {
 
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;
 
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
 
 
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);
1249
  },
 
1250
  updatePresetsDropdown() {
1251
  const sel = document.getElementById('cfgPresetsDropdown');
1252
  sel.innerHTML = Object.keys(App.presets).map(k => `<option value="${k}">${k}</option>`).join('');
1253
  },
 
1254
  loadPreset(presetName = null) {
1255
  const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1256
  if (name && App.presets[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;
 
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.');
 
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) {
 
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
  },
1424
 
1425
+ addSeriesRow() { this._appendSeriesRow(document.getElementById('cfgSeries')); },
 
 
1426
 
1427
  extractSeriesFromDOM() {
1428
  return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {