
    aj                       d dl mZ d dlmZ d dlZd dlmZmZmZm	Z	m
Z
mZ d dlmZ d dlmZmZ d dlmZmZmZmZ d dlmZ d d	lmZ d
 Zd4d5dZd6dZd7dZd7dZd7dZd7dZ d7dZ!d7dZ"d7dZ#d7dZ$d7dZ%d7dZ&d7dZ'd7dZ( G d  d!          Z)d7d"Z*d7d#Z+d7d$Z,d7d%Z-d7d&Z.d7d'Z/d7d(Z0d7d)Z1d7d*Z2d7d+Z3d7d,Z4d7d-Z5d7d.Z6d7d/Z7d7d0Z8d7d1Z9d7d2Z:d7d3Z;dS )8    )annotations)DecimalN)add_equity_initial_snapshotadd_equity_transaction add_manual_position_from_catalogcreate_manual_catalog_entryget_equity_management_optionssearch_instrument_candidates)calculate_positions)CatalogSearchResultinstrument_provider_statuses)FmpEquityPriceProviderMockEquityPriceProviderrefresh_market_pricesstore_market_price)connect_memory)apply_migrationsc                 @    t                      } t          |            | S N)r   r   )conns     tests/unit/test_equity_manage.py
setup_connr      s     DTK    Synthetic PostFinanceplatform_namestrreturnc                :   d|                                                     dd           }d|                                                     dd           }|                     d||f           |                     d||f           |                                  |S )Nz	platform- -zaccount-zINSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES (?, ?, 'broker', 'CH', 'CHF', '2026-01-01')zINSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES (?, ?, 'Synthetic Depot', 'brokerage', 'CHF', '2026-01-01'))lowerreplaceexecutecommit)r   r   platform_id
account_ids       r   seed_accountr'      s    Gm1133;;CEEGGKEM//1199#sCCEEJLL 	\	m$   	LL 	m	[!   	KKMMMr   actionintc                `    |                      d|f                                          d         S )Nz2SELECT COUNT(*) AS c FROM audit_log WHERE action=?c)r#   fetchone)r   r(   s     r   audit_countr-   +   s+    <<LviXXaaccdghhr   Nonec                 X   t                      } t          |           }t          | |ddddddddd	d
dd          }|                     d|j        f                                          }|                     d|j        f                                          }|d         dk    sJ |d         dk    sJ |d         dk    sJ |d         dk    sJ |d         dk    sJ |d         dk    sJ |d         dk    sJ |d         d
k    sJ t          | d          dk    sJ |j        g k    sJ d S )NstockzSynthetic NestleCH0038863350NESNSIXCHFz3.5z350.00
2025-12-31Corez%synthetic incomplete history acceptedTr&   asset_classnameisintickerexchangecurrencyquantity_textcost_basis_original_textsnapshot_datecategorynoteconfirm1SELECT * FROM transactions WHERE transaction_id=?/SELECT * FROM instruments WHERE instrument_id=?transaction_typeinitial_position_snapshotquantitygross_amount_originalfx_rate_to_chf1	fx_status
not_neededr:   r8   position_category equity_initial_position_snapshot   )	r   r'   r   r#   transaction_idr,   instrument_idr-   warnings)r   r&   resulttx
instruments        r   Htest_add_stock_initial_snapshot_creates_instrument_transaction_and_auditrW   /   s   <<Dd##J(!)"4  F" 
IFLaKc	d	d	m	m	o	oBORXRfQhiirrttJ !%@@@@@j>U""""%&(22223&&&&k?l****f////m$////)*f4444t?@@AEEEE?b      r   c                 h   t                      } t          | d          }t          | fi d|ddddddd	d
dddddddddddddddddddddd d!d"d#d$d%d&d'd(}|                     d)|j        f                                          }|                     d*|j        f                                          }|d+         dk    sJ |d         d k    sJ |d!         d"k    sJ |d#         d$k    sJ |d%         d&k    sJ |d,         dk    sJ |d-         dk    sJ |d.         d/k    sJ d S )0NzSynthetic True Wealthr&   r8   ETFr9   zSynthetic World ETFr:   IE00B4L5Y983r;   SWLDr<   LSEr=   USDr>   10r?   z1000.00r@   r5   rA   r6   rB   z$synthetic ETF snapshot with known FXfx_rate_to_chf_textz0.91	fx_sourcezsynthetic fixtureter_textz0.20distribution_policyaccumulating
index_namezMSCI World Syntheticfund_domicileIE	benchmarkzSynthetic BenchmarkrC   TrE   rD   tercurrency_originalrJ   gross_amount_chfz910.0000)r   r'   r   r#   rR   r,   rQ   )r   r&   rT   rV   rU   s        r   :test_add_etf_initial_snapshot_prepares_optional_etf_fieldsrk   R   s0   <<Dd$;<<J(  : E #"	
 ^ v   d "+ #l  43 #F &%  !" +N#$ *)%& d'( (')* +F0 ORXRfQhiirrttJ	IFLaKc	d	d	m	m	o	oBe&&&&+,>>>>l#'=====o&$....k"&;;;;;!"e++++6)))) !Z//////r   c                    t                      } t          | d          }t          | |dddddddd	d
dddd          }|                     d|j        f                                          }|d         dk    sJ |d         J d|j        v sJ |                     d                                          d         dk    sJ d S )NzSynthetic Raiffeisenr0   zSynthetic US StockUS0378331005AAPLNASDAQr]   2z400.00r5   Opportunity(synthetic missing FX accepted for reviewmissingT)r&   r8   r9   r:   r;   r<   r=   r>   r?   r@   rA   rB   rL   rC   rD   rL   rj   
missing_fxz;SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx'r+   rP   )r   r'   r   r#   rQ   r,   rS   )r   r&   rT   rU   s       r   ;test_usd_stock_requires_fx_or_explicit_missing_status_alertru   z   s    <<Dd$:;;J(!!)"7  F$ 
IFLaKc	d	d	m	m	o	oBk?i'''' !)))6?****<<UVV__aabefjkkkkkkkr   c                    t                      } t          |           }t          | |ddddddddd	d
dd          }t          | ||j        ddddddd
  
         t          |           j        ||j        f         }|j        t          d          k    sJ t          j
        t          d          5  t          | ||j        ddddddd
  
         d d d            d S # 1 swxY w Y   d S )Nr0   zSynthetic SellableCH0000000001SYNr3   r4   5500r5   Unknownsynthetic initialTr7   partial_sell
2026-01-10rp   220zsynthetic partial sell	r&   rR   rF   
trade_dater>   gross_amount_original_textr=   rB   rC   3negativematch
2026-01-11r^   1000zsynthetic oversellr   r'   r   r   rR   r   	positionsrH   r   pytestraises
ValueErrorr   r&   snappositions       r   .test_sell_reduces_position_and_blocks_negativer      sz   <<Dd##J&!!&"   D" ('#(%    #4((2J@R3STH,,,,	z	4	4	4 
 
!,+#'-%	
 	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   /CC"Cc                    t                      } t          |           }t          | |ddddddddd	d
dd          }t          | ||j        ddddddd
  
         t          |           j        ||j        f         j        t          d          k    sJ t          j
        t          d          5  t          | ||j        ddddddd
  
         d d d            d S # 1 swxY w Y   d S )NrY   zSynthetic Income ETFIE0000000001INCr3   r4   7700r5   r6   r|   Tr7   dividend
2026-02-010z12.50synthetic dividendr   rB   r   manual_adjustmentz
2026-02-02rK    r   )r   r&   r   s      r   Jtest_dividend_does_not_change_quantity_and_manual_adjustment_requires_noter      sy   <<Dd##J&#!&"   D" (##*!    t$$.
D<N/OPY]deh]i]iiiii	z	0	0	0 
 
!,0#'*	
 	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   -CC Cc                    t                      } t          |           }t          | |ddddddddd	d
dd          }t          j        t
          d          5  t          | ||j        ddddddd
  
         d d d            n# 1 swxY w Y   t          | ||j        ddddddd
  
         t          |           j	        ||j        f         }|j
        t          d          k    sJ t          | d          dk    sJ d S )Nr0   zSynthetic Full SellCH0000000002FULr3   r4   4400r5   r{   r|   Tr7   zfull_sell quantityr   	full_sellr~   r   330zsynthetic wrong full sellr   r   440zsynthetic full sellr   equity_full_sellrP   )r   r'   r   r   r   r   r   rR   r   r   rH   r   r-   r   s       r   Atest_full_sell_requires_exact_current_quantity_and_zeros_positionr      s   <<Dd##J&"!&"   D" 
z)=	>	>	> 
 
!,(#',,	
 	
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 ($#("    #4((2J@R3STH,,,,t/00A555555s   BBBc                     t                      } t          | d           t          |           }|d         sJ ddlm} dd |D             v sJ d S )Nr   accountsr   )PAGESzEquity/ETF Managec                    g | ]	}|j         
S  )
PAGE_TITLE).0pages     r   
<listcomp>zLtest_equity_management_options_and_dashboard_page_import.<locals>.<listcomp>5  s    "E"E"Et4?"E"E"Er   )r   r'   r	   jarvis_finance.dashboard.pagesr   )r   optionsr   s      r   8test_equity_management_options_and_dashboard_page_importr   .  sr    <<D.///+D11G:444444"E"Eu"E"E"EEEEEEEr   c                    t                      } t          | d          }t          | ddddddd	          }t          | dd
ddddd	           t          | d          }t	          |d                   dk    sJ |d         d         d         dk    sJ t          | d          }t	          |d                   dk    sJ |d         du sJ t          | d          }t	          |d                   dk    sJ |d         du sJ |                     d                                          d         dk    sJ t          j        t          d          5  t          | ||dddddd !	  	         d d d            n# 1 swxY w Y   t          | ||ddddd d"d#
  
        }|                     d$|j        f                                          }|d%         d&k    sJ |d'         dk    sJ |d(         d)k    sJ d*|j        v sJ |                     d+                                          d         dk    sJ |                     d,                                          d         dk    sJ d S )-NzSynthetic Manual Brokerr0   zSynthetic Alpha AGr4   CH1111111111ALPr3   zsynthetic catalog sourcer8   r9   r=   r:   r;   r<   rB   zSynthetic Alpha US Listingr]   US1111111111ro   )queryresultsrP   r   mapping_statusexact_isin_matchzSynthetic Alpha   selection_requiredTz&SELECT COUNT(*) AS c FROM transactionsr+   rC   r   initial_snapshotz1.25r5   z"synthetic review must be confirmedF)catalog_entry_idr&   position_typer>   r   r=   rB   rC   z,synthetic manually reviewed initial snapshot	r   r&   r   r>   r   r=   r?   rB   rC   rD   rF   rG   rH   rI   r   cost_basis_uncertainzFSELECT COUNT(*) AS c FROM audit_log WHERE action='manual_position_add'zESELECT COUNT(*) AS c FROM alerts WHERE rule_id='cost_basis_uncertain')r   r'   r   r
   lenr#   r,   r   r   r   r   rQ   rS   )r   r&   firstisin_resultname_resultticker_resultrT   rU   s           r   4test_manual_instrument_search_and_wizard_safety_flowr   8  sB   <<Dd$=>>J'!'	 	 	E  )'	 	 	 	 /t>JJJK{9%&&!++++y!!$%56:LLLLL.t;LMMMK{9%&&!+++++,44440UCCCM}Y'((A-----.$6666<<@AAJJLLSQUVVVVV	z	3	3	3 
 
("!, #5
	
 
	
 
	
 
	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 .(!%;  F 
IFLaKc	d	d	m	m	o	oB !%@@@@@j>V####%&#----!V_4444<<`aajjllmpquvvvvv<<_``iikkloptuuuuuuus   E66E:=E:c                 Z   t                      } t          | d          }t          | ddddddd	          }t          | ||d
dddddddd          }|                     d|j        f                                          }|d         dk    sJ |d         dk    sJ |d         dk    sJ t          | ddddddd	          }t          | ||dd d!dd"d#d$d%          }|                     d|j        f                                          }|d&         d#k    sJ |d         J d'|j        v sJ d S )(NzSynthetic FX BrokeretfzSynthetic USD ETFr]   IE2222222222USDENYSEzsynthetic manual ETF sourcer   r   rp   r5   z100.00z0.90zsynthetic fx sourcezsynthetic USD ETF with FXT)r   r&   r   r>   r   r=   r?   r_   r`   rB   rC   rD   ri   rJ   rj   z90.0000r0   zSynthetic EUR StockEURDE3333333333EURSXETRAzsynthetic manual stock sourcebuyrK   
2026-01-05z50.00rs   rr   
r   r&   r   r>   r   r=   r?   rL   rB   rC   rL   rt   )r   r'   r   r   r#   rQ   r,   rS   )r   r&   
catalog_idrT   rU   missing_fx_catalogrs   
tx_missings           r   1test_manual_catalog_entry_and_foreign_fx_handlingr     s   <<Dd$9::J, *	 	 	J .#(!)"'(  F 
IFLaKc	d	d	m	m	o	oB!"e++++6)))) !Y....4",	 	 	 /+!(7  G QT[TjSlmmvvxxJk"i////()1117+++++++r   c                 X   t                      } t          | d          }t          | ddddddd	          }t          | ||d
dddddd
  
        }t          | ||ddddddd
  
         t          | ||ddddddd
  
         t          | ||ddddddd
  
         t	          |           j        ||j        f         j        t          d           k    sJ d!d"l	m
}  |d#                                          }d$|v sJ d%|v sJ |                    d&d'          d!         }d(|vsJ d)|vsJ d S )*NzSynthetic Trading Brokerr0   zSynthetic Tradabler4   CH4444444444TRDr3   zsynthetic tradable sourcer   r   ry   r5   rz   zsynthetic starting pointTr   r   rK   r   110zsynthetic buyr}   rp   r~   r   zsynthetic sellr   r   r   z4.50r   r   r   )Pathz;src/jarvis_finance/dashboard/views/page_13_equity_manage.pyzsearch_instrument_candidates(zProvider-Suche explizitzwith tabs[1]:rP   zst.write({"instrument_id"zst.write({"catalog_entry_id")r   r'   r   r   r   r   rR   rH   r   pathlibr   	read_textsplit)r   r&   r   r   r   r   wizard_sources          r   Ntest_manual_wizard_supports_buy_sell_dividend_and_no_dashboard_live_api_or_idsr     sK   <<Dd$>??J,!(	 	 	J ,D:Zd  uG  WZ  gs  ~C  ^c  jD  NR  S  S  SD$TJS]mr  CF  S_  jo  JO  Ve  os  t  t  t  t$TJS]m{  LO  \h  sx  SX  _o  y}  ~  ~  ~  ~$TJS]mw  HK  Xd  ot  OU  \p  z~        t$$.
D<N/OPY]deh]i]iiiii4MNNXXZZD*d2222$,,,,JJ2215M(====+=@@@@@@r   c                   ddl m}m}m} d  |            D             g dk    sJ t	                      }|                     dd           |                     dd           |                     d	t          |                     |d
z                                   |d
z  dz  	                    dd            |            j
        dk    sJ |d
z  dz                                    ||d |            g          \  }}|g k    sJ d|v sJ  G d d|          } ||d |d          g          \  }}|g k    sJ |d         j        dk    sJ |                    d                                          d         dk    sJ d S )Nr   )OpenFigiLookupProvider#default_instrument_lookup_providerssearch_instrumentsc                    g | ]	}|j         
S r   )r9   )r   ps     r   r   zTtest_provider_lookup_classes_skip_missing_keys_and_cache_results.<locals>.<listcomp>  s    BBBqAFBBBr   )openfigifmpfinnhub
twelvedatamassiveOPENFIGI_API_KEYFraisingJARVIS_OPENFIGI_API_KEYJARVIS_FINANCE_RUNTIME_DIRsecrets.envz"OPENFIGI_API_KEY=runtime-file-key
utf-8encodingruntime-file-keyrm   	providersopenfigi_api_key_missingc                      e Zd ZddZdS )Vtest_provider_lookup_classes_skip_missing_keys_and_cache_results.<locals>.FakeOpenFigir   r   c                    dddddddgS )Nz	Apple Incrn   USzCommon StockEquityBBG000B9XRY4)r9   r;   exchCodesecurityTypemarketSecDescompositeFIGIr   selfr   s     r   _requestz_test_provider_lookup_classes_skip_missing_keys_and_cache_results.<locals>.FakeOpenFigi._request  s2    (F^l  G  Zh  i  i  j  jr   Nr   r   __name__
__module____qualname__r	  r   r   r   FakeOpenFigir     s.        	j 	j 	j 	j 	j 	jr   r  Applezruntime-only-keyapi_keyr   zNSELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE provider='openfigi'r+   rP   )"jarvis_finance.market_data.catalogr   r   r   r   delenvsetenvr   mkdir
write_textr  unlinksourcer#   r,   )	monkeypatchtmp_pathr   r   r   r   r   rS   r  s	            r   @test_provider_lookup_classes_skip_missing_keys_and_cache_resultsr    sC    C  C  C  C  C  C  C  C  C  CBB??AABBBF}F}F}}}}}<<D)59990%@@@3S]]CCC	  """	F"../Ydk.lll!!##+/AAAAA	F"**,,,**4LbLbLdLdKefffGXb====%1111j j j j j- j j j +*4\\ZlEmEmEmDnoooGXr>>>>1:
****<<hiirrttuxy}~~~~~~~r   c                P   ddl m} ddlm t	                      }ddi G fdd          |                     |dfd           t          |d	d
           d         dk    sJ t          |d	d          }d         dk    sJ |d         d         d         dk    sJ d S )Nr   )manager   nc                  &    e Zd ZdZdd fd
ZdS )[test_search_candidates_uses_external_providers_only_on_explicit_click.<locals>.FakeProviderfake_providerNr8   c               T    dxx         dz  cc<    d |pddddddd	d
d
  
        gS )Nr   rP   r0   zSynthetic Provider AGCH5555555555SPVr3   r4   fakemediumr#  )r:   r;   r<   trading_currencyprovider
confidencer  r   )r  r   r8   r   callss      r   searchzbtest_search_candidates_uses_external_providers_only_on_explicit_click.<locals>.FakeProvider.search  su    #JJJ!OJJJ''k.DWF]dr  |A  LQ  di  tz  GO  Xg  h  h  h  i  ir   r  r  r  r9   r.  )r   r-  s   r   FakeProviderr"    sO        /3 	i 	i 	i 	i 	i 	i 	i 	i 	i 	ir   r0  r   c                                   gS r   r   )r0  s   r   <lambda>zWtest_search_candidates_uses_external_providers_only_on_explicit_click.<locals>.<lambda>  s    P\P\P^P^O_ r   r'  F)r   include_externalTrP   r   data_sourcer#  )jarvis_finance.equityr  r  r   r   setattrr
   )r  r  r   rT   r   r0  r-  s       @@@r   Etest_search_candidates_uses_external_providers_only_on_explicit_clickr7    s   ,,,,,,FFFFFF<<D!HEi i i i i i i i i i i  EG_G_G_G_``` UUKKKK:????)$edSSSF:????)Q./AAAAAAr   c                  (    e Zd ZdZddZddddZdS )CountingProvidercountingr   r.   c                    d| _         d S )Nr   )r-  )r  s    r   __init__zCountingProvider.__init__
  s    


r   Nr$  r   r   r8   
str | Nonec               ^    | xj         dz  c_         t          d |pddddddddd	dd
          gS )NrP   r   zProvider Test ETFUS9229087690VTIr   r]   r:  r)  zsynthetic provider result)	r:   r;   r<   r*  r+  provider_symbolr,  r  evidence_note)r-  r   r  r   r8   s      r   r.  zCountingProvider.search  su    

a

#D+*>@SZhqv  BH  [`  ku  GL  Ya  jt  D_  `  `  `  a  	ar   r   r.   )r   r   r8   r=  )r  r  r  r9   r<  r.  r   r   r   r9  r9    sY        D    ?C a a a a a a a ar   r9  c            
        t                      } t          | ddddddd           t          | dd	dddd
d           t          | dd          }|d         sJ |d         d         d                             d          sJ t	          d |d         D                       sJ t          d |d         D                       sJ t          | dd          }t          |d                   dk    sJ |d         du sJ t          | dd          }t          |d                   dk    sJ |d         du sJ t          d |d         D                       sJ d S )Nr   Vanguard Total Stock Market ETFr]   r?  r@  r   zsynthetic VTI catalog entryr   z1Vanguard Total Stock Market ETF Secondary ListingARCAzsynthetic VTI alternate listingr   r8   r   r   r9   zVanguard Total Stock Marketc              3  F   K   | ]}|d          dk    o|d         dk    V  dS )r;   r@  r=   r]   Nr   r   rs     r   	<genexpr>zQtest_vti_examples_are_searchable_and_ticker_requires_selection.<locals>.<genexpr>  s9      [[1q{e#>*(>[[[[[[r   c              3  .   K   | ]}|d          dk    V  dS )trust_status	EindeutigNr   rJ  s     r   rL  zQtest_vti_examples_are_searchable_and_ticker_requires_selection.<locals>.<genexpr>  s,      LLAq K/LLLLLLr   Vanguard Total Stockr   r   Tc              3  .   K   | ]}|d          dk    V  dS )rN  u   Manuell prüfenNr   rJ  s     r   rL  zQtest_vti_examples_are_searchable_and_ticker_requires_selection.<locals>.<genexpr>$  s-      TT!q $55TTTTTTr   )r   r   r
   
startswithanyallr   )r   by_isinby_name	by_tickers       r   >test_vti_examples_are_searchable_and_ticker_requires_selectionrX    s   <<D%>_jo  wE  NS  ^d  kH  I  I  I  I%>q  }B  IW  `e  pv  }^  _  _  _  _*4~SXYYYG99a (334QRRRRR[[PYHZ[[[[[[[[LL9KLLLLLLLL*47M[`aaaGwy!""a'''''(D0000,TERRRIy#$$)))))*d2222TTy?STTTTTTTTTTr   c                    t                      } t                      }ddlm} t	          | ddd          }|d         g k    sJ |j        dk    sJ  || dd|g          \  }}|g k    sJ |j        d	k    sJ |d         j        d
k    sJ t	          | ddd          }|d         sJ |d         d         d         dk    sJ d S )Nr   r   r?  r   Fr   r8   r3  r   r   rP   r@  r4  local_catalog)r   r9  r  r   r
   r-  r;   )r   r+  r   
local_onlyr   rS   cacheds          r   Ktest_online_provider_is_only_used_when_explicitly_enabled_and_caches_resultr_  '  s   <<D!!HEEEEEE-d.V[nstttJi B&&&&>Q**4S[R\]]]GXr>>>>>Q1:%%%%)$nRWjopppF))Q./AAAAAAr   c                H   dD ]}|                      |d           |                     dt          |                     t                      }d |D             h dk    sJ t	          d |D                       d         d	k    sJ t          d
 |D                       rJ d S )N)r   r   FINNHUB_API_KEYJARVIS_FINNHUB_API_KEYFMP_API_KEYJARVIS_FMP_API_KEYFr   r   c                    h | ]
}|d          S Providerr   r   rows     r   	<setcomp>zStest_provider_statuses_report_missing_keys_without_secret_values.<locals>.<setcomp>>  s    000C
O000r   >   Lokale SucheManuelle AnlageFMPFinnhubOpenFIGIc              3  2   K   | ]}|d          dk    |V  dS rg  ro  Nr   rh  s     r   rL  zStest_provider_statuses_report_missing_keys_without_secret_values.<locals>.<genexpr>?  0      II3z?j+H+H+H+H+H+HIIr   StatuszAPI-Key fehltc              3  ~   K   | ]8}d t          |                                          v pdt          |          v V  9dS )secretr   N)r   r!   rh  s     r   rL  zStest_provider_statuses_report_missing_keys_without_secret_values.<locals>.<genexpr>@  sI      ffVY8s3xx~~///Q3ES3Qffffffr   )r  r  r   r   nextrS  )r  r  keystatusess       r   @test_provider_statuses_report_missing_keys_without_secret_valuesry  9  s     Q / /3....3S]]CCC+--H00x0004u4u4uuuuuIIxIIIII(SWfffffff]effffffffffr   c                    t                      } t          | d          }t          | ddddddd	          }t          | ||d
ddddddd          }|                     d|j        f                                          }|d         dk    sJ d S )NzSynthetic Category Brokerr   zSynthetic Core ETFr4   r&  COREr3   zsynthetic core entryr   r   rK   r5   100zsynthetic category testr6   T)
r   r&   r   r>   r   r=   r?   rB   rA   rC   z?SELECT position_category FROM instruments WHERE instrument_id=?rN   )r   r'   r   r   r#   rR   r,   )r   r&   r   rT   ri  s        r   7test_category_is_saved_for_manual_position_from_catalogr}  C  s    <<Dd$?@@J,TuK_jo  wE  NT  _d  kA  B  B  BJ-dZ\f  wI  Y\  iu  @E  `e  lE  PV  `d  e  e  eF
,,X[a[oZq
r
r
{
{
}
}C"#v------r   c                    ddl m} m}  G d d|          } G d d|           } |d          } |d          }|                    d	d
          }|                    dd
          }|d         j        d	k    sJ |d         j        dk    sJ |d         j        dk    sJ |d         j        dk    sJ |d         j        d	k    sJ |d         j        dk    sJ |d         j        dk    sJ d S )Nr   )FmpSearchProviderr   c                      e Zd ZddZdS )Vtest_openfigi_isin_and_fmp_name_search_candidates_are_structured.<locals>.FakeOpenFigir   r   c                ,    |dk    sJ dddddddd	gigS )
Nr?  datarF  r@  r   r]   rY   BBG000PSKYX7r9   r;   r  r=   r  r  r   r  s     r   r	  z_test_openfigi_isin_and_fmp_name_search_candidates_are_structured.<locals>.FakeOpenFigi._requestP  sT    N****'HTYgky~  QV  iw  x  x  y  z  {  {r   Nr
  r  r   r   r   r  r  O  s.        	{ 	{ 	{ 	{ 	{ 	{r   r  c                      e Zd Zd	dZd
dZdS )Qtest_openfigi_isin_and_fmp_name_search_candidates_are_structured.<locals>.FakeFmpr   r   r   list[dict[str, object]]c                "    |dk    sJ dddddgS )NrP  r@  rF  AMEXr]   symbolr9   exchangeShortNamer=   r   r  s     r   _request_manyz_test_openfigi_isin_and_fmp_name_search_candidates_are_structured.<locals>.FakeFmp._request_manyU  s8    22222$.Oflz  A  A  B  Br   r  c                    ddddddS )Nr?  r]   Tr   p=
Co@)r:   r=   isEtfcountrypricer   )r  r  s     r   _profilezZtest_openfigi_isin_and_fmp_name_search_candidates_are_structured.<locals>.FakeFmp._profileX  s    *Y]hnooor   N)r   r   r   r  )r  r   )r  r  r  r  r  r   r   r   FakeFmpr  T  sF        	B 	B 	B 	B	p 	p 	p 	p 	p 	pr   r  ztest-keyr  r?  r   rH  rP  r   r  highr   r)  )r  r  r   r.  r:   r+  rA  r,  )r  r   r  r  r   r   	figi_rowsfmp_rowss           r   @test_openfigi_isin_and_fmp_name_search_candidates_are_structuredr  L  s   \\\\\\\\{ { { { {- { { {
p p p p p# p p p |J///H
'*
%
%
%Cn%HHIzz 6EzJJHQ<....Q< J....Q<'>9999Q<"f,,,,A;~----A;5((((A;!X------r   c                 L   ddl m}   G d d          } G d d          } G d d          }t                      } | |d	d
 |             |            g          \  }}|g k    sJ t          |          dk    sJ |d         j        dk    sJ |d         j        dk    sJ t                      } | |d	d
 |             |            g          \  }}|g k    sJ t          |          dk    sJ |d         j        dk    sJ t          |d	d
d          }|d         du sJ d S )Nr   rZ  c                      e Zd ZdZdddZdS )\test_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeOpenFigir   Nr$  c               <    t          d |pddddddddd	d
          gS )Nr   rF  r?  r@  r   r]   r   r  r  r:   r;   r<   r*  r+  rA  r,  r  r  rC  s      r   r.  zctest_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeOpenFigi.searchm  sc    'k.BUDelz  DI  TX  kp  {E  We  rx  AK  L  L  L  M  Mr   r/  r   r   r   r  r  k  s<        /3 	M 	M 	M 	M 	M 	M 	Mr   r  c                      e Zd ZdZdddZdS )[test_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeFmpSamer   Nr$  c               <    t          d |pdddddddddd	          gS )
Nr   rF  r?  r@  r   r]   r   r)  r  r  rC  s      r   r.  zbtest_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeFmpSame.searchr  sc    'k.BUDelz  DI  TX  kp  {@  RW  dl  uz  {  {  {  |  |r   r/  r   r   r   FakeFmpSamer  p  s<        /3 	| 	| 	| 	| 	| 	| 	|r   r  c                      e Zd ZdZdddZdS )_test_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeFmpConflictr   Nr$  c               <    t          d |pdddddddddd	          gS )
Nr   rF  r?  VTIXr   r]   r   r)  r  r  rC  s      r   r.  zftest_provider_merge_raises_confidence_and_conflict_needs_manual_review.<locals>.FakeFmpConflict.searchw  sc    'k.BUDelz  DJ  UY  lq  |A  SY  fn  w|  }  }  }  ~  ~r   r/  r   r   r   FakeFmpConflictr  u  s<        /3 	~ 	~ 	~ 	~ 	~ 	~ 	~r   r  r?  r   r   rP   zfmp+openfigir  lowFr[  r   T)r  r   r   r   r+  r,  r
   )	r   r  r  r  r   mergedrS   
conflictedrT   s	            r   Ftest_provider_merge_raises_confidence_and_conflict_needs_manual_reviewr  h  s   EEEEEEM M M M M M M M
| | | | | | | |
~ ~ ~ ~ ~ ~ ~ ~
 <<D))$R^R^R`R`bmbmboboQpqqqFHr>>>>v;;!!9////!96))))<<D--dNEVbVbVdVdfufufwfwUxyyyJr>>>>z??aa=#u,,,,)$nRWjopppF&'4//////r   c                    ddl m}  t                      }t          |d          }t	          |dddddd	d
          }t          |||ddddddd
d          }t          ||j        dt          d          ddd           t          |dd          d         }|d         d         dk    sJ |d         d         dk    sJ d | |d                   v sJ  | ddd          dk    sJ d  | ddd          vsJ d S )!Nr   )_price_previewzSynthetic Price Brokerr   rF  r]   r?  r@  r   zsynthetic price previewr   r   rK   
2026-01-01r|  rs   Tr   
2026-01-02z250.12r   )rR   
price_datecloser=   r+  rA  rH  r   
last_priceprice_sourcez
250.12 USDzNo Pricer   )r9   r  u   Preis noch nicht verfügbarr   )
3jarvis_finance.dashboard.views.page_17_position_addr  r   r'   r   r   r   rR   r   r
   )r  r   r&   r   rT   rowss         r   Itest_price_preview_uses_local_market_prices_and_missing_price_is_not_zeror    s   RRRRRR<<Dd$<==J,TuKlw|  DR  [`  ko  vO  P  P  PJ-dZ\f  wI  Y\  iu  @E  `e  qz  AZ  dh  i  i  iFt6+?L`ghp`q`q  }B  MR  di  j  j  j  j'NPUVVVW`aD7< H,,,,7>"e++++>>$q'222222>:R@@AAEbbbbbnnj%K%KLLLLLLLLr   c                    t                      } t          | d          }t          | ddddddddd	

  
        }t          | ||ddddddd	d          }t	          | t          ddi          dd          }|j        dk    sJ |                     d|j        f          	                                }|d         dk    sJ |d         dk    sJ d S )NzSynthetic Update Brokerr   rF  r]   r?  r@  r   r   zsynthetic price update)	r8   r9   r=   r:   r;   r<   r+  rA  rB   r   rK   r  r|  rs   Tr   251.34r  )r+  r8   r  rP   z?SELECT close, provider FROM market_prices WHERE instrument_id=?r  r+  mock)
r   r'   r   r   r   r   updated_countr#   rR   r,   )r   r&   r   rT   summaryri  s         r   Etest_update_equity_prices_cli_workflow_uses_explicit_provider_mappingr    s[   <<Dd$=>>J,TuKlw|  DR  [`  ko  z  QV  ]u  v  v  vJ-dZ\f  wI  Y\  iu  @E  `e  qz  AY  cg  h  h  hF#D3JES[K\3]3]kp  ~J  K  K  KG A%%%%
,,X[a[oZq
r
r
{
{
}
}Cw<8####z?f$$$$$$r   c                   |                      dd           |                     dt          |                     t                                          d          }|j        J |j        dk    sJ d|j        k    sJ d S )Nrc  Fr   r   r@  fmp_api_key_missing)r  r  r   r   	get_pricer  error_messagerA  )r  r  quotes      r   0test_fmp_price_provider_missing_key_is_sanitizedr    s    }e4443S]]CCC"$$..u55E;"77777E)))))))r   c                   ddl m}m}m} dD ]}|                     |d           |dz  }|dz  }|                    d	           |d
z                      dd           |                     dt          |                      |d          dk    sJ  |            j	        dk    sJ  |            j	        dk    sJ t                      j	        dk    sJ t                      }t          d |D                       d         dk    sJ t          d |D                       d         dk    sJ t          d |D                       rJ d S )Nr   )r  r   _runtime_secret_value)r   r   rc  rd  Fr   runtimer   Tparentsr   z6OPENFIGI_API_KEY=dummy-openfigi
FMP_API_KEY=dummy-fmp
r   r   r   )r   r   dummy-openfigi	dummy-fmpc              3  2   K   | ]}|d          dk    |V  dS rq  r   rh  s     r   rL  zFtest_runtime_secret_file_is_loaded_without_repo_env.<locals>.<genexpr>  rr  r   rs  aktivc              3  2   K   | ]}|d          dk    |V  dS )rg  rm  Nr   rh  s     r   rL  zFtest_runtime_secret_file_is_loaded_without_repo_env.<locals>.<genexpr>  s0      DD3z?e+C+C+C+C+C+CDDr   	Kategorie
key_loadedc              3  Z   K   | ]&}d t          |          v pdt          |          v V  'dS )r  r  N)r   rh  s     r   rL  zFtest_runtime_secret_file_is_loaded_without_repo_env.<locals>.<genexpr>  s>      __s#s3xx/J;#c((3J______r   )r  r  r   r  r  r  r  r  r   r  r   r   rv  rS  )	r  r  r  r   r  rw  r  r   rx  s	            r   3test_runtime_secret_file_is_loaded_without_repo_envr    s   ssssssssssc / /3...."G	!GMM$Mv!!"kv}!~~~3S\\BBB  !PQQUeeeee!!##+/?????&+5555!##+{::::+--HIIxIIIII(SW^^^^^DDxDDDDD[QUaaaaa__V^__________r   c                    ddl m}  i  G fdd|           } |d                              dd	          }|sJ d
         dk    sJ dt          d
                                                   vsJ d         ddik    sJ d S )Nr   r   c                       e Zd Zdd fd
ZdS )Ntest_openfigi_key_is_sent_as_header_not_query_param.<locals>.CapturingOpenFigiNheadersc               @    |d<   |d<   |pi d<   dddddd	d
dgigS )Nurlpayloadr  r  rF  r@  r   r]   rY   r  r  r   )r  r  r  r  captureds       r   
_post_jsonzYtest_openfigi_key_is_sent_as_header_not_query_param.<locals>.CapturingOpenFigi._post_json  se    !HUO")HY")-RHY'HTYgky~  QV  iw  x  x  y  z  {  {r   r  r  r  r  r  s   r   CapturingOpenFigir    sD        6: 	{ 	{ 	{ 	{ 	{ 	{ 	{ 	{ 	{r   r  r  r  r?  r   rH  r  z#https://api.openfigi.com/v3/mappingapikeyr  zX-OPENFIGI-APIKEY)r  r   r.  r   r!   )r   r  r  r  s      @r   3test_openfigi_key_is_sent_as_header_not_query_paramr    s    IIIIII"$H{ { { { { { {2 { { { %5666==N`e=ffDKK4E?CCCCC3x//55777777I#68H"IIIIIIIr   c                    ddl m} m} ddlm} g  G fdd|          } |d                              dd	
          }|sJ  |  |d                   j                  }|d         dgk    sJ |d         dgk    sJ d S )Nr   parse_qsurlparser  c                       e Zd Zdd fd
ZdS );test_fmp_key_is_sent_as_api_parameter.<locals>.CapturingFmpNr  c               V                         |           d|v rdddddgS ddd	dd
gS )N/stable/profiler?  r]   Tr  )r:   r=   r  r  r@  rF  r  r  appendr  r  r  r  s      r   	_get_jsonzEtest_fmp_key_is_sent_as_api_parameter.<locals>.CapturingFmp._get_json  sZ    OOC    C''!/UT\bccdd$.Oflz  A  A  B  Br   r  r  r  r  r  s   r   CapturingFmpr    sD        ,0 	B 	B 	B 	B 	B 	B 	B 	B 	Br   r  r  r  rP  r   rH  r  r   )urllib.parser  r  r  r  r.  r   )r  r  r  r  r  parsedr  s         @r   %test_fmp_key_is_sent_as_api_parameterr    s    ////////DDDDDDHB B B B B B B( B B B <,,,33:P^c3ddDKK4Xhhx{++122F(},,,,'?56666666r   c                :   dd l mc m} ddlm  G fdd          } G fdd          } G d d          }|                     |d	|           |                     |d
|           |                     |d|           |                    d          }d |D             }|d         d         dk    sJ |d         d         dk    sJ |d         d         dk    sJ |d         d         dk    sJ |d         d         dk    sJ |d         d         dk    sJ d S )Nr   LookupUnavailablec                  (    e Zd ZdZdZdd fd
ZdS )Utest_provider_status_probe_distinguishes_error_categories.<locals>.AuthFailedProviderr   dummyNr$  c                    d          )Nopenfigi_auth_failedr   r  r   r8   r  s      r   r.  z\test_provider_status_probe_distinguishes_error_categories.<locals>.AuthFailedProvider.search  s    ##$:;;;r   r  r  r  r9   r  r.  r  s   r   AuthFailedProviderr    sE        /3 	< 	< 	< 	< 	< 	< 	< 	< 	<r   r  c                  (    e Zd ZdZdZdd fd
ZdS )Xtest_provider_status_probe_distinguishes_error_categories.<locals>.NetworkFailedProviderr   r  Nr$  c                    d          )Nfmp_network_errorr   r  s      r   r.  z_test_provider_status_probe_distinguishes_error_categories.<locals>.NetworkFailedProvider.search  s    ##$7888r   r  r  s   r   NetworkFailedProviderr    sE        /3 	9 	9 	9 	9 	9 	9 	9 	9 	9r   r  c                  "    e Zd ZdZdZdddZdS )Ttest_provider_status_probe_distinguishes_error_categories.<locals>.NoResultsProviderr   r  Nr$  c                   g S r   r   rC  s      r   r.  z[test_provider_status_probe_distinguishes_error_categories.<locals>.NoResultsProvider.search  s    Ir   r  r   r   r   NoResultsProviderr    s:        /3 	 	 	 	 	 	 	r   r  r   r  FinnhubSymbolLookupProviderT)probec                     i | ]}|d          |S rf  r   rh  s     r   
<dictcomp>zMtest_provider_status_probe_distinguishes_error_categories.<locals>.<dictcomp>  s    <<<C3z?C<<<r   ro  r  auth_failedrs  zAuth fehlgeschlagenrm  network_errorNetzwerkfehlerrn  
no_resultszKeine Treffer)r  market_datacatalog%jarvis_finance.market_data.candidatesr  r6  r   )r  r  r  r  r  rx  by_providerr  s          @r   9test_provider_status_probe_distinguishes_error_categoriesr    s   888888888GGGGGG< < < < < < < < < <9 9 9 9 9 9 9 9 9 9        !9;MNNN!46KLLL!>@QRRR33$3??H<<8<<<Kz";/=@@@@z"8,0EEEEEuk*o====uh'+;;;;;y!+.,>>>>y!(+>>>>>>r   c                   |dz  }|dz  }|                     d           |dz                      dd           |                     d	t          |                     |                     d
d           |                     dd           t                      }|                                }t          |          |j        z   |j        z   }d|vsJ d|vsJ d S )Nr  r   Tr  r   zDOPENFIGI_API_KEY=super-secret-openfigi
FMP_API_KEY=super-secret-fmp
r   r   r   r   Fr   rc  zsuper-secret-openfigizsuper-secret-fmp)	r  r  r  r   r  r   
readouterrouterr)r  r  capsysr  r   rx  r  texts           r   9test_provider_status_does_not_log_or_return_secret_valuesr    s   "G	!GMM$Mv!!"y  EL!  M  M  M3S\\BBB)5999}e444+--H  ""Hx==8<'(,6D"$....T))))))r   c                    ddl m} m} ddlm} g  G fdd|          } |d                              dd	
          } |d                              dd	
          } |d                              dd	
          }|r|r|sJ |d         j        dk    sJ |d         j        dk    sJ |d         j        dk    sJ t          d D                       sJ t          d D                       sJ t          d D                       sJ  |  |d                   j
                  }|d         dgk    sJ d S )Nr   r  r  c                       e Zd Zdd fd
ZdS )\test_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalized.<locals>.StableFmpNr  c                                        |           d|v r	ddddddgS d|v rddddd	gS d
|v rddddd	gS d|v rddddd	gS d|v r
dddddddgS g S )N/stable/search-isinr@  rF  r  r?  r]   )r  r9   r  r:   r=   /stable/search-symbolr  z /stable/search-exchange-variants/stable/search-namer  Tr   {Gjo@)r  r:   r=   r  r  r  r  r  s      r   r  zftest_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalized.<locals>.StableFmp._get_json$  s$   OOC   $++#(2Sjp  {I  W\  ]  ]  ^  ^&#--#(2Sjp  D  E  E  F  F1S88#(2Sjp  D  E  E  F  F$++#(2Sjp  D  E  E  F  F C''#(.e^bos  E  F  F  G  GIr   r  r  s   r   	StableFmpr  #  s;        ,0 	 	 	 	 	 	 	 	 	r   r#  r  r  r?  r   rH  rP  r@  r  zfmp profilec              3     K   | ]}d |v V  	dS )r  Nr   r   r  s     r   rL  z\test_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalized.<locals>.<genexpr>9  (      @@$+@@@@@@r   c              3     K   | ]}d |v V  	dS )r   Nr   r%  s     r   rL  z\test_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalized.<locals>.<genexpr>:  s(      BB#&#-BBBBBBr   c              3     K   | ]}d |v V  	dS )r!  Nr   r%  s     r   rL  z\test_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalized.<locals>.<genexpr>;  r&  r   r  )r  r  r  r  r  r.  r:   r  r  rS  r   )	r  r  r  r#  	isin_rows	name_rowsticker_rowsfirst_qsr  s	           @r   Itest_fmp_stable_isin_symbol_name_and_profile_price_preview_are_normalizedr-    s   ////////DDDDDDH      %    	+...55NX]5^^I	+...55<R`e5ffI)K00077eQV7WWK22{222Q<....Q<"h....Q<$5555@@x@@@@@@@@BBBBBBBBBB@@x@@@@@@@@x!--344HH+......r   c                     ddl m} m} ddlm} g  G fdd|          } |d                              d          }|j        J t          |j                  d	k    sJ |j        d
k    sJ d         	                    d          sJ d S )Nr   r  )r   c                      e Zd Zd fdZdS )Ttest_fmp_price_provider_uses_stable_profile_without_logging_key.<locals>.StablePricepathr   paramsdict[str, str]c           
                              |dz   d                    d t          |                                          D                       z              ddddgS )N?&c              3  *   K   | ]\  }}| d | V  dS )=Nr   )r   kvs      r   rL  zqtest_fmp_price_provider_uses_stable_profile_without_logging_key.<locals>.StablePrice._get_json.<locals>.<genexpr>H  s0      1`1`AQ****1`1`1`1`1`1`r   r@  r"  r]   )r  r  r=   )r  joinsorteditems)r  r1  r2  r  s      r   r  z^test_fmp_price_provider_uses_stable_profile_without_logging_key.<locals>.StablePrice._get_jsonG  sa    OOD3J1`1`PVP\P\P^P^I_I_1`1`1`)`)``aaa$v5IIJJr   N)r1  r   r2  r3  r  r  s   r   StablePricer0  F  s;        	K 	K 	K 	K 	K 	K 	K 	Kr   r>  r  r  r@  r  r]   z/stable/profile?)
r  r  r  !jarvis_finance.market_data.pricesr   r  r  r   r=   rR  )r  r  r   r>  r  r  s        @r   ?test_fmp_price_provider_uses_stable_profile_without_logging_keyr@  @  s    ////////HHHHHHHK K K K K K K, K K K
 K,,,66u==E;"""u{x''''>U""""A;!!"45555555r   c                ,   ddl m} ddlm} ddlm} ddlm  G fdd|          }	  |d	                              d
d           t          d          # $ r3}t          |          dk    sJ dt          |          vsJ Y d }~d S d }~ww xY w)Nr   )	HTTPError)BytesIOr  r  c                       e Zd Zdd fd
ZdS )Ytest_openfigi_429_is_reported_as_rate_limited_without_secret.<locals>.RateLimitedOpenFigiNr  c                    d          )Nopenfigi_rate_limitedr   )r  r  r  r  r  s       r   r  zdtest_openfigi_429_is_reported_as_rate_limited_without_secret.<locals>.RateLimitedOpenFigi._post_jsonY  s    ##$;<<<r   r  r  s   r   RateLimitedOpenFigirE  X  s;        6: 	= 	= 	= 	= 	= 	= 	= 	= 	=r   rH  r  r  r?  r   rH  zexpected rate limitrG  )urllib.errorrB  iorC  r  r   r  r  r.  AssertionErrorr   )r  rB  rC  r   rH  excr  s         @r   <test_openfigi_429_is_reported_as_rate_limited_without_secretrM  R  s!   &&&&&&IIIIIIGGGGGG= = = = = = =4 = = =4$4555<<>_d<eee
 2333	  0 0 03xx22222s3xx//////////0s   !A B (BBc                     ddl m} m}  G d d          }t                      } ||ddg           \  }}|g k    sJ |g k    sJ d S )Nr   )r   r   c                      e Zd ZdZdddZdS )Ttest_online_search_not_called_when_include_external_false.<locals>.ExplodingProvidershould_not_runNr$  c                    t          d          )Nz,provider was called during local-only search)rK  rC  s      r   r.  z[test_online_search_not_called_when_include_external_false.<locals>.ExplodingProvider.searchj  s     !OPPPr   r/  r   r   r   ExplodingProviderrP  h  s<        /3 	Q 	Q 	Q 	Q 	Q 	Q 	Qr   rS  r@  r   r   )r  r   r   r   )r   r   rS  r   r  rS   s         r   9test_online_search_not_called_when_include_external_falserT  e  s    ZZZZZZZZQ Q Q Q Q Q Q Q
 <<D''eUbIIIND(r>>>>2::::::r   )r   )r   r   r   r   )r(   r   r   r)   rD  )<
__future__r   decimalr   r   jarvis_finance.equity.manager   r   r   r   r	   r
   jarvis_finance.ledger.positionsr   r  r   r   r?  r   r   r   r   jarvis_finance.storage.databaser   !jarvis_finance.storage.migrationsr   r   r'   r-   rW   rk   ru   r   r   r   r   r   r   r   r  r7  r9  rX  r_  ry  r}  r  r  r  r  r  r  r  r  r  r  r-  r@  rM  rT  r   r   r   <module>r[     s+   " " " " " "                       @ ? ? ? ? ? ` ` ` ` ` ` ` ` I  I  I  I  I  I  I  I  I  I  I  I : : : : : : > > > > > >      i i i i !  !  !  !F%0 %0 %0 %0Pl l l l:/
 /
 /
 /
d.
 .
 .
 .
b16 16 16 16hF F F FDv Dv Dv DvN:, :, :, :,zA A A A8   4B B B B*a a a a a a a aU U U U*B B B B$g g g g. . . .. . . .80 0 0 0DM M M M 	% 	% 	% 	%* * * *` ` ` `*J J J J&7 7 7 7( ?  ?  ?  ?F* * * * /  /  /  /F6 6 6 6$4 4 4 4&     r   