import hashlib import json import os import time import urllib.error import urllib.parse import urllib.request GROWW_API_BASE = os.getenv("GROWW_API_BASE", "https://api.groww.in").rstrip("/") GROWW_API_VERSION = os.getenv("GROWW_API_VERSION", "1.0") class GrowwApiError(Exception): def __init__(self, status_code: int, error_type: str, message: str): super().__init__(f"Groww API error {status_code}: {error_type} - {message}") self.status_code = status_code self.error_type = error_type self.message = message class GrowwTokenError(GrowwApiError): pass class GrowwPermissionError(GrowwApiError): pass def _json_headers(extra: dict | None = None) -> dict: headers = { "Accept": "application/json", "Content-Type": "application/json", "X-API-VERSION": GROWW_API_VERSION, } if extra: headers.update(extra) return headers def _request( method: str, url: str, *, data: dict | None = None, headers: dict | None = None, ): payload = None if data is not None: payload = json.dumps(data).encode("utf-8") req = urllib.request.Request( url, data=payload, headers=headers or {}, method=method, ) try: with urllib.request.urlopen(req, timeout=20) as resp: body = resp.read().decode("utf-8") except urllib.error.HTTPError as err: error_body = err.read().decode("utf-8") if err.fp else "" try: parsed = json.loads(error_body) if error_body else {} except json.JSONDecodeError: parsed = {} error = parsed.get("error") if isinstance(parsed.get("error"), dict) else {} error_type = ( error.get("code") or parsed.get("error_code") or parsed.get("error_type") or parsed.get("status") or "unknown_error" ) message = ( error.get("message") or parsed.get("message") or parsed.get("detail") or error_body or err.reason ) normalized_error = str(error_type).strip().lower() exc_cls = GrowwApiError if err.code in {401, 403} or "token" in normalized_error or "auth" in normalized_error: exc_cls = GrowwTokenError elif "permission" in normalized_error: exc_cls = GrowwPermissionError raise exc_cls(err.code, str(error_type), str(message)) from err if not body: return {} return json.loads(body) def _first_data(payload: dict | None): if not isinstance(payload, dict): return payload data = payload.get("data") if data is not None: return data envelope = payload.get("payload") if envelope is not None: return envelope return payload def _auth_headers(access_token: str) -> dict: return _json_headers({"Authorization": f"Bearer {access_token}"}) def _api_key_headers(api_key: str) -> dict: return _json_headers({"Authorization": f"Bearer {api_key}"}) def _single_query_url(path: str, **params) -> str: query = urllib.parse.urlencode( [(key, value) for key, value in params.items() if value is not None and value != ""] ) if query: return f"{GROWW_API_BASE}{path}?{query}" return f"{GROWW_API_BASE}{path}" def generate_access_token(api_key: str, api_secret: str) -> dict: timestamp = str(int(time.time())) checksum = hashlib.sha256(f"{api_secret}{timestamp}".encode("utf-8")).hexdigest() response = _request( "POST", f"{GROWW_API_BASE}/v1/token/api/access", data={ "key_type": "approval", "checksum": checksum, "timestamp": timestamp, }, headers=_api_key_headers(api_key), ) return _first_data(response) or {} def fetch_profile(access_token: str) -> dict: response = _request( "GET", f"{GROWW_API_BASE}/v1/user/detail", headers=_auth_headers(access_token), ) return _first_data(response) or {} def fetch_holdings(access_token: str) -> list: response = _request( "GET", f"{GROWW_API_BASE}/v1/holdings/user", headers=_auth_headers(access_token), ) data = _first_data(response) if isinstance(data, list): return data if isinstance(data, dict): for key in ("holdings", "items", "records"): if isinstance(data.get(key), list): return data[key] return [] def fetch_positions(access_token: str) -> list: response = _request( "GET", f"{GROWW_API_BASE}/v1/positions/user", headers=_auth_headers(access_token), ) data = _first_data(response) if isinstance(data, list): return data if isinstance(data, dict): for key in ("positions", "items", "records"): if isinstance(data.get(key), list): return data[key] return [] def fetch_funds(access_token: str) -> dict: response = _request( "GET", f"{GROWW_API_BASE}/v1/margins/detail/user", headers=_auth_headers(access_token), ) return _first_data(response) or {} def fetch_ltp(access_token: str, *, exchange: str, segment: str, trading_symbol: str) -> dict: url = _single_query_url( "/v1/live-data/ltp", exchange=exchange, segment=segment, trading_symbol=trading_symbol, ) response = _request("GET", url, headers=_auth_headers(access_token)) return _first_data(response) or {} def place_order( access_token: str, *, trading_symbol: str, exchange: str, segment: str, transaction_type: str, order_type: str, quantity: int, product: str, validity: str = "DAY", price: float | None = None, trigger_price: float | None = None, order_reference_id: str | None = None, ) -> dict: payload = { "trading_symbol": trading_symbol, "quantity": int(quantity), "validity": validity, "exchange": exchange, "segment": segment, "product": product, "order_type": order_type, "transaction_type": transaction_type, } if price is not None: payload["price"] = float(price) if trigger_price is not None: payload["trigger_price"] = float(trigger_price) if order_reference_id: payload["order_reference_id"] = order_reference_id response = _request( "POST", f"{GROWW_API_BASE}/v1/order/create", data=payload, headers=_auth_headers(access_token), ) return _first_data(response) or {} def fetch_order_status(access_token: str, groww_order_id: str, *, segment: str = "CASH") -> dict: url = _single_query_url( f"/v1/order/status/{urllib.parse.quote(str(groww_order_id).strip())}", segment=segment, ) response = _request("GET", url, headers=_auth_headers(access_token)) return _first_data(response) or {} def fetch_order_detail(access_token: str, groww_order_id: str, *, segment: str = "CASH") -> dict: url = _single_query_url( f"/v1/order/detail/{urllib.parse.quote(str(groww_order_id).strip())}", segment=segment, ) response = _request("GET", url, headers=_auth_headers(access_token)) return _first_data(response) or {} def fetch_orders(access_token: str, *, segment: str = "CASH") -> list: url = _single_query_url("/v1/order/list", segment=segment) response = _request("GET", url, headers=_auth_headers(access_token)) data = _first_data(response) if isinstance(data, list): return data if isinstance(data, dict): for key in ("orders", "items", "records"): if isinstance(data.get(key), list): return data[key] return [] def _first_float(*values, default: float = 0.0) -> float: for value in values: try: if value is None or value == "": continue return float(value) except (TypeError, ValueError): continue return float(default) def _first_text(*values, default: str = "") -> str: for value in values: if value is None: continue text = str(value).strip() if text: return text return default def holding_quantity(item: dict | None) -> float: entry = item or {} return _first_float( entry.get("quantity"), entry.get("available_quantity"), entry.get("net_quantity"), default=0.0, ) def holding_average_price(item: dict | None) -> float: entry = item or {} return _first_float(entry.get("average_price"), entry.get("avg_price"), default=0.0) def holding_last_price(item: dict | None) -> float: entry = item or {} return _first_float( entry.get("last_price"), entry.get("ltp"), entry.get("close_price"), entry.get("average_price"), default=0.0, ) def normalize_holding(item: dict | None) -> dict: entry = dict(item or {}) quantity = holding_quantity(entry) average_price = holding_average_price(entry) last_price = holding_last_price(entry) tradingsymbol = _first_text( entry.get("trading_symbol"), entry.get("tradingsymbol"), entry.get("symbol"), entry.get("instrument_name"), default="", ) exchange = _first_text( entry.get("exchange"), entry.get("exchange_segment"), entry.get("exchange_name"), default="NSE", ).upper() segment = _first_text(entry.get("segment"), entry.get("product_segment"), default="CASH").upper() symbol = tradingsymbol if tradingsymbol and not tradingsymbol.endswith((".NS", ".BO")): if exchange == "NSE": symbol = f"{tradingsymbol}.NS" elif exchange == "BSE": symbol = f"{tradingsymbol}.BO" entry["settled_quantity"] = quantity entry["t1_quantity"] = 0.0 entry["effective_quantity"] = quantity entry["quantity"] = quantity entry["average_price"] = average_price entry["last_price"] = last_price entry["close_price"] = last_price entry["exchange"] = exchange entry["segment"] = segment entry["tradingsymbol"] = tradingsymbol entry["symbol"] = symbol entry["display_pnl"] = quantity * (last_price - average_price) entry["holding_value"] = quantity * last_price return entry def normalize_position(item: dict | None) -> dict: entry = normalize_holding(item) signed_quantity = _first_float( entry.get("net_quantity"), entry.get("effective_quantity"), entry.get("quantity"), default=0.0, ) quantity = abs(signed_quantity) entry["settled_quantity"] = quantity entry["effective_quantity"] = quantity entry["quantity"] = quantity entry["net_quantity"] = signed_quantity entry["display_pnl"] = _first_float( entry.get("display_pnl"), default=signed_quantity * (_first_float(entry.get("last_price"), default=0.0) - _first_float(entry.get("average_price"), default=0.0)), ) entry["holding_value"] = max(signed_quantity, 0.0) * _first_float(entry.get("last_price"), default=0.0) entry["product"] = _first_text(entry.get("product"), entry.get("product_type"), default="CNC").upper() return entry