1717logger = logging .getLogger ("sahmk" )
1818
1919_RETRIABLE_STATUS_CODES = frozenset ({500 , 502 , 503 , 504 })
20+ _MARKET_INDEX_ALIASES = {"NOMUC" : "NOMU" }
21+ _VALID_MARKET_INDEXES = frozenset ({"TASI" , "NOMU" })
2022
2123
2224class SahmkError (Exception ):
@@ -61,6 +63,18 @@ def __init__(
6163 self .rate_reset = rate_reset
6264
6365
66+ class SahmkInvalidIndexError (SahmkError ):
67+ """Raised when an invalid market index is supplied or returned by the API."""
68+
69+ def __init__ (self , message , response = None ):
70+ super ().__init__ (
71+ message ,
72+ status_code = 400 ,
73+ error_code = "INVALID_INDEX" ,
74+ response = response ,
75+ )
76+
77+
6478class SahmkClient :
6579 """
6680 SAHMK Developer API client.
@@ -217,6 +231,11 @@ def _build_api_error(response):
217231 except (ValueError , KeyError ):
218232 code = "UNKNOWN"
219233 message = response .text
234+ if response .status_code == 400 and code == "INVALID_INDEX" :
235+ return SahmkInvalidIndexError (
236+ f"Invalid market index: { message } " ,
237+ response = response ,
238+ )
220239 return SahmkError (
221240 f"API error { response .status_code } : { message } " ,
222241 status_code = response .status_code ,
@@ -234,6 +253,29 @@ def _rate_limit_wait(self, response, attempt):
234253 pass
235254 return self .backoff_factor * (2 ** attempt )
236255
256+ @staticmethod
257+ def _normalize_market_index (index ):
258+ """Normalize/validate market index query parameter."""
259+ if index is None :
260+ return None
261+ normalized = str (index ).strip ().upper ()
262+ normalized = _MARKET_INDEX_ALIASES .get (normalized , normalized )
263+ if normalized not in _VALID_MARKET_INDEXES :
264+ raise SahmkInvalidIndexError (
265+ "Index must be one of: TASI, NOMU (NOMUC is accepted as NOMU)."
266+ )
267+ return normalized
268+
269+ def _market_params (self , limit = None , index = None ):
270+ """Build validated query params for market endpoints."""
271+ params = {}
272+ if limit is not None :
273+ params ["limit" ] = limit
274+ normalized_index = self ._normalize_market_index (index )
275+ if normalized_index is not None :
276+ params ["index" ] = normalized_index
277+ return params or None
278+
237279 # -------------------------------------------------------------------------
238280 # Quotes
239281 # -------------------------------------------------------------------------
@@ -304,94 +346,122 @@ def historical(self, symbol, from_date=None, to_date=None, interval=None):
304346 # Market
305347 # -------------------------------------------------------------------------
306348
307- def market_summary (self ):
349+ def market_summary (self , index = None ):
308350 """
309351 Get market overview (TASI index, change, volume, market_mood).
310352
353+ Args:
354+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
355+ is accepted and normalized to "NOMU".
356+
311357 Returns:
312358 MarketSummary object
313359 """
314360 from .models import MarketSummary
315- data = self ._request ("GET" , "/market/summary/" )
361+ data = self ._request (
362+ "GET" ,
363+ "/market/summary/" ,
364+ params = self ._market_params (index = index ),
365+ )
316366 return MarketSummary .from_dict (data )
317367
318- def gainers (self , limit = None ):
368+ def gainers (self , limit = None , index = None ):
319369 """
320370 Get top gaining stocks.
321371
322372 Args:
323373 limit: Number of results (default: 10, max: 50)
374+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
375+ is accepted and normalized to "NOMU".
324376
325377 Returns:
326378 MarketMoversResponse with .stocks list
327379 """
328380 from .models import MarketMoversResponse
329- params = {}
330- if limit is not None :
331- params ["limit" ] = limit
332- data = self ._request ("GET" , "/market/gainers/" , params = params or None )
381+ data = self ._request (
382+ "GET" ,
383+ "/market/gainers/" ,
384+ params = self ._market_params (limit = limit , index = index ),
385+ )
333386 return MarketMoversResponse .from_dict (data , list_key = "gainers" )
334387
335- def losers (self , limit = None ):
388+ def losers (self , limit = None , index = None ):
336389 """
337390 Get top losing stocks.
338391
339392 Args:
340393 limit: Number of results (default: 10, max: 50)
394+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
395+ is accepted and normalized to "NOMU".
341396
342397 Returns:
343398 MarketMoversResponse with .stocks list
344399 """
345400 from .models import MarketMoversResponse
346- params = {}
347- if limit is not None :
348- params ["limit" ] = limit
349- data = self ._request ("GET" , "/market/losers/" , params = params or None )
401+ data = self ._request (
402+ "GET" ,
403+ "/market/losers/" ,
404+ params = self ._market_params (limit = limit , index = index ),
405+ )
350406 return MarketMoversResponse .from_dict (data , list_key = "losers" )
351407
352- def volume_leaders (self , limit = None ):
408+ def volume_leaders (self , limit = None , index = None ):
353409 """
354410 Get stocks with highest trading volume.
355411
356412 Args:
357413 limit: Number of results (default: 10, max: 50)
414+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
415+ is accepted and normalized to "NOMU".
358416
359417 Returns:
360418 MarketMoversResponse with .stocks list
361419 """
362420 from .models import MarketMoversResponse
363- params = {}
364- if limit is not None :
365- params ["limit" ] = limit
366- data = self ._request ("GET" , "/market/volume/" , params = params or None )
421+ data = self ._request (
422+ "GET" ,
423+ "/market/volume/" ,
424+ params = self ._market_params (limit = limit , index = index ),
425+ )
367426 return MarketMoversResponse .from_dict (data , list_key = "stocks" )
368427
369- def value_leaders (self , limit = None ):
428+ def value_leaders (self , limit = None , index = None ):
370429 """
371430 Get stocks with highest trading value (SAR).
372431
373432 Args:
374433 limit: Number of results (default: 10, max: 50)
434+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
435+ is accepted and normalized to "NOMU".
375436
376437 Returns:
377438 MarketMoversResponse with .stocks list
378439 """
379440 from .models import MarketMoversResponse
380- params = {}
381- if limit is not None :
382- params ["limit" ] = limit
383- data = self ._request ("GET" , "/market/value/" , params = params or None )
441+ data = self ._request (
442+ "GET" ,
443+ "/market/value/" ,
444+ params = self ._market_params (limit = limit , index = index ),
445+ )
384446 return MarketMoversResponse .from_dict (data , list_key = "stocks" )
385447
386- def sectors (self ):
448+ def sectors (self , index = None ):
387449 """
388450 Get sector performance.
389451
452+ Args:
453+ index: Optional market index ("TASI" or "NOMU"). "NOMUC" alias
454+ is accepted and normalized to "NOMU".
455+
390456 Returns:
391457 SectorsResponse with .sectors list
392458 """
393459 from .models import SectorsResponse
394- data = self ._request ("GET" , "/market/sectors/" )
460+ data = self ._request (
461+ "GET" ,
462+ "/market/sectors/" ,
463+ params = self ._market_params (index = index ),
464+ )
395465 return SectorsResponse .from_dict (data )
396466
397467 # -------------------------------------------------------------------------
0 commit comments