Update app.py
Browse files
app.py
CHANGED
|
@@ -32,7 +32,7 @@ def sb_get(endpoint: str, params: dict = None):
|
|
| 32 |
return r.json()
|
| 33 |
except Exception as e:
|
| 34 |
print(f"DB Error GET ({endpoint}): {e}")
|
| 35 |
-
return[]
|
| 36 |
|
| 37 |
def sb_rpc(func_name: str, params: dict = None):
|
| 38 |
if params is None:
|
|
@@ -44,7 +44,7 @@ def sb_rpc(func_name: str, params: dict = None):
|
|
| 44 |
return r.json()
|
| 45 |
except Exception as e:
|
| 46 |
print(f"DB Error RPC ({func_name}): {e}")
|
| 47 |
-
return[]
|
| 48 |
|
| 49 |
# ==========================================
|
| 50 |
# 3. API ROUTES - METADATA
|
|
@@ -74,7 +74,7 @@ def get_auto_config(date: str, root: str):
|
|
| 74 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 75 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 76 |
|
| 77 |
-
valid_expiries =[e for e in all_expiries if e >= date]
|
| 78 |
if not valid_expiries:
|
| 79 |
valid_expiries = all_expiries
|
| 80 |
|
|
@@ -110,8 +110,8 @@ def get_instruments(date: str, root: str, expiry: str):
|
|
| 110 |
data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
|
| 111 |
instruments = [row['instrument_name'] for row in data]
|
| 112 |
|
| 113 |
-
spot_fut =[]
|
| 114 |
-
options =[]
|
| 115 |
for i in instruments:
|
| 116 |
if i.endswith('CE') or i.endswith('PE'):
|
| 117 |
options.append(i)
|
|
@@ -141,9 +141,11 @@ async def fetch_series(
|
|
| 141 |
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
|
| 142 |
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
|
| 143 |
|
|
|
|
| 144 |
if required_vars is None:
|
| 145 |
-
required_vars =["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
|
| 146 |
|
|
|
|
| 147 |
required_vars = list(set(required_vars) | {"P"})
|
| 148 |
|
| 149 |
if mode == "advanced":
|
|
@@ -151,7 +153,7 @@ async def fetch_series(
|
|
| 151 |
instruments = [r['instrument_name'] for r in inst_data]
|
| 152 |
|
| 153 |
if not instruments:
|
| 154 |
-
return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P":[]})
|
| 155 |
|
| 156 |
ref_instrument = instrument
|
| 157 |
if not ref_instrument:
|
|
@@ -162,19 +164,8 @@ async def fetch_series(
|
|
| 162 |
elif fut_keys:
|
| 163 |
ref_instrument = fut_keys[0]
|
| 164 |
else:
|
| 165 |
-
others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
| 166 |
ref_instrument = others[0] if others else instruments[0]
|
| 167 |
-
|
| 168 |
-
# Build Accurate Strike Map locally using Fyers nomenclature length rules
|
| 169 |
-
strike_map = {}
|
| 170 |
-
for inst in instruments:
|
| 171 |
-
if inst.endswith('CE') or inst.endswith('PE'):
|
| 172 |
-
try:
|
| 173 |
-
prefix = inst.split(':')[1] if ':' in inst else inst
|
| 174 |
-
s = prefix[len(root):-2]
|
| 175 |
-
if len(s) > 5: strike_map[inst] = float(s[5:])
|
| 176 |
-
except:
|
| 177 |
-
pass
|
| 178 |
|
| 179 |
rpc_name = "get_advanced_chart_data"
|
| 180 |
base_params = {
|
|
@@ -182,12 +173,12 @@ async def fetch_series(
|
|
| 182 |
"p_expiry": expiry,
|
| 183 |
"p_ref_instrument": ref_instrument,
|
| 184 |
"p_atm_range": atm_range,
|
| 185 |
-
"p_strike_map":
|
| 186 |
}
|
| 187 |
|
| 188 |
else:
|
| 189 |
if not instrument:
|
| 190 |
-
return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
|
| 191 |
|
| 192 |
rpc_name = "get_normal_chart_data"
|
| 193 |
base_params = {
|
|
@@ -197,10 +188,8 @@ async def fetch_series(
|
|
| 197 |
}
|
| 198 |
|
| 199 |
# =========================================================
|
| 200 |
-
# CHUNKING:
|
| 201 |
# =========================================================
|
| 202 |
-
chunk_delta = timedelta(hours=1, minutes=45) if mode == "advanced" else timedelta(hours=2)
|
| 203 |
-
|
| 204 |
async def fetch_chunk(start_dt, end_dt):
|
| 205 |
params = base_params.copy()
|
| 206 |
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -208,24 +197,24 @@ async def fetch_series(
|
|
| 208 |
loop = asyncio.get_event_loop()
|
| 209 |
return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
|
| 210 |
|
| 211 |
-
tasks =[]
|
| 212 |
curr = t_start_dt
|
| 213 |
while curr <= t_end_dt:
|
| 214 |
-
nxt = curr +
|
| 215 |
if nxt > t_end_dt:
|
| 216 |
nxt = t_end_dt
|
| 217 |
tasks.append(fetch_chunk(curr, nxt))
|
| 218 |
-
curr +=
|
| 219 |
|
| 220 |
chunk_results = await asyncio.gather(*tasks)
|
| 221 |
|
| 222 |
-
all_records =[]
|
| 223 |
for res in chunk_results:
|
| 224 |
if isinstance(res, list):
|
| 225 |
all_records.extend(res)
|
| 226 |
|
| 227 |
if not all_records:
|
| 228 |
-
return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
|
| 229 |
|
| 230 |
# =========================================================
|
| 231 |
# DATA PROCESSING
|
|
@@ -250,44 +239,114 @@ async def fetch_series(
|
|
| 250 |
df.set_index('ts', inplace=True)
|
| 251 |
df.sort_index(inplace=True)
|
| 252 |
|
|
|
|
| 253 |
tf_map = {
|
| 254 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 255 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
| 256 |
}
|
| 257 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 258 |
|
|
|
|
|
|
|
| 259 |
agg_dict = {
|
| 260 |
"P": "last",
|
| 261 |
-
"V": "
|
| 262 |
-
"OI": "last",
|
| 263 |
-
"B": "last",
|
| 264 |
-
"S": "last",
|
| 265 |
-
"CB": "last",
|
| 266 |
-
"CS": "last",
|
| 267 |
-
"PB": "last",
|
| 268 |
-
"PS": "last",
|
| 269 |
-
"details": "last"
|
| 270 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
|
| 273 |
resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
|
| 274 |
|
|
|
|
| 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 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
else:
|
| 284 |
-
|
|
|
|
| 285 |
|
| 286 |
return JSONResponse(response_data)
|
| 287 |
|
| 288 |
|
| 289 |
# ==========================================
|
| 290 |
-
# 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
# ==========================================
|
| 292 |
|
| 293 |
HTML_TEMPLATE = """
|
|
@@ -390,6 +449,7 @@ HTML_TEMPLATE = """
|
|
| 390 |
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
| 391 |
}
|
| 392 |
.modal.wide { width: 820px; }
|
|
|
|
| 393 |
.modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
|
| 394 |
.modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
|
| 395 |
.modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
|
|
@@ -464,9 +524,86 @@ HTML_TEMPLATE = """
|
|
| 464 |
.breakdown-table th:first-child { text-align: left; }
|
| 465 |
.breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
|
| 466 |
.breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
|
|
|
|
| 467 |
.val-pos { color: #3fb950; }
|
| 468 |
.val-neg { color: #f85149; }
|
| 469 |
.breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
</style>
|
| 471 |
</head>
|
| 472 |
<body>
|
|
@@ -478,7 +615,7 @@ HTML_TEMPLATE = """
|
|
| 478 |
<div style="position:relative;">
|
| 479 |
<select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
|
| 480 |
<option value="LIVE">LIVE</option>
|
| 481 |
-
<option value="
|
| 482 |
</select>
|
| 483 |
<div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
|
| 484 |
</div>
|
|
@@ -560,7 +697,7 @@ HTML_TEMPLATE = """
|
|
| 560 |
<label style="color:var(--accent);">ATM (+/-) Strike Range</label>
|
| 561 |
<input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
|
| 562 |
<div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
|
| 563 |
-
Aggregates all CE/PE within ATM ±
|
| 564 |
</div>
|
| 565 |
</div>
|
| 566 |
</div>
|
|
@@ -616,17 +753,26 @@ HTML_TEMPLATE = """
|
|
| 616 |
</div>
|
| 617 |
</div>
|
| 618 |
|
| 619 |
-
<!-- ===== BREAKDOWN MODAL ===== -->
|
| 620 |
<div class="modal-backdrop" id="breakdownModal">
|
| 621 |
-
<div class="modal wide">
|
| 622 |
<div class="modal-head">
|
| 623 |
<span id="bdTitle">Calculation Breakdown</span>
|
| 624 |
<button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
|
| 625 |
</div>
|
| 626 |
<div class="modal-body" style="padding:12px;">
|
| 627 |
<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
|
| 628 |
-
<div
|
| 629 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
</div>
|
| 631 |
</div>
|
| 632 |
</div>
|
|
@@ -655,14 +801,16 @@ function evalFormula(formula, vars) {
|
|
| 655 |
try { return new Function('return ' + f)(); } catch { return null; }
|
| 656 |
}
|
| 657 |
|
|
|
|
| 658 |
function extractRequiredVars(seriesList) {
|
| 659 |
-
const vars = new Set(['P']);
|
| 660 |
seriesList.forEach(s => {
|
| 661 |
const f = s.formula || '';
|
| 662 |
if (f.includes('$CB')) vars.add('CB');
|
| 663 |
if (f.includes('$CS')) vars.add('CS');
|
| 664 |
if (f.includes('$PB')) vars.add('PB');
|
| 665 |
if (f.includes('$PS')) vars.add('PS');
|
|
|
|
| 666 |
if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
|
| 667 |
if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
|
| 668 |
if (f.includes('$V')) vars.add('V');
|
|
@@ -675,15 +823,15 @@ function extractRequiredVars(seriesList) {
|
|
| 675 |
// CUSTOM DROPDOWN
|
| 676 |
// ===================================================
|
| 677 |
const CustomDD = {
|
| 678 |
-
data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
|
| 679 |
-
recent: { cfgRoot:[] },
|
| 680 |
|
| 681 |
init() {
|
| 682 |
const saved = localStorage.getItem('dc_recent_symbols');
|
| 683 |
if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
|
| 684 |
},
|
| 685 |
|
| 686 |
-
clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
|
| 687 |
|
| 688 |
set(id, items, groupName = null) {
|
| 689 |
if (!groupName)
|
|
@@ -756,47 +904,51 @@ const CustomDD = {
|
|
| 756 |
// APP STATE
|
| 757 |
// ===================================================
|
| 758 |
const App = {
|
| 759 |
-
state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
|
| 760 |
|
|
|
|
| 761 |
presets: {
|
| 762 |
-
|
|
|
|
| 763 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 764 |
{ formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 765 |
{ formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 766 |
],
|
| 767 |
-
"Net Flow (Fut/Eq)":[
|
| 768 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 769 |
{ formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
|
| 770 |
],
|
| 771 |
-
|
|
|
|
| 772 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 773 |
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 774 |
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 775 |
],
|
| 776 |
-
"Put Flow (Options)":[
|
| 777 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 778 |
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 779 |
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 780 |
],
|
| 781 |
-
"Aggregated Flow (Options)":[
|
| 782 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 783 |
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
|
| 784 |
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 785 |
],
|
| 786 |
-
"Net Flow (Options)":[
|
| 787 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 788 |
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
|
| 789 |
],
|
| 790 |
-
|
|
|
|
| 791 |
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 792 |
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
|
| 793 |
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 794 |
],
|
| 795 |
-
"Net Flow (Advanced)":[
|
| 796 |
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 797 |
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
|
| 798 |
],
|
| 799 |
-
"Components (Advanced)":[
|
| 800 |
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 801 |
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 802 |
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
|
|
@@ -835,7 +987,7 @@ const App = {
|
|
| 835 |
|
| 836 |
initDefault() {
|
| 837 |
this.state.columns = 3;
|
| 838 |
-
this.state.layout = [[], [],[]];
|
| 839 |
this.state.refreshRate = 15;
|
| 840 |
for (let i = 0; i < 3; i++) {
|
| 841 |
this.addChartInternal(i);
|
|
@@ -880,7 +1032,7 @@ const App = {
|
|
| 880 |
setLayout(n) {
|
| 881 |
const all = this.state.layout.flat();
|
| 882 |
this.state.columns = n;
|
| 883 |
-
this.state.layout = Array.from({ length: n }, () =>[]);
|
| 884 |
all.forEach((id, i) => this.state.layout[i % n].push(id));
|
| 885 |
this.renderGrid(); this.save();
|
| 886 |
},
|
|
@@ -999,10 +1151,8 @@ const ChartLogic = {
|
|
| 999 |
endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
| 1000 |
}
|
| 1001 |
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
requiredVars.push('details');
|
| 1005 |
-
}
|
| 1006 |
|
| 1007 |
try {
|
| 1008 |
const res = await fetch('/api/fetch_series', {
|
|
@@ -1032,13 +1182,14 @@ const ChartLogic = {
|
|
| 1032 |
return;
|
| 1033 |
}
|
| 1034 |
|
|
|
|
| 1035 |
chart.lastData = data;
|
| 1036 |
|
| 1037 |
const body = document.getElementById(`body-${id}`);
|
| 1038 |
if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
|
| 1039 |
if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
|
| 1040 |
|
| 1041 |
-
const datasets = (cfg.series ||[]).map(s => {
|
| 1042 |
const vals = data.labels.map((_, i) => {
|
| 1043 |
const vars = {
|
| 1044 |
P: safeVal(data, 'P', i),
|
|
@@ -1087,7 +1238,7 @@ const ChartLogic = {
|
|
| 1087 |
return ds;
|
| 1088 |
});
|
| 1089 |
|
| 1090 |
-
this.render(id, data.labels, datasets, cfg.series ||[]);
|
| 1091 |
|
| 1092 |
} catch(e) { console.error('ChartLogic.plot error:', e); }
|
| 1093 |
},
|
|
@@ -1175,9 +1326,22 @@ const ChartLogic = {
|
|
| 1175 |
};
|
| 1176 |
|
| 1177 |
// ===================================================
|
| 1178 |
-
// BREAKDOWN TABLE
|
| 1179 |
// ===================================================
|
| 1180 |
const Breakdown = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
open(id) {
|
| 1182 |
const chart = App.state.charts[id];
|
| 1183 |
if (!chart || !chart.lastData || !chart.config.series) {
|
|
@@ -1188,19 +1352,49 @@ const Breakdown = {
|
|
| 1188 |
const data = chart.lastData;
|
| 1189 |
const cfg = chart.config;
|
| 1190 |
const series = cfg.series;
|
| 1191 |
-
const hasDetails = data.details && data.details.some(d => d);
|
| 1192 |
|
| 1193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
const presentVars = allVarKeys.filter(k =>
|
| 1195 |
data[k] && data[k].some(v => v && v !== 0)
|
| 1196 |
);
|
| 1197 |
|
|
|
|
| 1198 |
let thead = '<thead><tr><th>Time</th>';
|
| 1199 |
-
|
|
|
|
|
|
|
|
|
|
| 1200 |
presentVars.forEach(v => { thead += `<th>${v}</th>`; });
|
| 1201 |
series.forEach(s => { thead += `<th>${s.label}</th>`; });
|
| 1202 |
thead += '</tr></thead>';
|
| 1203 |
|
|
|
|
| 1204 |
let tbody = '<tbody>';
|
| 1205 |
data.labels.forEach((lbl, i) => {
|
| 1206 |
const vars = {
|
|
@@ -1211,25 +1405,22 @@ const Breakdown = {
|
|
| 1211 |
OI: safeVal(data, 'OI', i)
|
| 1212 |
};
|
| 1213 |
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
} else {
|
| 1221 |
-
detailsBtn = `<td>-</td>`;
|
| 1222 |
-
}
|
| 1223 |
}
|
| 1224 |
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
presentVars.forEach(k => {
|
| 1228 |
const v = vars[k];
|
| 1229 |
const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
|
| 1230 |
tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
|
| 1231 |
});
|
| 1232 |
|
|
|
|
| 1233 |
series.forEach(s => {
|
| 1234 |
const result = evalFormula(s.formula, vars);
|
| 1235 |
const rv = result !== null ? result : 0;
|
|
@@ -1244,49 +1435,146 @@ const Breakdown = {
|
|
| 1244 |
}
|
| 1245 |
tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
|
| 1246 |
});
|
|
|
|
| 1247 |
tbody += '</tr>';
|
|
|
|
|
|
|
| 1248 |
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1254 |
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
const
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
</
|
| 1265 |
-
|
| 1266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1267 |
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
<
|
| 1273 |
-
|
| 1274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1275 |
}
|
| 1276 |
-
|
|
|
|
|
|
|
| 1277 |
});
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
document.getElementById('
|
| 1282 |
-
document.getElementById('bdTable').innerHTML = thead + tbody;
|
| 1283 |
-
document.getElementById('breakdownModal').style.display = 'flex';
|
| 1284 |
},
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
const
|
| 1288 |
-
|
| 1289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
}
|
| 1291 |
};
|
| 1292 |
|
|
@@ -1305,6 +1593,7 @@ const Config = {
|
|
| 1305 |
document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
|
| 1306 |
document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
|
| 1307 |
this.updatePresetsDropdown();
|
|
|
|
| 1308 |
const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
|
| 1309 |
this.loadPreset(defPreset);
|
| 1310 |
},
|
|
@@ -1333,7 +1622,7 @@ const Config = {
|
|
| 1333 |
|
| 1334 |
deletePreset() {
|
| 1335 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1336 |
-
const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
|
| 1337 |
'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
|
| 1338 |
'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
|
| 1339 |
if (locked.includes(name)) return alert('Cannot delete default models.');
|
|
@@ -1482,6 +1771,7 @@ const Config = {
|
|
| 1482 |
<input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
|
| 1483 |
</div>
|
| 1484 |
`;
|
|
|
|
| 1485 |
const solidColor = r.querySelector('.s-color1');
|
| 1486 |
const condTrueColor = r.querySelector('.s-color1-cond');
|
| 1487 |
solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
|
|
|
|
| 32 |
return r.json()
|
| 33 |
except Exception as e:
|
| 34 |
print(f"DB Error GET ({endpoint}): {e}")
|
| 35 |
+
return []
|
| 36 |
|
| 37 |
def sb_rpc(func_name: str, params: dict = None):
|
| 38 |
if params is None:
|
|
|
|
| 44 |
return r.json()
|
| 45 |
except Exception as e:
|
| 46 |
print(f"DB Error RPC ({func_name}): {e}")
|
| 47 |
+
return []
|
| 48 |
|
| 49 |
# ==========================================
|
| 50 |
# 3. API ROUTES - METADATA
|
|
|
|
| 74 |
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
|
| 75 |
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
|
| 76 |
|
| 77 |
+
valid_expiries = [e for e in all_expiries if e >= date]
|
| 78 |
if not valid_expiries:
|
| 79 |
valid_expiries = all_expiries
|
| 80 |
|
|
|
|
| 110 |
data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
|
| 111 |
instruments = [row['instrument_name'] for row in data]
|
| 112 |
|
| 113 |
+
spot_fut = []
|
| 114 |
+
options = []
|
| 115 |
for i in instruments:
|
| 116 |
if i.endswith('CE') or i.endswith('PE'):
|
| 117 |
options.append(i)
|
|
|
|
| 141 |
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
|
| 142 |
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
|
| 143 |
|
| 144 |
+
# Default: all variables if not specified
|
| 145 |
if required_vars is None:
|
| 146 |
+
required_vars = ["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
|
| 147 |
|
| 148 |
+
# Always include P (price)
|
| 149 |
required_vars = list(set(required_vars) | {"P"})
|
| 150 |
|
| 151 |
if mode == "advanced":
|
|
|
|
| 153 |
instruments = [r['instrument_name'] for r in inst_data]
|
| 154 |
|
| 155 |
if not instruments:
|
| 156 |
+
return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P": []})
|
| 157 |
|
| 158 |
ref_instrument = instrument
|
| 159 |
if not ref_instrument:
|
|
|
|
| 164 |
elif fut_keys:
|
| 165 |
ref_instrument = fut_keys[0]
|
| 166 |
else:
|
| 167 |
+
others = [k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
|
| 168 |
ref_instrument = others[0] if others else instruments[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
rpc_name = "get_advanced_chart_data"
|
| 171 |
base_params = {
|
|
|
|
| 173 |
"p_expiry": expiry,
|
| 174 |
"p_ref_instrument": ref_instrument,
|
| 175 |
"p_atm_range": atm_range,
|
| 176 |
+
"p_strike_map": {}
|
| 177 |
}
|
| 178 |
|
| 179 |
else:
|
| 180 |
if not instrument:
|
| 181 |
+
return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P": []})
|
| 182 |
|
| 183 |
rpc_name = "get_normal_chart_data"
|
| 184 |
base_params = {
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
# =========================================================
|
| 191 |
+
# CHUNKING: 2-hour chunks fetched in parallel
|
| 192 |
# =========================================================
|
|
|
|
|
|
|
| 193 |
async def fetch_chunk(start_dt, end_dt):
|
| 194 |
params = base_params.copy()
|
| 195 |
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
| 197 |
loop = asyncio.get_event_loop()
|
| 198 |
return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
|
| 199 |
|
| 200 |
+
tasks = []
|
| 201 |
curr = t_start_dt
|
| 202 |
while curr <= t_end_dt:
|
| 203 |
+
nxt = curr + timedelta(hours=2) - timedelta(seconds=1)
|
| 204 |
if nxt > t_end_dt:
|
| 205 |
nxt = t_end_dt
|
| 206 |
tasks.append(fetch_chunk(curr, nxt))
|
| 207 |
+
curr += timedelta(hours=2)
|
| 208 |
|
| 209 |
chunk_results = await asyncio.gather(*tasks)
|
| 210 |
|
| 211 |
+
all_records = []
|
| 212 |
for res in chunk_results:
|
| 213 |
if isinstance(res, list):
|
| 214 |
all_records.extend(res)
|
| 215 |
|
| 216 |
if not all_records:
|
| 217 |
+
return JSONResponse({"error": "No valid data found in range.", "labels": [], "P": []})
|
| 218 |
|
| 219 |
# =========================================================
|
| 220 |
# DATA PROCESSING
|
|
|
|
| 239 |
df.set_index('ts', inplace=True)
|
| 240 |
df.sort_index(inplace=True)
|
| 241 |
|
| 242 |
+
# Resample
|
| 243 |
tf_map = {
|
| 244 |
"1min": "1min", "3min": "3min", "5min": "5min",
|
| 245 |
"15min": "15min", "30min": "30min", "1hour": "1h"
|
| 246 |
}
|
| 247 |
panda_tf = tf_map.get(timeframe, "1min")
|
| 248 |
|
| 249 |
+
# KEY FIX: Use 'last' for quantity fields — these are cumulative running totals.
|
| 250 |
+
# The LAST value in any period is the most current/accurate snapshot.
|
| 251 |
agg_dict = {
|
| 252 |
"P": "last",
|
| 253 |
+
"V": "sum", # Volume: summing new volume per candle makes sense
|
| 254 |
+
"OI": "last", # OI: last value
|
| 255 |
+
"B": "last", # Buy qty cumulative total → last value
|
| 256 |
+
"S": "last", # Sell qty cumulative total → last value
|
| 257 |
+
"CB": "last", # Call Buy → last value
|
| 258 |
+
"CS": "last", # Call Sell → last value
|
| 259 |
+
"PB": "last", # Put Buy → last value
|
| 260 |
+
"PS": "last", # Put Sell → last value
|
|
|
|
| 261 |
}
|
| 262 |
+
|
| 263 |
+
# Include ATM strike and breakdown data if available (for advanced mode)
|
| 264 |
+
if "atm_strike" in df.columns:
|
| 265 |
+
agg_dict["atm_strike"] = "last"
|
| 266 |
+
if "included_strikes" in df.columns:
|
| 267 |
+
agg_dict["included_strikes"] = "last"
|
| 268 |
+
if "breakdown_json" in df.columns:
|
| 269 |
+
agg_dict["breakdown_json"] = "last"
|
| 270 |
|
| 271 |
existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
|
| 272 |
resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
|
| 273 |
|
| 274 |
+
# Build response: only include requested variables (reduces egress)
|
| 275 |
response_data = {
|
| 276 |
"labels": resampled.index.strftime('%H:%M').tolist()
|
| 277 |
}
|
| 278 |
|
| 279 |
+
for var_key in ["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
|
| 280 |
if var_key in required_vars and var_key in resampled.columns:
|
| 281 |
+
response_data[var_key] = [round(float(v), 2) for v in resampled[var_key].tolist()]
|
| 282 |
+
# Omit entirely if not requested — JS handles missing keys gracefully
|
| 283 |
+
|
| 284 |
+
# Include ATM strike and breakdown data for advanced mode
|
| 285 |
+
if "atm_strike" in resampled.columns:
|
| 286 |
+
response_data["atm_strike"] = [round(float(v), 2) if pd.notna(v) else None for v in resampled["atm_strike"].tolist()]
|
| 287 |
+
if "included_strikes" in resampled.columns:
|
| 288 |
+
response_data["included_strikes"] = resampled["included_strikes"].tolist()
|
| 289 |
+
if "breakdown_json" in resampled.columns:
|
| 290 |
+
# Convert JSONB records to list of dicts
|
| 291 |
+
breakdown_data = []
|
| 292 |
+
for bd in resampled["breakdown_json"].tolist():
|
| 293 |
+
if pd.notna(bd) and bd:
|
| 294 |
+
if isinstance(bd, str):
|
| 295 |
+
import json
|
| 296 |
+
try:
|
| 297 |
+
breakdown_data.append(json.loads(bd))
|
| 298 |
+
except:
|
| 299 |
+
breakdown_data.append([])
|
| 300 |
+
else:
|
| 301 |
+
breakdown_data.append(bd)
|
| 302 |
else:
|
| 303 |
+
breakdown_data.append([])
|
| 304 |
+
response_data["breakdown_json"] = breakdown_data
|
| 305 |
|
| 306 |
return JSONResponse(response_data)
|
| 307 |
|
| 308 |
|
| 309 |
# ==========================================
|
| 310 |
+
# 5. NEW API: STRIKE BREAKDOWN FOR SPECIFIC TIMESTAMP
|
| 311 |
+
# ==========================================
|
| 312 |
+
|
| 313 |
+
@app.post("/api/strike_breakdown")
|
| 314 |
+
async def get_strike_breakdown(
|
| 315 |
+
date: str = Body(...),
|
| 316 |
+
root: str = Body(...),
|
| 317 |
+
expiry: str = Body(...),
|
| 318 |
+
ref_instrument: str = Body(...),
|
| 319 |
+
atm_range: int = Body(5),
|
| 320 |
+
timestamp: str = Body(...)
|
| 321 |
+
):
|
| 322 |
+
"""
|
| 323 |
+
Get detailed strike breakdown for a specific timestamp.
|
| 324 |
+
Shows all strikes with their ITM/OTM status and individual values.
|
| 325 |
+
"""
|
| 326 |
+
root = root.upper()
|
| 327 |
+
|
| 328 |
+
# Parse timestamp
|
| 329 |
+
ts_dt = pd.to_datetime(f"{date} {timestamp}")
|
| 330 |
+
|
| 331 |
+
result = sb_rpc("get_strike_breakdown", {
|
| 332 |
+
"p_root": root,
|
| 333 |
+
"p_expiry": expiry,
|
| 334 |
+
"p_timestamp": ts_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
| 335 |
+
"p_ref_instrument": ref_instrument,
|
| 336 |
+
"p_atm_range": atm_range
|
| 337 |
+
})
|
| 338 |
+
|
| 339 |
+
if not result:
|
| 340 |
+
return JSONResponse({"error": "No breakdown data found", "strikes": []})
|
| 341 |
+
|
| 342 |
+
return JSONResponse({
|
| 343 |
+
"timestamp": timestamp,
|
| 344 |
+
"strikes": result
|
| 345 |
+
})
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# ==========================================
|
| 349 |
+
# 6. FRONTEND TEMPLATE
|
| 350 |
# ==========================================
|
| 351 |
|
| 352 |
HTML_TEMPLATE = """
|
|
|
|
| 449 |
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
| 450 |
}
|
| 451 |
.modal.wide { width: 820px; }
|
| 452 |
+
.modal.extra-wide { width: 95%; max-width: 1400px; }
|
| 453 |
.modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
|
| 454 |
.modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
|
| 455 |
.modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
|
|
|
|
| 524 |
.breakdown-table th:first-child { text-align: left; }
|
| 525 |
.breakdown-table td { padding: 5px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
|
| 526 |
.breakdown-table td:first-child { text-align: left; color: var(--text-secondary); }
|
| 527 |
+
.breakdown-table tr:hover td { background: var(--bg-panel); }
|
| 528 |
.val-pos { color: #3fb950; }
|
| 529 |
.val-neg { color: #f85149; }
|
| 530 |
.breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 55vh; }
|
| 531 |
+
|
| 532 |
+
/* STRIKE BREAKDOWN STYLES */
|
| 533 |
+
.strike-row {
|
| 534 |
+
display: flex; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--border);
|
| 535 |
+
align-items: center; font-family: var(--font-mono); font-size: 11px;
|
| 536 |
+
}
|
| 537 |
+
.strike-row:hover { background: var(--bg-panel); }
|
| 538 |
+
.strike-row.itm { background: rgba(47,129,247,0.05); }
|
| 539 |
+
.strike-row.otm { background: rgba(0,0,0,0); }
|
| 540 |
+
.strike-row.atm { background: rgba(35,134,54,0.1); border-left: 3px solid var(--success); }
|
| 541 |
+
.strike-row.not-included { opacity: 0.4; }
|
| 542 |
+
.strike-label {
|
| 543 |
+
min-width: 100px; display: flex; gap: 6px; align-items: center;
|
| 544 |
+
}
|
| 545 |
+
.strike-badge {
|
| 546 |
+
font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600;
|
| 547 |
+
}
|
| 548 |
+
.badge-itm { background: var(--accent); color: white; }
|
| 549 |
+
.badge-otm { background: var(--bg-panel); color: var(--text-secondary); border: 1px solid var(--border); }
|
| 550 |
+
.badge-atm { background: var(--success); color: white; }
|
| 551 |
+
.strike-values {
|
| 552 |
+
display: flex; gap: 16px; flex: 1; justify-content: flex-end;
|
| 553 |
+
}
|
| 554 |
+
.strike-value-group {
|
| 555 |
+
display: flex; flex-direction: column; min-width: 60px;
|
| 556 |
+
}
|
| 557 |
+
.strike-value-label { font-size: 9px; color: var(--text-secondary); }
|
| 558 |
+
.strike-value-num { font-weight: 600; }
|
| 559 |
+
|
| 560 |
+
/* MINUTE BREAKDOWN ACCORDION */
|
| 561 |
+
.minute-row {
|
| 562 |
+
border-bottom: 1px solid var(--border);
|
| 563 |
+
}
|
| 564 |
+
.minute-header {
|
| 565 |
+
display: flex; align-items: center; padding: 8px 12px; cursor: pointer;
|
| 566 |
+
background: var(--bg-card); transition: background 0.2s;
|
| 567 |
+
}
|
| 568 |
+
.minute-header:hover { background: var(--bg-panel); }
|
| 569 |
+
.minute-header.expanded { background: var(--bg-panel); border-bottom: 1px solid var(--border); }
|
| 570 |
+
.minute-time { font-weight: 600; min-width: 60px; }
|
| 571 |
+
.minute-atm { color: var(--accent); font-family: var(--font-mono); min-width: 100px; }
|
| 572 |
+
.minute-summary { flex: 1; display: flex; gap: 16px; font-family: var(--font-mono); font-size: 11px; }
|
| 573 |
+
.expand-icon {
|
| 574 |
+
width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
|
| 575 |
+
color: var(--text-secondary); transition: transform 0.2s;
|
| 576 |
+
}
|
| 577 |
+
.expand-icon.expanded { transform: rotate(90deg); }
|
| 578 |
+
.minute-detail {
|
| 579 |
+
display: none; padding: 12px; background: var(--bg-body);
|
| 580 |
+
border-bottom: 1px solid var(--border);
|
| 581 |
+
}
|
| 582 |
+
.minute-detail.visible { display: block; }
|
| 583 |
+
|
| 584 |
+
/* STRIKE DETAIL GRID */
|
| 585 |
+
.strike-detail-grid {
|
| 586 |
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 587 |
+
gap: 8px;
|
| 588 |
+
}
|
| 589 |
+
.strike-detail-card {
|
| 590 |
+
background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px;
|
| 591 |
+
padding: 10px; font-family: var(--font-mono); font-size: 11px;
|
| 592 |
+
}
|
| 593 |
+
.strike-detail-card.ce { border-left: 3px solid #238636; }
|
| 594 |
+
.strike-detail-card.pe { border-left: 3px solid #da3633; }
|
| 595 |
+
.strike-detail-card.itm { background: rgba(47,129,247,0.08); }
|
| 596 |
+
.strike-detail-card.otm { background: transparent; }
|
| 597 |
+
.strike-detail-card.atm { background: rgba(35,134,54,0.1); }
|
| 598 |
+
.strike-detail-header {
|
| 599 |
+
display: flex; justify-content: space-between; margin-bottom: 8px;
|
| 600 |
+
font-weight: 600;
|
| 601 |
+
}
|
| 602 |
+
.strike-detail-values {
|
| 603 |
+
display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
|
| 604 |
+
}
|
| 605 |
+
.detail-val { color: var(--text-secondary); }
|
| 606 |
+
.detail-num { text-align: right; }
|
| 607 |
</style>
|
| 608 |
</head>
|
| 609 |
<body>
|
|
|
|
| 615 |
<div style="position:relative;">
|
| 616 |
<select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
|
| 617 |
<option value="LIVE">LIVE</option>
|
| 618 |
+
<option value="HISTORY">HISTORY</option>
|
| 619 |
</select>
|
| 620 |
<div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
|
| 621 |
</div>
|
|
|
|
| 697 |
<label style="color:var(--accent);">ATM (+/-) Strike Range</label>
|
| 698 |
<input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
|
| 699 |
<div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
|
| 700 |
+
Aggregates all CE/PE within ATM ± N strikes. Total 2N strikes included.
|
| 701 |
</div>
|
| 702 |
</div>
|
| 703 |
</div>
|
|
|
|
| 753 |
</div>
|
| 754 |
</div>
|
| 755 |
|
| 756 |
+
<!-- ===== BREAKDOWN MODAL (ENHANCED) ===== -->
|
| 757 |
<div class="modal-backdrop" id="breakdownModal">
|
| 758 |
+
<div class="modal extra-wide">
|
| 759 |
<div class="modal-head">
|
| 760 |
<span id="bdTitle">Calculation Breakdown</span>
|
| 761 |
<button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
|
| 762 |
</div>
|
| 763 |
<div class="modal-body" style="padding:12px;">
|
| 764 |
<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
|
| 765 |
+
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
| 766 |
+
<button id="bdTabSummary" class="primary" style="font-size:11px;" onclick="Breakdown.showTab('summary')">Summary</button>
|
| 767 |
+
<button id="bdTabDetail" style="font-size:11px; background:var(--bg-panel);" onclick="Breakdown.showTab('detail')">Strike Detail</button>
|
| 768 |
+
</div>
|
| 769 |
+
<div id="bdSummaryTab">
|
| 770 |
+
<div class="breakdown-scroll">
|
| 771 |
+
<table class="breakdown-table" id="bdTable"></table>
|
| 772 |
+
</div>
|
| 773 |
+
</div>
|
| 774 |
+
<div id="bdDetailTab" style="display:none;">
|
| 775 |
+
<div class="breakdown-scroll" id="bdDetailContent"></div>
|
| 776 |
</div>
|
| 777 |
</div>
|
| 778 |
</div>
|
|
|
|
| 801 |
try { return new Function('return ' + f)(); } catch { return null; }
|
| 802 |
}
|
| 803 |
|
| 804 |
+
// Extract which variables are used in a list of series formulas
|
| 805 |
function extractRequiredVars(seriesList) {
|
| 806 |
+
const vars = new Set(['P']); // Always need price
|
| 807 |
seriesList.forEach(s => {
|
| 808 |
const f = s.formula || '';
|
| 809 |
if (f.includes('$CB')) vars.add('CB');
|
| 810 |
if (f.includes('$CS')) vars.add('CS');
|
| 811 |
if (f.includes('$PB')) vars.add('PB');
|
| 812 |
if (f.includes('$PS')) vars.add('PS');
|
| 813 |
+
// Check $B / $S but NOT $CB/$CS/$PB/$PS (already handled above)
|
| 814 |
if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
|
| 815 |
if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
|
| 816 |
if (f.includes('$V')) vars.add('V');
|
|
|
|
| 823 |
// CUSTOM DROPDOWN
|
| 824 |
// ===================================================
|
| 825 |
const CustomDD = {
|
| 826 |
+
data: { cfgRoot: [], cfgExp: [], cfgInst: [] },
|
| 827 |
+
recent: { cfgRoot: [] },
|
| 828 |
|
| 829 |
init() {
|
| 830 |
const saved = localStorage.getItem('dc_recent_symbols');
|
| 831 |
if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
|
| 832 |
},
|
| 833 |
|
| 834 |
+
clear(id) { this.data[id] = []; document.getElementById(id).value = ''; },
|
| 835 |
|
| 836 |
set(id, items, groupName = null) {
|
| 837 |
if (!groupName)
|
|
|
|
| 904 |
// APP STATE
|
| 905 |
// ===================================================
|
| 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' },
|
| 915 |
{ formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 916 |
],
|
| 917 |
+
"Net Flow (Fut/Eq)": [
|
| 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' },
|
| 925 |
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 926 |
],
|
| 927 |
+
"Put Flow (Options)": [
|
| 928 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 929 |
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 930 |
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 931 |
],
|
| 932 |
+
"Aggregated Flow (Options)": [
|
| 933 |
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 934 |
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
|
| 935 |
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 936 |
],
|
| 937 |
+
"Net Flow (Options)": [
|
| 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' },
|
| 945 |
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
|
| 946 |
],
|
| 947 |
+
"Net Flow (Advanced)": [
|
| 948 |
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 949 |
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
|
| 950 |
],
|
| 951 |
+
"Components (Advanced)": [
|
| 952 |
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
|
| 953 |
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
|
| 954 |
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
|
|
|
|
| 987 |
|
| 988 |
initDefault() {
|
| 989 |
this.state.columns = 3;
|
| 990 |
+
this.state.layout = [[], [], []];
|
| 991 |
this.state.refreshRate = 15;
|
| 992 |
for (let i = 0; i < 3; i++) {
|
| 993 |
this.addChartInternal(i);
|
|
|
|
| 1032 |
setLayout(n) {
|
| 1033 |
const all = this.state.layout.flat();
|
| 1034 |
this.state.columns = n;
|
| 1035 |
+
this.state.layout = Array.from({ length: n }, () => []);
|
| 1036 |
all.forEach((id, i) => this.state.layout[i % n].push(id));
|
| 1037 |
this.renderGrid(); this.save();
|
| 1038 |
},
|
|
|
|
| 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 {
|
| 1158 |
const res = await fetch('/api/fetch_series', {
|
|
|
|
| 1182 |
return;
|
| 1183 |
}
|
| 1184 |
|
| 1185 |
+
// Store data for breakdown table
|
| 1186 |
chart.lastData = data;
|
| 1187 |
|
| 1188 |
const body = document.getElementById(`body-${id}`);
|
| 1189 |
if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
|
| 1190 |
if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
|
| 1191 |
|
| 1192 |
+
const datasets = (cfg.series || []).map(s => {
|
| 1193 |
const vals = data.labels.map((_, i) => {
|
| 1194 |
const vars = {
|
| 1195 |
P: safeVal(data, 'P', i),
|
|
|
|
| 1238 |
return ds;
|
| 1239 |
});
|
| 1240 |
|
| 1241 |
+
this.render(id, data.labels, datasets, cfg.series || []);
|
| 1242 |
|
| 1243 |
} catch(e) { console.error('ChartLogic.plot error:', e); }
|
| 1244 |
},
|
|
|
|
| 1326 |
};
|
| 1327 |
|
| 1328 |
// ===================================================
|
| 1329 |
+
// BREAKDOWN TABLE (ENHANCED)
|
| 1330 |
// ===================================================
|
| 1331 |
const Breakdown = {
|
| 1332 |
+
currentTab: 'summary',
|
| 1333 |
+
expandedMinutes: new Set(),
|
| 1334 |
+
|
| 1335 |
+
showTab(tab) {
|
| 1336 |
+
this.currentTab = tab;
|
| 1337 |
+
document.getElementById('bdTabSummary').className = tab === 'summary' ? 'primary' : '';
|
| 1338 |
+
document.getElementById('bdTabSummary').style.background = tab === 'summary' ? 'var(--accent)' : 'var(--bg-panel)';
|
| 1339 |
+
document.getElementById('bdTabDetail').className = tab === 'detail' ? 'primary' : '';
|
| 1340 |
+
document.getElementById('bdTabDetail').style.background = tab === 'detail' ? 'var(--accent)' : 'var(--bg-panel)';
|
| 1341 |
+
document.getElementById('bdSummaryTab').style.display = tab === 'summary' ? 'block' : 'none';
|
| 1342 |
+
document.getElementById('bdDetailTab').style.display = tab === 'detail' ? 'block' : 'none';
|
| 1343 |
+
},
|
| 1344 |
+
|
| 1345 |
open(id) {
|
| 1346 |
const chart = App.state.charts[id];
|
| 1347 |
if (!chart || !chart.lastData || !chart.config.series) {
|
|
|
|
| 1352 |
const data = chart.lastData;
|
| 1353 |
const cfg = chart.config;
|
| 1354 |
const series = cfg.series;
|
|
|
|
| 1355 |
|
| 1356 |
+
// Build summary table
|
| 1357 |
+
this.buildSummaryTable(data, cfg, series);
|
| 1358 |
+
|
| 1359 |
+
// Build detail view if breakdown data available
|
| 1360 |
+
if (data.breakdown_json && data.breakdown_json.length > 0) {
|
| 1361 |
+
this.buildDetailView(data, cfg);
|
| 1362 |
+
} else {
|
| 1363 |
+
document.getElementById('bdDetailContent').innerHTML = `
|
| 1364 |
+
<div style="text-align:center; padding:40px; color:var(--text-secondary);">
|
| 1365 |
+
<div style="font-size:24px; opacity:0.5;">📊</div>
|
| 1366 |
+
<div style="margin-top:12px;">Strike breakdown data available only in Advanced Mode</div>
|
| 1367 |
+
<div style="font-size:11px; margin-top:8px;">Switch to Advanced Mode to see per-strike details</div>
|
| 1368 |
+
</div>
|
| 1369 |
+
`;
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${cfg.chartMode === 'advanced' ? 'ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
|
| 1373 |
+
document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
|
| 1374 |
+
document.getElementById('breakdownModal').style.display = 'flex';
|
| 1375 |
+
|
| 1376 |
+
// Default to summary tab
|
| 1377 |
+
this.showTab('summary');
|
| 1378 |
+
},
|
| 1379 |
+
|
| 1380 |
+
buildSummaryTable(data, cfg, series) {
|
| 1381 |
+
// Determine which raw variables to show in table
|
| 1382 |
+
const allVarKeys = ['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
|
| 1383 |
const presentVars = allVarKeys.filter(k =>
|
| 1384 |
data[k] && data[k].some(v => v && v !== 0)
|
| 1385 |
);
|
| 1386 |
|
| 1387 |
+
// Build header
|
| 1388 |
let thead = '<thead><tr><th>Time</th>';
|
| 1389 |
+
// Add ATM strike column if available
|
| 1390 |
+
if (data.atm_strike) {
|
| 1391 |
+
thead += '<th>ATM</th>';
|
| 1392 |
+
}
|
| 1393 |
presentVars.forEach(v => { thead += `<th>${v}</th>`; });
|
| 1394 |
series.forEach(s => { thead += `<th>${s.label}</th>`; });
|
| 1395 |
thead += '</tr></thead>';
|
| 1396 |
|
| 1397 |
+
// Build rows
|
| 1398 |
let tbody = '<tbody>';
|
| 1399 |
data.labels.forEach((lbl, i) => {
|
| 1400 |
const vars = {
|
|
|
|
| 1405 |
OI: safeVal(data, 'OI', i)
|
| 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;
|
|
|
|
| 1435 |
}
|
| 1436 |
tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
|
| 1437 |
});
|
| 1438 |
+
|
| 1439 |
tbody += '</tr>';
|
| 1440 |
+
});
|
| 1441 |
+
tbody += '</tbody>';
|
| 1442 |
|
| 1443 |
+
document.getElementById('bdTable').innerHTML = thead + tbody;
|
| 1444 |
+
},
|
| 1445 |
+
|
| 1446 |
+
buildDetailView(data, cfg) {
|
| 1447 |
+
const atmRange = cfg.atmRange || 5;
|
| 1448 |
+
|
| 1449 |
+
let html = '<div style="margin-bottom:12px; padding:12px; background:var(--bg-panel); border-radius:6px; border:1px solid var(--border);">';
|
| 1450 |
+
html += `<div style="font-size:12px; color:var(--text-secondary);">`;
|
| 1451 |
+
html += `<strong style="color:var(--accent);">ATM ± ${atmRange}</strong> = ${atmRange} ITM + ${atmRange} OTM strikes = <strong>${atmRange * 2}</strong> total strikes per side (CE & PE)`;
|
| 1452 |
+
html += `<br><span style="font-size:10px;">ITM for CALLS = strikes below ATM | ITM for PUTS = strikes above ATM</span>`;
|
| 1453 |
+
html += `</div></div>`;
|
| 1454 |
+
|
| 1455 |
+
html += '<div class="minute-list">';
|
| 1456 |
+
|
| 1457 |
+
data.labels.forEach((lbl, i) => {
|
| 1458 |
+
const breakdown = data.breakdown_json ? data.breakdown_json[i] : null;
|
| 1459 |
+
const atmStrike = data.atm_strike ? data.atm_strike[i] : null;
|
| 1460 |
+
const includedStrikes = data.included_strikes ? data.included_strikes[i] : '';
|
| 1461 |
+
|
| 1462 |
+
// Calculate aggregates for this minute
|
| 1463 |
+
const vars = {
|
| 1464 |
+
P: safeVal(data, 'P', i), B: safeVal(data, 'B', i),
|
| 1465 |
+
S: safeVal(data, 'S', i), CB: safeVal(data, 'CB', i),
|
| 1466 |
+
CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
|
| 1467 |
+
PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i),
|
| 1468 |
+
OI: safeVal(data, 'OI', i)
|
| 1469 |
+
};
|
| 1470 |
+
|
| 1471 |
+
const minuteId = `minute-${i}`;
|
| 1472 |
+
const isExpanded = this.expandedMinutes.has(minuteId);
|
| 1473 |
+
|
| 1474 |
+
html += `<div class="minute-row">`;
|
| 1475 |
+
html += `<div class="minute-header ${isExpanded ? 'expanded' : ''}" onclick="Breakdown.toggleMinute('${minuteId}')">`;
|
| 1476 |
+
html += `<div class="expand-icon ${isExpanded ? 'expanded' : ''}">▶</div>`;
|
| 1477 |
+
html += `<div class="minute-time">${lbl}</div>`;
|
| 1478 |
+
if (atmStrike) {
|
| 1479 |
+
html += `<div class="minute-atm">ATM: ${atmStrike.toFixed(0)}</div>`;
|
| 1480 |
+
}
|
| 1481 |
+
html += `<div class="minute-summary">`;
|
| 1482 |
+
html += `<span style="color:var(--success);">CB: ${vars.CB.toFixed(0)}</span>`;
|
| 1483 |
+
html += `<span style="color:var(--danger);">CS: ${vars.CS.toFixed(0)}</span>`;
|
| 1484 |
+
html += `<span style="color:var(--success);">PB: ${vars.PB.toFixed(0)}</span>`;
|
| 1485 |
+
html += `<span style="color:var(--danger);">PS: ${vars.PS.toFixed(0)}</span>`;
|
| 1486 |
+
html += `</div>`;
|
| 1487 |
+
html += `</div>`; // minute-header
|
| 1488 |
+
|
| 1489 |
+
html += `<div class="minute-detail ${isExpanded ? 'visible' : ''}" id="${minuteId}">`;
|
| 1490 |
+
|
| 1491 |
+
if (breakdown && breakdown.length > 0) {
|
| 1492 |
+
// Group by strike
|
| 1493 |
+
const strikesMap = {};
|
| 1494 |
+
breakdown.forEach(s => {
|
| 1495 |
+
if (!strikesMap[s.strike]) {
|
| 1496 |
+
strikesMap[s.strike] = { CE: null, PE: null };
|
| 1497 |
+
}
|
| 1498 |
+
strikesMap[s.strike][s.type] = s;
|
| 1499 |
+
});
|
| 1500 |
+
|
| 1501 |
+
const strikes = Object.keys(strikesMap).sort((a, b) => b - a); // Descending
|
| 1502 |
+
|
| 1503 |
+
html += `<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;">`;
|
| 1504 |
+
html += `Strikes included: ${includedStrikes || 'N/A'}`;
|
| 1505 |
+
html += `</div>`;
|
| 1506 |
+
|
| 1507 |
+
html += `<div class="strike-detail-grid">`;
|
| 1508 |
+
|
| 1509 |
+
strikes.forEach(strike => {
|
| 1510 |
+
const ceData = strikesMap[strike].CE;
|
| 1511 |
+
const peData = strikesMap[strike].PE;
|
| 1512 |
|
| 1513 |
+
if (ceData && ceData.included) {
|
| 1514 |
+
const cls = ceData.moneyness === 'ITM' ? 'itm' : 'otm';
|
| 1515 |
+
const atmClass = Math.abs(ceData.strike - atmStrike) < 1 ? 'atm' : '';
|
| 1516 |
+
html += `<div class="strike-detail-card ce ${cls} ${atmClass}">`;
|
| 1517 |
+
html += `<div class="strike-detail-header">`;
|
| 1518 |
+
html += `<span>${ceData.strike} CE</span>`;
|
| 1519 |
+
html += `<span class="strike-badge ${ceData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${ceData.moneyness}</span>`;
|
| 1520 |
+
html += `</div>`;
|
| 1521 |
+
html += `<div class="strike-detail-values">`;
|
| 1522 |
+
html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(ceData.b || 0).toFixed(0)}</span>`;
|
| 1523 |
+
html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(ceData.s || 0).toFixed(0)}</span>`;
|
| 1524 |
+
html += `<span class="detail-val">Vol:</span><span class="detail-num">${(ceData.v || 0).toFixed(0)}</span>`;
|
| 1525 |
+
html += `<span class="detail-val">Price:</span><span class="detail-num">${(ceData.p || 0).toFixed(2)}</span>`;
|
| 1526 |
+
html += `</div>`;
|
| 1527 |
+
html += `</div>`;
|
| 1528 |
+
}
|
| 1529 |
|
| 1530 |
+
if (peData && peData.included) {
|
| 1531 |
+
const cls = peData.moneyness === 'ITM' ? 'itm' : 'otm';
|
| 1532 |
+
const atmClass = Math.abs(peData.strike - atmStrike) < 1 ? 'atm' : '';
|
| 1533 |
+
html += `<div class="strike-detail-card pe ${cls} ${atmClass}">`;
|
| 1534 |
+
html += `<div class="strike-detail-header">`;
|
| 1535 |
+
html += `<span>${peData.strike} PE</span>`;
|
| 1536 |
+
html += `<span class="strike-badge ${peData.moneyness === 'ITM' ? 'badge-itm' : 'badge-otm'}">${peData.moneyness}</span>`;
|
| 1537 |
+
html += `</div>`;
|
| 1538 |
+
html += `<div class="strike-detail-values">`;
|
| 1539 |
+
html += `<span class="detail-val">Buy:</span><span class="detail-num val-pos">${(peData.b || 0).toFixed(0)}</span>`;
|
| 1540 |
+
html += `<span class="detail-val">Sell:</span><span class="detail-num val-neg">${(peData.s || 0).toFixed(0)}</span>`;
|
| 1541 |
+
html += `<span class="detail-val">Vol:</span><span class="detail-num">${(peData.v || 0).toFixed(0)}</span>`;
|
| 1542 |
+
html += `<span class="detail-val">Price:</span><span class="detail-num">${(peData.p || 0).toFixed(2)}</span>`;
|
| 1543 |
+
html += `</div>`;
|
| 1544 |
+
html += `</div>`;
|
| 1545 |
+
}
|
| 1546 |
+
});
|
| 1547 |
+
|
| 1548 |
+
html += `</div>`; // strike-detail-grid
|
| 1549 |
+
} else {
|
| 1550 |
+
html += `<div style="color:var(--text-secondary); text-align:center; padding:20px;">No strike breakdown data for this minute</div>`;
|
| 1551 |
}
|
| 1552 |
+
|
| 1553 |
+
html += `</div>`; // minute-detail
|
| 1554 |
+
html += `</div>`; // minute-row
|
| 1555 |
});
|
| 1556 |
+
|
| 1557 |
+
html += '</div>'; // minute-list
|
| 1558 |
+
|
| 1559 |
+
document.getElementById('bdDetailContent').innerHTML = html;
|
|
|
|
|
|
|
| 1560 |
},
|
| 1561 |
+
|
| 1562 |
+
toggleMinute(minuteId) {
|
| 1563 |
+
const detail = document.getElementById(minuteId);
|
| 1564 |
+
const header = detail.previousElementSibling;
|
| 1565 |
+
const icon = header.querySelector('.expand-icon');
|
| 1566 |
+
|
| 1567 |
+
if (this.expandedMinutes.has(minuteId)) {
|
| 1568 |
+
this.expandedMinutes.delete(minuteId);
|
| 1569 |
+
detail.classList.remove('visible');
|
| 1570 |
+
header.classList.remove('expanded');
|
| 1571 |
+
icon.classList.remove('expanded');
|
| 1572 |
+
} else {
|
| 1573 |
+
this.expandedMinutes.add(minuteId);
|
| 1574 |
+
detail.classList.add('visible');
|
| 1575 |
+
header.classList.add('expanded');
|
| 1576 |
+
icon.classList.add('expanded');
|
| 1577 |
+
}
|
| 1578 |
}
|
| 1579 |
};
|
| 1580 |
|
|
|
|
| 1593 |
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 |
},
|
|
|
|
| 1622 |
|
| 1623 |
deletePreset() {
|
| 1624 |
const name = document.getElementById('cfgPresetsDropdown').value;
|
| 1625 |
+
const locked = ['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
|
| 1626 |
'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
|
| 1627 |
'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
|
| 1628 |
if (locked.includes(name)) return alert('Cannot delete default models.');
|
|
|
|
| 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; });
|