Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/ui/templates/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ <h3 style="margin: 0; font-size: 14px; color: #ccc;">Share Holding Pattern</h3>
<hr style="border-color: #333; margin: 15px 0;">

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: center;">
<label style="color: #ccc; font-size: 13px; font-weight: bold;">Total Outstanding:</label>
<label style="color: #ccc; font-size: 13px; font-weight: bold;">Total Outstanding (100%):</label>
<span id="bb-total-out" style="color: #60a5fa; font-weight: bold;">0</span>

<label style="color: #aaa; font-size: 13px;">Total Buy Back Offer:</label>
Expand Down Expand Up @@ -754,7 +754,7 @@ <h3 style="margin: 0; font-size: 14px; color: #ccc;">Share Holding Pattern</h3>
<hr style="border-color: #333; margin: 15px 0;">

<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: center;">
<label style="color: #ccc; font-size: 13px; font-weight: bold;">Total Outstanding:</label>
<label style="color: #ccc; font-size: 13px; font-weight: bold;">Total Outstanding (100%):</label>
<span id="ofs-total-out" style="color: #60a5fa; font-weight: bold;">0</span>

<label style="color: #aaa; font-size: 13px;">Total OFS Offer:</label>
Expand Down
290 changes: 109 additions & 181 deletions backend/web/api/data/view_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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():
Expand Down
Loading