topsecrettraders commited on
Commit
48f4008
·
verified ·
1 Parent(s): 70bf9d5

Create app.py v1

Browse files
Files changed (1) hide show
  1. app.py v1 +754 -0
app.py v1 ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import requests
4
+ import uvicorn
5
+ import asyncio
6
+ import re
7
+ import uuid
8
+ import json
9
+ import io
10
+ from datetime import timedelta
11
+ from fastapi import FastAPI, Body, HTTPException
12
+ from fastapi.responses import HTMLResponse, JSONResponse
13
+ from huggingface_hub import HfApi, hf_hub_download
14
+ from typing import Optional, List
15
+
16
+ # ==========================================
17
+ # 1. CONFIGURATION & SECRETS
18
+ # ==========================================
19
+ SB_URL = "https://ivyyfhymhdheykpntozd.supabase.co"
20
+ SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml2eXlmaHltaGRoZXlrcG50b3pkIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4Mzg5NzAwOCwiZXhwIjoyMDk5NDczMDA4fQ.cHvePFN3NML8Zv12D2vV91MigJGRerZvD_Cevbu1AFk"
21
+ HEADERS = {
22
+ "apikey": SB_KEY,
23
+ "Authorization": f"Bearer {SB_KEY}",
24
+ "Content-Type": "application/json",
25
+ "Prefer": "return=representation"
26
+ }
27
+
28
+ HF_TOKEN_PART_1 = "hf_EzaiwOHiejWFLN"
29
+ HF_TOKEN_PART_2 = "otvQqTPUTksQQfYxrbJg"
30
+ HF_TOKEN = HF_TOKEN_PART_1 + HF_TOKEN_PART_2
31
+ HF_REPO_ID = "topsecrettraders/DepthChain"
32
+
33
+ hf_api = HfApi(token=HF_TOKEN)
34
+ app = FastAPI()
35
+
36
+ # IN-MEMORY CACHE (Replaces local disk storage to prevent HF Space File Deletion issues)
37
+ SESSION_CACHE = {}
38
+
39
+ # ==========================================
40
+ # 2. SUPABASE HELPER FUNCTIONS
41
+ # ==========================================
42
+ def sb_rpc(func_name: str, params: dict = None):
43
+ if params is None: params = {}
44
+ try:
45
+ url = f"{SB_URL}/rest/v1/rpc/{func_name}"
46
+ r = requests.post(url, headers=HEADERS, json=params)
47
+ r.raise_for_status()
48
+ return r.json()
49
+ except Exception as e:
50
+ print(f"DB Error RPC ({func_name}): {e}")
51
+ return[]
52
+
53
+ # ==========================================
54
+ # 3. API ROUTES - METADATA & UI
55
+ # ==========================================
56
+ @app.get("/")
57
+ def home():
58
+ return HTMLResponse(HTML_TEMPLATE)
59
+
60
+ @app.get("/api/dates")
61
+ def get_dates():
62
+ data = sb_rpc("get_available_dates", {"p_limit": 50})
63
+ return JSONResponse([row['available_date'] for row in data])
64
+
65
+ @app.get("/api/roots")
66
+ def get_roots(date: str):
67
+ data = sb_rpc("get_roots_for_date", {"p_date": date})
68
+ return JSONResponse([row['root_name'] for row in data])
69
+
70
+ @app.get("/api/expiries")
71
+ def get_expiries(date: str, root: str):
72
+ data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root.upper()})
73
+ return JSONResponse([row['expiry_date'] for row in data])
74
+
75
+ # ==========================================
76
+ # 4. HUGGING FACE EXPLORER API
77
+ # ==========================================
78
+ @app.get("/api/hf_files")
79
+ def list_hf_files():
80
+ try:
81
+ files = hf_api.list_repo_files(repo_id=HF_REPO_ID, repo_type="dataset")
82
+ folders = set()
83
+ for f in files:
84
+ if f.startswith("Market Data/") and f.endswith("data.parquet"):
85
+ folders.add(f.replace("/data.parquet", ""))
86
+ return JSONResponse({"status": "success", "folders": sorted(list(folders), reverse=True)})
87
+ except Exception as e:
88
+ return JSONResponse({"status": "error", "message": str(e)})
89
+
90
+ @app.get("/api/hf_metadata")
91
+ def get_hf_metadata(folder_path: str):
92
+ try:
93
+ meta_path = f"{folder_path}/metadata.json"
94
+ local_path = hf_hub_download(repo_id=HF_REPO_ID, filename=meta_path, repo_type="dataset", token=HF_TOKEN)
95
+ with open(local_path, "r") as f:
96
+ metadata = json.load(f)
97
+ return JSONResponse({"status": "success", "metadata": metadata})
98
+ except Exception as e:
99
+ return JSONResponse({"status": "error", "message": "Metadata not found or error fetching."})
100
+
101
+ @app.get("/api/view_parquet")
102
+ def view_parquet(folder_path: str):
103
+ try:
104
+ file_path = f"{folder_path}/data.parquet"
105
+ local_path = hf_hub_download(repo_id=HF_REPO_ID, filename=file_path, repo_type="dataset", token=HF_TOKEN)
106
+ df = pd.read_parquet(local_path)
107
+ return JSONResponse({"status": "success", "columns": df.columns.tolist(), "data": df.to_dict(orient="records")})
108
+ except Exception as e:
109
+ return JSONResponse({"status": "error", "message": str(e)})
110
+
111
+ # ==========================================
112
+ # 5. STEP 1: FETCH & INSPECT
113
+ # ==========================================
114
+ @app.post("/api/inspect_data")
115
+ async def inspect_data(
116
+ date: str = Body(...),
117
+ root: str = Body(...),
118
+ expiry: str = Body(...),
119
+ start_time: str = Body("09:15"),
120
+ end_time: str = Body("15:30"),
121
+ atm_range: int = Body(10),
122
+ required_vars: List[str] = Body(["P", "CB", "PS", "CS", "PB"])
123
+ ):
124
+ root = root.upper()
125
+ # STRICT BOUNDS: Exactly the minute specified, no bleeding into trailing seconds.
126
+ t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
127
+ t_end_dt = pd.to_datetime(f"{date} {end_time}:00")
128
+
129
+ inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
130
+ instruments = [r['instrument_name'] for r in inst_data]
131
+
132
+ if not instruments:
133
+ return JSONResponse({"error": f"No data found in Supabase for {root} {expiry} on {date}"})
134
+
135
+ spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
136
+ fut_keys =[k for k in instruments if 'FUT' in k]
137
+ ref_instrument = spot_keys[0] if spot_keys else (fut_keys[0] if fut_keys else instruments[0])
138
+
139
+ strike_map = {}
140
+ for inst in instruments:
141
+ if inst.endswith('CE') or inst.endswith('PE'):
142
+ clean_inst = inst.split(":")[-1]
143
+ if clean_inst.startswith(root):
144
+ match = re.search(r'(\d+(?:\.\d+)?)(?:CE|PE)$', clean_inst[len(root)+5:])
145
+ if match:
146
+ strike_map[inst] = float(match.group(1))
147
+ continue
148
+ matches = re.findall(r'(\d+(?:\.\d+)?)(?=CE$|PE$)', clean_inst)
149
+ if matches: strike_map[inst] = float(matches[-1])
150
+
151
+ base_params = {
152
+ "p_root": root, "p_expiry": expiry, "p_ref_instrument": ref_instrument,
153
+ "p_atm_range": atm_range, "p_strike_map": strike_map
154
+ }
155
+
156
+ async def fetch_chunk(start_dt, end_dt, retries=3):
157
+ params = base_params.copy()
158
+ params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
159
+ params["p_end_time"] = end_dt.strftime("%Y-%m-%d %H:%M:%S")
160
+ loop = asyncio.get_event_loop()
161
+ for attempt in range(retries):
162
+ try:
163
+ return await loop.run_in_executor(None, sb_rpc, "export_advanced_10s_data", params)
164
+ except Exception as e:
165
+ if attempt == retries - 1: return[]
166
+ await asyncio.sleep(1)
167
+
168
+ tasks =[]
169
+ curr = t_start_dt
170
+ while curr <= t_end_dt:
171
+ nxt = curr + timedelta(hours=1) - timedelta(seconds=1)
172
+ if nxt > t_end_dt: nxt = t_end_dt
173
+ tasks.append(fetch_chunk(curr, nxt))
174
+ curr += timedelta(hours=1)
175
+
176
+ chunk_results = await asyncio.gather(*tasks)
177
+
178
+ all_records =[]
179
+ for res in chunk_results:
180
+ if isinstance(res, list): all_records.extend(res)
181
+
182
+ if not all_records:
183
+ return JSONResponse({"error": "No valid records fetched. Data might be empty for this date/time."})
184
+
185
+ df = pd.DataFrame(all_records)
186
+ df.rename(columns={"tick_ts": "timestamp", "price": "P", "cb": "CB", "cs": "CS", "pb": "PB", "ps": "PS", "b_qty": "B", "s_qty": "S", "vol": "V", "oi": "OI"}, inplace=True)
187
+ df['timestamp'] = pd.to_datetime(df['timestamp'])
188
+ df.drop_duplicates(subset=['timestamp'], inplace=True)
189
+ df.set_index('timestamp', inplace=True)
190
+ df.sort_index(inplace=True)
191
+
192
+ # 1. STRICT FILTER: Remove anything outside user bounds that the chunking may have dragged in
193
+ df = df.loc[(df.index >= t_start_dt) & (df.index <= t_end_dt)]
194
+ if df.empty:
195
+ return JSONResponse({"error": "Data exists, but completely outside your specified Start/End Time."})
196
+
197
+ available_vars =[v for v in required_vars if v in df.columns]
198
+ df = df[available_vars].copy()
199
+
200
+ # 2. DYNAMIC INDEX: Only pad between the actual bounds of fetched data.
201
+ # No more faking empty timestamps before the broker actually started sending data.
202
+ actual_min = df.index.min()
203
+ actual_max = df.index.max()
204
+
205
+ full_idx = pd.date_range(start=actual_min, end=actual_max, freq='10s')
206
+
207
+ raw_ticks = len(df)
208
+ expected_ticks = len(full_idx)
209
+ missing_ts = full_idx.difference(df.index).to_pydatetime()
210
+
211
+ missing_blocks =[]
212
+ if len(missing_ts) > 0:
213
+ start_block = missing_ts[0]
214
+ prev = missing_ts[0]
215
+ for ts in missing_ts[1:]:
216
+ if (ts - prev).total_seconds() > 10.5:
217
+ missing_blocks.append({
218
+ "start": start_block.strftime('%Y-%m-%d %H:%M:%S'),
219
+ "end": prev.strftime('%Y-%m-%d %H:%M:%S'),
220
+ "count": int((prev - start_block).total_seconds()/10) + 1
221
+ })
222
+ start_block = ts
223
+ prev = ts
224
+ missing_blocks.append({
225
+ "start": start_block.strftime('%Y-%m-%d %H:%M:%S'),
226
+ "end": prev.strftime('%Y-%m-%d %H:%M:%S'),
227
+ "count": int((prev - start_block).total_seconds()/10) + 1
228
+ })
229
+
230
+ # For the UI, we reindex to correctly identify all upcoming NaNs
231
+ df_reindexed = df.reindex(full_idx)
232
+
233
+ null_details_existing = {}
234
+ for col in available_vars:
235
+ # We flag 0.0 as an error because Flow/Volume/Price are cumulative and should never drop to 0 mid-day.
236
+ na_times = df_reindexed[df_reindexed[col].isna() | (df_reindexed[col] == 0)].index.strftime('%H:%M:%S').tolist()
237
+ null_details_existing[col] = {"count": len(na_times), "times": na_times[:50]}
238
+
239
+ # Cache original df (not the padded one, so trim operations work natively)
240
+ session_id = str(uuid.uuid4())
241
+ SESSION_CACHE[session_id] = df.copy()
242
+
243
+ return JSONResponse({
244
+ "status": "success",
245
+ "session_id": session_id,
246
+ "stats": {
247
+ "expected_ticks": expected_ticks,
248
+ "found_ticks": raw_ticks,
249
+ "missing_time_ticks": len(missing_ts),
250
+ "missing_blocks": missing_blocks,
251
+ "null_breakdown": null_details_existing,
252
+ "variables": available_vars,
253
+ "original_start": actual_min.strftime('%Y-%m-%d %H:%M:%S'),
254
+ "original_end": actual_max.strftime('%Y-%m-%d %H:%M:%S')
255
+ },
256
+ "meta": {
257
+ "date": date, "root": root, "expiry": expiry,
258
+ "start": start_time, "end": end_time, "atm_range": atm_range
259
+ }
260
+ })
261
+
262
+ # ==========================================
263
+ # 6. STEP 2: CLEAN & ARCHIVE
264
+ # ==========================================
265
+ @app.post("/api/clean_and_upload")
266
+ async def clean_and_upload(
267
+ session_id: str = Body(...),
268
+ fill_method: str = Body("ffill"),
269
+ trim_start: str = Body(None),
270
+ trim_end: str = Body(None),
271
+ metadata: dict = Body(...)
272
+ ):
273
+ if session_id not in SESSION_CACHE:
274
+ return JSONResponse({"error": "Session expired or invalid. Please Fetch & Inspect again."})
275
+
276
+ df = SESSION_CACHE[session_id].copy()
277
+
278
+ # 1. Apply Trimming boundaries
279
+ t_start = pd.to_datetime(trim_start) if trim_start else df.index.min()
280
+ t_end = pd.to_datetime(trim_end) if trim_end else df.index.max()
281
+
282
+ df = df.loc[(df.index >= t_start) & (df.index <= t_end)]
283
+
284
+ # 2. Enforce Strict 10-Second Indexing on the trimmed range
285
+ full_idx = pd.date_range(start=t_start, end=t_end, freq='10s')
286
+ df = df.reindex(full_idx)
287
+
288
+ # 3. Replace 0.0 with NaN! (Crucial so FFill actually fixes the glitches)
289
+ df.replace(0, np.nan, inplace=True)
290
+
291
+ # 4. Apply Imputation Strategy
292
+ if fill_method == "ffill":
293
+ df_clean = df.ffill().fillna(0)
294
+ elif fill_method == "interpolate":
295
+ df_clean = df.interpolate(method='linear').fillna(0)
296
+ elif fill_method == "zero":
297
+ df_clean = df.fillna(0)
298
+ else:
299
+ df_clean = df.ffill().fillna(0)
300
+
301
+ df_clean.reset_index(inplace=True)
302
+ df_clean.rename(columns={"index": "timestamp"}, inplace=True)
303
+ df_clean['timestamp'] = df_clean['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
304
+
305
+ metadata["imputation_applied"] = fill_method
306
+ metadata["trim_start_applied"] = trim_start
307
+ metadata["trim_end_applied"] = trim_end
308
+ metadata["total_final_rows"] = len(df_clean)
309
+
310
+ date = metadata["date"]
311
+ root = metadata["root"]
312
+ expiry = metadata["expiry"]
313
+ hf_folder = f"Market Data/{date}/{root}/{expiry}"
314
+
315
+ try:
316
+ parquet_buffer = io.BytesIO()
317
+ df_clean.to_parquet(parquet_buffer, index=False)
318
+ parquet_buffer.seek(0)
319
+
320
+ json_buffer = io.BytesIO(json.dumps(metadata, indent=4).encode('utf-8'))
321
+ json_buffer.seek(0)
322
+
323
+ hf_api.upload_file(path_or_fileobj=parquet_buffer, path_in_repo=f"{hf_folder}/data.parquet", repo_id=HF_REPO_ID, repo_type="dataset")
324
+ hf_api.upload_file(path_or_fileobj=json_buffer, path_in_repo=f"{hf_folder}/metadata.json", repo_id=HF_REPO_ID, repo_type="dataset")
325
+
326
+ del SESSION_CACHE[session_id]
327
+
328
+ return JSONResponse({"status": "success", "message": "Cleaned data & Metadata archived to Hugging Face!"})
329
+ except Exception as e:
330
+ return JSONResponse({"error": f"Upload Failed: {str(e)}"})
331
+
332
+
333
+ # ==========================================
334
+ # 7. FRONTEND TEMPLATE
335
+ # ==========================================
336
+ HTML_TEMPLATE = """
337
+ <!DOCTYPE html>
338
+ <html lang="en">
339
+ <head>
340
+ <meta charset="UTF-8">
341
+ <title>DepthChain Data Engineering</title>
342
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
343
+ <style>
344
+ :root { --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #e6edf3; --accent: #2f81f7; --success: #238636; --danger: #f85149; --warn: #d29922; }
345
+ body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; margin: 0; padding: 20px; font-size: 13px; }
346
+ .grid { display: grid; grid-template-columns: 1fr 1.2fr; gap: 20px; max-width: 1300px; margin: 0 auto; }
347
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 20px; display: flex; flex-direction: column; }
348
+ h2 { margin-top: 0; font-size: 15px; border-bottom: 1px solid var(--border); padding-bottom: 10px; color: var(--accent); }
349
+ label { display: block; margin-bottom: 6px; color: #8b949e; font-size: 11px; font-weight: 600; text-transform: uppercase; }
350
+ select, input { width: 100%; padding: 8px 12px; margin-bottom: 12px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; font-size: 13px; box-sizing: border-box; }
351
+ button { background: var(--accent); color: white; border: none; padding: 10px; border-radius: 6px; font-weight: bold; cursor: pointer; transition: 0.2s; width: 100%; }
352
+ button:hover { background: #58a6ff; }
353
+ button:disabled { background: var(--border); color: #8b949e; cursor: not-allowed; }
354
+ .btn-success { background: var(--success); }
355
+ .btn-success:hover { background: #2ea043; }
356
+ .btn-warn { background: transparent; border: 1px solid var(--warn); color: var(--warn); padding: 4px 8px; font-size: 11px; border-radius: 4px; width: auto; }
357
+ .btn-warn:hover { background: rgba(210, 153, 34, 0.2); }
358
+
359
+ .row { display: flex; gap: 10px; }
360
+ .col { flex: 1; }
361
+
362
+ .stat-box { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 15px; margin-bottom: 15px; }
363
+ .stat-item { display: flex; justify-content: space-between; margin-bottom: 8px; border-bottom: 1px dashed var(--border); padding-bottom: 4px; align-items: center; }
364
+ .stat-val { font-family: monospace; font-size: 13px; font-weight: bold; }
365
+ .val-good { color: var(--success); }
366
+ .val-bad { color: var(--danger); }
367
+ .val-warn { color: var(--warn); }
368
+
369
+ .hf-list { flex: 1; overflow-y: auto; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; max-height: 400px; }
370
+ .hf-item { padding: 8px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
371
+ .hf-item:hover { background: #21262d; }
372
+ .hf-btns { display: flex; gap: 6px; }
373
+ .hf-btn { cursor: pointer; font-size: 11px; padding: 4px 8px; background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: 4px; }
374
+ .hf-btn:hover { border-color: var(--accent); color: var(--accent); }
375
+
376
+ .toast { position: fixed; bottom: 20px; right: 20px; background: var(--success); color: white; padding: 12px 20px; border-radius: 6px; display: none; box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 9999; }
377
+
378
+ details { margin-bottom: 5px; font-size: 12px; }
379
+ summary { cursor: pointer; color: var(--accent); font-weight: bold; margin-bottom: 5px; outline: none; }
380
+ .inspect-times { font-family: monospace; font-size: 10px; color: #8b949e; background: #000; padding: 6px; border-radius: 4px; max-height: 100px; overflow-y: auto; }
381
+
382
+ .modal-bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: none; justify-content: center; align-items: center; z-index: 1000; }
383
+ .modal-content { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; width: 90%; max-width: 1000px; height: 85vh; display: flex; flex-direction: column; }
384
+ .modal-header { padding: 15px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-weight: bold; }
385
+ .modal-body { flex: 1; padding: 15px; overflow: auto; }
386
+ .close-btn { background: transparent; border: none; color: var(--danger); font-size: 18px; cursor: pointer; width: auto; padding: 0;}
387
+
388
+ table.data-table { width: 100%; border-collapse: collapse; font-family: monospace; font-size: 11px; text-align: right; }
389
+ table.data-table th { background: var(--bg); color: #8b949e; padding: 8px; border-bottom: 1px solid var(--border); position: sticky; top: 0; }
390
+ table.data-table td { padding: 6px 8px; border-bottom: 1px solid #21262d; }
391
+ table.data-table tr:hover { background: #21262d; }
392
+ </style>
393
+ </head>
394
+ <body>
395
+
396
+ <div class="grid">
397
+ <!-- LEFT COLUMN -->
398
+ <div style="display: flex; flex-direction: column; gap: 20px;">
399
+ <div class="card">
400
+ <h2>1. Extract & Inspect Data</h2>
401
+ <div class="row">
402
+ <div class="col"><label>Date</label><select id="selDate" onchange="App.loadRoots()"></select></div>
403
+ <div class="col"><label>Symbol</label><select id="selRoot" onchange="App.loadExpiries()"></select></div>
404
+ </div>
405
+ <label>Expiry Date</label><select id="selExpiry"></select>
406
+
407
+ <div class="row">
408
+ <div class="col"><label>ATM Range (± N)</label><input type="number" id="inpAtm" value="10"></div>
409
+ <div class="col"><label>Variables</label><input type="text" id="inpVars" value="P,CB,PS,CS,PB"></div>
410
+ </div>
411
+ <div class="row">
412
+ <div class="col"><label>Strict Start</label><input type="time" id="inpStart" value="09:15"></div>
413
+ <div class="col"><label>Strict End</label><input type="time" id="inpEnd" value="15:30"></div>
414
+ </div>
415
+
416
+ <button id="btnInspect" onclick="App.inspectData()">🔍 Fetch & Analyze Quality</button>
417
+ </div>
418
+
419
+ <div class="card" id="cardArchive" style="opacity: 0.4; pointer-events: none;">
420
+ <h2>2. Clean & Archive to Hugging Face</h2>
421
+ <div class="stat-box" id="statsBox">
422
+ <div style="text-align:center; color:#8b949e; font-style:italic;">Run Inspection first...</div>
423
+ </div>
424
+
425
+ <label>Missing Data Handling Strategy (Imputation)</label>
426
+ <select id="selFill">
427
+ <option value="ffill">Forward Fill (Fixes Nulls & Zeroes with Previous Values) - REC.</option>
428
+ <option value="interpolate">Linear Interpolation (Average between gaps)</option>
429
+ <option value="zero">Fill with Zeroes</option>
430
+ </select>
431
+
432
+ <button id="btnUpload" class="btn-success" onclick="App.uploadData()">☁️ Archive to Repository</button>
433
+ </div>
434
+ </div>
435
+
436
+ <!-- RIGHT COLUMN -->
437
+ <div class="card">
438
+ <div style="display:flex; justify-content:space-between; align-items:center;">
439
+ <h2>Hugging Face Repository Explorer</h2>
440
+ <button onclick="App.loadHFFolders()" style="width:auto; padding: 4px 12px; font-size:11px;">Refresh</button>
441
+ </div>
442
+
443
+ <div class="hf-list" id="hfList">Loading...</div>
444
+
445
+ <div class="stat-box" id="metaBox" style="margin-top: 15px; display: none;">
446
+ <h3 style="margin:0 0 10px 0; font-size:13px; color:var(--text);">Metadata Viewer</h3>
447
+ <pre id="metaContent" style="font-family:monospace; font-size:11px; color:#8b949e; margin:0; white-space:pre-wrap;"></pre>
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <div class="toast" id="toast">✅ Success!</div>
453
+
454
+ <!-- DATA VIEWER MODAL -->
455
+ <div class="modal-bg" id="dataModal">
456
+ <div class="modal-content">
457
+ <div class="modal-header">
458
+ <span id="modalTitle">Data Viewer</span>
459
+ <button class="close-btn" onclick="document.getElementById('dataModal').style.display='none'">✖</button>
460
+ </div>
461
+ <div class="modal-body" id="modalBody">
462
+ Loading data table...
463
+ </div>
464
+ </div>
465
+ </div>
466
+
467
+ <script>
468
+ const App = {
469
+ currentSession: null,
470
+ currentMeta: null,
471
+ trimStart: null,
472
+ trimEnd: null,
473
+ originalStartStr: null,
474
+ originalEndStr: null,
475
+ missingTimeTicks: 0,
476
+
477
+ async init() {
478
+ await this.loadDates();
479
+ await this.loadHFFolders();
480
+ },
481
+
482
+ showToast(msg) {
483
+ const t = document.getElementById('toast');
484
+ t.innerText = msg;
485
+ t.style.display = 'block';
486
+ setTimeout(() => t.style.display = 'none', 3000);
487
+ },
488
+
489
+ async loadDates() {
490
+ const res = await fetch('/api/dates');
491
+ const dates = await res.json();
492
+ document.getElementById('selDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
493
+ if(dates.length) this.loadRoots();
494
+ },
495
+
496
+ async loadRoots() {
497
+ const date = document.getElementById('selDate').value;
498
+ const res = await fetch(`/api/roots?date=${date}`);
499
+ const roots = await res.json();
500
+ document.getElementById('selRoot').innerHTML = roots.map(r => `<option value="${r}">${r}</option>`).join('');
501
+ if(roots.length) this.loadExpiries();
502
+ },
503
+
504
+ async loadExpiries() {
505
+ const date = document.getElementById('selDate').value;
506
+ const root = document.getElementById('selRoot').value;
507
+ if(!root) return;
508
+ const res = await fetch(`/api/expiries?date=${date}&root=${root}`);
509
+ const exps = await res.json();
510
+ document.getElementById('selExpiry').innerHTML = exps.map(e => `<option value="${e}">${e}</option>`).join('');
511
+ },
512
+
513
+ async inspectData() {
514
+ const btn = document.getElementById('btnInspect');
515
+ btn.disabled = true;
516
+ btn.innerText = "⏳ Fetching and Analyzing Chunks...";
517
+
518
+ const payload = {
519
+ date: document.getElementById('selDate').value,
520
+ root: document.getElementById('selRoot').value,
521
+ expiry: document.getElementById('selExpiry').value,
522
+ atm_range: parseInt(document.getElementById('inpAtm').value),
523
+ required_vars: document.getElementById('inpVars').value.split(',').map(s=>s.trim()),
524
+ start_time: document.getElementById('inpStart').value,
525
+ end_time: document.getElementById('inpEnd').value
526
+ };
527
+
528
+ try {
529
+ const res = await fetch('/api/inspect_data', {
530
+ method: 'POST',
531
+ headers: { 'Content-Type': 'application/json' },
532
+ body: JSON.stringify(payload)
533
+ });
534
+ const data = await res.json();
535
+
536
+ if(data.error) {
537
+ alert(data.error);
538
+ } else {
539
+ this.currentSession = data.session_id;
540
+ this.currentMeta = data.meta;
541
+ this.currentMeta.stats = data.stats;
542
+ this.trimStart = data.stats.original_start;
543
+ this.trimEnd = data.stats.original_end;
544
+ this.originalStartStr = data.stats.original_start;
545
+ this.originalEndStr = data.stats.original_end;
546
+ this.missingTimeTicks = data.stats.missing_time_ticks;
547
+
548
+ this.renderStats(data.stats);
549
+
550
+ const c2 = document.getElementById('cardArchive');
551
+ c2.style.opacity = '1';
552
+ c2.style.pointerEvents = 'auto';
553
+ }
554
+ } catch(e) {
555
+ alert(`Error: ${e.message}`);
556
+ }
557
+ btn.disabled = false;
558
+ btn.innerText = "🔍 Fetch & Analyze Quality";
559
+ },
560
+
561
+ trimBlock(start, end, count, type, btnId) {
562
+ if (type === 'start') {
563
+ let endObj = new Date(end);
564
+ endObj.setSeconds(endObj.getSeconds() + 10);
565
+ this.trimStart = endObj.toISOString().replace('T',' ').substring(0, 19);
566
+ } else if (type === 'end') {
567
+ let startObj = new Date(start);
568
+ startObj.setSeconds(startObj.getSeconds() - 10);
569
+ this.trimEnd = startObj.toISOString().replace('T',' ').substring(0, 19);
570
+ }
571
+
572
+ this.missingTimeTicks -= count;
573
+ document.getElementById('missingCountTxt').innerText = this.missingTimeTicks;
574
+ document.getElementById('missingCountTxt').className = "stat-val " + (this.missingTimeTicks === 0 ? 'val-good' : 'val-warn');
575
+
576
+ document.getElementById('bndStart').innerText = this.trimStart;
577
+ document.getElementById('bndEnd').innerText = this.trimEnd;
578
+
579
+ const b = document.getElementById(btnId);
580
+ b.innerText = "Trimmed ✔";
581
+ b.disabled = true;
582
+ b.style.textDecoration = "line-through";
583
+ b.style.color = "var(--success)";
584
+ b.style.borderColor = "var(--success)";
585
+ },
586
+
587
+ renderStats(s) {
588
+ const mClass = this.missingTimeTicks === 0 ? 'val-good' : (this.missingTimeTicks < 50 ? 'val-warn' : 'val-bad');
589
+
590
+ let nullHtml = '';
591
+ for(let key in s.null_breakdown) {
592
+ let v = s.null_breakdown[key];
593
+ let c = v.count === 0 ? 'val-good' : 'val-bad';
594
+
595
+ let details = v.count > 0
596
+ ? `<details><summary>Inspect ${v.count} Nulls/Zeroes</summary><div class="inspect-times">${v.times.join(', ')}${v.count>50?'...':''}</div></details>`
597
+ : `<div style="font-size:11px; color:var(--success)">Perfect Data</div>`;
598
+
599
+ nullHtml += `<div class="stat-item" style="flex-direction:column; align-items:flex-start; margin-bottom:10px;">
600
+ <div style="display:flex; justify-content:space-between; width:100%;">
601
+ <span>Glitched Nulls/Zeroes in ${key}</span><span class="stat-val ${c}">${v.count}</span>
602
+ </div>
603
+ ${details}
604
+ </div>`;
605
+ }
606
+
607
+ let blocksHtml = '';
608
+ if (s.missing_blocks.length > 0) {
609
+ blocksHtml = `<div style="margin-top:10px; font-weight:bold; font-size:11px; color:#8b949e; text-transform:uppercase;">Missing Time Ranges</div>`;
610
+ s.missing_blocks.forEach((b, i) => {
611
+ let btn = '';
612
+ if (b.start === this.originalStartStr) {
613
+ btn = `<button id="tb_${i}" class="btn-warn" onclick="App.trimBlock('${b.start}', '${b.end}', ${b.count}, 'start', 'tb_${i}')">✂ Trim Start</button>`;
614
+ } else if (b.end === this.originalEndStr) {
615
+ btn = `<button id="tb_${i}" class="btn-warn" onclick="App.trimBlock('${b.start}', '${b.end}', ${b.count}, 'end', 'tb_${i}')">✂ Trim End</button>`;
616
+ }
617
+
618
+ blocksHtml += `<div class="stat-item" style="font-size:11px;">
619
+ <span style="color:var(--warn)">${b.start.split(' ')[1]} ➔ ${b.end.split(' ')[1]} (${b.count} ticks)</span>
620
+ ${btn}
621
+ </div>`;
622
+ });
623
+ }
624
+
625
+ document.getElementById('statsBox').innerHTML = `
626
+ <div class="stat-item"><span>Data Start Bound</span><span class="stat-val" id="bndStart" style="font-size:11px; color:var(--accent);">${this.trimStart}</span></div>
627
+ <div class="stat-item"><span>Data End Bound</span><span class="stat-val" id="bndEnd" style="font-size:11px; color:var(--accent);">${this.trimEnd}</span></div>
628
+ <div class="stat-item"><span>Raw Fetched Ticks</span><span class="stat-val">${s.found_ticks}</span></div>
629
+ <div class="stat-item"><span>Missing Ticks (Internal)</span><span class="stat-val ${mClass}" id="missingCountTxt">${this.missingTimeTicks}</span></div>
630
+ ${blocksHtml}
631
+ <div style="margin-top:15px; font-weight:bold; font-size:11px; color:#8b949e; text-transform:uppercase;">Value Quality Breakdown</div>
632
+ ${nullHtml}
633
+ `;
634
+ },
635
+
636
+ async uploadData() {
637
+ const btn = document.getElementById('btnUpload');
638
+ btn.disabled = true;
639
+ btn.innerText = "☁️ Imputing & Uploading to HF...";
640
+
641
+ try {
642
+ const res = await fetch('/api/clean_and_upload', {
643
+ method: 'POST',
644
+ headers: { 'Content-Type': 'application/json' },
645
+ body: JSON.stringify({
646
+ session_id: this.currentSession,
647
+ fill_method: document.getElementById('selFill').value,
648
+ trim_start: this.trimStart,
649
+ trim_end: this.trimEnd,
650
+ metadata: this.currentMeta
651
+ })
652
+ });
653
+ const data = await res.json();
654
+
655
+ if(data.error) {
656
+ alert(data.error);
657
+ } else {
658
+ this.showToast(data.message);
659
+
660
+ document.getElementById('cardArchive').style.opacity = '0.4';
661
+ document.getElementById('cardArchive').style.pointerEvents = 'none';
662
+ document.getElementById('statsBox').innerHTML = '<div style="text-align:center; color:#8b949e; font-style:italic;">Run Inspection first...</div>';
663
+
664
+ this.loadHFFolders();
665
+ }
666
+ } catch(e) {
667
+ alert(`Error: ${e.message}`);
668
+ }
669
+ btn.disabled = false;
670
+ btn.innerText = "☁️ Archive to Repository";
671
+ },
672
+
673
+ async loadHFFolders() {
674
+ document.getElementById('hfList').innerHTML = 'Fetching...';
675
+ const res = await fetch('/api/hf_files');
676
+ const data = await res.json();
677
+
678
+ if(data.status === 'error') {
679
+ document.getElementById('hfList').innerHTML = `<span style="color:red">${data.message}</span>`;
680
+ return;
681
+ }
682
+
683
+ const folders = data.folders;
684
+ if(!folders.length) {
685
+ document.getElementById('hfList').innerHTML = '<div style="color:#8b949e; text-align:center; padding:20px;">No datasets archived yet.</div>';
686
+ return;
687
+ }
688
+
689
+ document.getElementById('hfList').innerHTML = folders.map(f => {
690
+ const cleanName = f.replace('Market Data/', '');
691
+ return `<div class="hf-item">
692
+ <b style="color:var(--accent)">📁 ${cleanName}</b>
693
+ <div class="hf-btns">
694
+ <button class="hf-btn" onclick="App.viewMeta('${f}')">📝 Meta</button>
695
+ <button class="hf-btn" onclick="App.viewData('${f}')">📊 View Data</button>
696
+ </div>
697
+ </div>`;
698
+ }).join('');
699
+ },
700
+
701
+ async viewMeta(folder) {
702
+ document.getElementById('metaBox').style.display = 'block';
703
+ document.getElementById('metaContent').innerText = 'Loading metadata...';
704
+
705
+ const res = await fetch(`/api/hf_metadata?folder_path=${encodeURIComponent(folder)}`);
706
+ const data = await res.json();
707
+
708
+ if(data.status === 'error') {
709
+ document.getElementById('metaContent').innerText = data.message;
710
+ } else {
711
+ document.getElementById('metaContent').innerText = JSON.stringify(data.metadata, null, 2);
712
+ }
713
+ },
714
+
715
+ async viewData(folder) {
716
+ document.getElementById('dataModal').style.display = 'flex';
717
+ document.getElementById('modalTitle').innerText = "Dataset: " + folder.replace('Market Data/', '');
718
+ const body = document.getElementById('modalBody');
719
+ body.innerHTML = '<div style="color:#8b949e; text-align:center; margin-top:50px;">Fetching and parsing Parquet file from Hugging Face...</div>';
720
+
721
+ const res = await fetch(`/api/view_parquet?folder_path=${encodeURIComponent(folder)}`);
722
+ const resp = await res.json();
723
+
724
+ if (resp.status === 'error') {
725
+ body.innerHTML = `<div style="color:var(--danger); text-align:center; margin-top:50px;">${resp.message}</div>`;
726
+ return;
727
+ }
728
+
729
+ let html = '<table class="data-table"><thead><tr>';
730
+ resp.columns.forEach(c => html += `<th>${c}</th>`);
731
+ html += '</tr></thead><tbody>';
732
+
733
+ resp.data.forEach(row => {
734
+ html += '<tr>';
735
+ resp.columns.forEach(c => {
736
+ let val = row[c];
737
+ if(typeof val === 'number') val = val.toFixed(2);
738
+ html += `<td>${val}</td>`;
739
+ });
740
+ html += '</tr>';
741
+ });
742
+ html += '</tbody></table>';
743
+ body.innerHTML = html;
744
+ }
745
+ };
746
+
747
+ window.onload = () => App.init();
748
+ </script>
749
+ </body>
750
+ </html>
751
+ """
752
+
753
+ if __name__ == "__main__":
754
+ uvicorn.run(app, host="0.0.0.0", port=7860)