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 (
Upstox API Token Required
Open Settings (Gear Icon in the top right corner) to paste your active Bearer access token.
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.
| 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)}
|
||||||||||||||||
Calculated Reversals
Required to query the NSE option API directly from the browser context without CORS blocking.
Settings are active instantly. If your API connection fails, switch the active source back to **Simulated** in the top header.