# ============================================================================== # PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION (FYERS LIGHT MODE) # ============================================================================== import os import time import json import threading import asyncio from collections import defaultdict from typing import Optional import pandas as pd import uvicorn from fastapi import FastAPI, Cookie, Query, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from fyers_apiv3 import fyersModel from fyers_apiv3.FyersWebsocket import data_ws # ========================================== # 1. EXACT CONFIGURATION (DO NOT CHANGE) # ========================================== CLIENT_ID = "FV3BVDFQTO-100" SECRET_KEY = "ZALDI5GCDF" REDIRECT_URI = "https://topindianhacker-optionschart.hf.space/callback" CACHE_FILE = "master_cache.json" app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # ========================================== # 2. MASTER DB LOADER # ========================================== MASTER_DB = {} SEARCH_INDEX =[] SYSTEM_STATUS = {"ready": False, "message": "Booting..."} UPDATE_LOCK = threading.Lock() 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(' ') return parts[0].upper().replace(':', '').replace('-', '') except: return symbol.split(':')[1] if ':' in symbol else 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..." headers = { "User-Agent": "Mozilla/5.0" } df_nse = pd.read_csv("https://public.fyers.in/sym_details/NSE_FO.csv", 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' df_mcx = pd.read_csv("https://public.fyers.in/sym_details/MCX_COM.csv", 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' df_bse = pd.read_csv("https://public.fyers.in/sym_details/BSE_FO.csv", 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' 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, desc, exch = row['Symbol'], row['Desc'], row['Exch'] root = get_root_symbol(desc, sym) if root not in temp_db: spot_sym = INDEX_MAP.get(root, f"NSE:{root}-EQ") if exch == "MCX": spot_sym = "MCX" elif exch == "BSE": spot_sym = f"BSE:{root}" temp_db[root] = { "spot": spot_sym, "exch": exch, "items":[] } try: exp = int(row['Expiry']) except: exp = 0 strike, opt_type = 0.0, "FUT" if "CE" in sym or "PE" in sym: parts = desc.strip().split(' ') if len(parts) >= 2: try: val_clean = parts[-2].replace('.', '', 1) if val_clean.isdigit(): strike = float(parts[-2]) 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(): final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted(data["items"], key=lambda x: (x['e'], x['k'])) } search_list.append({ "root": root, "exch": data["exch"], "display": 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.update({"ready": True, "message": "Ready"}) except Exception as 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: with open(CACHE_FILE, "r") as f: data = json.load(f) MASTER_DB, SEARCH_INDEX = data["db"], data["idx"] SYSTEM_STATUS.update({"ready": True, "message": "Ready (Cached)"}) except: pass if not SYSTEM_STATUS["ready"]: threading.Thread(target=update_master_db).start() # ========================================== # 3. FYERS WEBSOCKET ENGINE (BACKEND) # ========================================== STATE_MEMORY = defaultdict(lambda: {"ltp": 0, "b": 0, "s": 0}) FS = None SOCKET_RUNNING = False ACTIVE_SYMBOLS = set() GLOBAL_TOKEN = None def on_ws_message(msg): ticks = msg if isinstance(msg, list) else [msg] for t in ticks: sym = t.get('symbol') if sym: if 'ltp' in t: STATE_MEMORY[sym]['ltp'] = t['ltp'] if 'tot_buy_qty' in t: STATE_MEMORY[sym]['b'] = t['tot_buy_qty'] if 'tot_sell_qty' in t: STATE_MEMORY[sym]['s'] = t['tot_sell_qty'] def on_ws_error(e): print(f"[Fyers WS] Error: {e}") def on_ws_close(c): global SOCKET_RUNNING SOCKET_RUNNING = False print("[Fyers WS] Closed") def on_ws_open(): global SOCKET_RUNNING, ACTIVE_SYMBOLS SOCKET_RUNNING = True print("[Fyers WS] Connected Successfully.") if ACTIVE_SYMBOLS: FS.subscribe(symbols=list(ACTIVE_SYMBOLS), data_type="SymbolUpdate") def start_fyers_ws(token): global FS, SOCKET_RUNNING, GLOBAL_TOKEN GLOBAL_TOKEN = token if FS and SOCKET_RUNNING: return FS = data_ws.FyersDataSocket( access_token=token, log_path="", litemode=False, write_to_file=False, reconnect=True, on_connect=on_ws_open, on_close=on_ws_close, on_error=on_ws_error, on_message=on_ws_message ) threading.Thread(target=FS.connect, daemon=True).start() def stop_fyers_ws(): global FS, SOCKET_RUNNING if FS and SOCKET_RUNNING: FS.close_connection() SOCKET_RUNNING = False def update_subscriptions(symbols_list): global ACTIVE_SYMBOLS, FS, SOCKET_RUNNING new_set = set(symbols_list) to_add = list(new_set - ACTIVE_SYMBOLS) to_remove = list(ACTIVE_SYMBOLS - new_set) ACTIVE_SYMBOLS = new_set if SOCKET_RUNNING and FS: if to_add: for i in range(0, len(to_add), 50): FS.subscribe(symbols=to_add[i:i+50], data_type="SymbolUpdate") if to_remove: for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate") # ========================================== # 4. API & AUTH ROUTES # ========================================== def get_fyers_session(): return fyersModel.SessionModel( client_id=CLIENT_ID, secret_key=SECRET_KEY, redirect_uri=REDIRECT_URI, response_type="code", grant_type="authorization_code" ) @app.get("/") def home(access_token: Optional[str] = Cookie(None)): if not access_token: auth_url = get_fyers_session().generate_authcode() return HTMLResponse(f"""
{json.dumps(response, indent=2)}")
except Exception as e:
return HTMLResponse(f"{str(e)}")
@app.get("/logout")
def logout():
response = RedirectResponse(url="/")
response.delete_cookie("access_token")
stop_fyers_ws()
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)
# ==========================================
# 5. FASTAPI WEBSOCKET (STREAM TO BROWSER)
# ==========================================
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
active_client_symbols =[]
try:
while True:
try:
data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)
action = data.get("action")
if action == "init":
token = data.get("token")
if token: start_fyers_ws(token)
elif action == "subscribe":
active_client_symbols = data.get("symbols",[])
update_subscriptions(active_client_symbols)
elif action == "stop":
stop_fyers_ws()
elif action == "start":
if GLOBAL_TOKEN: start_fyers_ws(GLOBAL_TOKEN)
except asyncio.TimeoutError:
pass
if SOCKET_RUNNING and active_client_symbols:
payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
if payload:
await websocket.send_json({"type": "data", "payload": payload})
except WebSocketDisconnect:
print("[Client WS] Disconnected")
# ==========================================
# 6. FRONTEND UI (WORLD'S BEST LIGHT MODE)
# ==========================================
HTML_TEMPLATE = """