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 @@
-
+
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():