topsecrettraders commited on
Commit
100a1ce
·
verified ·
1 Parent(s): ca5eacb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -74
app.py CHANGED
@@ -7,6 +7,7 @@ import time
7
  import urllib.parse
8
  import os
9
  import threading
 
10
  from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query
11
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
12
  from typing import Optional, List, Dict
@@ -21,12 +22,19 @@ REDIRECT_URI = "https://topsecrettraders-depthchainindia.hf.space/callback"
21
 
22
  CACHE_FILE = "master_cache.json"
23
 
 
 
 
 
24
  app = FastAPI()
25
 
26
  # ==========================================
27
- # 2. ROBUST DATA LOADER (THREAD SAFE)
28
  # ==========================================
29
 
 
 
 
30
  MASTER_DB = {}
31
  SEARCH_INDEX = []
32
 
@@ -201,9 +209,33 @@ def startup_event():
201
  thread.start()
202
 
203
  # ==========================================
204
- # 3. ROUTES
205
  # ==========================================
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  @app.get("/")
208
  def home(access_token: Optional[str] = Cookie(None)):
209
  if not access_token:
@@ -279,6 +311,99 @@ def force_refresh(background_tasks: BackgroundTasks):
279
  background_tasks.add_task(update_master_db)
280
  return JSONResponse({"s": "ok"})
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  # ==========================================
283
  # 4. FRONTEND
284
  # ==========================================
@@ -306,6 +431,7 @@ HTML_TEMPLATE = """
306
  .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); }
307
  .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; color: var(--text-sub); }
308
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
 
309
 
310
  .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; }
311
  .icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
@@ -323,6 +449,14 @@ HTML_TEMPLATE = """
323
  .badge-BSE { background: var(--badge-bse); }
324
  .badge-MCX { background: var(--badge-mcx); }
325
 
 
 
 
 
 
 
 
 
326
  .grid-layout { display: flex; height: calc(100vh - 60px); }
327
 
328
  /* Chain Panel */
@@ -533,13 +667,24 @@ HTML_TEMPLATE = """
533
  <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY, GOLD)..." autocomplete="off">
534
  <div class="search-dropdown" id="searchRes"></div>
535
  </div>
536
-
537
- <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB">
538
- <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
 
539
  </button>
 
 
 
 
 
 
 
540
  </div>
541
 
542
  <div style="display:flex; gap:20px; align-items:center;">
 
 
 
543
  <div style="text-align:right;">
544
  <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
545
  <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
@@ -548,7 +693,7 @@ HTML_TEMPLATE = """
548
  <span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
549
  <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
550
  </button>
551
- <div class="live-dot" id="statusDot" title="Live Connection Status"></div>
552
  <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;">
553
  <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
554
  </button>
@@ -614,6 +759,10 @@ HTML_TEMPLATE = """
614
  let SEARCH_TIMER = null;
615
  let ATM_STRIKE = 0;
616
 
 
 
 
 
617
  // DATA STORE FOR INFO MODAL
618
  let LATEST_ROW_DATA = {};
619
 
@@ -628,12 +777,9 @@ HTML_TEMPLATE = """
628
  ]
629
  };
630
 
631
- // Load Config from LocalStorage with migration check
632
  let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS));
633
- if(!Array.isArray(CONFIG.side)) {
634
- // Migrate old format to new format
635
- CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side));
636
- }
637
 
638
  // Temp state for editing
639
  let EDITING_COL = null;
@@ -655,15 +801,147 @@ HTML_TEMPLATE = """
655
  };
656
 
657
  // ===================================
658
- // CONFIGURATION LOGIC
659
  // ===================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  function applyConfig() {
661
  document.getElementById('headL_txt').innerText = CONFIG.left.name;
662
  document.getElementById('headR_txt').innerText = CONFIG.right.name;
663
  renderSidebar();
664
  }
665
 
666
- // --- MAIN RENDERER FOR SIDEBAR ---
667
  function renderSidebar() {
668
  const con = document.getElementById('sideContainer');
669
  if(!CONFIG.side || CONFIG.side.length === 0) {
@@ -821,7 +1099,7 @@ HTML_TEMPLATE = """
821
  function saveAndReload() {
822
  localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
823
  applyConfig();
824
- pollData();
825
  }
826
 
827
  function closeModal(id) { document.getElementById(id).style.display = 'none'; }
@@ -830,12 +1108,8 @@ HTML_TEMPLATE = """
830
  // MATH ENGINE
831
  // ===================================
832
  function evaluateFormula(formula, vars) {
833
- // vars: { CS: number, CB: number, PS: number, PB: number, CO: number, PO: number, CLTP: number, PLTP: number, CV: number, PV: number }
834
- // ALL INPUTS ARE RAW NUMBERS (INTEGERS). NO 'K' or 'Cr' here.
835
  try {
836
  let exp = formula;
837
- // Replace vars with values.
838
- // We use regex with word boundary to ensure CS doesn't match inside CSS
839
  exp = exp.replace(/CS/g, vars.CS || 0);
840
  exp = exp.replace(/CB/g, vars.CB || 0);
841
  exp = exp.replace(/PS/g, vars.PS || 0);
@@ -848,19 +1122,13 @@ HTML_TEMPLATE = """
848
  exp = exp.replace(/CV/g, vars.CV || 0);
849
  exp = exp.replace(/PV/g, vars.PV || 0);
850
 
851
- // Safety check: only allow numbers and math ops
852
  if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
853
-
854
- // Evaluate
855
  return new Function('return ' + exp)();
856
  } catch(e) {
857
  return 0;
858
  }
859
  }
860
 
861
- // ===================================
862
- // INFO MODAL LOGIC
863
- // ===================================
864
  function showInfo(cardIdx, rangeIdx) {
865
  if(!CONFIG.side[cardIdx]) return;
866
 
@@ -871,10 +1139,9 @@ HTML_TEMPLATE = """
871
  document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
872
  document.getElementById('infoForm').innerText = formula;
873
 
874
- // RE-RUN SPLIT LOGIC
875
  const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
876
  const lowers = kList.filter(k => k < SPOT_PRICE);
877
- const uppers = kList.filter(k => k >= SPOT_PRICE); // Exact match goes to upper/call side
878
 
879
  const selLow = lowers.slice(-range);
880
  const selHigh = uppers.slice(0, range);
@@ -884,7 +1151,6 @@ HTML_TEMPLATE = """
884
 
885
  const table = document.getElementById('infoTable');
886
 
887
- // Headers based on formula vars? No, just show standard vars
888
  let hHTML = `<tr>
889
  <th style="width:50px;">STRIKE</th>
890
  <th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th>
@@ -895,7 +1161,6 @@ HTML_TEMPLATE = """
895
  const val = evaluateFormula(formula, d);
896
  grandTotal += val;
897
 
898
- // Styling for spot split
899
  const borderStyle = (k >= SPOT_PRICE && selLow.includes(k- (kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
900
 
901
  return `
@@ -909,14 +1174,12 @@ HTML_TEMPLATE = """
909
  </tr>`;
910
  }).join('');
911
 
912
- // Footer
913
  rowsHTML += `<tr class="info-row-res">
914
  <td colspan="5" style="text-align:right;">TOTAL</td>
915
  <td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td>
916
  </tr>`;
917
 
918
  table.innerHTML = hHTML + rowsHTML;
919
-
920
  document.getElementById('infoModal').style.display = 'flex';
921
  }
922
 
@@ -967,6 +1230,11 @@ HTML_TEMPLATE = """
967
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
968
  document.getElementById('loader').style.display = 'flex';
969
 
 
 
 
 
 
970
  try {
971
  const res = await fetch(`/api/chain?symbol=${root}`);
972
  if(!res.ok) throw new Error("Symbol not found");
@@ -975,7 +1243,7 @@ HTML_TEMPLATE = """
975
  CURRENT_ROOT = root;
976
  CHAIN_DATA = data.items;
977
  SPOT_SYM = data.spot;
978
- SPOT_PRICE = 0; // Reset price on new load
979
 
980
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
981
 
@@ -985,6 +1253,9 @@ HTML_TEMPLATE = """
985
 
986
  setupExpiries();
987
  document.getElementById('loader').style.display = 'none';
 
 
 
988
 
989
  } catch(e) {
990
  alert("Error: " + e.message);
@@ -997,13 +1268,16 @@ HTML_TEMPLATE = """
997
  const exps = [...new Set(opts.map(i => i.e))].sort((a,b) => a-b);
998
  const today = Math.floor(Date.now()/1000) - 86400;
999
 
1000
- const validExps = exps.filter(ts => ts > today);
 
 
 
1001
  if(validExps.length === 0) {
1002
  document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
1003
  return;
1004
  }
1005
 
1006
- ACTIVE_EXP = validExps[0];
1007
  updateExpText(ACTIVE_EXP);
1008
 
1009
  const grid = document.getElementById('expGrid');
@@ -1040,10 +1314,8 @@ HTML_TEMPLATE = """
1040
 
1041
  const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b);
1042
 
1043
- // DUAL COLUMN RENDER
1044
  body.innerHTML = strikes.map(k => `
1045
  <div class="row" id="row-${k}" data-k="${k}">
1046
- <!-- LEFT COLUMN -->
1047
  <div class="graph-col">
1048
  <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
1049
  <div class="visual-bar" id="barL-${k}"></div>
@@ -1051,7 +1323,6 @@ HTML_TEMPLATE = """
1051
  <span class="delta-txt" id="valL-${k}">-</span>
1052
  </div>
1053
  </div>
1054
- <!-- RIGHT COLUMN -->
1055
  <div class="graph-col">
1056
  <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
1057
  <div class="visual-bar" id="barR-${k}"></div>
@@ -1062,8 +1333,9 @@ HTML_TEMPLATE = """
1062
  </div>
1063
  `).join('');
1064
 
1065
- window.scrolled = false; // Reset scroll state
1066
- restartPoller();
 
1067
  }
1068
 
1069
  function restartPoller() {
@@ -1073,7 +1345,7 @@ HTML_TEMPLATE = """
1073
  }
1074
 
1075
  async function pollData() {
1076
- if(IS_POLLING || !CURRENT_ROOT) return;
1077
  IS_POLLING = true;
1078
 
1079
  try {
@@ -1081,7 +1353,6 @@ HTML_TEMPLATE = """
1081
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
1082
 
1083
  if(kList.length > 0) {
1084
- // LOGIC UPDATE: Select Next Strike just above live price
1085
  const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
1086
  let selectedK = kList.find(k => k >= refPrice);
1087
  if (selectedK === undefined) selectedK = kList[kList.length - 1];
@@ -1089,9 +1360,9 @@ HTML_TEMPLATE = """
1089
  ATM_STRIKE = selectedK;
1090
  const atmIdx = kList.indexOf(selectedK);
1091
 
 
1092
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
1093
  const atmRow = document.getElementById(`row-${selectedK}`);
1094
-
1095
  if(atmRow) {
1096
  atmRow.classList.add('atm');
1097
  if(!window.scrolled && SPOT_PRICE > 0) {
@@ -1100,7 +1371,7 @@ HTML_TEMPLATE = """
1100
  }
1101
  }
1102
 
1103
- // DETERMINE MAX RANGE NEEDED ACROSS ALL SIDEBAR CARDS
1104
  let maxRange = 10;
1105
  if(CONFIG.side && Array.isArray(CONFIG.side)) {
1106
  CONFIG.side.forEach(item => {
@@ -1111,12 +1382,11 @@ HTML_TEMPLATE = """
1111
  });
1112
  }
1113
 
1114
- // Visible Range (Fixed)
1115
  const start = Math.max(0, atmIdx - 20);
1116
  const end = Math.min(kList.length, atmIdx + 20);
1117
  const viewKs = kList.slice(start, end);
1118
 
1119
- // Side Calculation Range
1120
  const rangeStart = Math.max(0, atmIdx - maxRange);
1121
  const rangeEnd = Math.min(kList.length, atmIdx + maxRange + 1);
1122
  const calcKs = kList.slice(rangeStart, rangeEnd);
@@ -1153,12 +1423,13 @@ HTML_TEMPLATE = """
1153
  }
1154
 
1155
  function updateUI(data) {
 
1156
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
1157
  SPOT_PRICE = data[SPOT_SYM].ltp;
1158
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
1159
  }
1160
 
1161
- // Futures
1162
  const futHTML = FUT_SYMS.map(f => {
1163
  const d = data[f.s];
1164
  if(!d || typeof d.ltp !== 'number') return '';
@@ -1208,10 +1479,9 @@ HTML_TEMPLATE = """
1208
  }
1209
  }
1210
 
1211
- // STORE GLOBAL FOR INFO MODAL
1212
  LATEST_ROW_DATA = rowData;
1213
 
1214
- // --- RENDER CHAIN (Main Columns) ---
1215
  let maxL = 0, maxR = 0;
1216
  const processedRows = {};
1217
 
@@ -1228,7 +1498,6 @@ HTML_TEMPLATE = """
1228
  for(const k in processedRows) {
1229
  const r = processedRows[k];
1230
 
1231
- // 1. UPDATE LEFT COLUMN
1232
  const valL = document.getElementById(`valL-${k}`);
1233
  if(valL) {
1234
  valL.innerText = (r.left>0?"+":"") + fmt(r.left);
@@ -1239,16 +1508,10 @@ HTML_TEMPLATE = """
1239
  const pct = (Math.abs(r.left) / maxL) * 100;
1240
  barL.style.left = '0%';
1241
  barL.style.width = pct + '%';
1242
-
1243
- if(r.left >= 0) {
1244
- barL.style.background = 'var(--green-soft)';
1245
- } else {
1246
- barL.style.background = 'var(--red-soft)';
1247
- }
1248
  }
1249
  }
1250
 
1251
- // 2. UPDATE RIGHT COLUMN
1252
  const valR = document.getElementById(`valR-${k}`);
1253
  if(valR) {
1254
  valR.innerText = (r.right>0?"+":"") + fmt(r.right);
@@ -1259,43 +1522,41 @@ HTML_TEMPLATE = """
1259
  const pct = (Math.abs(r.right) / maxR) * 100;
1260
  barR.style.left = '0%';
1261
  barR.style.width = pct + '%';
1262
-
1263
- if(r.right >= 0) {
1264
- barR.style.background = 'var(--green-soft)';
1265
- } else {
1266
- barR.style.background = 'var(--red-soft)';
1267
- }
1268
  }
1269
  }
1270
  }
1271
 
1272
- // --- SIDEBAR DYNAMIC AGGREGATION (SYMMETRIC OTM LOGIC) ---
 
 
 
 
 
 
 
 
 
 
 
 
1273
  if(SPOT_PRICE > 0 && CONFIG.side && CONFIG.side.length > 0) {
1274
  const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
1275
-
1276
- // SPLIT LIST BASED ON EXACT SPOT PRICE
1277
  const lowers = kList.filter(k => k < SPOT_PRICE);
1278
- const uppers = kList.filter(k => k >= SPOT_PRICE); // Include exact match in upper/call bucket
1279
 
1280
  CONFIG.side.forEach((item, idx) => {
1281
  item.ranges.forEach((rng, rIdx) => {
1282
  const el = document.getElementById(`sv-${idx}-${rIdx}`);
1283
  if(el) {
1284
- // Take 'rng' amount from bottom of lowers (Closest below spot)
1285
  const selLow = lowers.slice(-rng);
1286
- // Take 'rng' amount from top of uppers (Closest above spot)
1287
  const selHigh = uppers.slice(0, rng);
1288
-
1289
- // Combine logic
1290
  const targetStrikes = [...selLow, ...selHigh];
1291
 
1292
  let total = 0;
1293
  for(const k of targetStrikes) {
1294
- if(rowData[k]) {
1295
- total += evaluateFormula(item.formula, rowData[k]);
1296
- }
1297
  }
1298
-
1299
  el.innerText = (total > 0 ? "+" : "") + fmt(total);
1300
  el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)';
1301
  }
@@ -1304,7 +1565,6 @@ HTML_TEMPLATE = """
1304
  }
1305
  }
1306
 
1307
- // Pure Display Formatting
1308
  function fmt(n) {
1309
  if(!n) return '0';
1310
  let abs = Math.abs(n);
 
7
  import urllib.parse
8
  import os
9
  import threading
10
+ from datetime import datetime
11
  from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query
12
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
13
  from typing import Optional, List, Dict
 
22
 
23
  CACHE_FILE = "master_cache.json"
24
 
25
+ # --- SUPABASE CONFIG (FROM v16.txt) ---
26
+ SUPABASE_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
27
+ SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
28
+
29
  app = FastAPI()
30
 
31
  # ==========================================
32
+ # 2. ROBUST DATA LOADER & SUPABASE
33
  # ==========================================
34
 
35
+ from supabase import create_client, Client
36
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
37
+
38
  MASTER_DB = {}
39
  SEARCH_INDEX = []
40
 
 
209
  thread.start()
210
 
211
  # ==========================================
212
+ # 3. ROUTES & HISTORICAL LOGIC
213
  # ==========================================
214
 
215
+ def decompress_payload(rich_data: List[Dict]) -> Dict:
216
+ """
217
+ Converts the v16.txt rich array format back to Fyers Data format.
218
+ Input: [ { "u": "NSE:...", "p": 100, "v": 50, "q": [10, 20], "o": [5, 0] }, ... ]
219
+ Output: { "NSE:...": { "ltp": 100, "v": 50, "totalbuyqty": 10, ... } }
220
+ """
221
+ fyers_data = {}
222
+ for item in rich_data:
223
+ sym = item.get('u')
224
+ if not sym: continue
225
+
226
+ # Mapping based on v16.txt logic
227
+ fyers_data[sym] = {
228
+ "ltp": item.get('p', 0),
229
+ "v": item.get('v', 0),
230
+ "totalbuyqty": item.get('q', [0,0])[0],
231
+ "totalsellqty": item.get('q', [0,0])[1],
232
+ "oi": item.get('o', [0,0])[0],
233
+ "oipercent": item.get('o', [0,0])[1],
234
+ # Helper for expiry if stored in payload (v16 handles this)
235
+ "expiry": item.get('e', None)
236
+ }
237
+ return fyers_data
238
+
239
  @app.get("/")
240
  def home(access_token: Optional[str] = Cookie(None)):
241
  if not access_token:
 
311
  background_tasks.add_task(update_master_db)
312
  return JSONResponse({"s": "ok"})
313
 
314
+ # --- HISTORICAL API ENDPOINTS ---
315
+
316
+ @app.get("/api/hist/dates")
317
+ def get_hist_dates(symbol: str):
318
+ """Fetches unique dates available for a symbol from Supabase"""
319
+ try:
320
+ # We fetch recorded_at, convert to date, distinct
321
+ # Supabase doesn't support easy 'distinct' on derived columns easily in python client without rpc
322
+ # So we fetch 'time_signature' which is string "YYYY-MM-DD HH:MM:SS"
323
+ # We'll just fetch entries, order by recency, and extract dates.
324
+ # Limit to last 500 records to extract unique dates
325
+ res = supabase.table('snapshots')\
326
+ .select('time_signature')\
327
+ .eq('symbol', symbol.upper())\
328
+ .order('recorded_at', desc=True)\
329
+ .limit(1000)\
330
+ .execute()
331
+
332
+ dates = set()
333
+ for item in res.data:
334
+ ts_str = item.get('time_signature', '')
335
+ if ts_str:
336
+ dates.add(ts_str.split(' ')[0]) # Extract YYYY-MM-DD
337
+
338
+ return JSONResponse(sorted(list(dates), reverse=True))
339
+ except Exception as e:
340
+ return JSONResponse({"error": str(e)}, status_code=500)
341
+
342
+ @app.get("/api/hist/minutes")
343
+ def get_hist_minutes(symbol: str, date: str):
344
+ """Fetches available minutes for a specific date"""
345
+ try:
346
+ # Filter strings starting with date
347
+ res = supabase.table('snapshots')\
348
+ .select('time_signature')\
349
+ .eq('symbol', symbol.upper())\
350
+ .like('time_signature', f'{date}%')\
351
+ .order('recorded_at', desc=True)\
352
+ .execute()
353
+
354
+ minutes = set()
355
+ for item in res.data:
356
+ ts_str = item.get('time_signature', '')
357
+ # Format: YYYY-MM-DD HH:MM:SS -> Extract HH:MM
358
+ parts = ts_str.split(' ')
359
+ if len(parts) > 1:
360
+ time_part = parts[1] # HH:MM:SS
361
+ minutes.add(time_part[:5]) # HH:MM
362
+
363
+ return JSONResponse(sorted(list(minutes), reverse=True))
364
+ except Exception as e:
365
+ return JSONResponse({"error": str(e)}, status_code=500)
366
+
367
+ @app.get("/api/hist/load")
368
+ def get_hist_data(symbol: str, date: str, minute: str):
369
+ """
370
+ Fetches ALL snapshots for a specific minute.
371
+ Returns a list of { time: "SS", data: {...reconstructed...} }
372
+ """
373
+ try:
374
+ # Construct query pattern like "2025-01-01 09:15%"
375
+ query_sig = f"{date} {minute}"
376
+
377
+ res = supabase.table('snapshots')\
378
+ .select('time_signature, payload')\
379
+ .eq('symbol', symbol.upper())\
380
+ .like('time_signature', f'{query_sig}%')\
381
+ .order('recorded_at', desc=False)\
382
+ .execute()
383
+
384
+ timeline = []
385
+ for item in res.data:
386
+ ts = item.get('time_signature', '')
387
+ sec = ts.split(':')[-1] if ':' in ts else "00"
388
+
389
+ raw_payload = item.get('payload')
390
+ # Handle if payload is stringified JSON
391
+ if isinstance(raw_payload, str):
392
+ raw_payload = json.loads(raw_payload)
393
+
394
+ clean_data = decompress_payload(raw_payload)
395
+ timeline.append({
396
+ "s": sec,
397
+ "d": clean_data
398
+ })
399
+
400
+ return JSONResponse({"s": "ok", "timeline": timeline})
401
+
402
+ except Exception as e:
403
+ print(e)
404
+ return JSONResponse({"error": str(e)}, status_code=500)
405
+
406
+
407
  # ==========================================
408
  # 4. FRONTEND
409
  # ==========================================
 
431
  .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); }
432
  .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; color: var(--text-sub); }
433
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
434
+ .btn.active { background: var(--accent); color: white; border-color: var(--accent); }
435
 
436
  .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; }
437
  .icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
 
449
  .badge-BSE { background: var(--badge-bse); }
450
  .badge-MCX { background: var(--badge-mcx); }
451
 
452
+ /* HISTORY CONTROLS */
453
+ .hist-controls { display: none; align-items: center; gap: 10px; margin-left: 20px; padding-left: 20px; border-left: 1px solid #eee; }
454
+ .hist-sel { padding: 8px; border-radius: 4px; border: 1px solid #ddd; font-weight: bold; font-size: 11px; background: #f9f9f9; }
455
+ .sec-bar-cont { display: flex; gap: 2px; overflow-x: auto; max-width: 300px; padding: 2px; background: #f0f0f0; border-radius: 4px; }
456
+ .sec-pill { padding: 2px 5px; font-size: 10px; cursor: pointer; border-radius: 2px; color: #666; font-family:'JetBrains Mono'; }
457
+ .sec-pill:hover { background: #ddd; color: #000; }
458
+ .sec-pill.active { background: var(--accent); color: white; font-weight: bold; }
459
+
460
  .grid-layout { display: flex; height: calc(100vh - 60px); }
461
 
462
  /* Chain Panel */
 
667
  <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY, GOLD)..." autocomplete="off">
668
  <div class="search-dropdown" id="searchRes"></div>
669
  </div>
670
+
671
+ <!-- HISTORY TOGGLE -->
672
+ <button class="btn" id="modeBtn" onclick="toggleMode()">
673
+ <span id="modeTxt">LIVE</span>
674
  </button>
675
+
676
+ <div class="hist-controls" id="histControls">
677
+ <select class="hist-sel" id="histDate" onchange="loadHistMinutes()"><option>DATE</option></select>
678
+ <select class="hist-sel" id="histTime" onchange="loadHistData()"><option>TIME</option></select>
679
+ <div class="sec-bar-cont" id="secBar"></div>
680
+ </div>
681
+
682
  </div>
683
 
684
  <div style="display:flex; gap:20px; align-items:center;">
685
+ <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB">
686
+ <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
687
+ </button>
688
  <div style="text-align:right;">
689
  <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
690
  <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
 
693
  <span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
694
  <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
695
  </button>
696
+ <div class="live-dot" id="statusDot" title="Connection Status"></div>
697
  <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;">
698
  <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
699
  </button>
 
759
  let SEARCH_TIMER = null;
760
  let ATM_STRIKE = 0;
761
 
762
+ // HISTORY MODE STATE
763
+ let IS_HISTORY = false;
764
+ let CACHED_TIMELINE = []; // Stores {s: "00", d: {...}} for current minute
765
+
766
  // DATA STORE FOR INFO MODAL
767
  let LATEST_ROW_DATA = {};
768
 
 
777
  ]
778
  };
779
 
780
+ // Load Config from LocalStorage
781
  let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS));
782
+ if(!Array.isArray(CONFIG.side)) CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side));
 
 
 
783
 
784
  // Temp state for editing
785
  let EDITING_COL = null;
 
801
  };
802
 
803
  // ===================================
804
+ // HISTORY MODE LOGIC
805
  // ===================================
806
+
807
+ function toggleMode() {
808
+ IS_HISTORY = !IS_HISTORY;
809
+ const btn = document.getElementById('modeBtn');
810
+ const txt = document.getElementById('modeTxt');
811
+ const ctrls = document.getElementById('histControls');
812
+ const dot = document.getElementById('statusDot');
813
+
814
+ if(IS_HISTORY) {
815
+ btn.classList.add('active');
816
+ txt.innerText = "HISTORY";
817
+ ctrls.style.display = "flex";
818
+ if(POLLER) clearInterval(POLLER); // Stop live polling
819
+ dot.style.background = "#2962ff"; // Blue for History
820
+
821
+ if(CURRENT_ROOT) loadHistDates();
822
+ } else {
823
+ btn.classList.remove('active');
824
+ txt.innerText = "LIVE";
825
+ ctrls.style.display = "none";
826
+ dot.style.background = "#ccc";
827
+ // Return to live
828
+ if(CURRENT_ROOT) {
829
+ loadSymbol(CURRENT_ROOT);
830
+ }
831
+ }
832
+ }
833
+
834
+ async function loadHistDates() {
835
+ if(!CURRENT_ROOT) return;
836
+ const sel = document.getElementById('histDate');
837
+ sel.innerHTML = "<option>LOADING...</option>";
838
+
839
+ try {
840
+ const res = await fetch(`/api/hist/dates?symbol=${CURRENT_ROOT}`);
841
+ const dates = await res.json();
842
+
843
+ sel.innerHTML = "<option value=''>SELECT DATE</option>" +
844
+ dates.map(d => `<option value="${d}">${d}</option>`).join('');
845
+
846
+ } catch(e) { console.error(e); sel.innerHTML = "<option>ERROR</option>"; }
847
+ }
848
+
849
+ async function loadHistMinutes() {
850
+ const date = document.getElementById('histDate').value;
851
+ if(!date) return;
852
+
853
+ const sel = document.getElementById('histTime');
854
+ sel.innerHTML = "<option>LOADING...</option>";
855
+
856
+ try {
857
+ const res = await fetch(`/api/hist/minutes?symbol=${CURRENT_ROOT}&date=${date}`);
858
+ const mins = await res.json();
859
+
860
+ sel.innerHTML = "<option value=''>SELECT TIME</option>" +
861
+ mins.map(m => `<option value="${m}">${m}</option>`).join('');
862
+ } catch(e) { console.error(e); }
863
+ }
864
+
865
+ async function loadHistData() {
866
+ const date = document.getElementById('histDate').value;
867
+ const time = document.getElementById('histTime').value;
868
+ if(!date || !time) return;
869
+
870
+ document.getElementById('loaderMsg').innerText = "FETCHING SNAPSHOTS...";
871
+ document.getElementById('loader').style.display = 'flex';
872
+
873
+ try {
874
+ const res = await fetch(`/api/hist/load?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`);
875
+ const json = await res.json();
876
+
877
+ CACHED_TIMELINE = json.timeline; // List of {s: "05", d: { ... }}
878
+
879
+ renderSecondBar();
880
+
881
+ // Auto-load first available second
882
+ if(CACHED_TIMELINE.length > 0) {
883
+ renderSnapshot(CACHED_TIMELINE[0]);
884
+ }
885
+
886
+ } catch(e) { alert("Error loading history: " + e); }
887
+ document.getElementById('loader').style.display = 'none';
888
+ }
889
+
890
+ function renderSecondBar() {
891
+ const bar = document.getElementById('secBar');
892
+ bar.innerHTML = CACHED_TIMELINE.map((item, idx) => `
893
+ <div class="sec-pill ${idx===0?'active':''}"
894
+ onclick="selectSnapshot(${idx})"
895
+ id="pill-${idx}">
896
+ :${item.s}
897
+ </div>
898
+ `).join('');
899
+ }
900
+
901
+ function selectSnapshot(idx) {
902
+ document.querySelectorAll('.sec-pill').forEach(p => p.classList.remove('active'));
903
+ document.getElementById(`pill-${idx}`).classList.add('active');
904
+ renderSnapshot(CACHED_TIMELINE[idx]);
905
+ }
906
+
907
+ function renderSnapshot(item) {
908
+ const data = item.d;
909
+
910
+ // 1. Extract Expiries from historical data if needed
911
+ // Since we are reusing buildChain which relies on CHAIN_DATA (Master),
912
+ // we assume Master DB still holds the correct structure.
913
+ // BUT for strikes/prices we use 'data'.
914
+
915
+ // Update UI with the historical packet
916
+ updateUI(data);
917
+
918
+ // Also ensure Expiries are valid for this historical snapshot
919
+ // We look into the data items to find distinct expiries present in this snapshot
920
+ const presentExpiries = new Set();
921
+ for(let key in data) {
922
+ // In reconstructed data, expiry is a field if available
923
+ if(data[key].expiry) {
924
+ // Format usually YYYY-MM-DD from v16 reconstruction
925
+ // But UI expects Unix Timestamp.
926
+ // We need to match it with CHAIN_DATA items.
927
+ // Actually, simpler: Let the user filter expiries based on what the Master DB has.
928
+ // History mode is just "Overwriting" the live price values.
929
+ }
930
+ }
931
+ }
932
+
933
+
934
+ // ===================================
935
+ // EXISTING APP LOGIC
936
+ // ===================================
937
+
938
+ // ... (Configuration & Sidebar logic is same) ...
939
  function applyConfig() {
940
  document.getElementById('headL_txt').innerText = CONFIG.left.name;
941
  document.getElementById('headR_txt').innerText = CONFIG.right.name;
942
  renderSidebar();
943
  }
944
 
 
945
  function renderSidebar() {
946
  const con = document.getElementById('sideContainer');
947
  if(!CONFIG.side || CONFIG.side.length === 0) {
 
1099
  function saveAndReload() {
1100
  localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
1101
  applyConfig();
1102
+ if(!IS_HISTORY) pollData();
1103
  }
1104
 
1105
  function closeModal(id) { document.getElementById(id).style.display = 'none'; }
 
1108
  // MATH ENGINE
1109
  // ===================================
1110
  function evaluateFormula(formula, vars) {
 
 
1111
  try {
1112
  let exp = formula;
 
 
1113
  exp = exp.replace(/CS/g, vars.CS || 0);
1114
  exp = exp.replace(/CB/g, vars.CB || 0);
1115
  exp = exp.replace(/PS/g, vars.PS || 0);
 
1122
  exp = exp.replace(/CV/g, vars.CV || 0);
1123
  exp = exp.replace(/PV/g, vars.PV || 0);
1124
 
 
1125
  if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
 
 
1126
  return new Function('return ' + exp)();
1127
  } catch(e) {
1128
  return 0;
1129
  }
1130
  }
1131
 
 
 
 
1132
  function showInfo(cardIdx, rangeIdx) {
1133
  if(!CONFIG.side[cardIdx]) return;
1134
 
 
1139
  document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
1140
  document.getElementById('infoForm').innerText = formula;
1141
 
 
1142
  const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
1143
  const lowers = kList.filter(k => k < SPOT_PRICE);
1144
+ const uppers = kList.filter(k => k >= SPOT_PRICE);
1145
 
1146
  const selLow = lowers.slice(-range);
1147
  const selHigh = uppers.slice(0, range);
 
1151
 
1152
  const table = document.getElementById('infoTable');
1153
 
 
1154
  let hHTML = `<tr>
1155
  <th style="width:50px;">STRIKE</th>
1156
  <th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th>
 
1161
  const val = evaluateFormula(formula, d);
1162
  grandTotal += val;
1163
 
 
1164
  const borderStyle = (k >= SPOT_PRICE && selLow.includes(k- (kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
1165
 
1166
  return `
 
1174
  </tr>`;
1175
  }).join('');
1176
 
 
1177
  rowsHTML += `<tr class="info-row-res">
1178
  <td colspan="5" style="text-align:right;">TOTAL</td>
1179
  <td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td>
1180
  </tr>`;
1181
 
1182
  table.innerHTML = hHTML + rowsHTML;
 
1183
  document.getElementById('infoModal').style.display = 'flex';
1184
  }
1185
 
 
1230
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
1231
  document.getElementById('loader').style.display = 'flex';
1232
 
1233
+ // Reset History Controls if switching symbol
1234
+ document.getElementById('histDate').innerHTML = "<option>DATE</option>";
1235
+ document.getElementById('histTime').innerHTML = "<option>TIME</option>";
1236
+ document.getElementById('secBar').innerHTML = "";
1237
+
1238
  try {
1239
  const res = await fetch(`/api/chain?symbol=${root}`);
1240
  if(!res.ok) throw new Error("Symbol not found");
 
1243
  CURRENT_ROOT = root;
1244
  CHAIN_DATA = data.items;
1245
  SPOT_SYM = data.spot;
1246
+ SPOT_PRICE = 0;
1247
 
1248
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
1249
 
 
1253
 
1254
  setupExpiries();
1255
  document.getElementById('loader').style.display = 'none';
1256
+
1257
+ // If we are in history mode, trigger date load
1258
+ if(IS_HISTORY) loadHistDates();
1259
 
1260
  } catch(e) {
1261
  alert("Error: " + e.message);
 
1268
  const exps = [...new Set(opts.map(i => i.e))].sort((a,b) => a-b);
1269
  const today = Math.floor(Date.now()/1000) - 86400;
1270
 
1271
+ // In history mode, allow past expiries? For now, simpler to just show what Master DB has.
1272
+ // Usually historical analysis is done on near-term data where master DB is still relevant.
1273
+ const validExps = exps; // exps.filter(ts => ts > today);
1274
+
1275
  if(validExps.length === 0) {
1276
  document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
1277
  return;
1278
  }
1279
 
1280
+ ACTIVE_EXP = validExps.find(ts => ts > today) || validExps[0];
1281
  updateExpText(ACTIVE_EXP);
1282
 
1283
  const grid = document.getElementById('expGrid');
 
1314
 
1315
  const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b);
1316
 
 
1317
  body.innerHTML = strikes.map(k => `
1318
  <div class="row" id="row-${k}" data-k="${k}">
 
1319
  <div class="graph-col">
1320
  <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
1321
  <div class="visual-bar" id="barL-${k}"></div>
 
1323
  <span class="delta-txt" id="valL-${k}">-</span>
1324
  </div>
1325
  </div>
 
1326
  <div class="graph-col">
1327
  <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
1328
  <div class="visual-bar" id="barR-${k}"></div>
 
1333
  </div>
1334
  `).join('');
1335
 
1336
+ window.scrolled = false;
1337
+
1338
+ if(!IS_HISTORY) restartPoller();
1339
  }
1340
 
1341
  function restartPoller() {
 
1345
  }
1346
 
1347
  async function pollData() {
1348
+ if(IS_POLLING || !CURRENT_ROOT || IS_HISTORY) return;
1349
  IS_POLLING = true;
1350
 
1351
  try {
 
1353
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
1354
 
1355
  if(kList.length > 0) {
 
1356
  const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
1357
  let selectedK = kList.find(k => k >= refPrice);
1358
  if (selectedK === undefined) selectedK = kList[kList.length - 1];
 
1360
  ATM_STRIKE = selectedK;
1361
  const atmIdx = kList.indexOf(selectedK);
1362
 
1363
+ // Highlight ATM
1364
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
1365
  const atmRow = document.getElementById(`row-${selectedK}`);
 
1366
  if(atmRow) {
1367
  atmRow.classList.add('atm');
1368
  if(!window.scrolled && SPOT_PRICE > 0) {
 
1371
  }
1372
  }
1373
 
1374
+ // Determine Max Range for Sidebar
1375
  let maxRange = 10;
1376
  if(CONFIG.side && Array.isArray(CONFIG.side)) {
1377
  CONFIG.side.forEach(item => {
 
1382
  });
1383
  }
1384
 
1385
+ // Optimized fetching range
1386
  const start = Math.max(0, atmIdx - 20);
1387
  const end = Math.min(kList.length, atmIdx + 20);
1388
  const viewKs = kList.slice(start, end);
1389
 
 
1390
  const rangeStart = Math.max(0, atmIdx - maxRange);
1391
  const rangeEnd = Math.min(kList.length, atmIdx + maxRange + 1);
1392
  const calcKs = kList.slice(rangeStart, rangeEnd);
 
1423
  }
1424
 
1425
  function updateUI(data) {
1426
+ // Update Spot
1427
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
1428
  SPOT_PRICE = data[SPOT_SYM].ltp;
1429
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
1430
  }
1431
 
1432
+ // Update Futures
1433
  const futHTML = FUT_SYMS.map(f => {
1434
  const d = data[f.s];
1435
  if(!d || typeof d.ltp !== 'number') return '';
 
1479
  }
1480
  }
1481
 
 
1482
  LATEST_ROW_DATA = rowData;
1483
 
1484
+ // --- RENDER CHAIN ---
1485
  let maxL = 0, maxR = 0;
1486
  const processedRows = {};
1487
 
 
1498
  for(const k in processedRows) {
1499
  const r = processedRows[k];
1500
 
 
1501
  const valL = document.getElementById(`valL-${k}`);
1502
  if(valL) {
1503
  valL.innerText = (r.left>0?"+":"") + fmt(r.left);
 
1508
  const pct = (Math.abs(r.left) / maxL) * 100;
1509
  barL.style.left = '0%';
1510
  barL.style.width = pct + '%';
1511
+ barL.style.background = r.left >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';
 
 
 
 
 
1512
  }
1513
  }
1514
 
 
1515
  const valR = document.getElementById(`valR-${k}`);
1516
  if(valR) {
1517
  valR.innerText = (r.right>0?"+":"") + fmt(r.right);
 
1522
  const pct = (Math.abs(r.right) / maxR) * 100;
1523
  barR.style.left = '0%';
1524
  barR.style.width = pct + '%';
1525
+ barR.style.background = r.right >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';
 
 
 
 
 
1526
  }
1527
  }
1528
  }
1529
 
1530
+ // ATM Highlighting Logic for History/Manual updates
1531
+ if(IS_HISTORY && SPOT_PRICE > 0) {
1532
+ const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
1533
+ const refPrice = SPOT_PRICE;
1534
+ let selectedK = kList.find(k => k >= refPrice);
1535
+ if (selectedK === undefined) selectedK = kList[kList.length - 1];
1536
+
1537
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
1538
+ const atmRow = document.getElementById(`row-${selectedK}`);
1539
+ if(atmRow) atmRow.classList.add('atm');
1540
+ }
1541
+
1542
+ // --- SIDEBAR AGGREGATION ---
1543
  if(SPOT_PRICE > 0 && CONFIG.side && CONFIG.side.length > 0) {
1544
  const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
 
 
1545
  const lowers = kList.filter(k => k < SPOT_PRICE);
1546
+ const uppers = kList.filter(k => k >= SPOT_PRICE);
1547
 
1548
  CONFIG.side.forEach((item, idx) => {
1549
  item.ranges.forEach((rng, rIdx) => {
1550
  const el = document.getElementById(`sv-${idx}-${rIdx}`);
1551
  if(el) {
 
1552
  const selLow = lowers.slice(-rng);
 
1553
  const selHigh = uppers.slice(0, rng);
 
 
1554
  const targetStrikes = [...selLow, ...selHigh];
1555
 
1556
  let total = 0;
1557
  for(const k of targetStrikes) {
1558
+ if(rowData[k]) total += evaluateFormula(item.formula, rowData[k]);
 
 
1559
  }
 
1560
  el.innerText = (total > 0 ? "+" : "") + fmt(total);
1561
  el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)';
1562
  }
 
1565
  }
1566
  }
1567
 
 
1568
  function fmt(n) {
1569
  if(!n) return '0';
1570
  let abs = Math.abs(n);