topsecrettraders commited on
Commit
aa6d24d
·
verified ·
1 Parent(s): f0ab407

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1407 -404
app.py CHANGED
@@ -1,14 +1,14 @@
1
- --- START OF FILE app.py ---
2
  import pandas as pd
3
  import requests
4
  import uvicorn
5
  import json
6
  import asyncio
7
- import numpy as np
8
  from datetime import datetime, timedelta
9
  from fastapi import FastAPI, Body, HTTPException
10
  from fastapi.responses import HTMLResponse, JSONResponse
 
11
  from typing import Optional, List, Dict, Any
 
12
 
13
  # ==========================================
14
  # 1. CONFIGURATION
@@ -28,6 +28,7 @@ app = FastAPI()
28
  # 2. HELPER FUNCTIONS
29
  # ==========================================
30
  def sb_get(endpoint: str, params: dict = None):
 
31
  try:
32
  url = f"{SB_URL}/rest/v1{endpoint}"
33
  r = requests.get(url, headers=HEADERS, params=params)
@@ -37,6 +38,31 @@ def sb_get(endpoint: str, params: dict = None):
37
  print(f"DB Error ({endpoint}): {e}")
38
  return []
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  # ==========================================
41
  # 3. API ROUTES - METADATA
42
  # ==========================================
@@ -47,45 +73,177 @@ def home():
47
 
48
  @app.get("/api/dates")
49
  def get_dates():
50
- data = sb_get("/history_buckets_fyers", {"select": "minute", "order": "minute.desc", "limit": "20000"})
51
- dates = sorted(list(set(row['minute'].split('T')[0] for row in data if row.get('minute'))), reverse=True)
52
- return JSONResponse(dates)
 
 
 
 
53
 
54
  @app.get("/api/roots")
55
  def get_roots(date: str):
 
56
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
57
  data = sb_get(f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}")
58
- roots = sorted(list(set(row['root'] for row in data if row.get('root'))))
59
- return JSONResponse(roots)
 
 
 
60
 
61
- @app.get("/api/config_meta")
62
- def get_config_meta(date: str, root: str):
63
- """Returns expiries and categorizes instruments into Futures and Options."""
64
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
65
- data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&minute=gte.{start}&minute=lte.{end}&limit=100")
 
66
 
67
- expiries = sorted(list(set(row['expiry'] for row in data)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- # Analyze one bucket to separate Futures/Spot from Options
70
- futures = set()
71
- options = set()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  for row in data:
74
- for inst in row.get('data', {}).keys():
75
- if "CE" in inst or "PE" in inst:
76
- # Extract root-like part for options (e.g., NIFTY24DEC25000CE -> NIFTY24DEC)
77
- options.add(inst)
78
- else:
79
- futures.add(inst)
80
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  return JSONResponse({
82
- "expiries": expiries,
83
- "futures": sorted(list(futures)),
84
- "options": sorted(list(options))
85
  })
86
 
87
  # ==========================================
88
- # 4. CORE ENGINE - DATA FETCHING & ATM LOGIC
89
  # ==========================================
90
 
91
  @app.post("/api/fetch_series")
@@ -93,145 +251,279 @@ def fetch_series(
93
  date: str = Body(...),
94
  root: str = Body(...),
95
  expiry: str = Body(...),
96
- mode: str = Body("STANDARD"), # STANDARD or ADVANCED
97
- instrument: str = Body(None), # Used in Standard
98
- range_val: int = Body(5), # Used in Advanced (ATM +- 5)
99
- timeframe: str = Body("1min"),
100
  start_time: str = Body(...),
101
  end_time: str = Body(...)
102
  ):
 
 
 
 
103
  t_start = f"{date}T{start_time}:00"
104
  t_end = f"{date}T{end_time}:59"
105
 
106
- # 1. Fetch data. For Advanced mode, we need full JSON for the expiry.
107
- # We also need a Reference Price (usually the first Future/Spot found in MARKET expiry)
108
- if mode == "ADVANCED":
109
- # Get Option Data
110
- url_opt = f"/history_buckets_fyers?select=minute,data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
111
- # Get Reference Data (Spot/Future) to calculate ATM
112
- url_ref = f"/history_buckets_fyers?select=minute,data&root=eq.{root}&expiry=eq.MARKET&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
113
-
114
- opt_rows = sb_get(url_opt)
115
- ref_rows = sb_get(url_ref)
116
-
117
- ref_map = {r['minute']: r['data'] for r in ref_rows}
118
-
119
- records = []
120
- for row in opt_rows:
121
- ts_base = datetime.fromisoformat(row['minute'])
122
- opt_data = row.get('data', {})
123
- ref_data = ref_map.get(row['minute'], {})
124
 
125
- # Find a reference price (Spot preferred, then Future)
126
- def get_ref_p(idx):
127
- for sym, d in ref_data.items():
128
- if "INDEX" in sym or "-EQ" in sym or "FUT" in sym:
129
- if d.get('p') and len(d['p']) > idx and d['p'][idx]:
130
- return d['p'][idx]
131
- return None
132
-
133
- # Process 10s buckets
134
- for i in range(6):
135
- lp = get_ref_p(i)
136
- if lp is None: continue
137
-
138
- # Identify ATM and Range
139
- all_inst = opt_data.keys()
140
- # Parse strikes: Expecting format like ...25000CE
141
- strikes = []
142
- for inst in all_inst:
143
- try:
144
- # Extract numeric part before CE/PE
145
- s_str = "".join(filter(str.isdigit, inst[-7:]))
146
- strikes.append(int(s_str))
147
- except: pass
148
-
149
- if not strikes: continue
150
- strikes = sorted(list(set(strikes)))
151
- atm = min(strikes, key=lambda x: abs(x - lp))
152
- atm_idx = strikes.index(atm)
153
-
154
- # 11 strikes (5 ITM, ATM, 5 OTM)
155
- start_idx = max(0, atm_idx - range_val)
156
- end_idx = min(len(strikes), atm_idx + range_val + 1)
157
- target_strikes = strikes[start_idx:end_idx]
158
-
159
- # Aggregators
160
- cb, cs, pb, ps = 0, 0, 0, 0
161
- vol, oi = 0, 0
162
-
163
- for inst, d in opt_data.items():
164
- # Check if this inst is in our strike range
165
- inst_strike = next((s for s in target_strikes if str(s) in inst), None)
166
- if inst_strike:
167
- b = d['b'][i] if d.get('b') and len(d['b']) > i and d['b'][i] else 0
168
- s = d['s'][i] if d.get('s') and len(d['s']) > i and d['s'][i] else 0
169
- v = d['v'][i] if d.get('v') and len(d['v']) > i and d['v'][i] else 0
170
- o = d['o'][i] if d.get('o') and len(d['o']) > i and d['o'][i] else 0
171
-
172
- vol += v
173
- oi += o
174
- if "CE" in inst:
175
- cb += b; cs += s
176
- elif "PE" in inst:
177
- pb += b; ps += s
178
-
179
- records.append({
180
- "ts": ts_base + timedelta(seconds=i*10),
181
- "price": lp, "vol": vol, "oi": oi,
182
- "cb": cb, "cs": cs, "pb": pb, "ps": ps,
183
- "b": cb + pb, "s": cs + ps # Total Buy/Sell
184
- })
185
 
186
- else:
187
- # STANDARD MODE: Simple JSON extraction
188
- select_query = f'minute,inst_data:data->"{instrument}"'
189
- url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
190
- rows = sb_get(url)
191
- records = []
192
- for row in rows:
193
- base_ts = datetime.fromisoformat(row['minute'])
194
- d = row.get('inst_data')
195
- if not d: continue
196
  for i in range(6):
197
- if d.get('p') and len(d['p']) > i and d['p'][i] is not None:
198
  records.append({
199
  "ts": base_ts + timedelta(seconds=i*10),
200
- "price": float(d['p'][i]),
201
- "b": float(d['b'][i]) if d.get('b') and len(d['b']) > i and d['b'][i] else 0,
202
- "s": float(d['s'][i]) if d.get('s') and len(d['s']) > i and d['s'][i] else 0,
203
- "vol": float(d['v'][i]) if d.get('v') and len(d['v']) > i and d['v'][i] else 0,
204
- "oi": float(d['o'][i]) if d.get('o') and len(d['o']) > i and d['o'][i] else 0,
205
- "cb": 0, "cs": 0, "pb": 0, "ps": 0
206
  })
 
 
207
 
208
  if not records:
209
- return JSONResponse({"error": "No data found"})
210
 
211
- df = pd.DataFrame(records).set_index('ts')
 
 
 
 
 
 
 
212
 
213
- # Fill missing 10s data to avoid line distortion
214
- df = df.resample('10s').ffill()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- # Resample to Target Timeframe
217
- tf_map = {"1min": "1min", "3min": "3min", "5min": "5min", "15min": "15min", "30min": "30min", "1hour": "1h"}
218
- resampled = df.resample(tf_map.get(timeframe, "1min")).agg({
219
- 'price': 'last', 'vol': 'sum', 'oi': 'last',
220
- 'b': 'sum', 's': 'sum', 'cb': 'sum', 'cs': 'sum', 'pb': 'sum', 'ps': 'sum'
221
- }).dropna()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
- return JSONResponse({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  "labels": resampled.index.strftime('%H:%M').tolist(),
225
  "P": resampled['price'].tolist(),
226
- "V": resampled['vol'].tolist(),
 
 
227
  "OI": resampled['oi'].tolist(),
228
- "B": resampled['b'].tolist(),
229
- "S": resampled['s'].tolist(),
230
- "CB": resampled['cb'].tolist(),
231
- "CS": resampled['cs'].tolist(),
232
- "PB": resampled['pb'].tolist(),
233
- "PS": resampled['ps'].tolist()
234
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  # ==========================================
237
  # 5. FRONTEND TEMPLATE
@@ -242,401 +534,1112 @@ HTML_TEMPLATE = """
242
  <html lang="en">
243
  <head>
244
  <meta charset="UTF-8">
245
- <title>DepthChain PRO v5</title>
246
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
247
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
248
  <style>
 
249
  :root {
250
- --bg-body: #0d1117; --bg-header: #161b22; --bg-card: #0d1117; --bg-panel: #21262d;
251
- --bg-input: #161b22; --border: #30363d; --text-primary: #e6edf3; --text-secondary: #8b949e;
252
- --accent: #2f81f7; --success: #238636; --danger: #da3633; --warning: #d29922;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
- * { box-sizing: border-box; margin: 0; padding: 0; }
255
- body { background: var(--bg-body); color: var(--text-primary); font-family: 'Inter', sans-serif; display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px; }
256
-
257
- .top-bar { height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0; }
258
- .brand { font-weight: 700; font-size: 15px; color: var(--text-primary); }
259
- .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.1); padding: 2px 6px; border-radius: 4px; font-size: 11px; margin-left: 5px; }
260
-
261
- select, button, input { background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary); padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: 0.2s; }
262
- button.primary { background: var(--accent); color: white; border: none; font-weight: 600; }
263
- button.primary:hover { opacity: 0.9; }
 
 
 
 
 
 
 
264
 
265
- .main-container { flex: 1; display: flex; overflow-y: auto; }
266
- .column { flex: 1; border-right: 1px solid var(--border); display: flex; flex-direction: column; }
267
- .chart-card { border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
268
- .card-header { height: 40px; background: var(--bg-header); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border-bottom: 1px solid var(--border); }
269
- .card-body { flex: 1; position: relative; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- .status-dot { width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; }
272
- .dot-live { background: var(--success); box-shadow: 0 0 8px var(--success); }
273
- .dot-hist { background: var(--accent); }
 
 
 
 
 
 
 
 
274
 
275
  /* MODAL */
276
- .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: none; align-items: center; justify-content: center; z-index: 1000; }
277
- .modal { background: var(--bg-header); border: 1px solid var(--border); border-radius: 12px; width: 550px; max-width: 95%; overflow: hidden; }
278
- .modal-tab-bar { display: flex; border-bottom: 1px solid var(--border); background: var(--bg-body); }
279
- .modal-tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; color: var(--text-secondary); font-weight: 600; }
280
- .modal-tab.active { color: var(--accent); border-bottom: 2px solid var(--accent); background: var(--bg-header); }
281
- .modal-content { padding: 20px; }
282
- .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }
283
- .form-group label { display: block; font-size: 11px; color: var(--text-secondary); margin-bottom: 5px; text-transform: uppercase; font-weight: 700; }
284
-
285
- .series-row { display: grid; grid-template-columns: 35px 1fr 1fr 60px 30px; gap: 8px; align-items: center; margin-bottom: 8px; }
286
- .info-icon { display: inline-block; width: 14px; height: 14px; background: var(--bg-panel); color: var(--text-secondary); border-radius: 50%; text-align: center; font-size: 10px; line-height: 14px; cursor: help; margin-left: 5px; }
287
-
288
- .empty-state { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0.5; }
289
-
290
- .layout-btn { width: 32px; font-weight: bold; }
291
- .layout-btn.active { background: var(--accent); border-color: var(--accent); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  </style>
293
  </head>
294
  <body>
295
 
296
  <div class="top-bar">
297
- <div class="brand">DEPTHCHAIN <span>PRO v5</span></div>
298
- <div style="display:flex; gap:10px;">
299
- <select id="globalDate" onchange="App.onDateChange()"></select>
300
- <select id="globalMode" onchange="App.onModeChange()">
301
- <option value="LIVE">LIVE</option>
302
- <option value="HIST">HISTORY</option>
303
- </select>
304
- <div style="width:1px; background:var(--border); margin: 0 10px;"></div>
305
- <div id="layoutGroup">
 
 
 
306
  <button class="layout-btn" onclick="App.setLayout(1)">1</button>
307
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
308
- <button class="layout-btn" onclick="App.setLayout(3)">3</button>
 
309
  </div>
310
- <button onclick="App.reset()" style="color:var(--danger)">Reset</button>
 
311
  </div>
312
  </div>
313
 
314
  <div class="main-container" id="gridRoot"></div>
315
 
316
- <div class="modal-overlay" id="configModal">
 
317
  <div class="modal">
318
- <div class="modal-tab-bar">
319
- <div class="modal-tab" id="tabStandard" onclick="Config.setMode('STANDARD')">STANDARD MODE</div>
320
- <div class="modal-tab" id="tabAdvanced" onclick="Config.setMode('ADVANCED')">ADVANCED (ATM Range)</div>
 
 
 
321
  </div>
322
- <div class="modal-content">
323
- <div class="form-grid">
324
- <div class="form-group">
325
- <label>Root Symbol</label>
326
- <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
327
- </div>
328
- <div class="form-group">
329
- <label>Expiry</label>
330
- <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
331
- </div>
332
  </div>
333
-
334
- <div id="uiStandard">
335
- <div class="form-group">
336
- <label>Instrument</label>
337
- <select id="cfgInst" style="width:100%"></select>
 
 
 
 
338
  </div>
339
- </div>
340
 
341
- <div id="uiAdvanced" style="display:none">
342
- <div class="form-group">
343
- <label>ATM Strike Range (+-)<span class="info-icon" title="Aggregates 11 strikes: 5 ITM + ATM + 5 OTM">?</span></label>
344
- <input type="number" id="cfgRange" value="5" style="width:100%">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  </div>
346
- </div>
347
 
348
- <div class="form-grid" style="margin-top:15px;">
349
- <div class="form-group">
350
- <label>Timeframe</label>
351
- <select id="cfgTF" style="width:100%">
352
- <option value="1min">1 Min</option><option value="5min">5 Min</option><option value="15min">15 Min</option>
353
- </select>
354
- </div>
355
- <div class="form-group">
356
- <label>Market Hours</label>
357
- <div style="display:flex; gap:5px;">
358
- <input type="time" id="cfgStart" value="09:15" style="flex:1">
359
- <input type="time" id="cfgEnd" value="15:30" style="flex:1">
360
- </div>
361
  </div>
362
  </div>
 
 
 
 
 
 
 
363
 
364
- <div style="margin-top:20px; border-top:1px solid var(--border); padding-top:15px;">
365
- <label style="font-size:11px; font-weight:700; color:var(--text-secondary);">PLOT SERIES
366
- <span class="info-icon" title="Variables: $P(Price), $V(Vol), $O(OI), $B(Buy), $S(Sell), $CB(Call Buy), $CS(Call Sell), $PB(Put Buy), $PS(Put Sell)">?</span>
367
- </label>
368
- <div id="cfgSeriesList" style="margin-top:10px;"></div>
369
- <button onclick="Config.addSeries()" style="width:100%; margin-top:10px; border-style:dashed;">+ Add New Line</button>
370
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
- <div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
373
- <button onclick="Config.close()">Cancel</button>
374
- <button class="primary" onclick="Config.save()">Apply Configuration</button>
375
- </div>
 
 
 
 
 
 
 
 
376
  </div>
377
  </div>
378
  </div>
379
 
380
  <script>
381
  const App = {
382
- state: { date: '', mode: 'LIVE', columns: 3, charts: {}, refreshRate: 15 },
383
-
 
 
 
 
 
 
 
 
 
 
 
 
384
  async init() {
385
  await this.loadDates();
386
- const saved = localStorage.getItem('dc_pro_v5');
387
- if (saved) this.state = {...this.state, ...JSON.parse(saved)};
388
- else this.initDefault();
 
 
 
 
 
 
 
 
389
 
 
 
 
 
 
 
 
390
  this.renderGrid();
391
- setInterval(() => { if(this.state.mode === 'LIVE') this.refreshAll(true); }, this.state.refreshRate * 1000);
 
 
 
 
 
 
 
 
 
 
 
392
  },
393
 
394
  async loadDates() {
395
- const res = await fetch('/api/dates');
396
- const dates = await res.json();
397
- const sel = document.getElementById('globalDate');
398
- sel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
399
- this.state.date = sel.value;
 
400
  },
401
 
402
- initDefault() {
403
- for(let i=0; i<3; i++) this.addChart(i);
 
 
 
404
  },
405
 
406
- addChart(col) {
407
- const id = 'ch_' + Math.random().toString(36).substr(2, 5);
408
- this.state.charts[id] = { col, config: {} };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  },
410
 
411
  setLayout(n) {
 
412
  this.state.columns = n;
 
 
413
  this.renderGrid();
414
  this.save();
415
  },
416
 
417
  renderGrid() {
418
- const root = document.getElementById('gridRoot');
419
- root.innerHTML = '';
420
- document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1)===this.state.columns));
 
 
421
 
422
- for(let i=0; i<this.state.columns; i++) {
423
  const col = document.createElement('div');
424
  col.className = 'column';
425
- const ids = Object.keys(this.state.charts).filter(k => this.state.charts[k].col === i);
426
-
 
427
  ids.forEach(id => {
428
- const card = this.createCard(id);
 
 
 
 
 
 
 
 
 
 
 
429
  col.appendChild(card);
430
- this.plot(id);
431
  });
432
 
433
- const addBtn = document.createElement('button');
434
- addBtn.innerText = '+ Add Chart';
435
- addBtn.style.margin = '10px';
436
- addBtn.onclick = () => { this.addChart(i); this.renderGrid(); };
437
- col.appendChild(addBtn);
438
- root.appendChild(col);
439
- }
440
  },
441
 
442
- createCard(id) {
443
  const div = document.createElement('div');
444
  div.className = 'chart-card';
445
- div.style.flex = '1';
446
- const cfg = this.state.charts[id].config;
 
 
 
 
447
  div.innerHTML = `
448
  <div class="card-header">
449
- <div style="display:flex; align-items:center;">
450
- <div class="status-dot ${this.state.mode==='LIVE'?'dot-live':'dot-hist'}" id="dot-${id}"></div>
451
- <b style="font-family:JetBrains Mono">${cfg.root || 'New Chart'}</b>
452
- <small style="margin-left:10px; color:var(--text-secondary)">${cfg.mode || ''}</small>
 
 
453
  </div>
454
- <div>
455
- <button onclick="Config.open('${id}')" style="height:24px; padding:0 5px;">⚙</button>
456
- <button onclick="App.removeChart('${id}')" style="height:24px; padding:0 5px; color:var(--danger)">✕</button>
457
  </div>
458
  </div>
459
  <div class="card-body" id="body-${id}">
460
- ${!cfg.root ? '<div class="empty-state">No Configuration</div>' : `<canvas id="canvas-${id}"></canvas>`}
 
 
 
 
 
461
  </div>
462
  `;
463
  return div;
464
  },
465
 
466
- removeChart(id) { delete this.state.charts[id]; this.renderGrid(); this.save(); },
467
- onDateChange() { this.state.date = document.getElementById('globalDate').value; this.refreshAll(); },
468
- onModeChange() { this.state.mode = document.getElementById('globalMode').value; this.refreshAll(); },
469
- refreshAll(isUpdate=false) { Object.keys(this.state.charts).forEach(id => this.plot(id, isUpdate)); },
470
- save() { localStorage.setItem('dc_pro_v5', JSON.stringify(this.state)); },
 
 
 
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  async plot(id, isUpdate=false) {
473
- const chart = this.state.charts[id];
 
 
474
  const cfg = chart.config;
475
- if(!cfg.root) return;
 
 
 
 
 
 
 
476
 
477
  try {
478
- const res = await fetch('/api/fetch_series', {
479
- method: 'POST',
480
- headers: {'Content-Type': 'application/json'},
481
- body: JSON.stringify({
482
- date: this.state.date, root: cfg.root, expiry: cfg.expiry,
483
- mode: cfg.mode, instrument: cfg.instrument, range_val: cfg.range_val,
484
- timeframe: cfg.timeframe, start_time: cfg.start, end_time: cfg.end
485
- })
486
- });
487
- const data = await res.json();
488
- if(data.error) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
- const ctx = document.getElementById(`canvas-${id}`);
491
- if(!ctx) return;
 
 
492
 
493
  const datasets = cfg.series.map(s => {
494
  const vals = data.labels.map((_, i) => {
495
- const params = {
496
- $P: data.P[i], $V: data.V[i], $O: data.OI[i],
497
- $B: data.B[i], $S: data.S[i],
498
- $CB: data.CB[i], $CS: data.CS[i], $PB: data.PB[i], $PS: data.PS[i]
499
- };
500
- let f = s.formula;
501
- Object.keys(params).forEach(k => f = f.replace(new RegExp('\\\\'+k, 'g'), params[k] || 0));
502
- try { return eval(f); } catch { return 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  });
504
  return {
505
  label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
506
- borderWidth: 1.5, pointRadius: 0, yAxisID: s.axis === 'right' ? 'y1' : 'y'
507
  };
508
  });
509
 
510
- if(chart.instance && !isUpdate) { chart.instance.destroy(); chart.instance = null; }
511
-
512
- if(chart.instance) {
513
- chart.instance.data.labels = data.labels;
514
- chart.instance.data.datasets = datasets;
515
- chart.instance.update('none');
516
- } else {
517
- chart.instance = new Chart(ctx, {
518
- type: 'line',
519
- data: { labels: data.labels, datasets },
520
- options: {
521
- responsive: true, maintainAspectRatio: false, animation: false,
522
- interaction: { mode: 'index', intersect: false },
523
- scales: {
524
- x: { grid: { color: '#21262d' }, ticks: { color: '#8b949e', maxTicksLimit: 8 } },
525
- y: { position: 'left', grid: { color: '#21262d' } },
526
- y1: { position: 'right', display: datasets.some(d => d.yAxisID === 'y1'), grid: { display: false } }
527
- },
528
- plugins: { legend: { display: false } }
529
- }
530
- });
531
- }
532
  } catch(e) { console.error(e); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  }
534
  };
535
 
536
  const Config = {
537
- currId: null, currMode: 'STANDARD',
 
 
 
 
538
 
539
  async open(id) {
540
- this.currId = id;
541
- const cfg = App.state.charts[id].config;
542
- this.setMode(cfg.mode || 'STANDARD');
543
  document.getElementById('configModal').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
- const roots = await fetch(`/api/roots?date=${App.state.date}`).then(r => r.json());
546
- document.getElementById('cfgRoot').innerHTML = roots.map(r => `<option>${r}</option>`).join('');
 
 
 
547
 
548
- if(cfg.root) {
549
- document.getElementById('cfgRoot').value = cfg.root;
 
 
 
 
 
 
 
 
 
 
 
550
  await this.onRootChange(cfg.expiry, cfg.instrument);
551
  }
552
 
553
- document.getElementById('cfgTF').value = cfg.timeframe || '1min';
554
- document.getElementById('cfgRange').value = cfg.range_val || 5;
555
- this.renderSeries(cfg.series || []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  },
557
 
558
- setMode(m) {
559
- this.currMode = m;
560
- document.getElementById('tabStandard').classList.toggle('active', m === 'STANDARD');
561
- document.getElementById('tabAdvanced').classList.toggle('active', m === 'ADVANCED');
562
- document.getElementById('uiStandard').style.display = m === 'STANDARD' ? 'block' : 'none';
563
- document.getElementById('uiAdvanced').style.display = m === 'ADVANCED' ? 'block' : 'none';
 
 
 
 
 
 
 
564
 
565
- // Reset default series for Advanced Mode
566
- if(m === 'ADVANCED' && document.getElementById('cfgSeriesList').children.length === 0) {
567
- this.renderSeries([
568
- { label: 'Buyers', formula: '$CB + $PS', color: '#238636', axis: 'left' },
569
- { label: 'Sellers', formula: '$CS + $PB', color: '#da3633', axis: 'left' }
570
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  }
572
  },
573
 
574
  async onRootChange(preExp, preInst) {
575
  const root = document.getElementById('cfgRoot').value;
576
- const meta = await fetch(`/api/config_meta?date=${App.state.date}&root=${root}`).then(r => r.json());
577
-
578
- document.getElementById('cfgExp').innerHTML = meta.expiries.map(e => `<option>${e}</option>`).join('');
579
- if(preExp) document.getElementById('cfgExp').value = preExp;
 
580
 
581
- this.meta = meta;
582
- this.onExpChange(preInst);
 
 
 
583
  },
584
 
585
- onExpChange(preInst) {
 
586
  const exp = document.getElementById('cfgExp').value;
587
- const iSel = document.getElementById('cfgInst');
588
- // Filter based on Expiry if needed, or show all
589
- const list = exp === "MARKET" ? this.meta.futures : this.meta.options;
590
- iSel.innerHTML = list.map(i => `<option>${i}</option>`).join('');
591
- if(preInst) iSel.value = preInst;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  },
593
 
594
  renderSeries(list) {
595
- const cont = document.getElementById('cfgSeriesList');
596
- cont.innerHTML = '';
597
- if(list.length === 0) list = [{label: 'Price', formula: '$P', color: '#2f81f7', axis: 'left'}];
598
- list.forEach(s => this.addSeries(s));
 
 
 
 
 
 
 
 
 
 
599
  },
600
 
601
- addSeries(s = {label: 'New', formula: '$P', color: '#ffffff', axis: 'left'}) {
602
- const row = document.createElement('div');
603
- row.className = 'series-row';
604
- row.innerHTML = `
605
- <input type="color" value="${s.color}" class="s-color" style="padding:0; border:none; height:28px;">
606
- <input type="text" value="${s.formula}" class="s-form" placeholder="Formula">
607
- <input type="text" value="${s.label}" class="s-lbl" placeholder="Label">
608
- <select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
609
- <button onclick="this.parentElement.remove()" style="border:none; color:var(--danger)">✕</button>
610
  `;
611
- document.getElementById('cfgSeriesList').appendChild(row);
612
  },
613
 
614
- save() {
615
- const series = Array.from(document.querySelectorAll('.series-row')).map(r => ({
616
- color: r.querySelector('.s-color').value,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  formula: r.querySelector('.s-form').value,
618
- label: r.querySelector('.s-lbl').value,
619
  axis: r.querySelector('.s-axis').value
620
  }));
621
 
622
- App.state.charts[this.tid || this.currId].config = {
623
- mode: this.currMode,
624
- root: document.getElementById('cfgRoot').value,
625
- expiry: document.getElementById('cfgExp').value,
626
- instrument: document.getElementById('cfgInst').value,
627
- range_val: parseInt(document.getElementById('cfgRange').value),
628
- timeframe: document.getElementById('cfgTF').value,
629
- start: document.getElementById('cfgStart').value,
630
- end: document.getElementById('cfgEnd').value,
631
- series
632
- };
633
- App.save();
634
- App.renderGrid();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  this.close();
636
- },
637
- close() { document.getElementById('configModal').style.display = 'none'; }
638
  };
639
 
 
 
 
 
 
 
 
 
640
  window.onload = () => App.init();
641
  </script>
642
  </body>
 
 
1
  import pandas as pd
2
  import requests
3
  import uvicorn
4
  import json
5
  import asyncio
 
6
  from datetime import datetime, timedelta
7
  from fastapi import FastAPI, Body, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
  from typing import Optional, List, Dict, Any
11
+ from collections import defaultdict
12
 
13
  # ==========================================
14
  # 1. CONFIGURATION
 
28
  # 2. HELPER FUNCTIONS
29
  # ==========================================
30
  def sb_get(endpoint: str, params: dict = None):
31
+ """Generic Supabase GET helper."""
32
  try:
33
  url = f"{SB_URL}/rest/v1{endpoint}"
34
  r = requests.get(url, headers=HEADERS, params=params)
 
38
  print(f"DB Error ({endpoint}): {e}")
39
  return []
40
 
41
+ def get_strike_from_symbol(symbol: str) -> tuple:
42
+ """Extract strike price and option type from symbol."""
43
+ try:
44
+ # Symbol format: NSE:NIFTY24JAN21000CE or similar
45
+ parts = symbol.split(':')[-1]
46
+ # Find CE or PE in the symbol
47
+ if 'CE' in parts:
48
+ opt_type = 'CE'
49
+ strike_part = parts.split('CE')[0]
50
+ elif 'PE' in parts:
51
+ opt_type = 'PE'
52
+ strike_part = parts.split('PE')[0]
53
+ else:
54
+ return 0, 'FUT'
55
+
56
+ # Extract strike from the end (last 5 digits typically)
57
+ import re
58
+ numbers = re.findall(r'\d+', strike_part)
59
+ if numbers:
60
+ strike = float(numbers[-1])
61
+ return strike, opt_type
62
+ except:
63
+ pass
64
+ return 0, 'FUT'
65
+
66
  # ==========================================
67
  # 3. API ROUTES - METADATA
68
  # ==========================================
 
73
 
74
  @app.get("/api/dates")
75
  def get_dates():
76
+ """Fetch distinct dates."""
77
+ data = sb_get("/history_buckets_fyers", {"select": "minute", "order": "minute.desc", "limit": "50000"})
78
+ dates = set()
79
+ for row in data:
80
+ if row.get('minute'):
81
+ dates.add(row['minute'].split('T')[0])
82
+ return JSONResponse(sorted(list(dates), reverse=True))
83
 
84
  @app.get("/api/roots")
85
  def get_roots(date: str):
86
+ """Fetch roots available for a specific date."""
87
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
88
  data = sb_get(f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}")
89
+ roots = set()
90
+ for row in data:
91
+ if row.get('root'):
92
+ roots.add(row['root'])
93
+ return JSONResponse(sorted(list(roots)))
94
 
95
+ @app.get("/api/auto_config")
96
+ def get_auto_config(date: str, root: str):
97
+ """Smart helper for defaults (Current/Next expiry)."""
98
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
99
+ # Fetch small limit just to inspect structure
100
+ data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
101
 
102
+ expiry_map = {}
103
+ for row in data:
104
+ exp = row.get('expiry')
105
+ if not exp: continue
106
+ if exp not in expiry_map:
107
+ keys = list(row.get('data', {}).keys())
108
+ expiry_map[exp] = keys
109
+ else:
110
+ existing = set(expiry_map[exp])
111
+ new_keys = row.get('data', {}).keys()
112
+ expiry_map[exp] = list(existing.union(new_keys))
113
+
114
+ all_expiries = sorted(expiry_map.keys())
115
+ valid_expiries = [e for e in all_expiries if e >= date]
116
+ if not valid_expiries:
117
+ valid_expiries = all_expiries
118
+
119
+ response = {"expiries": all_expiries, "current": None, "next": None}
120
+
121
+ def find_fut(instruments):
122
+ for i in instruments:
123
+ if 'FUT' in i: return i
124
+ return instruments[0] if instruments else None
125
+
126
+ if len(valid_expiries) > 0:
127
+ exp_c = valid_expiries[0]
128
+ inst_c = find_fut(expiry_map[exp_c])
129
+ response["current"] = {"expiry": exp_c, "instrument": inst_c}
130
+
131
+ if len(valid_expiries) > 1:
132
+ exp_n = valid_expiries[1]
133
+ inst_n = find_fut(expiry_map[exp_n])
134
+ response["next"] = {"expiry": exp_n, "instrument": inst_n}
135
+
136
+ return JSONResponse(response)
137
+
138
+ @app.get("/api/expiries")
139
+ def get_expiries(date: str, root: str):
140
+ """Fetch distinct expiries."""
141
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
142
+ data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
143
+ exps = set(row['expiry'] for row in data if row.get('expiry'))
144
+ return JSONResponse(sorted(list(exps)))
145
+
146
+ @app.get("/api/instruments")
147
+ def get_instruments(date: str, root: str, expiry: str, instrument_type: str = "all"):
148
+ """Fetch instruments filtered by type (futures/options/all)."""
149
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
150
+ data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=10")
151
+ instruments = set()
152
+ for row in data:
153
+ keys = row.get('data', {}).keys()
154
+ instruments.update(keys)
155
+
156
+ # Filter based on instrument type
157
+ instruments = sorted(list(instruments))
158
 
159
+ if instrument_type == "futures":
160
+ # Show only futures (including spot/equity)
161
+ filtered = [i for i in instruments if 'FUT' in i or '-EQ' in i or 'INDEX' in i]
162
+ return JSONResponse(filtered)
163
+ elif instrument_type == "options":
164
+ # Show only options
165
+ filtered = [i for i in instruments if 'CE' in i or 'PE' in i]
166
+ return JSONResponse(filtered)
167
+ else:
168
+ return JSONResponse(instruments)
169
+
170
+ @app.get("/api/options_chain")
171
+ def get_options_chain(date: str, root: str, expiry: str):
172
+ """Fetch all options for a specific expiry with strike information."""
173
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
174
+ data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
175
+
176
+ options_data = []
177
+ seen = set()
178
 
179
  for row in data:
180
+ for symbol in row.get('data', {}).keys():
181
+ if ('CE' in symbol or 'PE' in symbol) and symbol not in seen:
182
+ strike, opt_type = get_strike_from_symbol(symbol)
183
+ if strike > 0:
184
+ options_data.append({
185
+ "symbol": symbol,
186
+ "strike": strike,
187
+ "type": opt_type
188
+ })
189
+ seen.add(symbol)
190
+
191
+ # Sort by strike
192
+ options_data.sort(key=lambda x: x['strike'])
193
+ return JSONResponse(options_data)
194
+
195
+ @app.get("/api/spot_price")
196
+ def get_spot_price(date: str, root: str, timeframe: str = "1min"):
197
+ """Fetch spot/future price for ATM calculation."""
198
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
199
+
200
+ # First try to get from MARKET bucket (spot)
201
+ data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.MARKET&minute=gte.{start}&minute=lte.{end}&order=minute.asc&limit=500")
202
+
203
+ if not data:
204
+ # Fallback to futures
205
+ data = sb_get(f"/history_buckets_fyers?select=data,expiry&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&order=minute.asc&limit=500")
206
+
207
+ records = []
208
+ for row in data:
209
+ try:
210
+ base_ts = datetime.fromisoformat(row['minute'])
211
+ inst_data = row.get('data', {})
212
+
213
+ # Get first instrument (usually spot or main future)
214
+ for symbol, sdata in inst_data.items():
215
+ p_arr = sdata.get('p', []) or []
216
+ for i in range(6):
217
+ if i < len(p_arr) and p_arr[i] is not None:
218
+ records.append({
219
+ "ts": base_ts + timedelta(seconds=i*10),
220
+ "price": float(p_arr[i])
221
+ })
222
+ break # Only first instrument
223
+ except:
224
+ continue
225
+
226
+ if not records:
227
+ return JSONResponse({"error": "No spot data found"})
228
+
229
+ df = pd.DataFrame(records)
230
+ df.set_index('ts', inplace=True)
231
+
232
+ tf_map = {
233
+ "1min": "1min", "3min": "3min", "5min": "5min",
234
+ "15min": "15min", "30min": "30min", "1hour": "1h"
235
+ }
236
+ panda_tf = tf_map.get(timeframe, "1min")
237
+
238
+ resampled = df.resample(panda_tf).median().dropna()
239
+
240
  return JSONResponse({
241
+ "labels": resampled.index.strftime('%H:%M').tolist(),
242
+ "prices": resampled['price'].tolist()
 
243
  })
244
 
245
  # ==========================================
246
+ # 4. API ROUTES - DATA FETCHING (OPTIMIZED)
247
  # ==========================================
248
 
249
  @app.post("/api/fetch_series")
 
251
  date: str = Body(...),
252
  root: str = Body(...),
253
  expiry: str = Body(...),
254
+ instrument: str = Body(...),
255
+ timeframe: str = Body(...),
 
 
256
  start_time: str = Body(...),
257
  end_time: str = Body(...)
258
  ):
259
+ """
260
+ Fetches raw 10s data using Postgres JSON filtering to save bandwidth.
261
+ Normal mode - single instrument.
262
+ """
263
  t_start = f"{date}T{start_time}:00"
264
  t_end = f"{date}T{end_time}:59"
265
 
266
+ # CRITICAL: Postgres JSON Filtering
267
+ select_query = f'minute,inst_data:data->"{instrument}"'
268
+
269
+ url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
270
+
271
+ rows = sb_get(url)
272
+
273
+ records = []
274
+ for row in rows:
275
+ try:
276
+ base_ts = datetime.fromisoformat(row['minute'])
277
+ inst_data = row.get('inst_data')
 
 
 
 
 
 
278
 
279
+ if not inst_data: continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
+ # Extract buckets (0s to 50s)
282
+ p_arr = inst_data.get('p', []) or []
283
+ b_arr = inst_data.get('b', []) or []
284
+ s_arr = inst_data.get('s', []) or []
285
+ v_arr = inst_data.get('v', []) or []
286
+ o_arr = inst_data.get('o', []) or []
287
+
 
 
 
288
  for i in range(6):
289
+ if i < len(p_arr) and p_arr[i] is not None:
290
  records.append({
291
  "ts": base_ts + timedelta(seconds=i*10),
292
+ "price": float(p_arr[i]),
293
+ "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
294
+ "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
295
+ "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
296
+ "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0
 
297
  })
298
+ except Exception:
299
+ continue
300
 
301
  if not records:
302
+ return JSONResponse({"error": "No data found", "labels": [], "P": []})
303
 
304
+ df = pd.DataFrame(records)
305
+ df.set_index('ts', inplace=True)
306
+
307
+ tf_map = {
308
+ "1min": "1min", "3min": "3min", "5min": "5min",
309
+ "15min": "15min", "30min": "30min", "1hour": "1h"
310
+ }
311
+ panda_tf = tf_map.get(timeframe, "1min")
312
 
313
+ resampled = df.resample(panda_tf).median().dropna()
314
+
315
+ response_data = {
316
+ "labels": resampled.index.strftime('%H:%M').tolist(),
317
+ "P": resampled['price'].tolist(),
318
+ "B": resampled['buy_qty'].tolist(),
319
+ "S": resampled['sell_qty'].tolist(),
320
+ "V": resampled['volume'].tolist(),
321
+ "OI": resampled['oi'].tolist()
322
+ }
323
+
324
+ return JSONResponse(response_data)
325
+
326
+
327
+ @app.post("/api/fetch_advanced_series")
328
+ def fetch_advanced_series(
329
+ date: str = Body(...),
330
+ root: str = Body(...),
331
+ expiry: str = Body(...),
332
+ instruments: List[str] = Body(...),
333
+ timeframe: str = Body(...),
334
+ start_time: str = Body(...),
335
+ end_time: str = Body(...)
336
+ ):
337
+ """
338
+ Fetches data for multiple instruments (ATM +- strikes) for Advanced Mode.
339
+ Returns aggregated Call/Put Buyer/Seller data.
340
+ """
341
+ t_start = f"{date}T{start_time}:00"
342
+ t_end = f"{date}T{end_time}:59"
343
+
344
+ # Separate CE and PE instruments
345
+ ce_instruments = [i for i in instruments if 'CE' in i]
346
+ pe_instruments = [i for i in instruments if 'PE' in i]
347
+
348
+ all_records = {} # ts -> aggregated data
349
+
350
+ def fetch_instrument_data(inst_list, label_prefix):
351
+ """Fetch data for a list of instruments and aggregate."""
352
+ for instrument in inst_list:
353
+ select_query = f'minute,inst_data:data->"{instrument}"'
354
+ url = f"/history_buckets_fyers?select={select_query}&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
355
+ rows = sb_get(url)
356
+
357
+ for row in rows:
358
+ try:
359
+ base_ts = datetime.fromisoformat(row['minute'])
360
+ inst_data = row.get('inst_data')
361
+
362
+ if not inst_data: continue
363
 
364
+ p_arr = inst_data.get('p', []) or []
365
+ b_arr = inst_data.get('b', []) or []
366
+ s_arr = inst_data.get('s', []) or []
367
+ v_arr = inst_data.get('v', []) or []
368
+ o_arr = inst_data.get('o', []) or []
369
+
370
+ for i in range(6):
371
+ if i < len(p_arr) and p_arr[i] is not None:
372
+ ts = base_ts + timedelta(seconds=i*10)
373
+ ts_key = ts.isoformat()
374
+
375
+ if ts_key not in all_records:
376
+ all_records[ts_key] = {
377
+ "ts": ts,
378
+ "price": [], "buy_qty": [], "sell_qty": [],
379
+ "volume": [], "oi": [],
380
+ "cb": [], "cs": [], "pb": [], "ps": []
381
+ }
382
+
383
+ buy = float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0
384
+ sell = float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0
385
+
386
+ all_records[ts_key]["price"].append(float(p_arr[i]))
387
+ all_records[ts_key]["buy_qty"].append(buy)
388
+ all_records[ts_key]["sell_qty"].append(sell)
389
+ all_records[ts_key]["volume"].append(float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0)
390
+ all_records[ts_key]["oi"].append(float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0)
391
+
392
+ # Separate CB/CS (Call) and PB/PS (Put)
393
+ if 'CE' in instrument:
394
+ all_records[ts_key]["cb"].append(buy) # Call Buyers
395
+ all_records[ts_key]["cs"].append(sell) # Call Sellers
396
+ else:
397
+ all_records[ts_key]["pb"].append(buy) # Put Buyers
398
+ all_records[ts_key]["ps"].append(sell) # Put Sellers
399
+
400
+ except Exception:
401
+ continue
402
 
403
+ # Fetch data for all instruments
404
+ fetch_instrument_data(ce_instruments, 'CE')
405
+ fetch_instrument_data(pe_instruments, 'PE')
406
+
407
+ if not all_records:
408
+ return JSONResponse({"error": "No data found", "labels": [], "P": []})
409
+
410
+ # Aggregate records
411
+ aggregated = []
412
+ for ts_key in sorted(all_records.keys()):
413
+ rec = all_records[ts_key]
414
+
415
+ def safe_median(arr):
416
+ if not arr: return 0.0
417
+ filtered = [x for x in arr if x is not None]
418
+ return sum(filtered) / len(filtered) if filtered else 0.0
419
+
420
+ def safe_sum(arr):
421
+ if not arr: return 0.0
422
+ return sum(x for x in arr if x is not None)
423
+
424
+ aggregated.append({
425
+ "ts": rec["ts"],
426
+ "price": safe_median(rec["price"]),
427
+ "buy_qty": safe_sum(rec["buy_qty"]),
428
+ "sell_qty": safe_sum(rec["sell_qty"]),
429
+ "volume": safe_sum(rec["volume"]),
430
+ "oi": safe_sum(rec["oi"]),
431
+ "cb": safe_sum(rec["cb"]), # Total Call Buyers
432
+ "cs": safe_sum(rec["cs"]), # Total Call Sellers
433
+ "pb": safe_sum(rec["pb"]), # Total Put Buyers
434
+ "ps": safe_sum(rec["ps"]) # Total Put Sellers
435
+ })
436
+
437
+ df = pd.DataFrame(aggregated)
438
+ df.set_index('ts', inplace=True)
439
+
440
+ tf_map = {
441
+ "1min": "1min", "3min": "3min", "5min": "5min",
442
+ "15min": "15min", "30min": "30min", "1hour": "1h"
443
+ }
444
+ panda_tf = tf_map.get(timeframe, "1min")
445
+
446
+ # Use sum for quantities, median for price
447
+ resampled_price = df['price'].resample(panda_tf).median()
448
+ resampled_qty = df[['buy_qty', 'sell_qty', 'volume', 'oi', 'cb', 'cs', 'pb', 'ps']].resample(panda_tf).sum()
449
+
450
+ resampled = pd.concat([resampled_price, resampled_qty], axis=1).dropna()
451
+
452
+ response_data = {
453
  "labels": resampled.index.strftime('%H:%M').tolist(),
454
  "P": resampled['price'].tolist(),
455
+ "B": resampled['buy_qty'].tolist(),
456
+ "S": resampled['sell_qty'].tolist(),
457
+ "V": resampled['volume'].tolist(),
458
  "OI": resampled['oi'].tolist(),
459
+ "CB": resampled['cb'].tolist(), # Call Buyers
460
+ "CS": resampled['cs'].tolist(), # Call Sellers
461
+ "PB": resampled['pb'].tolist(), # Put Buyers
462
+ "PS": resampled['ps'].tolist() # Put Sellers
463
+ }
464
+
465
+ return JSONResponse(response_data)
466
+
467
+
468
+ @app.post("/api/get_atm_strikes")
469
+ def get_atm_strikes(
470
+ date: str = Body(...),
471
+ root: str = Body(...),
472
+ expiry: str = Body(...),
473
+ atm_range: int = Body(default=5),
474
+ timeframe: str = Body(default="1min"),
475
+ start_time: str = Body(default="09:15"),
476
+ end_time: str = Body(default="15:30")
477
+ ):
478
+ """
479
+ Calculate ATM strikes dynamically for each time bucket.
480
+ Returns list of instruments for each time point.
481
+ """
482
+ t_start = f"{date}T{start_time}:00"
483
+ t_end = f"{date}T{end_time}:59"
484
+
485
+ # Get spot prices for reference
486
+ spot_data = sb_get(f"/history_buckets_fyers?select=minute,data&root=eq.{root}&expiry=eq.MARKET&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc&limit=2000")
487
+
488
+ # Get all options for the expiry
489
+ options_data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&limit=5")
490
+
491
+ # Extract all options with strikes
492
+ options_list = []
493
+ seen = set()
494
+ for row in options_data:
495
+ for symbol in row.get('data', {}).keys():
496
+ if ('CE' in symbol or 'PE' in symbol) and symbol not in seen:
497
+ strike, opt_type = get_strike_from_symbol(symbol)
498
+ if strike > 0:
499
+ options_list.append({
500
+ "symbol": symbol,
501
+ "strike": strike,
502
+ "type": opt_type
503
+ })
504
+ seen.add(symbol)
505
+
506
+ if not options_list:
507
+ return JSONResponse({"error": "No options found for expiry"})
508
+
509
+ # Get unique strikes sorted
510
+ strikes = sorted(list(set(o['strike'] for o in options_list)))
511
+
512
+ # Build strike to symbol mapping
513
+ strike_map = {}
514
+ for o in options_list:
515
+ key = (o['strike'], o['type'])
516
+ strike_map[key] = o['symbol']
517
+
518
+ # For each time point, calculate ATM and return +- strikes
519
+ result = {
520
+ "strikes": strikes,
521
+ "strike_map": strike_map,
522
+ "options_list": options_list
523
+ }
524
+
525
+ return JSONResponse(result)
526
+
527
 
528
  # ==========================================
529
  # 5. FRONTEND TEMPLATE
 
534
  <html lang="en">
535
  <head>
536
  <meta charset="UTF-8">
537
+ <title>DepthChain PRO</title>
538
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
539
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
540
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
541
  <style>
542
+ /* --- THEME --- */
543
  :root {
544
+ --bg-body: #0d1117;
545
+ --bg-header: #161b22;
546
+ --bg-card: #0d1117;
547
+ --bg-panel: #21262d;
548
+ --bg-input: #161b22;
549
+ --border: #30363d;
550
+ --border-hover: #8b949e;
551
+ --text-primary: #e6edf3;
552
+ --text-secondary: #8b949e;
553
+ --accent: #2f81f7;
554
+ --accent-hover: #58a6ff;
555
+ --success: #238636;
556
+ --danger: #da3633;
557
+ --warning: #d29922;
558
+ --purple: #a371f7;
559
+ --cyan: #3fb950;
560
+ --font-main: 'Inter', sans-serif;
561
+ --font-mono: 'JetBrains Mono', monospace;
562
  }
563
+ * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
564
+ body {
565
+ background: var(--bg-body); color: var(--text-primary); font-family: var(--font-main);
566
+ display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px;
567
+ }
568
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
569
+ ::-webkit-scrollbar-track { background: var(--bg-body); }
570
+ ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
571
+
572
+ /* HEADER */
573
+ .top-bar {
574
+ height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
575
+ display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
576
+ }
577
+ .brand { font-weight: 700; font-size: 15px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
578
+ .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
579
+ .controls { display: flex; align-items: center; gap: 12px; }
580
 
581
+ select, button, input {
582
+ background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
583
+ padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer;
584
+ transition: all 0.2s ease;
585
+ }
586
+ select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(47, 129, 247, 0.2); }
587
+ button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
588
+ button.primary { background: var(--accent); border-color: var(--accent); color: white; }
589
+ button.primary:hover { background: var(--accent-hover); }
590
+ button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
591
+ button.danger:hover { background: rgba(218, 54, 51, 0.1); }
592
+
593
+ .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
594
+ .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
595
+ .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
596
+
597
+ /* GRID */
598
+ .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
599
+ .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
600
+ .column:last-child { border-right: none; }
601
+
602
+ .add-chart-btn {
603
+ height: 40px; border: none; border-top: 1px dashed var(--border);
604
+ background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600;
605
+ }
606
+ .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
607
+
608
+ /* CARD */
609
+ .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
610
+ .card-header {
611
+ height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
612
+ padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
613
+ }
614
+ .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
615
+ .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
616
+ .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
617
+ .adv-badge { background: rgba(163, 113, 247, 0.2); color: var(--purple); padding: 2px 6px; border-radius: 4px; font-size: 9px; font-weight: 700; }
618
 
619
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
620
+ .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
621
+ .status-dot.hist { background: var(--accent); }
622
+ .status-dot.retry { background: var(--warning); }
623
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
624
+
625
+ .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
626
+ .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
627
+
628
+ .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
629
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); background: radial-gradient(circle, var(--bg-panel) 0%, var(--bg-card) 100%); }
630
 
631
  /* MODAL */
632
+ .modal-backdrop {
633
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8);
634
+ z-index: 100; display: none; justify-content: center; align-items: center;
635
+ backdrop-filter: blur(4px);
636
+ }
637
+ .modal {
638
+ background: var(--bg-header); border: 1px solid var(--border); border-radius: 12px;
639
+ width: 600px; max-width: 95%; max-height: 90vh; display: flex; flex-direction: column;
640
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5);
641
+ }
642
+ .modal-head {
643
+ padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;
644
+ background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-header) 100%);
645
+ border-radius: 12px 12px 0 0;
646
+ }
647
+ .modal-title { font-weight: 700; font-size: 14px; display: flex; align-items: center; gap: 10px; }
648
+ .mode-badge {
649
+ padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; text-transform: uppercase;
650
+ }
651
+ .mode-badge.normal { background: rgba(47, 129, 247, 0.2); color: var(--accent); }
652
+ .mode-badge.advanced { background: rgba(163, 113, 247, 0.2); color: var(--purple); }
653
+
654
+ .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
655
+ .modal-foot { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); border-radius: 0 0 12px 12px; }
656
+
657
+ /* FORM SECTIONS */
658
+ .form-section { margin-bottom: 4px; }
659
+ .form-section label {
660
+ font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600;
661
+ margin-bottom: 6px; display: flex; align-items: center; gap: 6px;
662
+ }
663
+ .form-section label .info-icon {
664
+ display: inline-flex; align-items: center; justify-content: center;
665
+ width: 14px; height: 14px; border-radius: 50%; background: var(--border);
666
+ color: var(--text-secondary); font-size: 10px; font-weight: 700; cursor: help;
667
+ }
668
+ .form-section label .info-icon:hover { background: var(--accent); color: white; }
669
+
670
+ /* MODE TOGGLE */
671
+ .mode-toggle {
672
+ display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; padding: 3px; margin-bottom: 16px;
673
+ }
674
+ .mode-toggle-btn {
675
+ flex: 1; padding: 10px; text-align: center; border: none; background: transparent;
676
+ color: var(--text-secondary); border-radius: 6px; cursor: pointer; transition: all 0.2s; font-weight: 600;
677
+ }
678
+ .mode-toggle-btn:hover { color: var(--text-primary); }
679
+ .mode-toggle-btn.active {
680
+ background: var(--accent); color: white; box-shadow: 0 2px 8px rgba(47, 129, 247, 0.3);
681
+ }
682
+ .mode-toggle-btn.active.advanced {
683
+ background: var(--purple); box-shadow: 0 2px 8px rgba(163, 113, 247, 0.3);
684
+ }
685
+
686
+ /* INSTRUMENT TYPE TABS */
687
+ .inst-type-tabs {
688
+ display: flex; gap: 8px; margin-bottom: 12px;
689
+ }
690
+ .inst-type-btn {
691
+ flex: 1; padding: 8px 12px; border: 1px solid var(--border); background: transparent;
692
+ color: var(--text-secondary); border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 11px;
693
+ transition: all 0.2s;
694
+ }
695
+ .inst-type-btn:hover { border-color: var(--accent); color: var(--text-primary); }
696
+ .inst-type-btn.active {
697
+ background: var(--accent); border-color: var(--accent); color: white;
698
+ }
699
+
700
+ /* SECTION DIVIDER */
701
+ .section-divider {
702
+ height: 1px; background: linear-gradient(90deg, transparent, var(--border), transparent);
703
+ margin: 16px 0; position: relative;
704
+ }
705
+ .section-divider::before {
706
+ content: attr(data-label);
707
+ position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
708
+ background: var(--bg-header); padding: 0 10px; color: var(--text-secondary);
709
+ font-size: 10px; text-transform: uppercase; letter-spacing: 1px;
710
+ }
711
+
712
+ /* ATM RANGE INPUT */
713
+ .atm-range-section {
714
+ background: rgba(163, 113, 247, 0.1); border: 1px solid rgba(163, 113, 247, 0.3);
715
+ border-radius: 8px; padding: 12px; margin-top: 8px;
716
+ }
717
+ .atm-range-input {
718
+ display: flex; align-items: center; gap: 8px;
719
+ }
720
+ .atm-range-input input { width: 60px; text-align: center; }
721
+ .atm-range-label { font-size: 12px; color: var(--text-secondary); }
722
+ .strikes-preview {
723
+ margin-top: 8px; padding: 8px; background: var(--bg-input); border-radius: 6px;
724
+ font-family: var(--font-mono); font-size: 10px; color: var(--text-secondary);
725
+ }
726
+
727
+ /* VARIABLES INFO */
728
+ .variables-section {
729
+ background: var(--bg-panel); border-radius: 8px; padding: 12px; margin-top: 8px;
730
+ }
731
+ .var-grid {
732
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px;
733
+ }
734
+ .var-item {
735
+ display: flex; align-items: center; gap: 6px; padding: 6px 8px;
736
+ background: var(--bg-input); border-radius: 4px; font-size: 11px;
737
+ }
738
+ .var-code {
739
+ font-family: var(--font-mono); color: var(--accent); font-weight: 600;
740
+ min-width: 30px;
741
+ }
742
+ .var-desc { color: var(--text-secondary); font-size: 10px; }
743
+
744
+ /* INFO TOOLTIP */
745
+ .tooltip-container {
746
+ position: relative; display: inline-block;
747
+ }
748
+ .tooltip-content {
749
+ display: none; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
750
+ background: var(--bg-panel); border: 1px solid var(--border); border-radius: 8px;
751
+ padding: 12px; min-width: 280px; z-index: 1000; margin-bottom: 8px;
752
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
753
+ }
754
+ .tooltip-content.show { display: block; }
755
+ .tooltip-content h4 {
756
+ margin: 0 0 8px 0; font-size: 12px; color: var(--text-primary);
757
+ border-bottom: 1px solid var(--border); padding-bottom: 8px;
758
+ }
759
+
760
+ /* SERIES ROW */
761
+ .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
762
+ .color-input { padding: 0; width: 100%; border: none; background: none; height: 28px; cursor: pointer; }
763
+
764
+ /* FORM GRID */
765
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
766
+
767
+ /* LOADING */
768
+ .loading-overlay {
769
+ position: absolute; inset: 0; background: rgba(13, 17, 23, 0.9);
770
+ display: flex; align-items: center; justify-content: center; z-index: 10;
771
+ }
772
+ .loading-spinner {
773
+ width: 24px; height: 24px; border: 2px solid var(--border);
774
+ border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite;
775
+ }
776
+ @keyframes spin { to { transform: rotate(360deg); } }
777
+
778
  </style>
779
  </head>
780
  <body>
781
 
782
  <div class="top-bar">
783
+ <div class="brand">DepthChain <span>PRO</span></div>
784
+ <div class="controls">
785
+ <select id="globalDate" onchange="App.onDateChange()" style="min-width: 140px;"></select>
786
+ <div style="position: relative;">
787
+ <select id="globalMode" onchange="App.onModeChange()" style="padding-left: 28px; font-weight: 600;">
788
+ <option value="LIVE">LIVE</option>
789
+ <option value="HIST">HISTORY</option>
790
+ </select>
791
+ <div id="modeDot" class="status-dot live" style="position: absolute; left: 10px; top: 12px; pointer-events: none;"></div>
792
+ </div>
793
+ <div style="width:1px; height: 20px; background: var(--border);"></div>
794
+ <div class="layout-toggle">
795
  <button class="layout-btn" onclick="App.setLayout(1)">1</button>
796
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
797
+ <button class="layout-btn active" onclick="App.setLayout(3)">3</button>
798
+ <button class="layout-btn" onclick="App.setLayout(4)">4</button>
799
  </div>
800
+ <button onclick="Config.openDefaults()">Settings</button>
801
+ <button class="danger" onclick="App.reset()">Reset</button>
802
  </div>
803
  </div>
804
 
805
  <div class="main-container" id="gridRoot"></div>
806
 
807
+ <!-- CONFIG MODAL -->
808
+ <div class="modal-backdrop" id="configModal">
809
  <div class="modal">
810
+ <div class="modal-head">
811
+ <div class="modal-title">
812
+ <span id="cfgTitle">Configure Chart</span>
813
+ <span id="cfgModeBadge" class="mode-badge normal">Normal</span>
814
+ </div>
815
+ <button class="tool-btn" onclick="Config.close()">✕</button>
816
  </div>
817
+ <div class="modal-body" style="position: relative;">
818
+ <div id="cfgLoading" class="loading-overlay" style="display:none;">
819
+ <div class="loading-spinner"></div>
 
 
 
 
 
 
 
820
  </div>
821
+ <div id="cfgContent">
822
+
823
+ <!-- Global Settings Mode -->
824
+ <div id="cfgGlobalOnly" style="display:none;">
825
+ <div class="form-section">
826
+ <label>Refresh Rate (Seconds)</label>
827
+ <input type="number" id="cfgRefresh" value="15" min="5">
828
+ </div>
829
+ <div class="section-divider"></div>
830
  </div>
 
831
 
832
+ <!-- Single Chart Config Mode -->
833
+ <div id="cfgChartOnly">
834
+
835
+ <!-- MODE TOGGLE -->
836
+ <div class="mode-toggle">
837
+ <button class="mode-toggle-btn active" id="btnNormalMode" onclick="Config.setConfigMode('normal')">
838
+ 📊 Normal Mode
839
+ </button>
840
+ <button class="mode-toggle-btn" id="btnAdvancedMode" onclick="Config.setConfigMode('advanced')">
841
+ 🔬 Advanced Mode
842
+ </button>
843
+ </div>
844
+
845
+ <!-- INSTRUMENT TYPE TABS (Normal Mode) -->
846
+ <div id="instTypeSection" class="inst-type-tabs">
847
+ <button class="inst-type-btn active" id="btnOptions" onclick="Config.setInstType('options')">
848
+ 📈 Options
849
+ </button>
850
+ <button class="inst-type-btn" id="btnFutures" onclick="Config.setInstType('futures')">
851
+ 📉 Futures
852
+ </button>
853
+ </div>
854
+
855
+ <!-- SYMBOL & EXPIRY -->
856
+ <div class="form-grid">
857
+ <div class="form-section">
858
+ <label>Symbol</label>
859
+ <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
860
+ </div>
861
+ <div class="form-section">
862
+ <label>Expiry</label>
863
+ <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
864
+ </div>
865
+ </div>
866
+
867
+ <!-- INSTRUMENT SELECT (Normal Mode) -->
868
+ <div id="instrumentSelectSection" class="form-section">
869
+ <label>Instrument</label>
870
+ <select id="cfgInst" style="width:100%"></select>
871
+ </div>
872
+
873
+ <!-- ATM RANGE SECTION (Advanced Mode) -->
874
+ <div id="atmRangeSection" style="display:none;" class="atm-range-section">
875
+ <div class="form-section">
876
+ <label>
877
+ ATM ± Range
878
+ <span class="info-icon" onclick="Config.showVarTooltip(event)">i</span>
879
+ </label>
880
+ <div class="atm-range-input">
881
+ <input type="number" id="cfgAtmRange" value="5" min="1" max="20" onchange="Config.updateStrikesPreview()">
882
+ <span class="atm-range-label">strikes on each side (Total: <span id="totalStrikes">11</span> strikes)</span>
883
+ </div>
884
+ <div id="strikesPreview" class="strikes-preview" style="display:none;"></div>
885
+ </div>
886
+ </div>
887
+
888
+ <!-- VARIABLES INFO (Advanced Mode) -->
889
+ <div id="variablesSection" style="display:none;" class="variables-section">
890
+ <label style="margin-bottom: 8px;">
891
+ Available Variables
892
+ <span class="info-icon" onclick="Config.showVarTooltip(event)">i</span>
893
+ </label>
894
+ <div class="var-grid">
895
+ <div class="var-item"><span class="var-code">$P</span><span class="var-desc">Price</span></div>
896
+ <div class="var-item"><span class="var-code">$B</span><span class="var-desc">Buy Qty</span></div>
897
+ <div class="var-item"><span class="var-code">$S</span><span class="var-desc">Sell Qty</span></div>
898
+ <div class="var-item"><span class="var-code">$V</span><span class="var-desc">Volume</span></div>
899
+ <div class="var-item"><span class="var-code">$O</span><span class="var-desc">Open Interest</span></div>
900
+ <div class="var-item"><span class="var-code">$CB</span><span class="var-desc">Call Buyers</span></div>
901
+ <div class="var-item"><span class="var-code">$CS</span><span class="var-desc">Call Sellers</span></div>
902
+ <div class="var-item"><span class="var-code">$PB</span><span class="var-desc">Put Buyers</span></div>
903
+ <div class="var-item"><span class="var-code">$PS</span><span class="var-desc">Put Sellers</span></div>
904
+ </div>
905
+ </div>
906
+
907
+ <!-- TIMEFRAME & RANGE -->
908
+ <div class="form-grid" style="margin-top: 12px;">
909
+ <div class="form-section">
910
+ <label>Timeframe</label>
911
+ <select id="cfgTF" style="width:100%">
912
+ <option value="1min">1 Minute</option>
913
+ <option value="3min">3 Minutes</option>
914
+ <option value="5min">5 Minutes</option>
915
+ <option value="15min">15 Minutes</option>
916
+ <option value="30min">30 Minutes</option>
917
+ <option value="1hour">1 Hour</option>
918
+ </select>
919
+ </div>
920
+ <div class="form-section">
921
+ <label>Range (HH:MM)</label>
922
+ <div style="display:flex; gap:8px;">
923
+ <input type="time" id="cfgStart" value="09:15" style="width:50%">
924
+ <input type="time" id="cfgEnd" value="15:30" style="width:50%">
925
+ </div>
926
+ </div>
927
+ </div>
928
+
929
+ <div class="section-divider" data-label="Plot Series"></div>
930
  </div>
 
931
 
932
+ <!-- Common Series Config -->
933
+ <div class="form-section">
934
+ <label>
935
+ Plot Series
936
+ <span class="info-icon" onclick="Config.showSeriesTooltip(event)">i</span>
937
+ </label>
938
+ <div id="cfgSeries"></div>
939
+ <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary);" onclick="Config.addSeriesRow()">+ Add Line</button>
 
 
 
 
 
940
  </div>
941
  </div>
942
+ </div>
943
+ <div class="modal-foot">
944
+ <button style="background:transparent; border:1px solid var(--border); color:var(--text-primary);" onclick="Config.close()">Cancel</button>
945
+ <button class="primary" onclick="Config.save()">Save Configuration</button>
946
+ </div>
947
+ </div>
948
+ </div>
949
 
950
+ <!-- VARIABLE TOOLTIP -->
951
+ <div id="varTooltip" class="tooltip-content">
952
+ <h4>📊 Variable Reference</h4>
953
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
954
+ <div><span style="color: var(--accent);">$P</span> - Price (LTP)</div>
955
+ <div><span style="color: var(--accent);">$V</span> - Volume</div>
956
+ <div><span style="color: var(--accent);">$B</span> - Buy Quantity</div>
957
+ <div><span style="color: var(--accent);">$S</span> - Sell Quantity</div>
958
+ <div><span style="color: var(--accent);">$O</span> - Open Interest</div>
959
+ <div style="border-top: 1px solid var(--border); padding-top: 4px; margin-top: 4px; grid-column: span 2;">
960
+ <strong style="color: var(--purple);">Advanced Only:</strong>
961
+ </div>
962
+ <div><span style="color: var(--purple);">$CB</span> - Call Buyers</div>
963
+ <div><span style="color: var(--purple);">$CS</span> - Call Sellers</div>
964
+ <div><span style="color: var(--purple);">$PB</span> - Put Buyers</div>
965
+ <div><span style="color: var(--purple);">$PS</span> - Put Sellers</div>
966
+ </div>
967
+ <div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); font-size: 10px; color: var(--text-secondary);">
968
+ <strong>Advanced Mode Formulas:</strong><br>
969
+ 📈 Buyers = $CB + $PS (Call Buyers + Put Sellers)<br>
970
+ 📉 Sellers = $CS + $PB (Call Sellers + Put Buyers)
971
+ </div>
972
+ </div>
973
 
974
+ <!-- SERIES TOOLTIP -->
975
+ <div id="seriesTooltip" class="tooltip-content">
976
+ <h4>📈 Plot Series Guide</h4>
977
+ <div style="font-size: 11px; color: var(--text-secondary);">
978
+ <p style="margin-bottom: 6px;">Create custom formulas using variables:</p>
979
+ <div style="background: var(--bg-input); padding: 8px; border-radius: 4px; font-family: var(--font-mono); margin-bottom: 6px;">
980
+ Example: <span style="color: var(--cyan);">$B - $S</span>
981
+ </div>
982
+ <p style="margin-bottom: 6px;"><strong style="color: var(--purple);">Advanced Mode:</strong></p>
983
+ <div style="background: var(--bg-input); padding: 8px; border-radius: 4px; font-family: var(--font-mono);">
984
+ Buyers: <span style="color: var(--success);">$CB + $PS</span><br>
985
+ Sellers: <span style="color: var(--danger);">$CS + $PB</span>
986
  </div>
987
  </div>
988
  </div>
989
 
990
  <script>
991
  const App = {
992
+ state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [], []], charts: {}, refreshRate: 15 },
993
+ defaults: {
994
+ normal: [
995
+ { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
996
+ { formula: '$B', color: '#238636', label: 'Buy', axis: 'right' },
997
+ { formula: '$S', color: '#da3633', label: 'Sell', axis: 'right' }
998
+ ],
999
+ advanced: [
1000
+ { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
1001
+ { formula: '$CB + $PS', color: '#238636', label: 'Buyers', axis: 'right' },
1002
+ { formula: '$CS + $PB', color: '#da3633', label: 'Sellers', axis: 'right' }
1003
+ ]
1004
+ },
1005
+
1006
  async init() {
1007
  await this.loadDates();
1008
+ const saved = localStorage.getItem('dc_pro_v4_advanced');
1009
+ if (saved) {
1010
+ try {
1011
+ const s = JSON.parse(saved);
1012
+ this.state = { ...this.state, ...s };
1013
+ // Ensure layout matches columns
1014
+ if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
1015
+ } catch(e) { this.initDefault(); }
1016
+ } else {
1017
+ this.initDefault();
1018
+ }
1019
 
1020
+ // Sync Date UI
1021
+ const dSel = document.getElementById('globalDate');
1022
+ if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
1023
+ else if(dSel.options.length) this.state.date = dSel.options[0].value;
1024
+
1025
+ document.getElementById('globalMode').value = this.state.mode;
1026
+ this.updateModeUI();
1027
  this.renderGrid();
1028
+ this.startLoop();
1029
+ },
1030
+
1031
+ initDefault() {
1032
+ this.state.columns = 3;
1033
+ this.state.layout = [[], [], []];
1034
+ this.state.refreshRate = 15;
1035
+ // 3x2 Grid (3 Cols, 2 Rows = 6 Charts)
1036
+ for(let i=0; i<3; i++) {
1037
+ this.addChartInternal(i);
1038
+ this.addChartInternal(i);
1039
+ }
1040
  },
1041
 
1042
  async loadDates() {
1043
+ try {
1044
+ const res = await fetch('/api/dates');
1045
+ const dates = await res.json();
1046
+ document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
1047
+ if(!this.state.date && dates.length) this.state.date = dates[0];
1048
+ } catch(e) {}
1049
  },
1050
 
1051
+ addChartInternal(colIdx) {
1052
+ const id = 'c-' + Math.random().toString(36).substr(2, 6);
1053
+ this.state.layout[colIdx].push(id);
1054
+ this.state.charts[id] = { config: {}, instance: null };
1055
+ return id;
1056
  },
1057
 
1058
+ save() {
1059
+ // Strip non-serializable data
1060
+ const cleanCharts = {};
1061
+ for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
1062
+
1063
+ localStorage.setItem('dc_pro_v4_advanced', JSON.stringify({
1064
+ date: this.state.date,
1065
+ mode: this.state.mode,
1066
+ columns: this.state.columns,
1067
+ layout: this.state.layout,
1068
+ refreshRate: this.state.refreshRate,
1069
+ charts: cleanCharts
1070
+ }));
1071
+ },
1072
+
1073
+ reset() {
1074
+ if(confirm("Reset Workspace?")) {
1075
+ localStorage.removeItem('dc_pro_v4_advanced');
1076
+ location.reload();
1077
+ }
1078
  },
1079
 
1080
  setLayout(n) {
1081
+ const all = this.state.layout.flat();
1082
  this.state.columns = n;
1083
+ this.state.layout = Array.from({length: n}, () => []);
1084
+ all.forEach((id, i) => this.state.layout[i % n].push(id));
1085
  this.renderGrid();
1086
  this.save();
1087
  },
1088
 
1089
  renderGrid() {
1090
+ // Fix Layout Buttons
1091
+ document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
1092
+
1093
+ const container = document.getElementById('gridRoot');
1094
+ container.innerHTML = ''; // Wipes DOM
1095
 
1096
+ this.state.layout.forEach((ids, colIdx) => {
1097
  const col = document.createElement('div');
1098
  col.className = 'column';
1099
+ const count = ids.length;
1100
+ const viewH = window.innerHeight - 52;
1101
+
1102
  ids.forEach(id => {
1103
+ // IMPORTANT: Since we wiped DOM, old instances are dead.
1104
+ // We MUST nullify them so plot() creates new ones.
1105
+ if(this.state.charts[id] && this.state.charts[id].instance) {
1106
+ this.state.charts[id].instance.destroy();
1107
+ this.state.charts[id].instance = null;
1108
+ }
1109
+
1110
+ const card = this.createCardDOM(id);
1111
+ // 50/50 split if 2 charts, 33% if 3, else fixed
1112
+ if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
1113
+ else card.style.height = '300px';
1114
+
1115
  col.appendChild(card);
1116
+ setTimeout(() => ChartLogic.plot(id), 0);
1117
  });
1118
 
1119
+ const btn = document.createElement('button');
1120
+ btn.className = 'add-chart-btn';
1121
+ btn.innerText = '+ Add Chart';
1122
+ btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
1123
+ col.appendChild(btn);
1124
+ container.appendChild(col);
1125
+ });
1126
  },
1127
 
1128
+ createCardDOM(id) {
1129
  const div = document.createElement('div');
1130
  div.className = 'chart-card';
1131
+ div.id = `card-${id}`;
1132
+
1133
+ const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
1134
+ const isCfg = cfg && cfg.root;
1135
+ const isAdvanced = cfg && cfg.configMode === 'advanced';
1136
+
1137
  div.innerHTML = `
1138
  <div class="card-header">
1139
+ <div class="card-info">
1140
+ <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
1141
+ <span>${isCfg ? cfg.root : 'New Chart'}</span>
1142
+ ${isAdvanced ? '<span class="adv-badge">ADV</span>' : ''}
1143
+ ${isCfg && cfg.instrument ? `<span class="inst-label">${cfg.instrument}</span>` : ''}
1144
+ ${isCfg && cfg.expiry ? `<span class="card-meta">${cfg.expiry}</span>` : ''}
1145
  </div>
1146
+ <div class="card-tools">
1147
+ <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
1148
+ <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
1149
  </div>
1150
  </div>
1151
  <div class="card-body" id="body-${id}">
1152
+ ${!isCfg ? `
1153
+ <div class="empty-state">
1154
+ <div style="font-size:24px; opacity:0.3;">📊</div>
1155
+ <button style="background:transparent; border:1px solid var(--border); padding:5px 10px; border-radius:4px; color:var(--text-secondary);" onclick="Config.open('${id}')">Configure</button>
1156
+ </div>
1157
+ ` : `<canvas id="canvas-${id}"></canvas>`}
1158
  </div>
1159
  `;
1160
  return div;
1161
  },
1162
 
1163
+ removeChart(id) {
1164
+ if(!confirm("Delete?")) return;
1165
+ this.state.layout = this.state.layout.map(c => c.filter(x => x !== id));
1166
+ if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
1167
+ delete this.state.charts[id];
1168
+ this.renderGrid();
1169
+ this.save();
1170
+ },
1171
 
1172
+ onDateChange() {
1173
+ this.state.date = document.getElementById('globalDate').value;
1174
+ const today = new Date().toISOString().split('T')[0];
1175
+ if(this.state.date === today && this.state.mode !== 'LIVE') {
1176
+ this.state.mode = 'LIVE';
1177
+ document.getElementById('globalMode').value = 'LIVE';
1178
+ this.updateModeUI();
1179
+ }
1180
+ this.save();
1181
+ this.refreshAll();
1182
+ },
1183
+
1184
+ onModeChange() {
1185
+ this.state.mode = document.getElementById('globalMode').value;
1186
+ this.updateModeUI();
1187
+ this.save();
1188
+ this.refreshAll();
1189
+ },
1190
+
1191
+ updateModeUI() {
1192
+ document.getElementById('modeDot').className = `status-dot ${this.state.mode==='LIVE'?'live':'hist'}`;
1193
+ },
1194
+
1195
+ refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
1196
+
1197
+ startLoop() {
1198
+ const loop = () => {
1199
+ if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
1200
+ const rate = (this.state.refreshRate || 15) * 1000;
1201
+ setTimeout(loop, rate);
1202
+ };
1203
+ loop();
1204
+ }
1205
+ };
1206
+
1207
+ const ChartLogic = {
1208
  async plot(id, isUpdate=false) {
1209
+ const chart = App.state.charts[id];
1210
+ if(!chart || !chart.config.root) return;
1211
+
1212
  const cfg = chart.config;
1213
+ const dot = document.getElementById(`dot-${id}`);
1214
+ if(isUpdate && dot) dot.classList.add('retry');
1215
+
1216
+ let endTime = cfg.end;
1217
+ if (App.state.mode === 'LIVE') {
1218
+ const now = new Date();
1219
+ endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
1220
+ }
1221
 
1222
  try {
1223
+ let data;
1224
+
1225
+ // Use advanced endpoint for advanced mode
1226
+ if (cfg.configMode === 'advanced' && cfg.instruments && cfg.instruments.length > 0) {
1227
+ const res = await fetch('/api/fetch_advanced_series', {
1228
+ method: 'POST',
1229
+ headers: {'Content-Type': 'application/json'},
1230
+ body: JSON.stringify({
1231
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry,
1232
+ instruments: cfg.instruments, timeframe: cfg.timeframe,
1233
+ start_time: cfg.start, end_time: endTime
1234
+ })
1235
+ });
1236
+ data = await res.json();
1237
+ } else {
1238
+ // Normal mode - single instrument
1239
+ const res = await fetch('/api/fetch_series', {
1240
+ method: 'POST',
1241
+ headers: {'Content-Type': 'application/json'},
1242
+ body: JSON.stringify({
1243
+ date: App.state.date, root: cfg.root, expiry: cfg.expiry,
1244
+ instrument: cfg.instrument, timeframe: cfg.timeframe,
1245
+ start_time: cfg.start, end_time: endTime
1246
+ })
1247
+ });
1248
+ data = await res.json();
1249
+ }
1250
+
1251
+ if(data.error || !data.labels || !data.labels.length) {
1252
+ if(App.state.mode !== 'LIVE') {
1253
+ const b = document.getElementById(`body-${id}`);
1254
+ if(b) b.innerHTML = '<div class="empty-state" style="opacity:0.5">No Data</div>';
1255
+ }
1256
+ if(dot) dot.classList.remove('retry');
1257
+ return;
1258
+ }
1259
 
1260
+ const body = document.getElementById(`body-${id}`);
1261
+ if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
1262
+
1263
+ if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
1264
 
1265
  const datasets = cfg.series.map(s => {
1266
  const vals = data.labels.map((_, i) => {
1267
+ const P = data.P[i] || 0;
1268
+ const V = data.V[i] || 0;
1269
+ const OI = data.OI[i] || 0;
1270
+ const B = data.B[i] || 0;
1271
+ const S = data.S[i] || 0;
1272
+ const CB = data.CB ? (data.CB[i] || 0) : 0;
1273
+ const CS = data.CS ? (data.CS[i] || 0) : 0;
1274
+ const PB = data.PB ? (data.PB[i] || 0) : 0;
1275
+ const PS = data.PS ? (data.PS[i] || 0) : 0;
1276
+
1277
+ let f = s.formula
1278
+ .replace(/\$CB/g, CB)
1279
+ .replace(/\$CS/g, CS)
1280
+ .replace(/\$PB/g, PB)
1281
+ .replace(/\$PS/g, PS)
1282
+ .replace(/\$OI/g, OI)
1283
+ .replace(/\$O/g, OI)
1284
+ .replace(/\$P/g, P)
1285
+ .replace(/\$V/g, V)
1286
+ .replace(/\$B/g, B)
1287
+ .replace(/\$S/g, S);
1288
+ try { return new Function('return '+f)(); } catch{ return null; }
1289
  });
1290
  return {
1291
  label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
1292
+ borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
1293
  };
1294
  });
1295
 
1296
+ this.render(id, data.labels, datasets);
1297
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1298
  } catch(e) { console.error(e); }
1299
+ },
1300
+
1301
+ render(id, labels, datasets) {
1302
+ const ctx = document.getElementById(`canvas-${id}`);
1303
+ if(!ctx) return;
1304
+
1305
+ // Ensure we aren't updating a dead instance (Double check)
1306
+ if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
1307
+ App.state.charts[id].instance.destroy();
1308
+ App.state.charts[id].instance = null;
1309
+ }
1310
+
1311
+ if(App.state.charts[id].instance) {
1312
+ App.state.charts[id].instance.data.labels = labels;
1313
+ App.state.charts[id].instance.data.datasets = datasets;
1314
+ App.state.charts[id].instance.update('none');
1315
+ } else {
1316
+ const hasRight = datasets.some(d => d.yAxisID === 'y1');
1317
+ App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
1318
+ type: 'line',
1319
+ data: { labels, datasets },
1320
+ options: {
1321
+ responsive: true, maintainAspectRatio: false, animation: false,
1322
+ interaction: { mode: 'index', intersect: false },
1323
+ plugins: {
1324
+ legend: { display: false },
1325
+ tooltip: { backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor: '#30363d', borderWidth: 1 }
1326
+ },
1327
+ scales: {
1328
+ x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
1329
+ y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
1330
+ y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
1331
+ }
1332
+ }
1333
+ });
1334
+ }
1335
  }
1336
  };
1337
 
1338
  const Config = {
1339
+ tid: null,
1340
+ isGlobal: false,
1341
+ configMode: 'normal',
1342
+ instType: 'options',
1343
+ currentOptions: [],
1344
 
1345
  async open(id) {
1346
+ this.tid = id;
1347
+ this.isGlobal = false;
 
1348
  document.getElementById('configModal').style.display = 'flex';
1349
+ document.getElementById('cfgTitle').innerText = 'Configure Chart';
1350
+ document.getElementById('cfgLoading').style.display = 'flex';
1351
+ document.getElementById('cfgContent').style.display = 'none';
1352
+
1353
+ document.getElementById('cfgGlobalOnly').style.display = 'none';
1354
+ document.getElementById('cfgChartOnly').style.display = 'block';
1355
+
1356
+ const rSel = document.getElementById('cfgRoot');
1357
+ const res = await fetch(`/api/roots?date=${App.state.date}`);
1358
+ const roots = await res.json();
1359
+ rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
1360
+
1361
+ const cfg = App.state.charts[id].config;
1362
 
1363
+ // Set mode from config
1364
+ this.configMode = cfg.configMode || 'normal';
1365
+ this.instType = cfg.instType || 'options';
1366
+ this.setConfigMode(this.configMode);
1367
+ this.setInstType(this.instType);
1368
 
1369
+ document.getElementById('cfgTF').value = cfg.timeframe || '1min';
1370
+ document.getElementById('cfgStart').value = cfg.start || '09:15';
1371
+ document.getElementById('cfgEnd').value = cfg.end || '15:30';
1372
+
1373
+ if (cfg.atmRange) {
1374
+ document.getElementById('cfgAtmRange').value = cfg.atmRange;
1375
+ }
1376
+
1377
+ // Render series based on mode
1378
+ this.renderSeries(cfg.series || App.defaults[this.configMode]);
1379
+
1380
+ if(cfg.root) {
1381
+ rSel.value = cfg.root;
1382
  await this.onRootChange(cfg.expiry, cfg.instrument);
1383
  }
1384
 
1385
+ document.getElementById('cfgLoading').style.display = 'none';
1386
+ document.getElementById('cfgContent').style.display = 'block';
1387
+ },
1388
+
1389
+ openDefaults() {
1390
+ this.isGlobal = true;
1391
+ document.getElementById('configModal').style.display = 'flex';
1392
+ document.getElementById('cfgTitle').innerText = 'Global Settings';
1393
+ document.getElementById('cfgLoading').style.display = 'none';
1394
+ document.getElementById('cfgContent').style.display = 'block';
1395
+
1396
+ document.getElementById('cfgGlobalOnly').style.display = 'block';
1397
+ document.getElementById('cfgChartOnly').style.display = 'none';
1398
+ document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
1399
+
1400
+ this.renderSeries(App.defaults.normal);
1401
+ },
1402
+
1403
+ close() {
1404
+ document.getElementById('configModal').style.display = 'none';
1405
+ // Hide tooltips
1406
+ document.getElementById('varTooltip').classList.remove('show');
1407
+ document.getElementById('seriesTooltip').classList.remove('show');
1408
  },
1409
 
1410
+ setConfigMode(mode) {
1411
+ this.configMode = mode;
1412
+ const isAdvanced = mode === 'advanced';
1413
+
1414
+ // Update buttons
1415
+ document.getElementById('btnNormalMode').classList.toggle('active', !isAdvanced);
1416
+ document.getElementById('btnAdvancedMode').classList.toggle('active', isAdvanced);
1417
+ document.getElementById('btnAdvancedMode').classList.toggle('advanced', isAdvanced);
1418
+
1419
+ // Update badge
1420
+ const badge = document.getElementById('cfgModeBadge');
1421
+ badge.textContent = isAdvanced ? 'Advanced' : 'Normal';
1422
+ badge.className = 'mode-badge ' + mode;
1423
 
1424
+ // Toggle sections
1425
+ document.getElementById('instTypeSection').style.display = isAdvanced ? 'none' : 'flex';
1426
+ document.getElementById('instrumentSelectSection').style.display = isAdvanced ? 'none' : 'block';
1427
+ document.getElementById('atmRangeSection').style.display = isAdvanced ? 'block' : 'none';
1428
+ document.getElementById('variablesSection').style.display = isAdvanced ? 'block' : 'none';
1429
+
1430
+ // Update series defaults if no custom series set
1431
+ const currentSeries = document.querySelectorAll('#cfgSeries .series-row');
1432
+ if (currentSeries.length === 0 || (App.state.charts[this.tid] && !App.state.charts[this.tid].config.series)) {
1433
+ this.renderSeries(App.defaults[mode]);
1434
+ }
1435
+
1436
+ // If advanced mode, refresh options list
1437
+ if (isAdvanced && document.getElementById('cfgExp').value) {
1438
+ this.onExpChange();
1439
+ }
1440
+ },
1441
+
1442
+ setInstType(type) {
1443
+ this.instType = type;
1444
+ document.getElementById('btnOptions').classList.toggle('active', type === 'options');
1445
+ document.getElementById('btnFutures').classList.toggle('active', type === 'futures');
1446
+
1447
+ // Refresh instruments list
1448
+ if (document.getElementById('cfgExp').value) {
1449
+ this.onExpChange();
1450
  }
1451
  },
1452
 
1453
  async onRootChange(preExp, preInst) {
1454
  const root = document.getElementById('cfgRoot').value;
1455
+ const eSel = document.getElementById('cfgExp');
1456
+ eSel.innerHTML = '<option>Loading...</option>';
1457
+ const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
1458
+ const exps = await res.json();
1459
+ eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e=>`<option>${e}</option>`).join('');
1460
 
1461
+ if(preExp && exps.includes(preExp)) { eSel.value = preExp; await this.onExpChange(preInst); }
1462
+ else {
1463
+ const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
1464
+ if(auto.current) { eSel.value = auto.current.expiry; await this.onExpChange(auto.current.instrument); }
1465
+ }
1466
  },
1467
 
1468
+ async onExpChange(preInst) {
1469
+ const root = document.getElementById('cfgRoot').value;
1470
  const exp = document.getElementById('cfgExp').value;
1471
+
1472
+ if (this.configMode === 'advanced') {
1473
+ // Fetch options chain for advanced mode
1474
+ const res = await fetch(`/api/options_chain?date=${App.state.date}&root=${root}&expiry=${exp}`);
1475
+ const options = await res.json();
1476
+ this.currentOptions = options;
1477
+ this.updateStrikesPreview();
1478
+ } else {
1479
+ // Normal mode - fetch filtered instruments
1480
+ const iSel = document.getElementById('cfgInst');
1481
+ iSel.innerHTML = '<option>Loading...</option>';
1482
+
1483
+ const instType = this.instType === 'futures' ? 'futures' : 'options';
1484
+ const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}&instrument_type=${instType}`);
1485
+ const insts = await res.json();
1486
+ iSel.innerHTML = insts.map(i=>`<option>${i}</option>`).join('');
1487
+ if(preInst && insts.includes(preInst)) iSel.value = preInst;
1488
+ }
1489
+ },
1490
+
1491
+ updateStrikesPreview() {
1492
+ if (this.configMode !== 'advanced' || !this.currentOptions.length) return;
1493
+
1494
+ const range = parseInt(document.getElementById('cfgAtmRange').value) || 5;
1495
+ const totalStrikes = range * 2 + 1;
1496
+ document.getElementById('totalStrikes').textContent = totalStrikes;
1497
+
1498
+ // Get unique strikes
1499
+ const strikes = [...new Set(this.currentOptions.map(o => o.strike))].sort((a,b) => a - b);
1500
+
1501
+ if (strikes.length > 0) {
1502
+ // Show preview of strikes selection
1503
+ const preview = document.getElementById('strikesPreview');
1504
+ const midIdx = Math.floor(strikes.length / 2);
1505
+ const startIdx = Math.max(0, midIdx - range);
1506
+ const endIdx = Math.min(strikes.length, midIdx + range + 1);
1507
+
1508
+ const selectedStrikes = strikes.slice(startIdx, endIdx);
1509
+ preview.innerHTML = `
1510
+ <div>Strikes: ${selectedStrikes.slice(0, 3).join(', ')} ... ${selectedStrikes.slice(-3).join(', ')}</div>
1511
+ <div style="margin-top:4px; color: var(--text-primary);">
1512
+ ${(range)} ITM + ATM + ${range} OTM = ${totalStrikes} total
1513
+ </div>
1514
+ `;
1515
+ preview.style.display = 'block';
1516
+ }
1517
  },
1518
 
1519
  renderSeries(list) {
1520
+ const c = document.getElementById('cfgSeries');
1521
+ c.innerHTML = '';
1522
+ list.forEach(s => {
1523
+ const r = document.createElement('div');
1524
+ r.className = 'series-row';
1525
+ r.innerHTML = `
1526
+ <input type="color" value="${s.color}" class="color-input s-color">
1527
+ <input type="text" value="${s.formula}" class="s-form" placeholder="$P">
1528
+ <input type="text" value="${s.label}" class="s-lbl" placeholder="Label">
1529
+ <select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1530
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1531
+ `;
1532
+ c.appendChild(r);
1533
+ });
1534
  },
1535
 
1536
+ addSeriesRow() {
1537
+ const r = document.createElement('div');
1538
+ r.className = 'series-row';
1539
+ r.innerHTML = `
1540
+ <input type="color" value="#ffffff" class="color-input s-color">
1541
+ <input type="text" value="$P" class="s-form">
1542
+ <input type="text" value="New" class="s-lbl">
1543
+ <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1544
+ <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1545
  `;
1546
+ document.getElementById('cfgSeries').appendChild(r);
1547
  },
1548
 
1549
+ showVarTooltip(event) {
1550
+ event.stopPropagation();
1551
+ const tooltip = document.getElementById('varTooltip');
1552
+ tooltip.classList.toggle('show');
1553
+
1554
+ // Position tooltip near click
1555
+ const rect = event.target.getBoundingClientRect();
1556
+ tooltip.style.left = `${event.clientX}px`;
1557
+ tooltip.style.top = `${rect.bottom + 10}px`;
1558
+ },
1559
+
1560
+ showSeriesTooltip(event) {
1561
+ event.stopPropagation();
1562
+ const tooltip = document.getElementById('seriesTooltip');
1563
+ tooltip.classList.toggle('show');
1564
+
1565
+ const rect = event.target.getBoundingClientRect();
1566
+ tooltip.style.left = `${event.clientX}px`;
1567
+ tooltip.style.top = `${rect.bottom + 10}px`;
1568
+ },
1569
+
1570
+ async save() {
1571
+ const rows = document.querySelectorAll('#cfgSeries .series-row');
1572
+ const series = Array.from(rows).map(r => ({
1573
+ color: r.querySelector('.s-color').value,
1574
  formula: r.querySelector('.s-form').value,
1575
+ label: r.querySelector('.s-lbl').value,
1576
  axis: r.querySelector('.s-axis').value
1577
  }));
1578
 
1579
+ if(this.isGlobal) {
1580
+ App.defaults[this.configMode] = series;
1581
+ App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1582
+ App.save();
1583
+ }
1584
+ else {
1585
+ const root = document.getElementById('cfgRoot').value;
1586
+ const exp = document.getElementById('cfgExp').value;
1587
+ if(!root || !exp) return alert("Incomplete Details");
1588
+
1589
+ const config = {
1590
+ root,
1591
+ expiry: exp,
1592
+ configMode: this.configMode,
1593
+ instType: this.instType,
1594
+ timeframe: document.getElementById('cfgTF').value,
1595
+ start: document.getElementById('cfgStart').value,
1596
+ end: document.getElementById('cfgEnd').value,
1597
+ series
1598
+ };
1599
+
1600
+ if (this.configMode === 'advanced') {
1601
+ // Calculate instruments for ATM +- range
1602
+ const atmRange = parseInt(document.getElementById('cfgAtmRange').value) || 5;
1603
+ config.atmRange = atmRange;
1604
+ config.instrument = `ATM ±${atmRange}`;
1605
+
1606
+ // Get strikes and select instruments
1607
+ if (this.currentOptions.length > 0) {
1608
+ const strikes = [...new Set(this.currentOptions.map(o => o.strike))].sort((a,b) => a - b);
1609
+ const midIdx = Math.floor(strikes.length / 2);
1610
+ const startIdx = Math.max(0, midIdx - atmRange);
1611
+ const endIdx = Math.min(strikes.length, midIdx + atmRange + 1);
1612
+ const selectedStrikes = strikes.slice(startIdx, endIdx);
1613
+
1614
+ // Get all CE and PE instruments for selected strikes
1615
+ config.instruments = this.currentOptions
1616
+ .filter(o => selectedStrikes.includes(o.strike))
1617
+ .map(o => o.symbol);
1618
+ }
1619
+ } else {
1620
+ // Normal mode
1621
+ const inst = document.getElementById('cfgInst').value;
1622
+ if(!inst) return alert("Please select an instrument");
1623
+ config.instrument = inst;
1624
+ }
1625
+
1626
+ App.state.charts[this.tid].config = config;
1627
+ App.save();
1628
+ App.renderGrid();
1629
+ ChartLogic.plot(this.tid);
1630
+ }
1631
  this.close();
1632
+ }
 
1633
  };
1634
 
1635
+ // Close tooltips when clicking outside
1636
+ document.addEventListener('click', (e) => {
1637
+ if (!e.target.closest('.info-icon') && !e.target.closest('.tooltip-content')) {
1638
+ document.getElementById('varTooltip').classList.remove('show');
1639
+ document.getElementById('seriesTooltip').classList.remove('show');
1640
+ }
1641
+ });
1642
+
1643
  window.onload = () => App.init();
1644
  </script>
1645
  </body>