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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +546 -685
app.py CHANGED
@@ -4,15 +4,13 @@ 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,832 +24,695 @@ HEADERS = {
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>
 
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
 
 
24
  app = FastAPI()
25
 
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("/")
44
  def home():
45
+ return HTMLResponse(HTML_TEMPLATE)
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">
225
  <head>
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>
400
+ <option value="15min">15 Minutes</option>
401
+ <option value="30min">30 Minutes</option>
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>