topsecrettraders commited on
Commit
63d9609
·
verified ·
1 Parent(s): f65049f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +702 -504
app.py CHANGED
@@ -2,14 +2,17 @@ import pandas as pd
2
  import requests
3
  import uvicorn
4
  import json
 
5
  from datetime import datetime, timedelta
6
  from fastapi import FastAPI, Body
7
  from fastapi.responses import HTMLResponse, JSONResponse
8
- from typing import List, Dict, Any
 
9
 
10
  # ==========================================
11
- # 1. CONFIGURATION
12
  # ==========================================
 
13
  SB_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
14
  SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
15
 
@@ -23,637 +26,832 @@ HEADERS = {
23
  app = FastAPI()
24
 
25
  # ==========================================
26
- # 2. HELPER FUNCTIONS
27
  # ==========================================
28
- def sb_get(endpoint: str, params: dict = None):
 
29
  try:
30
  url = f"{SB_URL}/rest/v1{endpoint}"
31
- r = requests.get(url, headers=HEADERS, params=params)
32
- r.raise_for_status()
 
 
 
 
 
 
 
33
  return r.json()
34
  except Exception as e:
35
- print(f"DB Error: {e}")
36
  return []
37
 
38
- # ==========================================
39
- # 3. API ROUTES
40
- # ==========================================
 
 
 
 
 
 
 
 
 
41
  @app.get("/")
42
  def home():
43
- return HTMLResponse(HTML_TEMPLATE)
44
 
45
  @app.get("/api/dates")
46
  def get_dates():
47
- # Get unique dates
48
- data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "2000"})
49
- dates = set(row['minute'].split('T')[0] for row in data)
50
- return JSONResponse(sorted(list(dates), reverse=True))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  @app.get("/api/roots")
53
  def get_roots(date: str):
54
- start, end = f"{date}T00:00:00", f"{date}T23:59:59"
55
- data = sb_get(f"/history_buckets_upstox?select=root&minute=gte.{start}&minute=lte.{end}")
56
- roots = sorted(list(set(row['root'] for row in data)))
57
- return JSONResponse(roots)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  @app.get("/api/instruments")
60
- def get_instruments(date: str, root: str):
61
- """Fetch all available instruments (Spot, Fut, Options) for a specific root on a date"""
62
- start, end = f"{date}T00:00:00", f"{date}T23:59:59"
 
 
 
 
 
 
63
 
64
- # 1. Get Expiries to find buckets
65
- exp_data = sb_get(f"/history_buckets_upstox?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
66
- expiries = set(row['expiry'] for row in exp_data)
67
 
68
- all_insts = set()
69
- # 2. Sample data from each expiry bucket
70
- for exp in expiries:
71
- res = sb_get(f"/history_buckets_upstox?select=data&root=eq.{root}&expiry=eq.{exp}&minute=gte.{start}&minute=lte.{end}&limit=1")
72
- if res and len(res) > 0:
73
- keys = res[0]['data'].keys()
74
- all_insts.update(keys)
75
-
76
- return JSONResponse(sorted(list(all_insts)))
77
-
78
- @app.post("/api/fetch_bulk")
79
- def fetch_bulk_data(
80
- date: str = Body(...),
81
- requests_list: List[Dict[str, Any]] = Body(...), # List of { root: "NIFTY", instruments: ["NIFTY", "NIFTYFUT"] }
82
- timeframe: str = Body(...),
83
- start_time: str = Body(...),
84
- end_time: str = Body(...)
85
- ):
86
  """
87
- Fetches data for multiple roots in one go.
88
- requests_list: [ {root: 'NIFTY', instruments: [...]}, {root: 'BANKNIFTY', instruments: [...]} ]
 
 
 
 
 
 
 
 
 
 
89
  """
90
- t_start = f"{date}T{start_time}:00"
91
- t_end = f"{date}T{end_time}:59"
 
 
 
92
 
93
- combined_raw_map = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- # Iterate over roots to fetch data (Supabase doesn't support complex OR filtering easily on GET, so we loop roots)
96
- # Since typically user plots 1-3 roots, this is fast enough.
97
- for req in requests_list:
98
- root = req['root']
99
- target_insts = req['instruments']
100
-
101
- url = f"/history_buckets_upstox?select=minute,data&root=eq.{root}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
102
- rows = sb_get(url)
 
 
 
 
103
 
104
- for row in rows:
105
- try:
106
- base_ts = datetime.fromisoformat(row['minute'])
107
- bucket_data = row['data']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
- for inst in target_insts:
110
- if inst in bucket_data:
111
- if inst not in combined_raw_map: combined_raw_map[inst] = []
112
-
113
- d = bucket_data[inst]
114
- # 6 slots logic
115
- p, b = d.get('p', []), d.get('b', [])
116
- s, v = d.get('s', []), d.get('v', [])
117
- o = d.get('o', [])
118
-
119
- for i in range(6):
120
- if i >= len(p) or p[i] is None: continue
121
-
122
- combined_raw_map[inst].append({
123
- "ts": base_ts + timedelta(seconds=i*10),
124
- "price": float(p[i]),
125
- "buy_qty": float(b[i]) if i < len(b) and b[i] else 0.0,
126
- "sell_qty": float(s[i]) if i < len(s) and s[i] else 0.0,
127
- "volume": float(v[i]) if i < len(v) and v[i] else 0.0,
128
- "oi": float(o[i]) if i < len(o) and o[i] else 0.0
129
- })
130
- except: continue
131
-
132
- # Resample
133
- tf_map = {"1min": "1T", "3min": "3T", "5min": "5T", "15min": "15T", "30min": "30T", "1hour": "1H"}
134
- panda_tf = tf_map.get(timeframe, "1T")
135
 
136
- response_data = {}
 
137
 
138
- for inst, records in combined_raw_map.items():
139
- if not records: continue
140
- df = pd.DataFrame(records)
141
- df.set_index('ts', inplace=True)
142
- res = df.resample(panda_tf).median().dropna()
143
-
144
- response_data[inst] = {
145
- "labels": res.index.strftime('%H:%M').tolist(),
146
- "P": res['price'].tolist(),
147
- "B": res['buy_qty'].tolist(),
148
- "S": res['sell_qty'].tolist(),
149
- "V": res['volume'].tolist(),
150
- "OI": res['oi'].tolist()
151
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- return JSONResponse(response_data)
154
 
155
  # ==========================================
156
- # 4. FRONTEND
157
  # ==========================================
158
 
159
- HTML_TEMPLATE = """
160
  <!DOCTYPE html>
161
  <html lang="en">
162
  <head>
163
  <meta charset="UTF-8">
164
- <title>DepthChain PRO V2</title>
165
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Manrope:wght@400;600;700;800&display=swap" rel="stylesheet">
 
167
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
168
  <style>
169
- :root {
170
- --bg: #f3f4f6; --panel: #ffffff; --border: #e5e7eb;
171
- --text-main: #111827; --text-sub: #6b7280;
172
- --accent: #2563eb; --accent-hover: #1d4ed8;
173
- --danger: #ef4444; --success: #22c55e;
174
- --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
 
 
 
 
175
  }
176
  * { box-sizing: border-box; }
177
- body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Manrope', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
178
 
179
- /* SCROLLBAR */
180
- ::-webkit-scrollbar { width: 8px; height: 8px; }
181
- ::-webkit-scrollbar-track { background: transparent; }
182
- ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
183
- ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
184
-
185
  /* HEADER */
186
- .header { height: 65px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 25px; justify-content: space-between; flex-shrink: 0; z-index: 20; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
187
- .logo { font-family: 'JetBrains Mono'; font-weight: 800; font-size: 20px; letter-spacing: -1px; color: var(--text-main); }
188
- .logo span { color: var(--accent); }
 
 
 
 
 
 
 
189
 
190
- .controls { display: flex; gap: 15px; align-items: center; }
191
- .c-item { display: flex; flex-direction: column; gap: 4px; }
192
- .lbl { font-size: 10px; font-weight: 700; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; }
193
- .inp { background: #fff; border: 1px solid var(--border); color: var(--text-main); padding: 8px 12px; border-radius: 6px; font-family: 'JetBrains Mono'; font-size: 13px; outline: none; transition: 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
194
- .inp:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
195
 
196
- .btn { background: var(--accent); color: white; border: none; padding: 10px 24px; border-radius: 6px; font-weight: 700; cursor: pointer; font-size: 13px; transition: 0.2s; display: flex; align-items: center; gap: 6px; }
197
- .btn:hover { background: var(--accent-hover); transform: translateY(-1px); }
198
- .btn.outline { background: white; border: 1px solid var(--border); color: var(--text-sub); }
199
- .btn.outline:hover { border-color: var(--accent); color: var(--accent); }
200
-
201
- /* MAIN GRID */
202
- .grid-area { flex: 1; padding: 25px; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); gap: 20px; align-content: start; }
203
 
204
  /* CHART CARD */
205
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; display: flex; flex-direction: column; height: 500px; position: relative; overflow: hidden; box-shadow: var(--shadow); transition: transform 0.2s; }
206
- .card:hover { border-color: #cbd5e1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
- .card-head { padding: 12px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: #f9fafb; }
209
- .card-title { font-size: 13px; font-weight: 800; color: var(--text-main); display: flex; align-items: center; gap: 10px; }
210
- .root-tag { background: #e0e7ff; color: #3730a3; padding: 2px 8px; border-radius: 4px; font-size: 11px; }
211
 
212
- .card-actions { display: flex; gap: 10px; }
213
- .icon-btn { cursor: pointer; color: var(--text-sub); font-size: 16px; padding: 4px; border-radius: 4px; transition:0.2s; }
214
- .icon-btn:hover { background: #e5e7eb; color: var(--text-main); }
215
- .icon-btn.del:hover { background: #fee2e2; color: var(--danger); }
216
-
217
- .card-body { flex: 1; position: relative; padding: 15px; background: #fff; }
218
-
219
  /* MODAL */
220
- .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 100; display: none; justify-content: center; align-items: center; }
221
- .modal { background: #fff; width: 900px; max-width: 95%; height: 85vh; border-radius: 16px; display: flex; flex-direction: column; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); animation: popIn 0.2s ease-out; }
 
 
 
 
 
 
 
 
 
222
  @keyframes popIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
223
 
224
- .modal-header { padding: 20px 30px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
225
- .modal-title { font-size: 18px; font-weight: 800; color: var(--text-main); }
226
-
227
- .modal-body { flex: 1; padding: 30px; overflow-y: auto; background: #f9fafb; }
228
- .modal-footer { padding: 20px 30px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: #fff; border-bottom-left-radius: 16px; border-bottom-right-radius: 16px; }
229
-
230
- /* Config Forms */
231
- .cfg-section { background: #fff; padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 20px; }
232
- .cfg-label { font-size: 12px; font-weight: 700; color: var(--text-sub); margin-bottom: 8px; display: block; }
233
 
234
- .series-list { display: flex; flex-direction: column; gap: 10px; }
235
- .series-row { display: grid; grid-template-columns: 40px 1fr 2fr 100px 100px 40px; gap: 15px; align-items: center; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: #fff; }
236
- .series-row:hover { border-color: var(--accent); }
 
237
 
238
- .color-inp { width: 100%; height: 35px; border: none; padding: 0; cursor: pointer; background: none; }
239
- .modal-inp { width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 13px; font-family:'JetBrains Mono'; }
 
 
240
 
241
- .add-line-btn { width: 100%; padding: 12px; border: 2px dashed #d1d5db; border-radius: 8px; color: var(--text-sub); font-weight: 700; cursor: pointer; text-align: center; margin-top: 10px; transition: 0.2s; }
242
- .add-line-btn:hover { border-color: var(--accent); color: var(--accent); background: #eff6ff; }
243
 
244
  /* Loader */
245
- .loader-ov { position: fixed; inset: 0; background: rgba(255,255,255,0.8); z-index: 200; display: none; justify-content: center; align-items: center; font-weight: 800; color: var(--text-main); font-size: 14px; gap: 10px; }
246
- .spinner { width: 20px; height: 20px; border: 3px solid #e5e7eb; border-top-color: var(--accent); border-radius: 50%; animation: spin 1s infinite linear; }
247
- @keyframes spin { to { transform: rotate(360deg); } }
248
-
249
  </style>
250
  </head>
251
  <body>
252
 
253
- <div class="loader-ov" id="loader"><div class="spinner"></div> PROCESSING DATA...</div>
254
 
255
- <!-- HEADER -->
256
- <div class="header">
257
- <div class="logo">DepthChain <span>PRO</span></div>
258
- <div class="controls">
259
- <div class="c-item">
260
- <span class="lbl">1. Date</span>
261
- <select id="gDate" class="inp" onchange="loadGlobal()" style="width:140px;"></select>
262
- </div>
263
- <div class="c-item">
264
- <span class="lbl">2. Timeframe</span>
265
- <select id="gTF" class="inp" style="width:100px;">
266
- <option value="1min">1 Min</option>
267
- <option value="3min">3 Min</option>
268
- <option value="5min">5 Min</option>
269
- <option value="15min">15 Min</option>
270
- <option value="30min">30 Min</option>
271
- <option value="1hour">1 Hour</option>
272
- </select>
273
- </div>
274
- <div class="c-item">
275
- <span class="lbl">Start</span>
276
- <input type="time" id="gStart" class="inp" value="09:15" style="width:100px;">
277
- </div>
278
- <div class="c-item">
279
- <span class="lbl">End</span>
280
- <input type="time" id="gEnd" class="inp" value="15:30" style="width:100px;">
281
- </div>
282
- <div style="width:1px; height:30px; background:#e5e7eb; margin:0 10px;"></div>
283
- <button class="btn" onclick="plotAll()">PLOT ALL CHARTS</button>
284
- <button class="btn outline" onclick="addChart()">+ CHART</button>
285
  </div>
 
 
 
 
 
 
 
286
  </div>
287
 
288
- <!-- GRID -->
289
- <div class="grid-area" id="grid"></div>
 
 
 
 
 
 
290
 
291
- <!-- CONFIG MODAL -->
292
- <div class="modal-overlay" id="cfgModal">
293
  <div class="modal">
294
  <div class="modal-header">
295
- <div class="modal-title">CONFIGURE CHART <span id="cfgId" style="color:var(--accent)"></span></div>
296
- <div class="icon-btn" onclick="closeModal()">✕</div>
297
  </div>
298
  <div class="modal-body">
 
299
 
300
- <!-- Root Selection -->
301
- <div class="cfg-section">
302
- <span class="cfg-label">1. SELECT ROOT SYMBOL FOR THIS CHART</span>
303
- <select id="cfgRoot" class="modal-inp" onchange="onRootChange()">
304
- <!-- Populated dynamically -->
305
- </select>
 
 
 
 
306
  </div>
307
 
308
- <!-- Series Config -->
309
- <div class="cfg-section">
310
- <span class="cfg-label">2. DATA SERIES CONFIGURATION</span>
311
- <div style="display:grid; grid-template-columns: 40px 1fr 2fr 100px 100px 40px; gap:15px; padding:0 10px; margin-bottom:5px; font-size:11px; font-weight:800; color:#9ca3af;">
312
- <div>COLOR</div>
313
- <div>VARIABLE</div>
314
- <div>INSTRUMENT</div>
315
- <div>AXIS</div>
316
- <div>LABEL</div>
317
- <div></div>
 
 
 
 
 
 
 
 
318
  </div>
319
- <div id="seriesContainer" class="series-list"></div>
320
- <div class="add-line-btn" onclick="addSeriesRow()">+ ADD ANOTHER LINE</div>
321
  </div>
322
 
 
 
 
 
 
 
 
 
 
 
 
323
  </div>
324
  <div class="modal-footer">
325
- <button class="btn outline" onclick="closeModal()">CANCEL</button>
326
- <button class="btn" onclick="saveConfig()">SAVE & PLOT</button>
327
  </div>
328
  </div>
329
  </div>
330
 
331
  <script>
332
- // --- STATE ---
333
- let AVAILABLE_ROOTS = [];
334
- let CURRENT_INSTRUMENTS = []; // Instruments for the currently edited chart's root
335
- let CHARTS = []; // { id, root, series: [{color, var, inst, axis, label}] }
336
- let DATA_CACHE = {};
337
- let CHART_INSTANCES = {};
338
- let EDIT_ID = null; // ID of chart currently being edited
339
-
340
- // --- DEFAULTS ---
341
- const DEFAULTS = [
342
- { var: '$P', label: 'Price', color: '#fbbf24', axis: 'left' },
343
- { var: '$B', label: 'Buy Qty', color: '#3b82f6', axis: 'right' },
344
- { var: '$S', label: 'Sell Qty', color: '#ef4444', axis: 'right' }
345
- ];
346
 
 
347
  window.onload = async () => {
348
- const r = await fetch('/api/dates');
349
- const d = await r.json();
350
- document.getElementById('gDate').innerHTML = d.map(x=>`<option>${x}</option>`).join('');
351
-
352
- if(d.length > 0) await loadGlobal();
353
-
354
- // Init with 2 empty charts
355
- addChart();
356
- addChart();
357
  };
358
 
359
- async function loadGlobal() {
360
- const d = document.getElementById('gDate').value;
361
- const res = await fetch(`/api/roots?date=${d}`);
362
- AVAILABLE_ROOTS = await res.json();
 
 
363
  }
364
 
365
- // --- CHART MANAGEMENT ---
366
- function addChart() {
367
- const id = Date.now();
368
- // Default to first available root
369
- const defRoot = AVAILABLE_ROOTS[0] || "NIFTY";
370
-
371
- CHARTS.push({
372
- id: id,
373
- root: defRoot,
374
- series: [
375
- { ...DEFAULTS[0], inst: defRoot }, // Price of Spot
376
- { ...DEFAULTS[1], inst: defRoot },
377
- { ...DEFAULTS[2], inst: defRoot }
378
- ]
379
- });
380
- renderGrid();
381
  }
382
 
383
- function removeChart(id) {
384
- if(confirm("Delete this chart?")) {
385
- CHARTS = CHARTS.filter(c => c.id !== id);
386
- renderGrid();
387
- }
 
 
 
388
  }
389
 
390
- function renderGrid() {
391
- const g = document.getElementById('grid');
392
- g.innerHTML = '';
393
- CHARTS.forEach((c, idx) => {
394
- const html = `
395
- <div class="card">
396
- <div class="card-head">
397
- <div class="card-title">
398
- <span>CHART ${idx+1}</span>
399
- <span class="root-tag">${c.root}</span>
400
- </div>
401
- <div class="card-actions">
402
- <span class="icon-btn" onclick="openConfig(${c.id})">⚙️ CONFIGURE</span>
403
- <span class="icon-btn del" onclick="removeChart(${c.id})">✕</span>
404
- </div>
405
  </div>
406
- <div class="card-body">
407
- <canvas id="cvs-${c.id}"></canvas>
 
 
408
  </div>
409
- </div>`;
410
- g.insertAdjacentHTML('beforeend', html);
411
-
412
- // If data exists, re-render canvas
413
- if(Object.keys(DATA_CACHE).length > 0) renderCanvas(c);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  }
416
 
417
- // --- CONFIG MODAL LOGIC ---
418
- async function openConfig(id) {
419
- EDIT_ID = id;
420
- const chart = CHARTS.find(c => c.id === id);
421
- document.getElementById('cfgId').innerText = id;
422
 
423
- // 1. Populate Roots
424
- const rootSel = document.getElementById('cfgRoot');
425
- rootSel.innerHTML = AVAILABLE_ROOTS.map(r => `<option ${r===chart.root?'selected':''}>${r}</option>`).join('');
 
426
 
427
- // 2. Fetch Instruments for this root
428
- await fetchInstrumentsForRoot(chart.root);
 
 
 
 
 
429
 
430
- // 3. Render Series
431
- renderSeriesRows(chart.series);
432
 
433
- document.getElementById('cfgModal').style.display = 'flex';
434
- }
435
-
436
- async function onRootChange() {
437
- const newRoot = document.getElementById('cfgRoot').value;
438
- await fetchInstrumentsForRoot(newRoot);
439
- // Refresh instrument dropdowns in existing rows
440
- // We preserve selection if possible, else default to root
441
- const rows = document.querySelectorAll('.series-row');
442
- rows.forEach(r => {
443
- const sel = r.querySelector('.inst-drop');
444
- const oldVal = sel.value;
445
- sel.innerHTML = CURRENT_INSTRUMENTS.map(i => `<option>${i}</option>`).join('');
446
- if(CURRENT_INSTRUMENTS.includes(oldVal)) sel.value = oldVal;
447
- else sel.value = newRoot;
448
- });
449
- }
450
 
451
- async function fetchInstrumentsForRoot(root) {
452
- const d = document.getElementById('gDate').value;
453
- const res = await fetch(`/api/instruments?date=${d}&root=${root}`);
454
- CURRENT_INSTRUMENTS = await res.json();
455
  }
456
 
457
- function renderSeriesRows(series) {
458
- const cont = document.getElementById('seriesContainer');
459
- cont.innerHTML = '';
460
-
461
- const instOptions = CURRENT_INSTRUMENTS.map(i => `<option>${i}</option>`).join('');
462
-
463
- series.forEach((s, idx) => {
464
- const row = `
465
- <div class="series-row" data-idx="${idx}">
466
- <input type="color" class="color-inp val-color" value="${s.color}">
467
- <select class="modal-inp val-var">
468
- <option value="$P" ${s.var==='$P'?'selected':''}>Price</option>
469
- <option value="$B" ${s.var==='$B'?'selected':''}>Buy Qty</option>
470
- <option value="$S" ${s.var==='$S'?'selected':''}>Sell Qty</option>
471
- <option value="$V" ${s.var==='$V'?'selected':''}>Volume</option>
472
- <option value="$OI" ${s.var==='$OI'?'selected':''}>OI</option>
473
- </select>
474
- <select class="modal-inp inst-drop val-inst">
475
- ${instOptions.replace(`>${s.inst}<`, ` selected>${s.inst}<`)}
476
- <!-- Fallback if s.inst not in list, pre-select if matched -->
477
- </select>
478
- <select class="modal-inp val-axis">
479
- <option value="left" ${s.axis==='left'?'selected':''}>Left Axis</option>
480
- <option value="right" ${s.axis==='right'?'selected':''}>Right Axis</option>
481
- </select>
482
- <input type="text" class="modal-inp val-label" value="${s.label}">
483
- <div class="icon-btn del" onclick="this.parentElement.remove()">✕</div>
484
- </div>`;
485
- cont.insertAdjacentHTML('beforeend', row);
486
- });
487
- }
488
 
489
- function addSeriesRow() {
490
- const cont = document.getElementById('seriesContainer');
491
- // Auto-select instrument from last row
492
- const lastRow = cont.lastElementChild;
493
- let defInst = CURRENT_INSTRUMENTS[0];
494
- if(lastRow) defInst = lastRow.querySelector('.val-inst').value;
495
-
496
- const row = `
497
- <div class="series-row">
498
- <input type="color" class="color-inp val-color" value="#000000">
499
- <select class="modal-inp val-var">
500
- <option value="$P">Price</option>
501
- <option value="$B" selected>Buy Qty</option>
502
- <option value="$S">Sell Qty</option>
503
- <option value="$V">Volume</option>
504
- <option value="$OI">OI</option>
505
- </select>
506
- <select class="modal-inp inst-drop val-inst">
507
- ${CURRENT_INSTRUMENTS.map(i => `<option ${i===defInst?'selected':''}>${i}</option>`).join('')}
508
- </select>
509
- <select class="modal-inp val-axis">
510
- <option value="left">Left Axis</option>
511
- <option value="right" selected>Right Axis</option>
512
- </select>
513
- <input type="text" class="modal-inp val-label" value="New Line">
514
- <div class="icon-btn del" onclick="this.parentElement.remove()">✕</div>
515
- </div>`;
516
- cont.insertAdjacentHTML('beforeend', row);
517
  }
518
 
519
- function closeModal() { document.getElementById('cfgModal').style.display = 'none'; }
 
 
 
 
520
 
521
- function saveConfig() {
522
- const chart = CHARTS.find(c => c.id === EDIT_ID);
523
- chart.root = document.getElementById('cfgRoot').value;
524
 
525
- const newSeries = [];
526
- document.querySelectorAll('.series-row').forEach(r => {
527
- newSeries.push({
528
- color: r.querySelector('.val-color').value,
529
- var: r.querySelector('.val-var').value,
530
- inst: r.querySelector('.val-inst').value,
531
- axis: r.querySelector('.val-axis').value,
532
- label: r.querySelector('.val-label').value,
533
- });
534
  });
535
- chart.series = newSeries;
536
- closeModal();
537
- renderGrid(); // Refresh UI
538
- plotAll(); // Fetch new data
539
  }
540
 
541
- // --- PLOTTING ENGINE ---
542
- async function plotAll() {
543
- const l = document.getElementById('loader');
544
- l.style.display = 'flex';
545
 
546
- // 1. Gather all unique instruments required across all charts
547
- // The backend `fetch_bulk` now takes {root, instruments[]} structure
548
 
549
- const requestMap = {}; // { "NIFTY": Set(insts), "BANKNIFTY": Set(insts) }
 
 
 
 
 
 
 
 
 
 
550
 
551
- CHARTS.forEach(c => {
552
- if(!requestMap[c.root]) requestMap[c.root] = new Set();
553
- c.series.forEach(s => requestMap[c.root].add(s.inst));
554
- });
555
 
556
- const requestsList = Object.keys(requestMap).map(root => ({
557
- root: root,
558
- instruments: Array.from(requestMap[root])
559
- }));
560
-
561
- if(requestsList.length === 0) { l.style.display='none'; return; }
562
 
563
- const payload = {
564
- date: document.getElementById('gDate').value,
565
- requests_list: requestsList,
566
- timeframe: document.getElementById('gTF').value,
567
- start_time: document.getElementById('gStart').value,
568
- end_time: document.getElementById('gEnd').value
569
- };
570
 
571
- try {
572
- const res = await fetch('/api/fetch_bulk', {
573
- method:'POST', headers:{'Content-Type':'application/json'},
574
- body: JSON.stringify(payload)
575
- });
576
- DATA_CACHE = await res.json();
 
 
 
 
 
 
 
 
 
 
577
 
578
- // Render all canvases
579
- CHARTS.forEach(c => renderCanvas(c));
 
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
- } catch(e) { console.error(e); alert("Failed to fetch data"); }
582
- l.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  }
584
 
585
- function renderCanvas(chart) {
586
- const ctx = document.getElementById(`cvs-${chart.id}`);
587
- if(!ctx) return;
588
 
589
- if(CHART_INSTANCES[chart.id]) CHART_INSTANCES[chart.id].destroy();
590
 
591
- const datasets = [];
592
- let labels = [];
593
-
594
- chart.series.forEach(s => {
595
- const raw = DATA_CACHE[s.inst];
596
- if(!raw) return;
 
 
597
 
598
- if(labels.length === 0) labels = raw.labels;
599
-
600
- let data = [];
601
- if(s.var === '$P') data = raw.P;
602
- if(s.var === '$B') data = raw.B;
603
- if(s.var === '$S') data = raw.S;
604
- if(s.var === '$V') data = raw.V;
605
- if(s.var === '$OI') data = raw.OI;
606
-
607
- datasets.push({
608
- label: s.label + ` (${s.inst})`,
609
- data: data,
610
- borderColor: s.color,
611
- backgroundColor: s.color,
612
- borderWidth: 2,
613
- pointRadius: 0,
614
- pointHoverRadius: 4,
615
- tension: 0.1,
616
- yAxisID: s.axis // Explicit mapping
617
  });
618
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
 
620
- // Determine if we need Right Axis
621
- const hasRight = datasets.some(d => d.yAxisID === 'right');
 
622
 
623
- CHART_INSTANCES[chart.id] = new Chart(ctx, {
624
- type: 'line',
625
- data: { labels, datasets },
626
- options: {
627
- responsive: true,
628
- maintainAspectRatio: false,
629
- interaction: { mode: 'index', intersect: false },
630
- scales: {
631
- x: {
632
- grid: { display:false },
633
- ticks: { maxTicksLimit: 10, color:'#6b7280' }
634
- },
635
- left: {
636
- type: 'linear', display: true, position: 'left',
637
- grid: { color: '#f3f4f6' },
638
- title: { display: false }
639
- },
640
- right: {
641
- type: 'linear', display: hasRight, position: 'right',
642
- grid: { display: false }
643
- }
644
- },
645
- plugins: {
646
- legend: { labels: { usePointStyle:true, font:{size:11} } },
647
- tooltip: {
648
- backgroundColor: 'rgba(255,255,255,0.9)',
649
- titleColor: '#000',
650
- bodyColor: '#000',
651
- borderColor: '#e5e7eb',
652
- borderWidth: 1
653
- }
654
- }
655
- }
656
- });
657
  }
658
 
659
  </script>
 
2
  import requests
3
  import uvicorn
4
  import json
5
+ import numpy as np
6
  from datetime import datetime, timedelta
7
  from fastapi import FastAPI, Body
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from typing import List, Dict, Any, Optional
11
 
12
  # ==========================================
13
+ # 1. CONFIGURATION (SUPABASE HISTORY DB)
14
  # ==========================================
15
+ # Using the HIST keys provided in upstox4.txt
16
  SB_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
17
  SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
18
 
 
26
  app = FastAPI()
27
 
28
  # ==========================================
29
+ # 2. BACKEND LOGIC
30
  # ==========================================
31
+
32
+ def fetch_supabase(endpoint: str, params: dict = {}):
33
  try:
34
  url = f"{SB_URL}/rest/v1{endpoint}"
35
+ # Convert params to query string
36
+ query = "&".join([f"{k}={v}" for k, v in params.items()])
37
+ if query:
38
+ url += f"?{query}"
39
+
40
+ r = requests.get(url, headers=HEADERS)
41
+ if r.status_code >= 400:
42
+ print(f"Error fetching {url}: {r.text}")
43
+ return []
44
  return r.json()
45
  except Exception as e:
46
+ print(f"Exception: {e}")
47
  return []
48
 
49
+ def process_slots(slot_array):
50
+ """
51
+ Takes a 6-element list [v1, v2, null, v3, ...].
52
+ Returns the median of non-null values.
53
+ """
54
+ if not isinstance(slot_array, list):
55
+ return np.nan
56
+ valid = [x for x in slot_array if x is not None]
57
+ if not valid:
58
+ return np.nan
59
+ return np.median(valid)
60
+
61
  @app.get("/")
62
  def home():
63
+ return HTMLResponse(html_content)
64
 
65
  @app.get("/api/dates")
66
  def get_dates():
67
+ # Get distinct dates (based on minute column)
68
+ # Since we can't do complex SQL distinct easily on timestamp::date in standard PostgREST without RPC,
69
+ # we will fetch the last 1000 records id, minute and extract unique dates in python.
70
+ # Optimization: Create a separate table for dates in production, but here we scan.
71
+
72
+ # We fetch only the 'minute' column, order by desc, limit 5000 (approx 3-4 days of data)
73
+ data = fetch_supabase("/history_buckets_upstox", {
74
+ "select": "minute",
75
+ "order": "minute.desc",
76
+ "limit": "5000"
77
+ })
78
+
79
+ dates = set()
80
+ for row in data:
81
+ if row.get('minute'):
82
+ dt_str = row['minute'].split('T')[0]
83
+ dates.add(dt_str)
84
+
85
+ return sorted(list(dates), reverse=True)
86
 
87
  @app.get("/api/roots")
88
  def get_roots(date: str):
89
+ # Fetch roots present for this date
90
+ # start_dt = date T 00:00:00, end_dt = date T 23:59:59
91
+ data = fetch_supabase("/history_buckets_upstox", {
92
+ "select": "root,exchange", # Assuming exchange might be implicit in root or logic
93
+ "minute": f"gte.{date}T00:00:00&minute=lte.{date}T23:59:59",
94
+ "limit": "2000" # Sample enough rows to get distinct roots
95
+ })
96
+
97
+ unique_roots = set()
98
+ for row in data:
99
+ r = row.get('root')
100
+ if r: unique_roots.add(r)
101
+
102
+ return sorted(list(unique_roots))
103
+
104
+ @app.get("/api/expiries")
105
+ def get_expiries(date: str, root: str):
106
+ data = fetch_supabase("/history_buckets_upstox", {
107
+ "select": "expiry",
108
+ "minute": f"gte.{date}T00:00:00&minute=lte.{date}T23:59:59",
109
+ "root": f"eq.{root}",
110
+ "limit": "1000"
111
+ })
112
+ exps = set()
113
+ for row in data:
114
+ exps.add(row['expiry'])
115
+
116
+ # Sort: Put 'MARKET' first, then dates
117
+ sl = sorted(list(exps))
118
+ if "MARKET" in sl:
119
+ sl.remove("MARKET")
120
+ sl.insert(0, "MARKET")
121
+ return sl
122
 
123
  @app.get("/api/instruments")
124
+ def get_instruments(date: str, root: str, expiry: str):
125
+ # Fetch one row to get the keys from the 'data' JSONB
126
+ data = fetch_supabase("/history_buckets_upstox", {
127
+ "select": "data",
128
+ "minute": f"gte.{date}T00:00:00&minute=lte.{date}T23:59:59",
129
+ "root": f"eq.{root}",
130
+ "expiry": f"eq.{expiry}",
131
+ "limit": "1"
132
+ })
133
 
134
+ if not data or not data[0].get('data'):
135
+ return []
 
136
 
137
+ # Keys in 'data' are the instrument symbols (e.g., NIFTY24JAN..., Nifty 50)
138
+ keys = list(data[0]['data'].keys())
139
+ return sorted(keys)
140
+
141
+ @app.post("/api/plot-data")
142
+ def get_plot_data(payload: dict = Body(...)):
 
 
 
 
 
 
 
 
 
 
 
 
143
  """
144
+ Payload: {
145
+ date: "2025-01-27",
146
+ root: "NSE:NIFTY",
147
+ expiry: "27JAN",
148
+ time_start: "09:15",
149
+ time_end: "15:30",
150
+ timeframe: "1min",
151
+ datasets: [
152
+ { instrument: "NIFTY24JAN...", formula: "b - s", label: "Net Flow" },
153
+ { instrument: "Nifty 50", formula: "p", label: "Spot Price" }
154
+ ]
155
+ }
156
  """
157
+ date = payload.get("date")
158
+ root = payload.get("root")
159
+ expiry = payload.get("expiry")
160
+ tf = payload.get("timeframe", "1min")
161
+ datasets_config = payload.get("datasets", [])
162
 
163
+ if not datasets_config:
164
+ return JSONResponse({"error": "No datasets configured"})
165
+
166
+ # 1. Fetch Data from Supabase
167
+ # We fetch all rows for this Day/Root/Expiry
168
+ # Filtering by time_start/end in DB is better for performance
169
+ t_start = f"{date}T{payload.get('time_start', '00:00')}:00"
170
+ t_end = f"{date}T{payload.get('time_end', '23:59')}:59"
171
+
172
+ db_rows = fetch_supabase("/history_buckets_upstox", {
173
+ "select": "minute,data",
174
+ "root": f"eq.{root}",
175
+ "expiry": f"eq.{expiry}",
176
+ "minute": f"gte.{t_start}&minute=lte.{t_end}",
177
+ "order": "minute.asc"
178
+ })
179
+
180
+ if not db_rows:
181
+ return JSONResponse({"labels": [], "datasets": []})
182
 
183
+ # 2. Convert to DataFrame
184
+ # Structure: timestamp | instr_key | variable | value
185
+ # We need to expand the 6-slot arrays into 10s intervals
186
+
187
+ records = []
188
+
189
+ # Identify which instruments we actually need to process to save time
190
+ needed_instruments = set([d['instrument'] for d in datasets_config])
191
+
192
+ for row in db_rows:
193
+ base_ts = pd.to_datetime(row['minute'])
194
+ row_data = row.get('data', {})
195
 
196
+ for instr, metrics in row_data.items():
197
+ if instr not in needed_instruments:
198
+ continue
199
+
200
+ # metrics = {p: [..], b: [..], ...}
201
+ # Iterate 6 slots
202
+ for i in range(6):
203
+ offset_ts = base_ts + timedelta(seconds=i*10)
204
+
205
+ # Extract values for this slot
206
+ rec = {
207
+ "ts": offset_ts,
208
+ "instrument": instr
209
+ }
210
+
211
+ has_data = False
212
+ for var_name in ['p', 'b', 's', 'v', 'o']:
213
+ val_array = metrics.get(var_name)
214
+ val = None
215
+ if isinstance(val_array, list) and len(val_array) > i:
216
+ val = val_array[i]
217
+
218
+ if val is not None:
219
+ has_data = True
220
+ rec[var_name] = val
221
+ else:
222
+ rec[var_name] = np.nan
223
 
224
+ if has_data:
225
+ records.append(rec)
226
+
227
+ if not records:
228
+ return JSONResponse({"labels": [], "datasets": []})
229
+
230
+ df = pd.DataFrame(records)
231
+ df.set_index('ts', inplace=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
+ # 3. Resample and Aggregate
234
+ # We need to group by instrument, then resample
235
 
236
+ # Map for aggregation methods
237
+ agg_map = {
238
+ 'p': 'median', # Price: Median
239
+ 'b': 'sum', # Buy Qty: Sum (Accumulate flow over timeframe)
240
+ 's': 'sum', # Sell Qty: Sum
241
+ 'v': 'sum', # Volume: Sum
242
+ 'o': 'last' # OI: Last seen value (Snapshot)
243
+ }
244
+
245
+ # Ensure all columns exist
246
+ for col in agg_map.keys():
247
+ if col not in df.columns:
248
+ df[col] = np.nan
249
+
250
+ grouped = df.groupby('instrument')
251
+
252
+ # Resample Timeframe (e.g. '1min' -> '1T')
253
+ tf_map = {'1min': '1T', '3min': '3T', '5min': '5T', '15min': '15T', '30min': '30T', '60min': '60T'}
254
+ pandas_tf = tf_map.get(tf, '1T')
255
+
256
+ resampled_dfs = {}
257
+
258
+ for instr, group_df in grouped:
259
+ # Resample logic
260
+ res = group_df.resample(pandas_tf).agg(agg_map)
261
+ resampled_dfs[instr] = res
262
+
263
+ # 4. Calculate Formulas and Prepare Chart Data
264
+
265
+ # Find common time index (union of all)
266
+ all_indices = pd.Index([])
267
+ for d in resampled_dfs.values():
268
+ all_indices = all_indices.union(d.index)
269
+
270
+ all_indices = all_indices.sort_values()
271
+
272
+ final_datasets = []
273
+
274
+ for conf in datasets_config:
275
+ instr = conf['instrument']
276
+ formula = conf['formula'] # e.g. "b - s" or "p"
277
+ label = conf.get('label', instr)
278
+ color = conf.get('color', '#000000')
279
+ y_axis = conf.get('yAxisID', 'y')
280
+ type_chart = conf.get('type', 'line')
281
+
282
+ if instr not in resampled_dfs:
283
+ continue
284
+
285
+ rdf = resampled_dfs[instr].reindex(all_indices)
286
+
287
+ # Evaluate Formula safely
288
+ # Variables available: p, b, s, v, o
289
+ try:
290
+ # Create local context for eval
291
+ ctx = {
292
+ 'p': rdf['p'].fillna(0),
293
+ 'b': rdf['b'].fillna(0),
294
+ 's': rdf['s'].fillna(0),
295
+ 'v': rdf['v'].fillna(0),
296
+ 'o': rdf['o'].fillna(method='ffill').fillna(0), # OI should ffill
297
+ 'np': np
298
+ }
299
+
300
+ # Simple sanitization
301
+ allowed_chars = set("pbsvo0123456789+-*/(). ")
302
+ if not set(formula).issubset(allowed_chars):
303
+ # Fallback if complex chars, simpler logic
304
+ pass
305
+
306
+ result_series = eval(formula, {"__builtins__": None}, ctx)
307
+
308
+ # Replace nan with None for JSON
309
+ data_values = result_series.where(pd.notnull(result_series), None).tolist()
310
+
311
+ ds = {
312
+ "label": label,
313
+ "data": data_values,
314
+ "borderColor": color,
315
+ "backgroundColor": color,
316
+ "type": type_chart,
317
+ "yAxisID": y_axis,
318
+ "pointRadius": 1,
319
+ "borderWidth": 2,
320
+ "fill": False,
321
+ "tension": 0.1 # Slight curve
322
+ }
323
+ final_datasets.append(ds)
324
+
325
+ except Exception as e:
326
+ print(f"Formula Error ({formula}): {e}")
327
+ continue
328
+
329
+ # Format Labels (Time)
330
+ labels = [t.strftime('%H:%M') for t in all_indices]
331
+
332
+ return JSONResponse({
333
+ "labels": labels,
334
+ "datasets": final_datasets
335
+ })
336
 
 
337
 
338
  # ==========================================
339
+ # 3. FRONTEND (HTML/JS)
340
  # ==========================================
341
 
342
+ html_content = """
343
  <!DOCTYPE html>
344
  <html lang="en">
345
  <head>
346
  <meta charset="UTF-8">
 
347
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
348
+ <title>DepthChain Historical</title>
349
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
350
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
351
  <style>
352
+ :root {
353
+ --bg: #ffffff;
354
+ --panel: #f8f9fa;
355
+ --border: #e9ecef;
356
+ --text: #212529;
357
+ --primary: #0d6efd;
358
+ --accent: #6610f2;
359
+ --success: #198754;
360
+ --danger: #dc3545;
361
+ --shadow: 0 4px 12px rgba(0,0,0,0.05);
362
  }
363
  * { box-sizing: border-box; }
364
+ body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 0; }
365
 
 
 
 
 
 
 
366
  /* HEADER */
367
+ .top-bar {
368
+ position: sticky; top: 0; z-index: 100;
369
+ background: #fff; border-bottom: 1px solid var(--border);
370
+ padding: 10px 20px; display: flex; gap: 15px; align-items: center; flex-wrap: wrap;
371
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
372
+ }
373
+ .control-group { display: flex; flex-direction: column; gap: 4px; }
374
+ .control-group label { font-size: 11px; font-weight: 700; color: #6c757d; text-transform: uppercase; }
375
+ .select-input { padding: 8px; border: 1px solid #ced4da; border-radius: 4px; font-size: 13px; min-width: 120px; outline:none; }
376
+ .select-input:focus { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25); }
377
 
378
+ .btn { padding: 8px 16px; border: none; border-radius: 4px; font-weight: 600; font-size: 13px; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 6px; }
379
+ .btn-primary { background: var(--primary); color: white; }
380
+ .btn-primary:hover { background: #0b5ed7; }
381
+ .btn-outline { background: white; border: 1px solid #ced4da; color: var(--text); }
382
+ .btn-outline:hover { background: #f8f9fa; }
383
 
384
+ /* MAIN LAYOUT */
385
+ .container { padding: 20px; max-width: 1600px; margin: 0 auto; }
386
+ .charts-wrapper { display: flex; flex-direction: column; gap: 20px; }
 
 
 
 
387
 
388
  /* CHART CARD */
389
+ .chart-card {
390
+ background: white; border: 1px solid var(--border); border-radius: 8px;
391
+ padding: 15px; box-shadow: var(--shadow); position: relative;
392
+ transition: transform 0.2s;
393
+ }
394
+ .chart-header {
395
+ display: flex; justify-content: space-between; align-items: center;
396
+ margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #f1f3f5;
397
+ }
398
+ .chart-title { font-weight: 700; font-size: 14px; color: var(--text); display: flex; gap: 10px; align-items: center; }
399
+ .chart-meta { font-size: 12px; color: #868e96; font-family: 'JetBrains Mono'; background: #f8f9fa; padding: 2px 6px; border-radius: 4px; }
400
+ .chart-actions { display: flex; gap: 5px; }
401
+ .icon-btn { padding: 6px; border-radius: 4px; cursor: pointer; color: #adb5bd; transition: 0.2s; background: transparent; border:none; }
402
+ .icon-btn:hover { color: var(--primary); background: #e9ecef; }
403
+ .icon-btn.delete:hover { color: var(--danger); }
404
 
405
+ .chart-body { height: 60vh; position: relative; width: 100%; }
 
 
406
 
 
 
 
 
 
 
 
407
  /* MODAL */
408
+ .modal-overlay {
409
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
410
+ background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px);
411
+ z-index: 1000; display: none; justify-content: center; align-items: center;
412
+ }
413
+ .modal {
414
+ background: white; width: 600px; max-width: 90%;
415
+ border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.2);
416
+ display: flex; flex-direction: column; overflow: hidden;
417
+ animation: popIn 0.2s ease-out;
418
+ }
419
  @keyframes popIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
420
 
421
+ .modal-header { padding: 15px 20px; background: #f8f9fa; border-bottom: 1px solid var(--border); font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
422
+ .modal-body { padding: 20px; overflow-y: auto; max-height: 70vh; }
423
+ .modal-footer { padding: 15px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: #f8f9fa; }
 
 
 
 
 
 
424
 
425
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }
426
+ .form-full { margin-bottom: 15px; }
427
+ .inp-label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 5px; color: #495057; }
428
+ .inp-text { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 13px; }
429
 
430
+ .dataset-list { margin-top: 15px; border: 1px solid var(--border); border-radius: 4px; }
431
+ .dataset-item { padding: 10px; border-bottom: 1px solid #f1f3f5; display: flex; gap: 10px; align-items: center; background: #fff; }
432
+ .dataset-item:last-child { border-bottom: none; }
433
+ .color-picker { width: 30px; height: 30px; padding: 0; border: none; cursor: pointer; }
434
 
435
+ .no-data { text-align: center; padding: 40px; color: #adb5bd; font-weight: 500; font-size: 14px; border: 2px dashed #dee2e6; border-radius: 8px; }
 
436
 
437
  /* Loader */
438
+ .loader-line { height: 3px; width: 100%; background: #e9ecef; position: fixed; top: 0; left: 0; z-index: 200; display:none; }
439
+ .loader-sub { height: 100%; background: var(--primary); animation: load 1s infinite; width: 30%; }
440
+ @keyframes load { 0% { margin-left: -30%; } 100% { margin-left: 100%; } }
 
441
  </style>
442
  </head>
443
  <body>
444
 
445
+ <div class="loader-line" id="loader"><div class="loader-sub"></div></div>
446
 
447
+ <!-- TOP BAR -->
448
+ <div class="top-bar">
449
+ <div class="control-group">
450
+ <label>Date</label>
451
+ <select id="globalDate" class="select-input" onchange="onDateChange()"></select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  </div>
453
+
454
+ <div style="flex:1;"></div>
455
+
456
+ <button class="btn btn-outline" onclick="addChart()">
457
+ <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 5v14m-7-7h14"></path></svg>
458
+ Add Chart
459
+ </button>
460
  </div>
461
 
462
+ <!-- MAIN AREA -->
463
+ <div class="container">
464
+ <div id="chartsContainer" class="charts-wrapper">
465
+ <div class="no-data" id="emptyState">
466
+ No charts added. Click "Add Chart" to begin.
467
+ </div>
468
+ </div>
469
+ </div>
470
 
471
+ <!-- CHART CONFIG MODAL -->
472
+ <div class="modal-overlay" id="configModal">
473
  <div class="modal">
474
  <div class="modal-header">
475
+ <span id="modalTitle">Configure Chart</span>
476
+ <button class="icon-btn" onclick="closeModal()">✕</button>
477
  </div>
478
  <div class="modal-body">
479
+ <input type="hidden" id="editChartId">
480
 
481
+ <!-- Global Config for this Chart -->
482
+ <div class="form-row">
483
+ <div class="control-group">
484
+ <label class="inp-label">Root Symbol</label>
485
+ <select id="cfgRoot" class="select-input" style="width:100%" onchange="loadExpiries()"></select>
486
+ </div>
487
+ <div class="control-group">
488
+ <label class="inp-label">Expiry</label>
489
+ <select id="cfgExpiry" class="select-input" style="width:100%" onchange="loadInstruments()"></select>
490
+ </div>
491
  </div>
492
 
493
+ <div class="form-row">
494
+ <div class="control-group">
495
+ <label class="inp-label">Timeframe</label>
496
+ <select id="cfgTF" class="select-input" style="width:100%">
497
+ <option value="1min">1 Minute</option>
498
+ <option value="3min">3 Minute</option>
499
+ <option value="5min">5 Minute</option>
500
+ <option value="15min">15 Minute</option>
501
+ <option value="30min">30 Minute</option>
502
+ <option value="60min">1 Hour</option>
503
+ </select>
504
+ </div>
505
+ <div class="control-group">
506
+ <label class="inp-label">Axis Scaling</label>
507
+ <select id="cfgScale" class="select-input" style="width:100%">
508
+ <option value="shared">Shared Scale (Left)</option>
509
+ <option value="separate">Separate Scales (L/R)</option>
510
+ </select>
511
  </div>
 
 
512
  </div>
513
 
514
+ <div style="margin-top:20px; border-top:1px solid #eee; padding-top:10px;">
515
+ <label class="inp-label" style="display:flex; justify-content:space-between;">
516
+ DATA SERIES
517
+ <button class="btn btn-outline" style="padding:2px 8px; font-size:10px;" onclick="addDatasetRow()">+ Add Line</button>
518
+ </label>
519
+ <div id="datasetContainer" class="dataset-list"></div>
520
+ <small style="color:#999; display:block; margin-top:5px;">
521
+ Vars: <b>p</b> (Price), <b>b</b> (Buy), <b>s</b> (Sell), <b>v</b> (Vol), <b>o</b> (OI). <br>
522
+ Math: +, -, *, /, (). Example: <code>b - s</code> or <code>(b+s)/2</code>
523
+ </small>
524
+ </div>
525
  </div>
526
  <div class="modal-footer">
527
+ <button class="btn btn-outline" onclick="closeModal()">Cancel</button>
528
+ <button class="btn btn-primary" onclick="saveChartConfig()">Save & Plot</button>
529
  </div>
530
  </div>
531
  </div>
532
 
533
  <script>
534
+ // STATE
535
+ let DATES = [];
536
+ let CHART_REGISTRY = {}; // id -> { chartObj, config }
537
+
538
+ // CACHE FOR DROPDOWNS
539
+ let CACHE_ROOTS = [];
540
+ let CACHE_EXPS = [];
541
+ let CACHE_INSTRS = [];
 
 
 
 
 
 
542
 
543
+ // INIT
544
  window.onload = async () => {
545
+ await loadDates();
 
 
 
 
 
 
 
 
546
  };
547
 
548
+ // API CALLS
549
+ async function api(endpoint, params={}) {
550
+ const u = new URL('/api/' + endpoint, window.location.origin);
551
+ Object.keys(params).forEach(key => u.searchParams.append(key, params[key]));
552
+ const r = await fetch(u);
553
+ return r.json();
554
  }
555
 
556
+ async function loadDates() {
557
+ DATES = await api('dates');
558
+ const sel = document.getElementById('globalDate');
559
+ sel.innerHTML = DATES.map(d => `<option value="${d}">${d}</option>`).join('');
560
+ // Trigger generic update if charts exist (none initially)
 
 
 
 
 
 
 
 
 
 
 
561
  }
562
 
563
+ function onDateChange() {
564
+ // Reload all charts with new date
565
+ const dt = document.getElementById('globalDate').value;
566
+ Object.keys(CHART_REGISTRY).forEach(id => {
567
+ const config = CHART_REGISTRY[id].config;
568
+ config.date = dt;
569
+ fetchAndRender(id);
570
+ });
571
  }
572
 
573
+ // CHART MANAGEMENT
574
+ function addChart() {
575
+ document.getElementById('emptyState').style.display = 'none';
576
+ const id = 'chart_' + Date.now();
577
+
578
+ const div = document.createElement('div');
579
+ div.className = 'chart-card';
580
+ div.id = `card_${id}`;
581
+ div.innerHTML = `
582
+ <div class="chart-header">
583
+ <div class="chart-title">
584
+ <span id="title_${id}">New Chart</span>
585
+ <span class="chart-meta" id="meta_${id}">--</span>
 
 
586
  </div>
587
+ <div class="chart-actions">
588
+ <button class="icon-btn" onclick="editChart('${id}')" title="Configure">⚙️</button>
589
+ <button class="icon-btn" onclick="refreshChart('${id}')" title="Refresh">↻</button>
590
+ <button class="icon-btn delete" onclick="removeChart('${id}')" title="Remove">🗑</button>
591
  </div>
592
+ </div>
593
+ <div class="chart-body">
594
+ <canvas id="${id}"></canvas>
595
+ </div>
596
+ `;
597
+ document.getElementById('chartsContainer').appendChild(div);
598
+
599
+ // Init ChartJS
600
+ const ctx = document.getElementById(id).getContext('2d');
601
+ const chart = new Chart(ctx, {
602
+ type: 'line',
603
+ data: { labels: [], datasets: [] },
604
+ options: {
605
+ responsive: true,
606
+ maintainAspectRatio: false,
607
+ interaction: { mode: 'index', intersect: false },
608
+ scales: {
609
+ x: { grid: { display: false } },
610
+ y: { display: true, position: 'left' }
611
+ },
612
+ plugins: {
613
+ legend: { position: 'top', align: 'end' }
614
+ }
615
+ }
616
  });
617
+
618
+ // Default Config
619
+ CHART_REGISTRY[id] = {
620
+ chart: chart,
621
+ config: {
622
+ date: document.getElementById('globalDate').value,
623
+ root: '',
624
+ expiry: '',
625
+ timeframe: '1min',
626
+ scale: 'shared',
627
+ datasets: []
628
+ }
629
+ };
630
+
631
+ // Open Config Immediately
632
+ editChart(id);
633
+ }
634
+
635
+ function removeChart(id) {
636
+ if(CHART_REGISTRY[id]) {
637
+ CHART_REGISTRY[id].chart.destroy();
638
+ delete CHART_REGISTRY[id];
639
+ }
640
+ document.getElementById(`card_${id}`).remove();
641
+ if(Object.keys(CHART_REGISTRY).length === 0) {
642
+ document.getElementById('emptyState').style.display = 'block';
643
+ }
644
  }
645
 
646
+ // CONFIG MODAL LOGIC
647
+ async function editChart(id) {
648
+ const conf = CHART_REGISTRY[id].config;
649
+ document.getElementById('editChartId').value = id;
650
+ document.getElementById('modalTitle').innerText = `Configure Chart`;
651
 
652
+ // Load Roots for current date
653
+ const date = document.getElementById('globalDate').value;
654
+ const roots = await api('roots', { date: date });
655
+ CACHE_ROOTS = roots;
656
 
657
+ const rSel = document.getElementById('cfgRoot');
658
+ rSel.innerHTML = roots.map(r => `<option value="${r}">${r}</option>`).join('');
659
+ if(conf.root && roots.includes(conf.root)) rSel.value = conf.root;
660
+ else if(roots.length > 0) rSel.value = roots[0];
661
+
662
+ // Trigger cascade
663
+ await loadExpiries(conf.expiry); // Pass prev expiry to try preserve it
664
 
665
+ document.getElementById('cfgTF').value = conf.timeframe;
666
+ document.getElementById('cfgScale').value = conf.scale;
667
 
668
+ // Render Dataset Rows
669
+ const dsCont = document.getElementById('datasetContainer');
670
+ dsCont.innerHTML = '';
671
+ if(conf.datasets.length > 0) {
672
+ conf.datasets.forEach(ds => addDatasetRow(ds));
673
+ } else {
674
+ addDatasetRow(); // Add one empty
675
+ }
 
 
 
 
 
 
 
 
 
676
 
677
+ document.getElementById('configModal').style.display = 'flex';
 
 
 
678
  }
679
 
680
+ async function loadExpiries(preserveVal=null) {
681
+ const date = document.getElementById('globalDate').value;
682
+ const root = document.getElementById('cfgRoot').value;
683
+ if(!root) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
 
685
+ const exps = await api('expiries', { date, root });
686
+ CACHE_EXPS = exps;
687
+
688
+ const eSel = document.getElementById('cfgExpiry');
689
+ eSel.innerHTML = exps.map(e => `<option value="${e}">${e}</option>`).join('');
690
+
691
+ if(preserveVal && exps.includes(preserveVal)) eSel.value = preserveVal;
692
+
693
+ await loadInstruments();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  }
695
 
696
+ async function loadInstruments() {
697
+ const date = document.getElementById('globalDate').value;
698
+ const root = document.getElementById('cfgRoot').value;
699
+ const expiry = document.getElementById('cfgExpiry').value;
700
+ if(!root || !expiry) return;
701
 
702
+ const instrs = await api('instruments', { date, root, expiry });
703
+ CACHE_INSTRS = instrs;
 
704
 
705
+ // Update all instrument dropdowns in dataset list
706
+ document.querySelectorAll('.instr-select').forEach(sel => {
707
+ const curVal = sel.value;
708
+ sel.innerHTML = instrs.map(i => `<option value="${i}">${i}</option>`).join('');
709
+ if(curVal && instrs.includes(curVal)) sel.value = curVal;
 
 
 
 
710
  });
 
 
 
 
711
  }
712
 
713
+ function addDatasetRow(data=null) {
714
+ const div = document.createElement('div');
715
+ div.className = 'dataset-item';
 
716
 
717
+ const instOpts = CACHE_INSTRS.map(i => `<option value="${i}">${i}</option>`).join('');
 
718
 
719
+ div.innerHTML = `
720
+ <select class="select-input instr-select" style="flex:2;">${instOpts}</select>
721
+ <input type="text" class="inp-text form-input" style="flex:1;" placeholder="Formula (e.g. b-s)" value="${data ? data.formula : 'p'}">
722
+ <input type="text" class="inp-text label-input" style="flex:1;" placeholder="Legend" value="${data ? data.label : ''}">
723
+ <select class="select-input type-select" style="width:70px;">
724
+ <option value="line" ${data && data.type=='line'?'selected':''}>Line</option>
725
+ <option value="bar" ${data && data.type=='bar'?'selected':''}>Bar</option>
726
+ </select>
727
+ <input type="color" class="color-picker" value="${data ? data.color : getRandomColor()}">
728
+ <button class="icon-btn delete" onclick="this.parentElement.remove()">✕</button>
729
+ `;
730
 
731
+ // Set selected value for instrument
732
+ if(data && data.instrument) div.querySelector('.instr-select').value = data.instrument;
 
 
733
 
734
+ document.getElementById('datasetContainer').appendChild(div);
735
+ }
 
 
 
 
736
 
737
+ function closeModal() {
738
+ document.getElementById('configModal').style.display = 'none';
739
+ }
 
 
 
 
740
 
741
+ async function saveChartConfig() {
742
+ const id = document.getElementById('editChartId').value;
743
+ const root = document.getElementById('cfgRoot').value;
744
+ const expiry = document.getElementById('cfgExpiry').value;
745
+ const tf = document.getElementById('cfgTF').value;
746
+ const scale = document.getElementById('cfgScale').value;
747
+
748
+ const datasets = [];
749
+ const rows = document.querySelectorAll('.dataset-item');
750
+
751
+ rows.forEach((r, idx) => {
752
+ const instr = r.querySelector('.instr-select').value;
753
+ const form = r.querySelector('.form-input').value;
754
+ const lbl = r.querySelector('.label-input').value;
755
+ const clr = r.querySelector('.color-picker').value;
756
+ const typ = r.querySelector('.type-select').value;
757
 
758
+ // Axis logic
759
+ let yAxis = 'y'; // default shared
760
+ if(scale === 'separate') {
761
+ yAxis = 'y' + idx; // y0, y1, y2...
762
+ }
763
+
764
+ datasets.push({
765
+ instrument: instr,
766
+ formula: form,
767
+ label: lbl || instr,
768
+ color: clr,
769
+ type: typ,
770
+ yAxisID: yAxis
771
+ });
772
+ });
773
 
774
+ // Update Config
775
+ const conf = CHART_REGISTRY[id].config;
776
+ conf.root = root;
777
+ conf.expiry = expiry;
778
+ conf.timeframe = tf;
779
+ conf.scale = scale;
780
+ conf.datasets = datasets;
781
+
782
+ // Update Chart Scales Config
783
+ const chart = CHART_REGISTRY[id].chart;
784
+ chart.options.scales = { x: { display: true } }; // Reset
785
+
786
+ if(scale === 'shared') {
787
+ chart.options.scales['y'] = { display: true, position: 'left' };
788
+ } else {
789
+ datasets.forEach((d, i) => {
790
+ chart.options.scales[d.yAxisID] = {
791
+ display: true,
792
+ position: i % 2 === 0 ? 'left' : 'right',
793
+ grid: { drawOnChartArea: i === 0 } // Only first grid
794
+ };
795
+ });
796
+ }
797
+
798
+ closeModal();
799
+
800
+ // Update UI Text
801
+ document.getElementById(`title_${id}`).innerText = `${root} (${expiry})`;
802
+ document.getElementById(`meta_${id}`).innerText = tf.toUpperCase();
803
+
804
+ // Fetch
805
+ fetchAndRender(id);
806
  }
807
 
808
+ async function fetchAndRender(id) {
809
+ const conf = CHART_REGISTRY[id].config;
810
+ if(conf.datasets.length === 0) return;
811
 
812
+ document.getElementById('loader').style.display = 'block';
813
 
814
+ try {
815
+ const payload = {
816
+ date: document.getElementById('globalDate').value,
817
+ root: conf.root,
818
+ expiry: conf.expiry,
819
+ timeframe: conf.timeframe,
820
+ datasets: conf.datasets
821
+ };
822
 
823
+ const r = await fetch('/api/plot-data', {
824
+ method: 'POST',
825
+ headers: {'Content-Type': 'application/json'},
826
+ body: JSON.stringify(payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
  });
828
+
829
+ const data = await r.json();
830
+
831
+ const chart = CHART_REGISTRY[id].chart;
832
+ chart.data.labels = data.labels;
833
+ chart.data.datasets = data.datasets;
834
+ chart.update();
835
+
836
+ } catch(e) {
837
+ console.error(e);
838
+ alert("Error fetching data");
839
+ } finally {
840
+ document.getElementById('loader').style.display = 'none';
841
+ }
842
+ }
843
 
844
+ function refreshChart(id) {
845
+ fetchAndRender(id);
846
+ }
847
 
848
+ function getRandomColor() {
849
+ const letters = '0123456789ABCDEF';
850
+ let color = '#';
851
+ for (let i = 0; i < 6; i++) {
852
+ color += letters[Math.floor(Math.random() * 16)];
853
+ }
854
+ return color;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
  }
856
 
857
  </script>