Spaces:
Sleeping
Sleeping
Create app.py v1
Browse files
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)
|