Update app.py
Browse files
app.py
CHANGED
|
@@ -239,7 +239,8 @@ async def fetch_series(
|
|
| 239 |
|
| 240 |
# =========================================================
|
| 241 |
# CHUNKING LOGIC: Bypass Supabase DB/Timeout Limits
|
| 242 |
-
#
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 255 |
|
| 256 |
while curr <= t_end_dt:
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
if nxt > t_end_dt:
|
| 259 |
nxt = t_end_dt
|
| 260 |
|
| 261 |
tasks.append(fetch_chunk(curr, nxt))
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 1012 |
-
<button class="tool-btn info-btn" id="
|
| 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 |
-
//
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 1190 |
-
const
|
| 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,
|
| 1201 |
-
.replace(/\$PB/g,
|
| 1202 |
-
.replace(/\$OI/g,
|
| 1203 |
-
.replace(/\$V/g,
|
| 1204 |
|
| 1205 |
try { return new Function('return '+f)(); } catch{ return null; }
|
| 1206 |
});
|
| 1207 |
|
| 1208 |
let dataset = {
|
| 1209 |
-
label: s.label,
|
| 1210 |
-
|
| 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 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 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,
|
|
|
|
| 1285 |
interaction: { mode: 'index', intersect: false },
|
| 1286 |
-
plugins: {
|
| 1287 |
-
legend: {
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
borderWidth: 1,
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 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: {
|
| 1332 |
-
|
| 1333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
}
|
| 1335 |
}
|
| 1336 |
});
|
|
|
|
|
|
|
|
|
|
| 1337 |
}
|
| 1338 |
-
}
|
| 1339 |
-
};
|
| 1340 |
|
| 1341 |
-
|
| 1342 |
-
|
| 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 |
-
|
| 1351 |
-
|
| 1352 |
-
document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
|
| 1353 |
-
|
| 1354 |
-
this.updateInfoTooltip(mode);
|
| 1355 |
-
this.updatePresetsDropdown();
|
| 1356 |
|
| 1357 |
-
//
|
| 1358 |
-
|
| 1359 |
-
|
| 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 |
-
|
| 1384 |
-
|
| 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 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1411 |
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 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 |
-
|
| 1426 |
-
this.
|
| 1427 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 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 |
-
|
| 1449 |
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
btns[1].classList.toggle('active', this.currentMode === 'advanced');
|
| 1453 |
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 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 |
-
|
| 1479 |
-
document.getElementById('cfgTitle').innerText = '
|
| 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
|
|
|
|
|
|
|
| 1486 |
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
},
|
| 1490 |
|
| 1491 |
-
|
|
|
|
|
|
|
|
|
|
| 1492 |
|
| 1493 |
-
|
| 1494 |
-
const
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1508 |
-
|
| 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(
|
| 1521 |
-
const root = document.getElementById('cfgRoot').value
|
| 1522 |
-
const
|
| 1523 |
-
|
| 1524 |
|
| 1525 |
-
|
|
|
|
| 1526 |
|
| 1527 |
-
|
|
|
|
| 1528 |
const data = await res.json();
|
| 1529 |
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
},
|
| 1535 |
|
| 1536 |
-
|
| 1537 |
-
const
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
|
| 1543 |
-
|
| 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"
|
| 1549 |
-
<input type="text"
|
| 1550 |
-
<
|
| 1551 |
-
<button class="tool-btn" style="color:var(--danger)
|
| 1552 |
</div>
|
| 1553 |
<div class="series-row-bottom">
|
| 1554 |
-
<
|
| 1555 |
-
|
| 1556 |
-
<option value="
|
| 1557 |
-
<option value=">" ${s.cond==='>'?'selected':''}>If ></option>
|
| 1558 |
-
<option value="<" ${s.cond==='<'?'selected':''}>If <</option>
|
| 1559 |
</select>
|
| 1560 |
-
<div class="s-cond-group"
|
| 1561 |
-
<
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
<
|
| 1566 |
-
<input type="
|
| 1567 |
-
<input type="
|
| 1568 |
</div>
|
| 1569 |
</div>
|
| 1570 |
`;
|
| 1571 |
-
|
| 1572 |
});
|
| 1573 |
},
|
| 1574 |
|
| 1575 |
addSeriesRow() {
|
| 1576 |
-
const
|
| 1577 |
-
|
|
|
|
|
|
|
| 1578 |
<div class="series-row-top">
|
| 1579 |
-
<input type="text"
|
| 1580 |
-
<input type="text"
|
| 1581 |
-
<
|
| 1582 |
-
<button class="tool-btn" style="color:var(--danger)
|
| 1583 |
</div>
|
| 1584 |
<div class="series-row-bottom">
|
| 1585 |
-
<
|
| 1586 |
-
|
| 1587 |
-
<option value="
|
| 1588 |
-
<option value=">">If ></option>
|
| 1589 |
-
<option value="<">If <</option>
|
| 1590 |
</select>
|
| 1591 |
-
<div class="s-cond-group"
|
| 1592 |
-
<
|
| 1593 |
-
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
<
|
| 1597 |
-
<input type="
|
| 1598 |
-
<input type="
|
| 1599 |
</div>
|
| 1600 |
</div>
|
| 1601 |
`;
|
| 1602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1603 |
},
|
| 1604 |
|
| 1605 |
-
|
| 1606 |
const rows = document.querySelectorAll('#cfgSeries .series-row');
|
| 1607 |
-
return Array.from(rows).map(
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
|
| 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 |
-
|
| 1625 |
-
|
| 1626 |
-
if(this.isGlobal) {
|
| 1627 |
App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
|
| 1628 |
App.save();
|
| 1629 |
-
|
| 1630 |
-
|
| 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 |
-
//
|
| 1655 |
-
|
| 1656 |
-
|
| 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)
|