import hashlib import json import os import urllib.error import urllib.parse import urllib.request KITE_API_BASE = os.getenv("KITE_API_BASE", "https://api.kite.trade") KITE_LOGIN_URL = os.getenv("KITE_LOGIN_URL", "https://kite.trade/connect/login") KITE_VERSION = "3" class KiteApiError(Exception): def __init__(self, status_code: int, error_type: str, message: str): super().__init__(f"Kite API error {status_code}: {error_type} - {message}") self.status_code = status_code self.error_type = error_type self.message = message class KiteTokenError(KiteApiError): pass class KitePermissionError(KiteApiError): pass def build_login_url(api_key: str, redirect_url: str | None = None) -> str: params = {"api_key": api_key, "v": KITE_VERSION} redirect_url = (redirect_url or os.getenv("ZERODHA_REDIRECT_URL") or "").strip() if redirect_url: params["redirect_url"] = redirect_url query = urllib.parse.urlencode(params) return f"{KITE_LOGIN_URL}?{query}" def _request(method: str, url: str, data: dict | None = None, headers: dict | None = None): payload = None if data is not None: payload = urllib.parse.urlencode(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: payload = json.loads(error_body) if error_body else {} except json.JSONDecodeError: payload = {} error_type = payload.get("error_type") or payload.get("status") or "unknown_error" message = payload.get("message") or error_body or err.reason if error_type == "TokenException": exc_cls = KiteTokenError elif error_type == "PermissionException": exc_cls = KitePermissionError else: exc_cls = KiteApiError raise exc_cls(err.code, error_type, message) from err return json.loads(body) def _auth_headers(api_key: str, access_token: str) -> dict: return { "X-Kite-Version": KITE_VERSION, "Authorization": f"token {api_key}:{access_token}", } 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 holding_settled_quantity(item: dict | None) -> float: entry = item or {} return _first_float(entry.get("quantity"), entry.get("qty"), default=0.0) def holding_t1_quantity(item: dict | None) -> float: entry = item or {} return _first_float(entry.get("t1_quantity"), default=0.0) def holding_effective_quantity(item: dict | None) -> float: entry = item or {} return holding_settled_quantity(entry) + holding_t1_quantity(entry) 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("close_price"), entry.get("average_price"), entry.get("avg_price"), default=0.0, ) def holding_display_pnl(item: dict | None) -> float: entry = item or {} effective_qty = holding_effective_quantity(entry) last_price = holding_last_price(entry) avg_price = holding_average_price(entry) return effective_qty * (last_price - avg_price) def normalize_holding(item: dict | None) -> dict: entry = dict(item or {}) settled_qty = holding_settled_quantity(entry) t1_qty = holding_t1_quantity(entry) effective_qty = settled_qty + t1_qty last_price = holding_last_price(entry) avg_price = holding_average_price(entry) entry["settled_quantity"] = settled_qty entry["t1_quantity"] = t1_qty entry["effective_quantity"] = effective_qty entry["display_pnl"] = effective_qty * (last_price - avg_price) entry["holding_value"] = effective_qty * last_price return entry def exchange_request_token(api_key: str, api_secret: str, request_token: str) -> dict: checksum = hashlib.sha256( f"{api_key}{request_token}{api_secret}".encode("utf-8") ).hexdigest() url = f"{KITE_API_BASE}/session/token" response = _request( "POST", url, data={ "api_key": api_key, "request_token": request_token, "checksum": checksum, }, ) return response.get("data", {}) def fetch_holdings(api_key: str, access_token: str) -> list: url = f"{KITE_API_BASE}/portfolio/holdings" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", []) def fetch_funds(api_key: str, access_token: str) -> dict: url = f"{KITE_API_BASE}/user/margins" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", {}) def fetch_ltp_quotes(api_key: str, access_token: str, instruments: list[str]) -> dict: symbols = [str(item).strip() for item in instruments if str(item).strip()] if not symbols: return {} query = urllib.parse.urlencode([("i", symbol) for symbol in symbols]) url = f"{KITE_API_BASE}/quote/ltp?{query}" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", {}) def fetch_ohlc_quotes(api_key: str, access_token: str, instruments: list[str]) -> dict: symbols = [str(item).strip() for item in instruments if str(item).strip()] if not symbols: return {} query = urllib.parse.urlencode([("i", symbol) for symbol in symbols]) url = f"{KITE_API_BASE}/quote/ohlc?{query}" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", {}) def fetch_historical_candles( api_key: str, access_token: str, instrument_token: int | str, interval: str, *, from_dt, to_dt, continuous: bool = False, oi: bool = False, ) -> list: params = { "from": from_dt.strftime("%Y-%m-%d %H:%M:%S"), "to": to_dt.strftime("%Y-%m-%d %H:%M:%S"), "continuous": 1 if continuous else 0, "oi": 1 if oi else 0, } query = urllib.parse.urlencode(params) url = f"{KITE_API_BASE}/instruments/historical/{instrument_token}/{interval}?{query}" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", {}).get("candles", []) def place_order( api_key: str, access_token: str, *, tradingsymbol: str, exchange: str, transaction_type: str, order_type: str, quantity: int, product: str, price: float | None = None, validity: str = "DAY", variety: str = "regular", market_protection: int | None = None, tag: str | None = None, ) -> dict: payload = { "tradingsymbol": tradingsymbol, "exchange": exchange, "transaction_type": transaction_type, "order_type": order_type, "quantity": int(quantity), "product": product, "validity": validity, } if price is not None: payload["price"] = price if market_protection is not None: payload["market_protection"] = market_protection if tag: payload["tag"] = tag url = f"{KITE_API_BASE}/orders/{variety}" response = _request( "POST", url, data=payload, headers=_auth_headers(api_key, access_token), ) return response.get("data", {}) def fetch_orders(api_key: str, access_token: str) -> list: url = f"{KITE_API_BASE}/orders" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", []) def fetch_order_history(api_key: str, access_token: str, order_id: str) -> list: url = f"{KITE_API_BASE}/orders/{order_id}" response = _request("GET", url, headers=_auth_headers(api_key, access_token)) return response.get("data", []) def cancel_order( api_key: str, access_token: str, *, order_id: str, variety: str = "regular", ) -> dict: url = f"{KITE_API_BASE}/orders/{variety}/{order_id}" response = _request("DELETE", url, headers=_auth_headers(api_key, access_token)) return response.get("data", {})