
    ./jD                        S r SSKJr  SSKrSSKrSSKJrJr  \R                  " \	5      r
/ SQrSSSS	S
SSSSSSS.rSr1 SkrSS jrSS jrSS jr " S S5      rg)uN   LSEG Data Library client — fetches bond data via Platform session (App Key).    )annotationsN)AnyOptional)zTR.YieldToMaturityzTR.YieldToWorstz
TR.ZSpreadzTR.OASpreadzTR.BidPricezTR.AskPricezTR.AssetSwapSpreadzTR.TRBCIndustryzTR.TRBCIndustryGroupzTR.HeadquartersCountryzTR.ModifiedDurationytmytwz_spreadoas	bid_price	ask_priceasset_swap_spread
sub_sectorsectorcountryduration)zYield to MaturityzYield to WorstzZ Spreadz	OA Spreadz	Bid Pricez	Ask PricezAsset Swap SpreadzTRBC Industry NamezTRBC Industry Group NamezCountry of HeadquarterszModified DurationzRIC,ISIN,IssuerCommonName,IssuerName,MaturityDate,CouponRate,CouponFrequency,CouponType,MoodysRating,Currency,Price,IssueDate,AssetType,RCSTRBC2012Genealogy>    nannonen/a<na>no permissionc                t    U c  g [        U 5      R                  5       nUR                  5       [        ;   a  S $ U$ N)strstriplower_NULL_STRINGSvalss     3/root/lseg-credit/backend/app/providers/lseg_api.py_cleanr!   3   s2    
{CA779-4414    c                    [        U 5      nUc  g  [        [        U5      R                  SS5      5      $ ! [        [
        4 a     g f = f)N,r   )r!   floatr   replace
ValueError	TypeError)r   cleaneds     r    	_to_floatr*   :   sK    SkGS\))#r233	" s   #5 AAc                    U c  g [        U 5      R                  5       nUR                  5       [        ;   a  g SU;   a  UR	                  S5      S   $ U$ )N r   )r   r   r   r   splitr   s     r    _to_date_strr.   D   sH    
{CAwwyM!!Qh1773<?-A-r"   c                      \ rS rSrSrSS jrS rS rSSS jjrSr	Sr
S	rSS
 jrSS jrSS jrSSS jjrSSS jjrSS jrSS jrSrg)LsegApiClientN   ae  
Fetches bond data from LSEG via the lseg-data Python library (Platform session).
Requires LSEG_APP_KEY + LSEG_USERNAME + LSEG_PASSWORD in environment / .env file.

Strategy: discovery.search supplies static reference data (coupon, maturity, rating,
currency); ld.get_data supplies live market data (YTM, Z-spread, bid/ask). Both are
merged per instrument.
c                6    Xl         X l        X0l        S U l        g r   )_app_key	_username	_password_session)selfapp_keyusernamepasswords       r    __init__LsegApiClient.__init__X   s    !!r"   c                   U R                   b  g  SS KJn  SSKJn  UR                  U R                  UR                  U R                  U R                  S9SS9R                  5       nUR                  5         UR                  R                  U5        X0l         [        R                  S5        g ! [          a  n[        R#                  SU5        e S nAff = f)Nr   )platform)r9   r:   T)r8   grantsignon_controlzLSEG: Platform session openedu#   LSEG: failed to open session — %s)r6   	lseg.datadatalseg.data.sessionr>   
Definitionr3   GrantPasswordr4   r5   get_sessionopensessionset_defaultloggerinfo	Exceptionerror)r7   ldr>   rH   excs        r    _openLsegApiClient._open^   s    ==$	"2)),,!^^!^^ -   $ *  km  LLNJJ""7+#MKK78 	LL>D	s   BB. .
C8CCc                    U R                   b8   U R                   R                  5         [        R                  S5        S U l         g g ! [         a     Nf = f)NzLSEG: session closed)r6   closerJ   rK   rL   )r7   s    r    rS   LsegApiClient.closev   sP    ==$##%23 !DM %  s   /A 
AAc                h   U R                  5          SSKJn  UR                  R	                  USU[
        S9nUb  UR                  (       a  [        R                  SU5        / $ / nUR                  5        GHh  u  pg[        UR                  S5      5      nU(       d  M)  UR                  U[        UR                  S5      5      [        UR                  S5      5      [        UR                  S	5      5      [        UR                  S
5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      S.5        GMk     [        R                  S[!        U5      U5        U$ ! ["         a"  n	[        R%                  SX5        / s Sn	A	$ Sn	A	ff = f)z
Search LSEG for bonds by issuer. Returns a list of reference-data dicts,
each keyed by 'ric', plus static fields (coupon, maturity, rating, etc.).
r   N,AssetType eq 'CORP' and CouponType ne 'FXDI'queryfiltertopselectz$LSEG: no bonds found for issuer '%s'RICISINIssuerCommonName
IssuerName
CouponRateMaturityDate	IssueDateCouponFrequency
CouponTypeMoodysRatingCurrencyPriceRCSTRBC2012Genealogyricisinnameissuercouponmaturity_date
issue_datecoupon_freqcoupon_typerating_moodyscurrencyprice	genealogyzLSEG: found %d bonds for '%s'u#   LSEG: search failed for '%s' — %s)rP   rA   rB   	discoverysearch_SEARCH_SELECTemptyrJ   warningiterrowsr!   getappendr*   r.   rK   lenrL   rM   )
r7   issuer_namemax_resultsrN   resultsrecords_rowrj   rO   s
             r    search_bonds_by_issuer$LsegApiClient.search_bonds_by_issuer   s   
 	

%	"ll))!E%	 * G '--E{S	G!**,SWWU^,%(%+CGGFO%<%+CGG4F,G%H%+CGGL,A%B%.sww|/D%E%1#''.2I%J%1#''+2F%G%.sww7H/I%J%+CGGL,A%B%+CGGN,C%D%+CGGJ,?%@%.swww/?%@%+CGG4J,K%L  	 -( KK7W{SN 	LL>QI	s%   AH $F H 
H1H,&H1,H1      g      ?c           
        U R                   n[        U R                  5       H  n UR                  U[        5      s  $    g! [
         a  nX@R                  S-
  :  aG  [        R                  SUS-   U R                  X%U5        [        R                  " U5        US-  n SnAM  [        R                  SU R                  X%5         SnAM  SnAff = f)zMCall ld.get_data with exponential-backoff retries. Returns DataFrame or None.   uD   LSEG: get_data attempt %d/%d failed for %s — %s; retrying in %.0fs   u6   LSEG: get_data gave up after %d attempts for %s — %sN)
_RETRY_BASE_DELAYrange_MAX_RETRIESget_data_MARKET_FIELDSrL   rJ   r{   timesleep)r7   rN   instrumentsdelayattemptrO   s         r    _get_data_with_retry"LsegApiClient._get_data_with_retry   s    &&T../G{{;?? 0    ..22NN^!T%6%6% JJu%QJENNP)); s   A
CAC$!CCc           	     2   SSK Jn  SSKn0 n[        S[	        U5      U R
                  5       GHe  nXXPR
                  -    nU R                  X&5      nUc}  [	        U5      S:  an  [        R                  SXU[	        U5      -   5        / nU H+  n	U R                  X)/5      n
U
c  M  UR                  U
5        M-     U(       a  UR                  USS9OSnUb  UR                  (       a  M  UR                  5        H  u  p[        UR                  S5      =(       d    UR                  S5      5      n	U	(       d  M@  0 n[        R!                  5        H8  u  pUR                  U5      nUS	;   a  [        U5      X'   M+  [#        U5      X'   M:     XU	'   M     GMh     U$ )
z
Fetch live market data for a list of RICs in batches.
Each batch is retried up to _MAX_RETRIES times; batches that still fail
are retried one RIC at a time to maximise recovery.
Returns a dict keyed by RIC.
r   Nr   z6LSEG: falling back to per-RIC requests for batch %d-%dT)ignore_index
Instrument
instrument>   r   r   r   )rA   rB   pandasr   r   _BATCH_SIZEr   rJ   rK   r~   concatrz   r|   r!   r}   _MARKET_FIELD_MAPitemsr*   )r7   ricsrN   pdmarketibatchdfframesrj   singler   r   entrycol_namenorm_keyraws                    r    _fetch_market_data LsegApiClient._fetch_market_data   sf    	"$q#d)T%5%56AA 0 001E**25Bzc%j1nTVW]`af]gYgh C!66r5AF)f- ! >DRYYvDY9zRXX++-SWW\2Kcggl6KL *;*A*A*C&H''(+C#FF*0+*3C. +D $s (# 7< r"   c           
        U(       d  / $ U R                  5         U R                  U5      n/ nU H  nUR                  U0 5      nUR                  0 SU_SU_SS_SS_SS_SS_SS_S	S_S
S_SS_SS_SUR                  S5      _SUR                  S5      _SUR                  S5      _SUR                  S5      _SUR                  S5      _SUR                  S5      _UR                  S5      UR                  S5      UR                  S5      UR                  S5      S.E5        M     [        R                  S[        U5      5        U$ )u   
Fetch bond data for a list of RICs (used when ISINs/RICs are given directly,
without a prior search step — no reference data available in this path).
rj   rk   rl   Nrm   rn   ro   rp   rq   rr   rs   rt   ru   r
   r   r   r   r	   r   r   r   r   r   )r   r   r   r   zLSEG API: fetched %d bonds)rP   r   r}   r~   rJ   rK   r   )r7   r   r   r   rj   ms         r    fetch_bondsLsegApiClient.fetch_bonds   s   
 I

((5C

3#ANN   	
          {!3 u u z!2  u!" $QUU+>%?#$ "#y!1!"x!"|!4!"z!2+  4 	0#g,?r"   c                
   U R                  5          SSKJn  UR                  R	                  USU[
        S9nUb  UR                  (       a  / $ / nUR                  5        GHO  u  pg[        UR                  S5      5      nU(       d  M)  UR                  U[        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S	5      5      [        UR                  S
5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      S.5        GMR     [        R                  SU[        U5      5        U$ ! [          a"  n	[        R#                  SX5        / s Sn	A	$ Sn	A	ff = f)u   
Search for bonds using a generic text query (e.g. sector name).
Returns reference-data dicts only — no market data enrichment.
r   NrV   rW   r\   r]   r^   r_   r`   ra   rb   rc   rd   re   rf   rg   )rj   rk   rl   rm   rn   ro   rp   rq   rr   rs   rt   ru   u   LSEG: query '%s' → %d bondsu)   LSEG: query search failed for '%s' — %s)rP   rA   rB   rw   rx   ry   rz   r|   r!   r}   r~   r*   r.   rJ   rK   r   rL   rM   )
r7   rX   rZ   rN   r   r   r   r   rj   rO   s
             r    search_bonds_by_query#LsegApiClient.search_bonds_by_query  s   
 	

!	"ll))E%	 * G '--	G!**,SWWU^,%(%+CGGFO%<%+CGG4F,G%H%+CGGL,A%B%.sww|/D%E%1#''.2I%J%1#''+2F%G%.sww7H/I%J%+CGGL,A%B%+CGGN,C%D%+CGGJ,?%@%.swww/?%@  	 -$ KK7GMN 	LLDeQI	s$   ;G FG 
H G=7H=Hc                   U R                  5          SSKJn  UR                  SS5      nUR                  R                  UR                  R                  R                  SU S3U[        S9nUb  UR                  (       a  / $ / nUR                  5        GHh  u  px[        UR                  S5      5      n	U	(       d  M)  UR                  U	[        UR                  S	5      5      [        UR                  S
5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      [        UR                  S5      5      S.5        GMk     [         R#                  SU[%        U5      5        U$ ! [&         a"  n
[         R)                  SX5        / s Sn
A
$ Sn
A
ff = f)u  
Search for peer bonds in the same TRBC sector using a genealogy code prefix filter.
More reliable than free-text search — uses exact TRBC classification codes so the
full peer universe is captured regardless of issuer name phrasing.

sector_prefix: first N segments of RCSTRBC2012Genealogy joined by backslashes,
  e.g. 'B:180\B:181\B:182' for Industry Group (sector) level,
       'B:180\B:181\B:182\B:183' for Industry (sub-sector) level.
r   N\z\\zhAssetType eq 'CORP' and CouponType ne 'FXDI' and Currency eq 'USD' and startswith(RCSTRBC2012Genealogy,'z'))viewrY   rZ   r[   r\   r]   r^   r_   r`   ra   rb   rc   rd   re   rf   rg   rh   ri   u(   LSEG: genealogy prefix '%s' → %d bondsu*   LSEG: sector search failed for '%s' — %s)rP   rA   rB   r&   rw   rx   ViewsGOV_CORP_INSTRUMENTSry   rz   r|   r!   r}   r~   r*   r.   rJ   rK   r   rL   rM   )r7   sector_prefixrZ   rN   escapedr   r   r   r   rj   rO   s              r    search_peers_by_sector$LsegApiClient.search_peers_by_sectorA  s    	

(	"#++D&9Gll))\\''<<==DIRI % * 	G '--	G!**,SWWU^,%(%+CGGFO%<%+CGG4F,G%H%+CGGL,A%B%.sww|/D%E%1#''.2I%J%1#''+2F%G%.sww7H/I%J%+CGGL,A%B%+CGGN,C%D%+CGGJ,?%@%.swww/?%@%+CGG4J,K%L  	 -& KKBMSVW^S_`N 	LLE}ZI	s%   A/H# F H# #
I-I
I
Ic                D    U R                  5         U R                  U5      $ )z+Public wrapper for batch market data fetch.)rP   r   )r7   r   s     r    fetch_market_dataLsegApiClient.fetch_market_datav  s    

&&t,,r"   c                   U R                  5         0 nU H+  nU R                  U5       H  nUS   nXR;  d  M  XBU'   M     M-     U(       d  [        R                  S5        / $ [	        UR                  5       5      n[        R                  S[        U5      [        U5      5        U R                  U5      n/ nUR                  5        GH  u  pYUR                  U0 5      n
UR                  0 SU	R                  S5      =(       d    U_SU	R                  S5      _SU	R                  S5      _SU	R                  S5      _SU	R                  S5      _S	U	R                  S	5      _S
U	R                  S
5      _SU	R                  S5      _SU	R                  S5      _SU	R                  S5      _SU	R                  S5      =(       d    U
R                  S5      _SU_SU
R                  S5      _SU
R                  S5      _SU
R                  S5      _SU
R                  S5      _SU
R                  S5      _U
R                  S5      =(       d    U	R                  S5      U
R                  S5      U
R                  S5      U
R                  S5      U	R                  S5      S.E5        GM     [        R                  S[        U5      5        U$ )z
Search for bonds by issuer, capture reference data from search, then
enrich with live market data from get_data. Merges both into one record.
rj   z5LSEG: no bonds found for any of the requested issuersz9LSEG: fetching market data for %d bonds across %d issuersrk   rl   rm   rn   ro   rp   rq   rr   rs   rt   ru   r
   r   r   r   r	   r   r   r   r   r   rv   )r   r   r   r   rv   z-LSEG API: merged %d bonds (ref + market data))rP   r   rJ   r{   listkeysrK   r   r   r   r}   r~   )r7   issuer_names
ref_by_ricrl   bondrj   r   r   r   refr   s              r    fetch_bonds_for_issuers%LsegApiClient.fetch_bonds_for_issuers{  s   
 	

 ')
 D33D95k(&*sO : ! NNRSIJOO%&OIs<0	2 ((. "((*HC

3#ANN SWWV_%;SWWV_ SWWX%6 SWWX%6	
  SWW_%= SWW\%: SWW]%; SWW]%;  SWW_%= SWWZ%8 SWWW%5%K{9K S QUU5\ QUU5\ QUU:%6  QUU5\!" $QUU+>%?#$ &'UU9%5%K9K%&UU8_%&UU<%8%&UU:%6%(WW[%9-  +6 	CS\Rr"   )r3   r5   r6   r4   N)r8   r   r9   r   r:   r   )2   )r   r   r   intreturn
list[dict])rN   r   r   	list[str]r   zOptional[Any])r   r   r   zdict[str, dict])r   r   r   r   )i,  )rX   r   rZ   r   r   r   )r   r   rZ   r   r   r   )r   r   r   r   )__name__
__module____qualname____firstlineno____doc__r;   rP   rS   r   r   r   r   r   r   r   r   r   r   r   __static_attributes__ r"   r    r0   r0   N   sU    0!+Z KL*)V&P'R3j-
8r"   r0   )r   zOptional[str])r   zOptional[float])r   
__future__r   loggingr   typingr   r   	getLoggerr   rJ   r   r   ry   r   r!   r*   r.   r0   r   r"   r    <module>r      s    T "    			8	$  !& % * % + + 3 , ( ) *    D5.e er"   