Spaces:
Running
Running
| """ | |
| 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 | |