Skip to content

Commit 6118098

Browse files
committed
feat: add compound market analysis tool
1 parent 557b70e commit 6118098

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

examples/pendle.yaml

Whitespace-only changes.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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

Comments
 (0)