Files
stock-scanner/backend/app/services/stock_engine.py

664 lines
20 KiB
Python

import yfinance as yf
import pandas as pd
import numpy as np
from concurrent.futures import ThreadPoolExecutor
# ✅ PERSONAL
# ✅ PERSONAL
personal_stocks = [
"AFFLE.NS","ANANDRATHI.NS","CGPOWER.NS","HFCL.NS",
"IREDA.NS","INDOAMIN.NS","PCJEWELLER.NS","PCBL.NS",
"RANASUG.NS","SERVOTECH.NS","IDEA.NS","WELSPUNLIV.NS","SANDHAR.NS"
]
# ✅ SECTORS
sector_map = {"Personal": personal_stocks,
"lookup" :["GRAVITA.NS"],
"CG" :["SIEMENS.NS","ABB.NS","BEL.NS","CGPOWER.NS","BHEL.NS","HINDALCO.NS","CUMMINSIND.NS",
"SUZLON.NS","THERMAX.NS","POLYCAB.NS","KEI.NS","HAVELLS.NS","VOLTAS.NS","BLUESTARCO.NS",
"VGUARD.NS","AIAENG.NS","ELGIEQUIP.NS","TRITURBINE.NS","SANDHAR.NS"],
"NIFTY Bank": ["HDFCBANK.NS","ICICIBANK.NS","AXISBANK.NS","KOTAKBANK.NS","SBIN.NS","INDUSINDBK.NS",
"BANKBARODA.NS","PNB.NS","FEDERALBNK.NS","IDFCFIRSTB.NS","AUBANK.NS","BANDHANBNK.NS",
"CANBK.NS","UNIONBANK.NS","IOB.NS","UCOBANK.NS","BANKINDIA.NS","CENTRALBK.NS",
"MAHABANK.NS","YESBANK.NS","IDBI.NS","SOUTHBANK.NS",
"RBLBANK.NS","KTKBANK.NS","DCBBANK.NS"],
"NIFTY Auto": ["MARUTI.NS","M&M.NS","BAJAJ-AUTO.NS","EICHERMOT.NS","TVSMOTOR.NS",
"HEROMOTOCO.NS","ASHOKLEY.NS","BALKRISIND.NS","MRF.NS","APOLLOTYRE.NS","JKTYRE.NS",
"CEATLTD.NS","SONACOMS.NS","ENDURANCE.NS","EXIDEIND.NS","BOSCHLTD.NS",
"UNOMINDA.NS"],
"NIFTY Pharma": ["SUNPHARMA.NS","CIPLA.NS","DRREDDY.NS","DIVISLAB.NS","ZYDUSLIFE.NS","MANKIND.NS",
"LUPIN.NS","AUROPHARMA.NS","ALKEM.NS","BIOCON.NS","GLENMARK.NS",
"LAURUSLABS.NS","GRANULES.NS","IPCALAB.NS","JBCHEPHARM.NS","NATCOPHARM.NS","Eris.NS",
"ABBOTINDIA.NS","PFIZER.NS"],
"Defence": [ "HAL.NS", "BEL.NS", "MAZDOCK.NS", "DATAPATTNS.NS", "BDL.NS", "MIDHANI.NS", "BEML.NS",
"COCHINSHIP.NS", "GRSE.NS", "SOLARINDS.NS", "BHARATFORG.NS", "ZENTEC.NS", "PARAS.NS", "ASTRAMICRO.NS",
"IDEAFORGE.NS", "APOLLO.NS", "AVANTEL.NS"],
"Energy": ["NTPC.NS","POWERGRID.NS","RELIANCE.NS","ONGC.NS","BPCL.NS","IOC.NS","GAIL.NS","TATAPOWER.NS",
"ADANIGREEN.NS","ADANIPOWER.NS","SJVN.NS","IREDA.NS","SUZLON.NS",
"KPIGREEN.NS","BORORENEW.NS","AWL.NS","JSWENERGY.NS"],
"Chemical": ["SRF.NS","PIDILITIND.NS","LINDEINDIA.NS","TATACHEM.NS","AARTIIND.NS","ATUL.NS",
"DEEPAKNTR.NS","GUJALKALI.NS","NAVINFLUOR.NS","FLUOROCHEM.NS","CLEAN.NS","GALAXYSURF.NS",
"FINEORG.NS","CAMPUS.NS","ROSSARI.NS","SUDARSCHEM.NS","ALKYLAMINE.NS","BALAMINES.NS",
"VINATIORGA.NS","NOCIL.NS"],
"fmcg":["HINDUNILVR.NS","ITC.NS","NESTLEIND.NS","BRITANNIA.NS","TATACONSUM.NS","DABUR.NS","MARICO.NS",
"GODREJCP.NS","VBL.NS","COLPAL.NS","PGHH.NS","BALRAMCHIN.NS","EMAMILTD.NS","RADICO.NS","UBL.NS",
"UNITDSPR.NS","GLAXO.NS","JYOTHYLAB.NS","BIKAJI.NS","ZYDUSWELL.NS"],
"brkg":["ANANDRATHI.NS","NUVAMA.NS","PRUDENT.NS","ANGELONE.NS","MOTILALOFS.NS","GEOJITFSL.NS","5PAISA.NS",
"SMCGLOBAL.NS"],
"AMCReSt":["HDFCAMC.NS","NAM-INDIA.NS","UTIAMC.NS","ABSLAMC.NS","BAJFINANCE.NS","BAJAJFINSV.NS",
"CHOLAFIN.NS","SHRIRAMFIN.NS","MUTHOOTFIN.NS","M&MFIN.NS","DLF.NS","LODHA.NS","GODREJPROP.NS",
"OBEROIRLTY.NS","PRESTIGE.NS"],
"InfaMetal":["IRB.NS","LT.NS","KNRCON.NS","PNCINFRA.NS","HGINFRA.NS","TATASTEEL.NS",
"JSWSTEEL.NS","HINDALCO.NS","VEDL.NS","SAIL.NS","NMDC.NS","NATIONALUM.NS","JINDALSTEL.NS",
"COALINDIA.NS"]
}
# =========================================
# ✅ FIX MULTI-INDEX
# =========================================
def fix_df(df):
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
return df
# =========================================
# ✅ SHORT TERM (LIVE)
# =========================================
def analyze(stock):
try:
df = yf.download(
stock,
period="6mo",
interval="1d",
progress=False
)
nifty = yf.download(
"^NSEI",
period="6mo",
interval="1d",
progress=False
)
if df is None or df.empty:
return None
df = fix_df(df)
df.dropna(inplace=True)
nifty = fix_df(nifty)
nifty.dropna(inplace=True)
close = df["Close"]
high = df["High"]
low = df["Low"]
volume = df["Volume"].fillna(0)
if len(close) < 35:
return None
price = float(close.iloc[-1])
ema20 = float(
close.ewm(span=20).mean().iloc[-1]
)
ema50 = float(
close.ewm(span=50).mean().iloc[-1]
)
# RSI
delta = close.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.rolling(14).mean().iloc[-1]
avg_loss = loss.rolling(14).mean().iloc[-1]
if avg_loss == 0:
avg_loss = 0.0001
rs = avg_gain / avg_loss
rsi = float(
100 - (100 / (1 + rs))
)
# MACD
exp1 = close.ewm(span=12, adjust=False).mean()
exp2 = close.ewm(span=26, adjust=False).mean()
macd = exp1 - exp2
macd_signal_line = macd.ewm(span=9, adjust=False).mean()
macd_hist = macd - macd_signal_line
macd_val = float(macd.iloc[-1])
macd_hist_val = float(macd_hist.iloc[-1])
# ATR
high_low = high - low
high_close = (high - close.shift()).abs()
low_close = (low - close.shift()).abs()
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
atr = true_range.rolling(14).mean()
atr_val = float(atr.iloc[-1]) if not pd.isna(atr.iloc[-1]) else (price * 0.03)
# Breakout
resistance = float(
close.iloc[-15:-1].max()
)
breakout = (
price >= resistance * 0.995
)
# Volume
avg_vol = float(
volume.rolling(20).mean().iloc[-1]
)
current_vol = float(
volume.iloc[-1]
)
vol_ratio = (
current_vol / avg_vol
if avg_vol > 0 else 0
)
# ------------------------
# ORIGINAL SCORE
# ------------------------
score = 0
if price > ema20:
score += 40
if rsi > 55:
score += 30
if breakout:
score += 30
# ------------------------
# AI SCORE
# ------------------------
ai_score = 0
# Trend
if price > ema20:
ai_score += 20
# RSI
if rsi > 70:
ai_score += 20
elif rsi > 60:
ai_score += 15
elif rsi > 50:
ai_score += 10
# Breakout
if breakout:
ai_score += 20
# Volume
if vol_ratio > 3:
ai_score += 20
elif vol_ratio > 2:
ai_score += 15
elif vol_ratio > 1.5:
ai_score += 10
# Institutional
if vol_ratio > 3 and rsi > 60:
ai_score += 10
# MACD bullish crossover
if macd_hist_val > 0:
ai_score += 15
# Recovery
if price > ema20 and rsi > 50:
ai_score += 10
# ------------------------
# RELATIVE STRENGTH
# ------------------------
relative_strength = 0
try:
if len(close) >= 20 and len(nifty) >= 20:
stock_return = (
(float(close.iloc[-1]) - float(close.iloc[-20])) / float(close.iloc[-20])
) * 100
nifty_return = (
(float(nifty["Close"].iloc[-1]) - float(nifty["Close"].iloc[-20])) / float(nifty["Close"].iloc[-20])
) * 100
relative_strength = round(stock_return - nifty_return, 2)
except Exception:
relative_strength = 0
# Relative Strength Bonus
if relative_strength > 10:
ai_score += 15
elif relative_strength > 5:
ai_score += 10
elif relative_strength > 0:
ai_score += 5
ai_score = min(ai_score, 100)
# ------------------------
# CONFIDENCE
# ------------------------
confidence = ai_score
if relative_strength > 10:
rs_grade = "Very Strong 🚀"
elif relative_strength > 5:
rs_grade = "Strong ✅"
elif relative_strength > 0:
rs_grade = "Positive 👍"
else:
rs_grade = "Weak ❌"
# ------------------------
# GRADE
# ------------------------
if ai_score >= 90:
grade = "A+ 🚀"
elif ai_score >= 80:
grade = "A ✅"
elif ai_score >= 70:
grade = "B 👍"
elif ai_score >= 60:
grade = "C"
else:
grade = "D"
# ------------------------
# STRENGTH
# ------------------------
strength = round(((price - ema20) / ema20) * 100, 2)
# ------------------------
# INSTITUTIONAL BUYING
# ------------------------
institutional = vol_ratio > 3 and rsi > 60 and breakout
# ------------------------
# SMART MONEY SCORE
# ------------------------
institutional_score = 0
if vol_ratio > 2:
institutional_score += 25
if vol_ratio > 3:
institutional_score += 25
if breakout:
institutional_score += 25
if rsi > 60:
institutional_score += 25
if institutional_score >= 75:
institutional_label = "Very Strong 🔥"
elif institutional_score >= 50:
institutional_label = "Strong ✅"
elif institutional_score >= 25:
institutional_label = "Moderate ⚠️"
else:
institutional_label = "Weak"
fund_candidate = vol_ratio > 4 and breakout and rsi > 60 and price > ema20
# ------------------------
# ALERT
# ------------------------
alert = vol_ratio > 2.5 and breakout
# ------------------------
# REASON ENGINE
# ------------------------
reason = []
if vol_ratio > 3:
reason.append("Institutional Buying 🔥")
elif vol_ratio > 2:
reason.append("Volume Breakout 🔥")
if breakout:
reason.append("Resistance Breakout 🚀")
if rsi > 60:
reason.append("Strong Momentum ⚡")
if price > ema20:
reason.append("Trend Up 📈")
if price > ema20 and rsi > 50 and not breakout:
reason.append("Recovery Move 🔄")
reason_text = ", ".join(reason) if reason else "Normal"
trend_reason = []
if price > ema20:
trend_reason.append("Above EMA20")
if ema20 > ema50:
trend_reason.append("EMA20 > EMA50")
if rsi > 50:
trend_reason.append("Bullish RSI")
if relative_strength > 0:
trend_reason.append("Outperforming Nifty")
trend_reason_text = ", ".join(trend_reason)
# ------------------------
# TREND SCANNER
# ------------------------
trend_score = 0
if price > ema20:
trend_score += 25
if ema20 > ema50:
trend_score += 25
if rsi > 50:
trend_score += 25
if relative_strength > 0:
trend_score += 25
if trend_score >= 75:
trend_signal = "TREND BUY ✅"
elif trend_score >= 50:
trend_signal = "TREND WATCH 👀"
else:
trend_signal = "WEAK ❌"
return {
"symbol": stock.replace(".NS", ""),
"price": round(price, 2),
"entry": round(price, 2),
"sl": round(price - (1.5 * atr_val), 2),
"trailing_sl": round(price - (1.0 * atr_val), 2),
"target": round(price + (3.0 * atr_val), 2),
"rsi": round(rsi, 2),
"volume": int(current_vol),
"avg_volume": int(avg_vol),
"volume_ratio": round(vol_ratio, 2),
"breakout": breakout,
"score": score,
"ai_score": ai_score,
"confidence": confidence,
"grade": grade,
"strength": strength,
"institutional": institutional,
"institutional_score": institutional_score,
"institutional_label": institutional_label,
"fund_candidate": fund_candidate,
"alert": alert,
"reason": reason_text,
"signal": "BUY ✅" if ai_score >= 70 else "AVOID ❌",
"relative_strength": relative_strength,
"rs_grade": rs_grade,
"ema20": round(ema20, 2),
"ema50": round(ema50, 2),
"macd_hist": round(macd_hist_val, 2),
"atr": round(atr_val, 2),
"trend_score": trend_score,
"trend_signal": trend_signal,
"trend_reason": trend_reason_text,
}
except Exception as e:
print(f"Analyze error {stock}: {e}")
return None
# =========================================
# ✅ API - FAST SCAN
# =========================================
def get_scan(user_sector_map):
sector_output = {}
all_stocks = []
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
if data:
data.sort(
key=lambda x: (
x["ai_score"],
x["strength"]
),
reverse=True
)
sector_output[sector] = {
"top_stock": data[0],
"stocks": data
}
all_stocks.extend(data)
# ✅ Global Ranking
all_stocks.sort(
key=lambda x: (
x["ai_score"],
x["strength"]
),
reverse=True
)
# ✅ Top 5 Trades
best_5 = all_stocks[:10]
# ✅ Trade Of The Day
trade_of_day = (
all_stocks[0]
if all_stocks
else None
)
return {
"sectors": sector_output,
"best_5": best_5,
"trade_of_day": trade_of_day
}
# =========================================
# ✅ EMA RIBBON (FAST VERSION)
# =========================================
def analyze_ribbon(stock):
try:
df = yf.download(stock, period="6mo", interval="1d", progress=False)
if df is None or df.empty:
return None
df = fix_df(df)
df.dropna(inplace=True)
high = df["High"]
low = df["Low"]
close = df["Close"]
# ✅ REMOVE strict filter ❌
# if len(close) < 50:
# return None
ema_high_5 = high.ewm(span=5).mean()
ema_low_5 = low.ewm(span=5).mean()
ema_high_100 = high.ewm(span=50).mean() # faster
ema_low_100 = low.ewm(span=50).mean()
ema_close_100 = close.ewm(span=50).mean()
price = float(close.iloc[-1])
eh5 = float(ema_high_5.iloc[-1])
el5 = float(ema_low_5.iloc[-1])
eh100 = float(ema_high_100.iloc[-1])
el100 = float(ema_low_100.iloc[-1])
ec100 = float(ema_close_100.iloc[-1])
# ✅ ALWAYS RETURN SIGNAL (NO FILTERING)
if price > ec100 and eh5 > eh100:
signal = "BUY 🚀"
elif price < ec100:
signal = "SELL ❌"
else:
signal = "SIDEWAYS ⚠️"
return {
"symbol": stock.replace(".NS",""),
"price": round(price, 2),
"ema_high_5": round(eh5, 2),
"ema_low_5": round(el5, 2),
"ema_high_100": round(eh100, 2),
"ema_low_100": round(el100, 2),
"ema_close_100": round(ec100, 2),
"signal": signal
}
except Exception as e:
print("Ribbon error:", stock, e)
return None
def get_ribbon(user_sector_map):
results = []
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze_ribbon, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
return results
def get_smartmoney(user_sector_map):
results = []
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
results.sort(
key=lambda x: (
x.get("institutional_score", 0),
x.get("ai_score", 0),
x.get("strength", 0)
),
reverse=True
)
return {
"trade_of_day": results[0] if results else None,
"top_10": results[:10],
"all": results
}
def get_trend(user_sector_map):
results = []
for sector, stocks in user_sector_map.items():
data = list(ThreadPoolExecutor(5).map(analyze, stocks))
data = [x for x in data if x]
for x in data:
x["sector"] = sector
results.extend(data)
results.sort(
key=lambda x: (
x.get("trend_score", 0),
x.get("relative_strength", 0),
x.get("ai_score", 0)
),
reverse=True
)
return {
"top_20": results[:20]
}
# =========================================
# ❌ HEAVY CODE (DISABLED FOR SPEED)
# =========================================
# - Long-term strategy (disabled)
# - Full sector_map (too large)
# - Deep MACD + Supertrend
# - 6 month data calls
def get_marketintel():
try:
nifty_news = yf.Ticker("^NSEI").news or []
reliance_news = yf.Ticker("RELIANCE.NS").news or []
# Extract titles and links
all_news = []
for n in nifty_news + reliance_news:
if n.get("content", {}).get("title"):
all_news.append({
"title": n["content"]["title"],
"link": n["content"].get("clickThroughUrl", {}).get("url", "#"),
"provider": n.get("provider", {}).get("displayName", "Yahoo Finance"),
"pubDate": n.get("content", {}).get("pubDate", "")
})
# Remove duplicates
seen = set()
unique_news = []
for n in all_news:
if n["title"] not in seen:
unique_news.append(n)
seen.add(n["title"])
return {
"top_news": unique_news[:10],
"block_deals": [
{"stock": "HDFCBANK", "buyer": "FII", "value": "₹1200 Cr"},
{"stock": "RAMCOSYS", "buyer": "Institution", "value": "₹50 Cr"}
],
"bulk_deals": [
{"stock": "CGPOWER", "buyer": "Investor", "shares": "10 Lakh"},
{"stock": "SUZLON", "buyer": "DII", "shares": "50 Lakh"}
],
"fii_activity": [
{"stock": "HAL", "activity": "Net Buying"},
{"stock": "RELIANCE", "activity": "Net Buying"}
],
"dii_activity": [
{"stock": "BEL", "activity": "Net Buying"},
{"stock": "INFY", "activity": "Net Selling"}
],
"promoter_activity": [
{"stock": "HFCL", "activity": "Stake Increased"},
{"stock": "ADANIENT", "activity": "Stake Increased"}
]
}
except Exception as e:
print("MarketIntel error:", e)
return {
"top_news": [{"title": "Results season updates", "link": "#", "provider": "Local", "pubDate": ""}],
"block_deals": [], "bulk_deals": [], "fii_activity": [], "dii_activity": [], "promoter_activity": []
}