diff --git a/backend/ui/templates/workbench.html b/backend/ui/templates/workbench.html index 8a375d2b..9d948e8e 100644 --- a/backend/ui/templates/workbench.html +++ b/backend/ui/templates/workbench.html @@ -565,7 +565,7 @@

Share Holding Pattern


- + 0 @@ -754,7 +754,7 @@

Share Holding Pattern


- + 0 diff --git a/backend/web/api/data/view_routes.py b/backend/web/api/data/view_routes.py index 26d569a2..9ad1c24c 100644 --- a/backend/web/api/data/view_routes.py +++ b/backend/web/api/data/view_routes.py @@ -131,29 +131,55 @@ def get_fundamentals(symbol: str = Query(..., min_length=1), db: Session = Depen def get_live_price(symbol: str = Query(..., min_length=1), db: Session = Depends(get_db)): """Fetch CMP and Futures price from the backend database.""" try: - from backend.ingest.nse_models import DailyDerivativesAnalysis - - records = db.query(DailyDerivativesAnalysis).filter( - DailyDerivativesAnalysis.symbol == symbol.upper() - ).order_by(DailyDerivativesAnalysis.trade_date.desc()).limit(1).all() - - if records: - db_cmp = records[0].eq_close_price or records[0].close_price - return { - "symbol": symbol.upper(), - "price": db_cmp, - "near_fut_price": records[0].near_fut_close or db_cmp, - "next_fut_price": records[0].next_fut_close or db_cmp, - "far_fut_price": records[0].far_fut_close or db_cmp - } - else: - return { - "symbol": symbol.upper(), - "price": 0, - "near_fut_price": 0, - "next_fut_price": 0, - "far_fut_price": 0 - } + from backend.ingest.nse_models import BhavcopyEQ, BhavcopyFO, HistoricalIndexData + + # 1. Fetch CMP from BhavcopyEQ + latest_eq = db.query(BhavcopyEQ).filter( + BhavcopyEQ.symbol == symbol.upper(), + BhavcopyEQ.series == 'EQ' + ).order_by(BhavcopyEQ.trade_date.desc()).first() + + db_cmp = latest_eq.close_price if latest_eq else 0.0 + + # If CMP is 0, try fetching from HistoricalIndexData + if db_cmp == 0.0: + latest_idx = db.query(HistoricalIndexData).filter( + HistoricalIndexData.index_name == symbol.upper() + ).order_by(HistoricalIndexData.trade_date.desc()).first() + if latest_idx: + db_cmp = latest_idx.close_price + + # 2. Fetch Futures from BhavcopyFO + near_fut = 0.0 + next_fut = 0.0 + far_fut = 0.0 + + # We need the latest trade date in BhavcopyFO for this symbol to get active futures + latest_fo_date_record = db.query(BhavcopyFO).filter( + BhavcopyFO.ticker_symb == symbol.upper(), + BhavcopyFO.instrument_type.like('FUT%') + ).order_by(BhavcopyFO.trade_date.desc()).first() + + if latest_fo_date_record: + latest_fo_date = latest_fo_date_record.trade_date + futures = db.query(BhavcopyFO).filter( + BhavcopyFO.ticker_symb == symbol.upper(), + BhavcopyFO.trade_date == latest_fo_date, + BhavcopyFO.instrument_type.like('FUT%') + ).order_by(BhavcopyFO.expiry_date.asc()).limit(3).all() + + if len(futures) > 0: near_fut = futures[0].close_price + if len(futures) > 1: next_fut = futures[1].close_price + if len(futures) > 2: far_fut = futures[2].close_price + + # Fallback to CMP if no future exists + return { + "symbol": symbol.upper(), + "price": db_cmp, + "near_fut_price": near_fut or db_cmp, + "next_fut_price": next_fut or db_cmp, + "far_fut_price": far_fut or db_cmp + } except Exception as e: logger.error(f"Error fetching price for {symbol}: {e}") return { @@ -173,169 +199,71 @@ def get_shareholding(symbol: str = Query(..., min_length=1), db: Session = Depen import xml.etree.ElementTree as ET # Try fetching from NSE API first for absolute exact share counts - try: - url = f"https://www.nseindia.com/api/corporate-share-holdings-master?index=equities&symbol={symbol.upper()}" - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - 'Accept': 'application/json', - 'Accept-Language': 'en-US,en;q=0.9', - } - session = requests.Session() - session.get('https://www.nseindia.com', headers=headers, timeout=5) - response = session.get(url, headers=headers, timeout=5) - - if response.status_code == 200: - data = response.json() - if data and isinstance(data, list): - xbrl_url = data[0].get('xbrl') - if xbrl_url: - res_xml = session.get(xbrl_url, headers=headers, timeout=5) - if res_xml.status_code == 200: - root = ET.fromstring(res_xml.text) - def get_shares(context_id): - for elem in root: - tag_name = elem.tag.split('}')[-1] - if tag_name == 'NumberOfShares' and elem.attrib.get('contextRef') == context_id: - return float(elem.text) - return 0 - - promoter = get_shares('ShareholdingOfPromoterAndPromoterGroup_ContextI') - fii = get_shares('InstitutionsForeign_ContextI') - dii = get_shares('InstitutionsDomestic_ContextI') - retail_less_200k = get_shares('ResidentIndividualShareholdersHoldingNominalShareCapitalUpToRsTwoLakh_ContextI') - public_gt_200k = get_shares('ResidentIndividualShareholdersHoldingNominalShareCapitalInExcessOfRsTwoLakh_ContextI') - total_out = get_shares('ShareholdingPattern_ContextI') - - if total_out > 0: - return { - "symbol": symbol.upper(), - "promoter_holding": round((promoter/total_out)*100, 2), - "fii_holding": round((fii/total_out)*100, 2), - "dii_holding": round((dii/total_out)*100, 2), - "retail_holding": round((retail_less_200k/total_out)*100, 2), - "public_holding": round((public_gt_200k/total_out)*100, 2), - "total_outstanding": int(total_out), - - # Absolute values - "promoter_shares": int(promoter), - "fii_shares": int(fii), - "dii_shares": int(dii), - "retail_shares": int(retail_less_200k), - "public_shares": int(public_gt_200k) - } - except Exception as e: - logger.warning(f"Failed to fetch XBRL from NSE for {symbol}: {e}") - - # Fallback to Screener.in if NSE fails - from bs4 import BeautifulSoup - import yfinance as yf - - url = f"https://www.screener.in/company/{symbol.upper()}/consolidated/" - headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} - response = requests.get(url, headers=headers) - - if response.status_code != 200: - url = f"https://www.screener.in/company/{symbol.upper()}/" - response = requests.get(url, headers=headers) - - promoter_pct = 0.0 - fii_pct = 0.0 - dii_pct = 0.0 - retail_pct = 0.0 + url = f"https://www.nseindia.com/api/corporate-share-holdings-master?index=equities&symbol={symbol.upper()}" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + } + session = requests.Session() + session.get('https://www.nseindia.com', headers=headers, timeout=5) + response = session.get(url, headers=headers, timeout=5) if response.status_code == 200: - soup = BeautifulSoup(response.text, 'html.parser') - sh_section = soup.find('section', id='shareholding') - if sh_section: - table = sh_section.find('table') - if table: - rows = table.find_all('tr') - for row in rows: - cols = [col.text.strip() for col in row.find_all(['th', 'td'])] - if not cols: continue - label = cols[0].replace('+', '').strip().lower() - if len(cols) > 1: - val_str = cols[-1].replace('%', '') - try: - val = float(val_str) - if label == 'promoters': - promoter_pct = val - elif label == 'fiis': - fii_pct = val - elif label == 'diis': - dii_pct = val - elif label == 'public': - retail_pct = val - except ValueError: - pass - - total_shares = 0 - try: - ticker = yf.Ticker(f"{symbol.upper()}.NS") - total_shares = ticker.fast_info.get('shares', 0) - if not total_shares: - total_shares = ticker.info.get('sharesOutstanding', 0) - except Exception: - pass - - return { - "symbol": symbol.upper(), - "promoter_holding": promoter_pct, - "fii_holding": fii_pct, - "dii_holding": dii_pct, - "retail_holding": 0, # Cannot reliably distinguish from screener fallback - "public_holding": retail_pct, - "total_outstanding": int(total_shares) if total_shares else 0, - - # Since absolute values aren't known, don't return them so frontend falls back to pct math - } + data = response.json() + if data and isinstance(data, list): + xbrl_url = data[0].get('xbrl') + if xbrl_url: + res_xml = session.get(xbrl_url, headers=headers, timeout=5) + if res_xml.status_code == 200: + root = ET.fromstring(res_xml.text) + def get_shares(context_id): + for elem in root: + tag_name = elem.tag.split('}')[-1] + if tag_name == 'NumberOfShares' and elem.attrib.get('contextRef') == context_id: + return float(elem.text) + return 0 + + promoter = get_shares('ShareholdingOfPromoterAndPromoterGroup_ContextI') + fii = get_shares('InstitutionsForeign_ContextI') + dii = get_shares('InstitutionsDomestic_ContextI') + retail_less_200k = get_shares('ResidentIndividualShareholdersHoldingNominalShareCapitalUpToRsTwoLakh_ContextI') + total_out = get_shares('ShareholdingPattern_ContextI') + + if total_out > 0: + # Calculate Public / Others dynamically as a residual + public_gt_200k = total_out - (promoter + fii + dii + retail_less_200k) + public_gt_200k = max(0, public_gt_200k) + + promoter_holding = round((promoter/total_out)*100, 2) + fii_holding = round((fii/total_out)*100, 2) + dii_holding = round((dii/total_out)*100, 2) + retail_holding = round((retail_less_200k/total_out)*100, 2) + + public_holding = 100.0 - (promoter_holding + fii_holding + dii_holding + retail_holding) + public_holding = round(max(0, public_holding), 2) + + return { + "symbol": symbol.upper(), + "promoter_holding": promoter_holding, + "fii_holding": fii_holding, + "dii_holding": dii_holding, + "retail_holding": retail_holding, + "public_holding": public_holding, + "total_outstanding": int(total_out), + "promoter_shares": int(promoter), + "fii_shares": int(fii), + "dii_shares": int(dii), + "retail_shares": int(retail_less_200k), + "public_shares": int(public_gt_200k) + } + from fastapi import HTTPException + raise HTTPException(status_code=500, detail="NSE shareholding fetch failed or no data returned. Fallbacks are disabled.") except Exception as e: + from fastapi import HTTPException logger.error(f"Error fetching shareholding for {symbol}: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -# Share a single NseSession instance for proxying -_nse_session = None -def get_nse_session(): - global _nse_session - if _nse_session is None: - _nse_session = NseSession() - return _nse_session - -def get_model_for_type(data_type: str): - mapping = { - 'bhavcopy': Bhavcopy, - 'bhavcopy_eq': models.BhavcopyEQ, - 'bhavcopy_fo': models.BhavcopyFO, - 'participant_oi': models.FAOParticipantOI, - 'fao_participant_oi': models.FAOParticipantOI, - 'fo_volatility': models.FOVolatility, - 'fii_stats': models.FIIDerivativesStat, - 'bulk_deals': models.BulkDeal, - 'block_deals': models.BlockDeal, - 'mto': models.MTODelivery, - 'mwpl': models.MWPLClientPosition, - 'pe_ratio': models.PERatio, - 'pe_ratio_idx': models.IndexPERatio, - 'india_vix': models.IndiaVIX, - 'var_stats': models.VaRStat, - 'contract_delta': models.ContractDelta, - 'margin_trading': models.MarginTrading, - 'fii_dii_cash': models.FIIDIICash, - 'security_master': models.SecurityMaster, - 'historical_index_data': models.HistoricalIndexData, - 'auctions': models.Auction, # Added auctions just in case - 'historical_index_data': models.HistoricalIndexData - } - # Safely get CorporateAction if it exists in models (may be unmerged) - if data_type in ['corporate_actions', 'dividend'] and hasattr(models, 'CorporateAction'): - return getattr(models, 'CorporateAction') - if data_type in ['board_meetings', 'board_meeting'] and hasattr(models, 'BoardMeeting'): - return getattr(models, 'BoardMeeting') - - return mapping.get(data_type) + raise HTTPException(status_code=500, detail=f"NSE shareholding fetch error: {str(e)}") -import requests @router.get("/api/proxy/rights") def proxy_rights():