thinkingEverytime/QuantOracle
1
1#!/usr/bin/env python32"""Tiny Groww Trade API helpers (token + historical candles).3 4This is intentionally small and only supports what QuantOracle needs:5 - exchange: NSE6 - segment: CASH7 - candles: daily (interval_in_minutes=1440)8 9Docs reference: https://groww.in/trade-api/docs10"""11 12from __future__ import annotations13 14import hashlib15import time16from dataclasses import dataclass17from typing import Any18 19import requests20 21_TOKEN_URL = "https://api.groww.in/v1/token/api/access"22_CANDLE_RANGE_URL = "https://api.groww.in/v1/historical/candle/range"23 24 25@dataclass(frozen=True)26class GrowwAuth:27 api_key: str28 api_secret: str29 30 31def get_access_token(auth: GrowwAuth, *, key_type: str = "approval", timeout: int = 30) -> str:32 ts = str(int(time.time()))33 checksum = hashlib.sha256((auth.api_secret + ts).encode("utf-8")).hexdigest()34 r = requests.post(35 _TOKEN_URL,36 headers={"Authorization": f"Bearer {auth.api_key}"},37 json={"key_type": key_type, "checksum": checksum, "timestamp": ts},38 timeout=timeout,39 )40 if r.status_code != 200:41 raise RuntimeError(f"Groww token HTTP {r.status_code}: {(r.text or '')[:200]}")42 data = r.json() or {}43 token = data.get("token")44 if not token:45 raise RuntimeError(f"Groww token response missing 'token': {str(data)[:200]}")46 return str(token)47 48 49def get_candles_range(50 access_token: str,51 *,52 trading_symbol: str,53 start_time: str,54 end_time: str,55 interval_in_minutes: int = 1440,56 exchange: str = "NSE",57 segment: str = "CASH",58 timeout: int = 30,59) -> list[list[Any]]:60 r = requests.get(61 _CANDLE_RANGE_URL,62 headers={63 "Authorization": f"Bearer {access_token}",64 "Accept": "application/json",65 "X-API-VERSION": "1.0",66 },67 params={68 "exchange": exchange,69 "segment": segment,70 "trading_symbol": trading_symbol,71 "start_time": start_time,72 "end_time": end_time,73 "interval_in_minutes": int(interval_in_minutes),74 },75 timeout=timeout,76 )77 if r.status_code != 200:78 raise RuntimeError(f"Groww candles HTTP {r.status_code}: {(r.text or '')[:200]}")79 data = r.json() or {}80 # The API returns a top-level payload in some responses; be tolerant.81 payload = data.get("payload") if isinstance(data, dict) else None82 if isinstance(payload, dict) and "candles" in payload:83 candles = payload.get("candles") or []84 else:85 candles = data.get("candles") or []86 if not isinstance(candles, list):87 raise RuntimeError(f"Groww candles unexpected shape: {str(data)[:200]}")88 return candles89 90 