topsecrettraders commited on
Commit
9c26cef
·
verified ·
1 Parent(s): d0d5935

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -284
app.py CHANGED
@@ -7,11 +7,9 @@ import time
7
  import urllib.parse
8
  import os
9
  import threading
10
- import asyncio
11
- from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
12
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
13
  from typing import Optional, List, Dict
14
- from fyers_apiv3.FyersWebsocket import data_ws
15
 
16
  # ==========================================
17
  # 1. CONFIGURATION
@@ -26,25 +24,17 @@ CACHE_FILE = "master_cache.json"
26
  app = FastAPI()
27
 
28
  # ==========================================
29
- # 2. GLOBAL DATA & STATE
30
  # ==========================================
31
 
32
  MASTER_DB = {}
33
  SEARCH_INDEX = []
34
 
35
- # LIVE MARKET DATA STORE (The "Scoreboard")
36
- LIVE_DATA = {}
37
- LIVE_DATA_LOCK = threading.Lock()
38
-
39
  SYSTEM_STATUS = {
40
  "ready": False,
41
- "message": "Booting...",
42
- "socket": False
43
  }
44
-
45
- # FYERS SOCKET OBJECT
46
- FYERS_SOCKET = None
47
- CURRENT_TOKEN = None
48
 
49
  # Manual mapping for Index Spot symbols
50
  INDEX_MAP = {
@@ -57,13 +47,8 @@ INDEX_MAP = {
57
  "SENSEX50": "BSE:SENSEX50-INDEX"
58
  }
59
 
60
- # ==========================================
61
- # 3. MASTER DATA LOADER
62
- # ==========================================
63
-
64
- UPDATE_LOCK = threading.Lock()
65
-
66
  def get_root_symbol(desc: str, symbol: str) -> str:
 
67
  try:
68
  parts = desc.strip().split(' ')
69
  root = parts[0].upper()
@@ -87,17 +72,29 @@ def update_master_db():
87
 
88
  # 1. NSE F&O
89
  url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
90
- df_nse = pd.read_csv(url_nse, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
 
 
91
  df_nse['Exch'] = 'NSE'
92
 
93
  # 2. MCX F&O
94
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
95
- df_mcx = pd.read_csv(url_mcx, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
 
 
96
  df_mcx['Exch'] = 'MCX'
97
 
98
  # 3. BSE F&O
99
  url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
100
- df_bse = pd.read_csv(url_bse, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
 
 
101
  df_bse['Exch'] = 'BSE'
102
 
103
  SYSTEM_STATUS["message"] = "Processing Data..."
@@ -204,84 +201,7 @@ def startup_event():
204
  thread.start()
205
 
206
  # ==========================================
207
- # 4. WEBSOCKET MANAGER (SERVER <-> FYERS)
208
- # ==========================================
209
-
210
- def on_fyers_msg(msg):
211
- global LIVE_DATA
212
- # msg format: [{'symbol': 'NSE:SBIN-EQ', 'ltp': 500.0, ...}] or dict
213
- # Fyers SDK sometimes returns list, sometimes dict
214
-
215
- data_list = []
216
- if isinstance(msg, list): data_list = msg
217
- elif isinstance(msg, dict): data_list = [msg]
218
-
219
- with LIVE_DATA_LOCK:
220
- for tick in data_list:
221
- if 'symbol' in tick:
222
- sym = tick['symbol']
223
- # Create or Update
224
- if sym not in LIVE_DATA: LIVE_DATA[sym] = {}
225
-
226
- # Update fields present in tick
227
- if 'ltp' in tick: LIVE_DATA[sym]['ltp'] = tick['ltp']
228
- if 'vol_traded_today' in tick: LIVE_DATA[sym]['vol'] = tick['vol_traded_today']
229
- if 'oi' in tick: LIVE_DATA[sym]['oi'] = tick['oi']
230
- if 'ch' in tick: LIVE_DATA[sym]['ch'] = tick['ch']
231
-
232
- # Fyers sends BuyQty/SellQty differently sometimes
233
- if 'totalbuyqty' in tick: LIVE_DATA[sym]['tbq'] = tick['totalbuyqty']
234
- if 'totalsellqty' in tick: LIVE_DATA[sym]['tsq'] = tick['totalsellqty']
235
-
236
- # Also handle the depth packets if enabled
237
- if 'bid' in tick and 'ask' in tick:
238
- LIVE_DATA[sym]['bid'] = tick['bid']
239
- LIVE_DATA[sym]['ask'] = tick['ask']
240
-
241
- def on_fyers_error(msg):
242
- print(f"!!! FYERS SOCKET ERROR: {msg}")
243
-
244
- def on_fyers_open():
245
- print(">>> FYERS SOCKET CONNECTED")
246
- SYSTEM_STATUS['socket'] = True
247
-
248
- def on_fyers_close(msg):
249
- print(">>> FYERS SOCKET CLOSED")
250
- SYSTEM_STATUS['socket'] = False
251
-
252
- def start_fyers_socket(token):
253
- global FYERS_SOCKET
254
- try:
255
- # data_type: SymbolUpdate
256
- FYERS_SOCKET = data_ws.FyersDataSocket(
257
- access_token=token,
258
- log_path="logs",
259
- litemode=False, # We want OI and Volume
260
- write_to_file=False,
261
- reconnect=True,
262
- on_connect=on_fyers_open,
263
- on_close=on_fyers_close,
264
- on_error=on_fyers_error,
265
- on_message=on_fyers_msg
266
- )
267
- FYERS_SOCKET.connect()
268
- except Exception as e:
269
- print(f"!!! SOCKET START ERROR: {e}")
270
-
271
- def subscribe_to_fyers(symbols):
272
- global FYERS_SOCKET
273
- if FYERS_SOCKET and symbols:
274
- # Fyers requires max 50-100 symbols per call usually, batching recommended for massive lists
275
- # But for UI usage, simple list is fine
276
- try:
277
- print(f">>> SUBSCRIBING TO {len(symbols)} SYMBOLS")
278
- FYERS_SOCKET.subscribe(symbols=symbols, data_type="SymbolUpdate")
279
- FYERS_SOCKET.subscribe(symbols=symbols, data_type="DepthUpdate") # Orderflow need
280
- except Exception as e:
281
- print(f"!!! SUBSCRIPTION ERROR: {e}")
282
-
283
- # ==========================================
284
- # 5. ROUTES
285
  # ==========================================
286
 
287
  @app.get("/")
@@ -302,15 +222,6 @@ def home(access_token: Optional[str] = Cookie(None)):
302
  </div>
303
  """)
304
 
305
- # Start Socket if not running
306
- global CURRENT_TOKEN
307
- if not CURRENT_TOKEN:
308
- CURRENT_TOKEN = access_token
309
- # Start in thread
310
- t = threading.Thread(target=start_fyers_socket, args=(access_token,))
311
- t.daemon = True
312
- t.start()
313
-
314
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
315
 
316
  @app.get("/callback")
@@ -369,53 +280,7 @@ def force_refresh(background_tasks: BackgroundTasks):
369
  return JSONResponse({"s": "ok"})
370
 
371
  # ==========================================
372
- # 6. WEBSOCKET BRIDGE FOR FRONTEND
373
- # ==========================================
374
-
375
- @app.websocket("/ws")
376
- async def websocket_endpoint(websocket: WebSocket):
377
- await websocket.accept()
378
- active_symbols = set()
379
-
380
- try:
381
- while True:
382
- # 1. Check for incoming messages (commands from frontend)
383
- try:
384
- # Use a timeout so we don't block the sending loop
385
- data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
386
- msg = json.loads(data)
387
-
388
- if msg.get('t') == 'sub':
389
- # Subscribe command
390
- syms = msg.get('s', [])
391
- active_symbols.update(syms)
392
- # Trigger Fyers Subscription
393
- if syms: subscribe_to_fyers(list(active_symbols))
394
-
395
- except asyncio.TimeoutError:
396
- pass # No message from client, continue to send data
397
- except Exception:
398
- break # Client disconnected
399
-
400
- # 2. Send Data Updates to Client
401
- if active_symbols:
402
- updates = {}
403
- with LIVE_DATA_LOCK:
404
- for s in active_symbols:
405
- if s in LIVE_DATA:
406
- updates[s] = LIVE_DATA[s]
407
-
408
- if updates:
409
- await websocket.send_text(json.dumps({"t": "d", "d": updates}))
410
-
411
- # Throttle to avoid freezing browser (100ms updates)
412
- await asyncio.sleep(0.1)
413
-
414
- except WebSocketDisconnect:
415
- print(">>> CLIENT DISCONNECTED")
416
-
417
- # ==========================================
418
- # 7. FRONTEND
419
  # ==========================================
420
 
421
  HTML_TEMPLATE = """
@@ -423,7 +288,7 @@ HTML_TEMPLATE = """
423
  <html lang="en">
424
  <head>
425
  <meta charset="UTF-8">
426
- <title>PRO CHAIN V3 (LIVE WS)</title>
427
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
428
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
429
  <style>
@@ -705,12 +570,10 @@ HTML_TEMPLATE = """
705
  let SPOT_PRICE = 0;
706
  let FUT_SYMS = [];
707
  let OPT_SYMS = [];
 
 
708
  let SEARCH_TIMER = null;
709
  let ATM_STRIKE = 0;
710
-
711
- // WS State
712
- let WS = null;
713
- let WS_CACHE = {}; // Store latest values for smoothing
714
 
715
  // Default Configurations
716
  const DEFAULTS = {
@@ -731,7 +594,6 @@ HTML_TEMPLATE = """
731
 
732
  applyConfig();
733
  checkStatus();
734
- initWebSocket(); // START THE SOCKET
735
 
736
  const inp = document.getElementById('searchInp');
737
  inp.addEventListener('input', (e) => {
@@ -742,55 +604,6 @@ HTML_TEMPLATE = """
742
  if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
743
  });
744
  };
745
-
746
- // ===================================
747
- // WEBSOCKET LOGIC (THE HEART)
748
- // ===================================
749
- function initWebSocket() {
750
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
751
- WS = new WebSocket(`${proto}//${location.host}/ws`);
752
-
753
- WS.onopen = () => {
754
- document.getElementById('statusDot').classList.add('active');
755
- document.getElementById('statusDot').classList.remove('error');
756
- // Re-subscribe if we have active symbols
757
- if(OPT_SYMS.length > 0) requestSubscription();
758
- };
759
-
760
- WS.onmessage = (evt) => {
761
- const msg = JSON.parse(evt.data);
762
- if(msg.t === 'd') {
763
- updateUI(msg.d); // LIVE UPDATES
764
- }
765
- };
766
-
767
- WS.onclose = () => {
768
- document.getElementById('statusDot').classList.remove('active');
769
- document.getElementById('statusDot').classList.add('error');
770
- setTimeout(initWebSocket, 2000); // Auto Reconnect
771
- };
772
- }
773
-
774
- function requestSubscription() {
775
- if(!WS || WS.readyState !== 1) return;
776
-
777
- // Calculate symbols needed
778
- let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
779
-
780
- // Get Visible Range + Buffer
781
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
782
-
783
- if(kList.length > 0) {
784
- // Basic Logic: Subscribe to ALL loaded strikes for simplicity in V1
785
- // Optimization can be added later (only visible)
786
- OPT_SYMS.forEach(o => symbolsToFetch.push(o.s));
787
- }
788
-
789
- const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
790
- if(uniqueSyms.length > 0) {
791
- WS.send(JSON.stringify({ t: 'sub', s: uniqueSyms }));
792
- }
793
- }
794
 
795
  // ===================================
796
  // CONFIGURATION LOGIC
@@ -822,13 +635,14 @@ HTML_TEMPLATE = """
822
 
823
  function setFormula(val) {
824
  const input = document.getElementById('confForm');
 
825
  let f = val;
826
  if(EDITING_COL === 'left') {
827
  f = f.replace(/S/g, 'CS').replace(/B/g, 'CB');
828
  } else if(EDITING_COL === 'right') {
829
  f = f.replace(/S/g, 'PS').replace(/B/g, 'PB');
830
  } else {
831
- f = f.replace(/S/g, 'PS').replace(/B/g, 'CB');
832
  }
833
  input.value = f;
834
  }
@@ -848,7 +662,7 @@ HTML_TEMPLATE = """
848
 
849
  applyConfig();
850
  closeModal('confModal');
851
- // No polling to restart, UI updates automatically on next tick
852
  }
853
 
854
  function saveSideConfig() {
@@ -863,6 +677,7 @@ HTML_TEMPLATE = """
863
 
864
  applyConfig();
865
  closeModal('sideConfModal');
 
866
  }
867
 
868
  function resetConfig() {
@@ -871,6 +686,7 @@ HTML_TEMPLATE = """
871
  localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
872
  applyConfig();
873
  closeModal('confModal');
 
874
  }
875
  }
876
 
@@ -880,13 +696,20 @@ HTML_TEMPLATE = """
880
  // MATH ENGINE
881
  // ===================================
882
  function evaluateFormula(formula, vars) {
 
883
  try {
884
  let exp = formula;
 
 
885
  exp = exp.replace(/CS/g, vars.CS || 0);
886
  exp = exp.replace(/CB/g, vars.CB || 0);
887
  exp = exp.replace(/PS/g, vars.PS || 0);
888
  exp = exp.replace(/PB/g, vars.PB || 0);
 
 
889
  if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
 
 
890
  return new Function('return ' + exp)();
891
  } catch(e) {
892
  return 0;
@@ -948,8 +771,7 @@ HTML_TEMPLATE = """
948
  CURRENT_ROOT = root;
949
  CHAIN_DATA = data.items;
950
  SPOT_SYM = data.spot;
951
- SPOT_PRICE = 0;
952
- WS_CACHE = {}; // Clear old data
953
 
954
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
955
 
@@ -1026,111 +848,148 @@ HTML_TEMPLATE = """
1026
  </div>
1027
  `).join('');
1028
 
1029
- window.scrolled = false;
1030
- requestSubscription(); // Ask for data
1031
  }
1032
 
1033
- // ===================================
1034
- // UI UPDATER
1035
- // ===================================
1036
- function updateUI(newData) {
1037
- // Merge into cache
1038
- for(let k in newData) {
1039
- WS_CACHE[k] = { ...WS_CACHE[k], ...newData[k] };
1040
- }
1041
 
1042
- const data = WS_CACHE; // Use full cache state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
- // 1. Update SPOT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1045
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
1046
  SPOT_PRICE = data[SPOT_SYM].ltp;
1047
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
1048
  }
1049
 
1050
- // 2. Update Futures
1051
  const futHTML = FUT_SYMS.map(f => {
1052
  const d = data[f.s];
1053
  if(!d || typeof d.ltp !== 'number') return '';
1054
  const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
1055
-
1056
- // Handle incomplete data gracefully
1057
- const tbq = d.tbq || d.vol || 1; // Fallback
1058
- const tsq = d.tsq || d.vol || 1;
1059
- const tot = tbq + tsq;
1060
- const bPct = (tbq/tot)*100;
1061
- const ch = d.ch || 0;
1062
-
1063
  return `
1064
  <div class="fut-card">
1065
  <div style="display:flex; justify-content:space-between; font-weight:700;">
1066
- <span>${dt} FUT</span> <span style="color:${ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
1067
  </div>
1068
  <div class="meter-bg">
1069
  <div style="width:${bPct}%; background:var(--green)"></div>
1070
  <div style="width:${100-bPct}%; background:var(--red)"></div>
1071
  </div>
1072
  <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
1073
- <span>B: ${fmt(tbq)}</span> <span>S: ${fmt(tsq)}</span>
1074
  </div>
1075
  </div>`;
1076
  }).join('');
1077
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
1078
 
1079
- // 3. Process Options
1080
  const rowData = {};
1081
 
 
1082
  for(const item of OPT_SYMS) {
1083
  const d = data[item.s];
1084
  if(!d) continue;
1085
 
1086
  if(!rowData[item.k]) rowData[item.k] = { CS:0, CB:0, PS:0, PB:0 };
1087
 
1088
- // Fyers logic: We use OI (Open Interest) or Volume based on availability
1089
- // Often 'totalsellqty' is not sent in basic feed, we use OI for sellers proxy
1090
- // But user wants OI specifically
1091
- const oi = d.oi || 0;
1092
- const vol = d.vol || 0;
1093
-
1094
- // Simplified Logic for Demo:
1095
- // In reality, OI is just a single number. We can't split OI into Buyer/Seller perfectly without Orderflow.
1096
- // PRO TRICK: We use the 'Total Sell Qty' if available (Depth) else we use OI.
1097
- // Since we enabled Depth in backend, let's try to use Bid/Ask sums if available, or just OI.
1098
-
1099
- let sellPressure = d.tsq || (oi / 2); // Approximate if depth missing
1100
- let buyPressure = d.tbq || (oi / 2);
1101
-
1102
  if(item.t === 'CE') {
1103
- rowData[item.k].CS = sellPressure;
1104
- rowData[item.k].CB = buyPressure;
1105
  }
1106
  if(item.t === 'PE') {
1107
- rowData[item.k].PS = sellPressure;
1108
- rowData[item.k].PB = buyPressure;
1109
  }
1110
  }
1111
 
1112
- // 4. Update Strike Row Classes (ATM)
1113
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
1114
- if(kList.length > 0 && SPOT_PRICE > 0) {
1115
- let selectedK = kList.find(k => k >= SPOT_PRICE);
1116
- if (selectedK === undefined) selectedK = kList[kList.length - 1];
1117
-
1118
- ATM_STRIKE = selectedK;
1119
- document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
1120
- const atmRow = document.getElementById(`row-${selectedK}`);
1121
- if(atmRow) {
1122
- atmRow.classList.add('atm');
1123
- if(!window.scrolled) {
1124
- atmRow.scrollIntoView({block:'center', behavior:'auto'});
1125
- window.scrolled = true;
1126
- }
1127
- }
1128
- }
1129
-
1130
- // 5. Render Chain Values
1131
  let maxMidVal = 0;
1132
  const processedRows = {};
1133
 
 
1134
  for(const k in rowData) {
1135
  const vars = rowData[k];
1136
  processedRows[k] = {
@@ -1143,6 +1002,7 @@ HTML_TEMPLATE = """
1143
 
1144
  for(const k in processedRows) {
1145
  const r = processedRows[k];
 
1146
  const lEl = document.getElementById(`colL-${k}`);
1147
  if(lEl) {
1148
  lEl.querySelector('.col-val').innerText = fmt(r.left);
@@ -1170,19 +1030,19 @@ HTML_TEMPLATE = """
1170
  }
1171
  }
1172
 
1173
- // 6. Sidebar Aggregation
1174
  if(ATM_STRIKE > 0) {
1175
  let totalFlow = 0;
1176
- const kListSorted = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
1177
- const atmIdx = kListSorted.indexOf(ATM_STRIKE);
1178
  const sideRange = CONFIG.side.range;
1179
 
1180
  if(atmIdx !== -1) {
1181
  const start = Math.max(0, atmIdx - sideRange);
1182
- const end = Math.min(kListSorted.length, atmIdx + sideRange + 1);
1183
 
1184
  for(let i=start; i<end; i++) {
1185
- const k = kListSorted[i];
1186
  if(rowData[k]) {
1187
  totalFlow += evaluateFormula(CONFIG.side.formula, rowData[k]);
1188
  }
@@ -1194,8 +1054,14 @@ HTML_TEMPLATE = """
1194
 
1195
  nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
1196
  nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
1197
- nfSig.innerText = totalFlow >= 0 ? "POSITIVE FLOW" : "NEGATIVE FLOW";
1198
- nfSig.style.color = totalFlow >= 0 ? "var(--green)" : "var(--red)";
 
 
 
 
 
 
1199
  }
1200
  }
1201
 
 
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
 
13
 
14
  # ==========================================
15
  # 1. CONFIGURATION
 
24
  app = FastAPI()
25
 
26
  # ==========================================
27
+ # 2. ROBUST DATA LOADER (THREAD SAFE)
28
  # ==========================================
29
 
30
  MASTER_DB = {}
31
  SEARCH_INDEX = []
32
 
 
 
 
 
33
  SYSTEM_STATUS = {
34
  "ready": False,
35
+ "message": "Booting..."
 
36
  }
37
+ UPDATE_LOCK = threading.Lock()
 
 
 
38
 
39
  # Manual mapping for Index Spot symbols
40
  INDEX_MAP = {
 
47
  "SENSEX50": "BSE:SENSEX50-INDEX"
48
  }
49
 
 
 
 
 
 
 
50
  def get_root_symbol(desc: str, symbol: str) -> str:
51
+ """Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
52
  try:
53
  parts = desc.strip().split(' ')
54
  root = parts[0].upper()
 
72
 
73
  # 1. NSE F&O
74
  url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
75
+ df_nse = pd.read_csv(
76
+ url_nse, usecols=[0, 1, 8, 9, 13],
77
+ names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
78
+ header=0, on_bad_lines='skip', storage_options=headers
79
+ )
80
  df_nse['Exch'] = 'NSE'
81
 
82
  # 2. MCX F&O
83
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
84
+ df_mcx = pd.read_csv(
85
+ url_mcx, usecols=[0, 1, 8, 9, 13],
86
+ names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
87
+ header=0, on_bad_lines='skip', storage_options=headers
88
+ )
89
  df_mcx['Exch'] = 'MCX'
90
 
91
  # 3. BSE F&O
92
  url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
93
+ df_bse = pd.read_csv(
94
+ url_bse, usecols=[0, 1, 8, 9, 13],
95
+ names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
96
+ header=0, on_bad_lines='skip', storage_options=headers
97
+ )
98
  df_bse['Exch'] = 'BSE'
99
 
100
  SYSTEM_STATUS["message"] = "Processing Data..."
 
201
  thread.start()
202
 
203
  # ==========================================
204
+ # 3. ROUTES
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  # ==========================================
206
 
207
  @app.get("/")
 
222
  </div>
223
  """)
224
 
 
 
 
 
 
 
 
 
 
225
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
226
 
227
  @app.get("/callback")
 
280
  return JSONResponse({"s": "ok"})
281
 
282
  # ==========================================
283
+ # 4. FRONTEND
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  # ==========================================
285
 
286
  HTML_TEMPLATE = """
 
288
  <html lang="en">
289
  <head>
290
  <meta charset="UTF-8">
291
+ <title>PRO CHAIN V3</title>
292
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
293
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
294
  <style>
 
570
  let SPOT_PRICE = 0;
571
  let FUT_SYMS = [];
572
  let OPT_SYMS = [];
573
+ let POLLER = null;
574
+ let IS_POLLING = false;
575
  let SEARCH_TIMER = null;
576
  let ATM_STRIKE = 0;
 
 
 
 
577
 
578
  // Default Configurations
579
  const DEFAULTS = {
 
594
 
595
  applyConfig();
596
  checkStatus();
 
597
 
598
  const inp = document.getElementById('searchInp');
599
  inp.addEventListener('input', (e) => {
 
604
  if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
605
  });
606
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
608
  // ===================================
609
  // CONFIGURATION LOGIC
 
635
 
636
  function setFormula(val) {
637
  const input = document.getElementById('confForm');
638
+ // If editing Left, defaults use Call variables; Right uses Put variables
639
  let f = val;
640
  if(EDITING_COL === 'left') {
641
  f = f.replace(/S/g, 'CS').replace(/B/g, 'CB');
642
  } else if(EDITING_COL === 'right') {
643
  f = f.replace(/S/g, 'PS').replace(/B/g, 'PB');
644
  } else {
645
+ f = f.replace(/S/g, 'PS').replace(/B/g, 'CB'); // Middle default logic usually mix
646
  }
647
  input.value = f;
648
  }
 
662
 
663
  applyConfig();
664
  closeModal('confModal');
665
+ pollData(); // Force refresh
666
  }
667
 
668
  function saveSideConfig() {
 
677
 
678
  applyConfig();
679
  closeModal('sideConfModal');
680
+ pollData();
681
  }
682
 
683
  function resetConfig() {
 
686
  localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG));
687
  applyConfig();
688
  closeModal('confModal');
689
+ pollData();
690
  }
691
  }
692
 
 
696
  // MATH ENGINE
697
  // ===================================
698
  function evaluateFormula(formula, vars) {
699
+ // vars: { CS, CB, PS, PB }
700
  try {
701
  let exp = formula;
702
+ // Replace vars with values. We sort by length desc to avoid replacing 'CS' inside 'CSS' if that existed.
703
+ // But here we have 2-char vars.
704
  exp = exp.replace(/CS/g, vars.CS || 0);
705
  exp = exp.replace(/CB/g, vars.CB || 0);
706
  exp = exp.replace(/PS/g, vars.PS || 0);
707
  exp = exp.replace(/PB/g, vars.PB || 0);
708
+
709
+ // Safety check: only allow numbers and math ops
710
  if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
711
+
712
+ // Evaluate
713
  return new Function('return ' + exp)();
714
  } catch(e) {
715
  return 0;
 
771
  CURRENT_ROOT = root;
772
  CHAIN_DATA = data.items;
773
  SPOT_SYM = data.spot;
774
+ SPOT_PRICE = 0; // Reset price on new load
 
775
 
776
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
777
 
 
848
  </div>
849
  `).join('');
850
 
851
+ window.scrolled = false; // Reset scroll state
852
+ restartPoller();
853
  }
854
 
855
+ function restartPoller() {
856
+ if(POLLER) clearInterval(POLLER);
857
+ POLLER = setInterval(pollData, 1500);
858
+ pollData();
859
+ }
 
 
 
860
 
861
+ async function pollData() {
862
+ if(IS_POLLING || !CURRENT_ROOT) return;
863
+ IS_POLLING = true;
864
+
865
+ try {
866
+ let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
867
+ const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
868
+
869
+ if(kList.length > 0) {
870
+ // LOGIC UPDATE: Select Next Strike just above live price
871
+ // If price is 0 (first load), fallback to middle temporarily
872
+ const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
873
+
874
+ // Find first strike >= refPrice (Next Strike)
875
+ let selectedK = kList.find(k => k >= refPrice);
876
+
877
+ // Edge Case: If price is higher than all strikes, take the last one
878
+ if (selectedK === undefined) selectedK = kList[kList.length - 1];
879
 
880
+ ATM_STRIKE = selectedK;
881
+ const atmIdx = kList.indexOf(selectedK);
882
+
883
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
884
+ const atmRow = document.getElementById(`row-${selectedK}`);
885
+
886
+ if(atmRow) {
887
+ atmRow.classList.add('atm');
888
+ // SCROLL UPDATE: Only scroll if we have a valid LIVE price
889
+ if(!window.scrolled && SPOT_PRICE > 0) {
890
+ atmRow.scrollIntoView({block:'center', behavior:'auto'});
891
+ window.scrolled = true;
892
+ }
893
+ }
894
+
895
+ // Range settings for Side Panel
896
+ const sideRange = CONFIG.side.range;
897
+
898
+ // Visible Range (Fixed)
899
+ const start = Math.max(0, atmIdx - 20);
900
+ const end = Math.min(kList.length, atmIdx + 20);
901
+ const viewKs = kList.slice(start, end);
902
+
903
+ // Side Calculation Range
904
+ const rangeStart = Math.max(0, atmIdx - sideRange);
905
+ const rangeEnd = Math.min(kList.length, atmIdx + sideRange + 1);
906
+ const calcKs = kList.slice(rangeStart, rangeEnd);
907
+
908
+ const allNeededKs = new Set([...viewKs, ...calcKs]);
909
+
910
+ OPT_SYMS.forEach(o => {
911
+ if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s);
912
+ });
913
+ }
914
+
915
+ const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
916
+ if(uniqueSyms.length === 0) return;
917
+
918
+ const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
919
+ const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN }});
920
+
921
+ if(res.status === 401) { location.href = '/logout'; return; }
922
+
923
+ const json = await res.json();
924
+ if(json.s === 'ok') {
925
+ updateUI(json.d);
926
+ const dot = document.getElementById('statusDot');
927
+ dot.classList.add('active');
928
+ dot.classList.remove('error');
929
+ }
930
+ } catch(e) {
931
+ console.error(e);
932
+ const dot = document.getElementById('statusDot');
933
+ dot.classList.remove('active');
934
+ dot.classList.add('error');
935
+ }
936
+ finally { IS_POLLING = false; }
937
+ }
938
+
939
+ function updateUI(data) {
940
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
941
  SPOT_PRICE = data[SPOT_SYM].ltp;
942
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
943
  }
944
 
945
+ // Futures
946
  const futHTML = FUT_SYMS.map(f => {
947
  const d = data[f.s];
948
  if(!d || typeof d.ltp !== 'number') return '';
949
  const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
950
+ const tot = (d.totalbuyqty + d.totalsellqty) || 1;
951
+ const bPct = (d.totalbuyqty/tot)*100;
 
 
 
 
 
 
952
  return `
953
  <div class="fut-card">
954
  <div style="display:flex; justify-content:space-between; font-weight:700;">
955
+ <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
956
  </div>
957
  <div class="meter-bg">
958
  <div style="width:${bPct}%; background:var(--green)"></div>
959
  <div style="width:${100-bPct}%; background:var(--red)"></div>
960
  </div>
961
  <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
962
+ <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
963
  </div>
964
  </div>`;
965
  }).join('');
966
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
967
 
968
+ // Process Options
969
  const rowData = {};
970
 
971
+ // Populate raw data first
972
  for(const item of OPT_SYMS) {
973
  const d = data[item.s];
974
  if(!d) continue;
975
 
976
  if(!rowData[item.k]) rowData[item.k] = { CS:0, CB:0, PS:0, PB:0 };
977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  if(item.t === 'CE') {
979
+ rowData[item.k].CS = d.totalsellqty;
980
+ rowData[item.k].CB = d.totalbuyqty;
981
  }
982
  if(item.t === 'PE') {
983
+ rowData[item.k].PS = d.totalsellqty;
984
+ rowData[item.k].PB = d.totalbuyqty;
985
  }
986
  }
987
 
988
+ // --- RENDER CHAIN ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  let maxMidVal = 0;
990
  const processedRows = {};
991
 
992
+ // Calculate Formula values first
993
  for(const k in rowData) {
994
  const vars = rowData[k];
995
  processedRows[k] = {
 
1002
 
1003
  for(const k in processedRows) {
1004
  const r = processedRows[k];
1005
+
1006
  const lEl = document.getElementById(`colL-${k}`);
1007
  if(lEl) {
1008
  lEl.querySelector('.col-val').innerText = fmt(r.left);
 
1030
  }
1031
  }
1032
 
1033
+ // --- SIDEBAR AGGREGATION ---
1034
  if(ATM_STRIKE > 0) {
1035
  let totalFlow = 0;
1036
+ const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
1037
+ const atmIdx = kList.indexOf(ATM_STRIKE);
1038
  const sideRange = CONFIG.side.range;
1039
 
1040
  if(atmIdx !== -1) {
1041
  const start = Math.max(0, atmIdx - sideRange);
1042
+ const end = Math.min(kList.length, atmIdx + sideRange + 1);
1043
 
1044
  for(let i=start; i<end; i++) {
1045
+ const k = kList[i];
1046
  if(rowData[k]) {
1047
  totalFlow += evaluateFormula(CONFIG.side.formula, rowData[k]);
1048
  }
 
1054
 
1055
  nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
1056
  nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
1057
+
1058
+ if(totalFlow > 0) {
1059
+ nfSig.innerText = "POSITIVE FLOW";
1060
+ nfSig.style.color = "var(--green)";
1061
+ } else {
1062
+ nfSig.innerText = "NEGATIVE FLOW";
1063
+ nfSig.style.color = "var(--red)";
1064
+ }
1065
  }
1066
  }
1067