import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Play, Pause, RefreshCw, Settings, User, Search, TrendingUp, TrendingDown, AlertCircle, BarChart2, Filter, Download, Wifi, WifiOff, Key, Globe, Database, X } from 'lucide-react'; // --- MOCK DATA GENERATOR (Fallback and default preview state) --- const generateOptionChain = (spot, step, count) => { const baseStrike = Math.round(spot / step) * step - (Math.floor(count / 2) * step); const data = []; for (let i = 0; i < count; i++) { const strike = baseStrike + (i * step); const isCallITM = strike < spot; const isPutITM = strike > spot; const distance = Math.abs(strike - spot); const factor = Math.max(0.1, 1 - (distance / (step * 10))); const callOI = Math.floor((Math.random() * 50000 + 10000) * factor); const putOI = Math.floor((Math.random() * 50000 + 10000) * factor); const callVol = Math.floor((Math.random() * 200000 + 50000) * factor); const putVol = Math.floor((Math.random() * 200000 + 50000) * factor); data.push({ strike, calls: { oi: callOI, chgOi: Math.floor((Math.random() * 10000) - 5000), vol: callVol, iv: (Math.random() * 15 + 10).toFixed(2), ltp: isCallITM ? (spot - strike + Math.random() * 20).toFixed(2) : (Math.random() * 50).toFixed(2), chg: (Math.random() * 10 - 5).toFixed(2), bidQty: Math.floor(Math.random() * 1000), bidPrice: (Math.random() * 100).toFixed(2), askPrice: (Math.random() * 100).toFixed(2), askQty: Math.floor(Math.random() * 1000), itm: isCallITM }, puts: { oi: putOI, chgOi: Math.floor((Math.random() * 10000) - 5000), vol: putVol, iv: (Math.random() * 15 + 10).toFixed(2), ltp: isPutITM ? (strike - spot + Math.random() * 20).toFixed(2) : (Math.random() * 50).toFixed(2), chg: (Math.random() * 10 - 5).toFixed(2), bidQty: Math.floor(Math.random() * 1000), bidPrice: (Math.random() * 100).toFixed(2), askPrice: (Math.random() * 100).toFixed(2), askQty: Math.floor(Math.random() * 1000), itm: isPutITM } }); } return processOptionChainFlags(data); }; // --- DYNAMIC POST-PROCESSING FOR MAX VALUE FLAGGING --- // Essential for LTP Calculator calculations: Support, Resistance, WTT, and WTB highlight badges const processOptionChainFlags = (data) => { if (!data || data.length === 0) return []; let maxCallOI = 0; let maxPutOI = 0; let maxCallVol = 0; let maxPutVol = 0; // First pass: identify true maxima data.forEach(row => { if (row.calls) { if (row.calls.oi > maxCallOI) maxCallOI = row.calls.oi; if (row.calls.vol > maxCallVol) maxCallVol = row.calls.vol; } if (row.puts) { if (row.puts.oi > maxPutOI) maxPutOI = row.puts.oi; if (row.puts.vol > maxPutVol) maxPutVol = row.puts.vol; } }); // Second pass: apply visual styling classes and WTT flags return data.map(row => { const updatedRow = { ...row }; if (updatedRow.calls) { updatedRow.calls.isMaxOI = updatedRow.calls.oi === maxCallOI && maxCallOI > 0; updatedRow.calls.isMaxVol = updatedRow.calls.vol === maxCallVol && maxCallVol > 0; updatedRow.calls.isSecondMaxOI = maxCallOI > 0 && updatedRow.calls.oi > maxCallOI * 0.8 && updatedRow.calls.oi !== maxCallOI; } if (updatedRow.puts) { updatedRow.puts.isMaxOI = updatedRow.puts.oi === maxPutOI && maxPutOI > 0; updatedRow.puts.isMaxVol = updatedRow.puts.vol === maxPutVol && maxPutVol > 0; updatedRow.puts.isSecondMaxOI = maxPutOI > 0 && updatedRow.puts.oi > maxPutOI * 0.8 && updatedRow.puts.oi !== maxPutOI; } return updatedRow; }); }; const INDICES = { NIFTY: { spot: 22435.65, step: 50, count: 24, upstoxKey: 'NSE_INDEX|Nifty 50', nseSymbol: 'NIFTY' }, BANKNIFTY: { spot: 48120.30, step: 100, count: 24, upstoxKey: 'NSE_INDEX|Nifty Bank', nseSymbol: 'BANKNIFTY' }, FINNIFTY: { spot: 21340.15, step: 50, count: 20, upstoxKey: 'NSE_INDEX|Nifty Fin Service', nseSymbol: 'FINNIFTY' } }; export default function App() { const [selectedIndex, setSelectedIndex] = useState('NIFTY'); const [spotPrice, setSpotPrice] = useState(INDICES.NIFTY.spot); const [isLive, setIsLive] = useState(true); const [chainData, setChainData] = useState([]); const [selectedPopover, setSelectedPopover] = useState(null); // Data feed configuration const [dataSource, setDataSource] = useState('mock'); // 'mock' | 'nse_public' | 'upstox' const [apiStatus, setApiStatus] = useState('idle'); // 'idle' | 'loading' | 'success' | 'error' | 'unauthorized' const [isSettingsOpen, setIsSettingsOpen] = useState(false); // Credentials & Proxy options const [upstoxToken, setUpstoxToken] = useState(''); const [corsProxy, setCorsProxy] = useState('https://api.codetabs.com/v1/proxy?quest='); const authErrorOccurred = useRef(false); // Initialize data structured rows useEffect(() => { const config = INDICES[selectedIndex]; setSpotPrice(config.spot); setChainData(generateOptionChain(config.spot, config.step, config.count)); }, [selectedIndex]); // --- DATA FEEDS RESILIENT MANAGEMENT --- useEffect(() => { authErrorOccurred.current = false; if (!isLive) { setApiStatus('idle'); return; } const fetchLiveFeed = async () => { // 1. SIMULATED MOCK FEED if (dataSource === 'mock') { setApiStatus('success'); setSpotPrice(prev => +(prev + (Math.random() * 4 - 2)).toFixed(2)); setChainData(prev => prev.map(row => { if (Math.random() > 0.85) { return { ...row, calls: { ...row.calls, ltp: (+row.calls.ltp + (Math.random() * 1 - 0.5)).toFixed(2) }, puts: { ...row.puts, ltp: (+row.puts.ltp + (Math.random() * 1 - 0.5)).toFixed(2) } }; } return row; })); return; } // 2. UPSTOX OFFICIAL API FEED if (dataSource === 'upstox') { if (authErrorOccurred.current) return; if (!upstoxToken || upstoxToken.trim() === '' || upstoxToken === '{your_access_token}') { setApiStatus('unauthorized'); return; } setApiStatus('loading'); const activeKey = INDICES[selectedIndex].upstoxKey; const url = `https://api.upstox.com/v2/market-quote/quotes?instrument_key=${encodeURIComponent(activeKey)}`; try { const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${upstoxToken.trim()}` } }); if (response.status === 401) { authErrorOccurred.current = true; setApiStatus('unauthorized'); return; } if (!response.ok) throw new Error(`HTTP ${response.status}`); const jsonResponse = await response.json(); if (jsonResponse.data && jsonResponse.data[activeKey]) { const liveLtp = jsonResponse.data[activeKey].last_price; setSpotPrice(liveLtp); // Re-generate option matrix dynamically centered on live Upstox Spot price setChainData(generateOptionChain(liveLtp, INDICES[selectedIndex].step, INDICES[selectedIndex].count)); setApiStatus('success'); } } catch (error) { console.error("Upstox Connection Failed:", error); setApiStatus('error'); } } // 3. NSE PUBLIC API FEED (Safely parsed & CORS Guarded) if (dataSource === 'nse_public') { setApiStatus('loading'); const symbol = INDICES[selectedIndex].nseSymbol; const nseUrl = `https://www.nseindia.com/api/option-chain-indices?symbol=${symbol}`; const finalUrl = `${corsProxy}${encodeURIComponent(nseUrl)}`; try { const response = await fetch(finalUrl); if (!response.ok) throw new Error(`HTTP status ${response.status}`); const rawData = await response.json(); if (rawData && rawData.records) { const underlyingSpot = Number(rawData.records.underlyingValue); setSpotPrice(underlyingSpot); const allStrikes = rawData.records.data; if (allStrikes && allStrikes.length > 0) { // Center-cropping strikes: Locate closest to spot price and slice a neat subset const sorted = [...allStrikes].sort((a, b) => a.strikePrice - b.strikePrice); let closestIdx = 0; let minDiff = Infinity; for (let i = 0; i < sorted.length; i++) { const diff = Math.abs(sorted[i].strikePrice - underlyingSpot); if (diff < minDiff) { minDiff = diff; closestIdx = i; } } const visibleStrikes = sorted.slice( Math.max(0, closestIdx - 12), Math.min(sorted.length, closestIdx + 12) ); // Defensive helper utility to parse properties safely avoiding crashes on nulls const safeNum = (val) => (val === null || val === undefined || isNaN(Number(val))) ? 0 : Number(val); const safeStr = (val, dec = 2) => (val === null || val === undefined || isNaN(Number(val))) ? '0.00' : Number(val).toFixed(dec); const parsedChain = visibleStrikes.map(item => ({ strike: item.strikePrice, calls: { oi: item.CE ? safeNum(item.CE.openInterest) : 0, chgOi: item.CE ? safeNum(item.CE.changeinOpenInterest) : 0, vol: item.CE ? safeNum(item.CE.totalTradedVolume) : 0, iv: item.CE ? safeStr(item.CE.impliedVolatility) : '0.00', ltp: item.CE ? safeStr(item.CE.lastPrice) : '0.00', chg: item.CE ? safeStr(item.CE.change) : '0.00', bidQty: item.CE ? safeNum(item.CE.bidQty) : 0, bidPrice: item.CE ? safeStr(item.CE.bidprice) : '0.00', // NSE returns bidprice with lowercase 'p' askPrice: item.CE ? safeStr(item.CE.askprice) : '0.00', // NSE returns askprice with lowercase 'p' askQty: item.CE ? safeNum(item.CE.askQty) : 0, itm: item.strikePrice < underlyingSpot }, puts: { oi: item.PE ? safeNum(item.PE.openInterest) : 0, chgOi: item.PE ? safeNum(item.PE.changeinOpenInterest) : 0, vol: item.PE ? safeNum(item.PE.totalTradedVolume) : 0, iv: item.PE ? safeStr(item.PE.impliedVolatility) : '0.00', ltp: item.PE ? safeStr(item.PE.lastPrice) : '0.00', chg: item.PE ? safeStr(item.PE.change) : '0.00', bidQty: item.PE ? safeNum(item.PE.bidQty) : 0, bidPrice: item.PE ? safeStr(item.PE.bidprice) : '0.00', askPrice: item.PE ? safeStr(item.PE.askprice) : '0.00', askQty: item.PE ? safeNum(item.PE.askQty) : 0, itm: item.strikePrice > underlyingSpot } })); // Map flags across extracted rows setChainData(processOptionChainFlags(parsedChain)); } setApiStatus('success'); } } catch (error) { console.error("NSE Public Fetch Failed:", error); setApiStatus('error'); } } }; fetchLiveFeed(); const interval = setInterval(fetchLiveFeed, dataSource === 'mock' ? 2000 : 5000); return () => clearInterval(interval); }, [isLive, dataSource, selectedIndex, upstoxToken, corsProxy]); // Derived metrics const pcr = useMemo(() => { const totalCallOI = chainData.reduce((acc, row) => acc + row.calls.oi, 0); const totalPutOI = chainData.reduce((acc, row) => acc + row.puts.oi, 0); return totalCallOI === 0 ? 0 : (totalPutOI / totalCallOI).toFixed(2); }, [chainData]); const supportStrike = useMemo(() => { const maxPut = chainData.find(r => r.puts.isMaxVol); return maxPut ? maxPut.strike : 0; }, [chainData]); const resistanceStrike = useMemo(() => { const maxCall = chainData.find(r => r.calls.isMaxVol); return maxCall ? maxCall.strike : 0; }, [chainData]); const formatNum = (num) => new Intl.NumberFormat('en-IN').format(num); const handleCellClick = (type, strike, val, e) => { setSelectedPopover({ x: e.clientX, y: e.clientY, type, strike, val }); }; return (
{/* HEADER NAV */} {/* METRICS DASHBOARD */}
Spot Price
{formatNum(spotPrice)} 0.24%
Expiry Date
PCR 1 ? 'text-emerald-400' : 'text-rose-400'}`}>{pcr}
EOS
Support {formatNum(supportStrike)}
EOR
Resistance {formatNum(resistanceStrike)} WTT
{/* TABLE AREA */}
{/* Connection Failure Guidance Banners */} {apiStatus === 'unauthorized' && dataSource === 'upstox' && (

Upstox API Token Required

Open Settings (Gear Icon in the top right corner) to paste your active Bearer access token.

)} {apiStatus === 'error' && dataSource === 'nse_public' && (

CORS or Security Block Detected from NSE

The NSE website blocks direct requests made by browser frames. Try choosing a different **CORS Proxy** or toggle back to **Simulated** in Settings.

)}
{chainData.map((row, idx) => { const nextRow = chainData[idx + 1]; const drawImaginaryLine = nextRow && row.strike < spotPrice && nextRow.strike > spotPrice; const getCallVolClass = () => row.calls.isMaxVol ? 'bg-blue-900/40 border-x border-blue-500/80 text-blue-200 font-bold shadow-[inset_0_0_10px_rgba(59,130,246,0.2)]' : (row.calls.itm ? 'bg-yellow-600/10' : ''); const getCallOIClass = () => row.calls.isMaxOI ? 'bg-amber-900/40 border-x border-amber-500/80 text-amber-200 font-bold shadow-[inset_0_0_10px_rgba(245,158,11,0.2)]' : (row.calls.itm ? 'bg-yellow-600/10' : ''); const getPutVolClass = () => row.puts.isMaxVol ? 'bg-blue-900/40 border-x border-blue-500/80 text-blue-200 font-bold shadow-[inset_0_0_10px_rgba(59,130,246,0.2)]' : (row.puts.itm ? 'bg-yellow-600/10' : ''); const getPutOIClass = () => row.puts.isMaxOI ? 'bg-amber-900/40 border-x border-amber-500/80 text-amber-200 font-bold shadow-[inset_0_0_10px_rgba(245,158,11,0.2)]' : (row.puts.itm ? 'bg-yellow-600/10' : ''); return ( {drawImaginaryLine && ( )} ); })}
CE (Calls) Strike PE (Puts)
OI Chng OI Volume IV LTP Chng Bid Qty Bid Strike Ask Ask Qty Chng LTP IV Volume Chng OI OI
handleCellClick('Resistance OI', row.strike, row.calls.oi, e)}>{formatNum(row.calls.oi)} {formatNum(row.calls.chgOi)} handleCellClick('Resistance Vol', row.strike, row.calls.vol, e)}>{formatNum(row.calls.vol)} {row.calls.iv} handleCellClick('Call LTP Reversal', row.strike, row.calls.ltp, e)}>{row.calls.ltp} {row.calls.chg} {row.calls.bidQty} {row.calls.bidPrice} {row.strike === resistanceStrike &&
} {row.strike === supportStrike &&
} {formatNum(row.strike)}
{row.puts.askPrice} {row.puts.askQty} {row.puts.chg} handleCellClick('Put LTP Reversal', row.strike, row.puts.ltp, e)}>{row.puts.ltp} {row.puts.iv} handleCellClick('Support Vol', row.strike, row.puts.vol, e)}>{formatNum(row.puts.vol)} {formatNum(row.puts.chgOi)} handleCellClick('Support OI', row.strike, row.puts.oi, e)}>{formatNum(row.puts.oi)}
Imaginary Line Spot: {formatNum(spotPrice)}
{/* REVERSAL ANALYSIS POPOVER */} {selectedPopover && ( <>
setSelectedPopover(null)}>

Reversal Analysis

STRIKE: {selectedPopover.strike}
Context: {selectedPopover.type}
Value: {formatNum(selectedPopover.val)}

Calculated Reversals

UR+1 (Ext Res){formatNum(selectedPopover.strike + 22.45)}
Divergence{formatNum(selectedPopover.strike)}
US-1 (Ext Sup){formatNum(selectedPopover.strike - 18.20)}
)} {/* SETTINGS / CREDENTIALS MODAL */} {isSettingsOpen && ( <>
setIsSettingsOpen(false)}>

API & Stream Settings