Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -367,8 +367,11 @@ HTML_TEMPLATE = """
|
|
| 367 |
|
| 368 |
/* Aesthetics: Big Number, No BG, Small label below */
|
| 369 |
.aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; }
|
| 370 |
-
.aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; background: transparent; padding: 0; border-radius: 0; margin-top: 2px; }
|
| 371 |
|
|
|
|
|
|
|
|
|
|
| 372 |
/* Modals */
|
| 373 |
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
|
| 374 |
.modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
|
@@ -385,6 +388,12 @@ HTML_TEMPLATE = """
|
|
| 385 |
.modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; }
|
| 386 |
.modal-action.sec { background:#eee; color:#333; margin-top:10px; }
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
.preset-pill { display:inline-block; padding:4px 8px; background:#f0f0f0; border-radius:4px; font-size:10px; font-weight:700; margin-right:5px; cursor:pointer; color:#555; border:1px solid transparent; }
|
| 389 |
.preset-pill:hover { border-color:#aaa; background:#e0e0e0; }
|
| 390 |
|
|
@@ -439,6 +448,26 @@ HTML_TEMPLATE = """
|
|
| 439 |
</div>
|
| 440 |
</div>
|
| 441 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
<!-- CONFIG MODAL (Left/Right Columns) -->
|
| 443 |
<div class="modal" id="confModal">
|
| 444 |
<div class="modal-box">
|
|
@@ -584,6 +613,9 @@ HTML_TEMPLATE = """
|
|
| 584 |
let IS_POLLING = false;
|
| 585 |
let SEARCH_TIMER = null;
|
| 586 |
let ATM_STRIKE = 0;
|
|
|
|
|
|
|
|
|
|
| 587 |
|
| 588 |
// Default Configurations
|
| 589 |
const DEFAULTS = {
|
|
@@ -640,11 +672,13 @@ HTML_TEMPLATE = """
|
|
| 640 |
}
|
| 641 |
|
| 642 |
con.innerHTML = CONFIG.side.map((item, idx) => {
|
| 643 |
-
// Create grid boxes for ranges. Flex handles the 50/50, 33/33 automatically.
|
| 644 |
const boxes = item.ranges.map((r, rIdx) => `
|
| 645 |
<div class="aggr-item">
|
| 646 |
<span class="aggr-val" id="sv-${idx}-${rIdx}">0</span>
|
| 647 |
-
<span class="aggr-lbl">
|
|
|
|
|
|
|
|
|
|
| 648 |
</div>
|
| 649 |
`).join('');
|
| 650 |
|
|
@@ -823,6 +857,68 @@ HTML_TEMPLATE = """
|
|
| 823 |
return 0;
|
| 824 |
}
|
| 825 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
|
| 827 |
// ===================================
|
| 828 |
// APP LOGIC
|
|
@@ -1111,6 +1207,9 @@ HTML_TEMPLATE = """
|
|
| 1111 |
rowData[item.k].PV = d.v;
|
| 1112 |
}
|
| 1113 |
}
|
|
|
|
|
|
|
|
|
|
| 1114 |
|
| 1115 |
// --- RENDER CHAIN (Main Columns) ---
|
| 1116 |
let maxL = 0, maxR = 0;
|
|
@@ -1170,35 +1269,38 @@ HTML_TEMPLATE = """
|
|
| 1170 |
}
|
| 1171 |
}
|
| 1172 |
|
| 1173 |
-
// --- SIDEBAR DYNAMIC AGGREGATION ---
|
| 1174 |
-
if(
|
| 1175 |
const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
|
| 1176 |
-
const atmIdx = kList.indexOf(ATM_STRIKE);
|
| 1177 |
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
}
|
| 1195 |
-
|
| 1196 |
-
el.innerText = (total > 0 ? "+" : "") + fmt(total);
|
| 1197 |
-
el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)';
|
| 1198 |
}
|
| 1199 |
-
|
|
|
|
|
|
|
|
|
|
| 1200 |
});
|
| 1201 |
-
}
|
| 1202 |
}
|
| 1203 |
}
|
| 1204 |
|
|
|
|
| 367 |
|
| 368 |
/* Aesthetics: Big Number, No BG, Small label below */
|
| 369 |
.aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; }
|
| 370 |
+
.aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; background: transparent; padding: 0; border-radius: 0; margin-top: 2px; display:flex; align-items:center; gap:3px; }
|
| 371 |
|
| 372 |
+
.info-bubble { width:12px; height:12px; background:#eee; color:#666; border-radius:50%; font-size:8px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:'Inter'; font-weight:800; }
|
| 373 |
+
.info-bubble:hover { background:var(--accent); color:white; }
|
| 374 |
+
|
| 375 |
/* Modals */
|
| 376 |
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
|
| 377 |
.modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
|
|
|
| 388 |
.modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; }
|
| 389 |
.modal-action.sec { background:#eee; color:#333; margin-top:10px; }
|
| 390 |
|
| 391 |
+
/* INFO TABLE */
|
| 392 |
+
.info-table { width:100%; font-family:'JetBrains Mono'; font-size:10px; border-collapse: collapse; margin-top:10px; }
|
| 393 |
+
.info-table th { text-align:left; color:#888; border-bottom:1px solid #eee; padding:5px; }
|
| 394 |
+
.info-table td { padding:5px; border-bottom:1px solid #f9f9f9; color:#333; }
|
| 395 |
+
.info-row-res { background: #f0f7ff; font-weight:800; }
|
| 396 |
+
|
| 397 |
.preset-pill { display:inline-block; padding:4px 8px; background:#f0f0f0; border-radius:4px; font-size:10px; font-weight:700; margin-right:5px; cursor:pointer; color:#555; border:1px solid transparent; }
|
| 398 |
.preset-pill:hover { border-color:#aaa; background:#e0e0e0; }
|
| 399 |
|
|
|
|
| 448 |
</div>
|
| 449 |
</div>
|
| 450 |
|
| 451 |
+
<!-- INFO BREAKDOWN MODAL -->
|
| 452 |
+
<div class="modal" id="infoModal">
|
| 453 |
+
<div class="modal-box" style="width: 500px; max-width:95%;">
|
| 454 |
+
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
|
| 455 |
+
<span>CALCULATION BREAKDOWN</span>
|
| 456 |
+
<span style="cursor:pointer; color:#999;" onclick="closeModal('infoModal')">✕</span>
|
| 457 |
+
</div>
|
| 458 |
+
<div style="margin-top:15px; font-size:12px;">
|
| 459 |
+
<div><strong>LIVE SPOT:</strong> <span id="infoSpot" style="font-family:'JetBrains Mono'; color:var(--accent);">0.00</span></div>
|
| 460 |
+
<div style="margin-top:5px;"><strong>FORMULA:</strong> <span id="infoForm" style="font-family:'JetBrains Mono'; background:#f4f4f4; padding:2px 6px; border-radius:4px;"></span></div>
|
| 461 |
+
</div>
|
| 462 |
+
<div style="overflow-y:auto; max-height:400px; margin-top:10px;">
|
| 463 |
+
<table class="info-table" id="infoTable">
|
| 464 |
+
<!-- Dynamic Content -->
|
| 465 |
+
</table>
|
| 466 |
+
</div>
|
| 467 |
+
<div style="margin-top:10px; font-size:10px; color:#888; text-align:center;">* Uses Raw Integer Values for Accuracy</div>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
|
| 471 |
<!-- CONFIG MODAL (Left/Right Columns) -->
|
| 472 |
<div class="modal" id="confModal">
|
| 473 |
<div class="modal-box">
|
|
|
|
| 613 |
let IS_POLLING = false;
|
| 614 |
let SEARCH_TIMER = null;
|
| 615 |
let ATM_STRIKE = 0;
|
| 616 |
+
|
| 617 |
+
// DATA STORE FOR INFO MODAL
|
| 618 |
+
let LATEST_ROW_DATA = {};
|
| 619 |
|
| 620 |
// Default Configurations
|
| 621 |
const DEFAULTS = {
|
|
|
|
| 672 |
}
|
| 673 |
|
| 674 |
con.innerHTML = CONFIG.side.map((item, idx) => {
|
|
|
|
| 675 |
const boxes = item.ranges.map((r, rIdx) => `
|
| 676 |
<div class="aggr-item">
|
| 677 |
<span class="aggr-val" id="sv-${idx}-${rIdx}">0</span>
|
| 678 |
+
<span class="aggr-lbl">
|
| 679 |
+
ATM ±${r}
|
| 680 |
+
<span class="info-bubble" onclick="showInfo(${idx}, ${rIdx})">i</span>
|
| 681 |
+
</span>
|
| 682 |
</div>
|
| 683 |
`).join('');
|
| 684 |
|
|
|
|
| 857 |
return 0;
|
| 858 |
}
|
| 859 |
}
|
| 860 |
+
|
| 861 |
+
// ===================================
|
| 862 |
+
// INFO MODAL LOGIC
|
| 863 |
+
// ===================================
|
| 864 |
+
function showInfo(cardIdx, rangeIdx) {
|
| 865 |
+
if(!CONFIG.side[cardIdx]) return;
|
| 866 |
+
|
| 867 |
+
const cfg = CONFIG.side[cardIdx];
|
| 868 |
+
const range = cfg.ranges[rangeIdx];
|
| 869 |
+
const formula = cfg.formula;
|
| 870 |
+
|
| 871 |
+
document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
|
| 872 |
+
document.getElementById('infoForm').innerText = formula;
|
| 873 |
+
|
| 874 |
+
// RE-RUN SPLIT LOGIC
|
| 875 |
+
const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
|
| 876 |
+
const lowers = kList.filter(k => k < SPOT_PRICE);
|
| 877 |
+
const uppers = kList.filter(k => k >= SPOT_PRICE); // Exact match goes to upper/call side
|
| 878 |
+
|
| 879 |
+
const selLow = lowers.slice(-range);
|
| 880 |
+
const selHigh = uppers.slice(0, range);
|
| 881 |
+
|
| 882 |
+
const allSel = [...selLow, ...selHigh].sort((a,b)=>a-b);
|
| 883 |
+
let grandTotal = 0;
|
| 884 |
+
|
| 885 |
+
const table = document.getElementById('infoTable');
|
| 886 |
+
|
| 887 |
+
// Headers based on formula vars? No, just show standard vars
|
| 888 |
+
let hHTML = `<tr>
|
| 889 |
+
<th style="width:50px;">STRIKE</th>
|
| 890 |
+
<th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th>
|
| 891 |
+
</tr>`;
|
| 892 |
+
|
| 893 |
+
let rowsHTML = allSel.map(k => {
|
| 894 |
+
const d = LATEST_ROW_DATA[k] || {};
|
| 895 |
+
const val = evaluateFormula(formula, d);
|
| 896 |
+
grandTotal += val;
|
| 897 |
+
|
| 898 |
+
// Styling for spot split
|
| 899 |
+
const borderStyle = (k >= SPOT_PRICE && selLow.includes(k- (kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
|
| 900 |
+
|
| 901 |
+
return `
|
| 902 |
+
<tr style="${borderStyle}">
|
| 903 |
+
<td style="font-weight:bold;">${k}</td>
|
| 904 |
+
<td>${d.CS || 0}</td>
|
| 905 |
+
<td>${d.CB || 0}</td>
|
| 906 |
+
<td>${d.PS || 0}</td>
|
| 907 |
+
<td>${d.PB || 0}</td>
|
| 908 |
+
<td style="font-weight:bold; color:${val>=0?'var(--green)':'var(--red)'}">${val}</td>
|
| 909 |
+
</tr>`;
|
| 910 |
+
}).join('');
|
| 911 |
+
|
| 912 |
+
// Footer
|
| 913 |
+
rowsHTML += `<tr class="info-row-res">
|
| 914 |
+
<td colspan="5" style="text-align:right;">TOTAL</td>
|
| 915 |
+
<td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td>
|
| 916 |
+
</tr>`;
|
| 917 |
+
|
| 918 |
+
table.innerHTML = hHTML + rowsHTML;
|
| 919 |
+
|
| 920 |
+
document.getElementById('infoModal').style.display = 'flex';
|
| 921 |
+
}
|
| 922 |
|
| 923 |
// ===================================
|
| 924 |
// APP LOGIC
|
|
|
|
| 1207 |
rowData[item.k].PV = d.v;
|
| 1208 |
}
|
| 1209 |
}
|
| 1210 |
+
|
| 1211 |
+
// STORE GLOBAL FOR INFO MODAL
|
| 1212 |
+
LATEST_ROW_DATA = rowData;
|
| 1213 |
|
| 1214 |
// --- RENDER CHAIN (Main Columns) ---
|
| 1215 |
let maxL = 0, maxR = 0;
|
|
|
|
| 1269 |
}
|
| 1270 |
}
|
| 1271 |
|
| 1272 |
+
// --- SIDEBAR DYNAMIC AGGREGATION (SYMMETRIC OTM LOGIC) ---
|
| 1273 |
+
if(SPOT_PRICE > 0 && CONFIG.side && CONFIG.side.length > 0) {
|
| 1274 |
const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
|
|
|
|
| 1275 |
|
| 1276 |
+
// SPLIT LIST BASED ON EXACT SPOT PRICE
|
| 1277 |
+
const lowers = kList.filter(k => k < SPOT_PRICE);
|
| 1278 |
+
const uppers = kList.filter(k => k >= SPOT_PRICE); // Include exact match in upper/call bucket
|
| 1279 |
+
|
| 1280 |
+
CONFIG.side.forEach((item, idx) => {
|
| 1281 |
+
item.ranges.forEach((rng, rIdx) => {
|
| 1282 |
+
const el = document.getElementById(`sv-${idx}-${rIdx}`);
|
| 1283 |
+
if(el) {
|
| 1284 |
+
// Take 'rng' amount from bottom of lowers (Closest below spot)
|
| 1285 |
+
const selLow = lowers.slice(-rng);
|
| 1286 |
+
// Take 'rng' amount from top of uppers (Closest above spot)
|
| 1287 |
+
const selHigh = uppers.slice(0, rng);
|
| 1288 |
+
|
| 1289 |
+
// Combine logic
|
| 1290 |
+
const targetStrikes = [...selLow, ...selHigh];
|
| 1291 |
+
|
| 1292 |
+
let total = 0;
|
| 1293 |
+
for(const k of targetStrikes) {
|
| 1294 |
+
if(rowData[k]) {
|
| 1295 |
+
total += evaluateFormula(item.formula, rowData[k]);
|
| 1296 |
}
|
|
|
|
|
|
|
|
|
|
| 1297 |
}
|
| 1298 |
+
|
| 1299 |
+
el.innerText = (total > 0 ? "+" : "") + fmt(total);
|
| 1300 |
+
el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)';
|
| 1301 |
+
}
|
| 1302 |
});
|
| 1303 |
+
});
|
| 1304 |
}
|
| 1305 |
}
|
| 1306 |
|