topsecrettraders commited on
Commit
e86fe07
·
verified ·
1 Parent(s): ef13724

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +479 -1126
app.py CHANGED
@@ -2,357 +2,241 @@ import pandas as pd
2
  import requests
3
  import uvicorn
4
  import json
5
- import time
6
- import os
7
- import threading
8
- from datetime import datetime
9
- from fastapi import FastAPI, Cookie, BackgroundTasks, Query
10
- from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
11
- from typing import Optional
12
 
13
  # ==========================================
14
- # 1. CONFIGURATION
15
  # ==========================================
16
- # REPLACE THESE WITH YOUR UPSTOX CREDENTIALS
17
- API_KEY = "13c3ac18-b78f-4e5b-a6b8-d8f66de0b52c"
18
- API_SECRET = "qymj4zn0i1"
19
- REDIRECT_URI = "https://topsecrettraders-depthchainindiaup.hf.space/callback"
20
-
21
- CACHE_FILE = "upstox_master_cache.json"
 
 
 
22
 
23
  app = FastAPI()
24
 
25
  # ==========================================
26
- # 2. DATA LOADER (UPSTOX MASTER)
27
  # ==========================================
28
 
29
- MASTER_DB = {}
30
- SEARCH_INDEX = []
31
-
32
- SYSTEM_STATUS = {
33
- "ready": False,
34
- "message": "Booting..."
35
- }
36
- UPDATE_LOCK = threading.Lock()
 
 
37
 
38
- # Manual mapping for Index Spot Keys (Safety Net)
39
- INDEX_MAP = {
40
- "NIFTY": "NSE_INDEX|Nifty 50",
41
- "BANKNIFTY": "NSE_INDEX|Nifty Bank",
42
- "FINNIFTY": "NSE_INDEX|Nifty Fin Service",
43
- "MIDCPNIFTY": "NSE_INDEX|NIFTY MID SELECT",
44
- "SENSEX": "BSE_INDEX|SENSEX",
45
- "BANKEX": "BSE_INDEX|BANKEX",
46
- "SENSEX50": "BSE_INDEX|SENSEX50"
47
- }
48
 
49
- def update_master_db():
50
- global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- if not UPDATE_LOCK.acquire(blocking=False):
53
- return
54
-
55
- try:
56
- SYSTEM_STATUS["message"] = "Downloading Upstox Master Data..."
57
- print(">>> [BG TASK] STARTING DOWNLOAD (NSE, BSE, MCX)...")
58
-
59
- # 1. NSE
60
- url_nse = "https://assets.upstox.com/market-quote/instruments/exchange/NSE.json.gz"
61
- df_nse = pd.read_json(url_nse, compression='gzip')
62
-
63
- # 2. BSE
64
- url_bse = "https://assets.upstox.com/market-quote/instruments/exchange/BSE.json.gz"
65
- df_bse = pd.read_json(url_bse, compression='gzip')
66
-
67
- # 3. MCX
68
- url_mcx = "https://assets.upstox.com/market-quote/instruments/exchange/MCX.json.gz"
69
- df_mcx = pd.read_json(url_mcx, compression='gzip')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- SYSTEM_STATUS["message"] = "Processing Data..."
72
-
73
- # Combine all exchanges
74
- df = pd.concat([df_nse, df_bse, df_mcx], ignore_index=True)
 
 
 
 
 
 
 
 
75
 
76
- # Filter relevant segments
77
- mask = df['segment'].isin(['NSE_FO', 'BSE_FO', 'MCX_FO', 'NSE_INDEX', 'BSE_INDEX', 'NSE_EQ'])
78
- df = df[mask]
 
 
79
 
80
- temp_db = {}
81
- search_list = []
82
 
83
- # ---------------------------------------------------------
84
- # STEP 1: Build the SPOT LOOKUP Dictionary
85
- # Map: "RELIANCE" -> "NSE_EQ|INE002A01018"
86
- # ---------------------------------------------------------
87
- spot_lookup = {}
88
-
89
- for _, row in df.iterrows():
90
- seg = row['segment']
91
-
92
- # For Stocks (Equity): Use 'trading_symbol' (e.g., "RELIANCE")
93
- if seg == 'NSE_EQ':
94
- sym = str(row['trading_symbol']).strip().upper()
95
- key = row['instrument_key']
96
- spot_lookup[sym] = key
97
-
98
- # For Indices: Use 'trading_symbol' (e.g., "Nifty 50")
99
- elif seg == 'NSE_INDEX' or seg == 'BSE_INDEX':
100
- sym = str(row['trading_symbol']).strip().upper()
101
- key = row['instrument_key']
102
- spot_lookup[sym] = key
103
-
104
- # Also map 'name' just in case
105
- name = str(row['name']).strip().upper()
106
- spot_lookup[name] = key
107
-
108
- # ---------------------------------------------------------
109
- # STEP 2: Build the DERIVATIVES Chain
110
- # ---------------------------------------------------------
111
- for _, row in df.iterrows():
112
- seg = row['segment']
113
-
114
- # Skip non-derivatives
115
- if seg not in ['NSE_FO', 'BSE_FO', 'MCX_FO']:
116
- continue
117
-
118
- sym = row['trading_symbol'] # e.g. RELIANCE24JANFUT
119
- key = row['instrument_key']
120
 
121
- # -----------------------------------------------------
122
- # CRITICAL FIX: IDENTIFY THE ROOT SYMBOL
123
- # Priority: underlying_symbol > asset_symbol > name
124
- # -----------------------------------------------------
125
- root = ""
 
126
 
127
- # 1. Try 'underlying_symbol' (This gives "RELIANCE" correctly for stocks)
128
- us = row.get('underlying_symbol')
129
- if pd.notna(us) and str(us).strip() != "":
130
- root = str(us).strip().upper()
131
-
132
- # 2. Fallback to 'asset_symbol' if underlying is empty
133
- if not root:
134
- asym = row.get('asset_symbol')
135
- if pd.notna(asym) and str(asym).strip() != "":
136
- root = str(asym).strip().upper()
137
-
138
- # 3. Last resort: 'name' (Though usually Long Name for stocks)
139
- if not root:
140
- nm = row.get('name')
141
- if pd.notna(nm):
142
- root = str(nm).strip().upper()
143
-
144
- # Normalize (remove spaces just in case)
145
- clean_root = root.replace(' ', '')
146
-
147
- # Initialize DB Entry
148
- if clean_root not in temp_db:
149
- # Find Spot Key
150
- spot_key = ""
151
 
152
- # A: Manual Map (Indices)
153
- if clean_root in INDEX_MAP:
154
- spot_key = INDEX_MAP[clean_root]
155
-
156
- # B: Lookup using raw ROOT (e.g. "RELIANCE") matches "NSE_EQ" trading_symbol
157
- elif root in spot_lookup:
158
- spot_key = spot_lookup[root]
159
-
160
- # C: Lookup using cleaned ROOT
161
- elif clean_root in spot_lookup:
162
- spot_key = spot_lookup[clean_root]
163
-
164
- # Determine Exchange Code
165
- exch_code = seg.split('_')[0]
166
-
167
- # Use Short Name for display if possible, else Full Name
168
- # If root is "RELIANCE", display is likely "RELIANCE"
169
- # If row['name'] is "RELIANCE INDUSTRIES LTD", use that for nice display
170
- display_name = row['name'] if pd.notna(row['name']) else root
171
-
172
- temp_db[clean_root] = {
173
- "spot": spot_key,
174
- "exch": exch_code,
175
- "name": display_name,
176
- "items": []
177
- }
178
-
179
- inst_type = row.get('instrument_type', '')
180
-
181
- # Parse Expiry
182
- exp = 0
183
- raw_exp = row.get('expiry')
184
- if pd.notna(raw_exp):
185
- try:
186
- if isinstance(raw_exp, (int, float)):
187
- exp = int(raw_exp / 1000)
188
- elif isinstance(raw_exp, str):
189
- dt = datetime.strptime(raw_exp, "%Y-%m-%d")
190
- exp = int(dt.replace(tzinfo=None).timestamp())
191
- except: pass
192
-
193
- strike = float(row.get('strike_price', 0.0))
194
-
195
- # Map Types
196
- t_mapped = "FUT"
197
- if inst_type == "CE": t_mapped = "CE"
198
- elif inst_type == "PE": t_mapped = "PE"
199
- elif inst_type in ["FUTIDX", "FUTSTK", "FUTCOM", "FUTBLN", "FUTENR"]: t_mapped = "FUT"
200
-
201
- temp_db[clean_root]["items"].append({
202
- "s": key,
203
- "dis": sym,
204
- "e": exp,
205
- "k": strike,
206
- "t": t_mapped
207
- })
208
-
209
- # ---------------------------------------------------------
210
- # STEP 3: Finalize
211
- # ---------------------------------------------------------
212
- final_db = {}
213
- for root, data in temp_db.items():
214
- if not data["items"]: continue
215
-
216
- sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
217
-
218
- final_db[root] = {
219
- "spot": data["spot"],
220
- "exch": data["exch"],
221
- "items": sorted_items
222
- }
223
-
224
- # Add to Search
225
- # We use the full name in display so user sees "RELIANCE INDUSTRIES LTD"
226
- # But the 'root' key is "RELIANCE"
227
- search_list.append({
228
- "root": root,
229
- "name": data["name"],
230
- "exch": data["exch"],
231
- "display": f"{data['name']} ({root})"
232
  })
233
 
234
- MASTER_DB = final_db
235
- SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
236
-
237
- with open(CACHE_FILE, "w") as f:
238
- json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
239
-
240
- SYSTEM_STATUS["ready"] = True
241
- SYSTEM_STATUS["message"] = "Ready"
242
- print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} roots. Fixed Underlying Symbol Logic.")
243
-
244
- except Exception as e:
245
- print(f">>> [BG TASK] ERROR: {e}")
246
- SYSTEM_STATUS["message"] = f"Error: {str(e)}"
247
- finally:
248
- UPDATE_LOCK.release()
249
-
250
- @app.on_event("startup")
251
- def startup_event():
252
- global MASTER_DB, SEARCH_INDEX
253
- if os.path.exists(CACHE_FILE):
254
- try:
255
- print(">>> STARTUP: Loading Cache...")
256
- with open(CACHE_FILE, "r") as f:
257
- data = json.load(f)
258
- MASTER_DB = data["db"]
259
- SEARCH_INDEX = data["idx"]
260
- SYSTEM_STATUS["ready"] = True
261
- SYSTEM_STATUS["message"] = "Ready (Cached)"
262
- except: pass
263
-
264
- if not SYSTEM_STATUS["ready"]:
265
- thread = threading.Thread(target=update_master_db)
266
- thread.start()
267
-
268
- # ==========================================
269
- # 3. ROUTES
270
- # ==========================================
271
 
272
- @app.get("/")
273
- def home(access_token: Optional[str] = Cookie(None)):
274
- if not access_token:
275
- auth_url = (
276
- f"https://api.upstox.com/v2/login/authorization/dialog?"
277
- f"response_type=code&client_id={API_KEY}&redirect_uri={REDIRECT_URI}"
278
- )
279
- return HTMLResponse(f"""
280
- <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;">
281
- <div style="background:white; padding:40px; border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.1); text-align:center;">
282
- <h2 style="margin-top:0;">Authentication Required</h2>
283
- <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 UPSTOX</a>
284
- </div>
285
- </div>
286
- """)
287
 
288
- return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
289
-
290
- @app.get("/callback")
291
- def callback(code: Optional[str] = None):
292
- if not code: return JSONResponse({"error": "No Code Provided"})
293
-
294
- url = "https://api.upstox.com/v2/login/authorization/token"
295
- headers = { 'accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }
296
- data = {
297
- 'code': code,
298
- 'client_id': API_KEY,
299
- 'client_secret': API_SECRET,
300
- 'redirect_uri': REDIRECT_URI,
301
- 'grant_type': 'authorization_code'
302
  }
 
303
 
304
- resp = requests.post(url, headers=headers, data=data)
305
- json_resp = resp.json()
 
 
306
 
307
- if "access_token" in json_resp:
308
- response = RedirectResponse(url="/")
309
- response.set_cookie(key="access_token", value=json_resp['access_token'], httponly=True)
310
- return response
311
- else:
312
- return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(json_resp, indent=2)}</pre>")
313
-
314
- @app.get("/logout")
315
- def logout():
316
- response = RedirectResponse(url="/")
317
- response.delete_cookie("access_token")
318
- return response
319
-
320
- @app.get("/api/status")
321
- def get_status():
322
- return JSONResponse(SYSTEM_STATUS)
323
-
324
- @app.get("/api/search")
325
- def search_symbol(q: str = Query(..., min_length=1)):
326
- if not SYSTEM_STATUS["ready"]: return JSONResponse([])
327
- query = q.upper()
328
 
329
- results = []
330
- # 1. Exact Starts With Root (e.g. "REL")
331
- results += [x for x in SEARCH_INDEX if x['root'].startswith(query)]
332
 
333
- # 2. Contains in Name (e.g. "RELIANCE") - limit to 10
334
- if len(results) < 20:
335
- results += [x for x in SEARCH_INDEX if query in x['name'].upper() and x not in results][:10]
336
-
337
- return JSONResponse(results[:30])
338
-
339
- @app.get("/api/chain")
340
- def get_chain(symbol: str):
341
- root = symbol.upper()
342
- if root in MASTER_DB:
343
- return JSONResponse(MASTER_DB[root])
344
- return JSONResponse({"error": "Symbol not found"}, status_code=404)
345
-
346
- @app.get("/api/refresh")
347
- def force_refresh(background_tasks: BackgroundTasks):
348
- SYSTEM_STATUS["ready"] = False
349
- SYSTEM_STATUS["message"] = "Refreshing..."
350
- background_tasks.add_task(update_master_db)
351
- return JSONResponse({"s": "ok"})
352
 
353
 
354
  # ==========================================
355
- # 4. FRONTEND
356
  # ==========================================
357
 
358
  HTML_TEMPLATE = """
@@ -360,888 +244,357 @@ HTML_TEMPLATE = """
360
  <html lang="en">
361
  <head>
362
  <meta charset="UTF-8">
363
- <title>PRO CHAIN (UPSTOX)</title>
364
- <meta name="viewport" content="width=1100">
365
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
 
366
  <style>
367
  :root {
368
  --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
369
  --text-main: #1a1a1a; --text-sub: #555555;
370
- --accent: #6c4af5; --green: #00c853; --red: #d50000;
371
- --green-soft: rgba(0, 200, 83, 0.27); --red-soft: rgba(213, 0, 0, 0.18);
372
- --atm-bg: rgba(108, 74, 245, 0.08);
373
- --badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff;
374
  }
375
  * { box-sizing: border-box; }
376
- body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
377
-
378
- .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); }
379
- .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); }
380
- .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
381
- .btn.active { background: var(--accent); color: white; border-color: var(--accent); }
382
-
383
- .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; }
384
- .icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
385
-
386
- .search-box { position: relative; width: 350px; margin-right: 20px; }
387
- .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; }
388
- .search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(108, 74, 245, 0.1); }
389
- .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; }
390
-
391
- .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; }
392
- .search-item:hover { background: #f0f7ff; color: var(--accent); }
393
 
394
- .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
395
- .badge-NSE { background: var(--badge-nse); }
396
- .badge-BSE { background: var(--badge-bse); }
397
- .badge-MCX { background: var(--badge-mcx); }
398
 
399
- .grid-layout { display: flex; height: calc(100vh - 60px); }
400
-
401
- /* Chain Panel */
402
- .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
403
-
404
- .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; }
405
- .header-cell { padding: 12px 5px; display: flex; align-items: center; justify-content: center; gap: 8px; position:relative; border-right: 1px solid var(--border); }
406
- .header-icon { opacity: 0.3; cursor: pointer; transition: 0.2s; }
407
- .header-icon:hover { opacity: 1; color: var(--accent); }
408
-
409
- .chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; }
410
-
411
- .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; }
412
- .row:hover { background: #fafafa; }
413
- .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
414
 
415
- .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; }
416
- .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; }
417
 
418
- .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
419
- .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; }
420
 
421
- /* Futures / Sidebar Panel */
422
- .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
423
 
424
- .panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; }
425
- .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; }
426
-
427
- .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); }
428
- .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; }
429
-
430
- .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); }
431
-
432
- .nf-sec { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
433
- .nf-sec:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
434
- .nf-title { font-size: 10px; font-weight: 800; color: #888; text-transform: uppercase; margin-bottom: 8px; text-align: left; }
435
-
436
- .aggr-row { display: flex; width: 100%; border: 1px solid transparent; }
437
- .aggr-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
438
-
439
- .aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; }
440
- .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; }
441
-
442
- .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; }
443
- .info-bubble:hover { background:var(--accent); color:white; }
444
 
445
- /* Modals */
446
- .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); }
447
- .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); }
448
- @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
449
 
450
- .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
451
- .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; }
452
- .opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
453
- .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
454
 
455
- .inp-group { margin-top:15px; }
456
- .inp-label { font-size:11px; font-weight:800; color:#888; margin-bottom:5px; display:block; }
457
- .modal-inp { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; font-size:14px; }
458
- .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; }
459
- .modal-action.sec { background:#eee; color:#333; margin-top:10px; }
460
-
461
- .info-table { width:100%; font-family:'JetBrains Mono'; font-size:10px; border-collapse: collapse; margin-top:10px; }
462
- .info-table th { text-align:left; color:#888; border-bottom:1px solid #eee; padding:5px; }
463
- .info-table td { padding:5px; border-bottom:1px solid #f9f9f9; color:#333; }
464
- .info-row-res { background: #f0f7ff; font-weight:800; }
465
-
466
- .conf-list { max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px; background: #fafafa; }
467
- .conf-item { background: #fff; border-bottom: 1px solid #eee; padding: 10px; }
468
- .conf-item:last-child { border-bottom: none; }
469
- .conf-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
470
- .conf-body { margin-top: 10px; display: none; padding-top: 10px; border-top: 1px solid #f5f5f5; }
471
- .conf-item.expanded .conf-body { display: block; }
472
- .conf-item.expanded .conf-header { color: var(--accent); }
473
- .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; }
474
- .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; }
475
- .add-btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }
476
-
477
- .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; }
478
- .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; }
479
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
480
 
481
- @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
482
- .live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; }
483
- .live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); }
484
- .live-dot.error { background: var(--red); }
485
-
486
- ::-webkit-scrollbar { width: 6px; height: 6px; }
487
- ::-webkit-scrollbar-track { background: transparent; }
488
- ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
489
- ::-webkit-scrollbar-thumb:hover { background: #bbb; }
490
-
491
- .code-box { background:#f4f4f4; padding:10px; border-radius:6px; font-family:'JetBrains Mono'; font-size:11px; color:#555; margin-top:5px; display:none; }
492
- .var-tag { color:var(--accent); font-weight:bold; }
493
  </style>
494
  </head>
495
  <body>
496
 
497
- <!-- LOADER -->
498
- <div class="loader-ov" id="loader">
499
- <div class="spinner"></div>
500
- <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div>
501
- </div>
502
-
503
- <!-- EXPIRY MODAL -->
504
- <div class="modal" id="expModal">
505
- <div class="modal-box">
506
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
507
- <span>SELECT EXPIRY</span>
508
- <span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span>
509
- </div>
510
- <div class="modal-grid" id="expGrid"></div>
511
- </div>
512
  </div>
513
 
514
- <!-- INFO BREAKDOWN MODAL -->
515
- <div class="modal" id="infoModal">
516
- <div class="modal-box" style="width: 500px; max-width:95%;">
517
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
518
- <span>CALCULATION BREAKDOWN</span>
519
- <span style="cursor:pointer; color:#999;" onclick="closeModal('infoModal')"></span>
520
- </div>
521
- <div style="margin-top:15px; font-size:12px;">
522
- <div><strong>LIVE SPOT:</strong> <span id="infoSpot" style="font-family:'JetBrains Mono'; color:var(--accent);">0.00</span></div>
523
- <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>
524
  </div>
525
- <div style="overflow-y:auto; max-height:400px; margin-top:10px;">
526
- <table class="info-table" id="infoTable">
527
- <!-- Dynamic Content -->
528
- </table>
 
529
  </div>
530
- <div style="margin-top:10px; font-size:10px; color:#888; text-align:center;">* Uses Raw Integer Values for Accuracy</div>
531
- </div>
532
- </div>
533
 
534
- <!-- CONFIG MODAL -->
535
- <div class="modal" id="confModal">
536
- <div class="modal-box">
537
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
538
- <span id="confTitle">COLUMN SETTINGS</span>
539
- <span style="cursor:pointer; color:#999;" onclick="closeModal('confModal')">✕</span>
540
- </div>
541
- <div class="inp-group">
542
- <span class="inp-label">DISPLAY TITLE</span>
543
- <input type="text" id="confName" class="modal-inp" placeholder="e.g. NET FLOW">
544
  </div>
545
- <div class="inp-group">
546
- <span class="inp-label">CUSTOM FORMULA</span>
547
- <input type="text" id="confForm" class="modal-inp" placeholder="e.g. PS - CS">
548
- </div>
549
- <button class="modal-action" onclick="saveConfig()">SAVE CONFIGURATION</button>
550
- <button class="modal-action sec" onclick="resetConfig()">RESET TO DEFAULTS</button>
551
- </div>
552
- </div>
553
 
554
- <!-- SIDEBAR AGGREGATE SETTINGS MODAL -->
555
- <div class="modal" id="sideConfModal">
556
- <div class="modal-box">
557
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
558
- <span>SIDEBAR CONFIG</span>
559
- <span style="cursor:pointer; color:#999;" onclick="closeModal('sideConfModal')">✕</span>
560
- </div>
561
- <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;">
562
- <span>CONFIGURE CARDS & RANGES</span>
563
- <span onclick="toggleVarHelp()" style="color:var(--accent); cursor:pointer; font-weight:800;">[?] VARIABLES</span>
564
  </div>
565
- <div class="code-box" id="sideVarHelp">
566
- <div>VARIABLES AVAILABLE:</div>
567
- <div style="margin-top:4px;">
568
- <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>
569
- <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
570
- </div>
571
- <div style="margin-top:4px; font-size:10px; color:#888;">Supports +, -, *, /, ( ) arithmetic.</div>
 
 
 
 
 
572
  </div>
573
- <div class="conf-list" id="sideListContainer"></div>
574
- <div class="add-btn" onclick="addSideItem()">+ ADD NEW AGGREGATE CARD</div>
575
- <button class="modal-action" onclick="saveSideConfig()">SAVE SETTINGS</button>
576
- <button class="modal-action sec" onclick="resetSideConfig()">RESET DEFAULTS</button>
577
- </div>
578
- </div>
579
 
580
- <!-- TOP NAV -->
581
- <div class="top-nav">
582
- <div style="display:flex; align-items:center;">
583
- <div class="search-box">
584
- <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. SBIN, CRUDE)..." autocomplete="off">
585
- <div class="search-dropdown" id="searchRes"></div>
586
- </div>
587
- </div>
588
-
589
- <div style="display:flex; gap:20px; align-items:center;">
590
- <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB">
591
- <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>
592
- </button>
593
- <div style="text-align:right;">
594
- <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
595
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
596
  </div>
597
- <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
598
- <span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
599
- <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>
600
- </button>
601
- <div class="live-dot" id="statusDot" title="Connection Status"></div>
602
- <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;">
603
- <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>
604
- </button>
605
- </div>
606
- </div>
607
 
608
- <div class="grid-layout">
609
- <div class="chain-panel">
610
- <div class="chain-header">
611
- <div class="header-cell">
612
- <span id="headL_txt">NET FLOW</span>
613
- <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>
614
- </div>
615
- <div class="header-cell">
616
- <span id="headR_txt">OVERALL FLOW</span>
617
- <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>
618
  </div>
619
  </div>
620
- <div class="chain-body" id="chainBody">
621
- <div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div>
 
 
 
 
 
 
622
  </div>
 
 
 
623
  </div>
624
-
625
- <div class="fut-panel">
626
- <div class="panel-section">
627
- <div class="panel-title">
628
- <span>AGGREGATE FLOW</span>
629
- <div style="display:flex; align-items:center; gap:5px;">
630
- <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>
631
- </div>
632
- </div>
633
- <div class="netflow-card" id="sideContainer">
634
- <div style="padding:20px; color:#ccc; font-size:10px;">LOADING CONFIG...</div>
635
- </div>
636
- </div>
637
- <div class="panel-section">
638
- <div class="panel-title">FUTURES CONTRACTS</div>
639
- <div id="futBody"></div>
640
  </div>
641
  </div>
642
  </div>
643
 
644
  <script>
645
- const UPSTOX_TOKEN = "{{USER_TOKEN}}";
646
-
647
- // Core State
648
- let CURRENT_ROOT = "";
649
- let CHAIN_DATA = null;
650
- let ACTIVE_EXP = 0;
651
- let SPOT_SYM = "";
652
- let SPOT_PRICE = 0;
653
- let FUT_SYMS = [];
654
- let OPT_SYMS = [];
655
- let POLLER = null;
656
- let IS_POLLING = false;
657
- let SEARCH_TIMER = null;
658
- let ATM_STRIKE = 0;
659
- let LATEST_ROW_DATA = {};
660
-
661
- // Default Configurations
662
- const DEFAULTS = {
663
- left: { name: "NET FLOW", formula: "PS - CS" },
664
- right: { name: "OVERALL FLOW", formula: "CB + PS - (PB + CS)" },
665
- side: [
666
- { id: 1, name: "NET FLOW", formula: "PS - CS", ranges: [7,10], _expanded: true },
667
- { id: 2, name: "OVERALL", formula: "CB + PS - (PB + CS)", ranges: [7,10], _expanded: false },
668
- { id: 3, name: "NET OI", formula: "PO - CO", ranges: [10], _expanded: false }
669
- ]
670
- };
671
-
672
- // Load Config from LocalStorage
673
- let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS));
674
- if(!Array.isArray(CONFIG.side)) CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side));
675
 
676
- let EDITING_COL = null;
677
-
678
- window.onload = () => {
679
- applyConfig();
680
- checkStatus();
681
- const inp = document.getElementById('searchInp');
682
- inp.addEventListener('input', (e) => {
683
- clearTimeout(SEARCH_TIMER);
684
- SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300);
685
- });
686
- document.addEventListener('click', (e) => {
687
- if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
688
- });
689
- };
690
-
691
- // ===================================
692
- // MATH ENGINE
693
- // ===================================
694
- function evaluateFormula(formula, vars) {
695
  try {
696
- let exp = formula;
697
- exp = exp.replace(/CS/g, vars.CS || 0);
698
- exp = exp.replace(/CB/g, vars.CB || 0);
699
- exp = exp.replace(/PS/g, vars.PS || 0);
700
- exp = exp.replace(/PB/g, vars.PB || 0);
701
-
702
- exp = exp.replace(/CO/g, vars.CO || 0);
703
- exp = exp.replace(/PO/g, vars.PO || 0);
704
- exp = exp.replace(/CLTP/g, vars.CLTP || 0);
705
- exp = exp.replace(/PLTP/g, vars.PLTP || 0);
706
- exp = exp.replace(/CV/g, vars.CV || 0);
707
- exp = exp.replace(/PV/g, vars.PV || 0);
708
-
709
- if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
710
- return new Function('return ' + exp)();
711
- } catch(e) { return 0; }
712
- }
713
-
714
- // ===================================
715
- // CONFIG LOGIC
716
- // ===================================
717
- function applyConfig() {
718
- document.getElementById('headL_txt').innerText = CONFIG.left.name;
719
- document.getElementById('headR_txt').innerText = CONFIG.right.name;
720
- renderSidebar();
721
- }
722
-
723
- function renderSidebar() {
724
- const con = document.getElementById('sideContainer');
725
- if(!CONFIG.side || CONFIG.side.length === 0) {
726
- con.innerHTML = '<div style="padding:10px; color:#aaa; font-size:10px;">NO METRICS CONFIGURED</div>';
727
- return;
728
  }
 
729
 
730
- con.innerHTML = CONFIG.side.map((item, idx) => {
731
- const boxes = item.ranges.map((r, rIdx) => `
732
- <div class="aggr-item">
733
- <span class="aggr-val" id="sv-${idx}-${rIdx}">0</span>
734
- <span class="aggr-lbl">
735
- ATM ±${r}
736
- <span class="info-bubble" onclick="showInfo(${idx}, ${rIdx})">i</span>
737
- </span>
738
- </div>
739
- `).join('');
740
-
741
- return `
742
- <div class="nf-sec">
743
- <div class="nf-title">${item.name}</div>
744
- <div class="aggr-row">
745
- ${boxes}
746
- </div>
747
- </div>`;
748
- }).join('');
749
- }
750
-
751
- function showInfo(cardIdx, rangeIdx) {
752
- if(!CONFIG.side[cardIdx]) return;
753
- const cfg = CONFIG.side[cardIdx];
754
- const range = cfg.ranges[rangeIdx];
755
- const formula = cfg.formula;
756
-
757
- document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
758
- document.getElementById('infoForm').innerText = formula;
759
-
760
- const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
761
- const lowers = kList.filter(k => k < SPOT_PRICE);
762
- const uppers = kList.filter(k => k >= SPOT_PRICE);
763
 
764
- const selLow = lowers.slice(-range);
765
- const selHigh = uppers.slice(0, range);
766
- const allSel = [...selLow, ...selHigh].sort((a,b)=>a-b);
767
- let grandTotal = 0;
768
-
769
- const table = document.getElementById('infoTable');
770
-
771
- let hHTML = `<tr><th style="width:50px;">STRIKE</th><th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th></tr>`;
772
-
773
- let rowsHTML = allSel.map(k => {
774
- const d = LATEST_ROW_DATA[k] || {};
775
- const val = evaluateFormula(formula, d);
776
- grandTotal += val;
777
- const borderStyle = (k >= SPOT_PRICE && selLow.includes(k- (kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
778
- return `<tr style="${borderStyle}"><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>`;
779
- }).join('');
780
-
781
- rowsHTML += `<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>`;
782
- table.innerHTML = hHTML + rowsHTML;
783
- document.getElementById('infoModal').style.display = 'flex';
784
- }
785
-
786
- function openConfig(col) {
787
- EDITING_COL = col;
788
- const cfg = CONFIG[col];
789
- document.getElementById('confTitle').innerText = (col === 'left' ? "LEFT COLUMN" : "RIGHT COLUMN") + " SETTINGS";
790
- document.getElementById('confName').value = cfg.name;
791
- document.getElementById('confForm').value = cfg.formula;
792
- document.getElementById('confModal').style.display = 'flex';
793
- }
794
-
795
- function saveConfig() {
796
- const name = document.getElementById('confName').value;
797
- const form = document.getElementById('confForm').value.toUpperCase();
798
- if(!name || !form) { alert("Fields cannot be empty"); return; }
799
- CONFIG[EDITING_COL] = { name: name, formula: form };
800
- saveAndReload();
801
- closeModal('confModal');
802
- }
803
-
804
- function resetConfig() {
805
- if(confirm("Reset Main Columns to Default?")) {
806
- CONFIG.left = DEFAULTS.left;
807
- CONFIG.right = DEFAULTS.right;
808
- saveAndReload();
809
- closeModal('confModal');
810
- }
811
- }
812
-
813
- // --- SIDEBAR DYNAMIC CONFIG ---
814
- let TEMP_SIDE_CONFIG = [];
815
-
816
- function openSideConfig() {
817
- TEMP_SIDE_CONFIG = JSON.parse(JSON.stringify(CONFIG.side));
818
- renderSideConfigList();
819
- document.getElementById('sideConfModal').style.display = 'flex';
820
- }
821
-
822
- function renderSideConfigList() {
823
- const list = document.getElementById('sideListContainer');
824
- list.innerHTML = TEMP_SIDE_CONFIG.map((item, idx) => `
825
- <div class="conf-item ${item._expanded ? 'expanded' : ''}">
826
- <div class="conf-header" onclick="toggleSideItem(${idx})">
827
- <span style="font-weight:700; font-size:12px;">${idx+1}. ${item.name}</span>
828
- <div style="display:flex; align-items:center; gap:10px;">
829
- <span style="font-size:10px; color:#aaa;">${item.ranges.length} RANGES</span>
830
- <span class="del-btn" onclick="removeSideItem(event, ${idx})">DEL</span>
831
- </div>
832
- </div>
833
- <div class="conf-body" onclick="event.stopPropagation()">
834
- <div class="inp-group" style="margin-top:0;">
835
- <span class="inp-label">TITLE</span>
836
- <input type="text" class="modal-inp" value="${item.name}" onchange="updateSideItem(${idx}, 'name', this.value)">
837
- </div>
838
- <div class="inp-group">
839
- <span class="inp-label">FORMULA</span>
840
- <input type="text" class="modal-inp" value="${item.formula}" onchange="updateSideItem(${idx}, 'formula', this.value)">
841
- </div>
842
- <div class="inp-group">
843
- <span class="inp-label">RANGES (Comma separated, max 3 recommended)</span>
844
- <input type="text" class="modal-inp" value="${item.ranges.join(', ')}" onchange="updateSideItem(${idx}, 'ranges', this.value)">
845
- </div>
846
- </div>
847
- </div>
848
- `).join('');
849
- }
850
-
851
- function toggleSideItem(idx) { TEMP_SIDE_CONFIG[idx]._expanded = !TEMP_SIDE_CONFIG[idx]._expanded; renderSideConfigList(); }
852
- function toggleVarHelp() { const box = document.getElementById('sideVarHelp'); box.style.display = (box.style.display === 'none' || box.style.display === '') ? 'block' : 'none'; }
853
- function addSideItem() { TEMP_SIDE_CONFIG.push({ id: Date.now(), name: "NEW METRIC", formula: "PS - CS", ranges: [10], _expanded: true }); renderSideConfigList(); }
854
- function removeSideItem(e, idx) { e.stopPropagation(); if(confirm("Delete this card?")) { TEMP_SIDE_CONFIG.splice(idx, 1); renderSideConfigList(); } }
855
- function updateSideItem(idx, field, val) {
856
- if(field === 'ranges') {
857
- const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 50);
858
- TEMP_SIDE_CONFIG[idx][field] = nums.length > 0 ? nums : [10];
859
- } else if (field === 'formula') { TEMP_SIDE_CONFIG[idx][field] = val.toUpperCase();
860
- } else { TEMP_SIDE_CONFIG[idx][field] = val; }
861
- }
862
- function saveSideConfig() {
863
- const clean = TEMP_SIDE_CONFIG.map(x => { const { _expanded, ...rest } = x; return rest; });
864
- CONFIG.side = clean;
865
- saveAndReload();
866
- closeModal('sideConfModal');
867
- }
868
- function resetSideConfig() { if(confirm("Reset Sidebar to defaults?")) { CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); saveAndReload(); closeModal('sideConfModal'); } }
869
- function saveAndReload() { localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG)); applyConfig(); pollData(); }
870
- function closeModal(id) { document.getElementById(id).style.display = 'none'; }
871
-
872
- // ===================================
873
- // UPSTOX DATA LOGIC
874
- // ===================================
875
- async function checkStatus() {
876
  try {
877
- const res = await fetch('/api/status');
878
- const status = await res.json();
879
- if(status.ready) {
880
- document.getElementById('loader').style.display = 'none';
881
- if(!CURRENT_ROOT) loadSymbol('NIFTY');
882
- } else {
883
- document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
884
- setTimeout(checkStatus, 2000);
885
- }
886
- } catch(e) { setTimeout(checkStatus, 2000); }
887
  }
888
 
889
- async function refreshDB() {
890
- if(!confirm("Download fresh Master Data? (Takes ~15s)")) return;
891
- document.getElementById('loaderMsg').innerText = "DOWNLOADING...";
892
- document.getElementById('loader').style.display = 'flex';
893
- await fetch('/api/refresh');
894
- checkStatus();
895
- }
896
-
897
- async function doSearch(q) {
898
- const resDiv = document.getElementById('searchRes');
899
- if(q.length < 2) { resDiv.style.display = 'none'; return; }
900
  try {
901
- const res = await fetch(`/api/search?q=${q}`);
902
- const data = await res.json();
903
-
904
- // Format: "Company Name (Symbol) [Exchange]"
905
- resDiv.innerHTML = data.map(item =>
906
- `<div class="search-item" onclick="loadSymbol('${item.root}')">
907
- <span>${item.name} <small style="color:#888; font-weight:normal;">(${item.root})</small></span>
908
- <span class="exch-badge badge-${item.exch}">${item.exch}</span>
909
- </div>`
910
- ).join('');
911
- resDiv.style.display = 'block';
912
- } catch(e) { console.error(e); }
913
  }
914
 
915
- async function loadSymbol(root) {
916
- document.getElementById('searchInp').value = root;
917
- document.getElementById('searchRes').style.display = 'none';
918
- document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
919
- document.getElementById('loader').style.display = 'flex';
 
920
 
921
  try {
922
- const res = await fetch(`/api/chain?symbol=${root}`);
923
- if(!res.ok) throw new Error("Symbol not found");
924
- const data = await res.json();
925
-
926
- CURRENT_ROOT = root;
927
- CHAIN_DATA = data.items;
928
- SPOT_SYM = data.spot;
929
- SPOT_PRICE = 0;
930
-
931
- // Find valid futures (closest expiry)
932
- FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
933
-
934
- // For MCX, often spot sym is empty, use Future as reference
935
- if(SPOT_SYM === "" && FUT_SYMS.length > 0) SPOT_SYM = FUT_SYMS[0].s;
936
-
937
- document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('_EQ')) ? "SPOT PRICE" : "FUT REFERENCE";
938
-
939
- setupExpiries();
940
- document.getElementById('loader').style.display = 'none';
941
-
942
- } catch(e) {
943
- alert("Error: " + e.message);
944
- document.getElementById('loader').style.display = 'none';
945
- }
946
  }
947
 
948
- function setupExpiries() {
949
- const opts = CHAIN_DATA.filter(x => x.t === 'CE' || x.t === 'PE');
950
- const exps = [...new Set(opts.map(i => i.e))].sort((a,b) => a-b);
951
- const today = Math.floor(Date.now()/1000) - 86400;
952
- const validExps = exps.filter(ts => ts > today);
953
 
954
- if(validExps.length === 0) {
955
- document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
956
- return;
957
- }
958
 
959
- ACTIVE_EXP = validExps[0];
960
- updateExpText(ACTIVE_EXP);
961
 
962
- const grid = document.getElementById('expGrid');
963
- grid.innerHTML = validExps.map(ts => {
964
- const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
965
- return `<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${dStr}</div>`;
966
- }).join('');
967
 
968
- buildChain();
969
- }
970
-
971
- function selectExp(ts) {
972
- ACTIVE_EXP = ts;
973
- updateExpText(ts);
974
- closeModal('expModal');
975
- document.querySelectorAll('.opt-btn').forEach(b => {
976
- if(b.innerText === new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}))
977
- b.classList.add('active');
978
- else b.classList.remove('active');
979
- });
980
- buildChain();
981
- }
982
-
983
- function updateExpText(ts) {
984
- document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
985
- }
986
-
987
- function buildChain() {
988
- const body = document.getElementById('chainBody');
989
- const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
990
- OPT_SYMS = relevant;
991
-
992
- const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b);
993
-
994
- body.innerHTML = strikes.map(k => `
995
- <div class="row" id="row-${k}" data-k="${k}">
996
- <div class="graph-col">
997
- <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
998
- <div class="visual-bar" id="barL-${k}"></div>
999
- <span class="strike-txt">${k}</span>
1000
- <span class="delta-txt" id="valL-${k}">-</span>
1001
- </div>
1002
- </div>
1003
- <div class="graph-col">
1004
- <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
1005
- <div class="visual-bar" id="barR-${k}"></div>
1006
- <span class="strike-txt">${k}</span>
1007
- <span class="delta-txt" id="valR-${k}">-</span>
1008
- </div>
1009
- </div>
1010
- </div>
1011
- `).join('');
1012
-
1013
- window.scrolled = false;
1014
- restartPoller();
1015
- }
1016
-
1017
- function restartPoller() {
1018
- if(POLLER) clearInterval(POLLER);
1019
- POLLER = setInterval(pollData, 1500);
1020
- pollData();
1021
- }
1022
-
1023
- async function pollData() {
1024
- if(IS_POLLING || !CURRENT_ROOT) return;
1025
- IS_POLLING = true;
1026
 
1027
  try {
1028
- let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
1029
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
 
 
 
 
1030
 
1031
- if(kList.length > 0) {
1032
- const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
1033
- let selectedK = kList.find(k => k >= refPrice);
1034
- if (selectedK === undefined) selectedK = kList[kList.length - 1];
1035
-
1036
- ATM_STRIKE = selectedK;
1037
- const atmIdx = kList.indexOf(selectedK);
1038
-
1039
- document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
1040
- const atmRow = document.getElementById(`row-${selectedK}`);
1041
- if(atmRow) {
1042
- atmRow.classList.add('atm');
1043
- if(!window.scrolled && SPOT_PRICE > 0) {
1044
- atmRow.scrollIntoView({block:'center', behavior:'auto'});
1045
- window.scrolled = true;
1046
- }
1047
- }
1048
-
1049
- let maxRange = 10;
1050
- if(CONFIG.side && Array.isArray(CONFIG.side)) {
1051
- CONFIG.side.forEach(item => { if(item.ranges) { const m = Math.max(...item.ranges); if(m > maxRange) maxRange = m; } });
1052
- }
1053
-
1054
- const start = Math.max(0, atmIdx - 20);
1055
- const end = Math.min(kList.length, atmIdx + 20);
1056
- const viewKs = kList.slice(start, end);
1057
-
1058
- const rangeStart = Math.max(0, atmIdx - maxRange);
1059
- const rangeEnd = Math.min(kList.length, atmIdx + maxRange + 1);
1060
- const calcKs = kList.slice(rangeStart, rangeEnd);
1061
-
1062
- const allNeededKs = new Set([...viewKs, ...calcKs]);
1063
-
1064
- OPT_SYMS.forEach(o => {
1065
- if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s);
1066
- });
1067
  }
1068
-
1069
- const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
1070
- if(uniqueSyms.length === 0) return;
1071
 
1072
- const url = `https://api.upstox.com/v2/market-quote/quotes?instrument_key=${uniqueSyms.join(',')}`;
1073
- const res = await fetch(url, { headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${UPSTOX_TOKEN}` }});
 
 
 
 
 
1074
 
1075
- if(res.status === 401) { location.href = '/logout'; return; }
1076
 
1077
- const json = await res.json();
1078
- if(json.status === 'success') {
1079
- updateUI(json.data);
1080
- const dot = document.getElementById('statusDot');
1081
- dot.classList.add('active');
1082
- dot.classList.remove('error');
1083
- }
1084
- } catch(e) {
1085
- console.error(e);
1086
- const dot = document.getElementById('statusDot');
1087
- dot.classList.remove('active');
1088
- dot.classList.add('error');
1089
- }
1090
- finally { IS_POLLING = false; }
1091
- }
1092
-
1093
- function updateUI(raw_data) {
1094
- // Create a Token Map for easy lookup since Upstox keys by Symbol
1095
- const tokenMap = {};
1096
- for(const key in raw_data) {
1097
- const item = raw_data[key];
1098
- if(item && item.instrument_token) {
1099
- tokenMap[item.instrument_token] = item;
1100
- }
1101
- }
1102
-
1103
- // Update Spot
1104
- const spotData = tokenMap[SPOT_SYM];
1105
- if(spotData) {
1106
- SPOT_PRICE = spotData.last_price;
1107
- document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
1108
- }
1109
-
1110
- // Update Futures
1111
- const futHTML = FUT_SYMS.map(f => {
1112
- const d = tokenMap[f.s];
1113
- if(!d) return '';
1114
- const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
1115
- const b = d.total_buy_quantity || 0;
1116
- const s = d.total_sell_quantity || 0;
1117
- const ch = d.net_change || 0;
1118
- const tot = (b + s) || 1;
1119
- const bPct = (b/tot)*100;
1120
- return `
1121
- <div class="fut-card">
1122
- <div style="display:flex; justify-content:space-between; font-weight:700;">
1123
- <span>${dt} FUT</span> <span style="color:${ch>=0?'var(--green)':'var(--red)'}">${d.last_price}</span>
1124
- </div>
1125
- <div class="meter-bg">
1126
- <div style="width:${bPct}%; background:var(--green)"></div>
1127
- <div style="width:${100-bPct}%; background:var(--red)"></div>
1128
- </div>
1129
- <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
1130
- <span>B: ${fmt(b)}</span> <span>S: ${fmt(s)}</span>
1131
- </div>
1132
- </div>`;
1133
- }).join('');
1134
- if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
1135
-
1136
- // Process Options
1137
- const rowData = {};
1138
- for(const item of OPT_SYMS) {
1139
- const d = tokenMap[item.s];
1140
- if(!d) continue;
1141
-
1142
- if(!rowData[item.k]) rowData[item.k] = {
1143
- CS:0, CB:0, PS:0, PB:0,
1144
- CO:0, PO:0, CLTP:0, PLTP:0, CV:0, PV:0
1145
- };
1146
-
1147
- if(item.t === 'CE') {
1148
- rowData[item.k].CS = d.total_sell_quantity || 0;
1149
- rowData[item.k].CB = d.total_buy_quantity || 0;
1150
- rowData[item.k].CO = d.oi || 0;
1151
- rowData[item.k].CLTP = d.last_price || 0;
1152
- rowData[item.k].CV = d.volume || 0;
1153
- }
1154
- if(item.t === 'PE') {
1155
- rowData[item.k].PS = d.total_sell_quantity || 0;
1156
- rowData[item.k].PB = d.total_buy_quantity || 0;
1157
- rowData[item.k].PO = d.oi || 0;
1158
- rowData[item.k].PLTP = d.last_price || 0;
1159
- rowData[item.k].PV = d.volume || 0;
1160
- }
1161
- }
1162
-
1163
- LATEST_ROW_DATA = rowData;
1164
-
1165
- // --- RENDER CHAIN ---
1166
- let maxL = 0, maxR = 0;
1167
- const processedRows = {};
1168
-
1169
- for(const k in rowData) {
1170
- const vars = rowData[k];
1171
- processedRows[k] = {
1172
- left: evaluateFormula(CONFIG.left.formula, vars),
1173
- right: evaluateFormula(CONFIG.right.formula, vars)
1174
- };
1175
- maxL = Math.max(maxL, Math.abs(processedRows[k].left));
1176
- maxR = Math.max(maxR, Math.abs(processedRows[k].right));
1177
- }
1178
-
1179
- for(const k in processedRows) {
1180
- const r = processedRows[k];
1181
 
1182
- const valL = document.getElementById(`valL-${k}`);
1183
- if(valL) {
1184
- valL.innerText = (r.left>0?"+":"") + fmt(r.left);
1185
- valL.style.color = r.left >= 0 ? 'var(--green)' : 'var(--red)';
 
1186
 
1187
- const barL = document.getElementById(`barL-${k}`);
1188
- if(maxL > 0) {
1189
- const pct = (Math.abs(r.left) / maxL) * 100;
1190
- barL.style.left = '0%';
1191
- barL.style.width = pct + '%';
1192
- barL.style.background = r.left >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';
1193
- }
1194
- }
1195
-
1196
- const valR = document.getElementById(`valR-${k}`);
1197
- if(valR) {
1198
- valR.innerText = (r.right>0?"+":"") + fmt(r.right);
1199
- valR.style.color = r.right >= 0 ? 'var(--green)' : 'var(--red)';
1200
 
1201
- const barR = document.getElementById(`barR-${k}`);
1202
- if(maxR > 0) {
1203
- const pct = (Math.abs(r.right) / maxR) * 100;
1204
- barR.style.left = '0%';
1205
- barR.style.width = pct + '%';
1206
- barR.style.background = r.right >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';
1207
- }
1208
- }
1209
- }
1210
-
1211
- // --- SIDEBAR AGGREGATION ---
1212
- if(SPOT_PRICE > 0 && CONFIG.side && CONFIG.side.length > 0) {
1213
- const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
1214
- const lowers = kList.filter(k => k < SPOT_PRICE);
1215
- const uppers = kList.filter(k => k >= SPOT_PRICE);
1216
-
1217
- CONFIG.side.forEach((item, idx) => {
1218
- item.ranges.forEach((rng, rIdx) => {
1219
- const el = document.getElementById(`sv-${idx}-${rIdx}`);
1220
- if(el) {
1221
- const selLow = lowers.slice(-rng);
1222
- const selHigh = uppers.slice(0, rng);
1223
- const targetStrikes = [...selLow, ...selHigh];
1224
-
1225
- let total = 0;
1226
- for(const k of targetStrikes) {
1227
- if(rowData[k]) total += evaluateFormula(item.formula, rowData[k]);
1228
  }
1229
- el.innerText = (total > 0 ? "+" : "") + fmt(total);
1230
- el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)';
1231
  }
1232
  });
1233
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1234
  }
1235
  }
1236
-
1237
- function fmt(n) {
1238
- if(!n) return '0';
1239
- let abs = Math.abs(n);
1240
- if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
1241
- if(abs>=100000) return (n/100000).toFixed(2)+'L';
1242
- if(abs>=1000) return (n/1000).toFixed(1)+'k';
1243
- return n.toLocaleString();
1244
- }
1245
  </script>
1246
  </body>
1247
  </html>
 
2
  import requests
3
  import uvicorn
4
  import json
5
+ import io
6
+ from datetime import datetime, timedelta
7
+ from fastapi import FastAPI, Query, Body
8
+ from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from typing import Optional, List
 
11
 
12
  # ==========================================
13
+ # 1. CONFIGURATION (From upstox4.txt)
14
  # ==========================================
15
+ SB_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
16
+ SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
17
+
18
+ HEADERS = {
19
+ "apikey": SB_KEY,
20
+ "Authorization": f"Bearer {SB_KEY}",
21
+ "Content-Type": "application/json",
22
+ "Prefer": "return=representation"
23
+ }
24
 
25
  app = FastAPI()
26
 
27
  # ==========================================
28
+ # 2. SUPABASE HELPER FUNCTIONS
29
  # ==========================================
30
 
31
+ def sb_get(endpoint: str, params: dict = None):
32
+ """Generic Supabase GET request"""
33
+ try:
34
+ url = f"{SB_URL}/rest/v1{endpoint}"
35
+ r = requests.get(url, headers=HEADERS, params=params)
36
+ r.raise_for_status()
37
+ return r.json()
38
+ except Exception as e:
39
+ print(f"DB Error: {e}")
40
+ return []
41
 
42
+ # ==========================================
43
+ # 3. API ROUTES
44
+ # ==========================================
 
 
 
 
 
 
 
45
 
46
+ @app.get("/")
47
+ def home():
48
+ return HTMLResponse(HTML_TEMPLATE)
49
+
50
+ @app.get("/api/dates")
51
+ def get_available_dates():
52
+ """Fetch distinct dates available in the history DB."""
53
+ # Supabase doesn't support 'select distinct' easily on timestamps without RPC
54
+ # We fetch unique dates by querying a range or assuming data exists for recent days.
55
+ # Strategy: Fetch distinct 'minute' ordered descending, truncate to date in Python
56
+ # To save bandwidth, we limit to last 1000 buckets and extract dates
57
+ data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "2000"})
58
+ dates = set()
59
+ for row in data:
60
+ dt_str = row['minute'].split('T')[0]
61
+ dates.add(dt_str)
62
+ return JSONResponse(sorted(list(dates), reverse=True))
63
+
64
+ @app.get("/api/roots")
65
+ def get_roots(date: str):
66
+ """Fetch symbols (roots) available for a specific date."""
67
+ # Filter by the 24h range of that date
68
+ start = f"{date}T00:00:00"
69
+ end = f"{date}T23:59:59"
70
 
71
+ # We use a hack to get distinct roots: fetch all rows for date (lightweight select)
72
+ params = {
73
+ "select": "root",
74
+ "minute": f"gte.{start}",
75
+ "minute": f"lte.{end}"
76
+ }
77
+ # To avoid URL encoding issues in python requests dict, handle duplicate keys manually if needed
78
+ # But requests params dict doesn't support duplicate keys for 'minute'.
79
+ # Supabase allows 'and' logic naturally.
80
+ # Using raw query string for safety with multiple filters on same column
81
+ url = f"/history_buckets_upstox?select=root&minute=gte.{start}&minute=lte.{end}"
82
+ data = sb_get(url)
83
+
84
+ roots = set()
85
+ for row in data:
86
+ roots.add(row['root'])
87
+ return JSONResponse(sorted(list(roots)))
88
+
89
+ @app.get("/api/expiries")
90
+ def get_expiries(date: str, root: str):
91
+ """Fetch available expiries for a root on a specific date."""
92
+ start = f"{date}T00:00:00"
93
+ end = f"{date}T23:59:59"
94
+
95
+ url = f"/history_buckets_upstox?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}"
96
+ data = sb_get(url)
97
+
98
+ exps = set()
99
+ for row in data:
100
+ exps.add(row['expiry'])
101
+ return JSONResponse(sorted(list(exps)))
102
+
103
+ @app.get("/api/instruments")
104
+ def get_instruments(date: str, root: str, expiry: str):
105
+ """
106
+ Fetch one bucket to parse the JSON keys and find available Instruments
107
+ (e.g., NIFTY24JANFUT, RELIANCE, NIFTY24JAN21500CE).
108
+ """
109
+ start = f"{date}T00:00:00"
110
+ end = f"{date}T23:59:59"
111
+
112
+ # Fetch just 1 row to analyze the keys inside 'data' column
113
+ url = f"/history_buckets_upstox?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=1"
114
+ res = sb_get(url)
115
+
116
+ instruments = set()
117
+ if res and len(res) > 0:
118
+ bucket_data = res[0]['data']
119
+ instruments = bucket_data.keys()
120
+
121
+ return JSONResponse(sorted(list(instruments)))
122
+
123
+ @app.post("/api/plot")
124
+ def get_plot_data(
125
+ date: str = Body(...),
126
+ root: str = Body(...),
127
+ expiry: str = Body(...),
128
+ instrument: str = Body(...),
129
+ timeframe: str = Body(...), # "1min", "3min", "5min"
130
+ start_time: str = Body(...), # "HH:MM"
131
+ end_time: str = Body(...) # "HH:MM"
132
+ ):
133
+ """
134
+ Core Logic:
135
+ 1. Fetch buckets within time range.
136
+ 2. Extract 6-slot data for the specific instrument.
137
+ 3. Flatten into a TimeSeries.
138
+ 4. Resample to TimeFrame using Median.
139
+ 5. Return JSON for Chart.js.
140
+ """
141
+
142
+ # 1. Build Query
143
+ t_start = f"{date}T{start_time}:00"
144
+ t_end = f"{date}T{end_time}:59"
145
+
146
+ url = f"/history_buckets_upstox?select=minute,data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
147
+ rows = sb_get(url)
148
+
149
+ if not rows:
150
+ return JSONResponse({"error": "No data found for this selection"})
151
 
152
+ # 2. Process Data into DataFrame
153
+ # Columns: timestamp, price, buy_qty, sell_qty, volume, oi
154
+ records = []
155
+
156
+ for row in rows:
157
+ minute_str = row['minute'] # e.g., "2023-10-27T09:15:00"
158
+ try:
159
+ base_ts = datetime.fromisoformat(minute_str)
160
+ except:
161
+ continue # Skip malformed dates
162
+
163
+ json_data = row['data']
164
 
165
+ # Check if instrument exists in this bucket
166
+ if instrument not in json_data:
167
+ continue
168
+
169
+ inst_data = json_data[instrument]
170
 
171
+ # Data keys: 'p' (price), 'b' (buy), 's' (sell), 'v' (vol), 'o' (oi)
172
+ # Each is an array of 6 items (0s, 10s, 20s, 30s, 40s, 50s)
173
 
174
+ for i in range(6):
175
+ # Calculate exact timestamp for this slot (10s intervals)
176
+ slot_ts = base_ts + timedelta(seconds=i*10)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # Safe extraction handling potential missing arrays or shorter arrays
179
+ p = inst_data.get('p', [None]*6)[i] if i < len(inst_data.get('p', [])) else None
180
+ b = inst_data.get('b', [None]*6)[i] if i < len(inst_data.get('b', [])) else None
181
+ s = inst_data.get('s', [None]*6)[i] if i < len(inst_data.get('s', [])) else None
182
+ v = inst_data.get('v', [None]*6)[i] if i < len(inst_data.get('v', [])) else None
183
+ o = inst_data.get('o', [None]*6)[i] if i < len(inst_data.get('o', [])) else None
184
 
185
+ # Skip if Price is None (usually indicates no tick data for that 10s slot)
186
+ if p is None:
187
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ records.append({
190
+ "ts": slot_ts,
191
+ "price": float(p),
192
+ "buy_qty": float(b) if b is not None else 0,
193
+ "sell_qty": float(s) if s is not None else 0,
194
+ "volume": float(v) if v is not None else 0,
195
+ "oi": float(o) if o is not None else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  })
197
 
198
+ if not records:
199
+ return JSONResponse({"error": "Instrument found but contains no valid data points"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ df = pd.DataFrame(records)
202
+ df.set_index('ts', inplace=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
+ # 3. Resample Logic (Median)
205
+ # Map input timeframe (e.g., "1min") to Pandas Offset aliases (e.g., "1T")
206
+ tf_map = {
207
+ "1min": "1T", "3min": "3T", "5min": "5T",
208
+ "15min": "15T", "30min": "30T", "1hour": "1H"
 
 
 
 
 
 
 
 
 
209
  }
210
+ panda_tf = tf_map.get(timeframe, "1T")
211
 
212
+ # Resample
213
+ # For OHLC, we usually take Open/High/Low/Close.
214
+ # But prompt asks specifically: "do average... get the median value... ignore null"
215
+ # We will produce a single value per timeframe per variable using Median.
216
 
217
+ resampled = df.resample(panda_tf).median()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ # Remove rows where all data is NaN (gaps in data)
220
+ resampled.dropna(how='all', inplace=True)
 
221
 
222
+ # 4. Prepare Response
223
+ # Convert timestamps to string for JS
224
+ labels = resampled.index.strftime('%H:%M').tolist()
225
+
226
+ response_data = {
227
+ "labels": labels,
228
+ "price": resampled['price'].tolist(),
229
+ "buy_qty": resampled['buy_qty'].tolist(),
230
+ "sell_qty": resampled['sell_qty'].tolist(),
231
+ "volume": resampled['volume'].tolist(),
232
+ "oi": resampled['oi'].tolist(),
233
+ }
234
+
235
+ return JSONResponse(response_data)
 
 
 
 
 
236
 
237
 
238
  # ==========================================
239
+ # 4. FRONTEND (HTML/JS)
240
  # ==========================================
241
 
242
  HTML_TEMPLATE = """
 
244
  <html lang="en">
245
  <head>
246
  <meta charset="UTF-8">
247
+ <title>DepthChain Historical</title>
248
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
249
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
250
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
251
  <style>
252
  :root {
253
  --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
254
  --text-main: #1a1a1a; --text-sub: #555555;
255
+ --accent: #2962ff; --green: #00c853; --red: #d50000;
 
 
 
256
  }
257
  * { box-sizing: border-box; }
258
+ body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
+ /* Top Header */
261
+ .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; flex-shrink:0; }
262
+ .logo { font-family: 'JetBrains Mono', monospace; font-weight: 800; font-size: 16px; letter-spacing: -0.5px; }
263
+ .logo span { color: var(--accent); }
264
 
265
+ /* Layout */
266
+ .container { display: flex; flex: 1; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ /* Sidebar Controls */
269
+ .sidebar { width: 300px; background: var(--panel); border-right: 1px solid var(--border); padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
270
 
271
+ .control-group { display: flex; flex-direction: column; gap: 5px; }
272
+ .label { font-size: 11px; font-weight: 700; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
273
 
274
+ select, input { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 6px; font-family: 'JetBrains Mono'; font-size: 12px; outline: none; background: #fff; }
275
+ select:focus, input:focus { border-color: var(--accent); }
276
 
277
+ .btn { padding: 12px; background: var(--accent); color: white; border: none; border-radius: 6px; font-weight: 700; cursor: pointer; font-size: 13px; margin-top: 10px; transition: 0.2s; }
278
+ .btn:hover { background: #1e4bd6; }
279
+ .btn:disabled { background: #ccc; cursor: not-allowed; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
+ .checkbox-group { display: flex; flex-direction: column; gap: 8px; background: #fff; padding: 10px; border: 1px solid #ddd; border-radius: 6px; }
282
+ .cb-item { display: flex; align-items: center; gap: 10px; font-size: 12px; font-weight: 600; }
 
 
283
 
284
+ /* Main Chart Area */
285
+ .chart-area { flex: 1; padding: 20px; display: flex; flex-direction: column; position: relative; overflow-y:auto; }
286
+ .chart-container { position: relative; height: 100%; width: 100%; min-height: 400px; }
 
287
 
288
+ /* Loading Overlay */
289
+ .loader-ov { position: absolute; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.8); display:none; justify-content:center; align-items:center; z-index: 10; backdrop-filter: blur(2px); }
290
+ .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
292
+
293
+ /* Multiple Charts Layout */
294
+ .multi-chart-grid { display: grid; grid-template-rows: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; height: 100%; }
295
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  </style>
297
  </head>
298
  <body>
299
 
300
+ <div class="top-nav">
301
+ <div class="logo">DepthChain <span>HISTORICAL</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  </div>
303
 
304
+ <div class="container">
305
+ <div class="sidebar">
306
+ <!-- 1. DATE -->
307
+ <div class="control-group">
308
+ <span class="label">1. Select Date</span>
309
+ <select id="selDate" onchange="loadRoots()"><option>Loading...</option></select>
 
 
 
 
310
  </div>
311
+
312
+ <!-- 2. ROOT SYMBOL -->
313
+ <div class="control-group">
314
+ <span class="label">2. Root Symbol</span>
315
+ <select id="selRoot" onchange="loadExpiries()" disabled><option>--</option></select>
316
  </div>
 
 
 
317
 
318
+ <!-- 3. EXPIRY TYPE -->
319
+ <div class="control-group">
320
+ <span class="label">3. Expiry / Type</span>
321
+ <select id="selExpiry" onchange="loadInstruments()" disabled><option>--</option></select>
 
 
 
 
 
 
322
  </div>
 
 
 
 
 
 
 
 
323
 
324
+ <!-- 4. TIME RANGE -->
325
+ <div class="control-group">
326
+ <span class="label">4. Time Range (HH:MM)</span>
327
+ <div style="display:flex; gap:5px;">
328
+ <input type="time" id="startTime" value="09:15">
329
+ <input type="time" id="endTime" value="15:30">
330
+ </div>
 
 
 
331
  </div>
332
+
333
+ <!-- 5. TIME FRAME -->
334
+ <div class="control-group">
335
+ <span class="label">5. Time Frame</span>
336
+ <select id="selTF">
337
+ <option value="1min">1 Minute</option>
338
+ <option value="3min">3 Minutes</option>
339
+ <option value="5min">5 Minutes</option>
340
+ <option value="15min">15 Minutes</option>
341
+ <option value="30min">30 Minutes</option>
342
+ <option value="1hour">1 Hour</option>
343
+ </select>
344
  </div>
 
 
 
 
 
 
345
 
346
+ <!-- 6. INSTRUMENT -->
347
+ <div class="control-group">
348
+ <span class="label">6. Specific Instrument</span>
349
+ <select id="selInst" disabled><option>--</option></select>
 
 
 
 
 
 
 
 
 
 
 
 
350
  </div>
 
 
 
 
 
 
 
 
 
 
351
 
352
+ <!-- 7. VARIABLES -->
353
+ <div class="control-group">
354
+ <span class="label">7. Variables to Plot</span>
355
+ <div class="checkbox-group">
356
+ <div class="cb-item"><input type="checkbox" id="vPrice" checked> Price</div>
357
+ <div class="cb-item"><input type="checkbox" id="vVol"> Volume</div>
358
+ <div class="cb-item"><input type="checkbox" id="vOI" checked> OI (Open Interest)</div>
359
+ <div class="cb-item"><input type="checkbox" id="vBuy"> Total Buy Qty</div>
360
+ <div class="cb-item"><input type="checkbox" id="vSell"> Total Sell Qty</div>
 
361
  </div>
362
  </div>
363
+
364
+ <!-- 8. CHART MODE -->
365
+ <div class="control-group">
366
+ <span class="label">8. Plot Mode</span>
367
+ <select id="selMode">
368
+ <option value="multi">Separate Charts (Recommended)</option>
369
+ <option value="single">Same Chart (Dual Axis)</option>
370
+ </select>
371
  </div>
372
+
373
+ <button class="btn" onclick="plotData()">PLOT DATA</button>
374
+ <div id="statusMsg" style="font-size:10px; color:#d50000; text-align:center; min-height:15px;"></div>
375
  </div>
376
+
377
+ <div class="chart-area" id="chartArea">
378
+ <div class="loader-ov" id="loader"><div class="spinner"></div></div>
379
+ <!-- Canvas elements will be injected here -->
380
+ <div style="color:#aaa; display:flex; justify-content:center; align-items:center; height:100%;">
381
+ SELECT PARAMETERS AND CLICK PLOT
 
 
 
 
 
 
 
 
 
 
382
  </div>
383
  </div>
384
  </div>
385
 
386
  <script>
387
+ // State
388
+ let CHARTS = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
+ // Init
391
+ window.onload = async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  try {
393
+ const res = await fetch('/api/dates');
394
+ const dates = await res.json();
395
+ const sel = document.getElementById('selDate');
396
+ sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
397
+ if(dates.length > 0) loadRoots();
398
+ } catch(e) {
399
+ console.error(e);
400
+ document.getElementById('selDate').innerHTML = '<option>Error Loading Dates</option>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  }
402
+ };
403
 
404
+ async function loadRoots() {
405
+ const date = document.getElementById('selDate').value;
406
+ const sel = document.getElementById('selRoot');
407
+ sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  try {
410
+ const res = await fetch(`/api/roots?date=${date}`);
411
+ const roots = await res.json();
412
+ sel.innerHTML = roots.map(r => `<option value="${r}">${r}</option>`).join('');
413
+ sel.disabled = false;
414
+ if(roots.length > 0) loadExpiries();
415
+ } catch(e) { sel.innerHTML = '<option>Error</option>'; }
 
 
 
 
416
  }
417
 
418
+ async function loadExpiries() {
419
+ const date = document.getElementById('selDate').value;
420
+ const root = document.getElementById('selRoot').value;
421
+ const sel = document.getElementById('selExpiry');
422
+ sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
423
+
 
 
 
 
 
424
  try {
425
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
426
+ const exps = await res.json();
427
+ sel.innerHTML = exps.map(e => `<option value="${e}">${e}</option>`).join('');
428
+ sel.disabled = false;
429
+ if(exps.length > 0) loadInstruments();
430
+ } catch(e) { sel.innerHTML = '<option>Error</option>'; }
 
 
 
 
 
 
431
  }
432
 
433
+ async function loadInstruments() {
434
+ const date = document.getElementById('selDate').value;
435
+ const root = document.getElementById('selRoot').value;
436
+ const exp = document.getElementById('selExpiry').value;
437
+ const sel = document.getElementById('selInst');
438
+ sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
439
 
440
  try {
441
+ const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
442
+ const insts = await res.json();
443
+ sel.innerHTML = insts.map(i => `<option value="${i}">${i}</option>`).join('');
444
+ sel.disabled = false;
445
+ } catch(e) { sel.innerHTML = '<option>Error</option>'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  }
447
 
448
+ async function plotData() {
449
+ const inst = document.getElementById('selInst').value;
450
+ if(!inst || inst === '--') { alert("Please select an instrument"); return; }
 
 
451
 
452
+ const loader = document.getElementById('loader');
453
+ const area = document.getElementById('chartArea');
454
+ const msg = document.getElementById('statusMsg');
 
455
 
456
+ msg.innerText = "";
457
+ loader.style.display = 'flex';
458
 
459
+ // Destroy old charts
460
+ CHARTS.forEach(c => c.destroy());
461
+ CHARTS = [];
 
 
462
 
463
+ const payload = {
464
+ date: document.getElementById('selDate').value,
465
+ root: document.getElementById('selRoot').value,
466
+ expiry: document.getElementById('selExpiry').value,
467
+ instrument: inst,
468
+ timeframe: document.getElementById('selTF').value,
469
+ start_time: document.getElementById('startTime').value,
470
+ end_time: document.getElementById('endTime').value
471
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
  try {
474
+ const res = await fetch('/api/plot', {
475
+ method: 'POST',
476
+ headers: {'Content-Type': 'application/json'},
477
+ body: JSON.stringify(payload)
478
+ });
479
+ const data = await res.json();
480
 
481
+ if(data.error) {
482
+ msg.innerText = data.error;
483
+ loader.style.display = 'none';
484
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
 
 
 
486
 
487
+ // Determine Variables
488
+ const vars = [];
489
+ if(document.getElementById('vPrice').checked) vars.push({k:'price', l:'Price', c:'#2962ff'});
490
+ if(document.getElementById('vVol').checked) vars.push({k:'volume', l:'Volume', c:'#ff6d00'});
491
+ if(document.getElementById('vOI').checked) vars.push({k:'oi', l:'Open Interest', c:'#00c853'});
492
+ if(document.getElementById('vBuy').checked) vars.push({k:'buy_qty', l:'Buy Qty', c:'#00b0ff'});
493
+ if(document.getElementById('vSell').checked) vars.push({k:'sell_qty', l:'Sell Qty', c:'#d50000'});
494
 
495
+ if(vars.length === 0) { alert("Select at least one variable"); loader.style.display = 'none'; return; }
496
 
497
+ const mode = document.getElementById('selMode').value;
498
+ area.innerHTML = ''; // Clear container
499
+ area.appendChild(loader); // Re-append loader to keep it visible while rendering
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
+ if(mode === 'single') {
502
+ // Same Chart
503
+ const cvs = document.createElement('canvas');
504
+ cvs.style.maxHeight = '100%';
505
+ area.appendChild(cvs);
506
 
507
+ const datasets = vars.map((v, idx) => ({
508
+ label: v.l,
509
+ data: data[v.k],
510
+ borderColor: v.c,
511
+ backgroundColor: v.c,
512
+ borderWidth: 1.5,
513
+ pointRadius: 0,
514
+ pointHoverRadius: 4,
515
+ yAxisID: idx === 0 ? 'y' : 'y1', // Simplistic dual axis: First var left, others right
516
+ tension: 0.1
517
+ }));
 
 
518
 
519
+ const ctx = cvs.getContext('2d');
520
+ const chart = new Chart(ctx, {
521
+ type: 'line',
522
+ data: { labels: data.labels, datasets: datasets },
523
+ options: {
524
+ responsive: true,
525
+ maintainAspectRatio: false,
526
+ interaction: { mode: 'index', intersect: false },
527
+ scales: {
528
+ x: { grid: { display:false } },
529
+ y: { type: 'linear', display: true, position: 'left', grid: {color: '#f0f0f0'} },
530
+ y1: { type: 'linear', display: true, position: 'right', grid: {display: false} }
531
+ },
532
+ plugins: {
533
+ title: { display: true, text: `${inst} (${payload.timeframe})` }
 
 
 
 
 
 
 
 
 
 
 
 
534
  }
 
 
535
  }
536
  });
537
+ CHARTS.push(chart);
538
+
539
+ } else {
540
+ // Separate Charts
541
+ const grid = document.createElement('div');
542
+ grid.className = 'multi-chart-grid';
543
+ area.appendChild(grid);
544
+
545
+ vars.forEach(v => {
546
+ const wrap = document.createElement('div');
547
+ wrap.style.position = 'relative';
548
+ wrap.style.background = '#fff';
549
+ wrap.style.borderRadius = '8px';
550
+ wrap.style.border = '1px solid #eee';
551
+ wrap.style.padding = '10px';
552
+
553
+ const cvs = document.createElement('canvas');
554
+ wrap.appendChild(cvs);
555
+ grid.appendChild(wrap);
556
+
557
+ const ctx = cvs.getContext('2d');
558
+ const chart = new Chart(ctx, {
559
+ type: 'line',
560
+ data: {
561
+ labels: data.labels,
562
+ datasets: [{
563
+ label: v.l,
564
+ data: data[v.k],
565
+ borderColor: v.c,
566
+ backgroundColor: v.c + '22', // Transparent fill
567
+ borderWidth: 1.5,
568
+ fill: true,
569
+ pointRadius: 0,
570
+ tension: 0.1
571
+ }]
572
+ },
573
+ options: {
574
+ responsive: true,
575
+ maintainAspectRatio: false,
576
+ interaction: { mode: 'index', intersect: false },
577
+ scales: {
578
+ x: { grid: { display:false }, ticks: { maxTicksLimit: 10 } },
579
+ y: { grid: { color: '#f5f5f5' } }
580
+ },
581
+ plugins: {
582
+ title: { display: true, text: v.l, font: {size: 10} },
583
+ legend: { display: false }
584
+ }
585
+ }
586
+ });
587
+ CHARTS.push(chart);
588
+ });
589
+ }
590
+
591
+ } catch(e) {
592
+ console.error(e);
593
+ msg.innerText = "Processing Error";
594
+ } finally {
595
+ loader.style.display = 'none';
596
  }
597
  }
 
 
 
 
 
 
 
 
 
598
  </script>
599
  </body>
600
  </html>