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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +379 -511
app.py CHANGED
@@ -167,7 +167,8 @@ async def fetch_series(
167
  others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
168
  ref_instrument = others[0] if others else instruments[0]
169
 
170
- rpc_name = "get_advanced_chart_data"
 
171
  base_params = {
172
  "p_root": root,
173
  "p_expiry": expiry,
@@ -231,7 +232,9 @@ async def fetch_series(
231
  "cs": "CS",
232
  "pb": "PB",
233
  "ps": "PS",
234
- "price": "P"
 
 
235
  }, inplace=True)
236
 
237
  df['ts'] = pd.to_datetime(df['ts'])
@@ -258,95 +261,42 @@ async def fetch_series(
258
  "CS": "last", # Call Sell → last value
259
  "PB": "last", # Put Buy → last value
260
  "PS": "last", # Put Sell → last value
 
261
  }
262
-
263
- # Include ATM strike and breakdown data if available (for advanced mode)
264
- if "atm_strike" in df.columns:
265
- agg_dict["atm_strike"] = "last"
266
- if "included_strikes" in df.columns:
267
- agg_dict["included_strikes"] = "last"
268
- if "breakdown_json" in df.columns:
269
- agg_dict["breakdown_json"] = "last"
270
 
 
271
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
272
  resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
273
 
 
 
 
 
 
 
274
  # Build response: only include requested variables (reduces egress)
275
  response_data = {
276
- "labels": resampled.index.strftime('%H:%M').tolist()
 
277
  }
278
 
279
- for var_key in ["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
280
  if var_key in required_vars and var_key in resampled.columns:
281
  response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
282
- # Omit entirely if not requested — JS handles missing keys gracefully
283
-
284
- # Include ATM strike and breakdown data for advanced mode
285
- if "atm_strike" in resampled.columns:
286
- response_data["atm_strike"] = [round(float(v), 2) if pd.notna(v) else None for v in resampled["atm_strike"].tolist()]
287
- if "included_strikes" in resampled.columns:
288
- response_data["included_strikes"] = resampled["included_strikes"].tolist()
289
- if "breakdown_json" in resampled.columns:
290
- # Convert JSONB records to list of dicts
291
- breakdown_data = []
292
- for bd in resampled["breakdown_json"].tolist():
293
- if pd.notna(bd) and bd:
294
- if isinstance(bd, str):
295
- import json
296
- try:
297
- breakdown_data.append(json.loads(bd))
298
- except:
299
- breakdown_data.append([])
300
- else:
301
- breakdown_data.append(bd)
302
- else:
303
- breakdown_data.append([])
304
- response_data["breakdown_json"] = breakdown_data
305
 
306
- return JSONResponse(response_data)
307
-
308
-
309
- # ==========================================
310
- # 5. NEW API: STRIKE BREAKDOWN FOR SPECIFIC TIMESTAMP
311
- # ==========================================
 
312
 
313
- @app.post("/api/strike_breakdown")
314
- async def get_strike_breakdown(
315
- date: str = Body(...),
316
- root: str = Body(...),
317
- expiry: str = Body(...),
318
- ref_instrument: str = Body(...),
319
- atm_range: int = Body(5),
320
- timestamp: str = Body(...)
321
- ):
322
- """
323
- Get detailed strike breakdown for a specific timestamp.
324
- Shows all strikes with their ITM/OTM status and individual values.
325
- """
326
- root = root.upper()
327
-
328
- # Parse timestamp
329
- ts_dt = pd.to_datetime(f"{date} {timestamp}")
330
-
331
- result = sb_rpc("get_strike_breakdown", {
332
- "p_root": root,
333
- "p_expiry": expiry,
334
- "p_timestamp": ts_dt.strftime("%Y-%m-%d %H:%M:%S"),
335
- "p_ref_instrument": ref_instrument,
336
- "p_atm_range": atm_range
337
- })
338
-
339
- if not result:
340
- return JSONResponse({"error": "No breakdown data found", "strikes": []})
341
-
342
- return JSONResponse({
343
- "timestamp": timestamp,
344
- "strikes": result
345
- })
346
 
347
 
348
  # ==========================================
349
- # 6. FRONTEND TEMPLATE
350
  # ==========================================
351
 
352
  HTML_TEMPLATE = """
@@ -448,8 +398,7 @@ HTML_TEMPLATE = """
448
  width: 560px; max-width: 95%; max-height: 88vh; display: flex; flex-direction: column;
449
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
450
  }
451
- .modal.wide { width: 820px; }
452
- .modal.extra-wide { width: 95%; max-width: 1400px; }
453
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
454
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
455
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
@@ -529,81 +478,65 @@ HTML_TEMPLATE = """
529
  .val-neg { color: #f85149; }
530
  .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
531
 
532
- /* STRIKE BREAKDOWN STYLES */
533
- .strike-row {
534
- display: flex; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border);
535
- align-items: center; font-family: var(--font-mono); font-size: 11px;
 
 
 
536
  }
537
- .strike-row:hover { background: var(--bg-panel); }
538
- .strike-row.itm { background: rgba(47,129,247,0.05); }
539
- .strike-row.otm { background: rgba(0,0,0,0); }
540
- .strike-row.atm { background: rgba(35,134,54,0.1); border-left: 3px solid var(--success); }
541
- .strike-row.not-included { opacity: 0.4; }
542
- .strike-label {
543
- min-width: 100px; display: flex; gap: 6px; align-items: center;
544
- }
545
- .strike-badge {
546
- font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600;
547
- }
548
- .badge-itm { background: var(--accent); color: white; }
549
- .badge-otm { background: var(--bg-panel); color: var(--text-secondary); border: 1px solid var(--border); }
550
- .badge-atm { background: var(--success); color: white; }
551
- .strike-values {
552
- display: flex; gap: 16px; flex: 1; justify-content: flex-end;
553
- }
554
- .strike-value-group {
555
- display: flex; flex-direction: column; min-width: 60px;
556
- }
557
- .strike-value-label { font-size: 9px; color: var(--text-secondary); }
558
- .strike-value-num { font-weight: 600; }
559
-
560
- /* MINUTE BREAKDOWN ACCORDION */
561
- .minute-row {
562
  border-bottom: 1px solid var(--border);
563
  }
564
- .minute-header {
565
- display: flex; align-items: center; padding: 8px 12px; cursor: pointer;
566
- background: var(--bg-card); transition: background 0.2s;
 
 
 
567
  }
568
- .minute-header:hover { background: var(--bg-panel); }
569
- .minute-header.expanded { background: var(--bg-panel); border-bottom: 1px solid var(--border); }
570
- .minute-time { font-weight: 600; min-width: 60px; }
571
- .minute-atm { color: var(--accent); font-family: var(--font-mono); min-width: 100px; }
572
- .minute-summary { flex: 1; display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px; }
573
- .expand-icon {
574
- width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
575
- color: var(--text-secondary); transition: transform 0.2s;
576
  }
577
- .expand-icon.expanded { transform: rotate(90deg); }
578
- .minute-detail {
579
- display: none; padding: 12px; background: var(--bg-body);
580
  border-bottom: 1px solid var(--border);
581
  }
582
- .minute-detail.visible { display: block; }
 
 
 
583
 
584
- /* STRIKE DETAIL GRID */
585
- .strike-detail-grid {
586
- display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
587
- gap: 8px;
 
588
  }
589
- .strike-detail-card {
590
- background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px;
591
- padding: 10px; font-family: var(--font-mono); font-size: 11px;
592
- }
593
- .strike-detail-card.ce { border-left: 3px solid #238636; }
594
- .strike-detail-card.pe { border-left: 3px solid #da3633; }
595
- .strike-detail-card.itm { background: rgba(47,129,247,0.08); }
596
- .strike-detail-card.otm { background: transparent; }
597
- .strike-detail-card.atm { background: rgba(35,134,54,0.1); }
598
- .strike-detail-header {
599
- display: flex; justify-content: space-between; margin-bottom: 8px;
600
- font-weight: 600;
601
  }
602
- .strike-detail-values {
603
- display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
 
 
604
  }
605
- .detail-val { color: var(--text-secondary); }
606
- .detail-num { text-align: right; }
607
  </style>
608
  </head>
609
  <body>
@@ -615,7 +548,7 @@ HTML_TEMPLATE = """
615
  <div style="position:relative;">
616
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
617
  <option value="LIVE">LIVE</option>
618
- <option value="HISTORY">HISTORY</option>
619
  </select>
620
  <div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
621
  </div>
@@ -688,16 +621,18 @@ HTML_TEMPLATE = """
688
  <div id="dd-cfgInst" class="custom-dd"></div>
689
  </div>
690
  <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
691
- * Advanced Mode: select the Spot/Future as price reference. All CE/PE within ATM ± range will be aggregated.
 
692
  </div>
693
  </div>
694
 
695
  <div id="cfgAdvancedFields" style="display:none;">
696
  <div class="form-section" style="margin-top:12px;">
697
- <label style="color:var(--accent);">ATM (+/-) Strike Range</label>
698
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
699
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
700
- Aggregates all CE/PE within ATM ± N strikes. Total 2N strikes included.
 
701
  </div>
702
  </div>
703
  </div>
@@ -753,27 +688,17 @@ HTML_TEMPLATE = """
753
  </div>
754
  </div>
755
 
756
- <!-- ===== BREAKDOWN MODAL (ENHANCED) ===== -->
757
  <div class="modal-backdrop" id="breakdownModal">
758
- <div class="modal extra-wide">
759
  <div class="modal-head">
760
  <span id="bdTitle">Calculation Breakdown</span>
761
  <button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
762
  </div>
763
  <div class="modal-body" style="padding:12px;">
764
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
765
- <div style="display:flex; gap:8px; margin-bottom:12px;">
766
- <button id="bdTabSummary" class="primary" style="font-size:11px;" onclick="Breakdown.showTab('summary')">Summary</button>
767
- <button id="bdTabDetail" style="font-size:11px; background:var(--bg-panel);" onclick="Breakdown.showTab('detail')">Strike Detail</button>
768
- </div>
769
- <div id="bdSummaryTab">
770
- <div class="breakdown-scroll">
771
- <table class="breakdown-table" id="bdTable"></table>
772
- </div>
773
- </div>
774
- <div id="bdDetailTab" style="display:none;">
775
- <div class="breakdown-scroll" id="bdDetailContent"></div>
776
- </div>
777
  </div>
778
  </div>
779
  </div>
@@ -803,14 +728,13 @@ function evalFormula(formula, vars) {
803
 
804
  // Extract which variables are used in a list of series formulas
805
  function extractRequiredVars(seriesList) {
806
- const vars = new Set(['P']); // Always need price
807
  seriesList.forEach(s => {
808
  const f = s.formula || '';
809
  if (f.includes('$CB')) vars.add('CB');
810
  if (f.includes('$CS')) vars.add('CS');
811
  if (f.includes('$PB')) vars.add('PB');
812
  if (f.includes('$PS')) vars.add('PS');
813
- // Check $B / $S but NOT $CB/$CS/$PB/$PS (already handled above)
814
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
815
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
816
  if (f.includes('$V')) vars.add('V');
@@ -906,9 +830,7 @@ const CustomDD = {
906
  const App = {
907
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
908
 
909
- // ---- DEFAULT PRESETS (organized by use case) ----
910
  presets: {
911
- // --- Normal: Futures / Equity / Spot ---
912
  "Basic Flow (Fut/Eq)": [
913
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
914
  { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
@@ -918,7 +840,6 @@ const App = {
918
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
919
  { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
920
  ],
921
- // --- Normal: Options ---
922
  "Call Flow (Options)": [
923
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
924
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
@@ -938,7 +859,6 @@ const App = {
938
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
939
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
940
  ],
941
- // --- Advanced: Options (aggregated across ATM range) ---
942
  "Price & Flows (Advanced)": [
943
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
944
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
@@ -1151,7 +1071,6 @@ const ChartLogic = {
1151
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1152
  }
1153
 
1154
- // Determine which variables are actually needed by the series formulas
1155
  const requiredVars = extractRequiredVars(cfg.series || []);
1156
 
1157
  try {
@@ -1182,7 +1101,6 @@ const ChartLogic = {
1182
  return;
1183
  }
1184
 
1185
- // Store data for breakdown table
1186
  chart.lastData = data;
1187
 
1188
  const body = document.getElementById(`body-${id}`);
@@ -1326,22 +1244,11 @@ const ChartLogic = {
1326
  };
1327
 
1328
  // ===================================================
1329
- // BREAKDOWN TABLE (ENHANCED)
1330
  // ===================================================
1331
  const Breakdown = {
1332
  currentTab: 'summary',
1333
- expandedMinutes: new Set(),
1334
-
1335
- showTab(tab) {
1336
- this.currentTab = tab;
1337
- document.getElementById('bdTabSummary').className = tab === 'summary' ? 'primary' : '';
1338
- document.getElementById('bdTabSummary').style.background = tab === 'summary' ? 'var(--accent)' : 'var(--bg-panel)';
1339
- document.getElementById('bdTabDetail').className = tab === 'detail' ? 'primary' : '';
1340
- document.getElementById('bdTabDetail').style.background = tab === 'detail' ? 'var(--accent)' : 'var(--bg-panel)';
1341
- document.getElementById('bdSummaryTab').style.display = tab === 'summary' ? 'block' : 'none';
1342
- document.getElementById('bdDetailTab').style.display = tab === 'detail' ? 'block' : 'none';
1343
- },
1344
-
1345
  open(id) {
1346
  const chart = App.state.charts[id];
1347
  if (!chart || !chart.lastData || !chart.config.series) {
@@ -1352,49 +1259,59 @@ const Breakdown = {
1352
  const data = chart.lastData;
1353
  const cfg = chart.config;
1354
  const series = cfg.series;
 
1355
 
1356
- // Build summary table
1357
- this.buildSummaryTable(data, cfg, series);
1358
-
1359
- // Build detail view if breakdown data available
1360
- if (data.breakdown_json && data.breakdown_json.length > 0) {
1361
- this.buildDetailView(data, cfg);
1362
- } else {
1363
- document.getElementById('bdDetailContent').innerHTML = `
1364
- <div style="text-align:center; padding:40px; color:var(--text-secondary);">
1365
- <div style="font-size:24px; opacity:0.5;">📊</div>
1366
- <div style="margin-top:12px;">Strike breakdown data available only in Advanced Mode</div>
1367
- <div style="font-size:11px; margin-top:8px;">Switch to Advanced Mode to see per-strike details</div>
1368
- </div>
1369
  `;
 
 
 
1370
  }
1371
 
1372
- document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1373
- document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} ${cfg.end} | ${data.labels.length} bars`;
 
1374
  document.getElementById('breakdownModal').style.display = 'flex';
1375
-
1376
- // Default to summary tab
1377
- this.showTab('summary');
1378
  },
1379
-
1380
- buildSummaryTable(data, cfg, series) {
1381
- // Determine which raw variables to show in table
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1382
  const allVarKeys = ['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
1383
  const presentVars = allVarKeys.filter(k =>
1384
  data[k] && data[k].some(v => v && v !== 0)
1385
  );
1386
 
1387
- // Build header
1388
  let thead = '<thead><tr><th>Time</th>';
1389
- // Add ATM strike column if available
1390
- if (data.atm_strike) {
1391
- thead += '<th>ATM</th>';
1392
- }
1393
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1394
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1395
  thead += '</tr></thead>';
1396
 
1397
- // Build rows
1398
  let tbody = '<tbody>';
1399
  data.labels.forEach((lbl, i) => {
1400
  const vars = {
@@ -1406,21 +1323,13 @@ const Breakdown = {
1406
  };
1407
 
1408
  tbody += `<tr><td>${lbl}</td>`;
1409
-
1410
- // ATM strike if available
1411
- if (data.atm_strike) {
1412
- const atm = data.atm_strike[i];
1413
- tbody += `<td style="color:var(--accent); font-weight:600;">${atm ? atm.toFixed(0) : '-'}</td>`;
1414
- }
1415
 
1416
- // Raw variable values
1417
  presentVars.forEach(k => {
1418
  const v = vars[k];
1419
  const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1420
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1421
  });
1422
 
1423
- // Calculated formula values
1424
  series.forEach(s => {
1425
  const result = evalFormula(s.formula, vars);
1426
  const rv = result !== null ? result : 0;
@@ -1440,141 +1349,136 @@ const Breakdown = {
1440
  });
1441
  tbody += '</tbody>';
1442
 
1443
- document.getElementById('bdTable').innerHTML = thead + tbody;
1444
  },
1445
-
1446
- buildDetailView(data, cfg) {
1447
  const atmRange = cfg.atmRange || 5;
1448
-
1449
- let html = '<div style="margin-bottom:12px; padding:12px; background:var(--bg-panel); border-radius:6px; border:1px solid var(--border);">';
1450
- html += `<div style="font-size:12px; color:var(--text-secondary);">`;
1451
- html += `<strong style="color:var(--accent);">ATM ± ${atmRange}</strong> = ${atmRange} ITM + ${atmRange} OTM strikes = <strong>${atmRange * 2}</strong> total strikes per side (CE & PE)`;
1452
- html += `<br><span style="font-size:10px;">ITM for CALLS = strikes below ATM | ITM for PUTS = strikes above ATM</span>`;
1453
- html += `</div></div>`;
1454
-
1455
- html += '<div class="minute-list">';
1456
-
1457
- data.labels.forEach((lbl, i) => {
1458
- const breakdown = data.breakdown_json ? data.breakdown_json[i] : null;
1459
- const atmStrike = data.atm_strike ? data.atm_strike[i] : null;
1460
- const includedStrikes = data.included_strikes ? data.included_strikes[i] : '';
1461
-
1462
- // Calculate aggregates for this minute
1463
- const vars = {
1464
- P: safeVal(data, 'P', i), B: safeVal(data, 'B', i),
1465
- S: safeVal(data, 'S', i), CB: safeVal(data, 'CB', i),
1466
- CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
1467
- PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i),
1468
- OI: safeVal(data, 'OI', i)
1469
- };
1470
-
1471
- const minuteId = `minute-${i}`;
1472
- const isExpanded = this.expandedMinutes.has(minuteId);
1473
-
1474
- html += `<div class="minute-row">`;
1475
- html += `<div class="minute-header ${isExpanded ? 'expanded' : ''}" onclick="Breakdown.toggleMinute('${minuteId}')">`;
1476
- html += `<div class="expand-icon ${isExpanded ? 'expanded' : ''}">▶</div>`;
1477
- html += `<div class="minute-time">${lbl}</div>`;
1478
- if (atmStrike) {
1479
- html += `<div class="minute-atm">ATM: ${atmStrike.toFixed(0)}</div>`;
1480
- }
1481
- html += `<div class="minute-summary">`;
1482
- html += `<span style="color:var(--success);">CB: ${vars.CB.toFixed(0)}</span>`;
1483
- html += `<span style="color:var(--danger);">CS: ${vars.CS.toFixed(0)}</span>`;
1484
- html += `<span style="color:var(--success);">PB: ${vars.PB.toFixed(0)}</span>`;
1485
- html += `<span style="color:var(--danger);">PS: ${vars.PS.toFixed(0)}</span>`;
1486
- html += `</div>`;
1487
- html += `</div>`; // minute-header
1488
-
1489
- html += `<div class="minute-detail ${isExpanded ? 'visible' : ''}" id="${minuteId}">`;
1490
-
1491
- if (breakdown && breakdown.length > 0) {
1492
- // Group by strike
1493
- const strikesMap = {};
1494
- breakdown.forEach(s => {
1495
- if (!strikesMap[s.strike]) {
1496
- strikesMap[s.strike] = { CE: null, PE: null };
1497
- }
1498
- strikesMap[s.strike][s.type] = s;
 
 
 
 
 
 
 
 
 
 
 
 
1499
  });
1500
-
1501
- const strikes = Object.keys(strikesMap).sort((a, b) => b - a); // Descending
1502
-
1503
- html += `<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;">`;
1504
- html += `Strikes included: ${includedStrikes || 'N/A'}`;
1505
- html += `</div>`;
1506
-
1507
- html += `<div class="strike-detail-grid">`;
1508
-
1509
- strikes.forEach(strike => {
1510
- const ceData = strikesMap[strike].CE;
1511
- const peData = strikesMap[strike].PE;
1512
-
1513
- if (ceData && ceData.included) {
1514
- const cls = ceData.moneyness === 'ITM' ? 'itm' : 'otm';
1515
- const atmClass = Math.abs(ceData.strike - atmStrike) < 1 ? 'atm' : '';
1516
- html += `<div class="strike-detail-card ce ${cls} ${atmClass}">`;
1517
- html += `<div class="strike-detail-header">`;
1518
- html += `<span>${ceData.strike} CE</span>`;
1519
- html += `<span class="strike-badge ${ceData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${ceData.moneyness}</span>`;
1520
- html += `</div>`;
1521
- html += `<div class="strike-detail-values">`;
1522
- html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(ceData.b || 0).toFixed(0)}</span>`;
1523
- html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(ceData.s || 0).toFixed(0)}</span>`;
1524
- html += `<span class="detail-val">Vol:</span><span class="detail-num">${(ceData.v || 0).toFixed(0)}</span>`;
1525
- html += `<span class="detail-val">Price:</span><span class="detail-num">${(ceData.p || 0).toFixed(2)}</span>`;
1526
- html += `</div>`;
1527
- html += `</div>`;
1528
- }
1529
-
1530
- if (peData && peData.included) {
1531
- const cls = peData.moneyness === 'ITM' ? 'itm' : 'otm';
1532
- const atmClass = Math.abs(peData.strike - atmStrike) < 1 ? 'atm' : '';
1533
- html += `<div class="strike-detail-card pe ${cls} ${atmClass}">`;
1534
- html += `<div class="strike-detail-header">`;
1535
- html += `<span>${peData.strike} PE</span>`;
1536
- html += `<span class="strike-badge ${peData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${peData.moneyness}</span>`;
1537
- html += `</div>`;
1538
- html += `<div class="strike-detail-values">`;
1539
- html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(peData.b || 0).toFixed(0)}</span>`;
1540
- html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(peData.s || 0).toFixed(0)}</span>`;
1541
- html += `<span class="detail-val">Vol:</span><span class="detail-num">${(peData.v || 0).toFixed(0)}</span>`;
1542
- html += `<span class="detail-val">Price:</span><span class="detail-num">${(peData.p || 0).toFixed(2)}</span>`;
1543
- html += `</div>`;
1544
- html += `</div>`;
1545
- }
1546
  });
1547
-
1548
- html += `</div>`; // strike-detail-grid
1549
- } else {
1550
- html += `<div style="color:var(--text-secondary); text-align:center; padding:20px;">No strike breakdown data for this minute</div>`;
1551
  }
1552
-
1553
- html += `</div>`; // minute-detail
1554
- html += `</div>`; // minute-row
1555
- });
1556
-
1557
- html += '</div>'; // minute-list
1558
-
1559
- document.getElementById('bdDetailContent').innerHTML = html;
1560
- },
1561
-
1562
- toggleMinute(minuteId) {
1563
- const detail = document.getElementById(minuteId);
1564
- const header = detail.previousElementSibling;
1565
- const icon = header.querySelector('.expand-icon');
1566
-
1567
- if (this.expandedMinutes.has(minuteId)) {
1568
- this.expandedMinutes.delete(minuteId);
1569
- detail.classList.remove('visible');
1570
- header.classList.remove('expanded');
1571
- icon.classList.remove('expanded');
1572
- } else {
1573
- this.expandedMinutes.add(minuteId);
1574
- detail.classList.add('visible');
1575
- header.classList.add('expanded');
1576
- icon.classList.add('expanded');
1577
  }
 
 
1578
  }
1579
  };
1580
 
@@ -1593,7 +1497,6 @@ 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
  },
@@ -1634,210 +1537,175 @@ const Config = {
1634
  }
1635
  },
1636
 
1637
- async open(id) {
1638
- this.tid = id; this.isGlobal = false;
1639
- document.getElementById('configModal').style.display = 'flex';
1640
- document.getElementById('cfgTitle').innerText = 'Configure Chart';
1641
- document.getElementById('cfgLoading').style.display = 'block';
1642
- document.getElementById('cfgContent').style.display = 'none';
1643
- document.getElementById('cfgGlobalOnly').style.display = 'none';
1644
- document.getElementById('cfgChartOnly').style.display = 'block';
 
 
 
 
 
1645
 
1646
- this.updatePresetsDropdown();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1647
 
1648
- CustomDD.clear('cfgRoot');
1649
- const res = await fetch(`/api/roots?date=${App.state.date}`);
1650
- const roots = await res.json();
1651
- CustomDD.set('cfgRoot', roots);
 
1652
 
1653
- const cfg = App.state.charts[id].config;
1654
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1655
- document.getElementById('cfgStart').value = cfg.start || '09:15';
1656
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
1657
- document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1658
 
1659
- this.currentMode = cfg.chartMode || 'normal';
1660
- const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1661
- btns[0].classList.toggle('active', this.currentMode === 'normal');
1662
- btns[1].classList.toggle('active', this.currentMode === 'advanced');
1663
- document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1664
- document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1665
- document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1666
 
1667
- if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
1668
- else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
 
1669
 
1670
- document.getElementById('cfgRoot').value = cfg.root || '';
1671
- document.getElementById('cfgExp').value = cfg.expiry || '';
 
1672
  document.getElementById('cfgInst').value = cfg.instrument || '';
 
 
 
 
1673
 
1674
- if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
1675
 
1676
- document.getElementById('cfgLoading').style.display = 'none';
1677
- document.getElementById('cfgContent').style.display = 'block';
 
 
 
 
 
 
 
 
1678
  },
1679
 
1680
  openDefaults() {
1681
  this.isGlobal = true;
1682
- document.getElementById('configModal').style.display = 'flex';
1683
- document.getElementById('cfgTitle').innerText = 'Global Settings';
1684
- document.getElementById('cfgLoading').style.display = 'none';
1685
- document.getElementById('cfgContent').style.display = 'block';
1686
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1687
  document.getElementById('cfgChartOnly').style.display = 'none';
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) {
1695
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1696
- CustomDD.clear('cfgExp'); CustomDD.clear('cfgInst');
1697
- if (!preExp) document.getElementById('cfgExp').value = '';
1698
- if (!preInst) document.getElementById('cfgInst').value = '';
1699
  if (!root) return;
1700
-
1701
- const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1702
- const exps = await res.json();
1703
- CustomDD.set('cfgExp', exps);
1704
-
1705
- if (preExp && exps.includes(preExp)) {
1706
- document.getElementById('cfgExp').value = preExp;
1707
- await this.onExpChange(preInst);
1708
- } else if (exps.length > 0) {
1709
- const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
1710
- if (auto.current) {
1711
- document.getElementById('cfgExp').value = auto.current.expiry;
1712
- await this.onExpChange(auto.current.instrument);
1713
  }
1714
- }
1715
  },
1716
 
1717
- async onExpChange(preInst) {
1718
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1719
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1720
- CustomDD.clear('cfgInst');
1721
  if (!root || !exp) return;
1722
-
1723
- const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
1724
- const data = await res.json();
1725
- if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1726
- if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1727
- if (preInst) document.getElementById('cfgInst').value = preInst;
1728
- },
1729
-
1730
- toggleCond(sel) {
1731
- const condGroup = sel.closest('.series-row').querySelector('.series-row-cond');
1732
- if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex';
1733
- },
1734
-
1735
- renderSeries(list) {
1736
- const c = document.getElementById('cfgSeries');
1737
- c.innerHTML = '';
1738
- list.forEach(s => this._appendSeriesRow(c, s));
1739
- },
1740
-
1741
- _appendSeriesRow(container, s = {}) {
1742
- const r = document.createElement('div');
1743
- r.className = 'series-row';
1744
- const showCond = s.cond && s.cond !== 'none';
1745
- r.innerHTML = `
1746
- <div class="series-row-top">
1747
- <input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
1748
- <input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
1749
- <select class="s-axis" style="width:60px;">
1750
- <option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option>
1751
- <option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option>
1752
- </select>
1753
- <button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
1754
- </div>
1755
- <div class="series-row-bottom">
1756
- <input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
1757
- <select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)">
1758
- <option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option>
1759
- <option value=">" ${s.cond === '>' ? 'selected' : ''}>If &gt;</option>
1760
- <option value="<" ${s.cond === '<' ? 'selected' : ''}>If &lt;</option>
1761
- </select>
1762
- </div>
1763
- <div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
1764
- <span class="cond-label">Threshold:</span>
1765
- <input type="number" class="s-thresh" value="${s.thresh || 0}" style="width:65px;">
1766
- <span class="cond-label">True →</span>
1767
- <input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True">
1768
- <input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
1769
- <span class="cond-label">False →</span>
1770
- <input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False">
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 => {
1787
- const cond = r.querySelector('.s-cond')?.value || 'none';
1788
- return {
1789
- formula: r.querySelector('.s-form')?.value || '$P',
1790
- label: r.querySelector('.s-lbl')?.value || 'Line',
1791
- axis: r.querySelector('.s-axis')?.value || 'left',
1792
- color1: r.querySelector('.s-color1-cond')?.value || r.querySelector('.s-color1')?.value || '#2f81f7',
1793
- cond,
1794
- thresh: r.querySelector('.s-thresh')?.value || '0',
1795
- color2: r.querySelector('.s-color2')?.value || '#da3633',
1796
- labelTrue: r.querySelector('.s-label-true')?.value || '',
1797
- labelFalse: r.querySelector('.s-label-false')?.value || ''
1798
- };
1799
- });
1800
  },
1801
 
1802
  saveChartConfig() {
1803
  if (this.isGlobal) {
1804
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1805
  App.save();
1806
- this.close();
1807
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1808
  }
1809
-
1810
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1811
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1812
- const inst = document.getElementById('cfgInst').value.toUpperCase();
1813
-
1814
- if (!root || !exp) return alert('Please select a Symbol and Expiry.');
1815
-
1816
- const series = this.extractSeriesFromDOM();
1817
- if (!series.length) return alert('Add at least one plot line.');
1818
-
1819
- App.state.charts[this.tid].config = {
1820
- chartMode: this.currentMode,
1821
- atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1822
- root, expiry: exp, instrument: inst,
1823
- timeframe: document.getElementById('cfgTF').value,
1824
- start: document.getElementById('cfgStart').value,
1825
- end: document.getElementById('cfgEnd').value,
1826
- series
1827
- };
1828
-
1829
- App.save();
1830
- App.renderGrid();
1831
- ChartLogic.plot(this.tid);
1832
  this.close();
1833
  }
1834
  };
1835
 
1836
- window.onload = () => App.init();
 
 
 
1837
  </script>
1838
  </body>
1839
  </html>
1840
  """
1841
 
1842
  if __name__ == "__main__":
1843
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
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 that includes strike breakdown
171
+ rpc_name = "get_advanced_chart_data_with_breakdown"
172
  base_params = {
173
  "p_root": root,
174
  "p_expiry": expiry,
 
232
  "cs": "CS",
233
  "pb": "PB",
234
  "ps": "PS",
235
+ "price": "P",
236
+ "atm_strike": "ATM_Strike",
237
+ "strike_breakdown": "StrikeBreakdown"
238
  }, inplace=True)
239
 
240
  df['ts'] = pd.to_datetime(df['ts'])
 
261
  "CS": "last", # Call Sell → last value
262
  "PB": "last", # Put Buy → last value
263
  "PS": "last", # Put Sell → last value
264
+ "ATM_Strike": "last", # ATM strike → last value
265
  }
 
 
 
 
 
 
 
 
266
 
267
+ # For strike breakdown, we'll keep the last one per resampled period
268
  existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
269
  resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
270
 
271
+ # Handle strike breakdown separately - take last value for each period
272
+ if "StrikeBreakdown" in df.columns:
273
+ breakdown_series = df.resample(panda_tf)["StrikeBreakdown"].last()
274
+ else:
275
+ breakdown_series = None
276
+
277
  # Build response: only include requested variables (reduces egress)
278
  response_data = {
279
+ "labels": resampled.index.strftime('%H:%M').tolist(),
280
+ "mode": mode # Include mode so frontend knows how to display breakdown
281
  }
282
 
283
+ for var_key in ["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS", "ATM_Strike"]:
284
  if var_key in required_vars and var_key in resampled.columns:
285
  response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
+ # Include ATM_Strike and StrikeBreakdown for advanced mode
288
+ if mode == "advanced":
289
+ if "ATM_Strike" in resampled.columns:
290
+ response_data["ATM_Strike"] = [round(float(v), 2) if v else 0 for v in resampled["ATM_Strike"].tolist()]
291
+ if breakdown_series is not None:
292
+ # Convert breakdown to list of lists (each element is the strike breakdown for that timestamp)
293
+ response_data["StrikeBreakdown"] = breakdown_series.fillna('[]').tolist()
294
 
295
+ return JSONResponse(response_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
 
298
  # ==========================================
299
+ # 5. FRONTEND TEMPLATE
300
  # ==========================================
301
 
302
  HTML_TEMPLATE = """
 
398
  width: 560px; max-width: 95%; max-height: 88vh; display: flex; flex-direction: column;
399
  box-shadow: 0 20px 50px rgba(0,0,0,0.5);
400
  }
401
+ .modal.wide { width: 95%; max-width: 1200px; }
 
402
  .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
403
  .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
404
  .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
 
478
  .val-neg { color: #f85149; }
479
  .breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
480
 
481
+ /* STRIKE BREAKDOWN SUB-TABLE */
482
+ .strike-section {
483
+ margin: 10px 0;
484
+ padding: 10px;
485
+ background: var(--bg-panel);
486
+ border-radius: 6px;
487
+ border: 1px solid var(--border);
488
  }
489
+ .strike-section-header {
490
+ display: flex;
491
+ justify-content: space-between;
492
+ align-items: center;
493
+ margin-bottom: 8px;
494
+ padding-bottom: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  border-bottom: 1px solid var(--border);
496
  }
497
+ .strike-time { font-weight: 600; color: var(--accent); }
498
+ .strike-atm { font-size: 11px; color: var(--text-secondary); }
499
+ .strike-subtable {
500
+ width: 100%;
501
+ border-collapse: collapse;
502
+ font-size: 10px;
503
  }
504
+ .strike-subtable th {
505
+ background: var(--bg-input);
506
+ padding: 4px 6px;
507
+ text-align: right;
508
+ border-bottom: 1px solid var(--border);
 
 
 
509
  }
510
+ .strike-subtable td {
511
+ padding: 3px 6px;
512
+ text-align: right;
513
  border-bottom: 1px solid var(--border);
514
  }
515
+ .strike-subtable tr:hover td { background: var(--bg-input); }
516
+ .type-ce { color: #238636; font-weight: 600; }
517
+ .type-pe { color: #da3633; font-weight: 600; }
518
+ .atm-highlight { background: rgba(47,129,247,0.15); }
519
 
520
+ /* TABS FOR BREAKDOWN VIEW */
521
+ .breakdown-tabs {
522
+ display: flex;
523
+ gap: 4px;
524
+ margin-bottom: 12px;
525
  }
526
+ .breakdown-tab {
527
+ padding: 6px 12px;
528
+ border: 1px solid var(--border);
529
+ background: var(--bg-input);
530
+ color: var(--text-secondary);
531
+ border-radius: 4px;
532
+ cursor: pointer;
533
+ font-size: 11px;
 
 
 
 
534
  }
535
+ .breakdown-tab.active {
536
+ background: var(--accent);
537
+ color: white;
538
+ border-color: var(--accent);
539
  }
 
 
540
  </style>
541
  </head>
542
  <body>
 
548
  <div style="position:relative;">
549
  <select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
550
  <option value="LIVE">LIVE</option>
551
+ <option value="HIST">HISTORY</option>
552
  </select>
553
  <div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
554
  </div>
 
621
  <div id="dd-cfgInst" class="custom-dd"></div>
622
  </div>
623
  <div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
624
+ * Advanced Mode: select the Spot/Future as price reference. All CE/PE within ATM ± range will be aggregated.<br>
625
+ <strong>Selection Logic:</strong> For CALLS: N strikes BELOW ATM (ITM) + ATM + (N-1) ABOVE (OTM). For PUTS: N ABOVE (ITM) + ATM + (N-1) BELOW (OTM).
626
  </div>
627
  </div>
628
 
629
  <div id="cfgAdvancedFields" style="display:none;">
630
  <div class="form-section" style="margin-top:12px;">
631
+ <label style="color:var(--accent);">ATM (+/-) Strike Range (N)</label>
632
  <input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
633
  <div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
634
+ Selects 2N strikes per option type: N ITM + N OTM (ATM counted as first OTM).<br>
635
+ Total = 2N CALLS + 2N PUTS = 4N instruments maximum.
636
  </div>
637
  </div>
638
  </div>
 
688
  </div>
689
  </div>
690
 
691
+ <!-- ===== BREAKDOWN MODAL ===== -->
692
  <div class="modal-backdrop" id="breakdownModal">
693
+ <div class="modal wide">
694
  <div class="modal-head">
695
  <span id="bdTitle">Calculation Breakdown</span>
696
  <button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
697
  </div>
698
  <div class="modal-body" style="padding:12px;">
699
  <div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
700
+ <div class="breakdown-tabs" id="bdTabs"></div>
701
+ <div class="breakdown-scroll" id="bdContent"></div>
 
 
 
 
 
 
 
 
 
 
702
  </div>
703
  </div>
704
  </div>
 
728
 
729
  // Extract which variables are used in a list of series formulas
730
  function extractRequiredVars(seriesList) {
731
+ const vars = new Set(['P']);
732
  seriesList.forEach(s => {
733
  const f = s.formula || '';
734
  if (f.includes('$CB')) vars.add('CB');
735
  if (f.includes('$CS')) vars.add('CS');
736
  if (f.includes('$PB')) vars.add('PB');
737
  if (f.includes('$PS')) vars.add('PS');
 
738
  if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
739
  if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
740
  if (f.includes('$V')) vars.add('V');
 
830
  const App = {
831
  state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
832
 
 
833
  presets: {
 
834
  "Basic Flow (Fut/Eq)": [
835
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
836
  { formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
 
840
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
841
  { formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
842
  ],
 
843
  "Call Flow (Options)": [
844
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
845
  { formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
 
859
  { formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
860
  { formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
861
  ],
 
862
  "Price & Flows (Advanced)": [
863
  { formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
864
  { formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
 
1071
  endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1072
  }
1073
 
 
1074
  const requiredVars = extractRequiredVars(cfg.series || []);
1075
 
1076
  try {
 
1101
  return;
1102
  }
1103
 
 
1104
  chart.lastData = data;
1105
 
1106
  const body = document.getElementById(`body-${id}`);
 
1244
  };
1245
 
1246
  // ===================================================
1247
+ // BREAKDOWN TABLE (WITH STRIKE-LEVEL BREAKDOWN)
1248
  // ===================================================
1249
  const Breakdown = {
1250
  currentTab: 'summary',
1251
+
 
 
 
 
 
 
 
 
 
 
 
1252
  open(id) {
1253
  const chart = App.state.charts[id];
1254
  if (!chart || !chart.lastData || !chart.config.series) {
 
1259
  const data = chart.lastData;
1260
  const cfg = chart.config;
1261
  const series = cfg.series;
1262
+ const isAdvanced = cfg.chartMode === 'advanced';
1263
 
1264
+ // Set title
1265
+ document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${isAdvanced ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
1266
+ document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
1267
+
1268
+ // Setup tabs
1269
+ const tabsDiv = document.getElementById('bdTabs');
1270
+ if (isAdvanced && data.StrikeBreakdown) {
1271
+ tabsDiv.innerHTML = `
1272
+ <button class="breakdown-tab ${this.currentTab === 'summary' ? 'active' : ''}" onclick="Breakdown.showTab('summary')">Summary</button>
1273
+ <button class="breakdown-tab ${this.currentTab === 'strikes' ? 'active' : ''}" onclick="Breakdown.showTab('strikes')">Strike Breakdown</button>
 
 
 
1274
  `;
1275
+ tabsDiv.style.display = 'flex';
1276
+ } else {
1277
+ tabsDiv.style.display = 'none';
1278
  }
1279
 
1280
+ // Store data for tab switching
1281
+ this.chartData = { data, cfg, series, isAdvanced };
1282
+ this.showTab(this.currentTab);
1283
  document.getElementById('breakdownModal').style.display = 'flex';
 
 
 
1284
  },
1285
+
1286
+ showTab(tabName) {
1287
+ this.currentTab = tabName;
1288
+ const { data, cfg, series, isAdvanced } = this.chartData;
1289
+
1290
+ // Update tab buttons
1291
+ document.querySelectorAll('.breakdown-tab').forEach(btn => {
1292
+ btn.classList.toggle('active', btn.textContent.toLowerCase().includes(tabName));
1293
+ });
1294
+
1295
+ const contentDiv = document.getElementById('bdContent');
1296
+
1297
+ if (tabName === 'summary') {
1298
+ contentDiv.innerHTML = this.renderSummaryTable(data, series);
1299
+ } else if (tabName === 'strikes' && isAdvanced) {
1300
+ contentDiv.innerHTML = this.renderStrikeBreakdown(data, cfg);
1301
+ }
1302
+ },
1303
+
1304
+ renderSummaryTable(data, series) {
1305
  const allVarKeys = ['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
1306
  const presentVars = allVarKeys.filter(k =>
1307
  data[k] && data[k].some(v => v && v !== 0)
1308
  );
1309
 
 
1310
  let thead = '<thead><tr><th>Time</th>';
 
 
 
 
1311
  presentVars.forEach(v => { thead += `<th>${v}</th>`; });
1312
  series.forEach(s => { thead += `<th>${s.label}</th>`; });
1313
  thead += '</tr></thead>';
1314
 
 
1315
  let tbody = '<tbody>';
1316
  data.labels.forEach((lbl, i) => {
1317
  const vars = {
 
1323
  };
1324
 
1325
  tbody += `<tr><td>${lbl}</td>`;
 
 
 
 
 
 
1326
 
 
1327
  presentVars.forEach(k => {
1328
  const v = vars[k];
1329
  const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
1330
  tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
1331
  });
1332
 
 
1333
  series.forEach(s => {
1334
  const result = evalFormula(s.formula, vars);
1335
  const rv = result !== null ? result : 0;
 
1349
  });
1350
  tbody += '</tbody>';
1351
 
1352
+ return `<table class="breakdown-table">${thead}${tbody}</table>`;
1353
  },
1354
+
1355
+ renderStrikeBreakdown(data, cfg) {
1356
  const atmRange = cfg.atmRange || 5;
1357
+ let html = `<div style="margin-bottom:12px; padding:10px; background:var(--bg-panel); border-radius:6px; border:1px solid var(--border);">
1358
+ <div style="font-size:11px; color:var(--text-secondary);">
1359
+ <strong style="color:var(--accent);">Selection Logic:</strong><br>
1360
+ <strong>For CALLS (CE):</strong> ${atmRange} ITM (below ATM) + ${atmRange} OTM (ATM + ${(atmRange-1)} above) = ${atmRange*2} strikes<br>
1361
+ <strong>For PUTS (PE):</strong> ${atmRange} ITM (above ATM) + ${atmRange} OTM (ATM + ${(atmRange-1)} below) = ${atmRange*2} strikes<br>
1362
+ <strong>Total instruments:</strong> Up to ${atmRange*4} (some strikes may overlap between CE/PE)
1363
+ </div>
1364
+ </div>`;
1365
+
1366
+ // Check if we have strike breakdown data
1367
+ if (!data.StrikeBreakdown || !data.StrikeBreakdown.length) {
1368
+ return html + '<div style="color:var(--text-secondary); text-align:center; padding:20px;">No strike breakdown data available. Using summary view.</div>';
1369
+ }
1370
+
1371
+ // Render strike breakdown for each timestamp (limit to first 10 for performance)
1372
+ const maxTimestamps = 10;
1373
+ const labels = data.labels || [];
1374
+
1375
+ for (let i = 0; i < Math.min(labels.length, maxTimestamps); i++) {
1376
+ const time = labels[i];
1377
+ const breakdown = data.StrikeBreakdown[i];
1378
+ const atmStrike = data.ATM_Strike ? data.ATM_Strike[i] : null;
1379
+
1380
+ if (!breakdown || !Array.isArray(breakdown)) continue;
1381
+
1382
+ // Separate CE and PE
1383
+ const ceStrikes = breakdown.filter(s => s.type === 'CE').sort((a, b) => a.strike - b.strike);
1384
+ const peStrikes = breakdown.filter(s => s.type === 'PE').sort((a, b) => a.strike - b.strike);
1385
+
1386
+ html += `<div class="strike-section">
1387
+ <div class="strike-section-header">
1388
+ <span class="strike-time">${time}</span>
1389
+ <span class="strike-atm">ATM: ${atmStrike ? atmStrike.toFixed(0) : 'N/A'}</span>
1390
+ </div>`;
1391
+
1392
+ // CE Table
1393
+ if (ceStrikes.length > 0) {
1394
+ html += `<div style="margin-bottom:8px;"><span class="type-ce">CALLS (CE)</span></div>
1395
+ <table class="strike-subtable">
1396
+ <thead><tr>
1397
+ <th style="text-align:left;">Strike</th>
1398
+ <th>Rank</th>
1399
+ <th>Buy (b)</th>
1400
+ <th>Sell (s)</th>
1401
+ <th>Vol</th>
1402
+ <th>Price</th>
1403
+ <th>Offset</th>
1404
+ </tr></thead>
1405
+ <tbody>`;
1406
+
1407
+ ceStrikes.forEach(s => {
1408
+ const isAtm = atmStrike && Math.abs(s.strike - atmStrike) < 1;
1409
+ const rowClass = isAtm ? 'atm-highlight' : '';
1410
+ const offsetClass = s.offset < 0 ? 'val-pos' : (s.offset > 0 ? 'val-neg' : '');
1411
+ html += `<tr class="${rowClass}">
1412
+ <td style="text-align:left; font-weight:600;">${s.strike}</td>
1413
+ <td>${s.rank}</td>
1414
+ <td class="val-pos">${(s.b || 0).toLocaleString()}</td>
1415
+ <td class="val-neg">${(s.s || 0).toLocaleString()}</td>
1416
+ <td>${(s.v || 0).toLocaleString()}</td>
1417
+ <td>${(s.p || 0).toFixed(2)}</td>
1418
+ <td class="${offsetClass}">${s.offset > 0 ? '+' : ''}${s.offset}</td>
1419
+ </tr>`;
1420
  });
1421
+
1422
+ html += `</tbody></table>`;
1423
+ }
1424
+
1425
+ // PE Table
1426
+ if (peStrikes.length > 0) {
1427
+ html += `<div style="margin:8px 0;"><span class="type-pe">PUTS (PE)</span></div>
1428
+ <table class="strike-subtable">
1429
+ <thead><tr>
1430
+ <th style="text-align:left;">Strike</th>
1431
+ <th>Rank</th>
1432
+ <th>Buy (b)</th>
1433
+ <th>Sell (s)</th>
1434
+ <th>Vol</th>
1435
+ <th>Price</th>
1436
+ <th>Offset</th>
1437
+ </tr></thead>
1438
+ <tbody>`;
1439
+
1440
+ peStrikes.forEach(s => {
1441
+ const isAtm = atmStrike && Math.abs(s.strike - atmStrike) < 1;
1442
+ const rowClass = isAtm ? 'atm-highlight' : '';
1443
+ const offsetClass = s.offset > 0 ? 'val-pos' : (s.offset < 0 ? 'val-neg' : '');
1444
+ html += `<tr class="${rowClass}">
1445
+ <td style="text-align:left; font-weight:600;">${s.strike}</td>
1446
+ <td>${s.rank}</td>
1447
+ <td class="val-pos">${(s.b || 0).toLocaleString()}</td>
1448
+ <td class="val-neg">${(s.s || 0).toLocaleString()}</td>
1449
+ <td>${(s.v || 0).toLocaleString()}</td>
1450
+ <td>${(s.p || 0).toFixed(2)}</td>
1451
+ <td class="${offsetClass}">${s.offset > 0 ? '+' : ''}${s.offset}</td>
1452
+ </tr>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1453
  });
1454
+
1455
+ html += `</tbody></table>`;
 
 
1456
  }
1457
+
1458
+ // Aggregated totals for this timestamp
1459
+ const totalCB = ceStrikes.reduce((sum, s) => sum + (s.b || 0), 0);
1460
+ const totalCS = ceStrikes.reduce((sum, s) => sum + (s.s || 0), 0);
1461
+ const totalPB = peStrikes.reduce((sum, s) => sum + (s.b || 0), 0);
1462
+ const totalPS = peStrikes.reduce((sum, s) => sum + (s.s || 0), 0);
1463
+
1464
+ html += `<div style="margin-top:8px; padding-top:8px; border-top:1px solid var(--border); font-size:10px;">
1465
+ <strong>Totals:</strong>
1466
+ <span class="type-ce">CB: ${totalCB.toLocaleString()}</span> |
1467
+ <span class="type-ce">CS: ${totalCS.toLocaleString()}</span> |
1468
+ <span class="type-pe">PB: ${totalPB.toLocaleString()}</span> |
1469
+ <span class="type-pe">PS: ${totalPS.toLocaleString()}</span>
1470
+ </div>`;
1471
+
1472
+ html += `</div>`;
1473
+ }
1474
+
1475
+ if (labels.length > maxTimestamps) {
1476
+ html += `<div style="text-align:center; color:var(--text-secondary); font-size:11px; padding:10px;">
1477
+ Showing first ${maxTimestamps} of ${labels.length} timestamps. Use Summary tab for complete view.
1478
+ </div>`;
 
 
 
1479
  }
1480
+
1481
+ return html;
1482
  }
1483
  };
1484
 
 
1497
  document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
1498
  document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1499
  this.updatePresetsDropdown();
 
1500
  const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
1501
  this.loadPreset(defPreset);
1502
  },
 
1537
  }
1538
  },
1539
 
1540
+ extractSeriesFromDOM() {
1541
+ return Array.from(document.querySelectorAll('.series-row')).map(row => ({
1542
+ formula: row.querySelector('.s-form')?.value || '$P',
1543
+ label: row.querySelector('.s-lbl')?.value || 'Line',
1544
+ axis: row.querySelector('.s-axis')?.value || 'right',
1545
+ color1: row.querySelector('.color-input')?.value || '#2f81f7',
1546
+ cond: row.querySelector('.s-cond')?.value || 'none',
1547
+ thresh: row.querySelector('.s-thresh')?.value || '0',
1548
+ color2: '#da3633',
1549
+ labelTrue: row.querySelector('.s-label-true')?.value || '',
1550
+ labelFalse: row.querySelector('.s-label-false')?.value || ''
1551
+ }));
1552
+ },
1553
 
1554
+ renderSeries(list) {
1555
+ document.getElementById('cfgSeries').innerHTML = list.map((s, i) => `
1556
+ <div class="series-row">
1557
+ <div class="series-row-top">
1558
+ <input class="s-form" value="${s.formula}" placeholder="Formula ($P, $CB, etc.)" style="flex:2;">
1559
+ <input class="s-lbl" value="${s.label}" placeholder="Label" style="flex:1;">
1560
+ <select class="s-axis" style="width:70px;">
1561
+ <option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option>
1562
+ <option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option>
1563
+ </select>
1564
+ <input type="color" class="color-input" value="${s.color1}">
1565
+ </div>
1566
+ <div class="series-row-cond" style="${s.cond && s.cond !== 'none' ? '' : 'display:none;'}">
1567
+ <span class="cond-label">Condition:</span>
1568
+ <select class="s-cond" onchange="Config.toggleCond(this)" style="width:50px;">
1569
+ <option value="none" ${s.cond === 'none' ? 'selected' : ''}>—</option>
1570
+ <option value=">" ${s.cond === '>' ? 'selected' : ''}>&gt;</option>
1571
+ <option value="<" ${s.cond === '<' ? 'selected' : ''}>&lt;</option>
1572
+ </select>
1573
+ <input class="s-thresh" value="${s.thresh || '0'}" placeholder="0" style="width:60px;">
1574
+ <input class="color-input" value="${s.color2 || '#da3633'}" title="Color if false">
1575
+ <input class="s-label-true" value="${s.labelTrue || ''}" placeholder="True label" style="flex:1;">
1576
+ <input class="s-label-false" value="${s.labelFalse || ''}" placeholder="False label" style="flex:1;">
1577
+ </div>
1578
+ </div>
1579
+ `).join('');
1580
+ },
1581
 
1582
+ toggleCond(sel) {
1583
+ const row = sel.closest('.series-row');
1584
+ const condDiv = row.querySelector('.series-row-cond');
1585
+ condDiv.style.display = sel.value === 'none' ? 'none' : 'flex';
1586
+ },
1587
 
1588
+ addSeriesRow() {
1589
+ const list = this.extractSeriesFromDOM();
1590
+ list.push({ formula: '$P', label: 'New Line', axis: 'right', color1: '#2f81f7', cond: 'none' });
1591
+ this.renderSeries(list);
1592
+ },
1593
 
1594
+ open(id) {
1595
+ this.tid = id;
1596
+ this.isGlobal = false;
1597
+ const chart = App.state.charts[id];
1598
+ const cfg = chart?.config || {};
 
 
1599
 
1600
+ document.getElementById('cfgTitle').innerText = 'Configure Chart';
1601
+ document.getElementById('cfgGlobalOnly').style.display = 'none';
1602
+ document.getElementById('cfgChartOnly').style.display = 'block';
1603
 
1604
+ this.setMode(cfg.chartMode || 'normal');
1605
+ document.getElementById('cfgRoot').value = cfg.root || '';
1606
+ document.getElementById('cfgExp').value = cfg.expiry || '';
1607
  document.getElementById('cfgInst').value = cfg.instrument || '';
1608
+ document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1609
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1610
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
1611
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
1612
 
1613
+ CustomDD.clear('cfgRoot'); CustomDD.clear('cfgExp'); CustomDD.clear('cfgInst');
1614
 
1615
+ if (cfg.root) {
1616
+ this.onRootChange(true);
1617
+ if (cfg.expiry) this.onExpChange();
1618
+ }
1619
+
1620
+ this.renderSeries(cfg.series || App.presets['Basic Flow (Fut/Eq)']);
1621
+ this.updatePresetsDropdown();
1622
+ if (cfg.series) this.loadPreset();
1623
+
1624
+ document.getElementById('configModal').style.display = 'flex';
1625
  },
1626
 
1627
  openDefaults() {
1628
  this.isGlobal = true;
1629
+ document.getElementById('cfgTitle').innerText = 'Settings';
 
 
 
1630
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1631
  document.getElementById('cfgChartOnly').style.display = 'none';
1632
  document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1633
+ document.getElementById('configModal').style.display = 'flex';
1634
  },
1635
 
1636
+ close() {
1637
+ document.getElementById('configModal').style.display = 'none';
1638
+ },
1639
 
1640
+ async onRootChange(skipExpiryClear = false) {
1641
+ const root = document.getElementById('cfgRoot').value;
 
 
 
1642
  if (!root) return;
1643
+ try {
1644
+ const res = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`);
1645
+ const d = await res.json();
1646
+ CustomDD.set('cfgExp', d.expiries || []);
1647
+ if (d.current?.expiry) {
1648
+ document.getElementById('cfgExp').value = d.current.expiry;
1649
+ this.onExpChange();
 
 
 
 
 
 
1650
  }
1651
+ } catch(e) {}
1652
  },
1653
 
1654
+ async onExpChange() {
1655
+ const root = document.getElementById('cfgRoot').value;
1656
+ const exp = document.getElementById('cfgExp').value;
 
1657
  if (!root || !exp) return;
1658
+ try {
1659
+ const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
1660
+ const d = await res.json();
1661
+ CustomDD.set('cfgInst', d.spot_fut || [], 'Spot/Fut');
1662
+ CustomDD.set('cfgInst', d.options || [], 'Options');
1663
+ const inst = document.getElementById('cfgInst');
1664
+ if (!inst.value && d.spot_fut?.length) inst.value = d.spot_fut[0];
1665
+ } catch(e) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1666
  },
1667
 
1668
  saveChartConfig() {
1669
  if (this.isGlobal) {
1670
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1671
  App.save();
1672
+ } else {
1673
+ const chart = App.state.charts[this.tid];
1674
+ if (!chart) return;
1675
+ chart.config = {
1676
+ root: document.getElementById('cfgRoot').value,
1677
+ expiry: document.getElementById('cfgExp').value,
1678
+ instrument: document.getElementById('cfgInst').value,
1679
+ chartMode: this.currentMode,
1680
+ atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1681
+ timeframe: document.getElementById('cfgTF').value,
1682
+ start: document.getElementById('cfgStart').value,
1683
+ end: document.getElementById('cfgEnd').value,
1684
+ series: this.extractSeriesFromDOM()
1685
+ };
1686
+ App.save();
1687
+ App.renderGrid();
1688
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1689
  this.close();
1690
  }
1691
  };
1692
 
1693
+ // ===================================================
1694
+ // INIT
1695
+ // ===================================================
1696
+ window.addEventListener('DOMContentLoaded', () => App.init());
1697
  </script>
1698
  </body>
1699
  </html>
1700
  """
1701
 
1702
  if __name__ == "__main__":
1703
+ print("⚠️ Checking for existing processes on Port 8000...")
1704
+ import os
1705
+ os.system("fuser -k 8000/tcp 2>/dev/null")
1706
+ import time
1707
+ time.sleep(1)
1708
+
1709
+ config = uvicorn.Config(app, host="0.0.0.0", port=8000)
1710
+ server = uvicorn.Server(config)
1711
+ asyncio.run(server.serve())