11"""
22Price fetching service using Pyth Network API.
33"""
4+
45import logging
56import os
67import re
1112logger = logging .getLogger (__name__ )
1213
1314HERMES_API_URL = "https://hermes.pyth.network/api/latest_price_feeds"
14- GOLDSILVER_AI_URL = "https://goldsilver.ai/metal-prices/shanghai-silver-price"
1515
1616
1717class PriceService :
1818 def __init__ (self ):
1919 self .feeds = self ._load_feeds ()
2020 self .session : Optional [aiohttp .ClientSession ] = None
21-
21+
2222 def _load_feeds (self ) -> dict :
2323 """Load feed IDs from environment."""
2424 feeds_str = os .environ .get (
2525 "CRYPTO_FEEDS" ,
2626 "BTC:0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,"
2727 "ETH:0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace,"
28- "SOL:0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"
28+ "SOL:0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" ,
2929 )
30-
30+
3131 feeds = {}
3232 for pair in feeds_str .split ("," ):
3333 if ":" in pair :
3434 name , feed_id = pair .split (":" , 1 )
3535 feeds [name .strip ().upper ()] = feed_id .strip ()
36-
36+
3737 logger .info (f"Loaded { len (feeds )} price feeds" )
3838 return feeds
39-
39+
4040 async def _get_session (self ) -> aiohttp .ClientSession :
4141 if self .session is None or self .session .closed :
4242 timeout = aiohttp .ClientTimeout (total = 15 )
4343 self .session = aiohttp .ClientSession (timeout = timeout )
4444 return self .session
45-
46- async def get_shanghai_silver_price (self ) -> Optional [float ]:
47- """Fetch Shanghai Silver price from goldsilver.ai."""
48- try :
49- session = await self ._get_session ()
50- headers = {
51- "User-Agent" : "RustyMcPriceface/1.0 (crypto price bot)" ,
52- "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ,
53- "Accept-Language" : "en-US,en;q=0.9" ,
54- }
55- async with session .get (GOLDSILVER_AI_URL , headers = headers ) as resp :
56- if resp .status != 200 :
57- logger .warning (f"goldsilver.ai returned { resp .status } " )
58- return None
59-
60- text = await resp .text ()
61-
62- # Extract number after "Shanghai Spot" and "$"
63- shanghai_price = self ._extract_price_after (text , "Shanghai Spot" )
64- if shanghai_price and shanghai_price > 10 :
65- logger .info (f"Shanghai Silver: ${ shanghai_price } " )
66- return shanghai_price
67-
68- logger .warning ("Could not extract valid Shanghai price" )
69- return None
70-
71- except Exception as e :
72- logger .error (f"Failed to fetch Shanghai Silver: { e } " )
73- return None
74-
75- def _extract_price_after (self , html : str , prefix : str ) -> Optional [float ]:
76- """Extract dollar amount after a prefix."""
77- pos = html .find (prefix )
78- if pos == - 1 :
79- return None
80-
81- after = html [pos :pos + 200 ]
82-
83- # Find $ followed by number
84- match = re .search (r'\$([0-9,]+\.?[0-9]*)' , after )
85- if match :
86- price_str = match .group (1 ).replace ("," , "" )
87- try :
88- return float (price_str )
89- except ValueError :
90- return None
91- return None
92-
45+
9346 async def get_yahoo_price (self , ticker : str ) -> Optional [float ]:
9447 """Fetch price from Yahoo Finance API."""
9548 try :
@@ -102,79 +55,75 @@ async def get_yahoo_price(self, ticker: str) -> Optional[float]:
10255 if resp .status != 200 :
10356 logger .warning (f"Yahoo returned { resp .status } for { ticker } " )
10457 return None
105-
58+
10659 data = await resp .json ()
107-
60+
10861 # Extract price from Yahoo Finance JSON structure
10962 result = data .get ("chart" , {}).get ("result" , [])
11063 if not result :
11164 logger .warning (f"No result from Yahoo for { ticker } " )
11265 return None
113-
66+
11467 meta = result [0 ].get ("meta" , {})
11568 price = meta .get ("regularMarketPrice" )
116-
69+
11770 if price :
11871 logger .info (f"Yahoo { ticker } : { price } " )
11972 return float (price )
120-
73+
12174 logger .warning (f"No price in Yahoo response for { ticker } " )
12275 return None
123-
76+
12477 except Exception as e :
12578 logger .error (f"Failed to fetch { ticker } from Yahoo: { e } " )
12679 return None
127-
80+
12881 async def get_price (self , crypto : str ) -> Optional [float ]:
12982 """Get price for a single cryptocurrency."""
13083 crypto = crypto .upper ()
131-
132- # Special handling for Shanghai Silver (not in Pyth feeds)
133- if crypto == "SSILVER" :
134- return await self .get_shanghai_silver_price ()
135-
84+
13685 # Special handling for DXY (Yahoo Finance)
13786 if crypto == "DXY" :
13887 return await self .get_yahoo_price ("DX-Y.NYB" )
139-
88+
14089 if crypto not in self .feeds :
14190 logger .warning (f"No feed ID for { crypto } " )
14291 return None
143-
92+
14493 feed_id = self .feeds [crypto ]
14594 url = f"{ HERMES_API_URL } ?ids[]={ feed_id } "
146-
95+
14796 try :
14897 session = await self ._get_session ()
14998 async with session .get (url ) as resp :
15099 if resp .status != 200 :
151100 logger .warning (f"Pyth API returned { resp .status } for { crypto } " )
152101 return None
153-
102+
154103 data = await resp .json ()
155104 if not data or not isinstance (data , list ):
156105 return None
157-
106+
158107 price_data = data [0 ].get ("price" , {})
159108 price_str = price_data .get ("price" )
160109 expo = price_data .get ("expo" , 0 )
161-
110+
162111 if price_str is None :
163112 return None
164-
165- price = int (price_str ) * (10 ** expo )
166-
113+
114+ price = int (price_str ) * (10 ** expo )
115+
167116 if price <= 0 :
168117 logger .warning (f"Invalid price { price } for { crypto } " )
169118 return None
170-
119+
171120 logger .debug (f"Fetched { crypto } price: ${ price } " )
172121 return float (price )
173-
122+
174123 except Exception as e :
175124 logger .error (f"Failed to fetch { crypto } price: { e } " )
176125 return None
177-
126+
178127 async def get_all_prices (self ) -> dict :
179128 """Get prices for all configured cryptocurrencies."""
180129 results = {}
@@ -183,7 +132,7 @@ async def get_all_prices(self) -> dict:
183132 if price :
184133 results [crypto ] = price
185134 return results
186-
135+
187136 async def close (self ):
188137 """Close the HTTP session."""
189138 if self .session and not self .session .closed :
0 commit comments