-
Notifications
You must be signed in to change notification settings - Fork 14
Research cron observer #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
36e0b1e
Implement cron_observer example
Winston-503 44f5e0c
price change tool and observer agent example
ethancjackson a95a067
price change observer example
ethancjackson 2c3283f
remove interaction example in favor of observer agent example
ethancjackson 433eb06
Merge branch 'main' into research-cron-observer
ethancjackson 590c1cc
modified price chg observer example to use a basic LLM task
ethancjackson 6d92701
lint
ethancjackson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| from .alchemy_price_history import AlchemyPriceHistoryBySymbol, AlchemyPriceHistoryByAddress | ||
| from .alchemy_price_change import TokenPriceChangeCalculator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| from datetime import datetime | ||
| from decimal import Decimal | ||
| from typing import Optional | ||
|
|
||
| from alphaswarm.services.alchemy import AlchemyClient | ||
| from alphaswarm.tools.alchemy import AlchemyPriceHistoryByAddress | ||
| from pydantic import BaseModel | ||
| from smolagents import Tool | ||
|
|
||
|
|
||
| class TokenPriceChange(BaseModel): | ||
| token_address: str | ||
| network: str | ||
| start_time: datetime | ||
| end_time: datetime | ||
| start_price: float | ||
| end_price: float | ||
| percent_change: float | ||
| n_samples: int | ||
| frequency: str | ||
|
|
||
|
|
||
| class TokenPriceChangeCalculator(Tool): | ||
| name = "TokenPriceChangeCalculator" | ||
| description = "Calculate the percentage price change for a token over a specified number of samples and frequency" | ||
| inputs = { | ||
| "token_address": { | ||
| "type": "string", | ||
| "description": "The token address to analyze", | ||
| }, | ||
| "frequency": { | ||
| "type": "string", | ||
| "description": "Time interval between data points", | ||
| "enum": ["5m", "1h", "1d"], | ||
| }, | ||
| "n_samples": { | ||
| "type": "integer", | ||
| "description": "Number of samples to analyze (must be >= 2)", | ||
| "minimum": 2, | ||
| }, | ||
| "network": { | ||
| "type": "string", | ||
| "description": "Network where the token exists (e.g. eth-mainnet, base-mainnet)", | ||
| "default": "eth-mainnet", | ||
| "nullable": True, | ||
| }, | ||
| } | ||
| output_type = "object" | ||
|
|
||
| def __init__(self, alchemy_client: Optional[AlchemyClient] = None): | ||
| super().__init__() | ||
| self.price_history_tool = AlchemyPriceHistoryByAddress(alchemy_client) | ||
|
|
||
| def _calculate_percent_change(self, start_price: Decimal, end_price: Decimal) -> float: | ||
| return float((end_price - start_price) / start_price * 100) | ||
|
|
||
| def forward( | ||
| self, token_address: str, frequency: str, n_samples: int, network: str = "eth-mainnet" | ||
| ) -> TokenPriceChange: | ||
| # Calculate the required history in days based on frequency and n_samples | ||
| interval_to_minutes = {"5m": 5, "1h": 60, "1d": 1440} | ||
| minutes_needed = interval_to_minutes[frequency] * n_samples | ||
| days_needed = (minutes_needed // 1440) + 1 # Round up to nearest day | ||
|
|
||
| # Use the existing price history tool | ||
| price_history = self.price_history_tool.forward( | ||
| address=token_address, network=network, interval=frequency, history=days_needed | ||
| ) | ||
|
|
||
| # Ensure we have enough data points | ||
| prices = price_history.data[-n_samples:] # Get the most recent n_samples | ||
| if len(prices) < n_samples: | ||
| raise ValueError(f"Requested {n_samples} samples but only got {len(prices)}") | ||
|
|
||
| # Calculate percent changes | ||
| start_price = prices[0].value | ||
| end_price = prices[-1].value | ||
| percent_change = self._calculate_percent_change(start_price, end_price) | ||
|
|
||
| return TokenPriceChange( | ||
| token_address=token_address, | ||
| network=network, | ||
| start_time=prices[0].timestamp, | ||
| end_time=prices[-1].timestamp, | ||
| start_price=float(start_price), | ||
| end_price=float(end_price), | ||
| percent_change=percent_change, | ||
| n_samples=n_samples, | ||
| frequency=frequency, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import asyncio | ||
| import datetime | ||
| import logging | ||
| from typing import List | ||
|
|
||
| import dotenv | ||
| from alphaswarm.agent.agent import AlphaSwarmAgent | ||
| from alphaswarm.agent.clients import CronJobClient | ||
| from alphaswarm.services.alchemy import AlchemyClient | ||
| from alphaswarm.tools.alchemy import TokenPriceChangeCalculator | ||
|
|
||
|
|
||
| class PriceChangeObserver(AlphaSwarmAgent): | ||
| def __init__( | ||
| self, | ||
| token_addresses: List[str], | ||
| chain: str = "base-mainnet", | ||
| price_change_interval: str = "1h", | ||
| price_pct_chg_thresh: float = 2.0, | ||
| ) -> None: | ||
| """ | ||
| A basic agent that observes price changes for a set of token addresses. | ||
| This agent does not use any LLM calls, instead it will just execute a sequence of tool calls. | ||
| It can be adapted to use any LLM by adding logic to thea `process_message` method. | ||
|
|
||
| Args: | ||
| token_addresses: List of token addresses to observe | ||
| chain: Chain to observe | ||
| price_change_interval: Interval to observe | ||
| price_pct_chg_thresh: Percentage change threshold to observe | ||
| """ | ||
|
|
||
| self.alchemy_client = AlchemyClient.from_env() | ||
| self.price_change_calculator = TokenPriceChangeCalculator(self.alchemy_client) | ||
| self.token_addresses = token_addresses | ||
| self.chain = chain | ||
| self.price_change_interval = price_change_interval | ||
| self.price_pct_chg_thresh = price_pct_chg_thresh | ||
|
|
||
| hints = "Have any of the price changes increased or decreased (+/- 1%) since the last observation? Respond with either 'yes' or 'no'." | ||
|
|
||
| super().__init__(model_id="gpt-4o-mini", tools=[], hints=hints) | ||
|
|
||
| def get_price_alerts(self) -> str: | ||
| """ | ||
| Get the price alerts for the token addresses. | ||
| """ | ||
| price_alerts = [] | ||
| for address in self.token_addresses: | ||
| price_history = self.price_change_calculator.forward( | ||
| token_address=address, | ||
| frequency=self.price_change_interval, | ||
| n_samples=2, | ||
| network=self.chain, | ||
| ) | ||
|
|
||
| if abs(price_history.percent_change) >= self.price_pct_chg_thresh: | ||
| price_alerts.append( | ||
| f"{address}: {price_history.percent_change}% change in the last {self.price_change_interval}." | ||
| ) | ||
|
|
||
| logging.info(f"{len(price_alerts)} price alerts found.") | ||
| if len(price_alerts) > 0: | ||
| alert_message = f"Price changes have been observed for the following tokens as of {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: " | ||
| alert_message += "\n" + "\n".join(price_alerts) | ||
| return alert_message | ||
| else: | ||
| return "" | ||
|
|
||
| # async def process_message(self, message: str) -> Optional[str]: | ||
| # """ | ||
| # You can override the `process_message` method to specify how the agent will respond to the price alerts. | ||
| # When this method is not overridden, the default LLM-based agent configuration will be used to respond. | ||
| # """ | ||
| # logging.info(f"Agent received alerts:\n{message}") | ||
| # pass | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| dotenv.load_dotenv() | ||
| logging.basicConfig(level=logging.INFO) | ||
| token_addresses = [ | ||
| "0x4F9Fd6Be4a90f2620860d680c0d4d5Fb53d1A825", # AIXBT | ||
| "0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b", # VIRTUAL | ||
| "0x731814e491571A2e9eE3c5b1F7f3b962eE8f4870", # VADER | ||
| ] | ||
|
|
||
| agent = PriceChangeObserver( | ||
| token_addresses=token_addresses, | ||
| chain="base-mainnet", | ||
| price_change_interval="5m", # '5m', '1h', or '1d' | ||
| price_pct_chg_thresh=0.02, | ||
| ) | ||
|
|
||
| cron_client = CronJobClient( | ||
| agent=agent, | ||
| client_id="Price Change Observer", | ||
| interval_seconds=5, | ||
| response_handler=lambda _: None, | ||
| message_generator=agent.get_price_alerts, | ||
| should_process=lambda alerts: len(alerts) > 0, | ||
| skip_message=lambda _: None, | ||
| max_history=2, | ||
| ) | ||
| await asyncio.gather(cron_client.start()) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) |
14 changes: 14 additions & 0 deletions
14
tests/integration/tools/alchemy/test_alchemy_price_change_calculator.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from alphaswarm.services.alchemy import AlchemyClient | ||
| from alphaswarm.tools.alchemy.alchemy_price_change import TokenPriceChangeCalculator | ||
|
|
||
|
|
||
| def test_get_price_change(alchemy_client: AlchemyClient) -> None: | ||
| tool = TokenPriceChangeCalculator(alchemy_client) | ||
| result = tool.forward( | ||
| token_address="0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", # WBTC | ||
| frequency="5m", | ||
| n_samples=2, | ||
| network="eth-mainnet", | ||
| ) | ||
|
|
||
| assert abs(result.percent_change) > 0 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main thing I might want to discuss is:
Should we keep
cron_job.pyas-is and create a new client to add logic? For example we could call thisconditional_cron_job.pyinstead.Another point is that I'd like to also create something like a
swarm_cron_job.pythat allows the user to specify something like a chain of agents to handle processing, so we could have, e.g.PriceForecaster -> TradeAssistant