topsecrettraders commited on
Commit
78999cc
·
verified ·
1 Parent(s): 7844e79

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +387 -876
app.py CHANGED
@@ -1,131 +1,98 @@
1
- import pandas as pd
2
- import requests
3
- import uvicorn
4
- import hashlib
5
- import json
6
  import time
 
 
7
  import urllib.parse
8
- import os
9
  import threading
10
- from datetime import datetime, timedelta, timezone
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
 
14
 
15
  # ==========================================
16
  # 1. CONFIGURATION
17
  # ==========================================
18
- CLIENT_ID = "J4PNT5QI22-100"
19
- SECRET_KEY = "ABRSWC1PMR"
20
  REDIRECT_URI = "https://topsecrettraders-depthchainindia.hf.space/callback"
21
-
22
- CACHE_FILE = "master_cache.json"
23
-
24
- # --- SUPABASE CONFIG ---
25
- SUPABASE_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
26
- SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
27
 
28
  app = FastAPI()
 
29
 
30
  # ==========================================
31
- # 2. ROBUST DATA LOADER & SUPABASE
32
  # ==========================================
33
-
34
- from supabase import create_client, Client
35
- supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
36
-
37
  MASTER_DB = {}
38
- SEARCH_INDEX = []
39
-
40
- SYSTEM_STATUS = {
41
- "ready": False,
42
- "message": "Booting..."
43
- }
44
  UPDATE_LOCK = threading.Lock()
45
 
46
- # Manual mapping for Index Spot symbols
47
  INDEX_MAP = {
48
- "NIFTY": "NSE:NIFTY50-INDEX",
49
- "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
50
- "FINNIFTY": "NSE:FINNIFTY-INDEX",
51
- "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",
52
- "SENSEX": "BSE:SENSEX-INDEX",
53
- "BANKEX": "BSE:BANKEX-INDEX",
54
  "SENSEX50": "BSE:SENSEX50-INDEX"
55
  }
56
 
57
  def get_root_symbol(desc: str, symbol: str) -> str:
58
  try:
59
  parts = desc.strip().split(' ')
60
- root = parts[0].upper()
61
- return root.replace(':', '').replace('-', '')
62
  except:
63
- if ':' in symbol:
64
- return symbol.split(':')[1]
65
- return symbol
66
 
67
  def update_master_db():
68
  global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
69
-
70
- if not UPDATE_LOCK.acquire(blocking=False):
71
- return
72
 
73
  try:
74
  SYSTEM_STATUS["message"] = "Downloading Master Data..."
75
- print(">>> [BG TASK] STARTING DOWNLOAD...")
76
-
77
  headers = { "User-Agent": "Mozilla/5.0" }
78
 
79
- # 1. NSE F&O
80
- url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
81
- 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)
82
  df_nse['Exch'] = 'NSE'
83
-
84
- # 2. MCX F&O
85
- url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
86
- 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)
87
  df_mcx['Exch'] = 'MCX'
88
-
89
- # 3. BSE F&O
90
- url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
91
- 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)
92
  df_bse['Exch'] = 'BSE'
93
 
94
- SYSTEM_STATUS["message"] = "Processing Data..."
95
-
96
  df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
97
  df['Symbol'] = df['Symbol'].astype(str)
98
  df['Desc'] = df['Desc'].astype(str)
99
 
100
  temp_db = {}
101
-
102
  for _, row in df.iterrows():
103
- sym = row['Symbol']
104
- desc = row['Desc']
105
- exch = row['Exch']
106
  root = get_root_symbol(desc, sym)
107
 
108
  if root not in temp_db:
109
- spot_sym = ""
110
- if root in INDEX_MAP: spot_sym = INDEX_MAP[root]
111
- elif exch == "MCX": spot_sym = "MCX"
112
  elif exch == "BSE": spot_sym = f"BSE:{root}"
113
- else: spot_sym = f"NSE:{root}-EQ"
114
- temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
115
 
116
  try: exp = int(row['Expiry'])
117
  except: exp = 0
118
 
119
- strike = 0.0
120
- opt_type = "FUT"
121
-
122
  if "CE" in sym or "PE" in sym:
123
  parts = desc.strip().split(' ')
124
  if len(parts) >= 2:
125
  try:
126
- val = parts[-2]
127
- val_clean = val.replace('.', '', 1)
128
- if val_clean.isdigit(): strike = float(val)
129
  opt_type = parts[-1]
130
  except: pass
131
 
@@ -133,11 +100,9 @@ def update_master_db():
133
 
134
  final_db = {}
135
  search_list = []
136
-
137
  for root, data in temp_db.items():
138
- sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
139
- final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted_items }
140
- search_list.append({ "root": root, "exch": data["exch"], "display": f"{root}" })
141
 
142
  MASTER_DB = final_db
143
  SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
@@ -145,12 +110,8 @@ def update_master_db():
145
  with open(CACHE_FILE, "w") as f:
146
  json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
147
 
148
- SYSTEM_STATUS["ready"] = True
149
- SYSTEM_STATUS["message"] = "Ready"
150
- print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} symbols.")
151
-
152
  except Exception as e:
153
- print(f">>> [BG TASK] ERROR: {e}")
154
  SYSTEM_STATUS["message"] = f"Error: {str(e)}"
155
  finally:
156
  UPDATE_LOCK.release()
@@ -160,39 +121,89 @@ def startup_event():
160
  global MASTER_DB, SEARCH_INDEX
161
  if os.path.exists(CACHE_FILE):
162
  try:
163
- print(">>> STARTUP: Loading Cache...")
164
  with open(CACHE_FILE, "r") as f:
165
  data = json.load(f)
166
- MASTER_DB = data["db"]
167
- SEARCH_INDEX = data["idx"]
168
- SYSTEM_STATUS["ready"] = True
169
- SYSTEM_STATUS["message"] = "Ready (Cached)"
170
  except: pass
171
-
172
  if not SYSTEM_STATUS["ready"]:
173
- thread = threading.Thread(target=update_master_db)
174
- thread.start()
175
 
176
  # ==========================================
177
- # 3. ROUTES & API
178
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
 
 
 
180
  @app.get("/")
181
  def home(access_token: Optional[str] = Cookie(None)):
182
  if not access_token:
183
- state = "init"
184
- enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
185
- auth_url = (
186
- f"https://api-t1.fyers.in/api/v3/generate-authcode?"
187
- f"client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}"
188
- )
189
  return HTMLResponse(f"""
190
- <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;">
191
- <div style="background:white; padding:40px; border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.1); text-align:center;">
192
- <h2 style="margin-top:0;">Authentication Required</h2>
193
- <a href="{auth_url}" style="display:inline-block; background:#2962ff; color:white; padding:12px 25px; text-decoration:none; border-radius:5px; font-weight:bold; margin-top:10px;">LOGIN WITH FYERS</a>
194
  </div>
195
- </div>
196
  """)
197
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
198
 
@@ -201,21 +212,18 @@ def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
201
  final_code = auth_code if auth_code else code
202
  if not final_code: return JSONResponse({"error": "No Code Provided"})
203
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
204
- resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
205
- "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
206
- })
207
- data = resp.json()
208
- if "access_token" in data:
209
- full_token = f"{CLIENT_ID}:{data['access_token']}"
210
  response = RedirectResponse(url="/")
211
- response.set_cookie(key="access_token", value=full_token, httponly=True)
212
  return response
213
- else: return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(data, indent=2)}</pre>")
214
 
215
  @app.get("/logout")
216
  def logout():
217
  response = RedirectResponse(url="/")
218
  response.delete_cookie("access_token")
 
219
  return response
220
 
221
  @app.get("/api/status")
@@ -235,708 +243,251 @@ def get_chain(symbol: str):
235
  if root in MASTER_DB: return JSONResponse(MASTER_DB[root])
236
  return JSONResponse({"error": "Symbol not found"}, status_code=404)
237
 
238
- @app.get("/api/refresh")
239
- def force_refresh(background_tasks: BackgroundTasks):
240
- SYSTEM_STATUS["ready"] = False
241
- SYSTEM_STATUS["message"] = "Refreshing..."
242
- background_tasks.add_task(update_master_db)
243
- return JSONResponse({"s": "ok"})
244
-
245
- # --- HISTORICAL API ---
246
-
247
- def parse_bucket_data(bucket_rows):
248
- slots = [{"s": f"{i*10:02d}", "d": {}} for i in range(6)]
249
- for row in bucket_rows:
250
- data = row.get('data')
251
- if not data or not isinstance(data, dict): continue
252
- for symbol, metrics in data.items():
253
- p_arr = metrics.get('p', [])
254
- b_arr = metrics.get('b', [])
255
- s_arr = metrics.get('s', [])
256
- v_arr = metrics.get('v', [])
257
- o_arr = metrics.get('o', [])
258
- for i in range(6):
259
- try:
260
- ltp = p_arr[i] if i < len(p_arr) else None
261
- if ltp is None: continue
262
- slots[i]['d'][symbol] = {
263
- "ltp": ltp,
264
- "totalbuyqty": b_arr[i] if i < len(b_arr) else 0,
265
- "totalsellqty": s_arr[i] if i < len(s_arr) else 0,
266
- "v": v_arr[i] if i < len(v_arr) else 0,
267
- "oi": o_arr[i] if i < len(o_arr) else 0,
268
- "expiry": 0
269
- }
270
- except: continue
271
- return slots
272
-
273
- @app.get("/api/hist/dates")
274
- def get_hist_dates(symbol: str):
275
- try:
276
- res = supabase.table('history_buckets').select('minute').ilike('root', f"%{symbol}%").order('minute', desc=True).limit(2000).execute()
277
- dates = set()
278
- if res.data:
279
- for item in res.data:
280
- if item.get('minute'): dates.add(item.get('minute').split('T')[0].split(' ')[0])
281
- return JSONResponse(sorted(list(dates), reverse=True))
282
- except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
283
-
284
- @app.get("/api/hist/minutes")
285
- def get_hist_minutes(symbol: str, date: str):
286
- try:
287
- res = supabase.table('history_buckets').select('minute').ilike('root', f"%{symbol}%").gte('minute', f"{date}T00:00:00").lte('minute', f"{date}T23:59:59").order('minute', desc=True).limit(1000).execute()
288
- minutes = set()
289
- if res.data:
290
- for item in res.data:
291
- clean = item.get('minute').replace('T', ' ').split(' ')
292
- if len(clean)>1: minutes.add(clean[1][:5])
293
- return JSONResponse(sorted(list(minutes), reverse=True))
294
- except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
295
-
296
- @app.get("/api/hist/expiries")
297
- def get_hist_expiries(symbol: str, date: str, minute: str):
298
- try:
299
- q1 = f"{date} {minute}:00"
300
- q2 = f"{date}T{minute}:00"
301
- res = supabase.table('history_buckets').select('expiry').ilike('root', f"%{symbol}%").or_(f"minute.eq.{q1},minute.eq.{q2}").neq('expiry', 'MARKET').execute()
302
- expiries = set()
303
- if res.data:
304
- for item in res.data:
305
- if item.get('expiry') and item.get('expiry') != 'MARKET': expiries.add(item.get('expiry'))
306
- return JSONResponse(sorted(list(expiries)))
307
- except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
308
-
309
- @app.get("/api/hist/load")
310
- def get_hist_data(symbol: str, date: str, minute: str, expiry: Optional[str] = None):
311
  try:
312
- q1 = f"{date} {minute}:00"
313
- q2 = f"{date}T{minute}:00"
314
- res = supabase.table('history_buckets').select('data, expiry').ilike('root', f"%{symbol}%").or_(f"minute.eq.{q1},minute.eq.{q2}").execute()
315
- rows = res.data
316
- if not rows: return JSONResponse({"s": "ok", "timeline": []})
317
- filtered_rows = []
318
- for r in rows:
319
- e = r.get('expiry')
320
- if not expiry or e == 'MARKET' or e == expiry: filtered_rows.append(r)
321
- return JSONResponse({"s": "ok", "timeline": parse_bucket_data(filtered_rows)})
322
- except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
323
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
  # ==========================================
326
- # 4. FRONTEND
327
  # ==========================================
328
-
329
  HTML_TEMPLATE = """
330
  <!DOCTYPE html>
331
  <html lang="en">
332
  <head>
333
  <meta charset="UTF-8">
334
- <title>PRO CHAIN V3</title>
335
- <meta name="viewport" content="width=1100">
336
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
337
  <style>
338
  :root {
339
- --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
340
- --text-main: #1a1a1a; --text-sub: #555555;
341
- --accent: #2962ff; --green: #00c853; --red: #d50000;
342
- --green-soft: rgba(0, 200, 83, 0.27); --red-soft: rgba(213, 0, 0, 0.18);
343
- --atm-bg: rgba(41, 98, 255, 0.08);
344
- --badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff;
345
  }
346
  * { box-sizing: border-box; }
347
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
348
 
349
- .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); }
350
- .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); }
351
- .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
352
- .btn.active { background: var(--accent); color: white; border-color: var(--accent); }
353
- .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; }
354
- .icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
355
-
356
- .search-box { position: relative; width: 350px; margin-right: 20px; }
357
- .search-input { width: 100%; padding: 10px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; background: #f9f9f9; }
358
- .search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1); }
359
- .search-dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: white; border: 1px solid var(--border); max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 6px; z-index: 100; }
360
- .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
361
- .search-item:hover { background: #f0f7ff; color: var(--accent); }
362
- .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
363
- .badge-NSE { background: var(--badge-nse); } .badge-BSE { background: var(--badge-bse); } .badge-MCX { background: var(--badge-mcx); }
364
-
365
- /* HISTORY CONTROLS */
366
- .hist-controls { display: none; align-items: center; gap: 8px; margin-left: 20px; padding-left: 20px; border-left: 1px solid #eee; }
367
- .hist-sel { padding: 6px; border-radius: 4px; border: 1px solid #ddd; font-weight: bold; font-size: 11px; background: #f9f9f9; outline:none; }
368
- .sec-bar-cont { display: flex; gap: 2px; overflow-x: auto; max-width: 250px; padding: 2px; background: #f0f0f0; border-radius: 4px; }
369
- .sec-pill { padding: 3px 6px; font-size: 10px; cursor: pointer; border-radius: 2px; color: #666; font-family:'JetBrains Mono'; }
370
- .sec-pill:hover { background: #ddd; color: #000; }
371
- .sec-pill.active { background: var(--accent); color: white; font-weight: bold; }
372
- .sec-pill.bad { background: #ff5252; color: white; opacity: 0.8; }
373
- .sec-pill.bad:hover { background: #d50000; }
374
- .sec-pill.bad.active { background: #b71c1c; border: 1px solid #fff; opacity:1; }
375
-
376
- .play-btn { width:24px; height:24px; display:flex; align-items:center; justify-content:center; background:#eee; border-radius:4px; cursor:pointer; font-size:12px; }
377
- .play-btn:hover { background:#ddd; }
378
- .play-btn.active { background:var(--green); color:white; }
379
-
380
- .grid-layout { display: flex; height: calc(100vh - 60px); }
381
- .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
382
- .chain-header { display: grid; grid-template-columns: 1fr 1fr; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; }
383
- .header-cell { padding: 12px 5px; display: flex; align-items: center; justify-content: center; gap: 8px; position:relative; border-right: 1px solid var(--border); }
384
- .header-icon { opacity: 0.3; cursor: pointer; transition: 0.2s; }
385
- .header-icon:hover { opacity: 1; color: var(--accent); }
386
- .chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; }
387
 
388
- .row { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
389
- .row:hover { background: #fafafa; }
 
 
 
 
 
390
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
391
- .graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; }
392
- .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; }
393
- .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
394
- .delta-txt { z-index: 2; font-size: 10px; font-weight: 700; background: rgba(255,255,255,0.85); padding: 0 5px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-top: -2px; }
395
 
396
- .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
397
- .panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; }
398
- .panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; }
399
- .fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
400
- .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; }
401
 
402
- .netflow-card { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid var(--border); text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
403
- .nf-sec { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
404
- .nf-sec:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
405
- .nf-title { font-size: 10px; font-weight: 800; color: #888; text-transform: uppercase; margin-bottom: 8px; text-align: left; }
406
- .aggr-row { display: flex; width: 100%; border: 1px solid transparent; }
407
- .aggr-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
408
- .aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; }
409
- .aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; background: transparent; padding: 0; border-radius: 0; margin-top: 2px; display:flex; align-items:center; gap:3px; }
410
- .info-bubble { width:12px; height:12px; background:#eee; color:#666; border-radius:50%; font-size:8px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:'Inter'; font-weight:800; }
411
- .info-bubble:hover { background:var(--accent); color:white; }
412
-
413
- .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
414
- .modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
415
- @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
416
- .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
417
- .opt-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 6px; cursor: pointer; font-weight:600; font-size:12px; transition:0.2s; background:#f9f9f9; }
418
- .opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
419
- .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
420
- .inp-group { margin-top:15px; }
421
- .inp-label { font-size:11px; font-weight:800; color:#888; margin-bottom:5px; display:block; }
422
- .modal-inp { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; font-size:14px; }
423
- .modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; }
424
- .modal-action.sec { background:#eee; color:#333; margin-top:10px; }
425
-
426
- .info-table { width:100%; font-family:'JetBrains Mono'; font-size:10px; border-collapse: collapse; margin-top:10px; }
427
- .info-table th { text-align:left; color:#888; border-bottom:1px solid #eee; padding:5px; }
428
- .info-table td { padding:5px; border-bottom:1px solid #f9f9f9; color:#333; }
429
- .info-row-res { background: #f0f7ff; font-weight:800; }
430
- .preset-pill { display:inline-block; padding:4px 8px; background:#f0f0f0; border-radius:4px; font-size:10px; font-weight:700; margin-right:5px; cursor:pointer; color:#555; border:1px solid transparent; }
431
- .preset-pill:hover { border-color:#aaa; background:#e0e0e0; }
432
 
433
- .conf-list { max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px; background: #fafafa; }
434
- .conf-item { background: #fff; border-bottom: 1px solid #eee; padding: 10px; }
435
- .conf-item:last-child { border-bottom: none; }
436
- .conf-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
437
- .conf-body { margin-top: 10px; display: none; padding-top: 10px; border-top: 1px solid #f5f5f5; }
438
- .conf-item.expanded .conf-body { display: block; }
439
- .conf-item.expanded .conf-header { color: var(--accent); }
440
- .del-btn { color: var(--red); cursor: pointer; font-size: 10px; font-weight: bold; border: 1px solid var(--red-soft); padding: 2px 6px; border-radius: 4px; }
441
- .add-btn { width: 100%; border: 1px dashed #ccc; padding: 10px; text-align: center; font-size: 11px; font-weight: bold; color: #888; cursor: pointer; border-radius: 6px; margin-bottom: 15px; transition: 0.2s; background: #fff; }
442
- .add-btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }
443
-
444
- .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
445
- .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
446
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
447
- @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
448
- .live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; }
449
- .live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); }
450
- .live-dot.error { background: var(--red); }
451
 
452
- ::-webkit-scrollbar { width: 6px; height: 6px; }
453
- ::-webkit-scrollbar-track { background: transparent; }
454
- ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
455
- ::-webkit-scrollbar-thumb:hover { background: #bbb; }
 
 
456
 
457
- .code-box { background:#f4f4f4; padding:10px; border-radius:6px; font-family:'JetBrains Mono'; font-size:11px; color:#555; margin-top:5px; display:none; }
458
- .var-tag { color:var(--accent); font-weight:bold; }
 
 
 
 
 
459
  </style>
460
  </head>
461
  <body>
462
 
463
  <div class="loader-ov" id="loader">
464
  <div class="spinner"></div>
465
- <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div>
466
  </div>
467
 
468
  <div class="modal" id="expModal">
469
  <div class="modal-box">
470
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
471
  <span>SELECT EXPIRY</span>
472
- <span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span>
473
  </div>
474
  <div class="modal-grid" id="expGrid"></div>
475
  </div>
476
  </div>
477
 
478
- <div class="modal" id="infoModal">
479
- <div class="modal-box" style="width: 500px; max-width:95%;">
480
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
481
- <span>CALCULATION BREAKDOWN</span>
482
- <span style="cursor:pointer; color:#999;" onclick="closeModal('infoModal')">✕</span>
483
- </div>
484
- <div style="margin-top:15px; font-size:12px;">
485
- <div><strong>SNAPSHOT SPOT:</strong> <span id="infoSpot" style="font-family:'JetBrains Mono'; color:var(--accent);">0.00</span></div>
486
- <div style="margin-top:5px;"><strong>FORMULA:</strong> <span id="infoForm" style="font-family:'JetBrains Mono'; background:#f4f4f4; padding:2px 6px; border-radius:4px;"></span></div>
487
- </div>
488
- <div style="overflow-y:auto; max-height:400px; margin-top:10px;">
489
- <table class="info-table" id="infoTable"></table>
490
- </div>
491
- </div>
492
- </div>
493
-
494
- <div class="modal" id="confModal">
495
- <div class="modal-box">
496
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
497
- <span id="confTitle">COLUMN SETTINGS</span>
498
- <span style="cursor:pointer; color:#999;" onclick="closeModal('confModal')">✕</span>
499
- </div>
500
- <div class="inp-group"><span class="inp-label">DISPLAY TITLE</span><input type="text" id="confName" class="modal-inp"></div>
501
- <div class="inp-group"><span class="inp-label">CUSTOM FORMULA</span><input type="text" id="confForm" class="modal-inp"></div>
502
- <button class="modal-action" onclick="saveConfig()">SAVE CONFIGURATION</button>
503
- <button class="modal-action sec" onclick="resetConfig()">RESET TO DEFAULTS</button>
504
- </div>
505
- </div>
506
-
507
- <div class="modal" id="sideConfModal">
508
- <div class="modal-box">
509
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
510
- <span>SIDEBAR CONFIG</span>
511
- <span style="cursor:pointer; color:#999;" onclick="closeModal('sideConfModal')">✕</span>
512
- </div>
513
- <div style="margin-top:10px; font-size:11px; font-weight:700; color:#888; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">
514
- <span>CONFIGURE CARDS & RANGES</span>
515
- <span onclick="toggleVarHelp()" style="color:var(--accent); cursor:pointer; font-weight:800;">[?] VARIABLES</span>
516
- </div>
517
- <div class="code-box" id="sideVarHelp">
518
- <div>VARIABLES AVAILABLE:</div>
519
- <div style="margin-top:4px;">
520
- <span class="var-tag">CS</span> = Call Sell &nbsp; <span class="var-tag">CB</span> = Call Buy &nbsp; <span class="var-tag">CO</span> = Call OI &nbsp; <span class="var-tag">CLTP</span> = Call LTP &nbsp; <span class="var-tag">CV</span> = Call Vol<br>
521
- <span class="var-tag">PS</span> = Put Sell &nbsp;&nbsp; <span class="var-tag">PB</span> = Put Buy &nbsp; <span class="var-tag">PO</span> = Put OI &nbsp; <span class="var-tag">PLTP</span> = Put LTP &nbsp; <span class="var-tag">PV</span> = Put Vol
522
- </div>
523
- <div style="margin-top:4px; font-size:10px; color:#888;">Supports +, -, *, /, ( ) arithmetic.</div>
524
- </div>
525
- <div class="conf-list" id="sideListContainer"></div>
526
- <div class="add-btn" onclick="addSideItem()">+ ADD NEW AGGREGATE CARD</div>
527
- <button class="modal-action" onclick="saveSideConfig()">SAVE SETTINGS</button>
528
- <button class="modal-action sec" onclick="resetSideConfig()">RESET DEFAULTS</button>
529
- </div>
530
- </div>
531
-
532
  <div class="top-nav">
533
  <div style="display:flex; align-items:center;">
 
 
 
534
  <div class="search-box">
535
- <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY)..." autocomplete="off">
536
  <div class="search-dropdown" id="searchRes"></div>
537
  </div>
538
- <button class="btn" id="modeBtn" onclick="toggleMode()"><span id="modeTxt">LIVE</span></button>
539
-
540
- <div class="hist-controls" id="histControls">
541
- <select class="hist-sel" id="histDate" onchange="loadHistMinutes()"><option>DATE</option></select>
542
- <select class="hist-sel" id="histTime" onchange="loadHistExpiries()"><option>TIME</option></select>
543
- <div class="play-btn" id="btnPlay" onclick="togglePlay()" title="Play/Pause">▶</div>
544
- <select class="hist-sel" id="playSpeed" style="width:50px;">
545
- <option value="1000">1x</option>
546
- <option value="500">2x</option>
547
- <option value="200">5x</option>
548
- </select>
549
- <div class="play-btn" onclick="jumpHistory(-5)" title="-5 Minutes">«</div>
550
- <div class="play-btn" onclick="jumpHistory(5)" title="+5 Minutes">»</div>
551
- <div class="sec-bar-cont" id="secBar"></div>
552
- </div>
553
  </div>
554
 
555
  <div style="display:flex; gap:20px; align-items:center;">
556
- <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB"><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></button>
557
  <div style="text-align:right;">
558
- <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
559
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
560
  </div>
561
- <button class="btn" onclick="openExpModal()"><span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span><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></button>
562
- <div class="live-dot" id="statusDot" title="Connection Status"></div>
563
- <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;"><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></button>
 
564
  </div>
565
  </div>
566
 
567
- <div class="grid-layout">
568
- <div class="chain-panel">
569
- <div class="chain-header">
570
- <div class="header-cell"><span id="headL_txt">NET FLOW</span><svg class="header-icon" onclick="openConfig('left')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div>
571
- <div class="header-cell"><span id="headR_txt">OVERALL FLOW</span><svg class="header-icon" onclick="openConfig('right')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div>
572
- </div>
573
- <div class="chain-body" id="chainBody">
574
- <div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div>
575
- </div>
576
  </div>
577
- <div class="fut-panel">
578
- <div class="panel-section">
579
- <div class="panel-title"><span>AGGREGATE FLOW</span><div style="display:flex; align-items:center; gap:5px;"><svg onclick="openSideConfig()" style="cursor:pointer; opacity:0.5;" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div></div>
580
- <div class="netflow-card" id="sideContainer"><div style="padding:20px; color:#ccc; font-size:10px;">LOADING CONFIG...</div></div>
581
- </div>
582
- <div class="panel-section">
583
- <div class="panel-title">FUTURES CONTRACTS</div>
584
- <div id="futBody"></div>
585
- </div>
586
  </div>
587
  </div>
588
 
589
  <script>
590
  const AUTH_TOKEN = "{{USER_TOKEN}}";
591
- let CURRENT_ROOT="", CHAIN_DATA=null, ACTIVE_EXP=0, SPOT_SYM="", SPOT_PRICE=0, FUT_SYMS=[], OPT_SYMS=[];
592
- let POLLER=null, IS_POLLING=false, SEARCH_TIMER=null;
593
- let IS_HISTORY=false, CACHED_TIMELINE=[], HIST_EXPIRIES=[], SELECTED_HIST_EXP=null;
594
- let LATEST_ROW_DATA={};
595
- let PLAY_TIMER=null, IS_PLAYING=false, CUR_SEC_IDX=0;
596
-
597
- const DEFAULTS = {
598
- left: { name: "NET FLOW", formula: "PS - CS" },
599
- right: { name: "OVERALL FLOW", formula: "CB + PS - (PB + CS)" },
600
- side: [
601
- { id: 1, name: "NET FLOW", formula: "PS - CS", ranges: [7,10], _expanded: true },
602
- { id: 2, name: "OVERALL", formula: "CB + PS - (PB + CS)", ranges: [7,10], _expanded: false },
603
- { id: 3, name: "NET OI", formula: "PO - CO", ranges: [10], _expanded: false }
604
- ]
605
- };
606
- let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS));
607
- if(!Array.isArray(CONFIG.side)) CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side));
608
- let EDITING_COL = null;
609
-
610
  window.onload = () => {
611
  if(AUTH_TOKEN.indexOf("{{")!==-1){alert("Auth Error"); return;}
612
- applyConfig(); checkStatus();
613
  document.getElementById('searchInp').addEventListener('input', (e)=>{ clearTimeout(SEARCH_TIMER); SEARCH_TIMER=setTimeout(()=>doSearch(e.target.value),300); });
614
  document.addEventListener('click', (e)=>{ if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display='none'; });
 
615
  };
616
 
617
- function toggleMode() {
618
- IS_HISTORY = !IS_HISTORY;
619
- const btn = document.getElementById('modeBtn');
620
- const ctrls = document.getElementById('histControls');
621
- const dot = document.getElementById('statusDot');
622
 
623
- if(IS_HISTORY) {
624
- btn.classList.add('active'); document.getElementById('modeTxt').innerText="HISTORY";
625
- ctrls.style.display="flex";
626
- if(POLLER) clearInterval(POLLER);
627
- dot.style.background="#2962ff";
628
- if(CURRENT_ROOT) loadHistDates();
629
- } else {
630
- btn.classList.remove('active'); document.getElementById('modeTxt').innerText="LIVE";
631
- ctrls.style.display="none";
632
- dot.style.background="#ccc";
633
- pauseHistory();
634
- if(CURRENT_ROOT) loadSymbol(CURRENT_ROOT);
635
- }
636
- }
637
-
638
- async function loadHistDates() {
639
- if(!CURRENT_ROOT) return;
640
- const sel = document.getElementById('histDate'); sel.innerHTML="<option>LOADING...</option>";
641
- try {
642
- const res = await fetch(`/api/hist/dates?symbol=${CURRENT_ROOT}`);
643
- const dates = await res.json();
644
- sel.innerHTML = "<option value=''>SELECT DATE</option>" + dates.map(d=>`<option value="${d}">${d}</option>`).join('');
645
- } catch(e){ sel.innerHTML="<option>ERROR</option>"; }
646
- }
647
-
648
- async function loadHistMinutes() {
649
- const date = document.getElementById('histDate').value;
650
- if(!date) return;
651
- const sel = document.getElementById('histTime'); sel.innerHTML="<option>LOADING...</option>";
652
- HIST_EXPIRIES=[]; SELECTED_HIST_EXP=null; updateExpText(0);
653
- try {
654
- const res = await fetch(`/api/hist/minutes?symbol=${CURRENT_ROOT}&date=${date}`);
655
- const mins = await res.json();
656
- sel.innerHTML = "<option value=''>SELECT TIME</option>" + mins.map(m=>`<option value="${m}">${m}</option>`).join('');
657
- } catch(e){}
658
- }
659
-
660
- async function loadHistExpiries() {
661
- const date = document.getElementById('histDate').value;
662
- const time = document.getElementById('histTime').value;
663
- if(!date || !time) return;
664
- try {
665
- const res = await fetch(`/api/hist/expiries?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`);
666
- HIST_EXPIRIES = await res.json();
667
- if(HIST_EXPIRIES.length>0) { SELECTED_HIST_EXP=HIST_EXPIRIES[0]; }
668
- else { alert("Spot/Fut Only. No Options."); SELECTED_HIST_EXP=null; }
669
- updateHistExpUI(); loadHistData();
670
- } catch(e){}
671
- }
672
-
673
- function updateHistExpUI() { document.getElementById('selExpTxt').innerText = SELECTED_HIST_EXP || "NONE"; }
674
-
675
- async function loadHistData(bg=false) {
676
- const date = document.getElementById('histDate').value;
677
- const time = document.getElementById('histTime').value;
678
- if(!date || !time) return;
679
- if(!bg) { document.getElementById('loaderMsg').innerText="FETCHING SNAPSHOTS..."; document.getElementById('loader').style.display='flex'; }
680
 
681
- let url = `/api/hist/load?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`;
682
- if(SELECTED_HIST_EXP) url += `&expiry=${SELECTED_HIST_EXP}`;
683
-
684
- try {
685
- const res = await fetch(url);
686
- const json = await res.json();
687
- CACHED_TIMELINE = json.timeline;
688
- renderSecondBar();
689
-
690
- // Auto-select first green snapshot
691
- if(CACHED_TIMELINE.length>0) {
692
- let found=0;
693
- for(let i=0; i<CACHED_TIMELINE.length; i++) {
694
- if(validateSnapshot(CACHED_TIMELINE[i].d)) { found=i; break; }
695
- }
696
- CUR_SEC_IDX = found;
697
- selectSnapshot(found);
698
  }
699
- } catch(e){ console.error(e); }
700
- if(!bg) document.getElementById('loader').style.display='none';
701
- }
702
-
703
- function validateSnapshot(data) {
704
- // 1. Ref Price Check
705
- let ref = 0;
706
- if(SPOT_SYM && data[SPOT_SYM] && data[SPOT_SYM].ltp>0) ref = data[SPOT_SYM].ltp;
707
- if(ref===0 && FUT_SYMS.length>0) {
708
- for(let f of FUT_SYMS) if(data[f.s] && data[f.s].ltp>0) { ref = data[f.s].ltp; break; }
709
- }
710
- if(ref===0) return false;
711
 
712
- // 2. Strict ATM +/- 7 Logic (Red Seconds Check)
713
- if(SELECTED_HIST_EXP && CHAIN_DATA) {
714
- const expStr = SELECTED_HIST_EXP;
715
- const relevant = CHAIN_DATA.filter(x => {
716
- const dStr = new Date(x.e*1000).toISOString().split('T')[0];
717
- return dStr === expStr && (x.t==='CE' || x.t==='PE');
718
- });
719
- // Find ATM
720
- const strikes = [...new Set(relevant.map(x=>x.k))].sort((a,b)=>a-b);
721
- const atm = strikes.reduce((prev, curr) => Math.abs(curr - ref) < Math.abs(prev - ref) ? curr : prev);
722
- const atmIdx = strikes.indexOf(atm);
723
- const start = Math.max(0, atmIdx-7);
724
- const end = Math.min(strikes.length-1, atmIdx+7);
725
-
726
- // Map symbols to check
727
- for(let i=start; i<=end; i++) {
728
- const k = strikes[i];
729
- const ce = relevant.find(x => x.k===k && x.t==='CE');
730
- const pe = relevant.find(x => x.k===k && x.t==='PE');
731
-
732
- if(ce) {
733
- if(!data[ce.s] || ( (data[ce.s].totalbuyqty||0) + (data[ce.s].totalsellqty||0) === 0 )) return false;
734
- }
735
- if(pe) {
736
- if(!data[pe.s] || ( (data[pe.s].totalbuyqty||0) + (data[pe.s].totalsellqty||0) === 0 )) return false;
737
- }
738
- }
739
- }
740
- return true;
741
  }
742
 
743
- function renderSecondBar() {
744
- document.getElementById('secBar').innerHTML = CACHED_TIMELINE.map((item, idx) => {
745
- const isValid = validateSnapshot(item.d);
746
- return `<div class="sec-pill ${isValid?'':'bad'}" onclick="manualSelect(${idx})" id="pill-${idx}">:${item.s}</div>`;
747
- }).join('');
748
- }
749
-
750
- function manualSelect(idx) {
751
- CUR_SEC_IDX = idx;
752
- selectSnapshot(idx);
753
- }
754
-
755
- function selectSnapshot(idx) {
756
- document.querySelectorAll('.sec-pill').forEach(p => p.classList.remove('active'));
757
- const el = document.getElementById(`pill-${idx}`);
758
- if(el) el.classList.add('active');
759
- if(CACHED_TIMELINE[idx]) renderSnapshot(CACHED_TIMELINE[idx]);
760
- }
761
-
762
- function renderSnapshot(item) {
763
- const data = item.d;
764
- // Important: Update SPOT_PRICE Global for Info Modal
765
- let ref = 0;
766
- if(SPOT_SYM && data[SPOT_SYM]) ref = data[SPOT_SYM].ltp;
767
- else if(FUT_SYMS.length>0) for(let f of FUT_SYMS) if(data[f.s]) { ref=data[f.s].ltp; break; }
768
- SPOT_PRICE = ref; // Update global for Info Modal
769
-
770
- // Rebuild rows if expiry changed logic
771
- if(SELECTED_HIST_EXP && CHAIN_DATA) {
772
- const expStr = SELECTED_HIST_EXP;
773
- const relevant = CHAIN_DATA.filter(x => new Date(x.e*1000).toISOString().split('T')[0] === expStr && (x.t==='CE'||x.t==='PE'));
774
- if(relevant.length > 0) { OPT_SYMS = relevant; buildChain(); }
775
- }
776
- updateUI(data);
777
- }
778
-
779
- // PLAYBACK
780
- function togglePlay() {
781
- if(IS_PLAYING) pauseHistory();
782
- else playHistory();
783
- }
784
- function playHistory() {
785
- if(!IS_HISTORY) return;
786
- IS_PLAYING = true;
787
- document.getElementById('btnPlay').innerHTML = "❚❚";
788
- document.getElementById('btnPlay').classList.add('active');
789
- tickHistory();
790
- }
791
- function pauseHistory() {
792
- IS_PLAYING = false;
793
- clearTimeout(PLAY_TIMER);
794
- document.getElementById('btnPlay').innerHTML = "▶";
795
- document.getElementById('btnPlay').classList.remove('active');
796
- }
797
- async function tickHistory() {
798
- if(!IS_PLAYING) return;
799
- const spd = parseInt(document.getElementById('playSpeed').value);
800
-
801
- CUR_SEC_IDX++;
802
- if(CUR_SEC_IDX >= CACHED_TIMELINE.length) {
803
- // Move to next minute
804
- await jumpHistory(1);
805
- CUR_SEC_IDX = 0;
806
- } else {
807
- selectSnapshot(CUR_SEC_IDX);
808
  }
809
- PLAY_TIMER = setTimeout(tickHistory, spd);
810
  }
811
 
812
- async function jumpHistory(mins) {
813
- const dEl = document.getElementById('histDate');
814
- const tEl = document.getElementById('histTime');
815
- if(!dEl.value || !tEl.value) return;
816
-
817
- let currentDt = new Date(dEl.value + 'T' + tEl.value + ':00');
818
- currentDt.setMinutes(currentDt.getMinutes() + mins);
819
-
820
- const nextTime = currentDt.toTimeString().substring(0,5);
821
- // Check if exists in dropdown
822
- let exists = false;
823
- for(let i=0; i<tEl.options.length; i++) {
824
- if(tEl.options[i].value === nextTime) {
825
- tEl.selectedIndex = i;
826
- exists = true;
827
- break;
828
- }
829
- }
830
- if(exists) {
831
- await loadHistExpiries(); // Will trigger loadHistData
832
  } else {
833
- pauseHistory(); // Stop if end of data
 
 
834
  }
835
  }
836
 
837
- // --- STANDARD APP LOGIC ---
838
- function applyConfig() {
839
- document.getElementById('headL_txt').innerText = CONFIG.left.name;
840
- document.getElementById('headR_txt').innerText = CONFIG.right.name;
841
- renderSidebar();
842
- }
843
-
844
- function renderSidebar() {
845
- const con = document.getElementById('sideContainer');
846
- if(!CONFIG.side || CONFIG.side.length === 0) { con.innerHTML = '<div style="padding:10px; color:#aaa; font-size:10px;">NO METRICS</div>'; return; }
847
- con.innerHTML = CONFIG.side.map((item, idx) => {
848
- const boxes = item.ranges.map((r, rIdx) => `
849
- <div class="aggr-item"><span class="aggr-val" id="sv-${idx}-${rIdx}">0</span>
850
- <span class="aggr-lbl">ATM ±${r} <span class="info-bubble" onclick="showInfo(${idx}, ${rIdx})">i</span></span></div>
851
- `).join('');
852
- return `<div class="nf-sec"><div class="nf-title">${item.name}</div><div class="aggr-row">${boxes}</div></div>`;
853
- }).join('');
854
- }
855
-
856
- function openConfig(col) {
857
- EDITING_COL = col;
858
- const cfg = CONFIG[col];
859
- document.getElementById('confTitle').innerText = (col==='left'?"LEFT":"RIGHT")+" SETTINGS";
860
- document.getElementById('confName').value = cfg.name;
861
- document.getElementById('confForm').value = cfg.formula;
862
- document.getElementById('confModal').style.display = 'flex';
863
- }
864
- function saveConfig() {
865
- const name = document.getElementById('confName').value;
866
- const form = document.getElementById('confForm').value.toUpperCase();
867
- if(!name || !form) { alert("Empty fields"); return; }
868
- CONFIG[EDITING_COL] = { name, formula: form };
869
- saveAndReload(); closeModal('confModal');
870
- }
871
- function resetConfig() { if(confirm("Reset?")){CONFIG.left=DEFAULTS.left; CONFIG.right=DEFAULTS.right; saveAndReload(); closeModal('confModal');} }
872
-
873
- let TEMP_SIDE_CONFIG = [];
874
- function openSideConfig() { TEMP_SIDE_CONFIG = JSON.parse(JSON.stringify(CONFIG.side)); renderSideConfigList(); document.getElementById('sideConfModal').style.display = 'flex'; }
875
- function renderSideConfigList() {
876
- const list = document.getElementById('sideListContainer');
877
- list.innerHTML = TEMP_SIDE_CONFIG.map((item, idx) => `
878
- <div class="conf-item ${item._expanded?'expanded':''}">
879
- <div class="conf-header" onclick="toggleSideItem(${idx})">
880
- <span style="font-weight:700; font-size:12px;">${idx+1}. ${item.name}</span>
881
- <div style="display:flex; align-items:center; gap:10px;"><span style="font-size:10px; color:#aaa;">${item.ranges.length} RANGES</span><span class="del-btn" onclick="removeSideItem(event, ${idx})">DEL</span></div>
882
- </div>
883
- <div class="conf-body" onclick="event.stopPropagation()">
884
- <div class="inp-group" style="margin-top:0;"><span class="inp-label">TITLE</span><input type="text" class="modal-inp" value="${item.name}" onchange="updateSideItem(${idx}, 'name', this.value)"></div>
885
- <div class="inp-group"><span class="inp-label">FORMULA</span><input type="text" class="modal-inp" value="${item.formula}" onchange="updateSideItem(${idx}, 'formula', this.value)"></div>
886
- <div class="inp-group"><span class="inp-label">RANGES (Comma sep)</span><input type="text" class="modal-inp" value="${item.ranges.join(', ')}" onchange="updateSideItem(${idx}, 'ranges', this.value)"></div>
887
- </div>
888
- </div>`).join('');
889
- }
890
- function toggleSideItem(idx){ TEMP_SIDE_CONFIG[idx]._expanded = !TEMP_SIDE_CONFIG[idx]._expanded; renderSideConfigList(); }
891
- function toggleVarHelp(){ const b = document.getElementById('sideVarHelp'); b.style.display = (b.style.display==='none'||b.style.display==='')?'block':'none'; }
892
- function addSideItem(){ TEMP_SIDE_CONFIG.push({ id: Date.now(), name: "NEW", formula: "PS-CS", ranges: [10], _expanded: true }); renderSideConfigList(); }
893
- function removeSideItem(e, idx){ e.stopPropagation(); if(confirm("Del?")){ TEMP_SIDE_CONFIG.splice(idx, 1); renderSideConfigList(); } }
894
- function updateSideItem(idx, f, v){
895
- if(f==='ranges') TEMP_SIDE_CONFIG[idx][f] = v.split(',').map(s=>parseInt(s.trim())).filter(n=>!isNaN(n)&&n>0&&n<=50);
896
- else if(f==='formula') TEMP_SIDE_CONFIG[idx][f] = v.toUpperCase();
897
- else TEMP_SIDE_CONFIG[idx][f] = v;
898
- }
899
- function saveSideConfig(){ CONFIG.side = TEMP_SIDE_CONFIG.map(({_expanded,...r})=>r); saveAndReload(); closeModal('sideConfModal'); }
900
- function resetSideConfig(){ if(confirm("Reset Sidebar?")){ CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); saveAndReload(); closeModal('sideConfModal'); } }
901
- function saveAndReload(){ localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG)); applyConfig(); if(!IS_HISTORY) pollData(); }
902
- function closeModal(id){ document.getElementById(id).style.display='none'; }
903
-
904
- function evaluateFormula(formula, vars) {
905
- try {
906
- let exp = formula.replace(/CS/g,vars.CS||0).replace(/CB/g,vars.CB||0).replace(/PS/g,vars.PS||0).replace(/PB/g,vars.PB||0)
907
- .replace(/CO/g,vars.CO||0).replace(/PO/g,vars.PO||0).replace(/CLTP/g,vars.CLTP||0).replace(/PLTP/g,vars.PLTP||0)
908
- .replace(/CV/g,vars.CV||0).replace(/PV/g,vars.PV||0);
909
- if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
910
- return new Function('return '+exp)();
911
- } catch(e){ return 0; }
912
- }
913
-
914
- function showInfo(cardIdx, rangeIdx) {
915
- if(!CONFIG.side[cardIdx]) return;
916
- const cfg = CONFIG.side[cardIdx];
917
- const range = cfg.ranges[rangeIdx];
918
-
919
- document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
920
- document.getElementById('infoForm').innerText = cfg.formula;
921
-
922
- const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
923
- const lowers = kList.filter(k => k < SPOT_PRICE);
924
- const uppers = kList.filter(k => k >= SPOT_PRICE);
925
- const allSel = [...lowers.slice(-range), ...uppers.slice(0, range)].sort((a,b)=>a-b);
926
- let grandTotal = 0;
927
-
928
- document.getElementById('infoTable').innerHTML = `<tr><th style="width:50px;">STRIKE</th><th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th></tr>` +
929
- allSel.map(k => {
930
- const d = LATEST_ROW_DATA[k]||{};
931
- const val = evaluateFormula(cfg.formula, d);
932
- grandTotal += val;
933
- const style = (k >= SPOT_PRICE && lowers.slice(-range).includes(k-(kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
934
- return `<tr style="${style}"><td style="font-weight:bold;">${k}</td><td>${d.CS||0}</td><td>${d.CB||0}</td><td>${d.PS||0}</td><td>${d.PB||0}</td><td style="font-weight:bold; color:${val>=0?'var(--green)':'var(--red)'}">${val}</td></tr>`;
935
- }).join('') + `<tr class="info-row-res"><td colspan="5" style="text-align:right;">TOTAL</td><td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td></tr>`;
936
-
937
- document.getElementById('infoModal').style.display='flex';
938
  }
939
 
 
940
  async function checkStatus() {
941
  try {
942
  const res = await fetch('/api/status'); const s = await res.json();
@@ -945,176 +496,136 @@ HTML_TEMPLATE = """
945
  } catch(e){ setTimeout(checkStatus, 2000); }
946
  }
947
 
948
- async function refreshDB() {
949
- if(!confirm("Download fresh Master Data?")) return;
950
- document.getElementById('loaderMsg').innerText = "DOWNLOADING..."; document.getElementById('loader').style.display='flex';
951
- await fetch('/api/refresh'); checkStatus();
952
- }
953
-
954
  async function doSearch(q) {
955
  const d = document.getElementById('searchRes');
956
  if(q.length<2) { d.style.display='none'; return; }
957
- try {
958
- const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
959
- d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')"><span>${i.display}</span><span class="exch-badge badge-${i.exch}">${i.exch}</span></div>`).join('');
960
- d.style.display='block';
961
- } catch(e){}
962
  }
963
 
964
  async function loadSymbol(root) {
965
  document.getElementById('searchInp').value = root; document.getElementById('searchRes').style.display='none';
966
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; document.getElementById('loader').style.display='flex';
967
- document.getElementById('histDate').innerHTML="<option>DATE</option>"; document.getElementById('histTime').innerHTML="<option>TIME</option>";
968
- document.getElementById('secBar').innerHTML=""; HIST_EXPIRIES=[]; SELECTED_HIST_EXP=null;
969
 
970
- try {
971
- const r = await fetch(`/api/chain?symbol=${root}`); if(!r.ok) throw new Error("Symbol not found");
972
- const d = await r.json();
973
- CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot; SPOT_PRICE=0;
974
- FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b)=>a.e-b.e).slice(0,3);
975
- if(SPOT_SYM==="MCX" && FUT_SYMS.length>0) SPOT_SYM = FUT_SYMS[0].s;
976
- document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX')||SPOT_SYM.includes('-EQ')||SPOT_SYM.startsWith('BSE:'))?"SPOT PRICE":"FUT REF";
977
- setupExpiries(); document.getElementById('loader').style.display='none';
978
- if(IS_HISTORY) loadHistDates();
979
- } catch(e){ alert("Error: "+e.message); document.getElementById('loader').style.display='none'; }
980
- }
981
-
982
- function openExpModal() {
983
- if(IS_HISTORY) {
984
- if(HIST_EXPIRIES.length===0){alert("No History Expiries"); return;}
985
- document.getElementById('expGrid').innerHTML = HIST_EXPIRIES.map(e=>`<div class="opt-btn ${e===SELECTED_HIST_EXP?'active':''}" onclick="selectHistExp('${e}')">${e}</div>`).join('');
986
- document.getElementById('expModal').style.display='flex';
987
- } else {
988
- document.getElementById('expModal').style.display='flex';
989
- }
990
- }
991
- function selectHistExp(e) { SELECTED_HIST_EXP=e; updateHistExpUI(); closeModal('expModal'); loadHistData(); }
992
-
993
- function setupExpiries() {
994
- if(IS_HISTORY) return;
995
  const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
996
- const exps = [...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
997
  const today = Math.floor(Date.now()/1000)-86400;
998
- const valids = exps;
999
- if(valids.length===0){ document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
1000
- ACTIVE_EXP = valids.find(t=>t>today) || valids[0];
1001
- updateExpText(ACTIVE_EXP);
1002
- document.getElementById('expGrid').innerHTML = valids.map(ts=>`<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</div>`).join('');
1003
  buildChain();
 
 
 
 
 
 
 
 
 
1004
  }
1005
- function selectExp(ts) { ACTIVE_EXP=ts; updateExpText(ts); closeModal('expModal'); document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active')); buildChain(); }
1006
- function updateExpText(ts) { document.getElementById('selExpTxt').innerText = ts===0?"SELECT":new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'}); }
1007
 
1008
  function buildChain() {
1009
- let rel = IS_HISTORY ? OPT_SYMS : CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
1010
- if(!IS_HISTORY) OPT_SYMS = rel;
1011
- const ks = [...new Set(rel.map(x=>x.k))].sort((a,b)=>a-b);
1012
  if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
 
 
1013
  document.getElementById('chainBody').innerHTML = ks.map(k=>`
1014
  <div class="row" id="row-${k}" data-k="${k}">
1015
- <div class="graph-col"><div style="width:160px;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;">
1016
- <div class="visual-bar" id="barL-${k}"></div><span class="strike-txt">${k}</span><span class="delta-txt" id="valL-${k}">-</span>
1017
- </div></div>
1018
- <div class="graph-col"><div style="width:160px;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;">
1019
- <div class="visual-bar" id="barR-${k}"></div><span class="strike-txt">${k}</span><span class="delta-txt" id="valR-${k}">-</span>
1020
- </div></div>
 
1021
  </div>`).join('');
1022
- window.scrolled=false;
1023
- if(!IS_HISTORY) restartPoller();
1024
- }
1025
- function restartPoller() { if(POLLER) clearInterval(POLLER); POLLER=setInterval(pollData, 1500); pollData(); }
1026
-
1027
- async function pollData() {
1028
- if(IS_POLLING || !CURRENT_ROOT || IS_HISTORY) return;
1029
- IS_POLLING=true;
1030
- try {
1031
- let syms = [SPOT_SYM, ...FUT_SYMS.map(x=>x.s)];
1032
- const ks = Array.from(document.querySelectorAll('.row')).map(r=>parseFloat(r.dataset.k));
1033
- if(ks.length>0) {
1034
- const ref = SPOT_PRICE>0?SPOT_PRICE:ks[Math.floor(ks.length/2)];
1035
- let sk = ks.find(k=>k>=ref); if(sk===undefined) sk=ks[ks.length-1];
1036
- ATM_STRIKE=sk; const idx = ks.indexOf(sk);
1037
- document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
1038
- const r = document.getElementById(`row-${sk}`);
1039
- if(r) { r.classList.add('atm'); if(!window.scrolled && SPOT_PRICE>0){r.scrollIntoView({block:'center'}); window.scrolled=true;} }
1040
-
1041
- let maxR = 10;
1042
- if(CONFIG.side) CONFIG.side.forEach(i=>{if(i.ranges){const m=Math.max(...i.ranges); if(m>maxR) maxR=m;}});
1043
- const vKs = ks.slice(Math.max(0,idx-20), Math.min(ks.length,idx+20));
1044
- const cKs = ks.slice(Math.max(0,idx-maxR), Math.min(ks.length,idx+maxR+1));
1045
- const all = new Set([...vKs, ...cKs]);
1046
- OPT_SYMS.forEach(o=>{if(all.has(o.k)) syms.push(o.s)});
1047
- }
1048
- const u = [...new Set(syms)].filter(s=>s&&s.length>2);
1049
- if(u.length===0) return;
1050
-
1051
- const req = await fetch(`https://api-t1.fyers.in/data/depth?symbol=${u.join(',')}&ohlcv_flag=1`, {headers:{'Authorization':AUTH_TOKEN}});
1052
- if(req.status===401) { location.href='/logout'; return; }
1053
- const j = await req.json();
1054
- if(j.s==='ok') { updateUI(j.d); document.getElementById('statusDot').classList.add('active'); document.getElementById('statusDot').classList.remove('error'); }
1055
- } catch(e){ document.getElementById('statusDot').classList.remove('active'); document.getElementById('statusDot').classList.add('error'); }
1056
- finally { IS_POLLING=false; }
1057
- }
1058
-
1059
- function updateUI(data) {
1060
- if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp==='number') { SPOT_PRICE=data[SPOT_SYM].ltp; document.getElementById('spotPrice').innerText=SPOT_PRICE.toFixed(2); }
1061
- document.getElementById('futBody').innerHTML = FUT_SYMS.map(f=>{
1062
- const d=data[f.s]; if(!d) return '';
1063
- const t = (d.totalbuyqty+d.totalsellqty)||1; const bp = (d.totalbuyqty/t)*100;
1064
- return `<div class="fut-card"><div style="display:flex;justify-content:space-between;font-weight:700;"><span>${new Date(f.e*1000).toLocaleDateString('en-GB',{day:'numeric',month:'short'})} FUT</span><span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span></div><div class="meter-bg"><div style="width:${bp}%;background:var(--green)"></div><div style="width:${100-bp}%;background:var(--red)"></div></div><div style="display:flex;justify-content:space-between;font-size:10px;color:#666;margin-top:4px;"><span>B: ${fmt(d.totalbuyqty)}</span><span>S: ${fmt(d.totalsellqty)}</span></div></div>`;
1065
- }).join('');
1066
-
1067
- const rd = {};
1068
- for(const i of OPT_SYMS) {
1069
- const d=data[i.s]; if(!d) continue;
1070
- if(!rd[i.k]) rd[i.k]={CS:0,CB:0,PS:0,PB:0,CO:0,PO:0,CLTP:0,PLTP:0,CV:0,PV:0};
1071
- if(i.t==='CE'){ rd[i.k].CS=d.totalsellqty; rd[i.k].CB=d.totalbuyqty; rd[i.k].CO=d.oi; rd[i.k].CLTP=d.ltp; rd[i.k].CV=d.v; }
1072
- if(i.t==='PE'){ rd[i.k].PS=d.totalsellqty; rd[i.k].PB=d.totalbuyqty; rd[i.k].PO=d.oi; rd[i.k].PLTP=d.ltp; rd[i.k].PV=d.v; }
1073
  }
1074
- LATEST_ROW_DATA = rd;
1075
 
1076
- let mL=0, mR=0; const pR={};
1077
- for(const k in rd) {
1078
- pR[k] = { l: evaluateFormula(CONFIG.left.formula, rd[k]), r: evaluateFormula(CONFIG.right.formula, rd[k]) };
1079
- mL = Math.max(mL, Math.abs(pR[k].l)); mR = Math.max(mR, Math.abs(pR[k].r));
1080
- }
1081
- for(const k in pR) {
1082
- const r = pR[k];
1083
- const vl = document.getElementById(`valL-${k}`);
1084
- if(vl) { vl.innerText=(r.l>0?"+":"")+fmt(r.l); vl.style.color=r.l>=0?'var(--green)':'var(--red)';
1085
- const bl=document.getElementById(`barL-${k}`); if(mL>0){ bl.style.left='0%'; bl.style.width=(Math.abs(r.l)/mL)*100+'%'; bl.style.background=r.l>=0?'var(--green-soft)':'var(--red-soft)'; } }
1086
- const vr = document.getElementById(`valR-${k}`);
1087
- if(vr) { vr.innerText=(r.r>0?"+":"")+fmt(r.r); vr.style.color=r.r>=0?'var(--green)':'var(--red)';
1088
- const br=document.getElementById(`barR-${k}`); if(mR>0){ br.style.left='0%'; br.style.width=(Math.abs(r.r)/mR)*100+'%'; br.style.background=r.r>=0?'var(--green-soft)':'var(--red-soft)'; } }
1089
- }
1090
 
1091
- if(IS_HISTORY && SPOT_PRICE>0) {
1092
- const ks = Object.keys(rd).map(parseFloat).sort((a,b)=>a-b);
1093
- let sk = ks.find(k=>k>=SPOT_PRICE); if(sk===undefined) sk=ks[ks.length-1];
1094
- document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
1095
- const r = document.getElementById(`row-${sk}`); if(r) r.classList.add('atm');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  }
1097
 
1098
- if(SPOT_PRICE>0 && CONFIG.side) {
1099
- const ks = Object.keys(rd).map(parseFloat).sort((a,b)=>a-b);
1100
- const ls = ks.filter(k=>k<SPOT_PRICE); const us = ks.filter(k=>k>=SPOT_PRICE);
1101
- CONFIG.side.forEach((it,ix)=>{
1102
- it.ranges.forEach((rng,rx)=>{
1103
- const el=document.getElementById(`sv-${ix}-${rx}`);
1104
- if(el) {
1105
- const ts = [...ls.slice(-rng), ...us.slice(0,rng)];
1106
- let t=0; for(const k of ts) if(rd[k]) t+=evaluateFormula(it.formula, rd[k]);
1107
- el.innerText=(t>0?"+":"")+fmt(t); el.style.color=t>=0?'var(--green)':'var(--red)';
1108
- }
1109
- });
1110
- });
1111
  }
1112
  }
 
1113
  function fmt(n) {
1114
- if(!n) return '0'; let a=Math.abs(n);
1115
- if(a>=10000000) return (n/10000000).toFixed(2)+'Cr';
1116
- if(a>=100000) return (n/100000).toFixed(2)+'L';
1117
- if(a>=1000) return (n/1000).toFixed(1)+'k';
 
1118
  return n.toLocaleString();
1119
  }
1120
  </script>
 
1
+ # ==============================================================================
2
+ # PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION
3
+ # ==============================================================================
4
+
5
+ import os
6
  import time
7
+ import json
8
+ import hashlib
9
  import urllib.parse
 
10
  import threading
11
+ import asyncio
12
+ from collections import defaultdict
13
+ from typing import Optional, List
14
+
15
+ import pandas as pd
16
+ import requests
17
+ import uvicorn
18
+ from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
19
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fyers_apiv3.FyersWebsocket import data_ws
22
 
23
  # ==========================================
24
  # 1. CONFIGURATION
25
  # ==========================================
26
+ CLIENT_ID = "FV3BVDFQTO-100"
27
+ SECRET_KEY = "ZALDI5GCDF"
28
  REDIRECT_URI = "https://topsecrettraders-depthchainindia.hf.space/callback"
29
+ CACHE_FILE = "master_cache.json"
 
 
 
 
 
30
 
31
  app = FastAPI()
32
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
33
 
34
  # ==========================================
35
+ # 2. MASTER DB LOADER
36
  # ==========================================
 
 
 
 
37
  MASTER_DB = {}
38
+ SEARCH_INDEX =[]
39
+ SYSTEM_STATUS = {"ready": False, "message": "Booting..."}
 
 
 
 
40
  UPDATE_LOCK = threading.Lock()
41
 
 
42
  INDEX_MAP = {
43
+ "NIFTY": "NSE:NIFTY50-INDEX", "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
44
+ "FINNIFTY": "NSE:FINNIFTY-INDEX", "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",
45
+ "SENSEX": "BSE:SENSEX-INDEX", "BANKEX": "BSE:BANKEX-INDEX",
 
 
 
46
  "SENSEX50": "BSE:SENSEX50-INDEX"
47
  }
48
 
49
  def get_root_symbol(desc: str, symbol: str) -> str:
50
  try:
51
  parts = desc.strip().split(' ')
52
+ return parts[0].upper().replace(':', '').replace('-', '')
 
53
  except:
54
+ return symbol.split(':')[1] if ':' in symbol else symbol
 
 
55
 
56
  def update_master_db():
57
  global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
58
+ if not UPDATE_LOCK.acquire(blocking=False): return
 
 
59
 
60
  try:
61
  SYSTEM_STATUS["message"] = "Downloading Master Data..."
 
 
62
  headers = { "User-Agent": "Mozilla/5.0" }
63
 
64
+ df_nse = pd.read_csv("https://public.fyers.in/sym_details/NSE_FO.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
65
  df_nse['Exch'] = 'NSE'
66
+ df_mcx = pd.read_csv("https://public.fyers.in/sym_details/MCX_COM.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
 
67
  df_mcx['Exch'] = 'MCX'
68
+ df_bse = pd.read_csv("https://public.fyers.in/sym_details/BSE_FO.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
 
 
 
69
  df_bse['Exch'] = 'BSE'
70
 
 
 
71
  df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
72
  df['Symbol'] = df['Symbol'].astype(str)
73
  df['Desc'] = df['Desc'].astype(str)
74
 
75
  temp_db = {}
 
76
  for _, row in df.iterrows():
77
+ sym, desc, exch = row['Symbol'], row['Desc'], row['Exch']
 
 
78
  root = get_root_symbol(desc, sym)
79
 
80
  if root not in temp_db:
81
+ spot_sym = INDEX_MAP.get(root, f"NSE:{root}-EQ")
82
+ if exch == "MCX": spot_sym = "MCX"
 
83
  elif exch == "BSE": spot_sym = f"BSE:{root}"
84
+ temp_db[root] = { "spot": spot_sym, "exch": exch, "items":[] }
 
85
 
86
  try: exp = int(row['Expiry'])
87
  except: exp = 0
88
 
89
+ strike, opt_type = 0.0, "FUT"
 
 
90
  if "CE" in sym or "PE" in sym:
91
  parts = desc.strip().split(' ')
92
  if len(parts) >= 2:
93
  try:
94
+ val_clean = parts[-2].replace('.', '', 1)
95
+ if val_clean.isdigit(): strike = float(parts[-2])
 
96
  opt_type = parts[-1]
97
  except: pass
98
 
 
100
 
101
  final_db = {}
102
  search_list = []
 
103
  for root, data in temp_db.items():
104
+ final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted(data["items"], key=lambda x: (x['e'], x['k'])) }
105
+ search_list.append({ "root": root, "exch": data["exch"], "display": root })
 
106
 
107
  MASTER_DB = final_db
108
  SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
 
110
  with open(CACHE_FILE, "w") as f:
111
  json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
112
 
113
+ SYSTEM_STATUS.update({"ready": True, "message": "Ready"})
 
 
 
114
  except Exception as e:
 
115
  SYSTEM_STATUS["message"] = f"Error: {str(e)}"
116
  finally:
117
  UPDATE_LOCK.release()
 
121
  global MASTER_DB, SEARCH_INDEX
122
  if os.path.exists(CACHE_FILE):
123
  try:
 
124
  with open(CACHE_FILE, "r") as f:
125
  data = json.load(f)
126
+ MASTER_DB, SEARCH_INDEX = data["db"], data["idx"]
127
+ SYSTEM_STATUS.update({"ready": True, "message": "Ready (Cached)"})
 
 
128
  except: pass
 
129
  if not SYSTEM_STATUS["ready"]:
130
+ threading.Thread(target=update_master_db).start()
 
131
 
132
  # ==========================================
133
+ # 3. FYERS WEBSOCKET ENGINE (BACKEND)
134
  # ==========================================
135
+ STATE_MEMORY = defaultdict(lambda: {"ltp": 0, "b": 0, "s": 0, "v": 0})
136
+ FS = None
137
+ SOCKET_RUNNING = False
138
+ ACTIVE_SYMBOLS = set()
139
+ GLOBAL_TOKEN = None
140
+
141
+ def on_ws_message(msg):
142
+ ticks = msg if isinstance(msg, list) else[msg]
143
+ for t in ticks:
144
+ sym = t.get('symbol')
145
+ if sym:
146
+ if 'ltp' in t: STATE_MEMORY[sym]['ltp'] = t['ltp']
147
+ if 'tot_buy_qty' in t: STATE_MEMORY[sym]['b'] = t['tot_buy_qty']
148
+ if 'tot_sell_qty' in t: STATE_MEMORY[sym]['s'] = t['tot_sell_qty']
149
+ if 'vol_traded_today' in t: STATE_MEMORY[sym]['v'] = t['vol_traded_today']
150
+
151
+ def on_ws_error(e): print(f"[Fyers WS] Error: {e}")
152
+ def on_ws_close(c):
153
+ global SOCKET_RUNNING
154
+ SOCKET_RUNNING = False
155
+ print("[Fyers WS] Closed")
156
+ def on_ws_open():
157
+ global SOCKET_RUNNING, ACTIVE_SYMBOLS
158
+ SOCKET_RUNNING = True
159
+ print("[Fyers WS] Connected Successfully.")
160
+ if ACTIVE_SYMBOLS:
161
+ FS.subscribe(symbols=list(ACTIVE_SYMBOLS), data_type="SymbolUpdate")
162
+
163
+ def start_fyers_ws(token):
164
+ global FS, SOCKET_RUNNING, GLOBAL_TOKEN
165
+ GLOBAL_TOKEN = token
166
+ if FS and SOCKET_RUNNING: return
167
+
168
+ FS = data_ws.FyersDataSocket(
169
+ access_token=token, log_path="", litemode=False, write_to_file=False, reconnect=True,
170
+ on_connect=on_ws_open, on_close=on_ws_close, on_error=on_ws_error, on_message=on_ws_message
171
+ )
172
+ threading.Thread(target=FS.connect, daemon=True).start()
173
+
174
+ def stop_fyers_ws():
175
+ global FS, SOCKET_RUNNING
176
+ if FS and SOCKET_RUNNING:
177
+ FS.close_connection()
178
+ SOCKET_RUNNING = False
179
+
180
+ def update_subscriptions(symbols_list):
181
+ global ACTIVE_SYMBOLS, FS, SOCKET_RUNNING
182
+ new_set = set(symbols_list)
183
+ to_add = list(new_set - ACTIVE_SYMBOLS)
184
+ to_remove = list(ACTIVE_SYMBOLS - new_set)
185
+
186
+ ACTIVE_SYMBOLS = new_set
187
+ if SOCKET_RUNNING and FS:
188
+ if to_add:
189
+ for i in range(0, len(to_add), 50): FS.subscribe(symbols=to_add[i:i+50], data_type="SymbolUpdate")
190
+ if to_remove:
191
+ for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate")
192
 
193
+ # ==========================================
194
+ # 4. API & AUTH ROUTES
195
+ # ==========================================
196
  @app.get("/")
197
  def home(access_token: Optional[str] = Cookie(None)):
198
  if not access_token:
199
+ auth_url = f"https://api-t1.fyers.in/api/v3/generate-authcode?client_id={CLIENT_ID}&redirect_uri={urllib.parse.quote(REDIRECT_URI, safe='')}&response_type=code&state=init"
 
 
 
 
 
200
  return HTMLResponse(f"""
201
+ <body style="background:#0f172a; display:flex; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; color:white; margin:0;">
202
+ <div style="background:#1e293b; padding:40px; border-radius:12px; text-align:center; border:1px solid #334155;">
203
+ <h2 style="margin-top:0;">Auth Required</h2>
204
+ <a href="{auth_url}" style="display:inline-block; background:#3b82f6; color:white; padding:12px 25px; text-decoration:none; border-radius:6px; font-weight:bold;">LOGIN WITH FYERS</a>
205
  </div>
206
+ </body>
207
  """)
208
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
209
 
 
212
  final_code = auth_code if auth_code else code
213
  if not final_code: return JSONResponse({"error": "No Code Provided"})
214
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
215
+ resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={"grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code}).json()
216
+ if "access_token" in resp:
 
 
 
 
217
  response = RedirectResponse(url="/")
218
+ response.set_cookie(key="access_token", value=f"{CLIENT_ID}:{resp['access_token']}", httponly=True)
219
  return response
220
+ return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(resp, indent=2)}</pre>")
221
 
222
  @app.get("/logout")
223
  def logout():
224
  response = RedirectResponse(url="/")
225
  response.delete_cookie("access_token")
226
+ stop_fyers_ws()
227
  return response
228
 
229
  @app.get("/api/status")
 
243
  if root in MASTER_DB: return JSONResponse(MASTER_DB[root])
244
  return JSONResponse({"error": "Symbol not found"}, status_code=404)
245
 
246
+ # ==========================================
247
+ # 5. FASTAPI WEBSOCKET (STREAM TO BROWSER)
248
+ # ==========================================
249
+ @app.websocket("/ws")
250
+ async def websocket_endpoint(websocket: WebSocket):
251
+ await websocket.accept()
252
+ active_client_symbols =[]
253
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  try:
255
+ while True:
256
+ # 1. Check for incoming commands without blocking
257
+ try:
258
+ data = await asyncio.wait_for(websocket.receive_json(), timeout=0.5)
259
+ action = data.get("action")
260
+
261
+ if action == "init":
262
+ token = data.get("token")
263
+ if token: start_fyers_ws(token)
264
+ elif action == "subscribe":
265
+ active_client_symbols = data.get("symbols",[])
266
+ update_subscriptions(active_client_symbols)
267
+ elif action == "stop":
268
+ stop_fyers_ws()
269
+ elif action == "start":
270
+ if GLOBAL_TOKEN: start_fyers_ws(GLOBAL_TOKEN)
271
+ except asyncio.TimeoutError:
272
+ pass # Normal timeout, proceed to broadcast
273
+
274
+ # 2. Push State Snapshot to frontend
275
+ if SOCKET_RUNNING and active_client_symbols:
276
+ payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
277
+ if payload:
278
+ await websocket.send_json({"type": "data", "payload": payload})
279
+
280
+ except WebSocketDisconnect:
281
+ print("[Client WS] Disconnected")
282
 
283
  # ==========================================
284
+ # 6. FRONTEND (WORLD'S BEST UI)
285
  # ==========================================
 
286
  HTML_TEMPLATE = """
287
  <!DOCTYPE html>
288
  <html lang="en">
289
  <head>
290
  <meta charset="UTF-8">
291
+ <title>Pro Live Options Chain</title>
292
+ <meta name="viewport" content="width=1200">
293
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
294
  <style>
295
  :root {
296
+ --bg: #0f172a; --panel: #1e293b; --border: #334155;
297
+ --text-main: #f8fafc; --text-sub: #94a3b8;
298
+ --accent: #3b82f6; --green: #10b981; --red: #ef4444;
299
+ --buy-bg: rgba(16, 185, 129, 0.15); --sell-bg: rgba(239, 68, 68, 0.15);
300
+ --atm-bg: rgba(59, 130, 246, 0.15);
 
301
  }
302
  * { box-sizing: border-box; }
303
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
304
 
305
+ /* Top Navigation */
306
+ .top-nav { padding: 0 20px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 70px; z-index: 10; }
307
+ .btn { background: var(--panel); border: 1px solid var(--border); padding: 8px 16px; font-size: 13px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; color: var(--text-main); }
308
+ .btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(59, 130, 246, 0.1); }
309
+ .btn-play { background: var(--green); color: #fff; border:none; }
310
+ .btn-play.stopped { background: var(--red); }
311
+
312
+ .search-box { position: relative; width: 300px; margin-right: 20px; }
313
+ .search-input { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; background: #0f172a; color: white; transition:0.3s;}
314
+ .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); }
315
+ .search-dropdown { position: absolute; top: 110%; left: 0; width: 100%; background: var(--panel); border: 1px solid var(--border); max-height: 300px; overflow-y: auto; display: none; border-radius: 6px; z-index: 100; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
316
+ .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 700; color:var(--text-main); }
317
+ .search-item:hover { background: rgba(59, 130, 246, 0.1); color: var(--accent); }
318
+
319
+ /* Chain Layout */
320
+ .chain-container { flex: 1; display: flex; flex-direction: column; background: var(--bg); position:relative;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ .chain-header { display: grid; grid-template-columns: 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr 1.5fr; background: var(--panel); border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 1px; }
323
+ .header-cell { padding: 15px 5px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }
324
+ .header-cell.strike { background: #0b1120; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--accent); }
325
+
326
+ .chain-body { flex: 1; overflow-y: auto; scroll-behavior: smooth; position:relative; }
327
+ .row { display: grid; grid-template-columns: 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr 1.5fr; border-bottom: 1px solid var(--border); height: 46px; align-items: center; font-family: 'JetBrains Mono'; font-size: 13px; transition: background 0.2s;}
328
+ .row:hover { background: rgba(255,255,255,0.02); }
329
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
 
 
 
 
330
 
331
+ .cell { position: relative; height: 100%; display: flex; justify-content: center; align-items: center; border-right: 1px solid rgba(255,255,255,0.05); font-weight: 700;}
332
+ .cell.strike { background: #0b1120; font-size: 15px; font-weight: 800; color: white; border-right: 1px solid var(--border); border-left: 1px solid var(--border); }
 
 
 
333
 
334
+ .val-txt { z-index: 2; }
335
+ .val-buy { color: var(--green); }
336
+ .val-sell { color: var(--red); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
+ /* Depth Visualizer Bars */
339
+ .bar-bg { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 3px; opacity: 0.8; transition: width 0.3s ease; }
340
+ .bar-buy-left { right: 0; background: var(--buy-bg); }
341
+ .bar-sell-left { right: 0; background: var(--sell-bg); }
342
+ .bar-buy-right { left: 0; background: var(--buy-bg); }
343
+ .bar-sell-right { left: 0; background: var(--sell-bg); }
344
+
345
+ /* Modals & Overlays */
346
+ .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:var(--bg); display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
347
+ .spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
 
 
 
348
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
 
349
 
350
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.7); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(4px); }
351
+ .modal-box { background: var(--panel); width: 420px; border-radius: 12px; border: 1px solid var(--border); padding: 25px; display:flex; flex-direction:column; }
352
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
353
+ .opt-btn { padding: 12px; text-align: center; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight:700; font-size:13px; transition:0.2s; background:#0f172a; color:var(--text-sub); }
354
+ .opt-btn:hover { background: rgba(59, 130, 246, 0.1); color: var(--accent); border-color:var(--accent); }
355
+ .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
356
 
357
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
358
+ ::-webkit-scrollbar-track { background: var(--bg); }
359
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
360
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-sub); }
361
+
362
+ .live-dot { width:10px; height:10px; background:var(--red); border-radius:50%; margin-right:8px; box-shadow: 0 0 8px var(--red); }
363
+ .live-dot.active { background: var(--green); box-shadow: 0 0 10px var(--green); }
364
  </style>
365
  </head>
366
  <body>
367
 
368
  <div class="loader-ov" id="loader">
369
  <div class="spinner"></div>
370
+ <div id="loaderMsg" style="font-weight:800; color:var(--text-main); font-size:14px; letter-spacing:1px;">INITIALIZING...</div>
371
  </div>
372
 
373
  <div class="modal" id="expModal">
374
  <div class="modal-box">
375
+ <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:15px; color:white;">
376
  <span>SELECT EXPIRY</span>
377
+ <span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
378
  </div>
379
  <div class="modal-grid" id="expGrid"></div>
380
  </div>
381
  </div>
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  <div class="top-nav">
384
  <div style="display:flex; align-items:center;">
385
+ <div style="font-size:20px; font-weight:800; color:white; margin-right:30px; letter-spacing: -1px;">
386
+ <span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
387
+ </div>
388
  <div class="search-box">
389
+ <input type="text" class="search-input" id="searchInp" placeholder="Search (e.g. BANKNIFTY)..." autocomplete="off">
390
  <div class="search-dropdown" id="searchRes"></div>
391
  </div>
392
+ <button class="btn btn-play" id="wsBtn" onclick="toggleConnection()">
393
+ <div class="live-dot active" id="wsStatusDot"></div> <span id="wsBtnTxt">LIVE</span>
394
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
395
  </div>
396
 
397
  <div style="display:flex; gap:20px; align-items:center;">
 
398
  <div style="text-align:right;">
399
+ <div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px;" id="spotLabel">SPOT PRICE</div>
400
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--green);">0.00</div>
401
  </div>
402
+ <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
403
+ <span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:white">SELECT</span>
404
+ </button>
405
+ <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent;">LOGOUT</button>
406
  </div>
407
  </div>
408
 
409
+ <div class="chain-container">
410
+ <div class="chain-header">
411
+ <div class="header-cell">CALL BUY QTY</div>
412
+ <div class="header-cell">CALL SELL QTY</div>
413
+ <div class="header-cell">CALL LTP</div>
414
+ <div class="header-cell strike">STRIKE</div>
415
+ <div class="header-cell">PUT LTP</div>
416
+ <div class="header-cell">PUT SELL QTY</div>
417
+ <div class="header-cell">PUT BUY QTY</div>
418
  </div>
419
+ <div class="chain-body" id="chainBody">
420
+ <div style="padding:40px; text-align:center; color:var(--text-sub); font-weight:600; font-size:14px;">SEARCH & SELECT A SYMBOL TO LOAD LIVE CHAIN</div>
 
 
 
 
 
 
 
421
  </div>
422
  </div>
423
 
424
  <script>
425
  const AUTH_TOKEN = "{{USER_TOKEN}}";
426
+ let CURRENT_ROOT="", CHAIN_DATA=null, ACTIVE_EXP=0, SPOT_SYM="", SPOT_PRICE=0, OPT_SYMS=[];
427
+ let WS = null, IS_RUNNING = true, SEARCH_TIMER=null;
428
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  window.onload = () => {
430
  if(AUTH_TOKEN.indexOf("{{")!==-1){alert("Auth Error"); return;}
431
+ checkStatus();
432
  document.getElementById('searchInp').addEventListener('input', (e)=>{ clearTimeout(SEARCH_TIMER); SEARCH_TIMER=setTimeout(()=>doSearch(e.target.value),300); });
433
  document.addEventListener('click', (e)=>{ if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display='none'; });
434
+ connectWebSocket();
435
  };
436
 
437
+ // --- WEBSOCKET ENGINE ---
438
+ function connectWebSocket() {
439
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
440
+ WS = new WebSocket(`${protocol}//${window.location.host}/ws`);
 
441
 
442
+ WS.onopen = () => {
443
+ console.log("Browser WS Connected.");
444
+ WS.send(JSON.stringify({ action: "init", token: AUTH_TOKEN }));
445
+ if(OPT_SYMS.length > 0) subscribeCurrentChain();
446
+ updateConnectionUI(true);
447
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ WS.onmessage = (event) => {
450
+ const msg = JSON.parse(event.data);
451
+ if(msg.type === "data" && IS_RUNNING) {
452
+ renderLiveData(msg.payload);
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  }
454
+ };
 
 
 
 
 
 
 
 
 
 
 
455
 
456
+ WS.onclose = () => {
457
+ updateConnectionUI(false);
458
+ setTimeout(connectWebSocket, 2000); // Auto reconnect
459
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  }
461
 
462
+ function toggleConnection() {
463
+ IS_RUNNING = !IS_RUNNING;
464
+ if(WS && WS.readyState === WebSocket.OPEN) {
465
+ WS.send(JSON.stringify({ action: IS_RUNNING ? "start" : "stop" }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  }
467
+ updateConnectionUI(IS_RUNNING);
468
  }
469
 
470
+ function updateConnectionUI(active) {
471
+ const btn = document.getElementById('wsBtn');
472
+ const dot = document.getElementById('wsStatusDot');
473
+ if(active) {
474
+ btn.classList.remove('stopped');
475
+ dot.classList.add('active');
476
+ document.getElementById('wsBtnTxt').innerText = "LIVE (RUNNING)";
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  } else {
478
+ btn.classList.add('stopped');
479
+ dot.classList.remove('active');
480
+ document.getElementById('wsBtnTxt').innerText = "STOPPED";
481
  }
482
  }
483
 
484
+ function subscribeCurrentChain() {
485
+ if(!WS || WS.readyState !== WebSocket.OPEN) return;
486
+ const syms = [SPOT_SYM, ...OPT_SYMS.map(x=>x.s)];
487
+ WS.send(JSON.stringify({ action: "subscribe", symbols: syms }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
 
490
+ // --- DATA FETCHING & UI ---
491
  async function checkStatus() {
492
  try {
493
  const res = await fetch('/api/status'); const s = await res.json();
 
496
  } catch(e){ setTimeout(checkStatus, 2000); }
497
  }
498
 
 
 
 
 
 
 
499
  async function doSearch(q) {
500
  const d = document.getElementById('searchRes');
501
  if(q.length<2) { d.style.display='none'; return; }
502
+ const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
503
+ d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span style="font-size:10px; color:var(--accent); float:right;">${i.exch}</span></div>`).join('');
504
+ d.style.display='block';
 
 
505
  }
506
 
507
  async function loadSymbol(root) {
508
  document.getElementById('searchInp').value = root; document.getElementById('searchRes').style.display='none';
509
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; document.getElementById('loader').style.display='flex';
 
 
510
 
511
+ const r = await fetch(`/api/chain?symbol=${root}`);
512
+ if(!r.ok) { alert("Symbol not found"); document.getElementById('loader').style.display='none'; return;}
513
+
514
+ const d = await r.json();
515
+ CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot; SPOT_PRICE=0;
516
+
517
+ document.getElementById('spotLabel').innerText = SPOT_SYM.includes('INDEX') ? "SPOT PRICE" : "FUT PRICE";
518
+ document.getElementById('spotPrice').innerText = "0.00";
519
+ document.getElementById('spotPrice').style.color = "var(--text-main)";
520
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
522
+ const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
523
  const today = Math.floor(Date.now()/1000)-86400;
524
+ ACTIVE_EXP = exps.find(t=>t>today) || exps[0];
525
+
526
+ document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
527
+ document.getElementById('expGrid').innerHTML = exps.map(ts=>`<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</div>`).join('');
528
+
529
  buildChain();
530
+ document.getElementById('loader').style.display='none';
531
+ }
532
+
533
+ function selectExp(ts) {
534
+ ACTIVE_EXP=ts;
535
+ document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
536
+ document.getElementById('expModal').style.display='none';
537
+ document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active'));
538
+ buildChain();
539
  }
 
 
540
 
541
  function buildChain() {
542
+ OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
543
+ const ks = [...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);
 
544
  if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
545
+
546
+ // Generate pure HTML matrix
547
  document.getElementById('chainBody').innerHTML = ks.map(k=>`
548
  <div class="row" id="row-${k}" data-k="${k}">
549
+ <div class="cell"><div class="bar-bg bar-buy-left" id="cb-bar-${k}"></div><span class="val-txt val-buy" id="cb-${k}">0</span></div>
550
+ <div class="cell"><div class="bar-bg bar-sell-left" id="cs-bar-${k}"></div><span class="val-txt val-sell" id="cs-${k}">0</span></div>
551
+ <div class="cell"><span class="val-txt" id="cltp-${k}" style="color:var(--text-main)">0.00</span></div>
552
+ <div class="cell strike">${k}</div>
553
+ <div class="cell"><span class="val-txt" id="pltp-${k}" style="color:var(--text-main)">0.00</span></div>
554
+ <div class="cell"><div class="bar-bg bar-sell-right" id="ps-bar-${k}"></div><span class="val-txt val-sell" id="ps-${k}">0</span></div>
555
+ <div class="cell"><div class="bar-bg bar-buy-right" id="pb-bar-${k}"></div><span class="val-txt val-buy" id="pb-${k}">0</span></div>
556
  </div>`).join('');
557
+
558
+ window.scrolled = false;
559
+ subscribeCurrentChain();
560
+ }
561
+
562
+ // --- REALTIME RENDER ENGINE ---
563
+ function renderLiveData(data) {
564
+ if(data[SPOT_SYM] && data[SPOT_SYM].ltp > 0) {
565
+ const newSpot = data[SPOT_SYM].ltp;
566
+ const spotEl = document.getElementById('spotPrice');
567
+ if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
568
+ else if(newSpot < SPOT_PRICE) spotEl.style.color = "var(--red)";
569
+ SPOT_PRICE = newSpot;
570
+ spotEl.innerText = SPOT_PRICE.toFixed(2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
 
572
 
573
+ let maxQty = 1; // Base for bar percentage
574
+ const rowUpdates = {};
 
 
 
 
 
 
 
 
 
 
 
 
575
 
576
+ // 1. Process Raw Data
577
+ OPT_SYMS.forEach(opt => {
578
+ const d = data[opt.s];
579
+ if(!d) return;
580
+ if(!rowUpdates[opt.k]) rowUpdates[opt.k] = { cb:0, cs:0, cltp:0, pb:0, ps:0, pltp:0 };
581
+
582
+ if(opt.t === 'CE') {
583
+ rowUpdates[opt.k].cb = d.b; rowUpdates[opt.k].cs = d.s; rowUpdates[opt.k].cltp = d.ltp;
584
+ maxQty = Math.max(maxQty, d.b, d.s);
585
+ } else if(opt.t === 'PE') {
586
+ rowUpdates[opt.k].pb = d.b; rowUpdates[opt.k].ps = d.s; rowUpdates[opt.k].pltp = d.ltp;
587
+ maxQty = Math.max(maxQty, d.b, d.s);
588
+ }
589
+ });
590
+
591
+ // 2. Fast DOM Update
592
+ for(const [k, v] of Object.entries(rowUpdates)) {
593
+ // Update Texts
594
+ if(v.cb) document.getElementById(`cb-${k}`).innerText = fmt(v.cb);
595
+ if(v.cs) document.getElementById(`cs-${k}`).innerText = fmt(v.cs);
596
+ if(v.cltp) document.getElementById(`cltp-${k}`).innerText = v.cltp.toFixed(2);
597
+ if(v.pb) document.getElementById(`pb-${k}`).innerText = fmt(v.pb);
598
+ if(v.ps) document.getElementById(`ps-${k}`).innerText = fmt(v.ps);
599
+ if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);
600
+
601
+ // Update Visual Depth Bars
602
+ if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / maxQty) * 100}%`;
603
+ if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / maxQty) * 100}%`;
604
+ if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / maxQty) * 100}%`;
605
+ if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / maxQty) * 100}%`;
606
  }
607
 
608
+ // 3. Highlight ATM & Auto-Scroll
609
+ if(SPOT_PRICE > 0) {
610
+ const ks = Object.keys(rowUpdates).map(Number).sort((a,b)=>a-b);
611
+ if(ks.length > 0) {
612
+ const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
613
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
614
+ const atmRow = document.getElementById(`row-${atmK}`);
615
+ if(atmRow) {
616
+ atmRow.classList.add('atm');
617
+ if(!window.scrolled) { atmRow.scrollIntoView({block:'center', behavior:'smooth'}); window.scrolled = true; }
618
+ }
619
+ }
 
620
  }
621
  }
622
+
623
  function fmt(n) {
624
+ if(!n) return '-';
625
+ let a = Math.abs(n);
626
+ if(a >= 10000000) return (n/10000000).toFixed(2)+'Cr';
627
+ if(a >= 100000) return (n/100000).toFixed(2)+'L';
628
+ if(a >= 1000) return (n/1000).toFixed(1)+'k';
629
  return n.toLocaleString();
630
  }
631
  </script>