topsecrettraders commited on
Commit
b69ee78
·
verified ·
1 Parent(s): 8bb8c16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +330 -421
app.py CHANGED
@@ -239,7 +239,8 @@ async def fetch_series(
239
 
240
  # =========================================================
241
  # CHUNKING LOGIC: Bypass Supabase DB/Timeout Limits
242
- # Fetches 2-hour chunks concurrently!
 
243
  # =========================================================
244
  async def fetch_chunk(start_dt, end_dt):
245
  params = base_params.copy()
@@ -251,15 +252,27 @@ async def fetch_series(
251
 
252
  tasks = []
253
  curr = t_start_dt
254
- chunk_hours = 2 # Safe size for Postgres processing limit
 
 
255
 
256
  while curr <= t_end_dt:
257
- nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
 
 
 
 
 
 
258
  if nxt > t_end_dt:
259
  nxt = t_end_dt
260
 
261
  tasks.append(fetch_chunk(curr, nxt))
262
- curr += timedelta(hours=chunk_hours)
 
 
 
 
263
 
264
  # Execute all chunks in parallel
265
  chunk_results = await asyncio.gather(*tasks)
@@ -1008,8 +1021,8 @@ const App = {
1008
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
1009
  ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
1010
  </div>
1011
- <div class="card-tools" style="display: flex; gap: 4px;">
1012
- <button class="tool-btn info-btn" id="infoBtn-${id}" onclick="InspectionModal.open('${id}')" title="Calculation Breakdown" style="display: ${isCfg ? 'flex' : 'none'};">i</button>
1013
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
1014
  <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
1015
  </div>
@@ -1070,69 +1083,6 @@ const App = {
1070
  }
1071
  };
1072
 
1073
- // Inspection Modal for Calculation Breakdown
1074
- const InspectionModal = {
1075
- chartData: null,
1076
- seriesConfig: null,
1077
-
1078
- open(chartId) {
1079
- const chart = App.state.charts[chartId];
1080
- if(!chart || !chart.chartData) return;
1081
-
1082
- this.chartData = chart.chartData;
1083
- this.seriesConfig = chart.config.series;
1084
-
1085
- // Get the formula for Net Flow if exists
1086
- const netFlowSeries = this.seriesConfig.find(s => s.formula.includes('CB') || s.formula.includes('PS'));
1087
- document.getElementById('infoFormula').innerText = netFlowSeries ? netFlowSeries.formula : 'N/A';
1088
-
1089
- this.renderTable();
1090
- document.getElementById('infoModal').style.display = 'flex';
1091
- },
1092
-
1093
- renderTable() {
1094
- const tbody = document.getElementById('infoTableBody');
1095
- const data = this.chartData;
1096
-
1097
- if(!data || !data.labels) {
1098
- tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;">No data available</td></tr>';
1099
- return;
1100
- }
1101
-
1102
- let html = '';
1103
- for(let i = 0; i < data.labels.length; i++) {
1104
- const cb = data.CB[i] || 0;
1105
- const cs = data.CS[i] || 0;
1106
- const pb = data.PB[i] || 0;
1107
- const ps = data.PS[i] || 0;
1108
- const bullSide = cb + ps;
1109
- const bearSide = pb + cs;
1110
- const netFlow = bullSide - bearSide;
1111
- const isPositive = netFlow >= 0;
1112
-
1113
- html += `
1114
- <tr>
1115
- <td>${data.labels[i]}</td>
1116
- <td>${formatNumber(cb)}</td>
1117
- <td>${formatNumber(cs)}</td>
1118
- <td>${formatNumber(pb)}</td>
1119
- <td>${formatNumber(ps)}</td>
1120
- <td>${formatNumber(bullSide)}</td>
1121
- <td>${formatNumber(bearSide)}</td>
1122
- <td class="${isPositive ? 'positive' : 'negative'}">${formatNumber(netFlow)} (${isPositive ? 'BULLISH' : 'BEARISH'})</td>
1123
- </tr>
1124
- `;
1125
- }
1126
- tbody.innerHTML = html;
1127
- }
1128
- };
1129
-
1130
- function formatNumber(num) {
1131
- if(num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
1132
- if(num >= 1000) return (num / 1000).toFixed(1) + 'K';
1133
- return num.toFixed(0);
1134
- }
1135
-
1136
  const ChartLogic = {
1137
  async plot(id, isUpdate=false) {
1138
  const chart = App.state.charts[id];
@@ -1171,50 +1121,39 @@ const ChartLogic = {
1171
  if(dot) dot.classList.remove('retry');
1172
  return;
1173
  }
 
 
 
1174
 
1175
- // Store chart data for inspection
1176
- App.state.charts[id].chartData = data;
 
 
 
1177
 
1178
  const body = document.getElementById(`body-${id}`);
1179
  if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1180
 
1181
  if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
1182
 
1183
- // Show/hide info button
1184
- const infoBtn = document.getElementById(`infoBtn-${id}`);
1185
- if(infoBtn) infoBtn.style.display = 'flex';
1186
-
1187
  const datasets = cfg.series.map(s => {
1188
  const vals = data.labels.map((_, i) => {
1189
- const P = data.P[i] || 0;
1190
- const V = data.V[i] || 0;
1191
- const OI = data.OI[i] || 0;
1192
- const B = data.B[i] || 0;
1193
- const S = data.S[i] || 0;
1194
- const CB = data.CB[i] || 0;
1195
- const CS = data.CS[i] || 0;
1196
- const PB = data.PB[i] || 0;
1197
- const PS = data.PS[i] || 0;
1198
 
 
1199
  let f = s.formula
1200
- .replace(/\$CB/g, str(CB)).replace(/\$CS/g, str(CS))
1201
- .replace(/\$PB/g, str(PB)).replace(/\$PS/g, str(PS))
1202
- .replace(/\$OI/g, str(OI)).replace(/\$P/g, str(P))
1203
- .replace(/\$V/g, str(V)).replace(/\$B/g, str(B)).replace(/\$S/g, str(S));
1204
 
1205
  try { return new Function('return '+f)(); } catch{ return null; }
1206
  });
1207
 
1208
  let dataset = {
1209
- label: s.label,
1210
- data: vals,
1211
- borderWidth: 1.5,
1212
- pointRadius: 0,
1213
- hitRadius: 10,
1214
- tension: 0.1,
1215
- yAxisID: s.axis==='left'?'y':'y1',
1216
- labelTrue: s.labelTrue || '',
1217
- labelFalse: s.labelFalse || ''
1218
  };
1219
 
1220
  if(s.cond && s.cond !== 'none') {
@@ -1232,10 +1171,6 @@ const ChartLogic = {
1232
  };
1233
  dataset.borderColor = cTrue;
1234
  dataset.backgroundColor = cTrue;
1235
- dataset.threshold = thresh;
1236
- dataset.condition = s.cond;
1237
- dataset.colorTrue = cTrue;
1238
- dataset.colorFalse = cFalse;
1239
 
1240
  const hoverColorCallback = (ctx) => {
1241
  let v = ctx.raw;
@@ -1250,411 +1185,385 @@ const ChartLogic = {
1250
  } else {
1251
  dataset.borderColor = s.color1;
1252
  dataset.backgroundColor = s.color1;
1253
- dataset.pointHoverBackgroundColor = s.color1;
1254
- dataset.pointHoverBorderColor = s.color1;
1255
  }
1256
-
1257
  return dataset;
1258
  });
1259
 
1260
- this.render(id, data.labels, datasets, cfg.series);
1261
-
1262
- } catch(e) { console.error(e); }
1263
- },
1264
-
1265
- render(id, labels, datasets, seriesConfig) {
1266
- const ctx = document.getElementById(`canvas-${id}`);
1267
- if(!ctx) return;
1268
-
1269
- if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
1270
- App.state.charts[id].instance.destroy();
1271
- App.state.charts[id].instance = null;
1272
- }
1273
-
1274
- if(App.state.charts[id].instance) {
1275
- App.state.charts[id].instance.data.labels = labels;
1276
- App.state.charts[id].instance.data.datasets = datasets;
1277
- App.state.charts[id].instance.update('none');
1278
- } else {
1279
- const hasRight = datasets.some(d => d.yAxisID === 'y1');
1280
- App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
1281
  type: 'line',
1282
- data: { labels, datasets },
1283
  options: {
1284
- responsive: true, maintainAspectRatio: false, animation: false,
 
1285
  interaction: { mode: 'index', intersect: false },
1286
- plugins: {
1287
- legend: { display: false },
1288
- tooltip: {
1289
- backgroundColor: 'rgba(22, 27, 34, 0.95)',
1290
- titleColor:'#e6edf3',
1291
- bodyColor:'#e6edf3',
1292
- borderColor: '#30363d',
 
 
 
 
 
 
 
 
1293
  borderWidth: 1,
1294
- callbacks: {
1295
- label: function(context) {
1296
- let s = seriesConfig[context.datasetIndex];
1297
- let v = context.parsed.y;
1298
- let label = s.label;
1299
- let color = s.color1;
1300
-
1301
- if(s.cond && s.cond !== 'none') {
1302
- let thresh = parseFloat(s.thresh) || 0;
1303
- if(s.cond === '>') {
1304
- label = v > thresh ? (s.labelTrue || s.label) : (s.labelFalse || s.label);
1305
- color = v > thresh ? s.color1 : s.color2;
1306
- }
1307
- if(s.cond === '<') {
1308
- label = v < thresh ? (s.labelTrue || s.label) : (s.labelFalse || s.label);
1309
- color = v < thresh ? s.color1 : s.color2;
1310
- }
1311
- }
1312
-
1313
- return `${label}: ${formatNumber(v)}`;
1314
- },
1315
- labelColor: function(context) {
1316
- let s = seriesConfig[context.datasetIndex];
1317
- let v = context.parsed.y;
1318
- let c = s.color1;
1319
-
1320
- if(s.cond && s.cond !== 'none') {
1321
- let thresh = parseFloat(s.thresh) || 0;
1322
- if(s.cond === '>') c = v > thresh ? s.color1 : (s.color2 || s.color1);
1323
- if(s.cond === '<') c = v < thresh ? s.color1 : (s.color2 || s.color1);
1324
- }
1325
- return { borderColor: c, backgroundColor: c };
1326
- }
1327
- }
1328
  }
1329
  },
1330
  scales: {
1331
- x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
1332
- y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
1333
- y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1334
  }
1335
  }
1336
  });
 
 
 
1337
  }
1338
- }
1339
- };
1340
 
1341
- const Config = {
1342
- tid: null, isGlobal: false, currentMode: 'normal',
1343
-
1344
- setMode(mode) {
1345
- this.currentMode = mode;
1346
- const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1347
- btns[0].classList.toggle('active', mode === 'normal');
1348
- btns[1].classList.toggle('active', mode === 'advanced');
1349
 
1350
- document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
1351
- document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (For ATM Calc)';
1352
- document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
1353
-
1354
- this.updateInfoTooltip(mode);
1355
- this.updatePresetsDropdown();
1356
 
1357
- // Load appropriate default preset
1358
- if (mode === 'advanced') {
1359
- this.loadPreset("Advanced - Net Flow");
1360
- } else {
1361
- this.loadPreset("Overall Net Flow");
1362
- }
1363
- },
1364
-
1365
- updateInfoTooltip(mode) {
1366
- const icon = document.getElementById('infoIcon');
1367
- if (!icon) return;
1368
- if (mode === 'advanced') {
1369
- icon.title = "Standard Variables:\\n$P = Spot Price\\n$V = Volume\\n$OI = Open Interest\\n\\nAdvanced Mode Only:\\n$CB = Call Buy (Sum in range)\\n$CS = Call Sell (Sum in range)\\n$PB = Put Buy (Sum in range)\\n$PS = Put Sell (Sum in range)";
1370
- icon.style.color = "var(--accent)";
1371
- icon.style.borderColor = "var(--accent)";
1372
- } else {
1373
- icon.title = "Standard Variables:\\n$P = Price\\n$V = Volume\\n$OI = Open Interest\\n$B = Buy Qty (Futures/Spot)\\n$S = Sell Qty (Futures/Spot)\\n\\nOptions Only:\\n$CB = Call Buy\\n$CS = Call Sell\\n$PB = Put Buy\\n$PS = Put Sell";
1374
- icon.style.color = "var(--text-secondary)";
1375
- icon.style.borderColor = "var(--border)";
1376
- }
1377
- },
1378
-
1379
- updatePresetsDropdown() {
1380
- const sel = document.getElementById('cfgPresetsDropdown');
1381
- const mode = this.currentMode;
1382
 
1383
- // Filter presets based on mode
1384
- let presetNames;
1385
- if (mode === 'advanced') {
1386
- presetNames = Object.keys(App.presets).filter(k => k.startsWith('Advanced'));
1387
- } else {
1388
- presetNames = Object.keys(App.presets).filter(k => !k.startsWith('Advanced'));
1389
- }
1390
 
1391
- sel.innerHTML = presetNames.map(k => `<option value="${k}">${k}</option>`).join('');
1392
- },
1393
-
1394
- loadPreset(presetName = null) {
1395
- const name = presetName || document.getElementById('cfgPresetsDropdown').value;
1396
- if(name && App.presets[name]) {
1397
- document.getElementById('cfgPresetsDropdown').value = name;
1398
- this.renderSeries(App.presets[name]);
1399
- }
1400
- },
1401
-
1402
- savePreset() {
1403
- const name = prompt("Enter a name for this Quick Configuration:", "Custom Model");
1404
- if(!name) return;
1405
- const series = this.extractSeriesFromDOM();
1406
- App.presets[name] = series;
1407
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1408
- this.updatePresetsDropdown();
1409
- document.getElementById('cfgPresetsDropdown').value = name;
1410
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
1411
 
1412
- deletePreset() {
1413
- const name = document.getElementById('cfgPresetsDropdown').value;
1414
- if(name.startsWith('Basic') || name.startsWith('Net Flow') || name.startsWith('Call') || name.startsWith('Put') || name.startsWith('Aggregated') || name.startsWith('Overall') || name.startsWith('Advanced')) {
1415
- return alert("Cannot delete default models.");
1416
- }
1417
- if(confirm(`Delete model '${name}'?`)) {
1418
- delete App.presets[name];
1419
- localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1420
- this.updatePresetsDropdown();
1421
- this.loadPreset();
1422
- }
1423
- },
1424
 
1425
- async open(id) {
1426
- this.tid = id; this.isGlobal = false;
1427
- document.getElementById('configModal').style.display = 'flex';
 
 
1428
  document.getElementById('cfgTitle').innerText = 'Configure Chart';
1429
- document.getElementById('cfgLoading').style.display = 'block';
1430
- document.getElementById('cfgContent').style.display = 'none';
1431
-
1432
  document.getElementById('cfgGlobalOnly').style.display = 'none';
1433
  document.getElementById('cfgChartOnly').style.display = 'block';
1434
-
1435
- this.updatePresetsDropdown();
1436
-
1437
- CustomDD.clear('cfgRoot');
1438
- const res = await fetch(`/api/roots?date=${App.state.date}`);
1439
- const roots = await res.json();
1440
- CustomDD.set('cfgRoot', roots);
1441
-
1442
- const cfg = App.state.charts[id].config;
1443
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1444
  document.getElementById('cfgStart').value = cfg.start || '09:15';
1445
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
 
1446
 
 
1447
  this.currentMode = cfg.chartMode || 'normal';
1448
- document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1449
 
1450
- const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1451
- btns[0].classList.toggle('active', this.currentMode === 'normal');
1452
- btns[1].classList.toggle('active', this.currentMode === 'advanced');
1453
 
1454
- document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1455
- document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (For ATM Calc)';
1456
- document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
1457
-
1458
- this.updateInfoTooltip(this.currentMode);
1459
- this.updatePresetsDropdown();
1460
-
1461
- if (cfg.series && cfg.series.length > 0) {
1462
- this.renderSeries(cfg.series);
1463
- } else {
1464
- this.loadPreset(this.currentMode === 'advanced' ? "Advanced - Net Flow" : "Overall Net Flow");
1465
  }
1466
-
1467
- document.getElementById('cfgRoot').value = cfg.root || '';
1468
- document.getElementById('cfgExp').value = cfg.expiry || '';
1469
- document.getElementById('cfgInst').value = cfg.instrument || '';
1470
-
1471
- if(cfg.root) { await this.onRootChange(cfg.expiry, cfg.instrument); }
1472
- document.getElementById('cfgLoading').style.display = 'none';
1473
- document.getElementById('cfgContent').style.display = 'block';
1474
  },
1475
 
1476
  openDefaults() {
1477
  this.isGlobal = true;
1478
- document.getElementById('configModal').style.display = 'flex';
1479
- document.getElementById('cfgTitle').innerText = 'Global Settings';
1480
- document.getElementById('cfgLoading').style.display = 'none';
1481
- document.getElementById('cfgContent').style.display = 'block';
1482
-
1483
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1484
  document.getElementById('cfgChartOnly').style.display = 'none';
1485
- document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
 
 
1486
 
1487
- this.updatePresetsDropdown();
1488
- this.loadPreset("Overall Net Flow");
1489
  },
1490
 
1491
- close() { document.getElementById('configModal').style.display = 'none'; },
 
 
 
1492
 
1493
- async onRootChange(preExp, preInst) {
1494
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1495
- CustomDD.clear('cfgExp');
1496
- CustomDD.clear('cfgInst');
1497
 
1498
- if (!preExp) document.getElementById('cfgExp').value = '';
1499
- if (!preInst) document.getElementById('cfgInst').value = '';
1500
-
1501
- if (!root) return;
 
 
 
 
 
 
 
 
 
 
1502
 
 
 
 
 
 
1503
  const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1504
  const exps = await res.json();
1505
- CustomDD.set('cfgExp', exps);
1506
 
1507
- if(preExp && exps.includes(preExp)) {
1508
- document.getElementById('cfgExp').value = preExp;
1509
- await this.onExpChange(preInst);
1510
- }
1511
- else if (exps.length > 0) {
1512
- const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
1513
- if(auto.current) {
1514
- document.getElementById('cfgExp').value = auto.current.expiry;
1515
- await this.onExpChange(auto.current.instrument);
1516
- }
1517
- }
1518
  },
1519
 
1520
- async onExpChange(preInst) {
1521
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1522
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1523
- CustomDD.clear('cfgInst');
1524
 
1525
- if (!root || !exp) return;
 
1526
 
1527
- const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
 
1528
  const data = await res.json();
1529
 
1530
- if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
1531
- if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
1532
-
1533
- if(preInst) document.getElementById('cfgInst').value = preInst;
1534
  },
1535
 
1536
- toggleCond(sel) {
1537
- const group = sel.nextElementSibling;
1538
- group.style.display = sel.value === 'none' ? 'none' : 'flex';
1539
- },
1540
-
1541
- renderSeries(list) {
1542
- const c = document.getElementById('cfgSeries'); c.innerHTML = '';
1543
- list.forEach(s => {
1544
- const r = document.createElement('div'); r.className = 'series-row';
1545
- const showCond = s.cond && s.cond !== 'none';
1546
- r.innerHTML = `
1547
  <div class="series-row-top">
1548
- <input type="text" value="${s.formula}" class="s-form" placeholder="Formula (e.g. $P)" style="flex:1;">
1549
- <input type="text" value="${s.label}" class="s-lbl" placeholder="Label" style="width:100px;">
1550
- <select class="s-axis" style="width:60px;"><option value="left" ${s.axis==='left'?'selected':''}>Left</option><option value="right" ${s.axis==='right'?'selected':''}>Right</option></select>
1551
- <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1552
  </div>
1553
  <div class="series-row-bottom">
1554
- <input type="color" value="${s.color1}" class="color-input s-color1" title="Color">
1555
- <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1556
- <option value="none" ${s.cond==='none'||!s.cond?'selected':''}>Solid</option>
1557
- <option value=">" ${s.cond==='>'?'selected':''}>If &gt;</option>
1558
- <option value="<" ${s.cond==='<'?'selected':''}>If &lt;</option>
1559
  </select>
1560
- <div class="s-cond-group" style="display:${showCond?'flex':'none'}; flex-wrap: wrap;">
1561
- <input type="number" class="s-thresh" value="${s.thresh||0}" style="width:70px;">
1562
- <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">True:</span>
1563
- <input type="color" value="${s.color1||'#238636'}" class="color-input s-color-true" title="Color True">
1564
- <input type="text" class="s-label-true" value="${s.labelTrue||''}" placeholder="Label" style="width:80px;">
1565
- <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">False:</span>
1566
- <input type="color" value="${s.color2||'#da3633'}" class="color-input s-color-false" title="Color False">
1567
- <input type="text" class="s-label-false" value="${s.labelFalse||''}" placeholder="Label" style="width:80px;">
1568
  </div>
1569
  </div>
1570
  `;
1571
- c.appendChild(r);
1572
  });
1573
  },
1574
 
1575
  addSeriesRow() {
1576
- const r = document.createElement('div'); r.className = 'series-row';
1577
- r.innerHTML = `
 
 
1578
  <div class="series-row-top">
1579
- <input type="text" value="$P" class="s-form" placeholder="Formula (e.g. $P)" style="flex:1;">
1580
- <input type="text" value="New" class="s-lbl" placeholder="Label" style="width:100px;">
1581
- <select class="s-axis" style="width:60px;"><option value="left">Left</option><option value="right">Right</option></select>
1582
- <button class="tool-btn" style="color:var(--danger); width:32px; background:rgba(218,54,51,0.1);" onclick="this.parentElement.parentElement.remove()">✕</button>
1583
  </div>
1584
  <div class="series-row-bottom">
1585
- <input type="color" value="#2f81f7" class="color-input s-color1" title="Color">
1586
- <select class="s-cond" style="width:80px;" onchange="Config.toggleCond(this)">
1587
- <option value="none" selected>Solid</option>
1588
- <option value=">">If &gt;</option>
1589
- <option value="<">If &lt;</option>
1590
  </select>
1591
- <div class="s-cond-group" style="display:none; flex-wrap: wrap;">
1592
- <input type="number" class="s-thresh" value="0" style="width:70px;">
1593
- <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">True:</span>
1594
- <input type="color" value="#238636" class="color-input s-color-true" title="Color True">
1595
- <input type="text" class="s-label-true" value="" placeholder="Label" style="width:80px;">
1596
- <span style="color:var(--text-secondary); font-size:11px; font-weight:600;">False:</span>
1597
- <input type="color" value="#da3633" class="color-input s-color-false" title="Color False">
1598
- <input type="text" class="s-label-false" value="" placeholder="Label" style="width:80px;">
1599
  </div>
1600
  </div>
1601
  `;
1602
- document.getElementById('cfgSeries').appendChild(r);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  },
1604
 
1605
- extractSeriesFromDOM() {
1606
  const rows = document.querySelectorAll('#cfgSeries .series-row');
1607
- return Array.from(rows).map(r => {
1608
- const cond = r.querySelector('.s-cond').value;
1609
- return {
1610
- formula: r.querySelector('.s-form').value,
1611
- label: r.querySelector('.s-lbl').value,
1612
- axis: r.querySelector('.s-axis').value,
1613
- color1: r.querySelector('.s-color-true') ? r.querySelector('.s-color-true').value : r.querySelector('.s-color1').value,
1614
- cond: cond,
1615
- thresh: r.querySelector('.s-thresh') ? r.querySelector('.s-thresh').value : '0',
1616
- color2: r.querySelector('.s-color-false') ? r.querySelector('.s-color-false').value : '#ffffff',
1617
- labelTrue: r.querySelector('.s-label-true') ? r.querySelector('.s-label-true').value : '',
1618
- labelFalse: r.querySelector('.s-label-false') ? r.querySelector('.s-label-false').value : ''
1619
- };
1620
- });
1621
  },
1622
 
1623
  saveChartConfig() {
1624
- const series = this.extractSeriesFromDOM();
1625
-
1626
- if(this.isGlobal) {
1627
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1628
  App.save();
1629
- }
1630
- else {
1631
- const root = document.getElementById('cfgRoot').value.toUpperCase();
1632
- const exp = document.getElementById('cfgExp').value.toUpperCase();
1633
- const inst = document.getElementById('cfgInst').value.toUpperCase();
1634
-
1635
- if(!root || !exp) return alert("Incomplete Details. Please type or select valid options.");
1636
-
1637
- App.state.charts[this.tid].config = {
1638
- chartMode: this.currentMode,
1639
- atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1640
- root, expiry: exp, instrument: inst,
1641
- timeframe: document.getElementById('cfgTF').value,
1642
- start: document.getElementById('cfgStart').value,
1643
- end: document.getElementById('cfgEnd').value,
1644
- series
1645
- };
1646
- App.save();
1647
- App.renderGrid();
1648
- ChartLogic.plot(this.tid);
1649
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1650
  this.close();
 
1651
  }
1652
  };
1653
 
1654
- // Helper inside JS for string replacement in formulas
1655
- function str(val) {
1656
- return val !== null && val !== undefined ? val.toString() : "0";
1657
- }
 
 
 
 
 
 
 
 
 
 
 
1658
 
1659
  window.onload = () => App.init();
1660
  </script>
@@ -1663,4 +1572,4 @@ window.onload = () => App.init();
1663
  """
1664
 
1665
  if __name__ == "__main__":
1666
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
239
 
240
  # =========================================================
241
  # CHUNKING LOGIC: Bypass Supabase DB/Timeout Limits
242
+ # ADVANCED MODE: Use 30 min chunks (faster for heavy query)
243
+ # NORMAL MODE: Use 2 hour chunks
244
  # =========================================================
245
  async def fetch_chunk(start_dt, end_dt):
246
  params = base_params.copy()
 
252
 
253
  tasks = []
254
  curr = t_start_dt
255
+
256
+ # CRITICAL FIX: Smaller chunks for advanced mode to avoid RPC timeout
257
+ chunk_hours = 0.5 if mode == "advanced" else 2 # 30 min for advanced, 2 hours for normal
258
 
259
  while curr <= t_end_dt:
260
+ if mode == "advanced":
261
+ # 30 minute chunks for advanced mode
262
+ nxt = curr + timedelta(minutes=30) - timedelta(seconds=1)
263
+ else:
264
+ # 2 hour chunks for normal mode
265
+ nxt = curr + timedelta(hours=chunk_hours) - timedelta(seconds=1)
266
+
267
  if nxt > t_end_dt:
268
  nxt = t_end_dt
269
 
270
  tasks.append(fetch_chunk(curr, nxt))
271
+
272
+ if mode == "advanced":
273
+ curr += timedelta(minutes=30)
274
+ else:
275
+ curr += timedelta(hours=chunk_hours)
276
 
277
  # Execute all chunks in parallel
278
  chunk_results = await asyncio.gather(*tasks)
 
1021
  <span>${isCfg ? cfg.root : 'New Chart'}</span>
1022
  ${isCfg ? `${advancedLabel} <span class="card-meta">${cfg.expiry}</span>` : ''}
1023
  </div>
1024
+ <div class="card-tools">
1025
+ <button class="tool-btn info-btn" id="info-btn-${id}" onclick="ChartLogic.showBreakdown('${id}')" style="display:none;">i</button>
1026
  <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
1027
  <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
1028
  </div>
 
1083
  }
1084
  };
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  const ChartLogic = {
1087
  async plot(id, isUpdate=false) {
1088
  const chart = App.state.charts[id];
 
1121
  if(dot) dot.classList.remove('retry');
1122
  return;
1123
  }
1124
+
1125
+ // Store data for breakdown modal
1126
+ chart.chartData = data;
1127
 
1128
+ // Show info button if we have breakdown data
1129
+ const infoBtn = document.getElementById(`info-btn-${id}`);
1130
+ if(infoBtn && data.breakdown && data.breakdown.length > 0) {
1131
+ infoBtn.style.display = 'flex';
1132
+ }
1133
 
1134
  const body = document.getElementById(`body-${id}`);
1135
  if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1136
 
1137
  if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
1138
 
 
 
 
 
1139
  const datasets = cfg.series.map(s => {
1140
  const vals = data.labels.map((_, i) => {
1141
+ const P=data.P[i]||0, V=data.V[i]||0, OI=data.OI[i]||0, B=data.B[i]||0, S=data.S[i]||0;
1142
+ const CB=data.CB[i]||0, CS=data.CS[i]||0, PB=data.PB[i]||0, PS=data.PS[i]||0;
 
 
 
 
 
 
 
1143
 
1144
+ // CRITICAL FIX: Use String() not str() - JavaScript doesn't have str()
1145
  let f = s.formula
1146
+ .replace(/\$CB/g, String(CB)).replace(/\$CS/g, String(CS))
1147
+ .replace(/\$PB/g, String(PB)).replace(/\$PS/g, String(PS))
1148
+ .replace(/\$OI/g, String(OI)).replace(/\$P/g, String(P))
1149
+ .replace(/\$V/g, String(V)).replace(/\$B/g, String(B)).replace(/\$S/g, String(S));
1150
 
1151
  try { return new Function('return '+f)(); } catch{ return null; }
1152
  });
1153
 
1154
  let dataset = {
1155
+ label: s.label, data: vals,
1156
+ borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
 
 
 
 
 
 
 
1157
  };
1158
 
1159
  if(s.cond && s.cond !== 'none') {
 
1171
  };
1172
  dataset.borderColor = cTrue;
1173
  dataset.backgroundColor = cTrue;
 
 
 
 
1174
 
1175
  const hoverColorCallback = (ctx) => {
1176
  let v = ctx.raw;
 
1185
  } else {
1186
  dataset.borderColor = s.color1;
1187
  dataset.backgroundColor = s.color1;
 
 
1188
  }
 
1189
  return dataset;
1190
  });
1191
 
1192
+ const ctx = document.getElementById(`canvas-${id}`).getContext('2d');
1193
+
1194
+ if(chart.instance) chart.instance.destroy();
1195
+
1196
+ chart.instance = new Chart(ctx, {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1197
  type: 'line',
1198
+ data: { labels: data.labels, datasets: datasets },
1199
  options: {
1200
+ responsive: true,
1201
+ maintainAspectRatio: false,
1202
  interaction: { mode: 'index', intersect: false },
1203
+ plugins: {
1204
+ legend: {
1205
+ labels: {
1206
+ color: '#e6edf3',
1207
+ font: { size: 11, family: 'Inter' },
1208
+ boxWidth: 12,
1209
+ usePointStyle: true
1210
+ },
1211
+ position: 'top'
1212
+ },
1213
+ tooltip: {
1214
+ backgroundColor: 'rgba(13, 17, 23, 0.95)',
1215
+ titleColor: '#e6edf3',
1216
+ bodyColor: '#8b949e',
1217
+ borderColor: '#30363d',
1218
  borderWidth: 1,
1219
+ padding: 10,
1220
+ titleFont: { family: 'JetBrains Mono', size: 12 },
1221
+ bodyFont: { family: 'Inter', size: 11 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
  }
1223
  },
1224
  scales: {
1225
+ x: {
1226
+ grid: { color: '#21262d', drawBorder: false },
1227
+ ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 }, maxTicksLimit: 8 }
1228
+ },
1229
+ y: {
1230
+ type: 'linear',
1231
+ display: true,
1232
+ position: 'left',
1233
+ grid: { color: '#21262d', drawBorder: false },
1234
+ ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 } }
1235
+ },
1236
+ y1: {
1237
+ type: 'linear',
1238
+ display: true,
1239
+ position: 'right',
1240
+ grid: { drawOnChartArea: false },
1241
+ ticks: { color: '#8b949e', font: { family: 'JetBrains Mono', size: 10 } }
1242
+ }
1243
  }
1244
  }
1245
  });
1246
+ } catch(e) {
1247
+ console.error(e);
1248
+ if(dot) dot.classList.remove('retry');
1249
  }
1250
+ },
 
1251
 
1252
+ showBreakdown(id) {
1253
+ const chart = App.state.charts[id];
1254
+ if(!chart || !chart.chartData || !chart.chartData.breakdown) return;
 
 
 
 
 
1255
 
1256
+ const data = chart.chartData;
1257
+ const cfg = chart.config;
 
 
 
 
1258
 
1259
+ // Get the first series formula for display
1260
+ const formula = cfg.series && cfg.series[0] ? cfg.series[0].formula : 'N/A';
1261
+ document.getElementById('infoFormula').innerText = formula;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1262
 
1263
+ const tbody = document.getElementById('infoTableBody');
1264
+ tbody.innerHTML = '';
 
 
 
 
 
1265
 
1266
+ data.breakdown.forEach((item, idx) => {
1267
+ const bd = item.breakdown;
1268
+ if(!bd || !Array.isArray(bd)) return;
1269
+
1270
+ // Calculate aggregates
1271
+ let cbTotal = 0, csTotal = 0, pbTotal = 0, psTotal = 0;
1272
+ bd.forEach(strike => {
1273
+ cbTotal += strike.b || 0;
1274
+ csTotal += strike.s || 0;
1275
+ });
1276
+
1277
+ // For display, we'll show the sums
1278
+ const cbPlusPs = cbTotal + psTotal;
1279
+ const pbPlusCs = pbTotal + csTotal;
1280
+ const netFlow = cbPlusPs - pbPlusCs;
1281
+
1282
+ const row = document.createElement('tr');
1283
+ row.innerHTML = `
1284
+ <td>${item.time}</td>
1285
+ <td>${cbTotal.toFixed(0)}</td>
1286
+ <td>${csTotal.toFixed(0)}</td>
1287
+ <td>${pbTotal.toFixed(0)}</td>
1288
+ <td>${psTotal.toFixed(0)}</td>
1289
+ <td>${cbPlusPs.toFixed(0)}</td>
1290
+ <td>${pbPlusCs.toFixed(0)}</td>
1291
+ <td class="${netFlow >= 0 ? 'positive' : 'negative'}">${netFlow.toFixed(0)}</td>
1292
+ `;
1293
+ tbody.appendChild(row);
1294
+ });
1295
+
1296
+ document.getElementById('infoModal').style.display = 'flex';
1297
+ }
1298
+ };
1299
 
1300
+ const Config = {
1301
+ editingId: null,
1302
+ isGlobal: false,
1303
+ currentMode: 'normal',
 
 
 
 
 
 
 
 
1304
 
1305
+ open(id) {
1306
+ this.editingId = id;
1307
+ this.isGlobal = false;
1308
+ const cfg = App.state.charts[id].config;
1309
+
1310
  document.getElementById('cfgTitle').innerText = 'Configure Chart';
 
 
 
1311
  document.getElementById('cfgGlobalOnly').style.display = 'none';
1312
  document.getElementById('cfgChartOnly').style.display = 'block';
1313
+ document.getElementById('configModal').style.display = 'flex';
1314
+
1315
+ // Reset fields
1316
+ document.getElementById('cfgRoot').value = cfg.root || '';
1317
+ document.getElementById('cfgExp').value = cfg.expiry || '';
1318
+ document.getElementById('cfgInst').value = cfg.instrument || '';
1319
+ document.getElementById('cfgTF').value = cfg.timeframe || '5min';
 
 
 
1320
  document.getElementById('cfgStart').value = cfg.start || '09:15';
1321
  document.getElementById('cfgEnd').value = cfg.end || '15:30';
1322
+ document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
1323
 
1324
+ // Set mode
1325
  this.currentMode = cfg.chartMode || 'normal';
1326
+ this.updateModeUI();
1327
 
1328
+ // Load series
1329
+ this.renderSeries(cfg.series || []);
 
1330
 
1331
+ // Load presets dropdown
1332
+ this.loadPresetsDropdown();
1333
+
1334
+ // Load instruments if root and expiry set
1335
+ if(cfg.root && cfg.expiry) {
1336
+ this.loadInstruments(cfg.root, cfg.expiry);
 
 
 
 
 
1337
  }
 
 
 
 
 
 
 
 
1338
  },
1339
 
1340
  openDefaults() {
1341
  this.isGlobal = true;
1342
+ this.editingId = null;
1343
+ document.getElementById('cfgTitle').innerText = 'Settings';
 
 
 
1344
  document.getElementById('cfgGlobalOnly').style.display = 'block';
1345
  document.getElementById('cfgChartOnly').style.display = 'none';
1346
+ document.getElementById('cfgRefresh').value = App.state.refreshRate;
1347
+ document.getElementById('configModal').style.display = 'flex';
1348
+ },
1349
 
1350
+ close() {
1351
+ document.getElementById('configModal').style.display = 'none';
1352
  },
1353
 
1354
+ setMode(mode) {
1355
+ this.currentMode = mode;
1356
+ this.updateModeUI();
1357
+ },
1358
 
1359
+ updateModeUI() {
1360
+ const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
1361
+ btns.forEach(b => b.classList.remove('active'));
1362
+ btns[this.currentMode === 'normal' ? 0 : 1].classList.add('active');
1363
 
1364
+ const advFields = document.getElementById('cfgAdvancedFields');
1365
+ const instHint = document.getElementById('advInstHint');
1366
+ const lblInst = document.getElementById('lblInst');
1367
+
1368
+ if(this.currentMode === 'advanced') {
1369
+ advFields.style.display = 'block';
1370
+ instHint.style.display = 'block';
1371
+ lblInst.innerText = 'Reference Instrument (Spot/Future)';
1372
+ } else {
1373
+ advFields.style.display = 'none';
1374
+ instHint.style.display = 'none';
1375
+ lblInst.innerText = 'Instrument';
1376
+ }
1377
+ },
1378
 
1379
+ async onRootChange() {
1380
+ const root = document.getElementById('cfgRoot').value;
1381
+ if(!root) return;
1382
+
1383
+ // Load expiries
1384
  const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1385
  const exps = await res.json();
 
1386
 
1387
+ CustomDD.set('cfgExp', exps);
1388
+ CustomDD.render('cfgExp', '');
 
 
 
 
 
 
 
 
 
1389
  },
1390
 
1391
+ async onExpChange() {
1392
+ const root = document.getElementById('cfgRoot').value;
1393
+ const expiry = document.getElementById('cfgExp').value;
1394
+ if(!root || !expiry) return;
1395
 
1396
+ await this.loadInstruments(root, expiry);
1397
+ },
1398
 
1399
+ async loadInstruments(root, expiry) {
1400
+ const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${expiry}`);
1401
  const data = await res.json();
1402
 
1403
+ CustomDD.data.cfgInst = [];
1404
+ CustomDD.set('cfgInst', data.spot_fut, 'Spot/Futures');
1405
+ CustomDD.set('cfgInst', data.options, 'Options');
1406
+ CustomDD.render('cfgInst', '');
1407
  },
1408
 
1409
+ renderSeries(series) {
1410
+ const container = document.getElementById('cfgSeries');
1411
+ container.innerHTML = '';
1412
+
1413
+ series.forEach((s, idx) => {
1414
+ const row = document.createElement('div');
1415
+ row.className = 'series-row';
1416
+ row.innerHTML = `
 
 
 
1417
  <div class="series-row-top">
1418
+ <input type="text" class="s-form" placeholder="Formula (e.g. $CB + $PS)" value="${s.formula || ''}">
1419
+ <input type="text" class="s-lbl" placeholder="Label" value="${s.label || ''}" style="width:120px;">
1420
+ <input type="color" class="color-input" value="${s.color1 || '#2f81f7'}">
1421
+ <button class="tool-btn" onclick="Config.removeSeriesRow(this)" style="color:var(--danger)">✕</button>
1422
  </div>
1423
  <div class="series-row-bottom">
1424
+ <select class="s-axis">
1425
+ <option value="left" ${s.axis==='left'?'selected':''}>Left Axis</option>
1426
+ <option value="right" ${s.axis==='right'?'selected':''}>Right Axis</option>
 
 
1427
  </select>
1428
+ <div class="s-cond-group">
1429
+ <select class="s-cond">
1430
+ <option value="none" ${s.cond==='none'?'selected':''}>No Cond</option>
1431
+ <option value=">" ${s.cond==='>'?'selected':''}>></option>
1432
+ <option value="<" ${s.cond==='<'?'selected':''}><</option>
1433
+ </select>
1434
+ <input type="text" class="s-thresh" placeholder="Threshold" value="${s.thresh || '0'}" style="width:70px;">
1435
+ <input type="color" class="color-input" value="${s.color2 || '#ffffff'}" title="False Color">
1436
  </div>
1437
  </div>
1438
  `;
1439
+ container.appendChild(row);
1440
  });
1441
  },
1442
 
1443
  addSeriesRow() {
1444
+ const container = document.getElementById('cfgSeries');
1445
+ const row = document.createElement('div');
1446
+ row.className = 'series-row';
1447
+ row.innerHTML = `
1448
  <div class="series-row-top">
1449
+ <input type="text" class="s-form" placeholder="Formula (e.g. $CB + $PS)">
1450
+ <input type="text" class="s-lbl" placeholder="Label" style="width:120px;">
1451
+ <input type="color" class="color-input" value="#2f81f7">
1452
+ <button class="tool-btn" onclick="Config.removeSeriesRow(this)" style="color:var(--danger)">✕</button>
1453
  </div>
1454
  <div class="series-row-bottom">
1455
+ <select class="s-axis">
1456
+ <option value="left">Left Axis</option>
1457
+ <option value="right" selected>Right Axis</option>
 
 
1458
  </select>
1459
+ <div class="s-cond-group">
1460
+ <select class="s-cond">
1461
+ <option value="none">No Cond</option>
1462
+ <option value=">">></option>
1463
+ <option value="<"><</option>
1464
+ </select>
1465
+ <input type="text" class="s-thresh" placeholder="Threshold" value="0" style="width:70px;">
1466
+ <input type="color" class="color-input" value="#ffffff" title="False Color">
1467
  </div>
1468
  </div>
1469
  `;
1470
+ container.appendChild(row);
1471
+ },
1472
+
1473
+ removeSeriesRow(btn) {
1474
+ btn.closest('.series-row').remove();
1475
+ },
1476
+
1477
+ loadPresetsDropdown() {
1478
+ const sel = document.getElementById('cfgPresetsDropdown');
1479
+ sel.innerHTML = '<option value="">-- Select Preset --</option>' +
1480
+ Object.keys(App.presets).map(p => `<option value="${p}">${p}</option>`).join('');
1481
+ },
1482
+
1483
+ loadPreset() {
1484
+ const name = document.getElementById('cfgPresetsDropdown').value;
1485
+ if(!name || !App.presets[name]) return;
1486
+ this.renderSeries(App.presets[name]);
1487
+ },
1488
+
1489
+ savePreset() {
1490
+ const name = prompt('Enter preset name:');
1491
+ if(!name) return;
1492
+
1493
+ const series = this.getSeriesFromDOM();
1494
+ App.presets[name] = series;
1495
+
1496
+ // Save to localStorage
1497
+ localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1498
+ this.loadPresetsDropdown();
1499
+ alert('Preset saved!');
1500
+ },
1501
+
1502
+ deletePreset() {
1503
+ const name = document.getElementById('cfgPresetsDropdown').value;
1504
+ if(!name || !App.presets[name]) return;
1505
+ if(!confirm(`Delete preset "${name}"?`)) return;
1506
+
1507
+ delete App.presets[name];
1508
+ localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
1509
+ this.loadPresetsDropdown();
1510
  },
1511
 
1512
+ getSeriesFromDOM() {
1513
  const rows = document.querySelectorAll('#cfgSeries .series-row');
1514
+ return Array.from(rows).map(row => ({
1515
+ formula: row.querySelector('.s-form').value,
1516
+ label: row.querySelector('.s-lbl').value,
1517
+ color1: row.querySelector('.color-input').value,
1518
+ axis: row.querySelector('.s-axis').value,
1519
+ cond: row.querySelector('.s-cond').value,
1520
+ thresh: row.querySelector('.s-thresh').value,
1521
+ color2: row.querySelectorAll('.color-input')[1]?.value || '#ffffff'
1522
+ }));
 
 
 
 
 
1523
  },
1524
 
1525
  saveChartConfig() {
1526
+ if(this.isGlobal) {
 
 
1527
  App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1528
  App.save();
1529
+ this.close();
1530
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1531
  }
1532
+
1533
+ const cfg = {
1534
+ root: document.getElementById('cfgRoot').value.toUpperCase(),
1535
+ expiry: document.getElementById('cfgExp').value,
1536
+ instrument: document.getElementById('cfgInst').value,
1537
+ timeframe: document.getElementById('cfgTF').value,
1538
+ start: document.getElementById('cfgStart').value,
1539
+ end: document.getElementById('cfgEnd').value,
1540
+ chartMode: this.currentMode,
1541
+ atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
1542
+ series: this.getSeriesFromDOM()
1543
+ };
1544
+
1545
+ App.state.charts[this.editingId].config = cfg;
1546
+ App.save();
1547
  this.close();
1548
+ App.renderGrid();
1549
  }
1550
  };
1551
 
1552
+ // Info icon tooltip
1553
+ document.getElementById('infoIcon').addEventListener('click', () => {
1554
+ alert(`Formula Variables:
1555
+ $P = Price
1556
+ $V = Volume
1557
+ $OI = Open Interest
1558
+ $B = Buy Qty
1559
+ $S = Sell Qty
1560
+ $CB = Call Buy
1561
+ $CS = Call Sell
1562
+ $PB = Put Buy
1563
+ $PS = Put Sell
1564
+
1565
+ Example: $CB + $PS - ($PB + $CS)`);
1566
+ });
1567
 
1568
  window.onload = () => App.init();
1569
  </script>
 
1572
  """
1573
 
1574
  if __name__ == "__main__":
1575
+ uvicorn.run(app, host="0.0.0.0", port=7860)