topsecrettraders's picture
Update app.py
6481e77 verified
Raw
History Blame Contribute Delete
74.3 kB
import pandas as pd
import requests
import uvicorn
import asyncio
import re
from datetime import timedelta
from fastapi import FastAPI, Body
from fastapi.responses import HTMLResponse, JSONResponse
from typing import Optional, List
# ==========================================
# 1. CONFIGURATION
# ==========================================
SB_URL = "https://yoxkqqteouirysaiyozp.supabase.co"
SB_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlveGtxcXRlb3VpcnlzYWl5b3pwIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTg2NDM4OSwiZXhwIjoyMDg3NDQwMzg5fQ.pIGvZSk--Pks0DXcIwAKqIjudEJp3y_arQfltaiZKiQ"
HEADERS = {
"apikey": SB_KEY,
"Authorization": f"Bearer {SB_KEY}",
"Content-Type": "application/json",
"Prefer": "return=representation"
}
app = FastAPI()
# ==========================================
# 2. HELPER FUNCTIONS
# ==========================================
def sb_get(endpoint: str, params: dict = None):
try:
url = f"{SB_URL}/rest/v1{endpoint}"
r = requests.get(url, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
except Exception as e:
print(f"DB Error GET ({endpoint}): {e}")
return[]
def sb_rpc(func_name: str, params: dict = None):
if params is None:
params = {}
try:
url = f"{SB_URL}/rest/v1/rpc/{func_name}"
r = requests.post(url, headers=HEADERS, json=params)
r.raise_for_status()
return r.json()
except Exception as e:
print(f"DB Error RPC ({func_name}): {e}")
return[]
# ==========================================
# 3. API ROUTES - METADATA
# ==========================================
@app.get("/")
def home():
return HTMLResponse(HTML_TEMPLATE)
@app.get("/api/dates")
def get_dates():
data = sb_rpc("get_available_dates", {"p_limit": 30})
dates = [row['available_date'] for row in data]
return JSONResponse(dates)
@app.get("/api/roots")
def get_roots(date: str, q: Optional[str] = None):
params = {"p_date": date}
if q: params["p_q"] = q.upper()
data = sb_rpc("get_roots_for_date", params)
roots = [row['root_name'] for row in data]
return JSONResponse(roots)
@app.get("/api/auto_config")
def get_auto_config(date: str, root: str):
root = root.upper()
exps_data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
all_expiries = [row['expiry_date'] for row in exps_data if row['expiry_date'] != 'MARKET']
valid_expiries =[e for e in all_expiries if e >= date]
if not valid_expiries:
valid_expiries = all_expiries
response = {"expiries": all_expiries, "current": None, "next": None}
def find_fut_rpc(exp):
inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": exp})
instruments = [r['instrument_name'] for r in inst_data]
for i in instruments:
if 'FUT' in i: return i
return instruments[0] if instruments else None
if len(valid_expiries) > 0:
exp_c = valid_expiries[0]
response["current"] = {"expiry": exp_c, "instrument": find_fut_rpc(exp_c)}
if len(valid_expiries) > 1:
exp_n = valid_expiries[1]
response["next"] = {"expiry": exp_n, "instrument": find_fut_rpc(exp_n)}
return JSONResponse(response)
@app.get("/api/expiries")
def get_expiries(date: str, root: str):
root = root.upper()
data = sb_rpc("get_expiries_for_root", {"p_date": date, "p_root": root})
exps = [row['expiry_date'] for row in data]
return JSONResponse(exps)
@app.get("/api/instruments")
def get_instruments(date: str, root: str, expiry: str):
root = root.upper()
data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
instruments = [row['instrument_name'] for row in data]
spot_fut = []
options =[]
for i in instruments:
if i.endswith('CE') or i.endswith('PE'):
options.append(i)
else:
spot_fut.append(i)
return JSONResponse({"spot_fut": sorted(spot_fut), "options": sorted(options)})
# ==========================================
# 4. API ROUTES - DATA FETCHING
# ==========================================
@app.post("/api/fetch_series")
async def fetch_series(
date: str = Body(...),
root: str = Body(...),
expiry: str = Body(...),
instrument: str = Body(""),
timeframe: str = Body(...),
start_time: str = Body(...),
end_time: str = Body(...),
mode: str = Body("normal"),
atm_range: int = Body(5),
required_vars: List[str] = Body(default=None)
):
root = root.upper()
t_start_dt = pd.to_datetime(f"{date} {start_time}:00")
t_end_dt = pd.to_datetime(f"{date} {end_time}:59")
if required_vars is None:
required_vars =["P", "B", "S", "CB", "CS", "PB", "PS", "V", "OI"]
required_vars = list(set(required_vars) | {"P"})
if mode == "advanced":
inst_data = sb_rpc("get_instruments_for_expiry", {"p_date": date, "p_root": root, "p_expiry": expiry})
instruments = [r['instrument_name'] for r in inst_data]
if not instruments:
return JSONResponse({"error": f"No data found for expiry {expiry}", "labels": [], "P":[]})
ref_instrument = instrument
if not ref_instrument:
spot_keys =[k for k in instruments if k.endswith('-INDEX') or k.endswith('-EQ')]
fut_keys =[k for k in instruments if 'FUT' in k]
if spot_keys:
ref_instrument = spot_keys[0]
elif fut_keys:
ref_instrument = fut_keys[0]
else:
others =[k for k in instruments if not k.endswith('CE') and not k.endswith('PE')]
ref_instrument = others[0] if others else instruments[0]
# Robust Python Logic for Strike Pricing mapping
strike_map = {}
for inst in instruments:
if inst.endswith('CE') or inst.endswith('PE'):
clean_inst = inst.split(":")[-1]
if clean_inst.startswith(root):
remainder = clean_inst[len(root):]
# Fyers date codes are always 5-6 characters (e.g. 26310 or 26MAR).
# By skipping the first 5 chars, we isolate the true strike perfectly.
match = re.search(r'(\d+(?:\.\d+)?)(?:CE|PE)$', remainder[5:])
if match:
strike_map[inst] = float(match.group(1))
continue
# Fallback purely numerical extraction right before CE/PE
matches = re.findall(r'(\d+(?:\.\d+)?)(?=CE$|PE$)', clean_inst)
if matches:
strike_map[inst] = float(matches[-1])
rpc_name = "get_advanced_chart_data"
base_params = {
"p_root": root,
"p_expiry": expiry,
"p_ref_instrument": ref_instrument,
"p_atm_range": atm_range,
"p_strike_map": strike_map
}
else:
if not instrument:
return JSONResponse({"error": "No Instrument Selected.", "labels": [], "P":[]})
rpc_name = "get_normal_chart_data"
base_params = {
"p_root": root,
"p_expiry": expiry,
"p_instrument": instrument
}
# =========================================================
# CHUNKING: Massively reduced to 1 Hour to speed up backend query times.
# =========================================================
async def fetch_chunk(start_dt, end_dt):
params = base_params.copy()
params["p_start_time"] = start_dt.strftime("%Y-%m-%d %H:%M:%S")
params["p_end_time"] = end_dt.strftime("%Y-%m-%d %H:%M:%S")
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, sb_rpc, rpc_name, params)
tasks =[]
curr = t_start_dt
while curr <= t_end_dt:
nxt = curr + timedelta(hours=1) - timedelta(seconds=1)
if nxt > t_end_dt:
nxt = t_end_dt
tasks.append(fetch_chunk(curr, nxt))
curr += timedelta(hours=1)
chunk_results = await asyncio.gather(*tasks)
all_records =[]
for res in chunk_results:
if isinstance(res, list):
all_records.extend(res)
if not all_records:
return JSONResponse({"error": "No valid data found in range.", "labels": [], "P":[]})
df = pd.DataFrame(all_records)
df.rename(columns={
"tick_ts": "ts",
"b_qty": "B",
"s_qty": "S",
"vol": "V",
"oi": "OI",
"cb": "CB",
"cs": "CS",
"pb": "PB",
"ps": "PS",
"price": "P"
}, inplace=True)
df['ts'] = pd.to_datetime(df['ts'])
df.drop_duplicates(subset=['ts'], inplace=True)
df.set_index('ts', inplace=True)
df.sort_index(inplace=True)
tf_map = {
"1min": "1min", "3min": "3min", "5min": "5min",
"15min": "15min", "30min": "30min", "1hour": "1h"
}
panda_tf = tf_map.get(timeframe, "1min")
agg_dict = {
"P": "last",
"V": "sum",
"OI": "last",
"B": "last",
"S": "last",
"CB": "last",
"CS": "last",
"PB": "last",
"PS": "last",
}
if mode == "advanced":
agg_dict["strike_breakdown"] = "last"
existing_agg = {k: v for k, v in agg_dict.items() if k in df.columns}
resampled = df.resample(panda_tf).agg(existing_agg).ffill().fillna(0)
response_data = {
"labels": resampled.index.strftime('%H:%M').tolist()
}
for var_key in["P", "B", "S", "V", "OI", "CB", "CS", "PB", "PS"]:
if var_key in required_vars and var_key in resampled.columns:
response_data[var_key] =[round(float(v), 2) for v in resampled[var_key].tolist()]
if mode == "advanced" and "strike_breakdown" in resampled.columns:
response_data["strike_breakdown"] = [
x if isinstance(x, list) else [] for x in resampled["strike_breakdown"].tolist()
]
return JSONResponse(response_data)
# ==========================================
# 5. FRONTEND TEMPLATE
# ==========================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DepthChain PRO</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-body: #0d1117; --bg-header: #161b22; --bg-card: #0d1117;
--bg-panel: #21262d; --bg-input: #161b22; --border: #30363d;
--border-hover: #8b949e; --text-primary: #e6edf3;
--text-secondary: #8b949e; --accent: #2f81f7;
--success: #238636; --danger: #da3633; --warning: #d29922;
--font-main: 'Inter', sans-serif; --font-mono: 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
body {
background: var(--bg-body); color: var(--text-primary);
font-family: var(--font-main); display: flex; flex-direction: column;
height: 100vh; overflow: hidden; font-size: 13px;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-body); }
::-webkit-scrollbar-thumb { background: var(--bg-panel); border: 2px solid var(--bg-body); border-radius: 6px; }
.top-bar {
height: 52px; background: var(--bg-header); border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; flex-shrink: 0;
}
.brand { font-weight: 700; font-size: 15px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
.brand span { color: var(--accent); background: rgba(47,129,247,0.15); padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.controls { display: flex; align-items: center; gap: 12px; }
select, button, input {
background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
padding: 0 12px; height: 32px; border-radius: 6px; font-size: 12px; cursor: pointer; transition: 0.2s;
}
select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 1px rgba(47,129,247,0.2); }
button:hover { background: var(--bg-panel); border-color: var(--border-hover); }
button.primary { background: var(--accent); border-color: var(--accent); color: white; }
button.primary:hover { background: #58a6ff; }
button.danger { color: var(--danger); border-color: var(--border); background: transparent; }
button.danger:hover { background: rgba(218,54,51,0.1); }
.layout-toggle { display: flex; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
.layout-btn { width: 30px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
.layout-btn.active { background: var(--bg-panel); color: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
.main-container { flex: 1; display: flex; overflow-y: auto; overflow-x: hidden; }
.column { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
.column:last-child { border-right: none; }
.add-chart-btn {
height: 40px; border: none; border-top: 1px dashed var(--border);
background: transparent; color: var(--text-secondary); width: 100%; font-weight: 600;
}
.add-chart-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
.chart-card { background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; position: relative; }
.card-header {
height: 38px; border-bottom: 1px solid var(--border); display: flex; align-items: center;
justify-content: space-between; padding: 0 12px; background: var(--bg-header); flex-shrink: 0;
}
.card-info { display: flex; gap: 10px; align-items: center; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; }
.inst-label { color: var(--accent); font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
.card-meta { color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-secondary); }
.status-dot.live { background: #3fb950; box-shadow: 0 0 6px rgba(63,185,80,0.6); animation: pulse 2s infinite; }
.status-dot.hist { background: var(--accent); }
.status-dot.retry { background: var(--warning); }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
.tool-btn { width: 24px; height: 24px; border: none; background: transparent; color: var(--text-secondary); border-radius: 4px; }
.tool-btn:hover { background: var(--bg-panel); color: var(--text-primary); }
.card-body { flex: 1; position: relative; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; }
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100%; color: var(--text-secondary);
background: radial-gradient(circle, var(--bg-panel) 0%, var(--bg-card) 100%);
text-align: center; padding: 20px;
}
/* MODAL BASE */
.modal-backdrop {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); z-index: 100; display: none;
justify-content: center; align-items: center;
}
.modal {
background: var(--bg-header); border: 1px solid var(--border); border-radius: 10px;
width: 560px; max-width: 95%; max-height: 88vh; display: flex; flex-direction: column;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.modal.wide { width: 920px; }
.modal-head { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; font-weight: 700; }
.modal-body { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
.modal-foot { padding: 16px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-card); }
.form-section label { font-size: 11px; text-transform: uppercase; color: var(--text-secondary); font-weight: 600; margin-bottom: 6px; display: block; }
.input-wrapper { position: relative; width: 100%; }
.input-arrow { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--text-secondary); font-size: 10px; }
.custom-dd {
position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-panel); border: 1px solid var(--border);
border-radius: 6px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin-top: 4px;
}
.custom-dd-opt { padding: 8px 12px; font-size: 12px; cursor: pointer; color: var(--text-primary); border-bottom: 1px solid var(--border); }
.custom-dd-opt:last-child { border-bottom: none; }
.custom-dd-opt:hover { background: var(--bg-input); color: var(--accent); }
.custom-dd-group { padding: 6px 12px; font-size: 10px; font-weight: 700; color: var(--text-secondary); background: var(--bg-body); text-transform: uppercase; }
.series-row {
background: var(--bg-panel); padding: 12px; border-radius: 8px;
border: 1px solid var(--border); margin-bottom: 10px;
}
.series-row-top { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
.series-row-bottom { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.series-row-cond { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px; border: 1px dashed var(--border); }
.s-form, .s-lbl, .s-axis, .s-cond, .s-thresh, .s-label-true, .s-label-false {
background: var(--bg-input); border: 1px solid var(--border); color: var(--text-primary);
border-radius: 6px; padding: 0 10px; height: 30px; font-size: 12px; transition: 0.2s;
}
.s-form:focus, .s-lbl:focus { border-color: var(--accent); }
.color-input {
padding: 0; width: 34px; border: 1px solid var(--border); background: var(--bg-input);
height: 30px; cursor: pointer; border-radius: 6px; overflow: hidden;
}
.color-input::-webkit-color-swatch-wrapper { padding: 0; }
.color-input::-webkit-color-swatch { border: none; }
.cond-label { font-size: 10px; color: var(--text-secondary); font-weight: 600; white-space: nowrap; }
.segmented-control {
display: flex; background: var(--bg-body); border: 1px solid var(--border);
border-radius: 6px; padding: 2px; overflow: hidden; margin-bottom: 6px;
}
.seg-btn {
flex: 1; text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600;
color: var(--text-secondary); cursor: pointer; border: none; background: transparent;
transition: 0.2s; border-radius: 4px;
}
.seg-btn.active { background: var(--bg-panel); color: var(--accent); }
.info-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; border-radius: 50%; background: var(--bg-panel);
color: var(--text-secondary); font-size: 10px; font-weight: bold; cursor: help;
border: 1px solid var(--border); margin-left: 8px; transition: 0.2s;
}
.info-icon:hover { color: var(--text-primary); border-color: var(--accent); }
/* BREAKDOWN TABLE CSS */
.breakdown-scroll { overflow-x: auto; overflow-y: auto; max-height: 60vh; border-radius: 6px; border: 1px solid var(--border); }
.breakdown-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; }
.breakdown-table th {
background: var(--bg-panel); color: var(--text-secondary); padding: 8px 10px;
border-bottom: 2px solid var(--border); text-align: right; white-space: nowrap; position: sticky; top: 0; z-index: 2;
}
.breakdown-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); text-align: right; color: var(--text-primary); }
.breakdown-table tr:hover td { background: var(--bg-panel); }
/* EXPANDABLE SUB-TABLE */
.expand-btn {
background: transparent; border: none; color: var(--accent); cursor: pointer;
font-size: 11px; font-weight: bold; margin-right: 8px; transition: 0.2s; display: inline-block;
}
.expand-btn.open { transform: rotate(90deg); color: var(--text-primary); }
.sub-table-container { padding: 0 !important; border-bottom: 2px solid var(--accent); }
.sub-table { width: 100%; border-collapse: collapse; background: #07090d; }
.sub-table th { background: rgba(47,129,247,0.1); padding: 6px 12px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); color: var(--text-secondary); z-index: 1; }
.sub-table td { padding: 6px 12px; border-bottom: 1px solid var(--border); font-size: 10.5px; }
.sub-table tr:hover td { background: rgba(255,255,255,0.03); }
.val-pos { color: #3fb950; }
.val-neg { color: #f85149; }
</style>
</head>
<body>
<div class="top-bar">
<div class="brand">DepthChain <span>PRO</span></div>
<div class="controls">
<select id="globalDate" onchange="App.onDateChange()" style="min-width:140px;"></select>
<div style="position:relative;">
<select id="globalMode" onchange="App.onModeChange()" style="padding-left:28px; font-weight:600;">
<option value="LIVE">LIVE</option>
<option value="HIST">HISTORY</option>
</select>
<div id="modeDot" class="status-dot live" style="position:absolute; left:10px; top:12px; pointer-events:none;"></div>
</div>
<div style="width:1px; height:20px; background:var(--border);"></div>
<div class="layout-toggle">
<button class="layout-btn" onclick="App.setLayout(1)">1</button>
<button class="layout-btn" onclick="App.setLayout(2)">2</button>
<button class="layout-btn active" onclick="App.setLayout(3)">3</button>
<button class="layout-btn" onclick="App.setLayout(4)">4</button>
</div>
<button onclick="Config.openDefaults()">Settings</button>
<button class="danger" onclick="App.reset()">Reset</button>
</div>
</div>
<div class="main-container" id="gridRoot"></div>
<!-- ===== CONFIG MODAL ===== -->
<div class="modal-backdrop" id="configModal">
<div class="modal">
<div class="modal-head">
<span id="cfgTitle">Configure Chart</span>
<button class="tool-btn" onclick="Config.close()">✕</button>
</div>
<div class="modal-body">
<div id="cfgLoading" style="display:none; text-align:center; color:var(--text-secondary);">Loading...</div>
<div id="cfgContent">
<div id="cfgGlobalOnly" style="display:none;">
<div class="form-section">
<label>Refresh Rate (Seconds)</label>
<input type="number" id="cfgRefresh" value="15" min="5">
</div>
<div style="height:1px; background:var(--border); margin:16px 0;"></div>
</div>
<div id="cfgChartOnly">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div class="form-section">
<label>Symbol</label>
<div class="input-wrapper">
<input type="text" id="cfgRoot" onfocus="CustomDD.open('cfgRoot')" oninput="CustomDD.filter('cfgRoot')" style="width:100%; text-transform:uppercase;" placeholder="Type or select..." autocomplete="off">
<div class="input-arrow">▼</div>
<div id="dd-cfgRoot" class="custom-dd"></div>
</div>
</div>
<div class="form-section">
<label>Expiry</label>
<div class="input-wrapper">
<input type="text" id="cfgExp" onfocus="CustomDD.open('cfgExp')" oninput="CustomDD.filter('cfgExp')" style="width:100%; text-transform:uppercase;" placeholder="Type or select..." autocomplete="off">
<div class="input-arrow">▼</div>
<div id="dd-cfgExp" class="custom-dd"></div>
</div>
</div>
</div>
<div class="form-section" style="margin-top:8px;">
<label>Chart Mode</label>
<div class="segmented-control" id="cfgModeToggle">
<button class="seg-btn active" onclick="Config.setMode('normal')">Normal Mode</button>
<button class="seg-btn" onclick="Config.setMode('advanced')">Advanced Mode</button>
</div>
</div>
<div class="form-section" id="cfgInstrumentContainer">
<label id="lblInst">Instrument</label>
<div class="input-wrapper">
<input type="text" id="cfgInst" onfocus="CustomDD.open('cfgInst')" oninput="CustomDD.filter('cfgInst')" style="width:100%; text-transform:uppercase;" placeholder="Type or select..." autocomplete="off">
<div class="input-arrow">▼</div>
<div id="dd-cfgInst" class="custom-dd"></div>
</div>
<div id="advInstHint" style="display:none; font-size:10px; color:var(--text-secondary); margin-top:4px; line-height:1.4;">
* Advanced Mode: select the Spot/Future as price reference. All CE/PE within ATM ± N range will be strictly aggregated (Total 2N strikes per option type).
</div>
</div>
<div id="cfgAdvancedFields" style="display:none;">
<div class="form-section" style="margin-top:12px;">
<label style="color:var(--accent);">ATM (+/-) N Strike Range</label>
<input type="number" id="cfgAtmRange" value="5" min="1" max="50" style="width:100%;">
<div style="font-size:10px; color:var(--text-secondary); margin-top:4px;">
Will exactly aggregate N strikes ITM and N strikes OTM (ATM counts as 1st OTM) per tick.
</div>
</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:12px;">
<div class="form-section">
<label>Timeframe</label>
<select id="cfgTF" style="width:100%">
<option value="1min">1 Minute</option>
<option value="3min">3 Minutes</option>
<option value="5min">5 Minutes</option>
<option value="15min">15 Minutes</option>
<option value="30min">30 Minutes</option>
<option value="1hour">1 Hour</option>
</select>
</div>
<div class="form-section">
<label>Range (HH:MM)</label>
<div style="display:flex; gap:8px;">
<input type="time" id="cfgStart" value="09:15">
<input type="time" id="cfgEnd" value="15:30">
</div>
</div>
</div>
<div style="height:1px; background:var(--border); margin:12px 0 8px;"></div>
</div>
<!-- PRESET MANAGER -->
<div class="form-section" style="background:var(--bg-panel); padding:10px; border-radius:6px; border:1px solid var(--border); margin-bottom:12px;">
<label>Quick Models</label>
<div style="display:flex; gap:8px;">
<select id="cfgPresetsDropdown" style="flex:1;" onchange="Config.loadPreset()"></select>
<button class="primary" onclick="Config.savePreset()" title="Save Current Config as Model">Save</button>
<button class="danger" onclick="Config.deletePreset()" title="Delete" style="width:32px; padding:0;">✕</button>
</div>
</div>
<!-- SERIES CONFIG -->
<div class="form-section">
<div style="display:flex; align-items:center; margin-bottom:6px;">
<label style="margin:0;">Plot Lines</label>
<span class="info-icon" id="infoIcon" title="Variables: $P=Price, $B=Buy, $S=Sell (Fut/Eq) | $CB=Call Buy, $CS=Call Sell, $PB=Put Buy, $PS=Put Sell (Options)">i</span>
</div>
<div id="cfgSeries"></div>
<button style="width:100%; margin-top:8px; border:1px dashed var(--border); background:transparent; color:var(--text-secondary); height:32px; font-weight:600;" onclick="Config.addSeriesRow()">+ Add Line</button>
</div>
</div>
</div>
<div class="modal-foot">
<button style="background:transparent; border:1px solid var(--border);" onclick="Config.close()">Cancel</button>
<button class="primary" onclick="Config.saveChartConfig()">Apply</button>
</div>
</div>
</div>
<!-- ===== BREAKDOWN MODAL ===== -->
<div class="modal-backdrop" id="breakdownModal">
<div class="modal wide">
<div class="modal-head">
<span id="bdTitle">Calculation Breakdown</span>
<button class="tool-btn" onclick="document.getElementById('breakdownModal').style.display='none'">✕</button>
</div>
<div class="modal-body" style="padding:12px;">
<div style="font-size:11px; color:var(--text-secondary); margin-bottom:8px;" id="bdMeta"></div>
<div class="breakdown-scroll">
<table class="breakdown-table" id="bdTable"></table>
</div>
</div>
</div>
</div>
<script>
// ===================================================
// GLOBAL TOGGLE SCRIPT
// ===================================================
window.toggleSub = function(i) {
const tr = document.getElementById(`sub-${i}`);
const btn = document.getElementById(`btn-sub-${i}`);
if (tr.style.display === 'none') {
tr.style.display = 'table-row';
btn.classList.add('open');
} else {
tr.style.display = 'none';
btn.classList.remove('open');
}
};
// ===================================================
// HELPERS
// ===================================================
document.addEventListener('click', e => {
if (!e.target.closest('.input-wrapper'))
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
});
function safeVal(data, key, i) {
return (data && data[key] && data[key][i] !== undefined) ? (data[key][i] || 0) : 0;
}
function evalFormula(formula, vars) {
let f = formula
.replace(/\\$CB/g, vars.CB).replace(/\\$CS/g, vars.CS)
.replace(/\\$PB/g, vars.PB).replace(/\\$PS/g, vars.PS)
.replace(/\\$OI/g, vars.OI).replace(/\\$P/g, vars.P)
.replace(/\\$V/g, vars.V)
.replace(/\\$B/g, vars.B).replace(/\\$S/g, vars.S);
try { return new Function('return ' + f)(); } catch { return null; }
}
function extractRequiredVars(seriesList) {
const vars = new Set(['P']);
seriesList.forEach(s => {
const f = s.formula || '';
if (f.includes('$CB')) vars.add('CB');
if (f.includes('$CS')) vars.add('CS');
if (f.includes('$PB')) vars.add('PB');
if (f.includes('$PS')) vars.add('PS');
if (/\\$B(?!S)/.test(f) || f.includes('$B ') || f.endsWith('$B') || f.includes('$B-') || f.includes('$B+')) vars.add('B');
if (/\\$S(?!$)/.test(f) || f.includes('$S ') || f.endsWith('$S') || f.includes('$S-') || f.includes('$S+')) vars.add('S');
if (f.includes('$V')) vars.add('V');
if (f.includes('$OI')) vars.add('OI');
});
return Array.from(vars);
}
// ===================================================
// CUSTOM DROPDOWN
// ===================================================
const CustomDD = {
data: { cfgRoot: [], cfgExp: [], cfgInst:[] },
recent: { cfgRoot:[] },
init() {
const saved = localStorage.getItem('dc_recent_symbols');
if (saved) try { this.recent.cfgRoot = JSON.parse(saved); } catch(e){}
},
clear(id) { this.data[id] =[]; document.getElementById(id).value = ''; },
set(id, items, groupName = null) {
if (!groupName)
this.data[id] = items.map(i => ({ val: i, group: null }));
else
this.data[id].push(...items.map(i => ({ val: i, group: groupName })));
},
async open(id) {
document.querySelectorAll('.custom-dd').forEach(el => el.style.display = 'none');
document.getElementById(id).select();
this.render(id, '');
document.getElementById(`dd-${id}`).style.display = 'block';
},
async filter(id) {
const query = document.getElementById(id).value.toUpperCase();
if (id === 'cfgRoot' && query.length >= 2) {
const res = await fetch(`/api/roots?date=${App.state.date}&q=${query}`);
const results = await res.json();
this.set(id, results);
}
this.render(id, query);
document.getElementById(`dd-${id}`).style.display = 'block';
},
render(id, query) {
const el = document.getElementById(`dd-${id}`);
const q = query.toLowerCase();
let pool = [...this.data[id]];
if (id === 'cfgRoot' && q === '') {
const recent = this.recent.cfgRoot.map(r => ({ val: r, group: 'Recent' }));
pool = [...recent, ...pool];
}
const filtered = pool
.filter((v, i, a) => a.findIndex(t => t.val === v.val) === i)
.filter(item => item.val.toLowerCase().includes(q));
if (!filtered.length) {
el.innerHTML = '<div class="custom-dd-opt" style="color:var(--text-secondary);">No matches</div>';
return;
}
let html = '', currentGroup = null;
filtered.forEach(item => {
if (item.group && item.group !== currentGroup) {
html += `<div class="custom-dd-group">${item.group}</div>`;
currentGroup = item.group;
}
html += `<div class="custom-dd-opt" onclick="CustomDD.pick('${id}', '${item.val}')">${item.val}</div>`;
});
el.innerHTML = html;
},
pick(id, val) {
document.getElementById(id).value = val;
document.getElementById(`dd-${id}`).style.display = 'none';
if (id === 'cfgRoot') {
if (!this.recent.cfgRoot.includes(val)) {
this.recent.cfgRoot.unshift(val);
if (this.recent.cfgRoot.length > 10) this.recent.cfgRoot.pop();
localStorage.setItem('dc_recent_symbols', JSON.stringify(this.recent.cfgRoot));
}
Config.onRootChange();
}
if (id === 'cfgExp') Config.onExpChange();
}
};
// ===================================================
// APP STATE
// ===================================================
const App = {
state: { date: null, mode: 'LIVE', columns: 3, layout: [[], [],[]], charts: {}, refreshRate: 15 },
presets: {
"Basic Flow (Fut/Eq)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$B', label: 'Buy', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$S', label: 'Sell', axis: 'right', color1: '#da3633', cond: 'none' }
],
"Net Flow (Fut/Eq)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$B - $S', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BUY', labelFalse: 'SELL' }
],
"Call Flow (Options)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#da3633', cond: 'none' }
],
"Put Flow (Options)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#da3633', cond: 'none' }
],
"Aggregated Flow (Options)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
],
"Net Flow (Options)":[
{ formula: '$P', label: 'Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
],
"Price & Flows (Advanced)":[
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB + $PS', label: 'Bull Side', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$PB + $CS', label: 'Bear Side', axis: 'right', color1: '#da3633', cond: 'none' }
],
"Net Flow (Advanced)":[
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB + $PS - ($PB + $CS)', label: 'Net Flow', axis: 'right', color1: '#238636', cond: '>', thresh: '0', color2: '#da3633', labelTrue: 'BULLISH', labelFalse: 'BEARISH' }
],
"Components (Advanced)":[
{ formula: '$P', label: 'Spot Price', axis: 'left', color1: '#2f81f7', cond: 'none' },
{ formula: '$CB', label: 'Call Buy', axis: 'right', color1: '#238636', cond: 'none' },
{ formula: '$CS', label: 'Call Sell', axis: 'right', color1: '#3fb950', cond: 'none' },
{ formula: '$PB', label: 'Put Buy', axis: 'right', color1: '#da3633', cond: 'none' },
{ formula: '$PS', label: 'Put Sell', axis: 'right', color1: '#f85149', cond: 'none' }
]
},
async init() {
CustomDD.init();
await this.loadDates();
const savedPresets = localStorage.getItem('dc_pro_v4_presets');
if (savedPresets) try { this.presets = { ...this.presets, ...JSON.parse(savedPresets) }; } catch(e) {}
const saved = localStorage.getItem('dc_pro_v4_state');
if (saved) {
try {
const s = JSON.parse(saved);
this.state = { ...this.state, ...s };
} catch(e) { this.initDefault(); }
} else {
this.initDefault();
}
const dSel = document.getElementById('globalDate');
if (this.state.date && Array.from(dSel.options).some(o => o.value === this.state.date))
dSel.value = this.state.date;
else if (dSel.options.length)
this.state.date = dSel.options[0].value;
document.getElementById('globalMode').value = this.state.mode;
this.updateModeUI();
this.renderGrid();
this.startLoop();
},
initDefault() {
this.state.columns = 3;
this.state.layout = [[], [],[]];
this.state.refreshRate = 15;
for (let i = 0; i < 3; i++) {
this.addChartInternal(i);
this.addChartInternal(i);
}
},
async loadDates() {
try {
const res = await fetch('/api/dates');
const dates = await res.json();
document.getElementById('globalDate').innerHTML = dates.map(d => `<option value="${d}">${d}</option>`).join('');
if (!this.state.date && dates.length) this.state.date = dates[0];
} catch(e) {}
},
addChartInternal(colIdx) {
const id = 'c-' + Math.random().toString(36).substr(2, 6);
this.state.layout[colIdx].push(id);
this.state.charts[id] = { config: {}, instance: null, lastData: null };
return id;
},
save() {
const cleanCharts = {};
for (let id in this.state.charts)
cleanCharts[id] = { config: this.state.charts[id].config };
localStorage.setItem('dc_pro_v4_state', JSON.stringify({
date: this.state.date, mode: this.state.mode,
columns: this.state.columns, layout: this.state.layout,
refreshRate: this.state.refreshRate, charts: cleanCharts
}));
},
reset() {
if (confirm('Reset Workspace?')) {
localStorage.removeItem('dc_pro_v4_state');
location.reload();
}
},
setLayout(n) {
const all = this.state.layout.flat();
this.state.columns = n;
this.state.layout = Array.from({ length: n }, () =>[]);
all.forEach((id, i) => this.state.layout[i % n].push(id));
this.renderGrid(); this.save();
},
renderGrid() {
document.querySelectorAll('.layout-btn').forEach((b, i) => b.classList.toggle('active', (i + 1) === this.state.columns));
const container = document.getElementById('gridRoot');
container.innerHTML = '';
this.state.layout.forEach((ids, colIdx) => {
const col = document.createElement('div');
col.className = 'column';
const viewH = window.innerHeight - 52;
const count = ids.length;
ids.forEach(id => {
if (this.state.charts[id]?.instance) {
this.state.charts[id].instance.destroy();
this.state.charts[id].instance = null;
}
const card = this.createCardDOM(id);
card.style.height = (count > 0 && count <= 3) ? `calc(${viewH / count}px - 1px)` : '300px';
col.appendChild(card);
setTimeout(() => ChartLogic.plot(id), 0);
});
const btn = document.createElement('button');
btn.className = 'add-chart-btn';
btn.innerText = '+ Add Chart';
btn.onclick = () => { this.addChartInternal(colIdx); this.renderGrid(); this.save(); };
col.appendChild(btn);
container.appendChild(col);
});
},
createCardDOM(id) {
const div = document.createElement('div');
div.className = 'chart-card';
div.id = `card-${id}`;
const cfg = this.state.charts[id]?.config || {};
const isCfg = cfg && cfg.root;
const modeTag = cfg.chartMode === 'advanced'
? `<span class="inst-label">ATM ± ${cfg.atmRange || 5}</span>`
: `<span class="inst-label">${cfg.instrument || ''}</span>`;
div.innerHTML = `
<div class="card-header">
<div class="card-info">
<div class="status-dot ${isCfg ? (this.state.mode === 'LIVE' ? 'live' : 'hist') : ''}" id="dot-${id}"></div>
<span>${isCfg ? cfg.root : 'New Chart'}</span>
${isCfg ? `${modeTag}<span class="card-meta">${cfg.expiry}</span>` : ''}
</div>
<div style="display:flex; align-items:center; gap:2px;">
${isCfg ? `<button class="tool-btn" onclick="Breakdown.open('${id}')" title="Calculation Breakdown">📋</button>` : ''}
<button class="tool-btn" onclick="Config.open('${id}')">⚙</button>
<button class="tool-btn" style="color:var(--danger)" onclick="App.removeChart('${id}')">✕</button>
</div>
</div>
<div class="card-body" id="body-${id}">
${!isCfg ? `
<div class="empty-state">
<div style="font-size:24px; opacity:0.3;">📊</div>
<button style="background:transparent; border:1px solid var(--border); padding:5px 10px; border-radius:4px; color:var(--text-secondary); margin-top:8px;" onclick="Config.open('${id}')">Configure</button>
</div>
` : `<canvas id="canvas-${id}"></canvas>`}
</div>`;
return div;
},
removeChart(id) {
if (!confirm('Delete chart?')) return;
this.state.layout = this.state.layout.map(c => c.filter(x => x !== id));
if (this.state.charts[id]?.instance) this.state.charts[id].instance.destroy();
delete this.state.charts[id];
this.renderGrid(); this.save();
},
onDateChange() {
this.state.date = document.getElementById('globalDate').value;
this.save(); this.refreshAll();
},
onModeChange() {
this.state.mode = document.getElementById('globalMode').value;
this.updateModeUI(); this.save(); this.refreshAll();
},
updateModeUI() {
document.getElementById('modeDot').className = `status-dot ${this.state.mode === 'LIVE' ? 'live' : 'hist'}`;
},
refreshAll() { Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id)); },
startLoop() {
const loop = () => {
if (this.state.mode === 'LIVE') Object.keys(this.state.charts).forEach(id => ChartLogic.plot(id, true));
setTimeout(loop, (this.state.refreshRate || 15) * 1000);
};
loop();
}
};
// ===================================================
// CHART LOGIC
// ===================================================
const ChartLogic = {
async plot(id, isUpdate = false) {
const chart = App.state.charts[id];
if (!chart || !chart.config.root) return;
const cfg = chart.config;
const dot = document.getElementById(`dot-${id}`);
if (isUpdate && dot) dot.classList.add('retry');
let endTime = cfg.end;
if (App.state.mode === 'LIVE') {
const now = new Date();
endTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
}
const requiredVars = extractRequiredVars(cfg.series ||[]);
try {
const res = await fetch('/api/fetch_series', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: App.state.date,
root: cfg.root,
expiry: cfg.expiry,
instrument: cfg.instrument || '',
timeframe: cfg.timeframe,
start_time: cfg.start,
end_time: endTime,
mode: cfg.chartMode || 'normal',
atm_range: cfg.atmRange || 5,
required_vars: requiredVars
})
});
const data = await res.json();
if (data.error || !data.labels || !data.labels.length) {
if (App.state.mode !== 'LIVE') {
const b = document.getElementById(`body-${id}`);
if (b) b.innerHTML = `<div class="empty-state" style="opacity:0.8;"><span style="font-size:20px;">⚠️</span><br/>${data.error || 'No Data Found'}</div>`;
}
if (dot) dot.classList.remove('retry');
return;
}
chart.lastData = data;
const body = document.getElementById(`body-${id}`);
if (body && !body.querySelector('canvas')) body.innerHTML = `<canvas id="canvas-${id}"></canvas>`;
if (dot) dot.className = `status-dot ${App.state.mode === 'LIVE' ? 'live' : 'hist'}`;
const datasets = (cfg.series ||[]).map(s => {
const vals = data.labels.map((_, i) => {
const vars = {
P: safeVal(data, 'P', i),
B: safeVal(data, 'B', i),
S: safeVal(data, 'S', i),
CB: safeVal(data, 'CB', i),
CS: safeVal(data, 'CS', i),
PB: safeVal(data, 'PB', i),
PS: safeVal(data, 'PS', i),
V: safeVal(data, 'V', i),
OI: safeVal(data, 'OI', i)
};
return evalFormula(s.formula, vars);
});
const ds = {
label: s.label,
data: vals,
borderWidth: 1.5,
pointRadius: 0,
hitRadius: 10,
tension: 0.1,
yAxisID: s.axis === 'left' ? 'y' : 'y1'
};
if (s.cond && s.cond !== 'none') {
const thresh = parseFloat(s.thresh) || 0;
const cTrue = s.color1 || '#238636';
const cFalse = s.color2 || '#da3633';
const condFn = (v) => s.cond === '>' ? v > thresh : v < thresh;
ds.segment = {
borderColor: ctx => condFn(ctx.p1.parsed.y) ? cTrue : cFalse
};
ds.borderColor = cTrue;
ds.backgroundColor = cTrue;
ds.pointHoverBackgroundColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
ds.pointHoverBorderColor = ctx => condFn(ctx.raw) ? cTrue : cFalse;
} else {
ds.borderColor = s.color1 || '#2f81f7';
ds.backgroundColor = s.color1 || '#2f81f7';
ds.pointHoverBackgroundColor = s.color1 || '#2f81f7';
ds.pointHoverBorderColor = s.color1 || '#2f81f7';
}
return ds;
});
this.render(id, data.labels, datasets, cfg.series ||[]);
} catch(e) { console.error('ChartLogic.plot error:', e); }
},
render(id, labels, datasets, seriesConfig) {
const ctx = document.getElementById(`canvas-${id}`);
if (!ctx) return;
const chart = App.state.charts[id];
if (chart.instance && chart.instance.ctx?.canvas !== ctx) {
chart.instance.destroy();
chart.instance = null;
}
if (chart.instance) {
chart.instance.data.labels = labels;
chart.instance.data.datasets = datasets;
chart.instance.update('none');
} else {
const hasRight = datasets.some(d => d.yAxisID === 'y1');
chart.instance = new Chart(ctx.getContext('2d'), {
type: 'line',
data: { labels, datasets },
options: {
responsive: true, maintainAspectRatio: false, animation: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(22,27,34,0.97)',
titleColor: '#e6edf3', bodyColor: '#8b949e',
borderColor: '#30363d', borderWidth: 1,
padding: 10,
callbacks: {
label: function(context) {
const s = seriesConfig[context.datasetIndex];
if (!s) return '';
const v = context.parsed.y;
const fv = v !== null ? v.toFixed(2) : 'N/A';
let condText = '';
if (s.cond && s.cond !== 'none') {
const thresh = parseFloat(s.thresh) || 0;
const isTrue = s.cond === '>' ? v > thresh : v < thresh;
if (isTrue && s.labelTrue) condText = ` ▶ ${s.labelTrue}`;
else if (!isTrue && s.labelFalse) condText = ` ▶ ${s.labelFalse}`;
}
return `${s.label}: ${fv}${condText}`;
},
labelColor: function(context) {
const s = seriesConfig[context.datasetIndex];
if (!s) return { borderColor: '#fff', backgroundColor: '#fff' };
let c = s.color1 || '#fff';
if (s.cond && s.cond !== 'none') {
const thresh = parseFloat(s.thresh) || 0;
const v = context.parsed.y;
const isTrue = s.cond === '>' ? v > thresh : v < thresh;
c = isTrue ? (s.color1 || '#238636') : (s.color2 || '#da3633');
}
return { borderColor: c, backgroundColor: c };
}
}
}
},
scales: {
x: {
grid: { color: '#30363d', tickLength: 0 },
ticks: { color: '#8b949e', maxTicksLimit: 8, font: { size: 10 } }
},
y: {
type: 'linear', display: true, position: 'left',
grid: { color: '#30363d' },
ticks: { color: '#8b949e', font: { size: 10 } }
},
y1: {
type: 'linear', display: hasRight, position: 'right',
grid: { display: false },
ticks: { color: '#8b949e', font: { size: 10 } }
}
}
}
});
}
}
};
// ===================================================
// BREAKDOWN TABLE WITH DYNAMIC SUB-CALCULATIONS
// ===================================================
const Breakdown = {
open(id) {
const chart = App.state.charts[id];
if (!chart || !chart.lastData || !chart.config.series) {
alert('No data loaded yet. Please wait for chart to load.');
return;
}
const data = chart.lastData;
const cfg = chart.config;
const series = cfg.series;
const isAdv = cfg.chartMode === 'advanced';
const allVarKeys =['P', 'CB', 'CS', 'PB', 'PS', 'B', 'S', 'V', 'OI'];
const presentVars = allVarKeys.filter(k => data[k] && data[k].some(v => v && v !== 0));
// Header Construction
let thead = '<thead><tr><th style="text-align:left;">Time</th>';
presentVars.forEach(v => { thead += `<th>${v}</th>`; });
series.forEach(s => { thead += `<th>${s.label}</th>`; });
thead += '</tr></thead>';
const totalCols = 1 + presentVars.length + series.length;
// Rows Construction
let tbody = '<tbody>';
data.labels.forEach((lbl, i) => {
const vars = {
P: safeVal(data, 'P', i), B: safeVal(data, 'B', i),
S: safeVal(data, 'S', i), CB: safeVal(data, 'CB', i),
CS: safeVal(data, 'CS', i), PB: safeVal(data, 'PB', i),
PS: safeVal(data, 'PS', i), V: safeVal(data, 'V', i),
OI: safeVal(data, 'OI', i)
};
const bd = (isAdv && data.strike_breakdown) ? data.strike_breakdown[i] : null;
// Main Row
tbody += `<tr>`;
if (isAdv && bd && bd.length > 0) {
tbody += `<td style="text-align:left;"><button class="expand-btn" id="btn-sub-${i}" onclick="toggleSub(${i})">▶</button> ${lbl}</td>`;
} else {
tbody += `<td style="text-align:left;">${lbl}</td>`;
}
presentVars.forEach(k => {
const v = vars[k];
const cls = v > 0 ? 'val-pos' : (v < 0 ? 'val-neg' : '');
tbody += `<td class="${cls}">${v.toFixed(2)}</td>`;
});
series.forEach(s => {
const result = evalFormula(s.formula, vars);
const rv = result !== null ? result : 0;
let cls = rv > 0 ? 'val-pos' : (rv < 0 ? 'val-neg' : '');
let condLabel = '';
if (s.cond && s.cond !== 'none') {
const thresh = parseFloat(s.thresh) || 0;
const isTrue = s.cond === '>' ? rv > thresh : rv < thresh;
cls = isTrue ? 'val-pos' : 'val-neg';
if (isTrue && s.labelTrue) condLabel = ` <span style="font-size:9px; opacity:0.7;">${s.labelTrue}</span>`;
else if (!isTrue && s.labelFalse) condLabel = ` <span style="font-size:9px; opacity:0.7;">${s.labelFalse}</span>`;
}
tbody += `<td class="${cls}">${rv.toFixed(2)}${condLabel}</td>`;
});
tbody += '</tr>';
// Expendable Sub-table for 2N Strikes Breakdown
if (isAdv && bd && bd.length > 0) {
let subTbody = `<tr id="sub-${i}" style="display:none;"><td colspan="${totalCols}" class="sub-table-container">`;
subTbody += `<table class="sub-table"><thead><tr>
<th style="text-align:left;">Instrument</th>
<th>Type</th>
<th>Strike</th>
<th>CB</th><th>CS</th><th>PB</th><th>PS</th><th>Vol</th><th>OI</th>
</tr></thead><tbody>`;
const sortedBd = [...bd].sort((a,b) => a.strike - b.strike);
sortedBd.forEach(r => {
subTbody += `<tr>
<td style="text-align:left; color:var(--text-secondary);">${r.inst}</td>
<td style="color:${r.type==='CE'?'#3fb950':'#da3633'}">${r.type}</td>
<td>${r.strike}</td>
<td>${r.cb.toFixed(2)}</td>
<td>${r.cs.toFixed(2)}</td>
<td>${r.pb.toFixed(2)}</td>
<td>${r.ps.toFixed(2)}</td>
<td>${r.v.toFixed(2)}</td>
<td>${r.oi.toFixed(2)}</td>
</tr>`;
});
subTbody += `</tbody></table></td></tr>`;
tbody += subTbody;
}
});
tbody += '</tbody>';
document.getElementById('bdTitle').innerText = `Breakdown: ${cfg.root} ${cfg.expiry} — ${isAdv ? '2N ATM ± ' + (cfg.atmRange || 5) : (cfg.instrument || '')}`;
document.getElementById('bdMeta').innerText = `Timeframe: ${cfg.timeframe || '1min'} | Range: ${cfg.start} – ${cfg.end} | ${data.labels.length} bars`;
document.getElementById('bdTable').innerHTML = thead + tbody;
document.getElementById('breakdownModal').style.display = 'flex';
}
};
// ===================================================
// CONFIG MODAL
// ===================================================
const Config = {
tid: null, isGlobal: false, currentMode: 'normal',
setMode(mode) {
this.currentMode = mode;
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
btns[0].classList.toggle('active', mode === 'normal');
btns[1].classList.toggle('active', mode === 'advanced');
document.getElementById('cfgAdvancedFields').style.display = mode === 'advanced' ? 'block' : 'none';
document.getElementById('lblInst').innerText = mode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
document.getElementById('advInstHint').style.display = mode === 'advanced' ? 'block' : 'none';
this.updatePresetsDropdown();
const defPreset = mode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)';
this.loadPreset(defPreset);
},
updatePresetsDropdown() {
const sel = document.getElementById('cfgPresetsDropdown');
sel.innerHTML = Object.keys(App.presets).map(k => `<option value="${k}">${k}</option>`).join('');
},
loadPreset(presetName = null) {
const name = presetName || document.getElementById('cfgPresetsDropdown').value;
if (name && App.presets[name]) {
document.getElementById('cfgPresetsDropdown').value = name;
this.renderSeries(App.presets[name]);
}
},
savePreset() {
const name = prompt('Name for this model:', 'Custom Model');
if (!name) return;
App.presets[name] = this.extractSeriesFromDOM();
localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
this.updatePresetsDropdown();
document.getElementById('cfgPresetsDropdown').value = name;
},
deletePreset() {
const name = document.getElementById('cfgPresetsDropdown').value;
const locked =['Basic Flow (Fut/Eq)', 'Net Flow (Fut/Eq)', 'Call Flow (Options)',
'Put Flow (Options)', 'Aggregated Flow (Options)', 'Net Flow (Options)',
'Price & Flows (Advanced)', 'Net Flow (Advanced)', 'Components (Advanced)'];
if (locked.includes(name)) return alert('Cannot delete default models.');
if (confirm(`Delete model '${name}'?`)) {
delete App.presets[name];
localStorage.setItem('dc_pro_v4_presets', JSON.stringify(App.presets));
this.updatePresetsDropdown();
this.loadPreset();
}
},
async open(id) {
this.tid = id; this.isGlobal = false;
document.getElementById('configModal').style.display = 'flex';
document.getElementById('cfgTitle').innerText = 'Configure Chart';
document.getElementById('cfgLoading').style.display = 'block';
document.getElementById('cfgContent').style.display = 'none';
document.getElementById('cfgGlobalOnly').style.display = 'none';
document.getElementById('cfgChartOnly').style.display = 'block';
this.updatePresetsDropdown();
CustomDD.clear('cfgRoot');
const res = await fetch(`/api/roots?date=${App.state.date}`);
const roots = await res.json();
CustomDD.set('cfgRoot', roots);
const cfg = App.state.charts[id].config;
document.getElementById('cfgTF').value = cfg.timeframe || '1min';
document.getElementById('cfgStart').value = cfg.start || '09:15';
document.getElementById('cfgEnd').value = cfg.end || '15:30';
document.getElementById('cfgAtmRange').value = cfg.atmRange || 5;
this.currentMode = cfg.chartMode || 'normal';
const btns = document.querySelectorAll('#cfgModeToggle .seg-btn');
btns[0].classList.toggle('active', this.currentMode === 'normal');
btns[1].classList.toggle('active', this.currentMode === 'advanced');
document.getElementById('cfgAdvancedFields').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
document.getElementById('lblInst').innerText = this.currentMode === 'normal' ? 'Instrument' : 'Reference Spot/Fut (ATM Ref Price)';
document.getElementById('advInstHint').style.display = this.currentMode === 'advanced' ? 'block' : 'none';
if (cfg.series && cfg.series.length > 0) this.renderSeries(cfg.series);
else this.loadPreset(this.currentMode === 'advanced' ? 'Price & Flows (Advanced)' : 'Aggregated Flow (Options)');
document.getElementById('cfgRoot').value = cfg.root || '';
document.getElementById('cfgExp').value = cfg.expiry || '';
document.getElementById('cfgInst').value = cfg.instrument || '';
if (cfg.root) await this.onRootChange(cfg.expiry, cfg.instrument);
document.getElementById('cfgLoading').style.display = 'none';
document.getElementById('cfgContent').style.display = 'block';
},
openDefaults() {
this.isGlobal = true;
document.getElementById('configModal').style.display = 'flex';
document.getElementById('cfgTitle').innerText = 'Global Settings';
document.getElementById('cfgLoading').style.display = 'none';
document.getElementById('cfgContent').style.display = 'block';
document.getElementById('cfgGlobalOnly').style.display = 'block';
document.getElementById('cfgChartOnly').style.display = 'none';
document.getElementById('cfgRefresh').value = App.state.refreshRate || 15;
this.updatePresetsDropdown();
},
close() { document.getElementById('configModal').style.display = 'none'; },
async onRootChange(preExp, preInst) {
const root = document.getElementById('cfgRoot').value.toUpperCase();
CustomDD.clear('cfgExp'); CustomDD.clear('cfgInst');
if (!preExp) document.getElementById('cfgExp').value = '';
if (!preInst) document.getElementById('cfgInst').value = '';
if (!root) return;
const res = await fetch(`/api/expiries?date=${App.state.date}&root=${root}`);
const exps = await res.json();
CustomDD.set('cfgExp', exps);
if (preExp && exps.includes(preExp)) {
document.getElementById('cfgExp').value = preExp;
await this.onExpChange(preInst);
} else if (exps.length > 0) {
const auto = await fetch(`/api/auto_config?date=${App.state.date}&root=${root}`).then(r => r.json());
if (auto.current) {
document.getElementById('cfgExp').value = auto.current.expiry;
await this.onExpChange(auto.current.instrument);
}
}
},
async onExpChange(preInst) {
const root = document.getElementById('cfgRoot').value.toUpperCase();
const exp = document.getElementById('cfgExp').value.toUpperCase();
CustomDD.clear('cfgInst');
if (!root || !exp) return;
const res = await fetch(`/api/instruments?date=${App.state.date}&root=${root}&expiry=${exp}`);
const data = await res.json();
if (data.spot_fut.length > 0) CustomDD.set('cfgInst', data.spot_fut, 'Futures & Equity');
if (data.options.length > 0) CustomDD.set('cfgInst', data.options, 'Options');
if (preInst) document.getElementById('cfgInst').value = preInst;
},
toggleCond(sel) {
const condGroup = sel.closest('.series-row').querySelector('.series-row-cond');
if (condGroup) condGroup.style.display = sel.value === 'none' ? 'none' : 'flex';
},
renderSeries(list) {
const c = document.getElementById('cfgSeries');
c.innerHTML = '';
list.forEach(s => this._appendSeriesRow(c, s));
},
_appendSeriesRow(container, s = {}) {
const r = document.createElement('div');
r.className = 'series-row';
const showCond = s.cond && s.cond !== 'none';
r.innerHTML = `
<div class="series-row-top">
<input type="text" value="${s.formula || '$P'}" class="s-form" placeholder="Formula (e.g. $CB + $PS)" style="flex:1;">
<input type="text" value="${s.label || 'Line'}" class="s-lbl" placeholder="Label" style="width:90px;">
<select class="s-axis" style="width:60px;">
<option value="left" ${s.axis === 'left' ? 'selected' : ''}>Left</option>
<option value="right" ${s.axis === 'right' ? 'selected' : ''}>Right</option>
</select>
<button class="tool-btn" style="color:var(--danger); width:28px; background:rgba(218,54,51,0.1);" onclick="this.closest('.series-row').remove()">✕</button>
</div>
<div class="series-row-bottom">
<input type="color" value="${s.color1 || '#2f81f7'}" class="color-input s-color1" title="Line Color">
<select class="s-cond" style="width:76px;" onchange="Config.toggleCond(this)">
<option value="none" ${!s.cond || s.cond === 'none' ? 'selected' : ''}>Solid</option>
<option value=">" ${s.cond === '>' ? 'selected' : ''}>If &gt;</option>
<option value="<" ${s.cond === '<' ? 'selected' : ''}>If &lt;</option>
</select>
</div>
<div class="series-row-cond" style="display:${showCond ? 'flex' : 'none'};">
<span class="cond-label">Threshold:</span>
<input type="number" class="s-thresh" value="${s.thresh || 0}" style="width:65px;">
<span class="cond-label">True →</span>
<input type="color" value="${s.color1 || '#238636'}" class="color-input s-color1-cond" title="Color when True">
<input type="text" class="s-label-true" value="${s.labelTrue || ''}" placeholder="e.g. BULLISH" style="width:80px;">
<span class="cond-label">False →</span>
<input type="color" value="${s.color2 || '#da3633'}" class="color-input s-color2" title="Color when False">
<input type="text" class="s-label-false" value="${s.labelFalse || ''}" placeholder="e.g. BEARISH" style="width:80px;">
</div>
`;
const solidColor = r.querySelector('.s-color1');
const condTrueColor = r.querySelector('.s-color1-cond');
solidColor.addEventListener('input', () => { if (condTrueColor) condTrueColor.value = solidColor.value; });
container.appendChild(r);
},
addSeriesRow() {
this._appendSeriesRow(document.getElementById('cfgSeries'));
},
extractSeriesFromDOM() {
return Array.from(document.querySelectorAll('#cfgSeries .series-row')).map(r => {
const cond = r.querySelector('.s-cond')?.value || 'none';
return {
formula: r.querySelector('.s-form')?.value || '$P',
label: r.querySelector('.s-lbl')?.value || 'Line',
axis: r.querySelector('.s-axis')?.value || 'left',
color1: r.querySelector('.s-color1-cond')?.value || r.querySelector('.s-color1')?.value || '#2f81f7',
cond,
thresh: r.querySelector('.s-thresh')?.value || '0',
color2: r.querySelector('.s-color2')?.value || '#da3633',
labelTrue: r.querySelector('.s-label-true')?.value || '',
labelFalse: r.querySelector('.s-label-false')?.value || ''
};
});
},
saveChartConfig() {
if (this.isGlobal) {
App.state.refreshRate = parseInt(document.getElementById('cfgRefresh').value) || 15;
App.save();
this.close();
return;
}
const root = document.getElementById('cfgRoot').value.toUpperCase();
const exp = document.getElementById('cfgExp').value.toUpperCase();
const inst = document.getElementById('cfgInst').value.toUpperCase();
if (!root || !exp) return alert('Please select a Symbol and Expiry.');
const series = this.extractSeriesFromDOM();
if (!series.length) return alert('Add at least one plot line.');
App.state.charts[this.tid].config = {
chartMode: this.currentMode,
atmRange: parseInt(document.getElementById('cfgAtmRange').value) || 5,
root, expiry: exp, instrument: inst,
timeframe: document.getElementById('cfgTF').value,
start: document.getElementById('cfgStart').value,
end: document.getElementById('cfgEnd').value,
series
};
App.save();
App.renderGrid();
ChartLogic.plot(this.tid);
this.close();
}
};
window.onload = () => App.init();
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860)