Files changed (1) hide show
  1. app.py +380 -1002
app.py CHANGED
@@ -1,177 +1,146 @@
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
 
132
- temp_db[root]["items"].append({ "s": sym, "e": exp, "k": strike, "t": opt_type })
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'])
144
-
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()
157
 
158
  @app.on_event("startup")
159
- 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
@@ -182,940 +151,349 @@ 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
 
199
  @app.get("/callback")
200
- 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")
222
- def get_status(): return JSONResponse(SYSTEM_STATUS)
223
-
224
- @app.get("/api/search")
225
- def search_symbol(q: str = Query(..., min_length=1)):
226
- if not SYSTEM_STATUS["ready"]: return JSONResponse([])
227
- query = q.upper()
228
- results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
229
- if len(results) < 10: results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
230
- return JSONResponse(results[:20])
231
-
232
- @app.get("/api/chain")
233
- def get_chain(symbol: str):
234
- root = symbol.upper()
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();
943
- if(s.ready) { document.getElementById('loader').style.display='none'; if(!CURRENT_ROOT) loadSymbol('NIFTY'); }
944
- else { document.getElementById('loaderMsg').innerText = s.message.toUpperCase(); setTimeout(checkStatus, 2000); }
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>
1121
  </body>
 
 
1
  import requests
2
  import uvicorn
3
  import hashlib
4
  import json
5
  import time
 
 
6
  import threading
7
+ import urllib.parse
8
+ from datetime import datetime, timedelta
9
+ from fastapi import FastAPI, Request, Response, Cookie
10
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
11
+ from typing import Optional
12
 
13
  # ==========================================
14
  # 1. CONFIGURATION
15
  # ==========================================
16
+ CLIENT_ID = "2MHHCWN2JT-100"
17
+ SECRET_KEY = "ZR1795RRUO"
18
+ REDIRECT_URI = "https://topindianhacker-depthchainindia.hf.space/callback"
 
 
 
 
 
 
19
 
20
  app = FastAPI()
21
 
22
  # ==========================================
23
+ # 2. ROBUST DATA ENGINE
24
  # ==========================================
25
 
26
+ SYSTEM_STATUS = { "ready": False, "msg": "Booting..." }
27
+ DB = { "index": [], "stocks": [] }
 
 
 
 
 
 
 
 
28
  UPDATE_LOCK = threading.Lock()
29
 
30
+ # BACKUP DATA (Used if NSE blocks the connection)
31
+ BACKUP_NIFTY_50 = {
32
+ "HDFCBANK": 13.52, "RELIANCE": 9.81, "ICICIBANK": 7.89, "INFY": 5.80, "ITC": 4.41,
33
+ "TCS": 4.02, "L&T": 3.66, "AXISBANK": 3.23, "KOTAKBANK": 2.92, "SBIN": 2.89,
34
+ "BHARTIARTL": 2.68, "BAJFINANCE": 2.30, "ASIANPAINT": 1.76, "MARUTI": 1.57,
35
+ "HCLTECH": 1.54, "TITAN": 1.48, "SUNPHARMA": 1.40, "TATASTEEL": 1.34, "NTPC": 1.18,
36
+ "ULTRACEMO": 1.11, "TATAMOTORS": 1.09, "POWERGRID": 1.04, "INDUSINDBK": 0.98,
37
+ "BAJAJFINSV": 0.86, "HINDUNILVR": 2.50, "NESTLEIND": 0.91, "GRASIM": 0.83,
38
+ "JSWSTEEL": 0.79, "TECHM": 0.78, "ADANIENT": 0.75, "HINDALCO": 0.72, "CIPLA": 0.69,
39
+ "DRREDDY": 0.65, "WIPRO": 0.64, "SBILIFE": 0.63, "COALINDIA": 0.60, "TATACONSUM": 0.59,
40
+ "HDFCLIFE": 0.58, "ADANIPORTS": 0.57, "BAJAJ-AUTO": 0.56, "APOLLOHOSP": 0.55,
41
+ "EICHERMOT": 0.52, "DIVISLAB": 0.47, "BPCL": 0.44, "ONGC": 0.42, "HEROMOTOCO": 0.38,
42
+ "BRITANNIA": 0.36, "UPL": 0.35, "LTIM": 0.35, "M&M": 2.6
43
  }
44
 
45
+ def fetch_nse_weights():
46
+ """Tries to get live weights, returns fallback if fails"""
47
  try:
48
+ headers = {
49
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
50
+ 'Referer': 'https://www.nseindia.com/'
51
+ }
52
+ s = requests.Session()
53
+ s.get("https://www.nseindia.com", headers=headers, timeout=3) # Fast timeout
54
+ r = s.get("https://www.nseindia.com/api/equity-stockIndices?index=NIFTY 50", headers=headers, timeout=5)
55
+
56
+ if r.status_code == 200:
57
+ data = r.json().get('data', [])
58
+ stocks = [x for x in data if x['priority'] == 0]
59
+ tot = sum(x.get('ffmc', 0) for x in stocks)
60
+ if tot > 0:
61
+ return {x['symbol']: (x.get('ffmc', 0)/tot)*100 for x in stocks}
62
+ except Exception as e:
63
+ print(f"NSE Fetch Error (Using Backup): {e}")
64
+ return BACKUP_NIFTY_50
65
+
66
+ def get_expiry_tag(offset=0):
67
+ """
68
+ Generates '26FEB' style tag.
69
+ Logic: If today > 26th, we assume the 'current' month is actually next month.
70
+ """
71
+ now = datetime.now()
72
+ # Move to the target month based on offset
73
+ # If today is late in the month (e.g. 30th), 'Current' expiry is likely next month
74
+ if now.day > 26:
75
+ offset += 1
76
+
77
+ # Calculate Year and Month safely without dateutil
78
+ year = now.year
79
+ month = now.month + offset
80
+
81
+ # Handle year rollover
82
+ while month > 12:
83
+ month -= 12
84
+ year += 1
85
+
86
+ # Create date object to get Month Name
87
+ d = datetime(year, month, 1)
88
+
89
+ # Format: 26FEB (YYMON)
90
+ yy = d.strftime("%y")
91
+ mon = d.strftime("%b").upper()
92
+ return f"{yy}{mon}"
93
+
94
+ def update_db():
95
+ global SYSTEM_STATUS, DB
96
+ if not UPDATE_LOCK.acquire(blocking=False): return
97
 
 
 
 
98
  try:
99
+ SYSTEM_STATUS["msg"] = "Updating Data..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ # 1. Get Weights (Live or Backup)
102
+ weights = fetch_nse_weights()
103
+ roots = list(weights.keys())
104
 
105
+ # 2. Construct Stocks
106
+ final_stocks = []
107
+ for root in roots:
108
+ futs = []
109
+ for i in range(3): # Current, Next, Far
110
+ tag = get_expiry_tag(i)
111
+ # Correctly format M&M to match Fyers (NSE:M&M...)
112
+ futs.append({"s": f"NSE:{root}{tag}FUT"})
113
+
114
+ final_stocks.append({
115
+ "r": root,
116
+ "w": weights.get(root, 0),
117
+ "f": futs
118
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ # 3. Construct Index
121
+ idx_futs = []
122
+ for i in range(3):
123
+ tag = get_expiry_tag(i)
124
+ idx_futs.append({"s": f"NSE:NIFTY{tag}FUT"})
 
 
 
 
125
 
126
+ # 4. Sort by Weightage
127
+ final_stocks.sort(key=lambda x: x['w'], reverse=True)
 
 
 
128
 
129
+ DB = { "index": idx_futs, "stocks": final_stocks }
130
  SYSTEM_STATUS["ready"] = True
131
+ SYSTEM_STATUS["msg"] = "Ready"
132
+ print(f"DB Updated: {len(final_stocks)} Stocks Loaded")
133
+
134
  except Exception as e:
135
+ print(f"DB Error: {e}")
136
+ SYSTEM_STATUS["msg"] = "DB Error"
137
  finally:
138
  UPDATE_LOCK.release()
139
 
140
  @app.on_event("startup")
141
+ def startup():
142
+ t = threading.Thread(target=update_db)
143
+ t.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  # ==========================================
146
  # 3. ROUTES & API
 
151
  if not access_token:
152
  state = "init"
153
  enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
154
+ auth_url = (f"https://api-t1.fyers.in/api/v3/generate-authcode?client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}")
 
 
 
155
  return HTMLResponse(f"""
156
+ <body style="background:#050505; color:#fff; display:flex; height:100vh; justify-content:center; align-items:center; font-family:sans-serif;">
157
+ <div style="text-align:center; border:1px solid #333; padding:40px; border-radius:10px; background:#111;">
158
+ <h2 style="color:#2962ff; letter-spacing:1px; margin-bottom:20px;">NIFTY 50 PRO</h2>
159
+ <a href="{auth_url}" style="padding:12px 30px; background:#2962ff; color:white; text-decoration:none; border-radius:6px; font-weight:bold;">LOGIN WITH FYERS</a>
160
  </div>
161
+ </body>""")
162
+ return HTMLResponse(FRONTEND.replace("{{USER_TOKEN}}", access_token))
 
163
 
164
  @app.get("/callback")
165
+ def callback(auth_code: Optional[str]=None, code: Optional[str]=None):
166
+ c = auth_code or code
167
+ h = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
168
+ r = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={"grant_type":"authorization_code","appIdHash":h,"code":c})
169
+ if r.status_code==200:
170
+ resp = RedirectResponse("/")
171
+ resp.set_cookie("access_token", f"{CLIENT_ID}:{r.json()['access_token']}", httponly=True)
172
+ return resp
173
+ return JSONResponse(r.json())
 
 
 
 
 
174
 
175
  @app.get("/logout")
176
  def logout():
177
+ r = RedirectResponse("/")
178
+ r.delete_cookie("access_token")
179
+ return r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ @app.get("/api/config")
182
+ def get_conf(expiry: int = 0):
183
+ """Safely returns config without crashing"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  try:
185
+ if not SYSTEM_STATUS["ready"] or not DB["index"]:
186
+ # Trigger update if empty
187
+ if not UPDATE_LOCK.locked(): threading.Thread(target=update_db).start()
188
+ return JSONResponse({"s":"wait", "msg": SYSTEM_STATUS["msg"]})
189
+
190
+ idx = max(0, min(expiry, 2))
191
+
192
+ stk_list = []
193
+ for s in DB["stocks"]:
194
+ # Safe Access
195
+ sym = s["f"][idx]["s"] if len(s["f"]) > idx else s["f"][0]["s"]
196
+ stk_list.append({"r": s["r"], "w": s["w"], "s": sym})
197
+
198
+ idx_sym = DB["index"][idx]["s"] if len(DB["index"]) > idx else DB["index"][0]["s"]
199
+
200
+ return JSONResponse({"s": "ok", "idx": idx_sym, "stocks": stk_list})
201
+ except Exception as e:
202
+ return JSONResponse({"s": "error", "msg": str(e)})
203
 
204
  # ==========================================
205
+ # 4. FRONTEND (V6 Compact)
206
  # ==========================================
207
 
208
+ FRONTEND = """
209
  <!DOCTYPE html>
210
  <html lang="en">
211
  <head>
212
  <meta charset="UTF-8">
213
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
214
+ <title>NIFTY 50 PRO</title>
215
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
216
  <style>
217
+ :root {
218
+ --bg: #050505;
219
+ --panel: #0f0f0f;
220
+ --border: #222;
221
+ --accent: #2962ff;
222
+ --text: #e0e0e0;
223
+ --green: #00c853;
224
+ --red: #d50000;
225
+ --card-bg: #121212;
226
+ --bull-bg: rgba(0, 200, 83, 0.1);
227
+ --bear-bg: rgba(213, 0, 0, 0.1);
228
  }
229
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
230
+ body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
231
+
232
+ .head { height: 45px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 15px; flex-shrink: 0; }
233
+ .logo { font-weight: 800; font-size: 14px; letter-spacing: 0.5px; } .logo span { color: var(--accent); }
234
+ .ctrls { display: flex; gap: 8px; }
235
+ .btn { background: #1a1a1a; color: #ccc; border: 1px solid #333; padding: 4px 10px; border-radius: 4px; font-size: 10px; font-weight: 700; cursor: pointer; font-family: 'Inter'; transition: 0.2s; }
236
+ .btn:hover { border-color: var(--accent); color: #fff; background: #252525; }
237
+ .btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ .layout { display: flex; flex: 1; overflow: hidden; }
240
+ .side { width: 200px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
241
+ .side-head { padding: 12px; font-size: 10px; font-weight: 800; color: #666; border-bottom: 1px solid var(--border); letter-spacing: 1px; }
242
+ .agg-list { flex: 1; overflow-y: auto; padding: 10px; }
243
+ .agg-card { background: #181818; border-radius: 4px; padding: 8px; margin-bottom: 8px; border: 1px solid #2a2a2a; }
244
+ .agg-name { font-size: 9px; font-weight: 700; color: #888; margin-bottom: 2px; }
245
+ .agg-val { font-family: 'JetBrains Mono'; font-size: 14px; font-weight: 800; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--bg); }
248
+ .idx-strip { height: 50px; background: #0b0b0b; border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 30px; flex-shrink: 0; }
249
+ .idx-lbl { font-size: 10px; font-weight: 800; color: var(--accent); letter-spacing: 1px; }
250
+ .idx-val { font-family: 'JetBrains Mono'; font-size: 18px; font-weight: 800; color: #fff; }
251
+ .idx-meta { display: flex; gap: 15px; font-family: 'JetBrains Mono'; font-size: 12px; font-weight: 700; align-items: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ .grid-wrap { flex: 1; overflow-y: auto; padding: 10px; }
254
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 5px; }
255
+
256
+ .card { background: var(--card-bg); border: 1px solid #222; border-radius: 4px; padding: 8px; display: flex; flex-direction: column; justify-content: center; min-height: 50px; position: relative; transition: all 0.2s; cursor: default; }
257
+ .card.bull { background: var(--bull-bg); border-color: rgba(0, 200, 83, 0.3); }
258
+ .card.bear { background: var(--bear-bg); border-color: rgba(213, 0, 0, 0.3); }
259
+
260
+ .c-main { display: flex; justify-content: space-between; align-items: center; }
261
+ .sym { font-size: 11px; font-weight: 800; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 70px; }
262
+ .wgt-badge { font-size: 8px; font-family: 'JetBrains Mono'; color: #666; background: rgba(255,255,255,0.05); padding: 1px 3px; border-radius: 2px; }
263
+ .calc-big { font-family: 'JetBrains Mono'; font-size: 12px; font-weight: 800; text-align: right; }
264
+
265
+ .c-details { display: none; margin-top: 8px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.1); font-family: 'JetBrains Mono'; }
266
+ .card.expanded { min-height: 90px; justify-content: space-between; }
267
+ .card.expanded .c-details { display: block; }
268
+ .det-row { display: flex; justify-content: space-between; font-size: 9px; margin-bottom: 2px; color: #aaa; }
269
+ .det-val { font-weight: 700; color: #eee; }
270
+
271
+ .up { color: var(--green); } .dn { color: var(--red); }
272
+ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
273
 
274
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.85); z-index: 100; display: none; justify-content: center; align-items: center; }
275
+ .modal-box { background: var(--panel); width: 450px; padding: 20px; border-radius: 6px; border: 1px solid var(--border); }
276
+ textarea { width: 100%; height: 150px; background: #000; color: #fff; border: 1px solid #333; padding: 10px; font-family: 'JetBrains Mono'; font-size: 11px; margin-top: 5px; }
277
  </style>
278
  </head>
279
  <body>
280
 
281
+ <div class="modal" id="setModal">
 
 
 
 
 
282
  <div class="modal-box">
283
+ <h3 style="color:#fff; font-size:12px; margin-bottom:10px;">FORMULA CONFIG</h3>
284
+ <label style="font-size:9px; font-weight:700; color:var(--accent);">CARD FORMULA</label>
285
+ <input type="text" id="cardForm" style="width:100%; background:#000; border:1px solid #333; color:#fff; padding:6px; font-family:'JetBrains Mono'; font-size:11px; margin-bottom:10px;">
286
+ <label style="font-size:9px; font-weight:700; color:var(--accent);">AGGREGATES (JSON)</label>
287
+ <textarea id="aggJson"></textarea>
288
+ <div style="display:flex; gap:8px; margin-top:15px;">
289
+ <button class="btn active" style="flex:1" onclick="saveConfig()">SAVE</button>
290
+ <button class="btn" style="flex:1" onclick="closeModal()">CANCEL</button>
291
  </div>
 
292
  </div>
293
  </div>
294
 
295
+ <div class="head">
296
+ <div class="logo">NIFTY<span>50</span> PRO</div>
297
+ <div class="ctrls">
298
+ <button class="btn" id="togBtn" onclick="toggleDetails()">SHOW DETAILS: OFF</button>
299
+ <select id="sortSel" class="btn" onchange="render()"><option value="w">SORT: WEIGHT</option><option value="calc_h">SORT: VALUE HIGH</option><option value="calc_l">SORT: VALUE LOW</option></select>
300
+ <select id="expSel" class="btn" onchange="init()"><option value="0">NEAR</option><option value="1">NEXT</option></select>
301
+ <button class="btn" onclick="openModal()">⚙</button>
302
+ <button class="btn" style="color:var(--red)" onclick="location.href='/logout'">EXIT</button>
 
 
 
 
 
303
  </div>
304
  </div>
305
 
306
+ <div class="layout">
307
+ <div class="side">
308
+ <div class="side-head">AGGREGATES</div>
309
+ <div class="agg-list" id="aggList"></div>
 
 
 
 
 
 
310
  </div>
311
+ <div class="content">
312
+ <div class="idx-strip">
313
+ <div>
314
+ <div class="idx-lbl">NIFTY FUT</div>
315
+ <div class="idx-val" id="ixP">--.--</div>
316
+ </div>
317
+ <div class="idx-meta">
318
+ <span id="ixC">--%</span>
319
+ <span id="ixQ" style="color:#888; border-left:1px solid #333; padding-left:15px;">NET: --</span>
320
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  </div>
322
+ <div class="grid-wrap">
323
+ <div id="grid" class="grid"></div>
 
324
  </div>
325
  </div>
326
  </div>
327
 
328
  <script>
329
+ const TOKEN = "{{USER_TOKEN}}";
330
+ let CONFIG = {
331
+ cardFormula: "(TBQ - TSQ) * LTP * WGT",
332
+ aggregates: [
333
+ { "name": "NET MONEY FLOW", "formula": "(TBQ - TSQ) * LTP * WGT" },
334
+ { "name": "WEIGHTED DELTA", "formula": "(TBQ - TSQ) * LTP * WGT" },
335
+ { "name": "BUY PRESSURE", "formula": "TBQ * LTP * WGT" },
336
+ { "name": "SELL PRESSURE", "formula": "-1 * TSQ * LTP * WGT" }
 
 
 
 
 
 
337
  ]
338
  };
339
+ let API_CONF = null, DATA = {}, POLLER = null, SHOW_DETAILS = false;
340
+
341
+ window.onload = () => {
342
+ const stored = localStorage.getItem('n50_v6_conf');
343
+ if(stored) CONFIG = JSON.parse(stored);
344
+ document.getElementById('cardForm').value = CONFIG.cardFormula;
345
+ document.getElementById('aggJson').value = JSON.stringify(CONFIG.aggregates, null, 2);
346
+ init();
 
347
  };
348
 
349
+ function toggleDetails() {
350
+ SHOW_DETAILS = !SHOW_DETAILS;
351
+ document.getElementById('togBtn').innerText = "SHOW DETAILS: " + (SHOW_DETAILS ? "ON" : "OFF");
352
+ document.getElementById('togBtn').classList.toggle('active', SHOW_DETAILS);
353
+ render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
 
356
+ async function init() {
357
+ document.getElementById('grid').innerHTML = '<div style="padding:20px; color:#555">LOADING CONFIG...</div>';
358
+ const exp = document.getElementById('expSel').value;
359
  try {
360
+ const r = await fetch(`/api/config?expiry=${exp}`);
361
+ const j = await r.json();
362
+ if(j.s === 'ok') { API_CONF = j; render(); startPoll(); }
363
+ else { document.getElementById('grid').innerHTML = '<div style="padding:20px; color:#888">'+j.msg+'</div>'; setTimeout(init, 2000); }
364
+ } catch(e) { setTimeout(init, 2000); }
365
  }
366
 
367
+ function startPoll() {
368
+ if(POLLER) clearInterval(POLLER);
369
+ fetchData();
370
+ POLLER = setInterval(fetchData, 1000);
 
 
 
 
 
 
371
  }
372
 
373
+ async function fetchData() {
374
+ if(!API_CONF) return;
375
+ const s = [API_CONF.idx, ...API_CONF.stocks.map(x=>x.s)];
376
+ const b1 = s.slice(0,40); const b2 = s.slice(40);
377
  try {
378
+ let nd = {};
379
+ const call = async (list) => {
380
+ if(!list.length) return;
381
+ const q = list.map(x=>encodeURIComponent(x)).join(',');
382
+ const r = await fetch(`https://api-t1.fyers.in/data/depth?symbol=${q}&ohlcv_flag=1`, {headers:{'Authorization':TOKEN}});
383
+ const j = await r.json();
384
+ if(j.s==='ok') Object.assign(nd, j.d);
385
+ };
386
+ await Promise.all([call(b1), call(b2)]);
387
+ DATA = nd;
388
+ update();
389
  } catch(e){}
390
  }
391
 
392
+ function evalF(form, d, wgt) {
393
+ if(!d) return 0;
 
 
 
 
 
 
 
 
 
394
  try {
395
+ const TBQ = d.totalbuyqty || 0;
396
+ const TSQ = d.totalsellqty || 0;
397
+ const LTP = d.ltp || 0;
398
+ const WGT = wgt || 0;
399
+ return new Function('TBQ','TSQ','LTP','WGT','return '+form)(TBQ, TSQ, LTP, WGT);
400
+ } catch(e){ return 0; }
 
 
 
 
 
 
 
 
 
 
401
  }
402
 
403
+ function fmt(n) {
404
+ if(!n) return "-";
405
+ let a = Math.abs(n);
406
+ let s = n<0?"-":"+";
407
+ if(a>=10000000) return s+(a/10000000).toFixed(2)+" Cr";
408
+ if(a>=100000) return s+(a/100000).toFixed(2)+" L";
409
+ return s+(a/1000).toFixed(0)+" k";
410
+ }
411
+
412
+ function update() {
413
+ const i = DATA[API_CONF.idx];
414
+ if(i) {
415
+ document.getElementById('ixP').innerText = i.ltp;
416
+ const c = i.chp||0;
417
+ const ce = document.getElementById('ixC');
418
+ ce.innerText = c.toFixed(2)+"%"; ce.className = c>=0?"up":"dn";
419
+ const nq = (i.totalbuyqty||0) - (i.totalsellqty||0);
420
+ const qe = document.getElementById('ixQ');
421
+ qe.innerText = "NET: " + fmt(nq); qe.style.color = nq>=0?"var(--green)":"var(--red)";
422
  }
 
423
 
424
+ const sums = new Array(CONFIG.aggregates.length).fill(0);
425
+
426
+ API_CONF.stocks.forEach(stk => {
427
+ const d = DATA[stk.s];
428
+ if(!d) return;
429
+ CONFIG.aggregates.forEach((ag, idx) => sums[idx] += evalF(ag.formula, d, stk.w));
430
+ const el = document.getElementById(`c-${stk.r}`);
431
+ if(el) {
432
+ const cVal = evalF(CONFIG.cardFormula, d, stk.w);
433
+ const cv = el.querySelector('.calc-big');
434
+ cv.innerText = fmt(cVal);
435
+ cv.style.color = cVal>=0 ? "var(--green)" : "var(--red)";
436
+ el.className = "card " + (cVal>=0 ? "bull" : "bear") + (SHOW_DETAILS ? " expanded" : "");
437
+ if(SHOW_DETAILS) {
438
+ el.querySelector('.v-prc').innerText = d.ltp;
439
+ const c = d.chp||0;
440
+ const ec = el.querySelector('.v-chg');
441
+ ec.innerText = c.toFixed(2)+"%"; ec.style.color = c>=0?"var(--green)":"var(--red)";
442
+ el.querySelector('.v-buy').innerText = fmt(d.totalbuyqty);
443
+ el.querySelector('.v-sell').innerText = fmt(d.totalsellqty);
 
 
 
 
 
444
  }
445
  }
446
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
+ document.getElementById('aggList').innerHTML = CONFIG.aggregates.map((ag, i) => `
449
+ <div class="agg-card">
450
+ <div class="agg-name">${ag.name}</div>
451
+ <div class="agg-val" style="color:${sums[i]>=0?'var(--green)':'var(--red)'}">${fmt(sums[i])}</div>
452
+ </div>
453
+ `).join('');
454
+ }
455
+
456
+ function render() {
457
+ if(!API_CONF) return;
458
+ const sort = document.getElementById('sortSel').value;
459
+ let lst = [...API_CONF.stocks];
460
+ if(sort.includes('calc')) {
461
+ lst.sort((a,b) => {
462
+ const va = evalF(CONFIG.cardFormula, DATA[a.s], a.w);
463
+ const vb = evalF(CONFIG.cardFormula, DATA[b.s], b.w);
464
+ return sort==='calc_h' ? vb-va : va-vb;
465
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  }
467
+ document.getElementById('grid').innerHTML = lst.map(s => `
468
+ <div class="card ${SHOW_DETAILS?'expanded':''}" id="c-${s.r}">
469
+ <div class="c-main">
470
+ <div>
471
+ <div class="sym">${s.r}</div>
472
+ <div class="wgt-badge">${s.w.toFixed(2)}%</div>
473
+ </div>
474
+ <div class="calc-big">--</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  </div>
476
+ <div class="c-details">
477
+ <div class="det-row"><span>PRICE</span> <span class="det-val v-prc">--</span></div>
478
+ <div class="det-row"><span>CHANGE</span> <span class="det-val v-chg">--%</span></div>
479
+ <div class="det-row"><span>BUY Q</span> <span class="det-val v-buy" style="color:var(--green)">--</span></div>
480
+ <div class="det-row"><span>SELL Q</span> <span class="det-val v-sell" style="color:var(--red)">--</span></div>
481
  </div>
482
+ </div>
483
+ `).join('');
484
+ if(Object.keys(DATA).length>0) update();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
 
486
 
487
+ function openModal() { document.getElementById('setModal').style.display='flex'; }
488
+ function closeModal() { document.getElementById('setModal').style.display='none'; }
489
+ function saveConfig() {
490
  try {
491
+ const cf = document.getElementById('cardForm').value;
492
+ const ag = JSON.parse(document.getElementById('aggJson').value);
493
+ CONFIG = { cardFormula: cf, aggregates: ag };
494
+ localStorage.setItem('n50_v6_conf', JSON.stringify(CONFIG));
495
+ closeModal(); render(); update();
496
+ } catch(e) { alert("Invalid JSON"); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  }
498
  </script>
499
  </body>