DepthChainIndia / app.py
topsecrettraders's picture
Update app.py
61f7711 verified
Raw
History Blame Contribute Delete
66.9 kB
import pandas as pd
import requests
import uvicorn
import hashlib
import json
import time
import urllib.parse
import os
import threading
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Query
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from typing import Optional, List, Dict
# ==========================================
# 1. CONFIGURATION
# ==========================================
CLIENT_ID = "J4PNT5QI22-100"
SECRET_KEY = "ABRSWC1PMR"
REDIRECT_URI = "https://topsecrettraders-depthchainindia.hf.space/callback"
CACHE_FILE = "master_cache.json"
# --- SUPABASE CONFIG ---
SUPABASE_URL = "https://tuyhtqxfvjndbyjazosi.supabase.co"
SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR1eWh0cXhmdmpuZGJ5amF6b3NpIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2ODE0NjAyMiwiZXhwIjoyMDgzNzIyMDIyfQ.YsmGS67vL5TghB5OOi6pmsPL6rrgx7SNpARHmQ9b5cw"
app = FastAPI()
# ==========================================
# 2. ROBUST DATA LOADER & SUPABASE
# ==========================================
from supabase import create_client, Client
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
MASTER_DB = {}
SEARCH_INDEX = []
SYSTEM_STATUS = {
"ready": False,
"message": "Booting..."
}
UPDATE_LOCK = threading.Lock()
# Manual mapping for Index Spot symbols
INDEX_MAP = {
"NIFTY": "NSE:NIFTY50-INDEX",
"BANKNIFTY": "NSE:NIFTYBANK-INDEX",
"FINNIFTY": "NSE:FINNIFTY-INDEX",
"MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",
"SENSEX": "BSE:SENSEX-INDEX",
"BANKEX": "BSE:BANKEX-INDEX",
"SENSEX50": "BSE:SENSEX50-INDEX"
}
def get_root_symbol(desc: str, symbol: str) -> str:
try:
parts = desc.strip().split(' ')
root = parts[0].upper()
return root.replace(':', '').replace('-', '')
except:
if ':' in symbol:
return symbol.split(':')[1]
return symbol
def update_master_db():
global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
if not UPDATE_LOCK.acquire(blocking=False):
return
try:
SYSTEM_STATUS["message"] = "Downloading Master Data..."
print(">>> [BG TASK] STARTING DOWNLOAD...")
headers = { "User-Agent": "Mozilla/5.0" }
# 1. NSE F&O
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
df_nse = pd.read_csv(url_nse, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
df_nse['Exch'] = 'NSE'
# 2. MCX F&O
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
df_mcx = pd.read_csv(url_mcx, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
df_mcx['Exch'] = 'MCX'
# 3. BSE F&O
url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
df_bse = pd.read_csv(url_bse, usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)
df_bse['Exch'] = 'BSE'
SYSTEM_STATUS["message"] = "Processing Data..."
df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
df['Symbol'] = df['Symbol'].astype(str)
df['Desc'] = df['Desc'].astype(str)
temp_db = {}
for _, row in df.iterrows():
sym = row['Symbol']
desc = row['Desc']
exch = row['Exch']
root = get_root_symbol(desc, sym)
if root not in temp_db:
spot_sym = ""
if root in INDEX_MAP: spot_sym = INDEX_MAP[root]
elif exch == "MCX": spot_sym = "MCX"
elif exch == "BSE": spot_sym = f"BSE:{root}"
else: spot_sym = f"NSE:{root}-EQ"
temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
try: exp = int(row['Expiry'])
except: exp = 0
strike = 0.0
opt_type = "FUT"
if "CE" in sym or "PE" in sym:
parts = desc.strip().split(' ')
if len(parts) >= 2:
try:
val = parts[-2]
val_clean = val.replace('.', '', 1)
if val_clean.isdigit(): strike = float(val)
opt_type = parts[-1]
except: pass
temp_db[root]["items"].append({ "s": sym, "e": exp, "k": strike, "t": opt_type })
final_db = {}
search_list = []
for root, data in temp_db.items():
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted_items }
search_list.append({ "root": root, "exch": data["exch"], "display": f"{root}" })
MASTER_DB = final_db
SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
with open(CACHE_FILE, "w") as f:
json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
SYSTEM_STATUS["ready"] = True
SYSTEM_STATUS["message"] = "Ready"
print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} symbols.")
except Exception as e:
print(f">>> [BG TASK] ERROR: {e}")
SYSTEM_STATUS["message"] = f"Error: {str(e)}"
finally:
UPDATE_LOCK.release()
@app.on_event("startup")
def startup_event():
global MASTER_DB, SEARCH_INDEX
if os.path.exists(CACHE_FILE):
try:
print(">>> STARTUP: Loading Cache...")
with open(CACHE_FILE, "r") as f:
data = json.load(f)
MASTER_DB = data["db"]
SEARCH_INDEX = data["idx"]
SYSTEM_STATUS["ready"] = True
SYSTEM_STATUS["message"] = "Ready (Cached)"
except: pass
if not SYSTEM_STATUS["ready"]:
thread = threading.Thread(target=update_master_db)
thread.start()
# ==========================================
# 3. ROUTES & API
# ==========================================
@app.get("/")
def home(access_token: Optional[str] = Cookie(None)):
if not access_token:
state = "init"
enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
auth_url = (
f"https://api-t1.fyers.in/api/v3/generate-authcode?"
f"client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}"
)
return HTMLResponse(f"""
<div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;">
<div style="background:white; padding:40px; border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.1); text-align:center;">
<h2 style="margin-top:0;">Authentication Required</h2>
<a href="{auth_url}" style="display:inline-block; background:#2962ff; color:white; padding:12px 25px; text-decoration:none; border-radius:5px; font-weight:bold; margin-top:10px;">LOGIN WITH FYERS</a>
</div>
</div>
""")
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
@app.get("/callback")
def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
final_code = auth_code if auth_code else code
if not final_code: return JSONResponse({"error": "No Code Provided"})
app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
"grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
})
data = resp.json()
if "access_token" in data:
full_token = f"{CLIENT_ID}:{data['access_token']}"
response = RedirectResponse(url="/")
response.set_cookie(key="access_token", value=full_token, httponly=True)
return response
else: return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(data, indent=2)}</pre>")
@app.get("/logout")
def logout():
response = RedirectResponse(url="/")
response.delete_cookie("access_token")
return response
@app.get("/api/status")
def get_status(): return JSONResponse(SYSTEM_STATUS)
@app.get("/api/search")
def search_symbol(q: str = Query(..., min_length=1)):
if not SYSTEM_STATUS["ready"]: return JSONResponse([])
query = q.upper()
results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
if len(results) < 10: results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
return JSONResponse(results[:20])
@app.get("/api/chain")
def get_chain(symbol: str):
root = symbol.upper()
if root in MASTER_DB: return JSONResponse(MASTER_DB[root])
return JSONResponse({"error": "Symbol not found"}, status_code=404)
@app.get("/api/refresh")
def force_refresh(background_tasks: BackgroundTasks):
SYSTEM_STATUS["ready"] = False
SYSTEM_STATUS["message"] = "Refreshing..."
background_tasks.add_task(update_master_db)
return JSONResponse({"s": "ok"})
# --- HISTORICAL API ---
def parse_bucket_data(bucket_rows):
slots = [{"s": f"{i*10:02d}", "d": {}} for i in range(6)]
for row in bucket_rows:
data = row.get('data')
if not data or not isinstance(data, dict): continue
for symbol, metrics in data.items():
p_arr = metrics.get('p', [])
b_arr = metrics.get('b', [])
s_arr = metrics.get('s', [])
v_arr = metrics.get('v', [])
o_arr = metrics.get('o', [])
for i in range(6):
try:
ltp = p_arr[i] if i < len(p_arr) else None
if ltp is None: continue
slots[i]['d'][symbol] = {
"ltp": ltp,
"totalbuyqty": b_arr[i] if i < len(b_arr) else 0,
"totalsellqty": s_arr[i] if i < len(s_arr) else 0,
"v": v_arr[i] if i < len(v_arr) else 0,
"oi": o_arr[i] if i < len(o_arr) else 0,
"expiry": 0
}
except: continue
return slots
@app.get("/api/hist/dates")
def get_hist_dates(symbol: str):
try:
res = supabase.table('history_buckets').select('minute').ilike('root', f"%{symbol}%").order('minute', desc=True).limit(2000).execute()
dates = set()
if res.data:
for item in res.data:
if item.get('minute'): dates.add(item.get('minute').split('T')[0].split(' ')[0])
return JSONResponse(sorted(list(dates), reverse=True))
except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/hist/minutes")
def get_hist_minutes(symbol: str, date: str):
try:
res = supabase.table('history_buckets').select('minute').ilike('root', f"%{symbol}%").gte('minute', f"{date}T00:00:00").lte('minute', f"{date}T23:59:59").order('minute', desc=True).limit(1000).execute()
minutes = set()
if res.data:
for item in res.data:
clean = item.get('minute').replace('T', ' ').split(' ')
if len(clean)>1: minutes.add(clean[1][:5])
return JSONResponse(sorted(list(minutes), reverse=True))
except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/hist/expiries")
def get_hist_expiries(symbol: str, date: str, minute: str):
try:
q1 = f"{date} {minute}:00"
q2 = f"{date}T{minute}:00"
res = supabase.table('history_buckets').select('expiry').ilike('root', f"%{symbol}%").or_(f"minute.eq.{q1},minute.eq.{q2}").neq('expiry', 'MARKET').execute()
expiries = set()
if res.data:
for item in res.data:
if item.get('expiry') and item.get('expiry') != 'MARKET': expiries.add(item.get('expiry'))
return JSONResponse(sorted(list(expiries)))
except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/hist/load")
def get_hist_data(symbol: str, date: str, minute: str, expiry: Optional[str] = None):
try:
q1 = f"{date} {minute}:00"
q2 = f"{date}T{minute}:00"
res = supabase.table('history_buckets').select('data, expiry').ilike('root', f"%{symbol}%").or_(f"minute.eq.{q1},minute.eq.{q2}").execute()
rows = res.data
if not rows: return JSONResponse({"s": "ok", "timeline": []})
filtered_rows = []
for r in rows:
e = r.get('expiry')
if not expiry or e == 'MARKET' or e == expiry: filtered_rows.append(r)
return JSONResponse({"s": "ok", "timeline": parse_bucket_data(filtered_rows)})
except Exception as e: return JSONResponse({"error": str(e)}, status_code=500)
# ==========================================
# 4. FRONTEND
# ==========================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PRO CHAIN V3</title>
<meta name="viewport" content="width=1100">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
--text-main: #1a1a1a; --text-sub: #555555;
--accent: #2962ff; --green: #00c853; --red: #d50000;
--green-soft: rgba(0, 200, 83, 0.27); --red-soft: rgba(213, 0, 0, 0.18);
--atm-bg: rgba(41, 98, 255, 0.08);
--badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); }
.btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; color: var(--text-sub); }
.btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
.btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; }
.icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
.search-box { position: relative; width: 350px; margin-right: 20px; }
.search-input { width: 100%; padding: 10px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; background: #f9f9f9; }
.search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1); }
.search-dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: white; border: 1px solid var(--border); max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 6px; z-index: 100; }
.search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
.search-item:hover { background: #f0f7ff; color: var(--accent); }
.exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
.badge-NSE { background: var(--badge-nse); } .badge-BSE { background: var(--badge-bse); } .badge-MCX { background: var(--badge-mcx); }
/* HISTORY CONTROLS */
.hist-controls { display: none; align-items: center; gap: 8px; margin-left: 20px; padding-left: 20px; border-left: 1px solid #eee; }
.hist-sel { padding: 6px; border-radius: 4px; border: 1px solid #ddd; font-weight: bold; font-size: 11px; background: #f9f9f9; outline:none; }
.sec-bar-cont { display: flex; gap: 2px; overflow-x: auto; max-width: 250px; padding: 2px; background: #f0f0f0; border-radius: 4px; }
.sec-pill { padding: 3px 6px; font-size: 10px; cursor: pointer; border-radius: 2px; color: #666; font-family:'JetBrains Mono'; }
.sec-pill:hover { background: #ddd; color: #000; }
.sec-pill.active { background: var(--accent); color: white; font-weight: bold; }
.sec-pill.bad { background: #ff5252; color: white; opacity: 0.8; }
.sec-pill.bad:hover { background: #d50000; }
.sec-pill.bad.active { background: #b71c1c; border: 1px solid #fff; opacity:1; }
.play-btn { width:24px; height:24px; display:flex; align-items:center; justify-content:center; background:#eee; border-radius:4px; cursor:pointer; font-size:12px; }
.play-btn:hover { background:#ddd; }
.play-btn.active { background:var(--green); color:white; }
.grid-layout { display: flex; height: calc(100vh - 60px); }
.chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
.chain-header { display: grid; grid-template-columns: 1fr 1fr; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; }
.header-cell { padding: 12px 5px; display: flex; align-items: center; justify-content: center; gap: 8px; position:relative; border-right: 1px solid var(--border); }
.header-icon { opacity: 0.3; cursor: pointer; transition: 0.2s; }
.header-icon:hover { opacity: 1; color: var(--accent); }
.chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; }
.row { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
.row:hover { background: #fafafa; }
.row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
.graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; }
.visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; }
.strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
.delta-txt { z-index: 2; font-size: 10px; font-weight: 700; background: rgba(255,255,255,0.85); padding: 0 5px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-top: -2px; }
.fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
.panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; }
.panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; }
.fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
.meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; }
.netflow-card { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid var(--border); text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
.nf-sec { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
.nf-sec:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.nf-title { font-size: 10px; font-weight: 800; color: #888; text-transform: uppercase; margin-bottom: 8px; text-align: left; }
.aggr-row { display: flex; width: 100%; border: 1px solid transparent; }
.aggr-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
.aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; }
.aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; background: transparent; padding: 0; border-radius: 0; margin-top: 2px; display:flex; align-items:center; gap:3px; }
.info-bubble { width:12px; height:12px; background:#eee; color:#666; border-radius:50%; font-size:8px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:'Inter'; font-weight:800; }
.info-bubble:hover { background:var(--accent); color:white; }
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
.modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
.opt-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 6px; cursor: pointer; font-weight:600; font-size:12px; transition:0.2s; background:#f9f9f9; }
.opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
.opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.inp-group { margin-top:15px; }
.inp-label { font-size:11px; font-weight:800; color:#888; margin-bottom:5px; display:block; }
.modal-inp { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; font-size:14px; }
.modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; }
.modal-action.sec { background:#eee; color:#333; margin-top:10px; }
.info-table { width:100%; font-family:'JetBrains Mono'; font-size:10px; border-collapse: collapse; margin-top:10px; }
.info-table th { text-align:left; color:#888; border-bottom:1px solid #eee; padding:5px; }
.info-table td { padding:5px; border-bottom:1px solid #f9f9f9; color:#333; }
.info-row-res { background: #f0f7ff; font-weight:800; }
.preset-pill { display:inline-block; padding:4px 8px; background:#f0f0f0; border-radius:4px; font-size:10px; font-weight:700; margin-right:5px; cursor:pointer; color:#555; border:1px solid transparent; }
.preset-pill:hover { border-color:#aaa; background:#e0e0e0; }
.conf-list { max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px; background: #fafafa; }
.conf-item { background: #fff; border-bottom: 1px solid #eee; padding: 10px; }
.conf-item:last-child { border-bottom: none; }
.conf-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.conf-body { margin-top: 10px; display: none; padding-top: 10px; border-top: 1px solid #f5f5f5; }
.conf-item.expanded .conf-body { display: block; }
.conf-item.expanded .conf-header { color: var(--accent); }
.del-btn { color: var(--red); cursor: pointer; font-size: 10px; font-weight: bold; border: 1px solid var(--red-soft); padding: 2px 6px; border-radius: 4px; }
.add-btn { width: 100%; border: 1px dashed #ccc; padding: 10px; text-align: center; font-size: 11px; font-weight: bold; color: #888; cursor: pointer; border-radius: 6px; margin-bottom: 15px; transition: 0.2s; background: #fff; }
.add-btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }
.loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
.spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
@keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
.live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; }
.live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); }
.live-dot.error { background: var(--red); }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #bbb; }
.code-box { background:#f4f4f4; padding:10px; border-radius:6px; font-family:'JetBrains Mono'; font-size:11px; color:#555; margin-top:5px; display:none; }
.var-tag { color:var(--accent); font-weight:bold; }
</style>
</head>
<body>
<div class="loader-ov" id="loader">
<div class="spinner"></div>
<div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div>
</div>
<div class="modal" id="expModal">
<div class="modal-box">
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
<span>SELECT EXPIRY</span>
<span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span>
</div>
<div class="modal-grid" id="expGrid"></div>
</div>
</div>
<div class="modal" id="infoModal">
<div class="modal-box" style="width: 500px; max-width:95%;">
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
<span>CALCULATION BREAKDOWN</span>
<span style="cursor:pointer; color:#999;" onclick="closeModal('infoModal')">✕</span>
</div>
<div style="margin-top:15px; font-size:12px;">
<div><strong>SNAPSHOT SPOT:</strong> <span id="infoSpot" style="font-family:'JetBrains Mono'; color:var(--accent);">0.00</span></div>
<div style="margin-top:5px;"><strong>FORMULA:</strong> <span id="infoForm" style="font-family:'JetBrains Mono'; background:#f4f4f4; padding:2px 6px; border-radius:4px;"></span></div>
</div>
<div style="overflow-y:auto; max-height:400px; margin-top:10px;">
<table class="info-table" id="infoTable"></table>
</div>
</div>
</div>
<div class="modal" id="confModal">
<div class="modal-box">
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
<span id="confTitle">COLUMN SETTINGS</span>
<span style="cursor:pointer; color:#999;" onclick="closeModal('confModal')">✕</span>
</div>
<div class="inp-group"><span class="inp-label">DISPLAY TITLE</span><input type="text" id="confName" class="modal-inp"></div>
<div class="inp-group"><span class="inp-label">CUSTOM FORMULA</span><input type="text" id="confForm" class="modal-inp"></div>
<button class="modal-action" onclick="saveConfig()">SAVE CONFIGURATION</button>
<button class="modal-action sec" onclick="resetConfig()">RESET TO DEFAULTS</button>
</div>
</div>
<div class="modal" id="sideConfModal">
<div class="modal-box">
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
<span>SIDEBAR CONFIG</span>
<span style="cursor:pointer; color:#999;" onclick="closeModal('sideConfModal')">✕</span>
</div>
<div style="margin-top:10px; font-size:11px; font-weight:700; color:#888; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;">
<span>CONFIGURE CARDS & RANGES</span>
<span onclick="toggleVarHelp()" style="color:var(--accent); cursor:pointer; font-weight:800;">[?] VARIABLES</span>
</div>
<div class="code-box" id="sideVarHelp">
<div>VARIABLES AVAILABLE:</div>
<div style="margin-top:4px;">
<span class="var-tag">CS</span> = Call Sell &nbsp; <span class="var-tag">CB</span> = Call Buy &nbsp; <span class="var-tag">CO</span> = Call OI &nbsp; <span class="var-tag">CLTP</span> = Call LTP &nbsp; <span class="var-tag">CV</span> = Call Vol<br>
<span class="var-tag">PS</span> = Put Sell &nbsp;&nbsp; <span class="var-tag">PB</span> = Put Buy &nbsp; <span class="var-tag">PO</span> = Put OI &nbsp; <span class="var-tag">PLTP</span> = Put LTP &nbsp; <span class="var-tag">PV</span> = Put Vol
</div>
<div style="margin-top:4px; font-size:10px; color:#888;">Supports +, -, *, /, ( ) arithmetic.</div>
</div>
<div class="conf-list" id="sideListContainer"></div>
<div class="add-btn" onclick="addSideItem()">+ ADD NEW AGGREGATE CARD</div>
<button class="modal-action" onclick="saveSideConfig()">SAVE SETTINGS</button>
<button class="modal-action sec" onclick="resetSideConfig()">RESET DEFAULTS</button>
</div>
</div>
<div class="top-nav">
<div style="display:flex; align-items:center;">
<div class="search-box">
<input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY)..." autocomplete="off">
<div class="search-dropdown" id="searchRes"></div>
</div>
<button class="btn" id="modeBtn" onclick="toggleMode()"><span id="modeTxt">LIVE</span></button>
<div class="hist-controls" id="histControls">
<select class="hist-sel" id="histDate" onchange="loadHistMinutes()"><option>DATE</option></select>
<select class="hist-sel" id="histTime" onchange="loadHistExpiries()"><option>TIME</option></select>
<div class="play-btn" id="btnPlay" onclick="togglePlay()" title="Play/Pause">▶</div>
<select class="hist-sel" id="playSpeed" style="width:50px;">
<option value="1000">1x</option>
<option value="500">2x</option>
<option value="200">5x</option>
</select>
<div class="play-btn" onclick="jumpHistory(-5)" title="-5 Minutes">«</div>
<div class="play-btn" onclick="jumpHistory(5)" title="+5 Minutes">»</div>
<div class="sec-bar-cont" id="secBar"></div>
</div>
</div>
<div style="display:flex; gap:20px; align-items:center;">
<button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg></button>
<div style="text-align:right;">
<div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
<div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
</div>
<button class="btn" onclick="openExpModal()"><span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg></button>
<div class="live-dot" id="statusDot" title="Connection Status"></div>
<button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg></button>
</div>
</div>
<div class="grid-layout">
<div class="chain-panel">
<div class="chain-header">
<div class="header-cell"><span id="headL_txt">NET FLOW</span><svg class="header-icon" onclick="openConfig('left')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div>
<div class="header-cell"><span id="headR_txt">OVERALL FLOW</span><svg class="header-icon" onclick="openConfig('right')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div>
</div>
<div class="chain-body" id="chainBody">
<div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div>
</div>
</div>
<div class="fut-panel">
<div class="panel-section">
<div class="panel-title"><span>AGGREGATE FLOW</span><div style="display:flex; align-items:center; gap:5px;"><svg onclick="openSideConfig()" style="cursor:pointer; opacity:0.5;" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg></div></div>
<div class="netflow-card" id="sideContainer"><div style="padding:20px; color:#ccc; font-size:10px;">LOADING CONFIG...</div></div>
</div>
<div class="panel-section">
<div class="panel-title">FUTURES CONTRACTS</div>
<div id="futBody"></div>
</div>
</div>
</div>
<script>
const AUTH_TOKEN = "{{USER_TOKEN}}";
let CURRENT_ROOT="", CHAIN_DATA=null, ACTIVE_EXP=0, SPOT_SYM="", SPOT_PRICE=0, FUT_SYMS=[], OPT_SYMS=[];
let POLLER=null, IS_POLLING=false, SEARCH_TIMER=null;
let IS_HISTORY=false, CACHED_TIMELINE=[], HIST_EXPIRIES=[], SELECTED_HIST_EXP=null;
let LATEST_ROW_DATA={};
let PLAY_TIMER=null, IS_PLAYING=false, CUR_SEC_IDX=0;
const DEFAULTS = {
left: { name: "NET FLOW", formula: "PS - CS" },
right: { name: "OVERALL FLOW", formula: "CB + PS - (PB + CS)" },
side: [
{ id: 1, name: "NET FLOW", formula: "PS - CS", ranges: [7,10], _expanded: true },
{ id: 2, name: "OVERALL", formula: "CB + PS - (PB + CS)", ranges: [7,10], _expanded: false },
{ id: 3, name: "NET OI", formula: "PO - CO", ranges: [10], _expanded: false }
]
};
let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS));
if(!Array.isArray(CONFIG.side)) CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side));
let EDITING_COL = null;
window.onload = () => {
if(AUTH_TOKEN.indexOf("{{")!==-1){alert("Auth Error"); return;}
applyConfig(); checkStatus();
document.getElementById('searchInp').addEventListener('input', (e)=>{ clearTimeout(SEARCH_TIMER); SEARCH_TIMER=setTimeout(()=>doSearch(e.target.value),300); });
document.addEventListener('click', (e)=>{ if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display='none'; });
};
function toggleMode() {
IS_HISTORY = !IS_HISTORY;
const btn = document.getElementById('modeBtn');
const ctrls = document.getElementById('histControls');
const dot = document.getElementById('statusDot');
if(IS_HISTORY) {
btn.classList.add('active'); document.getElementById('modeTxt').innerText="HISTORY";
ctrls.style.display="flex";
if(POLLER) clearInterval(POLLER);
dot.style.background="#2962ff";
if(CURRENT_ROOT) loadHistDates();
} else {
btn.classList.remove('active'); document.getElementById('modeTxt').innerText="LIVE";
ctrls.style.display="none";
dot.style.background="#ccc";
pauseHistory();
if(CURRENT_ROOT) loadSymbol(CURRENT_ROOT);
}
}
async function loadHistDates() {
if(!CURRENT_ROOT) return;
const sel = document.getElementById('histDate'); sel.innerHTML="<option>LOADING...</option>";
try {
const res = await fetch(`/api/hist/dates?symbol=${CURRENT_ROOT}`);
const dates = await res.json();
sel.innerHTML = "<option value=''>SELECT DATE</option>" + dates.map(d=>`<option value="${d}">${d}</option>`).join('');
} catch(e){ sel.innerHTML="<option>ERROR</option>"; }
}
async function loadHistMinutes() {
const date = document.getElementById('histDate').value;
if(!date) return;
const sel = document.getElementById('histTime'); sel.innerHTML="<option>LOADING...</option>";
HIST_EXPIRIES=[]; SELECTED_HIST_EXP=null; updateExpText(0);
try {
const res = await fetch(`/api/hist/minutes?symbol=${CURRENT_ROOT}&date=${date}`);
const mins = await res.json();
sel.innerHTML = "<option value=''>SELECT TIME</option>" + mins.map(m=>`<option value="${m}">${m}</option>`).join('');
} catch(e){}
}
async function loadHistExpiries() {
const date = document.getElementById('histDate').value;
const time = document.getElementById('histTime').value;
if(!date || !time) return;
try {
const res = await fetch(`/api/hist/expiries?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`);
HIST_EXPIRIES = await res.json();
if(HIST_EXPIRIES.length>0) { SELECTED_HIST_EXP=HIST_EXPIRIES[0]; }
else { alert("Spot/Fut Only. No Options."); SELECTED_HIST_EXP=null; }
updateHistExpUI(); loadHistData();
} catch(e){}
}
function updateHistExpUI() { document.getElementById('selExpTxt').innerText = SELECTED_HIST_EXP || "NONE"; }
async function loadHistData(bg=false) {
const date = document.getElementById('histDate').value;
const time = document.getElementById('histTime').value;
if(!date || !time) return;
if(!bg) { document.getElementById('loaderMsg').innerText="FETCHING SNAPSHOTS..."; document.getElementById('loader').style.display='flex'; }
let url = `/api/hist/load?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`;
if(SELECTED_HIST_EXP) url += `&expiry=${SELECTED_HIST_EXP}`;
try {
const res = await fetch(url);
const json = await res.json();
CACHED_TIMELINE = json.timeline;
renderSecondBar();
// Auto-select first green snapshot
if(CACHED_TIMELINE.length>0) {
let found=0;
for(let i=0; i<CACHED_TIMELINE.length; i++) {
if(validateSnapshot(CACHED_TIMELINE[i].d)) { found=i; break; }
}
CUR_SEC_IDX = found;
selectSnapshot(found);
}
} catch(e){ console.error(e); }
if(!bg) document.getElementById('loader').style.display='none';
}
function validateSnapshot(data) {
// 1. Ref Price Check
let ref = 0;
if(SPOT_SYM && data[SPOT_SYM] && data[SPOT_SYM].ltp>0) ref = data[SPOT_SYM].ltp;
if(ref===0 && FUT_SYMS.length>0) {
for(let f of FUT_SYMS) if(data[f.s] && data[f.s].ltp>0) { ref = data[f.s].ltp; break; }
}
if(ref===0) return false;
// 2. Strict ATM +/- 7 Logic (Red Seconds Check)
if(SELECTED_HIST_EXP && CHAIN_DATA) {
const expStr = SELECTED_HIST_EXP;
const relevant = CHAIN_DATA.filter(x => {
const dStr = new Date(x.e*1000).toISOString().split('T')[0];
return dStr === expStr && (x.t==='CE' || x.t==='PE');
});
// Find ATM
const strikes = [...new Set(relevant.map(x=>x.k))].sort((a,b)=>a-b);
const atm = strikes.reduce((prev, curr) => Math.abs(curr - ref) < Math.abs(prev - ref) ? curr : prev);
const atmIdx = strikes.indexOf(atm);
const start = Math.max(0, atmIdx-7);
const end = Math.min(strikes.length-1, atmIdx+7);
// Map symbols to check
for(let i=start; i<=end; i++) {
const k = strikes[i];
const ce = relevant.find(x => x.k===k && x.t==='CE');
const pe = relevant.find(x => x.k===k && x.t==='PE');
if(ce) {
if(!data[ce.s] || ( (data[ce.s].totalbuyqty||0) + (data[ce.s].totalsellqty||0) === 0 )) return false;
}
if(pe) {
if(!data[pe.s] || ( (data[pe.s].totalbuyqty||0) + (data[pe.s].totalsellqty||0) === 0 )) return false;
}
}
}
return true;
}
function renderSecondBar() {
document.getElementById('secBar').innerHTML = CACHED_TIMELINE.map((item, idx) => {
const isValid = validateSnapshot(item.d);
return `<div class="sec-pill ${isValid?'':'bad'}" onclick="manualSelect(${idx})" id="pill-${idx}">:${item.s}</div>`;
}).join('');
}
function manualSelect(idx) {
CUR_SEC_IDX = idx;
selectSnapshot(idx);
}
function selectSnapshot(idx) {
document.querySelectorAll('.sec-pill').forEach(p => p.classList.remove('active'));
const el = document.getElementById(`pill-${idx}`);
if(el) el.classList.add('active');
if(CACHED_TIMELINE[idx]) renderSnapshot(CACHED_TIMELINE[idx]);
}
function renderSnapshot(item) {
const data = item.d;
// Important: Update SPOT_PRICE Global for Info Modal
let ref = 0;
if(SPOT_SYM && data[SPOT_SYM]) ref = data[SPOT_SYM].ltp;
else if(FUT_SYMS.length>0) for(let f of FUT_SYMS) if(data[f.s]) { ref=data[f.s].ltp; break; }
SPOT_PRICE = ref; // Update global for Info Modal
// Rebuild rows if expiry changed logic
if(SELECTED_HIST_EXP && CHAIN_DATA) {
const expStr = SELECTED_HIST_EXP;
const relevant = CHAIN_DATA.filter(x => new Date(x.e*1000).toISOString().split('T')[0] === expStr && (x.t==='CE'||x.t==='PE'));
if(relevant.length > 0) { OPT_SYMS = relevant; buildChain(); }
}
updateUI(data);
}
// PLAYBACK
function togglePlay() {
if(IS_PLAYING) pauseHistory();
else playHistory();
}
function playHistory() {
if(!IS_HISTORY) return;
IS_PLAYING = true;
document.getElementById('btnPlay').innerHTML = "❚❚";
document.getElementById('btnPlay').classList.add('active');
tickHistory();
}
function pauseHistory() {
IS_PLAYING = false;
clearTimeout(PLAY_TIMER);
document.getElementById('btnPlay').innerHTML = "▶";
document.getElementById('btnPlay').classList.remove('active');
}
async function tickHistory() {
if(!IS_PLAYING) return;
const spd = parseInt(document.getElementById('playSpeed').value);
CUR_SEC_IDX++;
if(CUR_SEC_IDX >= CACHED_TIMELINE.length) {
// Move to next minute
await jumpHistory(1);
CUR_SEC_IDX = 0;
} else {
selectSnapshot(CUR_SEC_IDX);
}
PLAY_TIMER = setTimeout(tickHistory, spd);
}
async function jumpHistory(mins) {
const dEl = document.getElementById('histDate');
const tEl = document.getElementById('histTime');
if(!dEl.value || !tEl.value) return;
let currentDt = new Date(dEl.value + 'T' + tEl.value + ':00');
currentDt.setMinutes(currentDt.getMinutes() + mins);
const nextTime = currentDt.toTimeString().substring(0,5);
// Check if exists in dropdown
let exists = false;
for(let i=0; i<tEl.options.length; i++) {
if(tEl.options[i].value === nextTime) {
tEl.selectedIndex = i;
exists = true;
break;
}
}
if(exists) {
await loadHistExpiries(); // Will trigger loadHistData
} else {
pauseHistory(); // Stop if end of data
}
}
// --- STANDARD APP LOGIC ---
function applyConfig() {
document.getElementById('headL_txt').innerText = CONFIG.left.name;
document.getElementById('headR_txt').innerText = CONFIG.right.name;
renderSidebar();
}
function renderSidebar() {
const con = document.getElementById('sideContainer');
if(!CONFIG.side || CONFIG.side.length === 0) { con.innerHTML = '<div style="padding:10px; color:#aaa; font-size:10px;">NO METRICS</div>'; return; }
con.innerHTML = CONFIG.side.map((item, idx) => {
const boxes = item.ranges.map((r, rIdx) => `
<div class="aggr-item"><span class="aggr-val" id="sv-${idx}-${rIdx}">0</span>
<span class="aggr-lbl">ATM ±${r} <span class="info-bubble" onclick="showInfo(${idx}, ${rIdx})">i</span></span></div>
`).join('');
return `<div class="nf-sec"><div class="nf-title">${item.name}</div><div class="aggr-row">${boxes}</div></div>`;
}).join('');
}
function openConfig(col) {
EDITING_COL = col;
const cfg = CONFIG[col];
document.getElementById('confTitle').innerText = (col==='left'?"LEFT":"RIGHT")+" SETTINGS";
document.getElementById('confName').value = cfg.name;
document.getElementById('confForm').value = cfg.formula;
document.getElementById('confModal').style.display = 'flex';
}
function saveConfig() {
const name = document.getElementById('confName').value;
const form = document.getElementById('confForm').value.toUpperCase();
if(!name || !form) { alert("Empty fields"); return; }
CONFIG[EDITING_COL] = { name, formula: form };
saveAndReload(); closeModal('confModal');
}
function resetConfig() { if(confirm("Reset?")){CONFIG.left=DEFAULTS.left; CONFIG.right=DEFAULTS.right; saveAndReload(); closeModal('confModal');} }
let TEMP_SIDE_CONFIG = [];
function openSideConfig() { TEMP_SIDE_CONFIG = JSON.parse(JSON.stringify(CONFIG.side)); renderSideConfigList(); document.getElementById('sideConfModal').style.display = 'flex'; }
function renderSideConfigList() {
const list = document.getElementById('sideListContainer');
list.innerHTML = TEMP_SIDE_CONFIG.map((item, idx) => `
<div class="conf-item ${item._expanded?'expanded':''}">
<div class="conf-header" onclick="toggleSideItem(${idx})">
<span style="font-weight:700; font-size:12px;">${idx+1}. ${item.name}</span>
<div style="display:flex; align-items:center; gap:10px;"><span style="font-size:10px; color:#aaa;">${item.ranges.length} RANGES</span><span class="del-btn" onclick="removeSideItem(event, ${idx})">DEL</span></div>
</div>
<div class="conf-body" onclick="event.stopPropagation()">
<div class="inp-group" style="margin-top:0;"><span class="inp-label">TITLE</span><input type="text" class="modal-inp" value="${item.name}" onchange="updateSideItem(${idx}, 'name', this.value)"></div>
<div class="inp-group"><span class="inp-label">FORMULA</span><input type="text" class="modal-inp" value="${item.formula}" onchange="updateSideItem(${idx}, 'formula', this.value)"></div>
<div class="inp-group"><span class="inp-label">RANGES (Comma sep)</span><input type="text" class="modal-inp" value="${item.ranges.join(', ')}" onchange="updateSideItem(${idx}, 'ranges', this.value)"></div>
</div>
</div>`).join('');
}
function toggleSideItem(idx){ TEMP_SIDE_CONFIG[idx]._expanded = !TEMP_SIDE_CONFIG[idx]._expanded; renderSideConfigList(); }
function toggleVarHelp(){ const b = document.getElementById('sideVarHelp'); b.style.display = (b.style.display==='none'||b.style.display==='')?'block':'none'; }
function addSideItem(){ TEMP_SIDE_CONFIG.push({ id: Date.now(), name: "NEW", formula: "PS-CS", ranges: [10], _expanded: true }); renderSideConfigList(); }
function removeSideItem(e, idx){ e.stopPropagation(); if(confirm("Del?")){ TEMP_SIDE_CONFIG.splice(idx, 1); renderSideConfigList(); } }
function updateSideItem(idx, f, v){
if(f==='ranges') TEMP_SIDE_CONFIG[idx][f] = v.split(',').map(s=>parseInt(s.trim())).filter(n=>!isNaN(n)&&n>0&&n<=50);
else if(f==='formula') TEMP_SIDE_CONFIG[idx][f] = v.toUpperCase();
else TEMP_SIDE_CONFIG[idx][f] = v;
}
function saveSideConfig(){ CONFIG.side = TEMP_SIDE_CONFIG.map(({_expanded,...r})=>r); saveAndReload(); closeModal('sideConfModal'); }
function resetSideConfig(){ if(confirm("Reset Sidebar?")){ CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); saveAndReload(); closeModal('sideConfModal'); } }
function saveAndReload(){ localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG)); applyConfig(); if(!IS_HISTORY) pollData(); }
function closeModal(id){ document.getElementById(id).style.display='none'; }
function evaluateFormula(formula, vars) {
try {
let exp = formula.replace(/CS/g,vars.CS||0).replace(/CB/g,vars.CB||0).replace(/PS/g,vars.PS||0).replace(/PB/g,vars.PB||0)
.replace(/CO/g,vars.CO||0).replace(/PO/g,vars.PO||0).replace(/CLTP/g,vars.CLTP||0).replace(/PLTP/g,vars.PLTP||0)
.replace(/CV/g,vars.CV||0).replace(/PV/g,vars.PV||0);
if(/[^0-9+\-*/(). ]/.test(exp)) return 0;
return new Function('return '+exp)();
} catch(e){ return 0; }
}
function showInfo(cardIdx, rangeIdx) {
if(!CONFIG.side[cardIdx]) return;
const cfg = CONFIG.side[cardIdx];
const range = cfg.ranges[rangeIdx];
document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2);
document.getElementById('infoForm').innerText = cfg.formula;
const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b);
const lowers = kList.filter(k => k < SPOT_PRICE);
const uppers = kList.filter(k => k >= SPOT_PRICE);
const allSel = [...lowers.slice(-range), ...uppers.slice(0, range)].sort((a,b)=>a-b);
let grandTotal = 0;
document.getElementById('infoTable').innerHTML = `<tr><th style="width:50px;">STRIKE</th><th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th></tr>` +
allSel.map(k => {
const d = LATEST_ROW_DATA[k]||{};
const val = evaluateFormula(cfg.formula, d);
grandTotal += val;
const style = (k >= SPOT_PRICE && lowers.slice(-range).includes(k-(kList[1]-kList[0]))) ? "border-top:2px solid var(--accent);" : "";
return `<tr style="${style}"><td style="font-weight:bold;">${k}</td><td>${d.CS||0}</td><td>${d.CB||0}</td><td>${d.PS||0}</td><td>${d.PB||0}</td><td style="font-weight:bold; color:${val>=0?'var(--green)':'var(--red)'}">${val}</td></tr>`;
}).join('') + `<tr class="info-row-res"><td colspan="5" style="text-align:right;">TOTAL</td><td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td></tr>`;
document.getElementById('infoModal').style.display='flex';
}
async function checkStatus() {
try {
const res = await fetch('/api/status'); const s = await res.json();
if(s.ready) { document.getElementById('loader').style.display='none'; if(!CURRENT_ROOT) loadSymbol('NIFTY'); }
else { document.getElementById('loaderMsg').innerText = s.message.toUpperCase(); setTimeout(checkStatus, 2000); }
} catch(e){ setTimeout(checkStatus, 2000); }
}
async function refreshDB() {
if(!confirm("Download fresh Master Data?")) return;
document.getElementById('loaderMsg').innerText = "DOWNLOADING..."; document.getElementById('loader').style.display='flex';
await fetch('/api/refresh'); checkStatus();
}
async function doSearch(q) {
const d = document.getElementById('searchRes');
if(q.length<2) { d.style.display='none'; return; }
try {
const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')"><span>${i.display}</span><span class="exch-badge badge-${i.exch}">${i.exch}</span></div>`).join('');
d.style.display='block';
} catch(e){}
}
async function loadSymbol(root) {
document.getElementById('searchInp').value = root; document.getElementById('searchRes').style.display='none';
document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; document.getElementById('loader').style.display='flex';
document.getElementById('histDate').innerHTML="<option>DATE</option>"; document.getElementById('histTime').innerHTML="<option>TIME</option>";
document.getElementById('secBar').innerHTML=""; HIST_EXPIRIES=[]; SELECTED_HIST_EXP=null;
try {
const r = await fetch(`/api/chain?symbol=${root}`); if(!r.ok) throw new Error("Symbol not found");
const d = await r.json();
CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot; SPOT_PRICE=0;
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b)=>a.e-b.e).slice(0,3);
if(SPOT_SYM==="MCX" && FUT_SYMS.length>0) SPOT_SYM = FUT_SYMS[0].s;
document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX')||SPOT_SYM.includes('-EQ')||SPOT_SYM.startsWith('BSE:'))?"SPOT PRICE":"FUT REF";
setupExpiries(); document.getElementById('loader').style.display='none';
if(IS_HISTORY) loadHistDates();
} catch(e){ alert("Error: "+e.message); document.getElementById('loader').style.display='none'; }
}
function openExpModal() {
if(IS_HISTORY) {
if(HIST_EXPIRIES.length===0){alert("No History Expiries"); return;}
document.getElementById('expGrid').innerHTML = HIST_EXPIRIES.map(e=>`<div class="opt-btn ${e===SELECTED_HIST_EXP?'active':''}" onclick="selectHistExp('${e}')">${e}</div>`).join('');
document.getElementById('expModal').style.display='flex';
} else {
document.getElementById('expModal').style.display='flex';
}
}
function selectHistExp(e) { SELECTED_HIST_EXP=e; updateHistExpUI(); closeModal('expModal'); loadHistData(); }
function setupExpiries() {
if(IS_HISTORY) return;
const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
const exps = [...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
const today = Math.floor(Date.now()/1000)-86400;
const valids = exps;
if(valids.length===0){ document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
ACTIVE_EXP = valids.find(t=>t>today) || valids[0];
updateExpText(ACTIVE_EXP);
document.getElementById('expGrid').innerHTML = valids.map(ts=>`<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</div>`).join('');
buildChain();
}
function selectExp(ts) { ACTIVE_EXP=ts; updateExpText(ts); closeModal('expModal'); document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active')); buildChain(); }
function updateExpText(ts) { document.getElementById('selExpTxt').innerText = ts===0?"SELECT":new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'}); }
function buildChain() {
let rel = IS_HISTORY ? OPT_SYMS : CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
if(!IS_HISTORY) OPT_SYMS = rel;
const ks = [...new Set(rel.map(x=>x.k))].sort((a,b)=>a-b);
if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
document.getElementById('chainBody').innerHTML = ks.map(k=>`
<div class="row" id="row-${k}" data-k="${k}">
<div class="graph-col"><div style="width:160px;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;">
<div class="visual-bar" id="barL-${k}"></div><span class="strike-txt">${k}</span><span class="delta-txt" id="valL-${k}">-</span>
</div></div>
<div class="graph-col"><div style="width:160px;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;align-items:center;">
<div class="visual-bar" id="barR-${k}"></div><span class="strike-txt">${k}</span><span class="delta-txt" id="valR-${k}">-</span>
</div></div>
</div>`).join('');
window.scrolled=false;
if(!IS_HISTORY) restartPoller();
}
function restartPoller() { if(POLLER) clearInterval(POLLER); POLLER=setInterval(pollData, 1500); pollData(); }
async function pollData() {
if(IS_POLLING || !CURRENT_ROOT || IS_HISTORY) return;
IS_POLLING=true;
try {
let syms = [SPOT_SYM, ...FUT_SYMS.map(x=>x.s)];
const ks = Array.from(document.querySelectorAll('.row')).map(r=>parseFloat(r.dataset.k));
if(ks.length>0) {
const ref = SPOT_PRICE>0?SPOT_PRICE:ks[Math.floor(ks.length/2)];
let sk = ks.find(k=>k>=ref); if(sk===undefined) sk=ks[ks.length-1];
ATM_STRIKE=sk; const idx = ks.indexOf(sk);
document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
const r = document.getElementById(`row-${sk}`);
if(r) { r.classList.add('atm'); if(!window.scrolled && SPOT_PRICE>0){r.scrollIntoView({block:'center'}); window.scrolled=true;} }
let maxR = 10;
if(CONFIG.side) CONFIG.side.forEach(i=>{if(i.ranges){const m=Math.max(...i.ranges); if(m>maxR) maxR=m;}});
const vKs = ks.slice(Math.max(0,idx-20), Math.min(ks.length,idx+20));
const cKs = ks.slice(Math.max(0,idx-maxR), Math.min(ks.length,idx+maxR+1));
const all = new Set([...vKs, ...cKs]);
OPT_SYMS.forEach(o=>{if(all.has(o.k)) syms.push(o.s)});
}
const u = [...new Set(syms)].filter(s=>s&&s.length>2);
if(u.length===0) return;
const req = await fetch(`https://api-t1.fyers.in/data/depth?symbol=${u.join(',')}&ohlcv_flag=1`, {headers:{'Authorization':AUTH_TOKEN}});
if(req.status===401) { location.href='/logout'; return; }
const j = await req.json();
if(j.s==='ok') { updateUI(j.d); document.getElementById('statusDot').classList.add('active'); document.getElementById('statusDot').classList.remove('error'); }
} catch(e){ document.getElementById('statusDot').classList.remove('active'); document.getElementById('statusDot').classList.add('error'); }
finally { IS_POLLING=false; }
}
function updateUI(data) {
if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp==='number') { SPOT_PRICE=data[SPOT_SYM].ltp; document.getElementById('spotPrice').innerText=SPOT_PRICE.toFixed(2); }
document.getElementById('futBody').innerHTML = FUT_SYMS.map(f=>{
const d=data[f.s]; if(!d) return '';
const t = (d.totalbuyqty+d.totalsellqty)||1; const bp = (d.totalbuyqty/t)*100;
return `<div class="fut-card"><div style="display:flex;justify-content:space-between;font-weight:700;"><span>${new Date(f.e*1000).toLocaleDateString('en-GB',{day:'numeric',month:'short'})} FUT</span><span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span></div><div class="meter-bg"><div style="width:${bp}%;background:var(--green)"></div><div style="width:${100-bp}%;background:var(--red)"></div></div><div style="display:flex;justify-content:space-between;font-size:10px;color:#666;margin-top:4px;"><span>B: ${fmt(d.totalbuyqty)}</span><span>S: ${fmt(d.totalsellqty)}</span></div></div>`;
}).join('');
const rd = {};
for(const i of OPT_SYMS) {
const d=data[i.s]; if(!d) continue;
if(!rd[i.k]) rd[i.k]={CS:0,CB:0,PS:0,PB:0,CO:0,PO:0,CLTP:0,PLTP:0,CV:0,PV:0};
if(i.t==='CE'){ rd[i.k].CS=d.totalsellqty; rd[i.k].CB=d.totalbuyqty; rd[i.k].CO=d.oi; rd[i.k].CLTP=d.ltp; rd[i.k].CV=d.v; }
if(i.t==='PE'){ rd[i.k].PS=d.totalsellqty; rd[i.k].PB=d.totalbuyqty; rd[i.k].PO=d.oi; rd[i.k].PLTP=d.ltp; rd[i.k].PV=d.v; }
}
LATEST_ROW_DATA = rd;
let mL=0, mR=0; const pR={};
for(const k in rd) {
pR[k] = { l: evaluateFormula(CONFIG.left.formula, rd[k]), r: evaluateFormula(CONFIG.right.formula, rd[k]) };
mL = Math.max(mL, Math.abs(pR[k].l)); mR = Math.max(mR, Math.abs(pR[k].r));
}
for(const k in pR) {
const r = pR[k];
const vl = document.getElementById(`valL-${k}`);
if(vl) { vl.innerText=(r.l>0?"+":"")+fmt(r.l); vl.style.color=r.l>=0?'var(--green)':'var(--red)';
const bl=document.getElementById(`barL-${k}`); if(mL>0){ bl.style.left='0%'; bl.style.width=(Math.abs(r.l)/mL)*100+'%'; bl.style.background=r.l>=0?'var(--green-soft)':'var(--red-soft)'; } }
const vr = document.getElementById(`valR-${k}`);
if(vr) { vr.innerText=(r.r>0?"+":"")+fmt(r.r); vr.style.color=r.r>=0?'var(--green)':'var(--red)';
const br=document.getElementById(`barR-${k}`); if(mR>0){ br.style.left='0%'; br.style.width=(Math.abs(r.r)/mR)*100+'%'; br.style.background=r.r>=0?'var(--green-soft)':'var(--red-soft)'; } }
}
if(IS_HISTORY && SPOT_PRICE>0) {
const ks = Object.keys(rd).map(parseFloat).sort((a,b)=>a-b);
let sk = ks.find(k=>k>=SPOT_PRICE); if(sk===undefined) sk=ks[ks.length-1];
document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
const r = document.getElementById(`row-${sk}`); if(r) r.classList.add('atm');
}
if(SPOT_PRICE>0 && CONFIG.side) {
const ks = Object.keys(rd).map(parseFloat).sort((a,b)=>a-b);
const ls = ks.filter(k=>k<SPOT_PRICE); const us = ks.filter(k=>k>=SPOT_PRICE);
CONFIG.side.forEach((it,ix)=>{
it.ranges.forEach((rng,rx)=>{
const el=document.getElementById(`sv-${ix}-${rx}`);
if(el) {
const ts = [...ls.slice(-rng), ...us.slice(0,rng)];
let t=0; for(const k of ts) if(rd[k]) t+=evaluateFormula(it.formula, rd[k]);
el.innerText=(t>0?"+":"")+fmt(t); el.style.color=t>=0?'var(--green)':'var(--red)';
}
});
});
}
}
function fmt(n) {
if(!n) return '0'; let a=Math.abs(n);
if(a>=10000000) return (n/10000000).toFixed(2)+'Cr';
if(a>=100000) return (n/100000).toFixed(2)+'L';
if(a>=1000) return (n/1000).toFixed(1)+'k';
return n.toLocaleString();
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860)