44from typing import Any , Dict , List , Optional
55
66import httpx
7+ from tenacity import (
8+ Retrying ,
9+ retry_if_exception ,
10+ stop_after_attempt ,
11+ wait_random_exponential ,
12+ )
713
814from .models import (
915 Branch ,
3339 StockPrice ,
3440 StockPriceLastResponse ,
3541 StockPriceLastValue ,
42+ StockPricesArrayResp ,
43+ StockPricesArrayRespList ,
3644 StockPricesResponse ,
3745 StockSplit ,
3846 StockSplitResponse ,
@@ -51,14 +59,34 @@ class BorsdataClient:
5159
5260 BASE_URL = "https://apiservice.borsdata.se/v1"
5361
54- def __init__ (self , api_key : str ):
62+ def __init__ (self , api_key : str , retry : bool = True , max_retries : int = 5 ):
5563 """Initialize the Borsdata API client.
5664
5765 Args:
5866 api_key: Your Borsdata API authentication key
67+ retry: Whether to enable retry on rate limit errors
68+ max_retries: Maximum number of retries for rate limit errors
5969 """
6070 self .api_key = api_key
6171 self ._client = httpx .Client (timeout = 30.0 )
72+ self .retry = retry
73+ self .retryer = None
74+
75+ def is_retryable_exception (exception ):
76+ """Check if the exception is retryable."""
77+ if isinstance (exception , httpx .HTTPStatusError ):
78+ # Retry for 429 Too Many Requests
79+ if exception .response .status_code == 429 :
80+ print ("Rate limit exceeded. Retrying..." )
81+ return True
82+ return False
83+
84+ self .retryer = Retrying (
85+ wait = wait_random_exponential (multiplier = 1 , min = 1 , max = 20 ),
86+ stop = stop_after_attempt (max_retries ),
87+ reraise = True ,
88+ retry = retry_if_exception (is_retryable_exception ),
89+ )
6290
6391 def _get (
6492 self , endpoint : str , params : Optional [Dict [str , Any ]] = None
@@ -79,10 +107,23 @@ def _get(
79107 params = {}
80108 params ["authKey" ] = self .api_key
81109
82- try :
83- response = self ._client .get (f" { self . BASE_URL } { endpoint } " , params = params )
84- response .raise_for_status () # This will raise an HTTPError for 4XX/5XX responses
110+ def _get_wrapper ( api_endpoint = endpoint , params = params ) :
111+ response = self ._client .get (api_endpoint , params = params )
112+ response .raise_for_status () # This raises HTTPStatusError for 4xx/5xx codes
85113 return response .json ()
114+
115+ try :
116+ if self .retry :
117+ return self .retryer (
118+ _get_wrapper ,
119+ api_endpoint = f"{ self .BASE_URL } { endpoint } " ,
120+ params = params ,
121+ )
122+ else :
123+ return _get_wrapper (
124+ api_endpoint = f"{ self .BASE_URL } { endpoint } " , params = params
125+ )
126+
86127 except httpx .HTTPStatusError as e :
87128 error_msg = str (e )
88129 status_code = e .response .status_code
@@ -177,6 +218,38 @@ def get_stock_prices(
177218 # Convert each stock price dict to a StockPrice object
178219 return [StockPrice (** price ) for price in response_model .stockPricesList ]
179220
221+ def get_stock_prices_batch (
222+ self ,
223+ instrument_ids : list [int ],
224+ from_date : Optional [datetime ] = None ,
225+ to_date : Optional [datetime ] = None ,
226+ ) -> List [StockPricesArrayRespList ]:
227+ """Get stock prices for multiple instruments, max 50 instruments per call.
228+
229+ Args:
230+ instrument_ids: List of instrument IDs
231+ from_date: Start date for price data
232+ to_date: End date for price data
233+
234+ Returns:
235+ List of StockPrice objects
236+ """
237+ assert isinstance (instrument_ids , list ), "instrument_ids must be a list"
238+ assert len (instrument_ids ) <= 50 , "Max 50 instrument IDs allowed per request"
239+
240+ params = {"instList" : "," .join (map (str , instrument_ids ))}
241+
242+ if from_date :
243+ params ["from" ] = from_date .strftime ("%Y-%m-%d" )
244+ if to_date :
245+ params ["to" ] = to_date .strftime ("%Y-%m-%d" )
246+
247+ response = self ._get ("/instruments/stockprices" , params )
248+ response_model = StockPricesArrayResp (** response )
249+
250+ # Return list of instrument stock prices
251+ return response_model .stockPricesArrayList
252+
180253 def get_reports (
181254 self ,
182255 instrument_id : int ,
@@ -238,6 +311,58 @@ def get_kpi_updated(self) -> datetime:
238311 response = self ._get ("/instruments/kpis/updated" )
239312 return KpiCalcUpdatedResponse (** response ).kpis_calc_updated
240313
314+ def get_kpi_history (
315+ self ,
316+ instrument_id : str ,
317+ kpi_id : int ,
318+ report_type : str ,
319+ price_type : str = "mean" ,
320+ max_count : Optional [int ] = None ,
321+ ) -> List [KpiAllResponse ]:
322+ """Get KPI history for an instrument.
323+
324+ Args:
325+ instrument_id: ID of the instrument
326+ kpi_id: ID of the KPI
327+ report_type: Type of report ('year', 'r12', 'quarter')
328+ price_type: Type of price calculation
329+ max_count: Maximum number of results to return
330+
331+ Returns:
332+ List of KPI responses
333+ """
334+ params = {}
335+ if max_count :
336+ params ["maxCount" ] = str (max_count )
337+
338+ response = self ._get (
339+ f"/instruments/{ instrument_id } /kpis/{ kpi_id } /{ report_type } /{ price_type } /history" ,
340+ params ,
341+ )
342+ return KpiAllResponse (** response )
343+
344+ def get_kpi_summary (
345+ self , instrument_id : str , report_type : str , max_count : Optional [int ] = None
346+ ) -> List [KpiSummaryGroup ]:
347+ """Get all KPI history for an instrument.
348+
349+ Args:
350+ instrument_id: ID of the instrument
351+ report_type: Type of report ('year', 'r12', 'quarter')
352+ max_count: Maximum number of results to return
353+
354+ Returns:
355+ List of all KPI responses
356+ """
357+ params = {}
358+ if max_count :
359+ params ["maxCount" ] = str (max_count )
360+
361+ response = self ._get (
362+ f"/instruments/{ instrument_id } /kpis/{ report_type } /summary" , params
363+ )
364+ return KpisSummaryResponse (** response ).kpis or []
365+
241366 def get_insider_holdings (
242367 self , instrument_ids : List [int ]
243368 ) -> List [InsiderListResponse ]:
@@ -320,58 +445,6 @@ def get_dividend_calendar(
320445 response = self ._get ("/instruments/dividend/calendar" , params )
321446 return DividendCalendarListResponse (** response ).list or []
322447
323- def get_kpi_history (
324- self ,
325- instrument_id : str ,
326- kpi_id : int ,
327- report_type : str ,
328- price_type : str = "mean" ,
329- max_count : Optional [int ] = None ,
330- ) -> List [KpiAllResponse ]:
331- """Get KPI history for an instrument.
332-
333- Args:
334- instrument_id: ID of the instrument
335- kpi_id: ID of the KPI
336- report_type: Type of report ('year', 'r12', 'quarter')
337- price_type: Type of price calculation
338- max_count: Maximum number of results to return
339-
340- Returns:
341- List of KPI responses
342- """
343- params = {}
344- if max_count :
345- params ["maxCount" ] = str (max_count )
346-
347- response = self ._get (
348- f"/instruments/{ instrument_id } /kpis/{ kpi_id } /{ report_type } /{ price_type } /history" ,
349- params ,
350- )
351- return KpiAllResponse (** response )
352-
353- def get_kpi_summary (
354- self , instrument_id : str , report_type : str , max_count : Optional [int ] = None
355- ) -> List [KpiSummaryGroup ]:
356- """Get all KPI history for an instrument.
357-
358- Args:
359- instrument_id: ID of the instrument
360- report_type: Type of report ('year', 'r12', 'quarter')
361- max_count: Maximum number of results to return
362-
363- Returns:
364- List of all KPI responses
365- """
366- params = {}
367- if max_count :
368- params ["maxCount" ] = str (max_count )
369-
370- response = self ._get (
371- f"/instruments/{ instrument_id } /kpis/{ report_type } /summary" , params
372- )
373- return KpisSummaryResponse (** response ).kpis or []
374-
375448 def get_last_stock_prices (self ) -> List [StockPriceLastValue ]:
376449 """Get last stock prices for all instruments.
377450
0 commit comments