1+ from dataclasses import dataclass
2+ from textwrap import dedent
3+ from typing import Optional
4+
5+ import httpx
6+ from langchain .chat_models import init_chat_model
7+ from langchain .prompts import PromptTemplate
8+ from langchain_core .output_parsers import StrOutputParser
9+ from loguru import logger
10+ from pydantic import BaseModel , Field
11+ from openagent .agent .config import ModelConfig
12+ from openagent .core .tool import Tool
13+
14+ @dataclass
15+ class CompoundMarketData :
16+ address : str
17+ collateralAssets : list [str ]
18+ borrowAPR : float
19+ supplyAPR : float
20+ borrowAPRChange24h : float
21+ supplyAPRChange24h : float
22+
23+ class CompoundMarketConfig (BaseModel ):
24+ model : Optional [ModelConfig ] = Field (
25+ default = None ,
26+ description = "Model configuration for LLM. If not provided, will use agent's core model" ,
27+ )
28+
29+ class ArbitrumCompoundMarketTool (Tool [CompoundMarketConfig ]):
30+ def __init__ (self , core_model = None ):
31+ super ().__init__ ()
32+ self .core_model = core_model
33+ self .tool_model = None
34+ self .tool_prompt = None
35+
36+ @property
37+ def name (self ) -> str :
38+ return "arbitrum_compound_market_analysis"
39+
40+ @property
41+ def description (self ):
42+ return "You are a DeFi data analyst that analyze Compound markets' APR."
43+
44+ async def setup (self , config : CompoundMarketConfig ) -> None :
45+ model_config = config .model if config .model else self .core_model
46+ if not model_config :
47+ raise RuntimeError ("No model configuration provided" )
48+
49+ self .tool_model = init_chat_model (
50+ model = model_config .name ,
51+ model_provider = model_config .provider ,
52+ temperature = model_config .temperature ,
53+ )
54+
55+ self .tool_prompt = PromptTemplate (
56+ template = dedent (
57+ f"""\
58+ { self .description }
59+
60+ ### Data
61+ {{data}}
62+
63+ ### Data Structure
64+ - Market object with:
65+ - `collateralAssets`: List of supported collateral assets
66+ - `borrowAPR`: Current borrow APR
67+ - `supplyAPR`: Current supply APR
68+ - `borrowAPRChange24h`: 24h borrow APR change
69+ - `supplyAPRChange24h`: 24h supply APR change
70+
71+ ### Task
72+ Analyze the market data and provide:
73+ - Must be concise with clear statements about APR changes
74+ - Include both supply and borrow APR changes
75+ - Include list of supported collateral assets
76+ - Do not provide personal opinions or financial advice\
77+ """
78+ ),
79+ input_variables = ["data" ],
80+ )
81+
82+ async def __call__ (self ) -> str :
83+ logger .info (f"{ self .name } tool is called." )
84+
85+ if not self .tool_model :
86+ raise RuntimeError ("Model not initialized" )
87+
88+ try :
89+ # Fetch Compound arbitrum-network market data
90+ arbitrum_market_list = await self ._fetch_compound_arbitrum_market_data ()
91+
92+ # Analyze market data
93+ chain = self .tool_prompt | self .tool_model | StrOutputParser ()
94+
95+ # Run analysis chain
96+ response = await chain .ainvoke (
97+ {
98+ "data" : arbitrum_market_list ,
99+ }
100+ )
101+
102+ logger .info (f"{ self .name } tool response: { response .strip ()} ." )
103+
104+ return response .strip ()
105+
106+ except Exception as e :
107+ logger .error (f"Error in { self .name } tool: { e } " )
108+ return f"Error in { self .name } tool: { e } "
109+
110+ async def _fetch_compound_arbitrum_market_data (self ) -> list [CompoundMarketData ]:
111+ async with httpx .AsyncClient (timeout = 30.0 ) as client :
112+ response = await client .get (
113+ "https://v3-api.compound.finance/market/all-networks/all-contracts/summary"
114+ )
115+
116+ if response .status_code != 200 :
117+ raise Exception (f"Failed to fetch Compound market data: { response .text } , { response .status_code } " )
118+
119+ results = response .json ()
120+
121+ # Filter for Arbitrum markets (chain_id 42161)
122+ arbitrum_markets = [market for market in results if market ["chain_id" ] == 42161 ]
123+
124+ market_data = []
125+
126+ for market in arbitrum_markets :
127+ # Fetch historical data for each address
128+ historical_response = await client .get (
129+ f"https://v3-api.compound.finance/market/arbitrum-mainnet/{ market ['comet' ]['address' ]} /historical/summary"
130+ )
131+
132+ if historical_response .status_code != 200 :
133+ logger .warning (f"Failed to fetch historical data for { market ['comet' ]['address' ]} : { historical_response .text } , { historical_response .status_code } " )
134+ continue
135+
136+ historical_data = historical_response .json ()
137+
138+ # Sort historical data by timestamp in descending order (newest first)
139+ sorted_data = sorted (historical_data , key = lambda x : x ['timestamp' ], reverse = True )
140+
141+ if len (sorted_data ) < 2 :
142+ logger .warning (f"Insufficient historical data for { market ['comet' ]['address' ]} " )
143+ continue
144+
145+ # Convert string APRs to float
146+ current_borrow_apr = float (sorted_data [0 ]["borrow_apr" ])
147+ current_supply_apr = float (sorted_data [0 ]["supply_apr" ])
148+ yesterday_borrow_apr = float (sorted_data [1 ]["borrow_apr" ])
149+ yesterday_supply_apr = float (sorted_data [1 ]["supply_apr" ])
150+
151+
152+ # Calculate 24h changes
153+ borrow_apr_change_24h = current_borrow_apr - yesterday_borrow_apr
154+ supply_apr_change_24h = current_supply_apr - yesterday_supply_apr
155+
156+ market_data .append (
157+ CompoundMarketData (
158+ address = market ['comet' ]['address' ],
159+ collateralAssets = market ['collateral_asset_symbols' ],
160+ borrowAPR = current_borrow_apr ,
161+ supplyAPR = current_supply_apr ,
162+ borrowAPRChange24h = borrow_apr_change_24h ,
163+ supplyAPRChange24h = supply_apr_change_24h
164+ )
165+ )
166+
167+ return market_data
0 commit comments