topsecrettraders commited on
Commit
29388da
·
verified ·
1 Parent(s): 673c8a9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +751 -528
app.py CHANGED
@@ -2,15 +2,17 @@ import pandas as pd
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, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse
 
9
  from typing import Optional, List, Dict, Any
10
 
11
  # ==========================================
12
  # 1. CONFIGURATION
13
  # ==========================================
 
14
  SB_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
15
  SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
16
 
@@ -26,18 +28,19 @@ app = FastAPI()
26
  # ==========================================
27
  # 2. HELPER FUNCTIONS
28
  # ==========================================
29
- def sb_request(endpoint: str, params: dict = None):
 
30
  try:
31
  url = f"{SB_URL}/rest/v1{endpoint}"
32
  r = requests.get(url, headers=HEADERS, params=params)
33
  r.raise_for_status()
34
  return r.json()
35
  except Exception as e:
36
- print(f"Supabase Error: {e}")
37
  return []
38
 
39
  # ==========================================
40
- # 3. API ROUTES
41
  # ==========================================
42
 
43
  @app.get("/")
@@ -46,179 +49,129 @@ def home():
46
 
47
  @app.get("/api/dates")
48
  def get_dates():
49
- # Fetch distinct dates based on the minute column
50
- # We fetch a large range of minutes to extract unique dates
51
- data = sb_request("/history_buckets_upstox", {
52
- "select": "minute",
53
- "order": "minute.desc",
54
- "limit": "5000"
55
- })
56
- dates = set()
57
- for row in data:
58
- if row.get('minute'):
59
- dates.add(row['minute'].split('T')[0])
60
  return JSONResponse(sorted(list(dates), reverse=True))
61
 
62
  @app.get("/api/roots")
63
  def get_roots(date: str):
64
- # Fetch roots present on this specific date
65
- start = f"{date}T00:00:00"
66
- end = f"{date}T23:59:59"
67
- data = sb_request("/history_buckets_upstox", {
68
- "select": "root",
69
- "minute": f"gte.{start}&minute=lte.{end}",
70
- "limit": "2000"
71
- })
72
- roots = set()
73
- for row in data:
74
- if row.get('root'):
75
- roots.add(row['root'])
76
  return JSONResponse(sorted(list(roots)))
77
 
78
  @app.get("/api/expiries")
79
  def get_expiries(date: str, root: str):
80
- start = f"{date}T00:00:00"
81
- end = f"{date}T23:59:59"
82
- data = sb_request("/history_buckets_upstox", {
83
- "select": "expiry",
84
- "root": f"eq.{root}",
85
- "minute": f"gte.{start}&minute=lte.{end}",
86
- "limit": "2000"
87
- })
88
- exps = set()
89
- for row in data:
90
- if row.get('expiry'):
91
- exps.add(row['expiry'])
92
-
93
- # Sort with MARKET at top
94
- sl = sorted(list(exps))
95
- if "MARKET" in sl:
96
- sl.remove("MARKET")
97
- sl.insert(0, "MARKET")
98
- return JSONResponse(sl)
99
 
100
  @app.get("/api/instruments")
101
  def get_instruments(date: str, root: str, expiry: str):
102
- # Fetch one row to get keys from the JSONB 'data' column
103
- start = f"{date}T00:00:00"
104
- end = f"{date}T23:59:59"
105
- data = sb_request("/history_buckets_upstox", {
106
- "select": "data",
107
- "root": f"eq.{root}",
108
- "expiry": f"eq.{expiry}",
109
- "minute": f"gte.{start}&minute=lte.{end}",
110
- "limit": "1",
111
- "order": "minute.desc"
112
- })
113
-
114
- instruments = []
115
- if data and data[0].get('data'):
116
- instruments = list(data[0]['data'].keys())
117
-
118
- return JSONResponse(sorted(instruments))
119
 
120
- @app.post("/api/fetch_plot")
121
- def fetch_plot_data(
122
  date: str = Body(...),
123
  root: str = Body(...),
124
  expiry: str = Body(...),
125
  instrument: str = Body(...),
126
- timeframe: str = Body(...),
127
  start_time: str = Body(...),
128
  end_time: str = Body(...)
129
  ):
 
 
 
130
  t_start = f"{date}T{start_time}:00"
131
  t_end = f"{date}T{end_time}:59"
132
-
133
- # Fetch rows
134
- rows = sb_request("/history_buckets_upstox", {
135
- "select": "minute,data",
136
- "root": f"eq.{root}",
137
- "expiry": f"eq.{expiry}",
138
- "minute": f"gte.{t_start}&minute=lte.{t_end}",
139
- "order": "minute.asc"
140
- })
141
-
142
- if not rows:
143
- return JSONResponse({"error": "No data found for this selection"})
144
-
145
- # Process 6-slot sub-minute data
146
  records = []
147
 
 
148
  for row in rows:
149
  try:
150
- base_ts = pd.to_datetime(row['minute'])
151
  inst_data = row['data'].get(instrument)
152
 
153
  if not inst_data: continue
154
 
155
- # Extract arrays
156
- # Upstox buckets: p=Price, b=BuyQty, s=SellQty, v=Vol, o=OI
157
- p = inst_data.get('p', [])
158
- b = inst_data.get('b', [])
159
- s = inst_data.get('s', [])
160
- v = inst_data.get('v', [])
161
- o = inst_data.get('o', [])
162
-
163
- # Iterate 6 slots (0s, 10s, 20s, 30s, 40s, 50s)
164
  for i in range(6):
165
- # We need at least a price to consider the slot valid
166
- if i < len(p) and p[i] is not None:
167
- ts = base_ts + timedelta(seconds=i*10)
168
-
169
- val_p = float(p[i])
170
- val_b = float(b[i]) if i < len(b) and b[i] is not None else 0.0
171
- val_s = float(s[i]) if i < len(s) and s[i] is not None else 0.0
172
- val_v = float(v[i]) if i < len(v) and v[i] is not None else 0.0
173
- val_o = float(o[i]) if i < len(o) and o[i] is not None else 0.0
174
-
175
  records.append({
176
- "ts": ts,
177
- "P": val_p,
178
- "B": val_b,
179
- "S": val_s,
180
- "V": val_v,
181
- "OI": val_o
182
  })
183
- except Exception as e:
184
  continue
185
 
186
  if not records:
187
- return JSONResponse({"error": "Empty dataset after processing"})
188
 
189
- # Create DataFrame
190
  df = pd.DataFrame(records)
191
  df.set_index('ts', inplace=True)
192
-
193
- # Resample Logic
194
- # 1min, 3min, 5min, etc.
195
  tf_map = {
196
- "1min": "1T", "3min": "3T", "5min": "5T",
197
- "15min": "15T", "30min": "30T", "1hour": "1H"
198
  }
199
- panda_tf = tf_map.get(timeframe, "1T")
200
-
201
- # Aggregation: Median is best for noise reduction as requested
202
- resampled = df.resample(panda_tf).median()
203
 
204
- # Drop rows where Price is NaN (gap in data)
205
- resampled.dropna(subset=['P'], inplace=True)
206
-
207
- return JSONResponse({
208
- "status": "ok",
209
  "labels": resampled.index.strftime('%H:%M').tolist(),
210
- "data": {
211
- "P": resampled['P'].tolist(),
212
- "B": resampled['B'].tolist(),
213
- "S": resampled['S'].tolist(),
214
- "V": resampled['V'].tolist(),
215
- "OI": resampled['OI'].tolist()
216
- }
217
- })
 
218
 
219
  # ==========================================
220
- # 4. FRONTEND (HTML/JS/CSS)
221
  # ==========================================
 
222
  HTML_TEMPLATE = """
223
  <!DOCTYPE html>
224
  <html lang="en">
@@ -226,174 +179,356 @@ HTML_TEMPLATE = """
226
  <meta charset="UTF-8">
227
  <title>DepthChain Historical</title>
228
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
229
- <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
230
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
231
  <style>
232
- :root {
233
- --bg: #f3f4f6;
234
- --panel: #ffffff;
235
- --border: #e5e7eb;
236
- --text: #1f2937;
237
- --primary: #2563eb;
238
- --danger: #dc2626;
239
- --success: #16a34a;
240
- }
241
- * { box-sizing: border-box; }
242
- body {
243
- margin: 0; background: var(--bg); color: var(--text);
244
- font-family: 'Manrope', sans-serif; height: 100vh;
245
- display: flex; flex-direction: column; overflow: hidden;
246
- }
247
-
248
- /* TOP BAR */
249
- .top-nav {
250
- background: #fff; border-bottom: 1px solid var(--border);
251
- height: 50px; display: flex; align-items: center; justify-content: space-between;
252
- padding: 0 20px; flex-shrink: 0;
253
- }
254
- .logo { font-weight: 800; font-size: 18px; color: var(--text); display:flex; gap:5px; align-items:center; }
255
- .logo span { color: var(--primary); }
256
-
257
- /* MAIN LAYOUT */
258
- .layout { display: flex; flex: 1; height: calc(100vh - 50px); }
259
-
260
- /* SIDEBAR: CONTROLS */
261
- .sidebar {
262
- width: 320px; background: var(--panel); border-right: 1px solid var(--border);
263
- display: flex; flex-direction: column; overflow-y: auto; flex-shrink: 0;
264
- padding: 15px; gap: 15px;
265
  }
 
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  .section-title {
268
- font-size: 11px; font-weight: 800; color: #9ca3af;
269
- text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;
270
- border-bottom: 1px solid var(--border); padding-bottom: 4px;
 
 
 
271
  }
272
 
273
- .form-group { margin-bottom: 10px; }
274
- .form-label { font-size: 11px; font-weight: 700; color: #4b5563; display: block; margin-bottom: 4px; }
275
- .form-select, .form-input {
276
- width: 100%; padding: 8px 10px; border: 1px solid var(--border);
277
- border-radius: 6px; font-size: 12px; font-family: 'Manrope';
278
- outline: none; background: #f9fafb; transition: 0.2s;
279
  }
280
- .form-select:focus, .form-input:focus {
281
- border-color: var(--primary); background: #fff;
282
- box-shadow: 0 0 0 2px rgba(37,99,235,0.1);
 
 
 
 
 
 
 
 
283
  }
284
-
285
- .btn {
286
- width: 100%; padding: 10px; border: none; border-radius: 6px;
287
- font-weight: 700; font-size: 12px; cursor: pointer; transition: 0.2s;
288
- }
289
- .btn-primary { background: var(--primary); color: white; }
290
- .btn-primary:hover { background: #1d4ed8; }
291
- .btn-outline { background: white; border: 1px solid var(--border); color: var(--text); }
292
- .btn-outline:hover { background: #f3f4f6; }
293
- .btn-sm { padding: 4px 8px; font-size: 10px; width: auto; }
294
-
295
- /* PANELS CONFIGURATION */
296
- .panels-container { display: flex; flex-direction: column; gap: 10px; }
297
- .panel-card {
298
- background: #fff; border: 1px solid var(--border);
299
- border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.05);
300
- }
301
- .panel-header {
302
- background: #f8fafc; padding: 8px 12px; border-bottom: 1px solid var(--border);
303
- display: flex; justify-content: space-between; align-items: center;
304
  }
305
- .panel-title { font-size: 11px; font-weight: 800; color: #64748b; }
306
- .panel-body { padding: 10px; }
307
 
308
- .series-row {
309
- display: grid; grid-template-columns: 20px 1fr 60px 20px; gap: 8px;
310
- align-items: center; margin-bottom: 8px; border-bottom: 1px dashed #f1f5f9;
311
- padding-bottom: 8px;
 
 
 
 
 
 
 
312
  }
313
- .color-picker { width: 20px; height: 20px; border: none; padding: 0; cursor: pointer; background:none; }
314
- .formula-box {
315
- display: flex; gap: 5px; align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
- .var-select { width: 100%; font-family: 'JetBrains Mono'; font-size: 11px; }
318
 
319
- /* CHART AREA */
320
- .chart-main {
321
- flex: 1; background: #f1f5f9; padding: 20px; overflow-y: auto;
322
- display: flex; flex-direction: column; gap: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  }
324
- .chart-wrapper {
325
- background: #fff; border-radius: 8px; padding: 15px;
326
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
327
- min-height: 400px; border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  position: relative;
 
 
329
  }
330
- .chart-wrapper canvas { width: 100% !important; height: 100% !important; }
331
 
332
- /* LOADER */
333
- .loader {
334
- position: fixed; inset: 0; background: rgba(255,255,255,0.8);
335
- z-index: 1000; display: none; justify-content: center; align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  }
338
- .spinner {
339
- width: 40px; height: 40px; border: 4px solid #e5e7eb;
340
- border-top: 4px solid var(--primary); border-radius: 50%;
341
- animation: spin 1s linear infinite; margin-bottom: 10px;
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
344
 
 
 
 
 
 
 
 
 
 
 
345
  </style>
346
  </head>
347
  <body>
348
 
349
  <!-- LOADER -->
350
  <div class="loader" id="loader">
351
- <div class="spinner"></div>
352
- <div style="font-weight:700; color:#4b5563;">LOADING DATA...</div>
353
  </div>
354
 
355
- <!-- TOP NAV -->
356
- <div class="top-nav">
357
- <div class="logo">DepthChain <span>HISTORICAL</span></div>
358
- <div>
359
- <button class="btn btn-primary btn-sm" onclick="fetchAndPlot()">REFRESH PLOTS</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  </div>
361
  </div>
362
 
363
- <div class="layout">
364
- <!-- LEFT SIDEBAR -->
365
- <div class="sidebar">
 
 
 
 
 
 
 
 
 
 
 
366
 
367
- <!-- 1. DATA SELECT -->
368
- <div class="section-title">1. Data Selection</div>
369
- <div class="form-group">
370
- <span class="form-label">Date</span>
371
- <select id="selDate" class="form-select" onchange="loadRoots()"></select>
372
- </div>
373
- <div class="form-group">
374
- <span class="form-label">Root Symbol</span>
375
- <select id="selRoot" class="form-select" onchange="loadExpiries()" disabled>
376
- <option>-- Select Date First --</option>
377
- </select>
378
  </div>
379
- <div class="form-group">
380
- <span class="form-label">Expiry</span>
381
- <select id="selExp" class="form-select" onchange="loadInstruments()" disabled>
382
- <option>--</option>
383
- </select>
384
- </div>
385
- <div class="form-group">
386
- <span class="form-label">Instrument (Strike/Future)</span>
387
- <select id="selInst" class="form-select" disabled>
388
- <option>--</option>
389
- </select>
390
- </div>
391
-
392
- <!-- 2. TIME & RESAMPLE -->
393
- <div class="section-title" style="margin-top:10px;">2. Time Settings</div>
394
- <div class="form-group">
395
- <span class="form-label">Timeframe (Resample)</span>
396
- <select id="selTF" class="form-select">
397
  <option value="1min">1 Minute</option>
398
  <option value="3min">3 Minutes</option>
399
  <option value="5min">5 Minutes</option>
@@ -402,319 +537,407 @@ HTML_TEMPLATE = """
402
  <option value="1hour">1 Hour</option>
403
  </select>
404
  </div>
405
- <div style="display:flex; gap:10px;">
406
- <div class="form-group" style="flex:1;">
407
- <span class="form-label">Start</span>
408
- <input type="time" id="startTime" class="form-input" value="09:15">
 
409
  </div>
410
- <div class="form-group" style="flex:1;">
411
- <span class="form-label">End</span>
412
- <input type="time" id="endTime" class="form-input" value="15:30">
413
  </div>
414
  </div>
415
 
416
- <!-- 3. PLOT CONFIG -->
417
- <div class="section-title" style="margin-top:10px; display:flex; justify-content:space-between;">
418
- <span>3. Panels Config</span>
419
- <button class="btn btn-outline btn-sm" onclick="addPanel()">+ PANEL</button>
420
  </div>
421
-
422
- <div id="panelsContainer" class="panels-container"></div>
423
-
424
  </div>
425
 
426
- <!-- RIGHT CHART AREA -->
427
- <div class="chart-main" id="chartMain">
428
- <div style="text-align:center; margin-top:100px; color:#94a3b8;">
429
- <div style="font-size:40px; margin-bottom:10px;">📊</div>
430
- <div style="font-weight:700;">No Data Loaded</div>
431
- <div style="font-size:12px;">Select Data on the left and click Refresh Plots</div>
432
  </div>
433
  </div>
 
434
  </div>
435
 
436
  <script>
437
  // --- STATE ---
438
- let PANELS = [
439
- {
440
- id: 1,
441
- series: [{ id: 101, type: 'predefined', var: '$P', color: '#2563eb', axis: 'left' }]
442
- }
443
- ];
444
- let CACHED_DATA = null;
445
- let CHARTS = [];
446
-
447
- // --- PREDEFINED VARIABLES ---
448
- const VARS = [
449
- { label: 'Price ($P)', val: '$P' },
450
- { label: 'Net Flow ($B - $S)', val: '$B - $S' },
451
- { label: 'Open Interest ($OI)', val: '$OI' },
452
- { label: 'Volume ($V)', val: '$V' },
453
- { label: 'Buy Qty ($B)', val: '$B' },
454
- { label: 'Sell Qty ($S)', val: '$S' },
455
- { label: 'OI Change', val: '$OI' }, // Logic handled in render if needed
456
- { label: 'Custom Formula...', val: 'custom' }
457
- ];
458
-
459
  // --- INIT ---
460
  window.onload = async () => {
461
- renderPanels();
462
- await loadDates();
 
 
 
 
 
 
 
 
 
 
463
  };
464
 
465
- // --- API CALLS ---
466
- async function loadDates() {
467
- const sel = document.getElementById('selDate');
468
- sel.innerHTML = '<option>Loading...</option>';
469
- try {
470
- const r = await fetch('/api/dates');
471
- const d = await r.json();
472
- sel.innerHTML = d.map(x => `<option value="${x}">${x}</option>`).join('');
473
- if(d.length > 0) loadRoots();
474
- } catch(e) { sel.innerHTML = '<option>Error</option>'; }
475
  }
476
 
477
- async function loadRoots() {
478
- const date = document.getElementById('selDate').value;
479
- const sel = document.getElementById('selRoot');
480
- sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
481
- try {
482
- const r = await fetch(`/api/roots?date=${date}`);
483
- const d = await r.json();
484
- sel.innerHTML = d.map(x => `<option value="${x}">${x}</option>`).join('');
485
- sel.disabled = false;
486
- if(d.length > 0) loadExpiries();
487
- } catch(e) { sel.innerHTML = '<option>None</option>'; }
488
- }
489
-
490
- async function loadExpiries() {
491
- const date = document.getElementById('selDate').value;
492
- const root = document.getElementById('selRoot').value;
493
- const sel = document.getElementById('selExp');
494
- sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
495
- try {
496
- const r = await fetch(`/api/expiries?date=${date}&root=${root}`);
497
- const d = await r.json();
498
- sel.innerHTML = d.map(x => `<option value="${x}">${x}</option>`).join('');
499
- sel.disabled = false;
500
- if(d.length > 0) loadInstruments();
501
- } catch(e) { sel.innerHTML = '<option>None</option>'; }
502
  }
503
 
504
- async function loadInstruments() {
505
- const date = document.getElementById('selDate').value;
506
- const root = document.getElementById('selRoot').value;
507
- const exp = document.getElementById('selExp').value;
508
- const sel = document.getElementById('selInst');
509
- sel.disabled = true; sel.innerHTML = '<option>Loading...</option>';
510
- try {
511
- const r = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
512
- const d = await r.json();
513
- sel.innerHTML = d.map(x => `<option value="${x}">${x}</option>`).join('');
514
- sel.disabled = false;
515
- } catch(e) { sel.innerHTML = '<option>None</option>'; }
516
  }
517
 
518
- // --- PANEL CONFIG RENDERER ---
519
- function renderPanels() {
520
- const c = document.getElementById('panelsContainer');
521
- c.innerHTML = '';
522
 
523
- PANELS.forEach((p, pIdx) => {
524
- let seriesHTML = '';
525
- p.series.forEach((s, sIdx) => {
526
- // Variable Select Options
527
- const opts = VARS.map(v => `<option value="${v.val}" ${s.var === v.val ? 'selected' : ''}>${v.label}</option>`).join('');
528
- const inputDisplay = s.var === 'custom' || !VARS.find(v => v.val === s.var) ? 'block' : 'none';
529
-
530
- seriesHTML += `
531
- <div class="series-row">
532
- <input type="color" class="color-picker" value="${s.color}" onchange="updateSeries(${pIdx}, ${sIdx}, 'color', this.value)">
533
-
534
- <div style="display:flex; flex-direction:column; gap:2px;">
535
- <select class="form-select" style="padding:4px; font-size:10px;" onchange="updateSeriesVar(${pIdx}, ${sIdx}, this.value)">
536
- ${opts}
537
- </select>
538
- <input type="text" class="form-input" style="padding:4px; font-size:10px; display:${inputDisplay};"
539
- value="${s.var === 'custom' ? '' : s.var}"
540
- placeholder="Formula (e.g. $B - $S)"
541
- onchange="updateSeries(${pIdx}, ${sIdx}, 'var', this.value)">
542
  </div>
543
-
544
- <select class="form-select" style="padding:4px; font-size:10px;" onchange="updateSeries(${pIdx}, ${sIdx}, 'axis', this.value)">
545
- <option value="left" ${s.axis==='left'?'selected':''}>L-Axis</option>
546
- <option value="right" ${s.axis==='right'?'selected':''}>R-Axis</option>
547
- </select>
548
-
549
- <button style="color:red; background:none; border:none; cursor:pointer; font-weight:bold;" onclick="removeSeries(${pIdx}, ${sIdx})">×</button>
550
- </div>`;
551
- });
552
-
553
- c.innerHTML += `
554
- <div class="panel-card">
555
- <div class="panel-header">
556
- <span class="panel-title">PANEL ${pIdx+1}</span>
557
- <button class="btn-outline btn-sm" style="color:red; border:none;" onclick="removePanel(${pIdx})">DEL</button>
558
  </div>
559
- <div class="panel-body">
560
- ${seriesHTML}
561
- <button class="btn btn-outline btn-sm" style="width:100%; margin-top:5px; border-style:dashed;" onclick="addSeries(${pIdx})">+ Add Series</button>
562
  </div>
563
- </div>`;
564
- });
 
 
 
565
  }
566
 
567
- // --- CONFIG LOGIC ---
568
- function addPanel() { PANELS.push({ id: Date.now(), series: [{ var: '$P', color: '#000000', axis: 'left' }] }); renderPanels(); }
569
- function removePanel(i) { PANELS.splice(i, 1); renderPanels(); }
570
- function addSeries(pi) { PANELS[pi].series.push({ var: '$P', color: getRandomColor(), axis: 'left' }); renderPanels(); }
571
- function removeSeries(pi, si) { PANELS[pi].series.splice(si, 1); renderPanels(); }
572
-
573
- function updateSeries(pi, si, key, val) { PANELS[pi].series[si][key] = val; }
574
- function updateSeriesVar(pi, si, val) {
575
- if (val === 'custom') {
576
- PANELS[pi].series[si].var = '';
 
 
 
 
 
 
 
 
 
 
 
577
  } else {
578
- PANELS[pi].series[si].var = val;
 
579
  }
580
- renderPanels();
 
 
581
  }
582
- function getRandomColor() { return '#' + Math.floor(Math.random()*16777215).toString(16); }
583
 
584
- // --- DATA FETCH & PLOT ---
585
- async function fetchAndPlot() {
586
- const inst = document.getElementById('selInst').value;
587
- if (!inst || inst === '--' || inst === 'Loading...') return alert("Please select an Instrument first.");
 
 
 
 
 
 
 
 
588
 
589
- document.getElementById('loader').style.display = 'flex';
 
 
 
 
590
 
591
- const payload = {
592
- date: document.getElementById('selDate').value,
593
- root: document.getElementById('selRoot').value,
594
- expiry: document.getElementById('selExp').value,
595
- instrument: inst,
596
- timeframe: document.getElementById('selTF').value,
597
- start_time: document.getElementById('startTime').value,
598
- end_time: document.getElementById('endTime').value
599
- };
600
-
601
- try {
602
- const r = await fetch('/api/fetch_plot', {
603
- method: 'POST',
604
- headers: { 'Content-Type': 'application/json' },
605
- body: JSON.stringify(payload)
606
- });
607
- const resp = await r.json();
608
 
609
- if (resp.error) {
610
- alert(resp.error);
611
- document.getElementById('loader').style.display = 'none';
612
- return;
613
- }
614
 
615
- CACHED_DATA = resp; // { labels: [], data: { P:[], B:[], ... } }
616
- drawCharts();
617
 
618
- } catch (e) {
619
- console.error(e);
620
- alert("Failed to fetch data.");
621
- } finally {
622
- document.getElementById('loader').style.display = 'none';
623
  }
624
  }
625
 
626
- // --- CHART DRAWING ---
627
- function drawCharts() {
628
- const container = document.getElementById('chartMain');
629
- container.innerHTML = '';
630
- CHARTS.forEach(c => c.destroy());
631
- CHARTS = [];
632
-
633
- if(!CACHED_DATA) return;
634
-
635
- PANELS.forEach((panel, idx) => {
636
- // Container
637
- const wrapper = document.createElement('div');
638
- wrapper.className = 'chart-wrapper';
639
- const canvas = document.createElement('canvas');
640
- wrapper.appendChild(canvas);
641
- container.appendChild(wrapper);
642
-
643
- // Compute Data
644
- const datasets = panel.series.map(s => {
645
- const values = CACHED_DATA.labels.map((_, i) => evaluateFormula(s.var, i));
646
- return {
647
- label: s.var,
648
- data: values,
649
- borderColor: s.color,
650
- backgroundColor: s.color,
651
- borderWidth: 2,
652
- pointRadius: 0,
653
- pointHoverRadius: 4,
654
- yAxisID: s.axis === 'left' ? 'y' : 'y1',
655
- tension: 0.2
656
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- // Config ChartJS
660
- const ctx = canvas.getContext('2d');
661
- const chart = new Chart(ctx, {
662
- type: 'line',
663
- data: {
664
- labels: CACHED_DATA.labels,
665
- datasets: datasets
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  },
667
- options: {
668
- responsive: true,
669
- maintainAspectRatio: false,
670
- interaction: { mode: 'index', intersect: false },
671
- plugins: {
672
- legend: { position: 'top', labels: { usePointStyle: true, boxWidth: 8 } },
673
- title: { display: true, text: `Panel ${idx+1}`, align: 'start', font: { size: 14 } }
674
  },
675
- scales: {
676
- x: {
677
- grid: { display: false },
678
- ticks: { maxTicksLimit: 10 }
679
- },
680
- y: {
681
- type: 'linear', display: true, position: 'left',
682
- grid: { color: '#f1f5f9' },
683
- title: { display: true, text: 'Left Axis' }
684
- },
685
- y1: {
686
- type: 'linear', display: datasets.some(d => d.yAxisID === 'y1'), position: 'right',
687
- grid: { display: false },
688
- title: { display: true, text: 'Right Axis' }
689
- }
690
  }
691
  }
692
- });
693
- CHARTS.push(chart);
694
  });
695
  }
696
 
697
- function evaluateFormula(formula, idx) {
698
- if(!CACHED_DATA) return 0;
699
- const P = CACHED_DATA.data.P[idx] || 0;
700
- const B = CACHED_DATA.data.B[idx] || 0;
701
- const S = CACHED_DATA.data.S[idx] || 0;
702
- const V = CACHED_DATA.data.V[idx] || 0;
703
- const OI = CACHED_DATA.data.OI[idx] || 0;
704
-
705
- // Replace vars with values
706
- // Note: Replace strictly
707
- let exp = formula
708
- .replaceAll('$P', P)
709
- .replaceAll('$B', B)
710
- .replaceAll('$S', S)
711
- .replaceAll('$V', V)
712
- .replaceAll('$OI', OI);
713
-
714
- try { return new Function('return ' + exp)(); }
715
- catch (e) { return 0; }
716
- }
717
-
718
  </script>
719
  </body>
720
  </html>
 
2
  import requests
3
  import uvicorn
4
  import json
5
+ import asyncio
6
  from datetime import datetime, timedelta
7
  from fastapi import FastAPI, Body, HTTPException
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
  from typing import Optional, List, Dict, Any
11
 
12
  # ==========================================
13
  # 1. CONFIGURATION
14
  # ==========================================
15
+ # Supabase Configuration (Using provided keys)
16
  SB_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
17
  SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
18
 
 
28
  # ==========================================
29
  # 2. HELPER FUNCTIONS
30
  # ==========================================
31
+ def sb_get(endpoint: str, params: dict = None):
32
+ """Generic Supabase GET helper."""
33
  try:
34
  url = f"{SB_URL}/rest/v1{endpoint}"
35
  r = requests.get(url, headers=HEADERS, params=params)
36
  r.raise_for_status()
37
  return r.json()
38
  except Exception as e:
39
+ print(f"DB Error ({endpoint}): {e}")
40
  return []
41
 
42
  # ==========================================
43
+ # 3. API ROUTES - METADATA
44
  # ==========================================
45
 
46
  @app.get("/")
 
49
 
50
  @app.get("/api/dates")
51
  def get_dates():
52
+ """Fetch distinct dates available in history."""
53
+ # We limit to check recent 5000 records to find dates.
54
+ # For a full history scan, a specific RPC function in Supabase is better,
55
+ # but this works for standard usage.
56
+ data = sb_get("/history_buckets_upstox", {"select": "minute", "order": "minute.desc", "limit": "5000"})
57
+ dates = set(row['minute'].split('T')[0] for row in data)
 
 
 
 
 
58
  return JSONResponse(sorted(list(dates), reverse=True))
59
 
60
  @app.get("/api/roots")
61
  def get_roots(date: str):
62
+ """Fetch roots available for a specific date."""
63
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
64
+ # RPC or distinct select is ideal, but here we query range and set-ify
65
+ data = sb_get(f"/history_buckets_upstox?select=root&minute=gte.{start}&minute=lte.{end}")
66
+ roots = set(row['root'] for row in data)
 
 
 
 
 
 
 
67
  return JSONResponse(sorted(list(roots)))
68
 
69
  @app.get("/api/expiries")
70
  def get_expiries(date: str, root: str):
71
+ """Fetch expiries for a specific date and root."""
72
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
73
+ data = sb_get(f"/history_buckets_upstox?select=expiry&root=eq.{root}&minute=gte.{start}&minute=lte.{end}")
74
+ exps = set(row['expiry'] for row in data)
75
+ return JSONResponse(sorted(list(exps)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  @app.get("/api/instruments")
78
  def get_instruments(date: str, root: str, expiry: str):
79
+ """Fetch specific instrument keys (Symbol names) from the JSON data blob."""
80
+ start, end = f"{date}T00:00:00", f"{date}T23:59:59"
81
+ # We only need 1 record to see the keys structure for that day/root/expiry
82
+ res = sb_get(f"/history_buckets_upstox?select=data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{start}&minute=lte.{end}&limit=1")
83
+ instruments = res[0]['data'].keys() if res else []
84
+ return JSONResponse(sorted(list(instruments)))
85
+
86
+ # ==========================================
87
+ # 4. API ROUTES - DATA FETCHING
88
+ # ==========================================
 
 
 
 
 
 
 
89
 
90
+ @app.post("/api/fetch_series")
91
+ def fetch_series(
92
  date: str = Body(...),
93
  root: str = Body(...),
94
  expiry: str = Body(...),
95
  instrument: str = Body(...),
96
+ timeframe: str = Body(...),
97
  start_time: str = Body(...),
98
  end_time: str = Body(...)
99
  ):
100
+ """
101
+ Fetches raw 10s data, resamples to timeframe (Median), and returns base vectors.
102
+ """
103
  t_start = f"{date}T{start_time}:00"
104
  t_end = f"{date}T{end_time}:59"
105
+
106
+ # Supabase query
107
+ url = f"/history_buckets_upstox?select=minute,data&root=eq.{root}&expiry=eq.{expiry}&minute=gte.{t_start}&minute=lte.{t_end}&order=minute.asc"
108
+ rows = sb_get(url)
109
+
 
 
 
 
 
 
 
 
 
110
  records = []
111
 
112
+ # Process 10-second buckets
113
  for row in rows:
114
  try:
115
+ base_ts = datetime.fromisoformat(row['minute'])
116
  inst_data = row['data'].get(instrument)
117
 
118
  if not inst_data: continue
119
 
120
+ # Extract arrays (p=price, b=buy_qty, s=sell_qty, v=vol, o=oi)
121
+ # Upstox buckets usually have 6 slots (0s, 10s, 20s, 30s, 40s, 50s)
122
+ p_arr = inst_data.get('p', []) or []
123
+ b_arr = inst_data.get('b', []) or []
124
+ s_arr = inst_data.get('s', []) or []
125
+ v_arr = inst_data.get('v', []) or []
126
+ o_arr = inst_data.get('o', []) or []
127
+
 
128
  for i in range(6):
129
+ # Ensure we have price data at minimum to record a tick
130
+ if i < len(p_arr) and p_arr[i] is not None:
 
 
 
 
 
 
 
 
131
  records.append({
132
+ "ts": base_ts + timedelta(seconds=i*10),
133
+ "price": float(p_arr[i]),
134
+ "buy_qty": float(b_arr[i]) if i < len(b_arr) and b_arr[i] else 0.0,
135
+ "sell_qty": float(s_arr[i]) if i < len(s_arr) and s_arr[i] else 0.0,
136
+ "volume": float(v_arr[i]) if i < len(v_arr) and v_arr[i] else 0.0,
137
+ "oi": float(o_arr[i]) if i < len(o_arr) and o_arr[i] else 0.0
138
  })
139
+ except Exception:
140
  continue
141
 
142
  if not records:
143
+ return JSONResponse({"error": "No data found for this selection"})
144
 
 
145
  df = pd.DataFrame(records)
146
  df.set_index('ts', inplace=True)
147
+
148
+ # Timeframe Mapping
 
149
  tf_map = {
150
+ "1min": "1min", "3min": "3min", "5min": "5min",
151
+ "15min": "15min", "30min": "30min", "1hour": "1h"
152
  }
153
+ panda_tf = tf_map.get(timeframe, "1min")
 
 
 
154
 
155
+ # Resample using Median to smooth out noise/nulls
156
+ resampled = df.resample(panda_tf).median().dropna()
157
+
158
+ # Format for Frontend
159
+ response_data = {
160
  "labels": resampled.index.strftime('%H:%M').tolist(),
161
+ "P": resampled['price'].tolist(),
162
+ "B": resampled['buy_qty'].tolist(),
163
+ "S": resampled['sell_qty'].tolist(),
164
+ "V": resampled['volume'].tolist(),
165
+ "OI": resampled['oi'].tolist()
166
+ }
167
+
168
+ return JSONResponse(response_data)
169
+
170
 
171
  # ==========================================
172
+ # 5. FRONTEND TEMPLATE
173
  # ==========================================
174
+
175
  HTML_TEMPLATE = """
176
  <!DOCTYPE html>
177
  <html lang="en">
 
179
  <meta charset="UTF-8">
180
  <title>DepthChain Historical</title>
181
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
182
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
183
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
184
  <style>
185
+ /* --- THEME & RESET --- */
186
+ :root {
187
+ --bg-body: #f4f5f7;
188
+ --bg-card: #ffffff;
189
+ --text-primary: #172b4d;
190
+ --text-secondary: #5e6c84;
191
+ --border: #dfe1e6;
192
+ --accent: #0052cc;
193
+ --accent-hover: #0747a6;
194
+ --danger: #de350b;
195
+ --success: #00875a;
196
+ --font-main: 'Inter', sans-serif;
197
+ --font-mono: 'JetBrains Mono', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  }
199
+
200
+ * { box-sizing: border-box; margin: 0; padding: 0; }
201
 
202
+ body {
203
+ background-color: var(--bg-body);
204
+ color: var(--text-primary);
205
+ font-family: var(--font-main);
206
+ height: 100vh;
207
+ display: flex;
208
+ flex-direction: column;
209
+ overflow: hidden;
210
+ }
211
+
212
+ /* --- HEADER --- */
213
+ header {
214
+ background: var(--bg-card);
215
+ border-bottom: 1px solid var(--border);
216
+ height: 50px;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: space-between;
220
+ padding: 0 20px;
221
+ flex-shrink: 0;
222
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
223
+ }
224
+ .brand { font-weight: 700; font-size: 16px; display:flex; align-items:center; gap:8px;}
225
+ .brand span { color: var(--accent); background: #e6effc; padding: 2px 6px; border-radius: 4px; font-size: 11px; text-transform:uppercase;}
226
+
227
+ /* --- MAIN LAYOUT --- */
228
+ .main-container {
229
+ display: flex;
230
+ flex: 1;
231
+ overflow: hidden;
232
+ }
233
+
234
+ /* --- GLOBAL SIDEBAR (Date & Time) --- */
235
+ .global-sidebar {
236
+ width: 260px;
237
+ background: var(--bg-card);
238
+ border-right: 1px solid var(--border);
239
+ display: flex;
240
+ flex-direction: column;
241
+ padding: 15px;
242
+ gap: 15px;
243
+ z-index: 10;
244
+ }
245
+
246
  .section-title {
247
+ font-size: 11px;
248
+ font-weight: 700;
249
+ color: var(--text-secondary);
250
+ text-transform: uppercase;
251
+ letter-spacing: 0.5px;
252
+ margin-bottom: 5px;
253
  }
254
 
255
+ .control-group label {
256
+ display: block;
257
+ font-size: 12px;
258
+ font-weight: 600;
259
+ margin-bottom: 4px;
260
+ color: var(--text-primary);
261
  }
262
+
263
+ select, input {
264
+ width: 100%;
265
+ padding: 8px 10px;
266
+ border: 1px solid var(--border);
267
+ border-radius: 4px;
268
+ font-size: 13px;
269
+ font-family: var(--font-main);
270
+ background: #fafbfc;
271
+ color: var(--text-primary);
272
+ transition: all 0.2s;
273
  }
274
+ select:focus, input:focus {
275
+ outline: none;
276
+ border-color: var(--accent);
277
+ background: #fff;
278
+ box-shadow: 0 0 0 2px rgba(0,82,204,0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }
 
 
280
 
281
+ .btn-primary {
282
+ background: var(--accent);
283
+ color: white;
284
+ border: none;
285
+ padding: 10px;
286
+ border-radius: 4px;
287
+ font-weight: 600;
288
+ cursor: pointer;
289
+ width: 100%;
290
+ font-size: 13px;
291
+ margin-top: auto;
292
  }
293
+ .btn-primary:hover { background: var(--accent-hover); }
294
+
295
+ .btn-add-panel {
296
+ background: transparent;
297
+ border: 1px dashed var(--border);
298
+ color: var(--text-secondary);
299
+ padding: 8px;
300
+ width: 100%;
301
+ border-radius: 4px;
302
+ cursor: pointer;
303
+ font-weight: 600;
304
+ font-size: 12px;
305
+ }
306
+ .btn-add-panel:hover { background: #ebecf0; color: var(--text-primary); border-color: #c1c7d0; }
307
+
308
+ /* --- CHART AREA --- */
309
+ .charts-wrapper {
310
+ flex: 1;
311
+ overflow-y: auto;
312
+ padding: 20px;
313
+ display: flex;
314
+ flex-direction: column;
315
+ gap: 20px;
316
  }
 
317
 
318
+ .chart-card {
319
+ background: var(--bg-card);
320
+ border: 1px solid var(--border);
321
+ border-radius: 8px;
322
+ padding: 15px;
323
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
324
+ display: flex;
325
+ flex-direction: column;
326
+ min-height: 450px; /* Large charts */
327
+ position: relative;
328
+ }
329
+
330
+ .chart-header {
331
+ display: flex;
332
+ justify-content: space-between;
333
+ align-items: center;
334
+ margin-bottom: 10px;
335
+ border-bottom: 1px solid #f0f0f0;
336
+ padding-bottom: 10px;
337
+ }
338
+
339
+ .chart-title {
340
+ font-weight: 700;
341
+ font-size: 14px;
342
+ color: var(--text-primary);
343
  }
344
+
345
+ .chart-actions {
346
+ display: flex;
347
+ gap: 8px;
348
+ }
349
+
350
+ .icon-btn {
351
+ background: none;
352
+ border: 1px solid var(--border);
353
+ border-radius: 4px;
354
+ padding: 4px 8px;
355
+ cursor: pointer;
356
+ font-size: 11px;
357
+ font-weight: 600;
358
+ color: var(--text-secondary);
359
+ }
360
+ .icon-btn:hover { background: #ebecf0; color: var(--text-primary); }
361
+ .icon-btn.danger { color: var(--danger); border-color: #ffd2cc; }
362
+ .icon-btn.danger:hover { background: #ffebe6; }
363
+
364
+ .chart-canvas-container {
365
+ flex: 1;
366
  position: relative;
367
+ width: 100%;
368
+ min-height: 0; /* Flexbox trick */
369
  }
 
370
 
371
+ /* --- MODAL (Chart Config) --- */
372
+ .modal-overlay {
373
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
374
+ background: rgba(9, 30, 66, 0.54);
375
+ z-index: 100;
376
+ display: none;
377
+ justify-content: center;
378
+ align-items: center;
379
+ }
380
+ .modal-content {
381
+ background: white;
382
+ width: 600px;
383
+ max-width: 90%;
384
+ max-height: 85vh;
385
+ border-radius: 8px;
386
+ box-shadow: 0 8px 30px rgba(0,0,0,0.12);
387
+ display: flex;
388
  flex-direction: column;
389
+ overflow: hidden;
390
+ }
391
+ .modal-header {
392
+ padding: 15px 20px;
393
+ border-bottom: 1px solid var(--border);
394
+ font-weight: 700;
395
+ font-size: 15px;
396
+ display: flex; justify-content: space-between; align-items: center;
397
+ background: #fafbfc;
398
+ }
399
+ .modal-body {
400
+ padding: 20px;
401
+ overflow-y: auto;
402
+ }
403
+ .modal-footer {
404
+ padding: 15px 20px;
405
+ border-top: 1px solid var(--border);
406
+ display: flex; justify-content: flex-end; gap: 10px;
407
+ background: #fafbfc;
408
+ }
409
+
410
+ /* Config Grid */
411
+ .config-grid {
412
+ display: grid;
413
+ grid-template-columns: 1fr 1fr;
414
+ gap: 15px;
415
+ margin-bottom: 20px;
416
+ padding-bottom: 20px;
417
+ border-bottom: 1px solid var(--border);
418
+ }
419
+
420
+ /* Series List in Modal */
421
+ .series-list {
422
+ display: flex; flex-direction: column; gap: 10px;
423
  }
424
+ .series-row {
425
+ display: grid;
426
+ grid-template-columns: 40px 1fr 1fr 80px 30px;
427
+ gap: 10px;
428
+ align-items: center;
429
+ background: #f4f5f7;
430
+ padding: 8px;
431
+ border-radius: 4px;
432
+ }
433
+ .series-row input[type="color"] { padding: 0; height: 30px; border:none; cursor: pointer; }
434
+
435
+ .helper-text { font-size: 11px; color: var(--text-secondary); margin-top: 5px; font-family: var(--font-mono); }
436
+ .var-tag {
437
+ display: inline-block; background: #e6effc; color: var(--accent);
438
+ padding: 2px 5px; border-radius: 3px; cursor: pointer; font-family: var(--font-mono); font-size: 10px; margin-right: 4px;
439
  }
 
440
 
441
+ /* Loader */
442
+ .loader {
443
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
444
+ background: rgba(255,255,255,0.8);
445
+ z-index: 200;
446
+ display: flex; flex-direction: column;
447
+ justify-content: center; align-items: center;
448
+ font-weight: 700; color: var(--accent);
449
+ display: none;
450
+ }
451
  </style>
452
  </head>
453
  <body>
454
 
455
  <!-- LOADER -->
456
  <div class="loader" id="loader">
457
+ <div style="font-size: 24px;">FETCHING & COMPUTING...</div>
458
+ <div style="font-size: 12px; margin-top:10px; color:#666;">Processing high-resolution historical data</div>
459
  </div>
460
 
461
+ <!-- MODAL FOR CHART CONFIG -->
462
+ <div class="modal-overlay" id="configModal">
463
+ <div class="modal-content">
464
+ <div class="modal-header">
465
+ <span id="modalTitle">Configure Panel</span>
466
+ <span style="cursor:pointer; font-size:20px;" onclick="closeModal()">×</span>
467
+ </div>
468
+ <div class="modal-body">
469
+
470
+ <div class="section-title">Data Source (Specific to this Panel)</div>
471
+ <div class="config-grid">
472
+ <div class="control-group">
473
+ <label>Root Symbol</label>
474
+ <select id="cfgRoot" onchange="modalRootChanged()"></select>
475
+ </div>
476
+ <div class="control-group">
477
+ <label>Expiry</label>
478
+ <select id="cfgExp" onchange="modalExpChanged()" disabled></select>
479
+ </div>
480
+ <div class="control-group" style="grid-column: span 2;">
481
+ <label>Instrument</label>
482
+ <select id="cfgInst" disabled></select>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="section-title">Plots / Formulas</div>
487
+ <div style="margin-bottom: 10px;">
488
+ <span class="helper-text">Variables: </span>
489
+ <span class="var-tag" onclick="insertVar('$P')">$P (Price)</span>
490
+ <span class="var-tag" onclick="insertVar('$V')">$V (Vol)</span>
491
+ <span class="var-tag" onclick="insertVar('$OI')">$OI (Open Int)</span>
492
+ <span class="var-tag" onclick="insertVar('$B')">$B (Buy Qty)</span>
493
+ <span class="var-tag" onclick="insertVar('$S')">$S (Sell Qty)</span>
494
+ </div>
495
+
496
+ <div class="series-list" id="seriesList">
497
+ <!-- Dynamic Rows -->
498
+ </div>
499
+ <button class="btn-add-panel" style="margin-top:10px;" onclick="addSeriesRow()">+ Add Plot Line</button>
500
+
501
+ </div>
502
+ <div class="modal-footer">
503
+ <button class="icon-btn" onclick="closeModal()">Cancel</button>
504
+ <button class="btn-primary" style="width: auto;" onclick="saveConfig()">Save & Update</button>
505
+ </div>
506
  </div>
507
  </div>
508
 
509
+ <!-- TOP BAR -->
510
+ <header>
511
+ <div class="brand">DepthChain <span>Historical India</span></div>
512
+ <div style="font-size:12px; font-weight:600; color:var(--text-secondary);">
513
+ NO LOGIN REQUIRED • DATA VIEWER
514
+ </div>
515
+ </header>
516
+
517
+ <!-- MAIN CONTENT -->
518
+ <div class="main-container">
519
+
520
+ <!-- LEFT SIDEBAR: GLOBAL TIME SETTINGS -->
521
+ <div class="global-sidebar">
522
+ <div class="section-title">Global Time Settings</div>
523
 
524
+ <div class="control-group">
525
+ <label>Trade Date</label>
526
+ <select id="glDate" onchange="loadGlobalDate()"></select>
 
 
 
 
 
 
 
 
527
  </div>
528
+
529
+ <div class="control-group">
530
+ <label>Timeframe (Median)</label>
531
+ <select id="glTF">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  <option value="1min">1 Minute</option>
533
  <option value="3min">3 Minutes</option>
534
  <option value="5min">5 Minutes</option>
 
537
  <option value="1hour">1 Hour</option>
538
  </select>
539
  </div>
540
+
541
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px;">
542
+ <div class="control-group">
543
+ <label>Start</label>
544
+ <input type="time" id="glStart" value="09:15">
545
  </div>
546
+ <div class="control-group">
547
+ <label>End</label>
548
+ <input type="time" id="glEnd" value="15:30">
549
  </div>
550
  </div>
551
 
552
+ <div style="margin-top:20px; border-top: 1px solid var(--border); padding-top:20px;">
553
+ <div class="section-title">Dashboard Layout</div>
554
+ <button class="btn-add-panel" onclick="addNewPanel()">+ Add New Chart Panel</button>
 
555
  </div>
556
+
557
+ <button class="btn-primary" onclick="plotAll()">FETCH & PLOT ALL</button>
 
558
  </div>
559
 
560
+ <!-- RIGHT: CHARTS CONTAINER -->
561
+ <div class="charts-wrapper" id="chartsContainer">
562
+ <!-- Dynamic Charts -->
563
+ <div style="text-align:center; color:#888; margin-top:100px;">
564
+ <h3>Welcome to DepthChain Historical</h3>
565
+ <p>Add a panel or click "Fetch" to begin.</p>
566
  </div>
567
  </div>
568
+
569
  </div>
570
 
571
  <script>
572
  // --- STATE ---
573
+ let PANELS = []; // Stores config for each panel
574
+ let CURRENT_EDIT_ID = null; // ID of panel currently being edited in modal
575
+ let CHARTS_INSTANCES = {}; // Stores Chart.js instances
576
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  // --- INIT ---
578
  window.onload = async () => {
579
+ // 1. Fetch Dates
580
+ const res = await fetch('/api/dates');
581
+ const dates = await res.json();
582
+ const dSel = document.getElementById('glDate');
583
+ dSel.innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
584
+
585
+ // 2. Add Default Panel
586
+ if(dates.length > 0) {
587
+ addNewPanel();
588
+ // Trigger root load for modal internal usage
589
+ loadGlobalDate();
590
+ }
591
  };
592
 
593
+ function loadGlobalDate() {
594
+ // When date changes, we might want to clear caches or just let user re-plot
595
+ console.log("Date Changed");
 
 
 
 
 
 
 
596
  }
597
 
598
+ // --- PANEL MANAGEMENT ---
599
+ function addNewPanel() {
600
+ const id = Date.now();
601
+ PANELS.push({
602
+ id: id,
603
+ // Default Config
604
+ root: '',
605
+ expiry: '',
606
+ instrument: '',
607
+ series: [
608
+ { formula: '$P', color: '#0052cc', label: 'Price', axis: 'left' }
609
+ ]
610
+ });
611
+ renderDashboard();
612
+ // Automatically open settings for new panel to force selection
613
+ openSettings(id);
 
 
 
 
 
 
 
 
 
614
  }
615
 
616
+ function deletePanel(id) {
617
+ if(!confirm("Remove this chart?")) return;
618
+ PANELS = PANELS.filter(p => p.id !== id);
619
+ if(CHARTS_INSTANCES[id]) {
620
+ CHARTS_INSTANCES[id].destroy();
621
+ delete CHARTS_INSTANCES[id];
622
+ }
623
+ renderDashboard();
 
 
 
 
624
  }
625
 
626
+ function renderDashboard() {
627
+ const container = document.getElementById('chartsContainer');
 
 
628
 
629
+ // We only append new ones or remove old ones in a real framework (React/Vue).
630
+ // Here, simplistic re-render of container structure, but we keep canvas references if possible?
631
+ // To keep it simple: We will re-render HTML structure, but we need to re-bind charts.
632
+
633
+ // Save existing chart canvases if needed? No, let's rebuild.
634
+ container.innerHTML = PANELS.map((p, idx) => `
635
+ <div class="chart-card" id="card-${p.id}">
636
+ <div class="chart-header">
637
+ <div class="chart-title">
638
+ PANEL ${idx+1}: <span style="color:var(--accent);">${p.instrument || 'Not Configured'}</span>
639
+ </div>
640
+ <div class="chart-actions">
641
+ <button class="icon-btn" onclick="openSettings(${p.id})">⚙ SETTINGS</button>
642
+ <button class="icon-btn danger" onclick="deletePanel(${p.id})">✖</button>
 
 
 
 
 
643
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  </div>
645
+ <div class="chart-canvas-container">
646
+ <canvas id="canvas-${p.id}"></canvas>
 
647
  </div>
648
+ </div>
649
+ `).join('');
650
+
651
+ // Restore charts if data exists (this happens after "Fetch" usually)
652
+ // If we just added a panel, it's empty.
653
  }
654
 
655
+ // --- MODAL & CONFIGURATION ---
656
+ async function openSettings(panelId) {
657
+ CURRENT_EDIT_ID = panelId;
658
+ const panel = PANELS.find(p => p.id === panelId);
659
+ const modal = document.getElementById('configModal');
660
+ const glDate = document.getElementById('glDate').value;
661
+
662
+ // 1. Populate Roots
663
+ const rSel = document.getElementById('cfgRoot');
664
+ rSel.innerHTML = '<option>Loading...</option>';
665
+ modal.style.display = 'flex';
666
+
667
+ // Fetch Roots for current date
668
+ const res = await fetch(`/api/roots?date=${glDate}`);
669
+ const roots = await res.json();
670
+ rSel.innerHTML = `<option value="">-- Select Root --</option>` + roots.map(r => `<option>${r}</option>`).join('');
671
+
672
+ // Set existing values if present
673
+ if(panel.root && roots.includes(panel.root)) {
674
+ rSel.value = panel.root;
675
+ await modalRootChanged(panel.expiry, panel.instrument);
676
  } else {
677
+ document.getElementById('cfgExp').innerHTML = ''; document.getElementById('cfgExp').disabled=true;
678
+ document.getElementById('cfgInst').innerHTML = ''; document.getElementById('cfgInst').disabled=true;
679
  }
680
+
681
+ // Render Series Rows
682
+ renderSeriesRows(panel.series);
683
  }
 
684
 
685
+ async function modalRootChanged(preSelectExp=null, preSelectInst=null) {
686
+ const date = document.getElementById('glDate').value;
687
+ const root = document.getElementById('cfgRoot').value;
688
+ const eSel = document.getElementById('cfgExp');
689
+
690
+ eSel.innerHTML = '<option>Loading...</option>'; eSel.disabled = true;
691
+
692
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
693
+ const exps = await res.json();
694
+
695
+ eSel.innerHTML = `<option value="">-- Select Expiry --</option>` + exps.map(e => `<option>${e}</option>`).join('');
696
+ eSel.disabled = false;
697
 
698
+ if(preSelectExp && exps.includes(preSelectExp)) {
699
+ eSel.value = preSelectExp;
700
+ await modalExpChanged(preSelectInst);
701
+ }
702
+ }
703
 
704
+ async function modalExpChanged(preSelectInst=null) {
705
+ const date = document.getElementById('glDate').value;
706
+ const root = document.getElementById('cfgRoot').value;
707
+ const exp = document.getElementById('cfgExp').value;
708
+ const iSel = document.getElementById('cfgInst');
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
+ iSel.innerHTML = '<option>Loading...</option>'; iSel.disabled = true;
711
+
712
+ const res = await fetch(`/api/instruments?date=${date}&root=${root}&expiry=${exp}`);
713
+ const insts = await res.json();
 
714
 
715
+ iSel.innerHTML = `<option value="">-- Select Instrument --</option>` + insts.map(i => `<option>${i}</option>`).join('');
716
+ iSel.disabled = false;
717
 
718
+ if(preSelectInst && insts.includes(preSelectInst)) {
719
+ iSel.value = preSelectInst;
 
 
 
720
  }
721
  }
722
 
723
+ function renderSeriesRows(seriesData) {
724
+ const list = document.getElementById('seriesList');
725
+ list.innerHTML = '';
726
+ seriesData.forEach((s, idx) => {
727
+ const div = document.createElement('div');
728
+ div.className = 'series-row';
729
+ div.innerHTML = `
730
+ <input type="color" value="${s.color}" class="s-color">
731
+ <input type="text" value="${s.formula}" placeholder="Formula" class="s-formula">
732
+ <input type="text" value="${s.label || s.formula}" placeholder="Legend Label" class="s-label">
733
+ <select class="s-axis">
734
+ <option value="left" ${s.axis==='left'?'selected':''}>Left Axis</option>
735
+ <option value="right" ${s.axis==='right'?'selected':''}>Right Axis</option>
736
+ </select>
737
+ <span style="color:red; font-weight:bold; cursor:pointer;" onclick="this.parentElement.remove()">×</span>
738
+ `;
739
+ list.appendChild(div);
740
+ });
741
+ }
742
+
743
+ function addSeriesRow() {
744
+ const list = document.getElementById('seriesList');
745
+ const div = document.createElement('div');
746
+ div.className = 'series-row';
747
+ div.innerHTML = `
748
+ <input type="color" value="#000000" class="s-color">
749
+ <input type="text" value="$P" class="s-formula">
750
+ <input type="text" value="Price" class="s-label">
751
+ <select class="s-axis">
752
+ <option value="left">Left Axis</option>
753
+ <option value="right">Right Axis</option>
754
+ </select>
755
+ <span style="color:red; font-weight:bold; cursor:pointer;" onclick="this.parentElement.remove()">×</span>
756
+ `;
757
+ list.appendChild(div);
758
+ }
759
+
760
+ function insertVar(v) {
761
+ // Crude insert into last focused input could be complex.
762
+ // For now, prompt user or just alert.
763
+ // Better: append to all empty inputs or just alert logic
764
+ alert("Type " + v + " in the formula box.");
765
+ }
766
+
767
+ function saveConfig() {
768
+ if(!CURRENT_EDIT_ID) return;
769
+
770
+ // Get Form Data
771
+ const root = document.getElementById('cfgRoot').value;
772
+ const exp = document.getElementById('cfgExp').value;
773
+ const inst = document.getElementById('cfgInst').value;
774
+
775
+ if(!root || !exp || !inst) {
776
+ alert("Please select Root, Expiry and Instrument.");
777
+ return;
778
+ }
779
+
780
+ // Get Series Data
781
+ const rows = document.querySelectorAll('.series-row');
782
+ const newSeries = [];
783
+ rows.forEach(r => {
784
+ newSeries.push({
785
+ color: r.querySelector('.s-color').value,
786
+ formula: r.querySelector('.s-formula').value,
787
+ label: r.querySelector('.s-label').value,
788
+ axis: r.querySelector('.s-axis').value
789
  });
790
+ });
791
+
792
+ // Update Global State
793
+ const pIdx = PANELS.findIndex(p => p.id === CURRENT_EDIT_ID);
794
+ PANELS[pIdx].root = root;
795
+ PANELS[pIdx].expiry = exp;
796
+ PANELS[pIdx].instrument = inst;
797
+ PANELS[pIdx].series = newSeries;
798
+
799
+ closeModal();
800
+ renderDashboard(); // Re-render headers
801
+
802
+ // Optionally auto-plot this specific chart if Global Date matches?
803
+ // Better to wait for user to click Fetch All to avoid partial state.
804
+ }
805
+
806
+ function closeModal() {
807
+ document.getElementById('configModal').style.display = 'none';
808
+ CURRENT_EDIT_ID = null;
809
+ }
810
+
811
+ // --- DATA FETCHING AND PLOTTING ---
812
+ async function plotAll() {
813
+ document.getElementById('loader').style.display = 'flex';
814
+
815
+ // Global Settings
816
+ const date = document.getElementById('glDate').value;
817
+ const tf = document.getElementById('glTF').value;
818
+ const start = document.getElementById('glStart').value;
819
+ const end = document.getElementById('glEnd').value;
820
+
821
+ // Process each panel
822
+ for(let panel of PANELS) {
823
+ if(!panel.instrument) continue; // Skip unconfigured
824
+
825
+ try {
826
+ // 1. Fetch Data
827
+ const res = await fetch('/api/fetch_series', {
828
+ method: 'POST',
829
+ headers: {'Content-Type': 'application/json'},
830
+ body: JSON.stringify({
831
+ date: date,
832
+ root: panel.root,
833
+ expiry: panel.expiry,
834
+ instrument: panel.instrument,
835
+ timeframe: tf,
836
+ start_time: start,
837
+ end_time: end
838
+ })
839
+ });
840
+
841
+ const data = await res.json();
842
+
843
+ if(data.error) {
844
+ console.warn(`Panel ${panel.id} error: ${data.error}`);
845
+ continue;
846
+ }
847
+
848
+ // 2. Compute Formulas
849
+ const datasets = panel.series.map(s => {
850
+ const computedData = data.labels.map((_, i) => {
851
+ const P = data.P[i] || 0;
852
+ const V = data.V[i] || 0;
853
+ const OI = data.OI[i] || 0;
854
+ const B = data.B[i] || 0;
855
+ const S = data.S[i] || 0;
856
+
857
+ // Eval
858
+ let formulaStr = s.formula
859
+ .replace(/\$OI/g, OI)
860
+ .replace(/\$P/g, P)
861
+ .replace(/\$V/g, V)
862
+ .replace(/\$B/g, B)
863
+ .replace(/\$S/g, S);
864
+
865
+ try { return new Function('return ' + formulaStr)(); }
866
+ catch { return null; }
867
+ });
868
+
869
+ return {
870
+ label: s.label,
871
+ data: computedData,
872
+ borderColor: s.color,
873
+ backgroundColor: s.color,
874
+ borderWidth: 2,
875
+ pointRadius: 0,
876
+ pointHoverRadius: 6,
877
+ yAxisID: s.axis === 'left' ? 'y' : 'y1',
878
+ tension: 0.1
879
+ };
880
+ });
881
+
882
+ // 3. Draw Chart
883
+ drawChart(panel.id, data.labels, datasets);
884
+
885
+ } catch(e) {
886
+ console.error("Plot Error", e);
887
+ }
888
+ }
889
+
890
+ document.getElementById('loader').style.display = 'none';
891
+ }
892
 
893
+ function drawChart(id, labels, datasets) {
894
+ const ctx = document.getElementById(`canvas-${id}`).getContext('2d');
895
+
896
+ // Destroy old if exists
897
+ if(CHARTS_INSTANCES[id]) CHARTS_INSTANCES[id].destroy();
898
+
899
+ // Check if Y1 is needed
900
+ const hasRightAxis = datasets.some(d => d.yAxisID === 'y1');
901
+
902
+ CHARTS_INSTANCES[id] = new Chart(ctx, {
903
+ type: 'line',
904
+ data: { labels, datasets },
905
+ options: {
906
+ responsive: true,
907
+ maintainAspectRatio: false,
908
+ interaction: { mode: 'index', intersect: false },
909
+ plugins: {
910
+ legend: { display: true, position: 'top' },
911
+ tooltip: {
912
+ mode: 'index',
913
+ intersect: false,
914
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
915
+ titleColor: '#000',
916
+ bodyColor: '#000',
917
+ borderColor: '#ccc',
918
+ borderWidth: 1
919
+ }
920
  },
921
+ scales: {
922
+ x: {
923
+ grid: { display: false },
924
+ ticks: { maxTicksLimit: 10, maxRotation: 0 }
 
 
 
925
  },
926
+ y: {
927
+ type: 'linear', display: true, position: 'left',
928
+ grid: { color: '#f0f0f0' },
929
+ title: { display: true, text: 'Left Axis' }
930
+ },
931
+ y1: {
932
+ type: 'linear', display: hasRightAxis, position: 'right',
933
+ grid: { display: false },
934
+ title: { display: hasRightAxis, text: 'Right Axis' }
 
 
 
 
 
 
935
  }
936
  }
937
+ }
 
938
  });
939
  }
940
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
941
  </script>
942
  </body>
943
  </html>