""" Groww charting API client. Fetches 1-minute OHLCV candle data for NSE CASH segment tickers. Handles: - Retry with exponential backoff (3 attempts) - None / missing values in candle arrays - Cumulative volume -> per-candle volume conversion """ import time import logging import traceback from datetime import datetime import numpy as np import pandas as pd import requests logger = logging.getLogger("live_trader") GROWW_HEADERS = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ), "Accept": "application/json", } def fetch_groww_candles(ticker, days=5, max_retries=3): """ Fetch 1-min OHLCV from Groww for the last *days* calendar days. Returns a DataFrame [open, high, low, close, volume] with DateTimeIndex, or None on complete failure. """ end_ts = int(time.time() * 1000) start_ts = end_ts - (days * 24 * 3600 * 1000) url = ( f"https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE" f"/segment/CASH/{ticker}" f"?endTimeInMillis={end_ts}" f"&intervalInMinutes=1" f"&startTimeInMillis={start_ts}" ) for attempt in range(1, max_retries + 1): try: resp = requests.get(url, headers=GROWW_HEADERS, timeout=15) resp.raise_for_status() data = resp.json() if "candles" not in data or not data["candles"]: logger.warning(f"[{ticker}] No candles in API response (attempt {attempt})") if attempt < max_retries: time.sleep(2 * attempt) continue return None rows = [] for c in data["candles"]: # Skip candles with None / missing OHLCV values if c[1] is None or c[2] is None or c[3] is None or c[4] is None or c[5] is None: continue try: dt = datetime.fromtimestamp(c[0]) rows.append({ "date": dt, "open": float(c[1]), "high": float(c[2]), "low": float(c[3]), "close": float(c[4]), "cum_vol": float(c[5]), }) except (TypeError, ValueError): continue if not rows: logger.warning(f"[{ticker}] All candles had None values (attempt {attempt})") if attempt < max_retries: time.sleep(2 * attempt) continue return None df = pd.DataFrame(rows) df.set_index("date", inplace=True) df.sort_index(inplace=True) # Groww volume is cumulative per day -> difference it df["date_only"] = df.index.date df["volume"] = df.groupby("date_only")["cum_vol"].diff().fillna(df["cum_vol"]) df["volume"] = np.where(df["volume"] < 0, df["cum_vol"], df["volume"]) df.drop(columns=["cum_vol", "date_only"], inplace=True) logger.info(f"[{ticker}] Fetched {len(df)} candles " f"({df.index.min()} -> {df.index.max()})") return df except requests.exceptions.RequestException as e: logger.error(f"[{ticker}] API error attempt {attempt}/{max_retries}: {e}") if attempt < max_retries: time.sleep(3 * attempt) except Exception as e: logger.error(f"[{ticker}] Unexpected error attempt {attempt}/{max_retries}: {e}") logger.debug(traceback.format_exc()) if attempt < max_retries: time.sleep(3 * attempt) return None