| import os |
| import math |
| import json |
| import asyncio |
| import httpx |
| import pandas as pd |
| import threading |
| import urllib.parse |
| from datetime import datetime, timedelta, timezone |
| from typing import Optional, List, Dict, Any |
| from fastapi import FastAPI, Request |
| from fastapi.responses import HTMLResponse, JSONResponse |
| from supabase import create_client, Client |
| import uvicorn |
|
|
| |
| |
| |
| try: |
| creds = json.loads(os.environ.get("SUPABASE_CREDS", "{}")) |
| SUPABASE_URL = creds.get("url", "") |
| SUPABASE_KEY = creds.get("key", "") |
| except Exception: |
| SUPABASE_URL = os.environ.get("SUPABASE_URL", "") |
| SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "") |
|
|
| app = FastAPI() |
|
|
| try: |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) |
| except Exception as e: |
| print(f"Warning: Supabase client failed to initialize. Check Secrets. Error: {e}") |
| supabase = None |
|
|
| |
| |
| |
| SYSTEM_STATUS = {"ready": False, "message": "Booting..."} |
| NIFTY_MASTER: Dict[str, Dict[str, Dict[float, str]]] = {} |
| EXPIRIES_LIST: List[str] = [] |
|
|
| SPOT_SYMBOL = "NSE:NIFTY50-INDEX" |
|
|
| def get_fyers_token() -> str: |
| if not supabase: raise Exception("Supabase not initialized.") |
| res = supabase.table('apps').select('*').eq('broker', 'FYERS').eq('is_enabled', True).order('updated_at', desc=True).limit(1).execute() |
| if res.data and res.data[0].get('access_token'): |
| return f"{res.data[0]['app_id']}:{res.data[0]['access_token']}" |
| raise Exception("No active Fyers token found in database.") |
|
|
| def update_master_db(): |
| global SYSTEM_STATUS, NIFTY_MASTER, EXPIRIES_LIST |
| try: |
| SYSTEM_STATUS["message"] = "Downloading NSE Master Data..." |
| headers = {"User-Agent": "Mozilla/5.0"} |
| url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv" |
| df = pd.read_csv(url_nse, header=None, on_bad_lines='skip', storage_options=headers) |
| |
| df_nifty = df[(df[13].astype(str) == 'NIFTY') & (df[2] == 14)] |
| |
| temp_master = {} |
| unique_exp = set() |
| |
| for _, row in df_nifty.iterrows(): |
| desc = str(row[1]) |
| exp_epoch = int(row[8]) |
| sym_ticker = str(row[9]) |
| |
| exp_date = datetime.fromtimestamp(exp_epoch, tz=timezone.utc).strftime('%Y-%m-%d') |
| parts = desc.strip().split(' ') |
| |
| if len(parts) >= 2 and parts[-1] in ['CE', 'PE']: |
| try: |
| strike = float(parts[-2].replace(',', '')) |
| opt_type = parts[-1] |
| if exp_date not in temp_master: |
| temp_master[exp_date] = {'CE': {}, 'PE': {}} |
| temp_master[exp_date][opt_type][strike] = sym_ticker |
| unique_exp.add(exp_date) |
| except Exception: |
| continue |
| |
| NIFTY_MASTER = temp_master |
| EXPIRIES_LIST = sorted(list(unique_exp)) |
| SYSTEM_STATUS["ready"] = True |
| SYSTEM_STATUS["message"] = "Ready" |
| except Exception as e: |
| SYSTEM_STATUS["message"] = f"Master Error: {str(e)}" |
|
|
| @app.on_event("startup") |
| def startup_event(): |
| threading.Thread(target=update_master_db).start() |
|
|
| |
| |
| |
| async def fetch_history(client: httpx.AsyncClient, token: str, symbol: str, date: str, semaphore: asyncio.Semaphore) -> tuple: |
| encoded_symbol = urllib.parse.quote(symbol) |
| url = f"https://api-t1.fyers.in/data/history?symbol={encoded_symbol}&resolution=1&date_format=1&range_from={date}&range_to={date}&cont_flag=0" |
| headers = {"Authorization": token} |
| |
| async with semaphore: |
| await asyncio.sleep(0.35) |
| for attempt in range(4): |
| try: |
| resp = await client.get(url, headers=headers, timeout=15.0) |
| data = resp.json() |
| |
| if data.get('s') == 'error': |
| if "limit" in str(data).lower(): |
| await asyncio.sleep(1.5 * (attempt + 1)) |
| continue |
| return symbol, {} |
|
|
| price_map = {} |
| if data.get('s') == 'ok' and 'candles' in data: |
| for candle in data['candles']: |
| price_map[int(candle[0])] = { |
| "o": float(candle[1]), |
| "h": float(candle[2]), |
| "l": float(candle[3]), |
| "c": float(candle[4]) |
| } |
| return symbol, price_map |
| except Exception: |
| await asyncio.sleep(1.0 * (attempt + 1)) |
| return symbol, {} |
|
|
| def forward_fill_prices(price_map: dict, ep_list: list) -> dict: |
| filled = {} |
| last_close = 0.0 |
| for ep in ep_list: |
| if ep in price_map: |
| filled[ep] = price_map[ep] |
| last_close = price_map[ep]['c'] |
| else: |
| |
| filled[ep] = {"o": last_close, "h": last_close, "l": last_close, "c": last_close} |
| return filled |
|
|
| def get_tf_candle(price_map: dict, end_ep: int, tf_mins: int, fallback_val: float) -> dict: |
| epochs = [end_ep - (tf_mins * 60) + (i * 60) for i in range(1, tf_mins + 1)] |
| valid_candles = [] |
| |
| for e in epochs: |
| if e in price_map and price_map[e].get('c', 0) > 0: |
| valid_candles.append(price_map[e]) |
| |
| if not valid_candles: |
| c = price_map.get(end_ep, {}).get("c", fallback_val) |
| return {"o": c, "h": c, "l": c, "c": c} |
| |
| O = valid_candles[0]['o'] |
| H = max(c['h'] for c in valid_candles) |
| L = min(c['l'] for c in valid_candles) |
| C = valid_candles[-1]['c'] |
| return {"o": O, "h": H, "l": L, "c": C} |
|
|
| def calc_diff(mode: str, curr: dict, prev: dict, formula: str, c_sign: str) -> float: |
| O, H, L, C = curr.get('o', 0.0), curr.get('h', 0.0), curr.get('l', 0.0), curr.get('c', 0.0) |
| O_prev, H_prev, L_prev, C_prev = prev.get('o', 0.0), prev.get('h', 0.0), prev.get('l', 0.0), prev.get('c', 0.0) |
| |
| if mode == "1": |
| mag = H - L |
| sign_val = 1 if C >= O else -1 |
| return sign_val * mag |
| |
| elif mode == "2": |
| return C - O |
| |
| elif mode == "3": |
| return C - C_prev |
| |
| elif mode == "4": |
| if C > C_prev: val = H |
| elif C < C_prev: val = L |
| else: val = C |
| return val - C_prev |
| |
| elif mode == "5": |
| allowed_vars = { |
| 'O': O, 'H': H, 'L': L, 'C': C, |
| 'O_prev': O_prev, 'H_prev': H_prev, 'L_prev': L_prev, 'C_prev': C_prev, |
| 'abs': abs, 'max': max, 'min': min |
| } |
| try: |
| res = float(eval(formula, {"__builtins__": None}, allowed_vars)) |
| if c_sign == "candle": |
| mag = abs(res) |
| sign_val = 1 if C >= O else -1 |
| return sign_val * mag |
| return res |
| except Exception: |
| return 0.0 |
| |
| return C - O |
|
|
| |
| |
| |
| @app.get("/api/status") |
| def get_status(): |
| return JSONResponse(SYSTEM_STATUS) |
|
|
| @app.get("/api/expiries") |
| def get_expiries(): |
| if not SYSTEM_STATUS["ready"]: |
| return JSONResponse({"s": "error", "message": "Master Data Initializing..."}) |
| return JSONResponse({"s": "ok", "expiries": EXPIRIES_LIST}) |
|
|
| @app.get("/api/heatmap") |
| async def generate_heatmap( |
| request: Request, date: str, expiry: str, tfs: str = "1,3,15", |
| atm_range: int = 10, start: str = "09:15", end: str = "15:30", |
| method: str = "1", formula: str = "(H-L)", sign: str = "formula", |
| divisor: str = "true", strike_mode: str = "itm" |
| ): |
| if not SYSTEM_STATUS["ready"]: |
| return JSONResponse({"s": "error", "message": "System Booting... Please wait."}) |
| |
| try: token = get_fyers_token() |
| except Exception as e: return JSONResponse({"s": "error", "message": str(e)}) |
| |
| timeframes = [int(x.strip()) for x in tfs.split(",") if x.strip().isdigit()] |
| use_divisor = (divisor.lower() == "true") |
| |
| ist_tz = timezone(timedelta(hours=5, minutes=30)) |
| start_dt = datetime.strptime(f"{date} {start}:00", "%Y-%m-%d %H:%M:%S").replace(tzinfo=ist_tz) |
| end_dt = datetime.strptime(f"{date} {end}:00", "%Y-%m-%d %H:%M:%S").replace(tzinfo=ist_tz) |
| |
| total_mins = int((end_dt - start_dt).total_seconds() // 60) |
| ep_list = [int((start_dt + timedelta(minutes=i)).timestamp()) for i in range(total_mins + 1)] |
| |
| async with httpx.AsyncClient() as client: |
| _, raw_spot = await fetch_history(client, token, SPOT_SYMBOL, date, asyncio.Semaphore(1)) |
| if not raw_spot: |
| return JSONResponse({"s": "error", "message": f"No Spot data found. Date may be a holiday or wrong format."}) |
| |
| spot_map = forward_fill_prices(raw_spot, ep_list) |
| |
| required_symbols = set() |
| for ep in ep_list: |
| spot_price = spot_map.get(ep, {}).get("c", 0) |
| if spot_price > 0: |
| atm = int(spot_price // 50) * 50 |
| strikes = [atm - (i * 50) for i in range(atm_range - 1, -1, -1)] + [atm + (i * 50) for i in range(1, atm_range + 1)] |
| |
| for k in strikes: |
| if expiry in NIFTY_MASTER: |
| if k in NIFTY_MASTER[expiry]['CE']: required_symbols.add(NIFTY_MASTER[expiry]['CE'][k]) |
| if k in NIFTY_MASTER[expiry]['PE']: required_symbols.add(NIFTY_MASTER[expiry]['PE'][k]) |
| |
| semaphore = asyncio.Semaphore(4) |
| tasks = [fetch_history(client, token, sym, date, semaphore) for sym in required_symbols] |
| results = await asyncio.gather(*tasks) |
| |
| opt_data = {sym: forward_fill_prices(raw, ep_list) for sym, raw in results} |
| |
| matrix = [] |
| for ep in ep_list: |
| time_str = datetime.fromtimestamp(ep, tz=ist_tz).strftime('%H:%M') |
| spot_t2_info = spot_map.get(ep, {"o": 0, "h": 0, "l": 0, "c": 0}) |
| spot_t2_c = spot_t2_info["c"] |
| |
| if spot_t2_c == 0: continue |
| |
| spot_min_diff = spot_t2_c - spot_t2_info["o"] |
| atm = int(spot_t2_c // 50) * 50 |
| strikes = [atm - (i * 50) for i in range(atm_range - 1, -1, -1)] + [atm + (i * 50) for i in range(1, atm_range + 1)] |
| |
| row_data = {"time": time_str, "spot_min_diff": round(spot_min_diff, 2), "tfs": {}} |
| |
| for tf in timeframes: |
| ep_t1 = ep - (tf * 60) |
| if ep_t1 < start_dt.timestamp(): |
| row_data["tfs"][str(tf)] = None |
| continue |
| |
| spot_curr = get_tf_candle(spot_map, ep, tf, spot_t2_c) |
| spot_prev = get_tf_candle(spot_map, ep_t1, tf, spot_t2_c) |
| |
| spot_diff = calc_diff(method, spot_curr, spot_prev, formula, sign) |
| |
| if use_divisor: |
| abs_spot_diff = abs(spot_diff) |
| safe_spot_diff = abs_spot_diff if abs_spot_diff != 0.0 else 0.05 |
| else: |
| safe_spot_diff = 1.0 |
| |
| sum_c_diff, sum_p_diff = 0.0, 0.0 |
| strike_details = [] |
| |
| for k in strikes: |
| |
| ce_valid = True |
| pe_valid = True |
| if strike_mode == "itm": |
| ce_valid = (k <= atm) |
| pe_valid = (k >= atm) |
| elif strike_mode == "otm": |
| ce_valid = (k > atm) |
| pe_valid = (k < atm) |
| |
| ce_sym = NIFTY_MASTER[expiry]['CE'].get(k) if expiry in NIFTY_MASTER else None |
| pe_sym = NIFTY_MASTER[expiry]['PE'].get(k) if expiry in NIFTY_MASTER else None |
| |
| ce_curr = get_tf_candle(opt_data[ce_sym], ep, tf, 0.0) if ce_sym and ce_sym in opt_data else {"o":0,"h":0,"l":0,"c":0} |
| ce_prev = get_tf_candle(opt_data[ce_sym], ep_t1, tf, 0.0) if ce_sym and ce_sym in opt_data else {"o":0,"h":0,"l":0,"c":0} |
| pe_curr = get_tf_candle(opt_data[pe_sym], ep, tf, 0.0) if pe_sym and pe_sym in opt_data else {"o":0,"h":0,"l":0,"c":0} |
| pe_prev = get_tf_candle(opt_data[pe_sym], ep_t1, tf, 0.0) if pe_sym and pe_sym in opt_data else {"o":0,"h":0,"l":0,"c":0} |
| |
| c_diff = calc_diff(method, ce_curr, ce_prev, formula, sign) if ce_valid else 0.0 |
| p_diff = calc_diff(method, pe_curr, pe_prev, formula, sign) if pe_valid else 0.0 |
| |
| c_ratio = c_diff / safe_spot_diff |
| p_ratio = p_diff / safe_spot_diff |
| |
| sum_c_diff += c_diff |
| sum_p_diff += p_diff |
| |
| strike_details.append({ |
| "k": k, |
| "c1": round(ce_prev['c'], 2), "c2": round(ce_curr['c'], 2), "cd": round(c_diff, 2), "cr": round(c_ratio, 4), "cv": ce_valid, |
| "p1": round(pe_prev['c'], 2), "p2": round(pe_curr['c'], 2), "pd": round(p_diff, 2), "pr": round(p_ratio, 4), "pv": pe_valid |
| }) |
| |
| emp_ce_delta = sum_c_diff / safe_spot_diff |
| emp_pe_delta = sum_p_diff / safe_spot_diff |
| net_emp_delta = emp_ce_delta - emp_pe_delta |
| |
| |
| is_anomaly = (spot_diff > 0 and net_emp_delta < 0) or (spot_diff < 0 and net_emp_delta > 0) |
| |
| row_data["tfs"][str(tf)] = { |
| "tf_name": f"{tf} Min", |
| "method": method, |
| "atm_idx": atm_range - 1, |
| "net_delta": round(net_emp_delta, 4), |
| "ce_delta": round(emp_ce_delta, 4), |
| "pe_delta": round(emp_pe_delta, 4), |
| "t1_spot": round(spot_prev['c'], 2), |
| "t2_spot": round(spot_curr['c'], 2), |
| "spot_diff": round(spot_diff, 2), |
| "ce_diff_total": round(sum_c_diff, 2), |
| "pe_diff_total": round(sum_p_diff, 2), |
| "strike_details": strike_details, |
| "atm_range": atm_range, |
| "is_anomaly": is_anomaly |
| } |
| matrix.append(row_data) |
| return JSONResponse({"s": "ok", "matrix": matrix, "timeframes": timeframes}) |
|
|
| |
| |
| |
| @app.get("/") |
| def serve_ui(): |
| return HTMLResponse(HTML_TEMPLATE) |
|
|
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Empirical Delta Terminal - Ultra Compact</title> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <style> |
| :root { |
| --bg: #ffffff; --panel: #f8f9fa; --border: #ced4da; |
| --text: #212529; --text-dim: #6c757d; |
| --green: #d1e7dd; --green-text: #198754; |
| --red: #f8d7da; --red-text: #dc3545; |
| --accent: #0d6efd; --accent-hover: #0b5ed7; |
| --table-hover: #e9ecef; |
| } |
| body { margin: 0; padding: 5px; font-family: -apple-system, sans-serif; background: var(--bg); color: var(--text); font-size: 11px; } |
| |
| .controls { display: flex; gap: 8px; margin-bottom: 8px; background: var(--panel); padding: 6px; border-radius: 4px; border: 1px solid var(--border); align-items: flex-end; flex-wrap: wrap; } |
| .control-group { display: flex; flex-direction: column; gap: 2px; } |
| .control-group label { font-size: 9px; color: var(--text-dim); text-transform: uppercase; font-weight: bold; } |
| input, select { padding: 4px 6px; border: 1px solid var(--border); border-radius: 3px; outline: none; font-size:11px; } |
| input:focus, select:focus { border-color: var(--accent); } |
| |
| button { background: var(--accent); color: white; border: none; padding: 4px 10px; border-radius: 3px; cursor: pointer; font-weight: bold; font-size: 11px; transition:0.2s; height: 24px;} |
| button:hover { background: var(--accent-hover); } |
| button:disabled { background: #adb5bd; cursor: not-allowed; } |
| .manual-btn { background: #6c757d; margin-left: auto;} |
| .manual-btn:hover { background: #5a6268; } |
| |
| .table-container { height: calc(100vh - 65px); overflow-y: auto; border: 1px solid var(--border); border-radius: 4px; background: #fff; } |
| table { width: 100%; border-collapse: collapse; } |
| th, td { padding: 4px 6px; text-align: center; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; } |
| td:last-child, th:last-child { border-right: none; } |
| th { background: #dee2e6; color: #495057; font-size: 10px; text-transform: uppercase; position: sticky; top: 0; z-index: 10; } |
| tr:hover { background-color: var(--table-hover); } |
| |
| .time-cell { font-weight: bold; color: var(--accent); cursor: pointer; text-decoration: underline; } |
| .time-cell:hover { color: var(--accent-hover); } |
| .spot-cell { font-family: monospace; font-weight: bold; } |
| |
| .delta-cell { font-family: monospace; font-weight: bold; padding: 2px 4px; border-radius: 2px; display: block; } |
| .pos-val { background-color: var(--green); color: var(--green-text); } |
| .neg-val { background-color: var(--red); color: var(--red-text); } |
| |
| /* The Massive Anomaly Indicator */ |
| .anomaly-cell { box-shadow: inset 0px 0px 0px 2px #0d6efd; border-radius: 3px; } |
| |
| .blank-cell { color: #adb5bd; font-style: italic; font-size: 10px; } |
| .excluded-cell { color: #adb5bd !important; opacity: 0.5; } |
| |
| /* Modal Styles */ |
| #modal, #manualModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000; } |
| .modal-content { background: #fff; padding: 15px; border-radius: 4px; width: 1000px; max-width: 98%; max-height: 95vh; display: flex; flex-direction: column; border: 1px solid var(--border); } |
| .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid var(--border); } |
| .modal-header h3 { margin: 0; font-size: 14px; } |
| .modal-close { cursor: pointer; color: var(--text-dim); font-size: 18px; font-weight: bold; } |
| |
| .manual-content { padding-right: 5px; overflow-y: auto; font-size: 12px; line-height: 1.5; color: #333;} |
| .manual-content code { background: #e9ecef; padding: 2px 4px; border-radius: 3px; font-family: monospace; color: var(--accent-hover); font-weight: bold;} |
| |
| .summary-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-bottom: 10px; background: var(--panel); padding: 8px; border-radius: 4px; border: 1px solid var(--border); } |
| .summary-item { font-size: 11px; } |
| .summary-label { color: var(--text-dim); font-size: 9px; font-weight: bold; text-transform: uppercase; display: block; } |
| .summary-value { font-family: monospace; font-weight: bold; font-size: 12px; } |
| |
| .breakdown-table-wrapper { overflow-y: auto; flex-grow: 1; border: 1px solid var(--border); } |
| .breakdown-table { font-size: 10px; } |
| .breakdown-table th { background: #e9ecef; } |
| |
| .atm-row td { background-color: #ffeeba !important; border-bottom: 3px solid #343a40 !important; font-weight: bold; } |
| .total-row td { background: #dee2e6; font-weight: bold; } |
| |
| .loader { display:none; border: 2px solid transparent; border-top: 2px solid white; border-radius: 50%; width: 10px; height: 10px; animation: spin 1s linear infinite; vertical-align: middle; } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| </style> |
| </head> |
| <body> |
| <div class="controls"> |
| <div class="control-group"><label>Date</label><input type="date" id="i_date"></div> |
| <div class="control-group"><label>Start</label><input type="time" id="i_start" value="09:15"></div> |
| <div class="control-group"><label>End</label><input type="time" id="i_end" value="15:30"></div> |
| <div class="control-group"><label>Expiry</label><select id="i_exp" style="width:105px;"><option>Loading...</option></select></div> |
| <div class="control-group"><label>TFs (m)</label><input type="text" id="i_tfs" value="1, 3, 15" style="width:70px;"></div> |
| <div class="control-group"><label>± Strikes</label><input type="number" id="i_atm" value="10" style="width:45px;"></div> |
| |
| <div class="control-group"> |
| <label>Strike Mode</label> |
| <select id="i_strike_mode" style="width:90px;"> |
| <option value="all">Full 4 Quad.</option> |
| <option value="itm" selected>2 Quad ITM</option> |
| <option value="otm">2 Quad OTM</option> |
| </select> |
| </div> |
| |
| <div class="control-group"> |
| <label>Diff Method</label> |
| <select id="i_method" style="width:110px;" onchange="toggleCustom()"> |
| <option value="1" selected>1. HH/LL of TF</option> |
| <option value="2">2. TF Open-Close</option> |
| <option value="3">3. T2 - T1 Close</option> |
| <option value="4">4. Extremes vs T1</option> |
| <option value="5">5. Custom Formula</option> |
| </select> |
| </div> |
| <div class="control-group" id="c_formula" style="display:none;"> |
| <label>Formula</label> |
| <input type="text" id="i_formula" value="(H-L)" style="width:70px;"> |
| </div> |
| <div class="control-group" id="c_sign" style="display:none;"> |
| <label>Form. Sign</label> |
| <select id="i_sign" style="width:80px;"> |
| <option value="formula">Absolute</option> |
| <option value="candle">Candle Dir.</option> |
| </select> |
| </div> |
| <div class="control-group"> |
| <label>Divisor (Spot Δ)</label> |
| <select id="i_divisor" style="width:50px;"> |
| <option value="true" selected>On</option> |
| <option value="false">Off</option> |
| </select> |
| </div> |
| |
| <button onclick="loadHeatmap()" id="btnLoad"><span class="loader" id="btnLoader"></span><span id="btnTxt">Load</span></button> |
| <button type="button" class="manual-btn" onclick="document.getElementById('manualModal').style.display='flex'">Manual</button> |
| </div> |
| |
| <div class="table-container"> |
| <table> |
| <thead> |
| <tr id="tableHead"> |
| <th style="width:60px;">Time</th><th style="width:60px;">Spot 1m Δ</th><th>Matrix generates here...</th> |
| </tr> |
| </thead> |
| <tbody id="tableBody"></tbody> |
| </table> |
| </div> |
| |
| <div id="modal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h3 id="modalTitle">Calculation Table</h3> |
| <span class="modal-close" onclick="document.getElementById('modal').style.display='none'">×</span> |
| </div> |
| |
| <div class="summary-grid"> |
| <div class="summary-item"><span class="summary-label">Spot T1 Close</span><span class="summary-value" id="m_t1spot"></span></div> |
| <div class="summary-item"><span class="summary-label">Spot T2 Close</span><span class="summary-value" id="m_t2spot"></span></div> |
| <div class="summary-item"><span class="summary-label">Spot Diff (True Δ)</span><span class="summary-value" id="m_spotdiff"></span></div> |
| |
| <div class="summary-item"><span class="summary-label">Emp. Call Delta</span><span class="summary-value" id="m_cedelta"></span></div> |
| <div class="summary-item"><span class="summary-label">Emp. Put Delta</span><span class="summary-value" id="m_pedelta"></span></div> |
| <div class="summary-item"><span class="summary-label">Net Emp. Delta</span><span class="summary-value" id="m_netdelta"></span></div> |
| </div> |
| |
| <div class="breakdown-table-wrapper"> |
| <table class="breakdown-table"> |
| <thead> |
| <tr> |
| <th>Strike</th> |
| <th>CE T1(C)</th><th>CE T2(C)</th><th>CE Call Diff</th><th>CE Ratio (Δ)</th> |
| <th>PE T1(C)</th><th>PE T2(C)</th><th>PE Put Diff</th><th>PE Ratio (Δ)</th> |
| </tr> |
| </thead> |
| <tbody id="modalTableBody"></tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| |
| <div id="manualModal"> |
| <div class="modal-content" style="max-width: 550px;"> |
| <div class="modal-header"> |
| <h3>User Manual & Settings</h3> |
| <span class="modal-close" onclick="document.getElementById('manualModal').style.display='none'">×</span> |
| </div> |
| <div class="manual-content"> |
| <p><strong>Strike Mode (Quadrants):</strong></p> |
| <ul> |
| <li><strong>2 Quad ITM:</strong> Measures Call ITMs (Strikes <= ATM) and Put ITMs (Strikes >= ATM). Default behavior for flow analysis.</li> |
| <li><strong>2 Quad OTM:</strong> Measures Call OTMs and Put OTMs only.</li> |
| <li><strong>Full 4 Quad:</strong> Sums up all selected strikes uniformly.</li> |
| </ul> |
| <p><strong>Anomaly Highlight (Blue Outline):</strong></p> |
| <p>If the cell has a thick blue border, it indicates a massive anomaly where Nifty moved in one direction (e.g., dropped), but Net Options premium moved heavily in the opposite direction (e.g., massive calls accumulated). Watch these closely!</p> |
| <hr style="border:0; border-top:1px solid #dee2e6; margin:10px 0;"> |
| <p><strong>Difference Methods (Modes):</strong></p> |
| <ol> |
| <li><strong>1. HH/LL of TF:</strong> Calculates <code>High - Low</code> of the entire timeframe. Sign (+/-) is determined by the TF Candle's Open & Close direction.</li> |
| <li><strong>2. TF Open-Close:</strong> Calculates <code>Close - Open</code> of the TF candle.</li> |
| <li><strong>3. T2 - T1 Close:</strong> Current TF Close minus Previous TF Close.</li> |
| <li><strong>4. Extremes vs T1:</strong> Uses Current High/Low against Prev Close based on TF trend.</li> |
| <li><strong>5. Custom Formula:</strong> Write your own math logic using defined variables.</li> |
| </ol> |
| <hr style="border:0; border-top:1px solid #dee2e6; margin:10px 0;"> |
| <p><strong>Custom Formula Variables:</strong></p> |
| <p> |
| <code>O, H, L, C</code> : Current TF Candle<br> |
| <code>O_prev, H_prev, L_prev, C_prev</code> : Previous TF Candle<br> |
| <em>Functions:</em> <code>abs(x)</code>, <code>max(a,b)</code>, <code>min(a,b)</code><br> |
| <em>Form. Sign Option:</em> If Absolute, calculation determines sign. If Candle Dir, the TF Open/Close overrides the formula's calculated math sign. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| let d = new Date(); |
| if (d.getDay() === 0) d.setDate(d.getDate() - 2); |
| else if (d.getDay() === 6) d.setDate(d.getDate() - 1); |
| document.getElementById('i_date').value = d.toISOString().split('T')[0]; |
| |
| function fetchExpiries() { |
| fetch('./api/expiries' + window.location.search).then(r=>r.json()).then(d => { |
| if(d.s === 'ok' && d.expiries.length > 0) { |
| document.getElementById('i_exp').innerHTML = d.expiries.map(e => `<option value="${e}">${e}</option>`).join(''); |
| } else setTimeout(fetchExpiries, 2000); |
| }).catch(() => setTimeout(fetchExpiries, 2000)); |
| } |
| fetchExpiries(); |
| |
| function toggleCustom() { |
| const val = document.getElementById('i_method').value; |
| document.getElementById('c_formula').style.display = val === '5' ? 'flex' : 'none'; |
| document.getElementById('c_sign').style.display = val === '5' ? 'flex' : 'none'; |
| } |
| |
| function formatNum(n) { return n > 0 ? '+' + n.toFixed(2) : n.toFixed(2); } |
| |
| function showBreakdown(dataStr, timeStr) { |
| const d = JSON.parse(decodeURIComponent(dataStr)); |
| |
| document.getElementById('modalTitle').innerText = `Breakdown: ${timeStr} (${d.tf_name} Lookback)`; |
| document.getElementById('m_t1spot').innerText = d.t1_spot.toFixed(2); |
| document.getElementById('m_t2spot').innerText = d.t2_spot.toFixed(2); |
| |
| const sDiff = document.getElementById('m_spotdiff'); |
| sDiff.innerText = formatNum(d.spot_diff); |
| sDiff.style.color = d.spot_diff > 0 ? 'var(--green-text)' : (d.spot_diff < 0 ? 'var(--red-text)' : 'inherit'); |
| |
| document.getElementById('m_cedelta').innerText = formatNum(d.ce_delta); |
| document.getElementById('m_pedelta').innerText = formatNum(d.pe_delta); |
| |
| const nd = document.getElementById('m_netdelta'); |
| nd.innerText = formatNum(d.net_delta); |
| nd.style.color = d.net_delta > 0 ? 'var(--green-text)' : (d.net_delta < 0 ? 'var(--red-text)' : 'inherit'); |
| |
| let tHtml = ""; |
| const atm_idx = parseInt(d.atm_idx); |
| |
| d.strike_details.forEach((row, i) => { |
| const cClass = (i === atm_idx) ? "atm-row" : ""; |
| |
| const ceStyling = !row.cv ? 'class="excluded-cell"' : ''; |
| const peStyling = !row.pv ? 'class="excluded-cell"' : ''; |
| |
| const cdColor = (!row.cv) ? 'inherit' : (row.cd > 0 ? 'var(--green-text)' : (row.cd < 0 ? 'var(--red-text)' : 'inherit')); |
| const crColor = (!row.cv) ? 'inherit' : (row.cr > 0 ? 'var(--green-text)' : (row.cr < 0 ? 'var(--red-text)' : 'inherit')); |
| |
| const pdColor = (!row.pv) ? 'inherit' : (row.pd > 0 ? 'var(--green-text)' : (row.pd < 0 ? 'var(--red-text)' : 'inherit')); |
| const prColor = (!row.pv) ? 'inherit' : (row.pr > 0 ? 'var(--green-text)' : (row.pr < 0 ? 'var(--red-text)' : 'inherit')); |
| |
| let strikeText = (i === atm_idx) ? `${row.k} (ATM)` : row.k; |
| |
| tHtml += `<tr class="${cClass}"> |
| <td style="font-weight:bold; background:#f8f9fa;">${strikeText}</td> |
| |
| <td ${ceStyling}>${row.c1.toFixed(2)}</td><td ${ceStyling}>${row.c2.toFixed(2)}</td> |
| <td ${ceStyling} style="color:${cdColor}; font-weight:bold;">${formatNum(row.cd)}</td> |
| <td ${ceStyling} style="color:${crColor}; font-weight:bold;">${row.cr.toFixed(4)}</td> |
| |
| <td ${peStyling}>${row.p1.toFixed(2)}</td><td ${peStyling}>${row.p2.toFixed(2)}</td> |
| <td ${peStyling} style="color:${pdColor}; font-weight:bold;">${formatNum(row.pd)}</td> |
| <td ${peStyling} style="color:${prColor}; font-weight:bold;">${row.pr.toFixed(4)}</td> |
| </tr>`; |
| }); |
| |
| tHtml += `<tr class="total-row"> |
| <td>TOTAL Diffs</td> |
| <td colspan="2"></td> |
| <td style="color:${d.ce_diff_total>0?'var(--green-text)':'var(--red-text)'}">${formatNum(d.ce_diff_total)}</td> |
| <td></td> |
| <td colspan="2"></td> |
| <td style="color:${d.pe_diff_total>0?'var(--green-text)':'var(--red-text)'}">${formatNum(d.pe_diff_total)}</td> |
| <td></td> |
| </tr>`; |
| |
| document.getElementById('modalTableBody').innerHTML = tHtml; |
| document.getElementById('modal').style.display = 'flex'; |
| } |
| |
| async function loadHeatmap() { |
| const btn = document.getElementById('btnLoad'); |
| const loader = document.getElementById('btnLoader'); |
| const txt = document.getElementById('btnTxt'); |
| |
| txt.innerText = "Wait..."; loader.style.display = "inline-block"; btn.disabled = true; |
| |
| try { |
| const params = new URLSearchParams(window.location.search); |
| params.set('date', document.getElementById('i_date').value); |
| params.set('start', document.getElementById('i_start').value); |
| params.set('end', document.getElementById('i_end').value); |
| params.set('expiry', document.getElementById('i_exp').value); |
| params.set('tfs', document.getElementById('i_tfs').value); |
| params.set('atm_range', document.getElementById('i_atm').value); |
| |
| params.set('strike_mode', document.getElementById('i_strike_mode').value); |
| params.set('method', document.getElementById('i_method').value); |
| params.set('formula', document.getElementById('i_formula').value); |
| params.set('sign', document.getElementById('i_sign').value); |
| params.set('divisor', document.getElementById('i_divisor').value); |
| |
| const res = await fetch(`./api/heatmap?${params.toString()}`); |
| const data = await res.json(); |
| |
| if (data.s !== 'ok') { alert("API Error: " + data.message); return; } |
| |
| let headHtml = `<th style="width:50px;">Time</th><th style="width:60px;">Spot 1m Δ</th>`; |
| data.timeframes.forEach(tf => { headHtml += `<th>${tf} Min</th>`; }); |
| document.getElementById('tableHead').innerHTML = headHtml; |
| |
| let bodyHtml = ""; |
| const firstTf = data.timeframes[0].toString(); |
| |
| data.matrix.forEach(row => { |
| const spotColor = row.spot_min_diff > 0 ? 'var(--green-text)' : (row.spot_min_diff < 0 ? 'var(--red-text)' : 'inherit'); |
| const spotSign = row.spot_min_diff > 0 ? '+' : ''; |
| |
| let timeCellHtml = `<td class="time-cell">${row.time}</td>`; |
| if (row.tfs[firstTf]) { |
| const enc1 = encodeURIComponent(JSON.stringify(row.tfs[firstTf])); |
| timeCellHtml = `<td class="time-cell" onclick="showBreakdown('${enc1}', '${row.time}')">${row.time}</td>`; |
| } |
| |
| let rHtml = `<tr> |
| ${timeCellHtml} |
| <td class="spot-cell" style="color: ${spotColor}">${spotSign}${row.spot_min_diff.toFixed(2)}</td>`; |
| |
| data.timeframes.forEach(tf => { |
| const cell = row.tfs[tf.toString()]; |
| if (!cell) { rHtml += `<td><span class="blank-cell">-</span></td>`; } |
| else { |
| const cssClass = cell.net_delta > 0 ? 'pos-val' : (cell.net_delta < 0 ? 'neg-val' : ''); |
| const anomalyClass = cell.is_anomaly ? 'anomaly-cell' : ''; |
| const sign = cell.net_delta > 0 ? '+' : ''; |
| rHtml += `<td><span class="delta-cell ${cssClass} ${anomalyClass}">${sign}${cell.net_delta.toFixed(1)}</span></td>`; |
| } |
| }); |
| bodyHtml += rHtml + `</tr>`; |
| }); |
| document.getElementById('tableBody').innerHTML = bodyHtml; |
| } catch (e) { |
| console.error(e); |
| alert("Network Error. Check console logs."); |
| } finally { |
| txt.innerText = "Load"; loader.style.display = "none"; btn.disabled = false; |
| } |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |