File size: 11,997 Bytes
8da5378 bfe0820 8da5378 a02845e eead5a7 a02845e eead5a7 a02845e 8da5378 49382d9 8da5378 49382d9 8da5378 bfe0820 8da5378 49382d9 bfe0820 49382d9 8da5378 a02845e 8da5378 bfe0820 8da5378 a02845e 8da5378 a02845e 8da5378 eead5a7 8da5378 a02845e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | # data/fetch_prices.py
try:
import yfinance as yf
except ImportError: # pragma: no cover - exercised when yfinance is not installed
yf = None
import pandas as pd
from datetime import datetime
import os
import requests
import logging
from decimal import Decimal
from data.commodity_pricing import (
GRAMS_PER_KILOGRAM,
GRAMS_PER_10_GRAMS,
rounded_inr,
usd_per_troy_ounce_to_indian_landed_price,
)
logger = logging.getLogger(__name__)
# Curated NSE stocks + ETFs for TradeMind's ML MODEL. Kept intentionally
# small β this is the universe the RF/XGBoost model was trained and
# labeled on. Don't add to this list without retraining.
STOCKS = [
# NSE Stocks
"RELIANCE.NS", "TCS.NS", "INFY.NS",
"HDFCBANK.NS", "WIPRO.NS", "ICICIBANK.NS",
"BAJFINANCE.NS", "SBIN.NS", "ITC.NS", "ADANIENT.NS",
# Gold & Silver ETFs (NSE listed, in rupees)
"GOLDBEES.NS", # Nippon India Gold ETF
"SILVERBEES.NS", # Nippon India Silver ETF
# Popular Index ETFs
"NIFTYBEES.NS", # Nifty50 ETF
]
# Broader universe for the NL SCREENER only. Unlike the ML model, the
# screener has no training/label dependency β it's pure rule evaluation
# against live indicators, so nothing stops it covering far more symbols
# than STOCKS. Includes STOCKS plus other liquid NSE large-caps.
SCREENER_UNIVERSE = STOCKS + [
"LT.NS", "HINDUNILVR.NS", "AXISBANK.NS", "KOTAKBANK.NS", "MARUTI.NS",
"SUNPHARMA.NS", "TITAN.NS", "ASIANPAINT.NS", "BHARTIARTL.NS",
"TMPV.NS", # was TATAMOTORS.NS β renamed after Oct 2025 CV/PV demerger
"TATASTEEL.NS", "ULTRACEMCO.NS", "NESTLEIND.NS",
"POWERGRID.NS", "NTPC.NS", "ONGC.NS", "COALINDIA.NS", "HCLTECH.NS",
"TECHM.NS", "DRREDDY.NS", "CIPLA.NS", "DIVISLAB.NS", "GRASIM.NS",
"JSWSTEEL.NS", "HINDALCO.NS", "BPCL.NS", "EICHERMOT.NS",
"HEROMOTOCO.NS", "BAJAJFINSV.NS", "INDUSINDBK.NS",
]
def fetch_prices(symbol: str, period: str = "1y") -> pd.DataFrame:
"""Fetch OHLCV data for a single NSE stock or ETF"""
if yf is None:
logger.warning("data.fetch_prices.unavailable β yfinance not installed")
return pd.DataFrame()
try:
df = yf.download(symbol, period=period, interval="1d", progress=False)
if df.empty:
return pd.DataFrame()
# Flatten MultiIndex columns from yfinance
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df["symbol"] = symbol
df.reset_index(inplace=True)
# Filter out data anomalies β remove rows where price moved >20% in one day
if "Close" in df.columns:
df["pct_change"] = df["Close"].pct_change().abs()
df = df[df["pct_change"] < 0.20]
df = df.drop(columns=["pct_change"])
df.reset_index(drop=True, inplace=True)
print(f"Fetched {len(df)} rows for {symbol}")
return df
except Exception as e:
print(f"Error fetching {symbol}: {e}")
return pd.DataFrame()
def fetch_prices_batch(symbols: list, period: str = "6mo") -> dict:
"""
Fetch OHLCV for multiple symbols in ONE yfinance call instead of N
sequential ones. Matters once the universe is bigger than a handful of
symbols (see SCREENER_UNIVERSE) β sequential fetch_prices() calls in a
loop would make a live screener query noticeably slow.
Returns {symbol: DataFrame}. Falls back to per-symbol fetch_prices()
for the whole batch if the batch call itself fails, and for any
individual symbol yfinance's batch response is missing/malformed for
(batch calls can silently drop a problem ticker rather than raising).
"""
if not symbols:
return {}
if yf is None:
logger.warning("data.fetch_prices.batch_unavailable β yfinance not installed")
return {s: fetch_prices(s, period) for s in symbols}
try:
raw = yf.download(tickers=symbols, period=period, interval="1d",
group_by="ticker", progress=False, threads=True)
except Exception as e:
print(f"Batch fetch failed ({e}) β falling back to per-symbol fetch")
return {s: fetch_prices(s, period) for s in symbols}
results = {}
for symbol in symbols:
try:
# yfinance returns a flat (non-MultiIndex) frame when only one
# symbol was requested, and a MultiIndex (symbol, field) frame
# for multiple β handle both shapes.
df = raw.copy() if len(symbols) == 1 else raw[symbol].copy()
df = df.dropna(how="all")
if df.empty:
results[symbol] = fetch_prices(symbol, period)
continue
df["symbol"] = symbol
df.reset_index(inplace=True)
if "Close" in df.columns:
df["pct_change"] = df["Close"].pct_change().abs()
df = df[df["pct_change"] < 0.20]
df = df.drop(columns=["pct_change"])
df.reset_index(drop=True, inplace=True)
print(f"Fetched {len(df)} rows for {symbol} (batch)")
results[symbol] = df
except Exception as e:
print(f"Batch fetch missing {symbol} ({e}) β fetching individually")
results[symbol] = fetch_prices(symbol, period)
return results
def fetch_all_stocks(period: str = "1y") -> pd.DataFrame:
"""Fetch data for all stocks and combine"""
all_data = []
for symbol in STOCKS:
df = fetch_prices(symbol, period)
if not df.empty:
all_data.append(df)
return pd.concat(all_data, ignore_index=True)
def save_to_csv(df: pd.DataFrame, filename: str = "data/prices.csv"):
"""Save fetched data locally"""
df.to_csv(filename, index=False)
print(f"Saved to {filename}")
def fetch_gold_price_inr() -> dict:
"""
Fetch real gold price in INR per 10 grams
Converts international USD/troy oz β INR/10g
Applies the current 6% import duty and 3% GST exactly once.
"""
if yf is None:
logger.warning("data.fetch_prices.gold_unavailable β yfinance not installed")
return {"error": "Could not fetch gold data"}
try:
# Fetch gold futures price in USD
gold_df = yf.download("GC=F", period="3mo",
interval="1d", progress=False)
# Fetch USD to INR exchange rate
usd_inr_df = yf.download("INR=X", period="3mo",
interval="1d", progress=False)
if gold_df.empty or usd_inr_df.empty:
return {"error": "Could not fetch gold data"}
# Flatten MultiIndex
if isinstance(gold_df.columns, pd.MultiIndex):
gold_df.columns = gold_df.columns.get_level_values(0)
if isinstance(usd_inr_df.columns, pd.MultiIndex):
usd_inr_df.columns = usd_inr_df.columns.get_level_values(0)
# Latest values
gold_usd_per_oz = float(gold_df["Close"].iloc[-1])
usd_to_inr = float(usd_inr_df["Close"].iloc[-1])
conversion = usd_per_troy_ounce_to_indian_landed_price(
gold_usd_per_oz, usd_to_inr, GRAMS_PER_10_GRAMS
)
gold_inr_per_10g = rounded_inr(conversion.inr_per_display_unit)
logger.debug(
"gold_price.conversion",
extra={
"source": "Yahoo Finance GC=F (COMEX futures)",
"raw_usd_per_troy_ounce": str(conversion.raw_usd_per_troy_ounce),
"usd_to_inr": str(conversion.usd_to_inr),
"inr_per_gram_before_taxes": str(conversion.inr_per_gram_before_taxes),
"inr_per_10g": gold_inr_per_10g,
},
)
# Build historical data for chart
gold_df.reset_index(inplace=True)
usd_inr_df.reset_index(inplace=True)
# Merge gold + fx on date
merged = pd.merge(
gold_df[["Date", "Close"]].rename(columns={"Close": "gold_usd"}),
usd_inr_df[["Date", "Close"]].rename(columns={"Close": "usd_inr"}),
on="Date", how="inner"
)
# Use the same audited conversion for every historical point.
merged["price_10g_inr"] = merged.apply(
lambda row: rounded_inr(usd_per_troy_ounce_to_indian_landed_price(
row["gold_usd"], row["usd_inr"], GRAMS_PER_10_GRAMS
).inr_per_display_unit),
axis=1,
)
return {
"current_price_10g": round(gold_inr_per_10g, 2),
"current_price_oz": round(gold_usd_per_oz, 2),
"usd_to_inr": round(usd_to_inr, 2),
"history": [
{
"Date": str(row["Date"])[:10],
"Close": row["price_10g_inr"]
}
for _, row in merged.iterrows()
]
}
except Exception as e:
print(f"Gold price error: {e}")
return {"error": str(e)}
def fetch_live_commodity_prices_inr() -> dict:
"""Fetch recent COMEX futures bars and convert them to indicative INR prices.
Yahoo Finance's intraday feed may be delayed and is not an MCX quote.
Prices are shown before Indian import duty, GST, and MCX basis.
"""
if yf is None:
raise RuntimeError("Could not fetch commodity data: yfinance is unavailable")
try:
fx_df = yf.download("INR=X", period="5d", interval="1d", progress=False)
if fx_df.empty:
raise RuntimeError("Could not fetch USD/INR exchange-rate data")
if isinstance(fx_df.columns, pd.MultiIndex):
fx_df.columns = fx_df.columns.get_level_values(0)
usd_to_inr = float(fx_df["Close"].dropna().iloc[-1])
tax_free = {"import_duty_rate": Decimal("0"), "gst_rate": Decimal("0")}
quotes = {}
for name, ticker, grams, unit in (
("gold", "GC=F", GRAMS_PER_10_GRAMS, "10g"),
("silver", "SI=F", GRAMS_PER_KILOGRAM, "kg"),
):
frame = yf.download(ticker, period="5d", interval="5m", progress=False)
if frame.empty:
raise RuntimeError(f"Could not fetch {name} futures data")
if isinstance(frame.columns, pd.MultiIndex):
frame.columns = frame.columns.get_level_values(0)
closes = frame["Close"].dropna().tail(60)
if closes.empty:
raise RuntimeError(f"No recent {name} futures prices are available")
history = []
for timestamp, usd_price in closes.items():
conversion = usd_per_troy_ounce_to_indian_landed_price(
float(usd_price), usd_to_inr, grams, **tax_free
)
history.append({
"timestamp": timestamp.isoformat(),
"price_inr": rounded_inr(conversion.inr_per_display_unit),
})
quotes[name] = {
"price_inr": history[-1]["price_inr"],
"unit": unit,
"usd_per_troy_ounce": round(float(closes.iloc[-1]), 2),
"as_of": history[-1]["timestamp"],
"history": history,
"source": f"Yahoo Finance COMEX futures ({ticker})",
}
return {
"gold": quotes["gold"],
"silver": quotes["silver"],
"usd_to_inr": round(usd_to_inr, 4),
"fetched_at": datetime.now().astimezone().isoformat(),
"price_note": "Indicative COMEX futures equivalent; excludes local taxes and MCX basis.",
}
except Exception as exc:
raise RuntimeError(f"Could not fetch live commodity prices: {exc}") from exc
if __name__ == "__main__":
print("Fetching NSE stock data...")
df = fetch_all_stocks()
save_to_csv(df)
print(f"\nTotal rows fetched: {len(df)}")
print(df.head())
print("\nFetching gold price...")
gold = fetch_gold_price_inr()
print(f"24K Gold per 10g: βΉ{gold.get('current_price_10g', 'N/A')}")
|