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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +407 -847
app.py CHANGED
@@ -1,13 +1,13 @@
 
1
  import pandas as pd
2
  import requests
3
  import uvicorn
4
  import json
5
  import asyncio
6
- import re
7
  from datetime import datetime, timedelta
8
  from fastapi import FastAPI, Body, HTTPException
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
- from fastapi.staticfiles import StaticFiles
11
  from typing import Optional, List, Dict, Any
12
 
13
  # ==========================================
@@ -28,7 +28,6 @@ app = FastAPI()
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)
@@ -36,7 +35,7 @@ def sb_get(endpoint: str, params: dict = None):
36
  return r.json()
37
  except Exception as e:
38
  print(f"DB Error ({endpoint}): {e}")
39
- return[]
40
 
41
  # ==========================================
42
  # 3. API ROUTES - METADATA
@@ -48,88 +47,45 @@ def home():
48
 
49
  @app.get("/api/dates")
50
  def get_dates():
51
- """Fetch distinct dates."""
52
- data = sb_get("/history_buckets_fyers", {"select": "minute", "order": "minute.desc", "limit": "50000"})
53
- dates = set()
54
- for row in data:
55
- if row.get('minute'):
56
- dates.add(row['minute'].split('T')[0])
57
- return JSONResponse(sorted(list(dates), reverse=True))
58
 
59
  @app.get("/api/roots")
60
  def get_roots(date: str):
61
- """Fetch roots available for a specific date."""
62
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
63
  data = sb_get(f"/history_buckets_fyers?select=root&minute=gte.{start}&minute=lte.{end}")
64
- roots = set()
65
- for row in data:
66
- if row.get('root'):
67
- roots.add(row['root'])
68
- return JSONResponse(sorted(list(roots)))
69
 
70
- @app.get("/api/auto_config")
71
- def get_auto_config(date: str, root: str):
72
- """Smart helper for defaults (Current/Next expiry)."""
73
  start, end = f"{date}T00:00:00", f"{date}T23:59:59"
74
- data = sb_get(f"/history_buckets_fyers?select=expiry,data&root=eq.{root}&expiry=neq.MARKET&minute=gte.{start}&minute=lte.{end}&limit=50")
 
 
 
 
 
 
75
 
76
- expiry_map = {}
77
- for row in data:
78
- exp = row.get('expiry')
79
- if not exp: continue
80
- if exp not in expiry_map:
81
- keys = list(row.get('data', {}).keys())
82
- expiry_map[exp] = keys
83
- else:
84
- existing = set(expiry_map[exp])
85
- new_keys = row.get('data', {}).keys()
86
- expiry_map[exp] = list(existing.union(new_keys))
87
-
88
- all_expiries = sorted(expiry_map.keys())
89
- valid_expiries = [e for e in all_expiries if e >= date]
90
- if not valid_expiries:
91
- valid_expiries = all_expiries
92
-
93
- response = {"expiries": all_expiries, "current": None, "next": None}
94
-
95
- def find_fut(instruments):
96
- for i in instruments:
97
- if 'FUT' in i: return i
98
- return instruments[0] if instruments else None
99
-
100
- if len(valid_expiries) > 0:
101
- exp_c = valid_expiries[0]
102
- inst_c = find_fut(expiry_map[exp_c])
103
- response["current"] = {"expiry": exp_c, "instrument": inst_c}
104
-
105
- if len(valid_expiries) > 1:
106
- exp_n = valid_expiries[1]
107
- inst_n = find_fut(expiry_map[exp_n])
108
- response["next"] = {"expiry": exp_n, "instrument": inst_n}
109
-
110
- return JSONResponse(response)
111
-
112
- @app.get("/api/expiries")
113
- def get_expiries(date: str, root: str):
114
- """Fetch distinct expiries."""
115
- start, end = f"{date}T00:00:00", f"{date}T23:59:59"
116
- data = sb_get(f"/history_buckets_fyers?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
117
- exps = set(row['expiry'] for row in data if row.get('expiry'))
118
- return JSONResponse(sorted(list(exps)))
119
-
120
- @app.get("/api/instruments")
121
- def get_instruments(date: str, root: str, expiry: str):
122
- """Fetch instruments."""
123
- start, end = f"{date}T00:00:00", f"{date}T23:59:59"
124
- data = sb_get(f"/history_buckets_fyers?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=5")
125
- instruments = set()
126
  for row in data:
127
- keys = row.get('data', {}).keys()
128
- instruments.update(keys)
129
- return JSONResponse(sorted(list(instruments)))
 
 
 
 
 
 
 
 
 
130
 
131
  # ==========================================
132
- # 4. API ROUTES - DATA FETCHING
133
  # ==========================================
134
 
135
  @app.post("/api/fetch_series")
@@ -137,185 +93,145 @@ def fetch_series(
137
  date: str = Body(...),
138
  root: str = Body(...),
139
  expiry: str = Body(...),
140
- instrument: str = Body(""),
141
- timeframe: str = Body(...),
 
 
142
  start_time: str = Body(...),
143
- end_time: str = Body(...),
144
- adv_mode: bool = Body(False),
145
- atm_range: int = Body(5)
146
  ):
147
- """
148
- Fetches raw 10s data. Supports normal (optimized Postgres JSON filtering)
149
- and advanced mode (aggregated ATM +/- Options dynamics).
150
- """
151
  t_start = f"{date}T{start_time}:00"
152
  t_end = f"{date}T{end_time}:59"
153
 
154
- records =[]
155
-
156
- if not adv_mode:
157
- # --- NORMAL MODE (Highly Optimized Postgres Select) ---
158
- select_query = f'minute,inst_data:data->"{instrument}"'
159
- 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"
160
- rows = sb_get(url)
161
 
162
- for row in rows:
163
- try:
164
- base_ts = datetime.fromisoformat(row['minute'])
165
- inst_data = row.get('inst_data')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- if not inst_data: continue
168
-
169
- p_arr = inst_data.get('p', []) or []
170
- b_arr = inst_data.get('b', []) or[]
171
- s_arr = inst_data.get('s', []) or[]
172
- v_arr = inst_data.get('v', []) or []
173
- o_arr = inst_data.get('o',[]) or[]
 
 
 
174
 
175
- for i in range(6):
176
- if i < len(p_arr) and p_arr[i] is not None:
177
- records.append({
178
- "ts": base_ts + timedelta(seconds=i*10),
179
- "price": float(p_arr[i]),
180
- "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
181
- "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
182
- "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
183
- "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0
184
- })
185
- except Exception:
186
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  else:
189
- # --- ADVANCED MODE (Full JSON Fetch & Custom Multi-Strike Aggregation) ---
190
- select_query = 'minute,data'
191
  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"
192
  rows = sb_get(url)
193
-
194
- last_p_ref = 0.0
195
-
196
  for row in rows:
197
- try:
198
- base_ts = datetime.fromisoformat(row['minute'])
199
- data_dict = row.get('data', {})
200
- if not data_dict: continue
201
-
202
- # Identify reference instrument (Index, Equity or Future)
203
- ref_inst = None
204
- for inst in data_dict.keys():
205
- if "-EQ" in inst or "-INDEX" in inst or "FUT" in inst:
206
- ref_inst = inst
207
- break
208
-
209
- # Pre-parse option instruments to map strikes to CE/PE
210
- options_meta = {}
211
- strikes_set = set()
212
- for inst in data_dict.keys():
213
- match = re.search(r'(\d+)(CE|PE)$', inst)
214
- if match:
215
- strike = int(match.group(1))
216
- opt_type = match.group(2)
217
- strikes_set.add(strike)
218
- if strike not in options_meta:
219
- options_meta[strike] = {}
220
- options_meta[strike][opt_type] = inst
221
-
222
- sorted_strikes = sorted(list(strikes_set))
223
- if not sorted_strikes: continue
224
-
225
- # Process all 6 x 10sec slots inside the minute
226
- for i in range(6):
227
- p_ref = last_p_ref
228
- if ref_inst and ref_inst in data_dict:
229
- p_arr = data_dict[ref_inst].get('p', [])
230
- if i < len(p_arr) and p_arr[i] is not None:
231
- p_ref = float(p_arr[i])
232
- last_p_ref = p_ref
233
-
234
- if p_ref == 0.0:
235
- continue
236
-
237
- # Dynamic ATM calculation per tick
238
- atm_strike = min(sorted_strikes, key=lambda x: abs(x - p_ref))
239
- atm_idx = sorted_strikes.index(atm_strike)
240
-
241
- start_idx = max(0, atm_idx - atm_range)
242
- end_idx = min(len(sorted_strikes) - 1, atm_idx + atm_range)
243
- selected_strikes = sorted_strikes[start_idx : end_idx + 1]
244
-
245
- cb_tot, cs_tot, pb_tot, ps_tot = 0.0, 0.0, 0.0, 0.0
246
- v_tot, o_tot = 0.0, 0.0
247
-
248
- for s in selected_strikes:
249
- # Call Accumulation
250
- ce_inst = options_meta[s].get('CE')
251
- if ce_inst:
252
- ce_data = data_dict[ce_inst]
253
- if i < len(ce_data.get('b',[])) and ce_data['b'][i] is not None: cb_tot += float(ce_data['b'][i])
254
- if i < len(ce_data.get('s', [])) and ce_data['s'][i] is not None: cs_tot += float(ce_data['s'][i])
255
- if i < len(ce_data.get('v', [])) and ce_data['v'][i] is not None: v_tot += float(ce_data['v'][i])
256
- if i < len(ce_data.get('o', [])) and ce_data['o'][i] is not None: o_tot += float(ce_data['o'][i])
257
-
258
- # Put Accumulation
259
- pe_inst = options_meta[s].get('PE')
260
- if pe_inst:
261
- pe_data = data_dict[pe_inst]
262
- if i < len(pe_data.get('b', [])) and pe_data['b'][i] is not None: pb_tot += float(pe_data['b'][i])
263
- if i < len(pe_data.get('s',[])) and pe_data['s'][i] is not None: ps_tot += float(pe_data['s'][i])
264
- if i < len(pe_data.get('v', [])) and pe_data['v'][i] is not None: v_tot += float(pe_data['v'][i])
265
- if i < len(pe_data.get('o', [])) and pe_data['o'][i] is not None: o_tot += float(pe_data['o'][i])
266
-
267
  records.append({
268
  "ts": base_ts + timedelta(seconds=i*10),
269
- "price": p_ref,
270
- "cb": cb_tot,
271
- "cs": cs_tot,
272
- "pb": pb_tot,
273
- "ps": ps_tot,
274
- "v": v_tot,
275
- "o": o_tot
276
  })
277
- except Exception as e:
278
- print(f"Error parsing advanced row: {e}")
279
- continue
280
 
281
  if not records:
282
- return JSONResponse({"error": "No data found", "labels": [], "P":[]})
283
 
284
- df = pd.DataFrame(records)
285
- df.set_index('ts', inplace=True)
286
-
287
- tf_map = {
288
- "1min": "1min", "3min": "3min", "5min": "5min",
289
- "15min": "15min", "30min": "30min", "1hour": "1h"
290
- }
291
- panda_tf = tf_map.get(timeframe, "1min")
292
 
293
- resampled = df.resample(panda_tf).median().dropna()
 
 
 
 
 
 
 
 
294
 
295
- # Return formatted JSON properly matching active mode
296
- if not adv_mode:
297
- response_data = {
298
- "labels": resampled.index.strftime('%H:%M').tolist(),
299
- "P": resampled['price'].tolist(),
300
- "B": resampled['buy_qty'].tolist(),
301
- "S": resampled['sell_qty'].tolist(),
302
- "V": resampled['volume'].tolist(),
303
- "OI": resampled['oi'].tolist()
304
- }
305
- else:
306
- response_data = {
307
- "labels": resampled.index.strftime('%H:%M').tolist(),
308
- "P": resampled['price'].tolist(),
309
- "CB": resampled['cb'].tolist(),
310
- "CS": resampled['cs'].tolist(),
311
- "PB": resampled['pb'].tolist(),
312
- "PS": resampled['ps'].tolist(),
313
- "V": resampled['v'].tolist(),
314
- "OI": resampled['o'].tolist()
315
- }
316
-
317
- return JSONResponse(response_data)
318
-
319
 
320
  # ==========================================
321
  # 5. FRONTEND TEMPLATE
@@ -326,755 +242,399 @@ HTML_TEMPLATE = """
326
  <html lang="en">
327
  <head>
328
  <meta charset="UTF-8">
329
- <title>DepthChain PRO</title>
330
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
331
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
332
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
333
  <style>
334
- /* --- THEME --- */
335
  :root {
336
- --bg-body: #0d1117;
337
- --bg-header: #161b22;
338
- --bg-card: #0d1117;
339
- --bg-panel: #21262d;
340
- --bg-input: #161b22;
341
- --border: #30363d;
342
- --border-hover: #8b949e;
343
- --text-primary: #e6edf3;
344
- --text-secondary: #8b949e;
345
- --accent: #2f81f7;
346
- --success: #238636;
347
- --danger: #da3633;
348
- --warning: #d29922;
349
- --font-main: 'Inter', sans-serif;
350
- --font-mono: 'JetBrains Mono', monospace;
351
- }
352
- * { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
353
- body {
354
- background: var(--bg-body); color: var(--text-primary); font-family: var(--font-main);
355
- display: flex; flex-direction: column; height: 100vh; overflow: hidden; font-size: 13px;
356
- }
357
- ::-webkit-scrollbar { width: 10px; height: 10px; }
358
- ::-webkit-scrollbar-track { background: var(--bg-body); }
359
- ::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
360
-
361
- /* HEADER */
362
- .top-bar {
363
- height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
364
- display: flex; align-items: center; justify-content: space-between; padding: 0 16px; flex-shrink: 0;
365
- }
366
- .brand { font-weight: 700; font-size: 15px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
367
- .brand span { color: var(--accent); background: rgba(47, 129, 247, 0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
368
- .controls { display: flex; align-items: center; gap: 12px; }
369
-
370
- select, button, input {
371
- background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
372
- padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer;
373
- }
374
- select:focus, input:focus { border-color: var(--accent); }
375
- button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
376
- button.primary { background: var(--accent); border-color: var(--accent); color: white; }
377
- button.primary:hover { background: #58a6ff; }
378
- button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
379
- button.danger:hover { background: rgba(218, 54, 51, 0.1); }
380
-
381
- .layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
382
- .layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
383
- .layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
384
-
385
- /* GRID */
386
- .main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
387
- .column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
388
- .column:last-child { border-right: none; }
389
-
390
- .add-chart-btn {
391
- height: 40px; border: none; border-top: 1px dashed var(--border);
392
- background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600;
393
  }
394
- .add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
395
-
396
- /* CARD */
397
- .chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
398
- .card-header {
399
- height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
400
- padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
401
- }
402
- .card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
403
- .inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; background:rgba(47,129,247,0.1); padding:2px 6px; border-radius:4px; }
404
- .card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
405
 
406
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
407
- .status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63, 185, 80, 0.6); animation: pulse 2s infinite; }
408
- .status-dot.hist { background: var(--accent); }
409
- .status-dot.retry { background: var(--warning); }
410
- @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
411
-
412
- .tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
413
- .tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
414
-
415
- .card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
416
- .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%); }
 
 
 
 
 
 
417
 
418
  /* MODAL */
419
- .modal-backdrop {
420
- position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7);
421
- z-index: 100; display: none; justify-content: center; align-items: center;
422
- }
423
- .modal {
424
- background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px;
425
- width: 480px; max-width: 90%; max-height: 85vh; display: flex; flex-direction: column;
426
- box-shadow: 0 20px 50px rgba(0,0,0,0.5);
427
- }
428
- .modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
429
- .modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
430
- .modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
431
 
432
- .form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
433
- .series-row { display: grid; grid-template-columns: 32px 1fr 1fr 60px 24px; gap: 8px; align-items: center; margin-bottom: 6px; }
434
- .color-input { padding: 0; width: 100%; border: none; background: none; height: 28px; cursor: pointer; }
435
-
436
- /* TOOLTIP & INFO ICON */
437
- .tooltip-container { position: relative; display: inline-flex; align-items: center; cursor: help; margin-left:8px; }
438
- .tooltip-icon {
439
- width: 14px; height: 14px; border-radius: 50%; background: var(--border-hover);
440
- color: #fff; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold;
441
- }
442
- .tooltip-content {
443
- visibility: hidden; width: 220px; background: var(--bg-panel); color: var(--text-primary); text-align: left;
444
- border: 1px solid var(--border); border-radius: 6px; padding: 12px; position: absolute; z-index: 10;
445
- bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity 0.2s;
446
- font-size: 11px; font-weight: normal; font-family: var(--font-mono); box-shadow: 0 4px 12px rgba(0,0,0,0.5);
447
- text-transform: none; line-height: 1.4;
448
- }
449
- .tooltip-container:hover .tooltip-content { visibility: visible; opacity: 1; }
450
- optgroup { background: var(--bg-panel); color: var(--text-primary); font-weight: bold; }
451
  </style>
452
  </head>
453
  <body>
454
 
455
  <div class="top-bar">
456
- <div class="brand">DepthChain <span>PRO</span></div>
457
- <div class="controls">
458
- <select id="globalDate" onchange="App.onDateChange()" style="min-width: 140px;"></select>
459
- <div style="position: relative;">
460
- <select id="globalMode" onchange="App.onModeChange()" style="padding-left: 28px; font-weight: 600;">
461
- <option value="LIVE">LIVE</option>
462
- <option value="HIST">HISTORY</option>
463
- </select>
464
- <div id="modeDot" class="status-dot live" style="position: absolute; left: 10px; top: 12px; pointer-events: none;"></div>
465
- </div>
466
- <div style="width:1px; height: 20px; background: var(--border);"></div>
467
- <div class="layout-toggle">
468
  <button class="layout-btn" onclick="App.setLayout(1)">1</button>
469
  <button class="layout-btn" onclick="App.setLayout(2)">2</button>
470
- <button class="layout-btn active" onclick="App.setLayout(3)">3</button>
471
- <button class="layout-btn" onclick="App.setLayout(4)">4</button>
472
  </div>
473
- <button onclick="Config.openDefaults()">Settings</button>
474
- <button class="danger" onclick="App.reset()">Reset</button>
475
  </div>
476
  </div>
477
 
478
  <div class="main-container" id="gridRoot"></div>
479
 
480
- <!-- CONFIG MODAL -->
481
- <div class="modal-backdrop" id="configModal">
482
  <div class="modal">
483
- <div class="modal-head">
484
- <span id="cfgTitle">Configure Chart</span>
485
- <button class="tool-btn" onclick="Config.close()"></button>
486
  </div>
487
- <div class="modal-body">
488
- <div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading...</div>
489
- <div id="cfgContent">
490
-
491
- <!-- Global Settings Mode -->
492
- <div id="cfgGlobalOnly" style="display:none;">
493
- <div class="form-section">
494
- <label>Refresh Rate (Seconds)</label>
495
- <input type="number" id="cfgRefresh" value="15" min="5">
496
- </div>
497
- <div style="height:1px; background:var(--border); margin: 16px 0;"></div>
498
  </div>
 
499
 
500
- <!-- Single Chart Config Mode -->
501
- <div id="cfgChartOnly">
502
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px;">
503
- <div class="form-section">
504
- <label>Symbol</label>
505
- <select id="cfgRoot" onchange="Config.onRootChange()" style="width:100%"></select>
506
- </div>
507
- <div class="form-section">
508
- <label>Expiry</label>
509
- <select id="cfgExp" onchange="Config.onExpChange()" style="width:100%"></select>
510
- </div>
511
- </div>
512
 
513
- <!-- Mode Toggle & Instrument / ATM Options -->
514
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px; padding: 12px; border: 1px solid var(--border); border-radius: 6px; background:rgba(255,255,255,0.02);">
515
- <div class="form-section" style="margin-bottom:0;">
516
- <label>Analysis Mode</label>
517
- <select id="cfgMode" onchange="Config.onChartModeChange()" style="width:100%">
518
- <option value="normal">Normal (Single)</option>
519
- <option value="advanced">Advanced (ATM Options)</option>
520
- </select>
521
- </div>
522
-
523
- <!-- Normal Mode: Instrument Selector -->
524
- <div class="form-section" id="instBox" style="margin-bottom:0;">
525
- <label>Instrument</label>
526
- <select id="cfgInst" style="width:100%"></select>
527
- </div>
528
-
529
- <!-- Advanced Mode: ATM Range Input -->
530
- <div class="form-section" id="atmBox" style="margin-bottom:0; display:none;">
531
- <label>ATM +/- Strikes</label>
532
- <input type="number" id="cfgAtm" value="5" min="1" max="25" style="width:100%">
533
- </div>
534
- </div>
535
 
536
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
537
- <div class="form-section">
538
- <label>Timeframe</label>
539
- <select id="cfgTF" style="width:100%">
540
- <option value="1min">1 Minute</option>
541
- <option value="3min">3 Minutes</option>
542
- <option value="5min">5 Minutes</option>
543
- <option value="15min">15 Minutes</option>
544
- <option value="30min">30 Minutes</option>
545
- <option value="1hour">1 Hour</option>
546
- </select>
547
- </div>
548
- <div class="form-section">
549
- <label>Range (HH:MM)</label>
550
- <div style="display:flex; gap:8px;">
551
- <input type="time" id="cfgStart" value="09:15">
552
- <input type="time" id="cfgEnd" value="15:30">
553
- </div>
554
- </div>
555
  </div>
556
- <div style="height:1px; background:var(--border); margin: 12px 0;"></div>
557
  </div>
 
558
 
559
- <!-- Common Series Config -->
560
- <div class="form-section">
561
- <label style="display:flex; align-items:center;">
562
- Plot Series Formulas
563
- <div class="tooltip-container">
564
- <div class="tooltip-icon">i</div>
565
- <div class="tooltip-content">
566
- <strong style="color:var(--accent)">Global Variables:</strong><br>
567
- $P = Price (Spot/Future)<br>
568
- $V = Total Volume<br>
569
- $OI = Total Open Interest<br>
570
- $B = Total Buy Qty<br>
571
- $S = Total Sell Qty<br><br>
572
- <strong style="color:var(--warning)">Advanced Mode Only:</strong><br>
573
- $CB = Call Buy Qty<br>
574
- $CS = Call Sell Qty<br>
575
- $PB = Put Buy Qty<br>
576
- $PS = Put Sell Qty
577
- </div>
578
- </div>
579
- </label>
580
- <div id="cfgSeries"></div>
581
- <button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary);" onclick="Config.addSeriesRow()">+ Add Line</button>
582
- </div>
583
  </div>
584
- </div>
585
- <div class="modal-foot">
586
- <button style="background:transparent; border:1px solid var(--border); color:var(--text-primary);" onclick="Config.close()">Cancel</button>
587
- <button class="primary" onclick="Config.save()">Save Configuration</button>
588
  </div>
589
  </div>
590
  </div>
591
 
592
  <script>
593
  const App = {
594
- state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
595
- defaults:[
596
- { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'left' },
597
- { formula: '$B', color: '#238636', label: 'Buy', axis: 'right' },
598
- { formula: '$S', color: '#da3633', label: 'Sell', axis: 'right' }
599
- ],
600
-
601
  async init() {
602
  await this.loadDates();
603
- const saved = localStorage.getItem('dc_pro_v3_final');
604
- if (saved) {
605
- try {
606
- const s = JSON.parse(saved);
607
- this.state = { ...this.state, ...s };
608
- if(this.state.layout.length !== this.state.columns) this.setLayout(this.state.columns);
609
- } catch(e) { this.initDefault(); }
610
- } else {
611
- this.initDefault();
612
- }
613
-
614
- const dSel = document.getElementById('globalDate');
615
- if(this.state.date && Array.from(dSel.options).some(o=>o.value===this.state.date)) dSel.value = this.state.date;
616
- else if(dSel.options.length) this.state.date = dSel.options[0].value;
617
 
618
- document.getElementById('globalMode').value = this.state.mode;
619
- this.updateModeUI();
620
  this.renderGrid();
621
- this.startLoop();
622
- },
623
-
624
- initDefault() {
625
- this.state.columns = 3;
626
- this.state.layout = [[], [],[]];
627
- this.state.refreshRate = 15;
628
- for(let i=0; i<3; i++) {
629
- this.addChartInternal(i);
630
- this.addChartInternal(i);
631
- }
632
  },
633
 
634
  async loadDates() {
635
- try {
636
- const res = await fetch('/api/dates');
637
- const dates = await res.json();
638
- document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
639
- if(!this.state.date && dates.length) this.state.date = dates[0];
640
- } catch(e) {}
641
- },
642
-
643
- addChartInternal(colIdx) {
644
- const id = 'c-' + Math.random().toString(36).substr(2, 6);
645
- this.state.layout[colIdx].push(id);
646
- this.state.charts[id] = { config: {}, instance: null };
647
- return id;
648
  },
649
 
650
- save() {
651
- const cleanCharts = {};
652
- for(let id in this.state.charts) cleanCharts[id] = { config: this.state.charts[id].config };
653
-
654
- localStorage.setItem('dc_pro_v3_final', JSON.stringify({
655
- date: this.state.date,
656
- mode: this.state.mode,
657
- columns: this.state.columns,
658
- layout: this.state.layout,
659
- refreshRate: this.state.refreshRate,
660
- charts: cleanCharts
661
- }));
662
  },
663
 
664
- reset() {
665
- if(confirm("Reset Workspace?")) {
666
- localStorage.removeItem('dc_pro_v3_final');
667
- location.reload();
668
- }
669
  },
670
 
671
  setLayout(n) {
672
- const all = this.state.layout.flat();
673
  this.state.columns = n;
674
- this.state.layout = Array.from({length: n}, () =>[]);
675
- all.forEach((id, i) => this.state.layout[i % n].push(id));
676
  this.renderGrid();
677
  this.save();
678
  },
679
 
680
  renderGrid() {
681
- document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i+1) === this.state.columns));
682
-
683
- const container = document.getElementById('gridRoot');
684
- container.innerHTML = '';
685
 
686
- this.state.layout.forEach((ids, colIdx) => {
687
  const col = document.createElement('div');
688
  col.className = 'column';
689
- const count = ids.length;
690
- const viewH = window.innerHeight - 52;
691
-
692
  ids.forEach(id => {
693
- if(this.state.charts[id] && this.state.charts[id].instance) {
694
- this.state.charts[id].instance.destroy();
695
- this.state.charts[id].instance = null;
696
- }
697
-
698
- const card = this.createCardDOM(id);
699
- if (count > 0 && count <= 3) card.style.height = `calc(${viewH / count}px - 1px)`;
700
- else card.style.height = '300px';
701
-
702
  col.appendChild(card);
703
- setTimeout(() => ChartLogic.plot(id), 0);
704
  });
705
 
706
- const btn = document.createElement('button');
707
- btn.className = 'add-chart-btn';
708
- btn.innerText = '+ Add Chart';
709
- btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
710
- col.appendChild(btn);
711
- container.appendChild(col);
712
- });
713
  },
714
 
715
- createCardDOM(id) {
716
  const div = document.createElement('div');
717
  div.className = 'chart-card';
718
- div.id = `card-${id}`;
719
-
720
- const cfg = this.state.charts[id] ? this.state.charts[id].config : {};
721
- const isCfg = cfg && cfg.root;
722
-
723
- let displayInst = cfg.instrument;
724
- if(cfg.mode === 'advanced') {
725
- displayInst = `ADV: ATM ±${cfg.atm_range}`;
726
- }
727
-
728
  div.innerHTML = `
729
  <div class="card-header">
730
- <div class="card-info">
731
- <div class="status-dot ${isCfg ? (this.state.mode==='LIVE'?'live':'hist') : ''}" id="dot-${id}"></div>
732
- <span>${isCfg ? cfg.root : 'New Chart'}</span>
733
- ${isCfg ? `<span class="inst-label">${displayInst}</span> <span class="card-meta">${cfg.expiry}</span>` : ''}
734
  </div>
735
- <div class="card-tools">
736
- <button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
737
- <button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
738
  </div>
739
  </div>
740
  <div class="card-body" id="body-${id}">
741
- ${!isCfg ? `
742
- <div class="empty-state">
743
- <div style="font-size:24px; opacity:0.3;">📊</div>
744
- <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>
745
- </div>
746
- ` : `<canvas id="canvas-${id}"></canvas>`}
747
  </div>
748
  `;
749
  return div;
750
  },
751
 
752
- removeChart(id) {
753
- if(!confirm("Delete?")) return;
754
- this.state.layout = this.state.layout.map(c => c.filter(x => x !== id));
755
- if(this.state.charts[id].instance) this.state.charts[id].instance.destroy();
756
- delete this.state.charts[id];
757
- this.renderGrid();
758
- this.save();
759
- },
760
-
761
- onDateChange() {
762
- this.state.date = document.getElementById('globalDate').value;
763
- const today = new Date().toISOString().split('T')[0];
764
- if(this.state.date === today && this.state.mode !== 'LIVE') {
765
- this.state.mode = 'LIVE';
766
- document.getElementById('globalMode').value = 'LIVE';
767
- this.updateModeUI();
768
- }
769
- this.save();
770
- this.refreshAll();
771
- },
772
-
773
- onModeChange() {
774
- this.state.mode = document.getElementById('globalMode').value;
775
- this.updateModeUI();
776
- this.save();
777
- this.refreshAll();
778
- },
779
 
780
- updateModeUI() {
781
- document.getElementById('modeDot').className = `status-dot ${this.state.mode==='LIVE'?'live':'hist'}`;
782
- },
783
-
784
- refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
785
-
786
- startLoop() {
787
- const loop = () => {
788
- if(this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
789
- const rate = (this.state.refreshRate || 15) * 1000;
790
- setTimeout(loop, rate);
791
- };
792
- loop();
793
- }
794
- };
795
-
796
- const ChartLogic = {
797
  async plot(id, isUpdate=false) {
798
- const chart = App.state.charts[id];
799
- if(!chart || !chart.config.root) return;
800
-
801
  const cfg = chart.config;
802
- const dot = document.getElementById(`dot-${id}`);
803
- if(isUpdate && dot) dot.classList.add('retry');
804
-
805
- let endTime = cfg.end;
806
- if (App.state.mode === 'LIVE') {
807
- const now = new Date();
808
- endTime = now.getHours().toString().padStart(2,'0') + ":" + now.getMinutes().toString().padStart(2,'0');
809
- }
810
 
811
  try {
812
  const res = await fetch('/api/fetch_series', {
813
  method: 'POST',
814
  headers: {'Content-Type': 'application/json'},
815
  body: JSON.stringify({
816
- date: App.state.date, root: cfg.root, expiry: cfg.expiry,
817
- instrument: cfg.instrument || "", timeframe: cfg.timeframe,
818
- start_time: cfg.start, end_time: endTime,
819
- adv_mode: cfg.mode === 'advanced',
820
- atm_range: cfg.atm_range || 5
821
  })
822
  });
823
  const data = await res.json();
 
824
 
825
- if(data.error || !data.labels || !data.labels.length) {
826
- if(App.state.mode !== 'LIVE') {
827
- const b = document.getElementById(`body-${id}`);
828
- if(b) b.innerHTML = '<div class="empty-state" style="opacity:0.5">No Data</div>';
829
- }
830
- if(dot) dot.classList.remove('retry');
831
- return;
832
- }
833
-
834
- const body = document.getElementById(`body-${id}`);
835
- if(body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
836
-
837
- if(dot) dot.className = `status-dot ${App.state.mode==='LIVE'?'live':'hist'}`;
838
 
839
  const datasets = cfg.series.map(s => {
840
  const vals = data.labels.map((_, i) => {
841
- let f = s.formula
842
- .replace(/\$OI/g, data.OI ? (data.OI[i]||0) : 0)
843
- .replace(/\$CB/g, data.CB ? (data.CB[i]||0) : 0)
844
- .replace(/\$CS/g, data.CS ? (data.CS[i]||0) : 0)
845
- .replace(/\$PB/g, data.PB ? (data.PB[i]||0) : 0)
846
- .replace(/\$PS/g, data.PS ? (data.PS[i]||0) : 0)
847
- .replace(/\$P/g, data.P ? (data.P[i]||0) : 0)
848
- .replace(/\$V/g, data.V ? (data.V[i]||0) : 0)
849
- .replace(/\$B/g, data.B ? (data.B[i]||0) : 0)
850
- .replace(/\$S/g, data.S ? (data.S[i]||0) : 0);
851
- try { return new Function('return '+f)(); } catch{ return null; }
852
  });
853
  return {
854
  label: s.label, data: vals, borderColor: s.color, backgroundColor: s.color,
855
- borderWidth: 1.5, pointRadius: 0, hitRadius: 10, tension: 0.1, yAxisID: s.axis==='left'?'y':'y1'
856
  };
857
  });
858
 
859
- this.render(id, data.labels, datasets);
860
-
861
- } catch(e) { console.error(e); }
862
- },
863
-
864
- render(id, labels, datasets) {
865
- const ctx = document.getElementById(`canvas-${id}`);
866
- if(!ctx) return;
867
-
868
- if(App.state.charts[id].instance && App.state.charts[id].instance.ctx.canvas !== ctx) {
869
- App.state.charts[id].instance.destroy();
870
- App.state.charts[id].instance = null;
871
- }
872
-
873
- if(App.state.charts[id].instance) {
874
- App.state.charts[id].instance.data.labels = labels;
875
- App.state.charts[id].instance.data.datasets = datasets;
876
- App.state.charts[id].instance.update('none');
877
- } else {
878
- const hasRight = datasets.some(d => d.yAxisID === 'y1');
879
- App.state.charts[id].instance = new Chart(ctx.getContext('2d'), {
880
- type: 'line',
881
- data: { labels, datasets },
882
- options: {
883
- responsive: true, maintainAspectRatio: false, animation: false,
884
- interaction: { mode: 'index', intersect: false },
885
- plugins: {
886
- legend: { display: false },
887
- tooltip: { backgroundColor: 'rgba(22, 27, 34, 0.95)', titleColor:'#e6edf3', bodyColor:'#e6edf3', borderColor: '#30363d', borderWidth: 1 }
888
- },
889
- scales: {
890
- x: { grid:{color:'#30363d', tickLength:0}, ticks:{color:'#8b949e', maxTicksLimit:8, font:{size:10}} },
891
- y: { type:'linear', display:true, position:'left', grid:{color:'#30363d'}, ticks:{color:'#8b949e', font:{size:10}} },
892
- y1: { type:'linear', display:hasRight, position:'right', grid:{display:false}, ticks:{color:'#8b949e', font:{size:10}} }
893
  }
894
- }
895
- });
896
- }
897
  }
898
  };
899
 
900
  const Config = {
901
- tid: null, isGlobal: false,
 
902
  async open(id) {
903
- this.tid = id; this.isGlobal = false;
904
- document.getElementById('configModal').style.display = 'flex';
905
- document.getElementById('cfgTitle').innerText = 'Configure Chart';
906
- document.getElementById('cfgLoading').style.display = 'block';
907
- document.getElementById('cfgContent').style.display = 'none';
908
-
909
- document.getElementById('cfgGlobalOnly').style.display = 'none';
910
- document.getElementById('cfgChartOnly').style.display = 'block';
911
-
912
- const rSel = document.getElementById('cfgRoot');
913
- const res = await fetch(`/api/roots?date=${App.state.date}`);
914
- const roots = await res.json();
915
- rSel.innerHTML = `<option value="">-- Select --</option>` + roots.map(r=>`<option>${r}</option>`).join('');
916
-
917
  const cfg = App.state.charts[id].config;
 
 
 
 
 
 
 
 
 
 
918
 
919
- // Mode Setting
920
- document.getElementById('cfgMode').value = cfg.mode || 'normal';
921
- document.getElementById('cfgAtm').value = cfg.atm_range || 5;
922
- this.onChartModeChange(false); // Update UI visibility, but don't reset series formulas
923
-
924
  document.getElementById('cfgTF').value = cfg.timeframe || '1min';
925
- document.getElementById('cfgStart').value = cfg.start || '09:15';
926
- document.getElementById('cfgEnd').value = cfg.end || '15:30';
927
- this.renderSeries(cfg.series || App.defaults);
928
-
929
- if(cfg.root) { rSel.value = cfg.root; await this.onRootChange(cfg.expiry, cfg.instrument); }
930
- document.getElementById('cfgLoading').style.display = 'none';
931
- document.getElementById('cfgContent').style.display = 'block';
932
  },
933
 
934
- openDefaults() {
935
- this.isGlobal = true;
936
- document.getElementById('configModal').style.display = 'flex';
937
- document.getElementById('cfgTitle').innerText = 'Global Settings';
938
- document.getElementById('cfgLoading').style.display = 'none';
939
- document.getElementById('cfgContent').style.display = 'block';
940
 
941
- document.getElementById('cfgGlobalOnly').style.display = 'block';
942
- document.getElementById('cfgChartOnly').style.display = 'none';
943
- document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
944
-
945
- this.renderSeries(App.defaults);
946
- },
947
-
948
- close() { document.getElementById('configModal').style.display = 'none'; },
949
-
950
- onChartModeChange(resetSeries = true) {
951
- const mode = document.getElementById('cfgMode').value;
952
- if(mode === 'advanced') {
953
- document.getElementById('instBox').style.display = 'none';
954
- document.getElementById('atmBox').style.display = 'block';
955
- if(resetSeries) {
956
- // Default formula mapping as requested by user
957
- this.renderSeries([
958
- { formula: '$CB + $PS', color: '#238636', label: 'Buyers', axis: 'left' },
959
- { formula: '$CS + $PB', color: '#da3633', label: 'Sellers', axis: 'left' },
960
- { formula: '$P', color: '#2f81f7', label: 'Price', axis: 'right' }
961
- ]);
962
- }
963
- } else {
964
- document.getElementById('instBox').style.display = 'block';
965
- document.getElementById('atmBox').style.display = 'none';
966
- if(resetSeries) {
967
- this.renderSeries(App.defaults);
968
- }
969
  }
970
  },
971
 
972
  async onRootChange(preExp, preInst) {
973
  const root = document.getElementById('cfgRoot').value;
974
- const eSel = document.getElementById('cfgExp');
975
- eSel.innerHTML = '<option>Loading...</option>';
976
- const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
977
- const exps = await res.json();
978
- eSel.innerHTML = `<option value="">-- Select --</option>` + exps.map(e=>`<option>${e}</option>`).join('');
979
 
980
- if(preExp && exps.includes(preExp)) { eSel.value = preExp; await this.onExpChange(preInst); }
981
- else {
982
- const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r=>r.json());
983
- if(auto.current) { eSel.value = auto.current.expiry; await this.onExpChange(auto.current.instrument); }
984
- }
985
  },
986
 
987
- async onExpChange(preInst) {
988
- const root = document.getElementById('cfgRoot').value;
989
  const exp = document.getElementById('cfgExp').value;
990
  const iSel = document.getElementById('cfgInst');
991
- iSel.innerHTML = '<option>Loading...</option>';
992
-
993
- const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
994
- const insts = await res.json();
995
-
996
- // Split instruments neatly for Normal Mode
997
- const futEq = insts.filter(i => !i.endswith('CE') && !i.endswith('PE'));
998
- const opts = insts.filter(i => i.endswith('CE') || i.endswith('PE'));
999
-
1000
- let html = '<option value="">-- Select --</option>';
1001
- if(futEq.length) {
1002
- html += '<optgroup label="Futures & Equity">';
1003
- html += futEq.map(i => `<option>${i}</option>`).join('');
1004
- html += '</optgroup>';
1005
- }
1006
- if(opts.length) {
1007
- html += '<optgroup label="Options">';
1008
- html += opts.map(i => `<option>${i}</option>`).join('');
1009
- html += '</optgroup>';
1010
- }
1011
-
1012
- iSel.innerHTML = html;
1013
- if(preInst && insts.includes(preInst)) iSel.value = preInst;
1014
  },
1015
 
1016
  renderSeries(list) {
1017
- const c = document.getElementById('cfgSeries'); c.innerHTML = '';
1018
- list.forEach(s => {
1019
- const r = document.createElement('div'); r.className = 'series-row';
1020
- r.innerHTML = `
1021
- <input type="color" value="${s.color}" class="color-input s-color">
1022
- <input type="text" value="${s.formula}" class="s-form" placeholder="$P">
1023
- <input type="text" value="${s.label}" class="s-lbl" placeholder="Label">
1024
- <select class="s-axis"><option value="left" ${s.axis==='left'?'selected':''}>L</option><option value="right" ${s.axis==='right'?'selected':''}>R</option></select>
1025
- <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()">✕</button>
1026
- `;
1027
- c.appendChild(r);
1028
- });
1029
  },
1030
 
1031
- addSeriesRow() {
1032
- const r = document.createElement('div'); r.className = 'series-row';
1033
- r.innerHTML = `
1034
- <input type="color" value="#ffffff" class="color-input s-color">
1035
- <input type="text" value="$P" class="s-form">
1036
- <input type="text" value="New" class="s-lbl">
1037
- <select class="s-axis"><option value="left">L</option><option value="right">R</option></select>
1038
- <button class="tool-btn" style="color:var(--danger)" onclick="this.parentElement.remove()"></button>
 
1039
  `;
1040
- document.getElementById('cfgSeries').appendChild(r);
1041
  },
1042
 
1043
  save() {
1044
- const rows = document.querySelectorAll('#cfgSeries .series-row');
1045
- const series = Array.from(rows).map(r => ({
1046
- color: r.querySelector('.s-color').value, formula: r.querySelector('.s-form').value,
1047
- label: r.querySelector('.s-lbl').value, axis: r.querySelector('.s-axis').value
 
1048
  }));
1049
 
1050
- if(this.isGlobal) {
1051
- App.defaults = series;
1052
- App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
1053
- App.save();
1054
- }
1055
- else {
1056
- const root = document.getElementById('cfgRoot').value;
1057
- const exp = document.getElementById('cfgExp').value;
1058
- const mode = document.getElementById('cfgMode').value;
1059
- const inst = document.getElementById('cfgInst').value;
1060
- const atm = parseInt(document.getElementById('cfgAtm').value) || 5;
1061
-
1062
- if(!root || !exp || (mode === 'normal' && !inst)) return alert("Incomplete Details");
1063
-
1064
- App.state.charts[this.tid].config = {
1065
- root, expiry: exp, instrument: inst,
1066
- mode: mode, atm_range: atm,
1067
- timeframe: document.getElementById('cfgTF').value,
1068
- start: document.getElementById('cfgStart').value,
1069
- end: document.getElementById('cfgEnd').value,
1070
- series
1071
- };
1072
- App.save();
1073
- App.renderGrid();
1074
- ChartLogic.plot(this.tid);
1075
- }
1076
  this.close();
1077
- }
 
1078
  };
1079
 
1080
  window.onload = () => App.init();
 
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
  # ==========================================
 
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)
 
35
  return r.json()
36
  except Exception as e:
37
  print(f"DB Error ({endpoint}): {e}")
38
+ return []
39
 
40
  # ==========================================
41
  # 3. API ROUTES - METADATA
 
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
  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
  <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();