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