From dd95824d2a69f19c07179e10489040b2798afcd6 Mon Sep 17 00:00:00 2001 From: Herklos Date: Tue, 28 Oct 2025 12:15:02 +0100 Subject: [PATCH] Add Coindesk, Lunarcrush and Alternative.me services --- Evaluator/Social/news_evaluator/__init__.py | 2 +- .../config/CryptoNewsEvaluator.json | 3 + Evaluator/Social/news_evaluator/metadata.json | 4 +- Evaluator/Social/news_evaluator/news.py | 65 ++++- .../resources/CryptoNewsEvaluator.md | 7 + .../Social/sentiment_evaluator/__init__.py | 1 + .../config/FearAndGreedIndexEvaluator.json | 3 + .../config/SocialScoreEvaluator.json | 2 + .../Social/sentiment_evaluator/metadata.json | 6 + .../resources/FearAndGreedIndexEvaluator.md | 7 + .../resources/SocialScoreEvaluator.md | 7 + .../Social/sentiment_evaluator/sentiment.py | 111 +++++++++ Evaluator/Social/trends_evaluator/__init__.py | 2 +- .../config/MarketCapEvaluator.json | 4 + .../Social/trends_evaluator/metadata.json | 4 +- .../resources/MarketCapEvaluator.md | 7 + Evaluator/Social/trends_evaluator/trends.py | 48 +++- .../mixed_strategies.py | 4 +- .../alternative_me_service/__init__.py | 1 + .../alternative_me_service/alternative_me.py | 42 ++++ .../alternative_me_service/metadata.json | 6 + .../coindesk_service/__init__.py | 1 + .../coindesk_service/coindesk.py | 42 ++++ .../coindesk_service/metadata.json | 6 + .../lunarcrush_service/__init__.py | 1 + .../lunarcrush_service/lunarcrush.py | 62 +++++ .../lunarcrush_service/metadata.json | 6 + .../alternative_me_service_feed/__init__.py | 2 + .../alternative_me_feed.py | 136 +++++++++++ .../alternative_me_service_feed/metadata.json | 6 + .../coindesk_service_feed/__init__.py | 3 + .../coindesk_service_feed/coindesk_feed.py | 227 ++++++++++++++++++ .../coindesk_service_feed/metadata.json | 6 + .../google_service_feed/google_feed.py | 12 +- .../lunarcrush_service_feed/__init__.py | 1 + .../lunarcrush_feed.py | 151 ++++++++++++ .../lunarcrush_service_feed/metadata.json | 6 + 37 files changed, 991 insertions(+), 13 deletions(-) create mode 100644 Evaluator/Social/news_evaluator/config/CryptoNewsEvaluator.json create mode 100644 Evaluator/Social/news_evaluator/resources/CryptoNewsEvaluator.md create mode 100644 Evaluator/Social/sentiment_evaluator/__init__.py create mode 100644 Evaluator/Social/sentiment_evaluator/config/FearAndGreedIndexEvaluator.json create mode 100644 Evaluator/Social/sentiment_evaluator/config/SocialScoreEvaluator.json create mode 100644 Evaluator/Social/sentiment_evaluator/metadata.json create mode 100644 Evaluator/Social/sentiment_evaluator/resources/FearAndGreedIndexEvaluator.md create mode 100644 Evaluator/Social/sentiment_evaluator/resources/SocialScoreEvaluator.md create mode 100644 Evaluator/Social/sentiment_evaluator/sentiment.py create mode 100644 Evaluator/Social/trends_evaluator/config/MarketCapEvaluator.json create mode 100644 Evaluator/Social/trends_evaluator/resources/MarketCapEvaluator.md create mode 100644 Services/Services_bases/alternative_me_service/__init__.py create mode 100644 Services/Services_bases/alternative_me_service/alternative_me.py create mode 100644 Services/Services_bases/alternative_me_service/metadata.json create mode 100644 Services/Services_bases/coindesk_service/__init__.py create mode 100644 Services/Services_bases/coindesk_service/coindesk.py create mode 100644 Services/Services_bases/coindesk_service/metadata.json create mode 100644 Services/Services_bases/lunarcrush_service/__init__.py create mode 100644 Services/Services_bases/lunarcrush_service/lunarcrush.py create mode 100644 Services/Services_bases/lunarcrush_service/metadata.json create mode 100644 Services/Services_feeds/alternative_me_service_feed/__init__.py create mode 100644 Services/Services_feeds/alternative_me_service_feed/alternative_me_feed.py create mode 100644 Services/Services_feeds/alternative_me_service_feed/metadata.json create mode 100644 Services/Services_feeds/coindesk_service_feed/__init__.py create mode 100644 Services/Services_feeds/coindesk_service_feed/coindesk_feed.py create mode 100644 Services/Services_feeds/coindesk_service_feed/metadata.json create mode 100644 Services/Services_feeds/lunarcrush_service_feed/__init__.py create mode 100644 Services/Services_feeds/lunarcrush_service_feed/lunarcrush_feed.py create mode 100644 Services/Services_feeds/lunarcrush_service_feed/metadata.json diff --git a/Evaluator/Social/news_evaluator/__init__.py b/Evaluator/Social/news_evaluator/__init__.py index 065d74377..b3eabe5b0 100644 --- a/Evaluator/Social/news_evaluator/__init__.py +++ b/Evaluator/Social/news_evaluator/__init__.py @@ -1 +1 @@ -from .news import TwitterNewsEvaluator \ No newline at end of file +from .news import TwitterNewsEvaluator, CryptoNewsEvaluator \ No newline at end of file diff --git a/Evaluator/Social/news_evaluator/config/CryptoNewsEvaluator.json b/Evaluator/Social/news_evaluator/config/CryptoNewsEvaluator.json new file mode 100644 index 000000000..db3a4f277 --- /dev/null +++ b/Evaluator/Social/news_evaluator/config/CryptoNewsEvaluator.json @@ -0,0 +1,3 @@ +{ + "language": "en" +} \ No newline at end of file diff --git a/Evaluator/Social/news_evaluator/metadata.json b/Evaluator/Social/news_evaluator/metadata.json index 685eaba67..5bc986899 100644 --- a/Evaluator/Social/news_evaluator/metadata.json +++ b/Evaluator/Social/news_evaluator/metadata.json @@ -1,6 +1,6 @@ { "version": "1.2.0", "origin_package": "OctoBot-Default-Tentacles", - "tentacles": ["TwitterNewsEvaluator"], - "tentacles-requirements": ["text_analysis", "twitter_service_feed"] + "tentacles": ["TwitterNewsEvaluator", "CryptoNewsEvaluator"], + "tentacles-requirements": ["text_analysis", "twitter_service_feed", "coindesk_service_feed"] } \ No newline at end of file diff --git a/Evaluator/Social/news_evaluator/news.py b/Evaluator/Social/news_evaluator/news.py index 339178c89..3696abc2b 100644 --- a/Evaluator/Social/news_evaluator/news.py +++ b/Evaluator/Social/news_evaluator/news.py @@ -16,12 +16,11 @@ import octobot_commons.constants as commons_constants import octobot_commons.enums as commons_enums - import octobot_commons.tentacles_management as tentacles_management import octobot_services.constants as services_constants import octobot_evaluators.evaluators as evaluators -from tentacles.Evaluator.Util.text_analysis import TextAnalysis import tentacles.Services.Services_feeds as Services_feeds +import tentacles.Evaluator.Util as EvaluatorUtil # disable inheritance to disable tentacle visibility. Disabled as starting from feb 9 2023, API is now paid only @@ -177,4 +176,64 @@ def _get_config_elements(self, config_cryptocurrencies, key): return {} async def prepare(self): - self.sentiment_analyser = tentacles_management.get_single_deepest_child_class(TextAnalysis)() + self.sentiment_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.TextAnalysis)() + + +NEWS_CONFIG_LANGUAGE = "language" + +# Should use any feed available to fetch crypto news (coindesk, etc.) +class CryptoNewsEvaluator(evaluators.SocialEvaluator): + SERVICE_FEED_CLASS = Services_feeds.CoindeskServiceFeed + + def __init__(self, tentacles_setup_config): + evaluators.SocialEvaluator.__init__(self, tentacles_setup_config) + self.stats_analyser = None + self.language = None + + def init_user_inputs(self, inputs: dict) -> None: + self.language = self.UI.user_input(NEWS_CONFIG_LANGUAGE, + commons_enums.UserInputTypes.TEXT, + self.language, inputs, + title="Language to use to fetch crypto news.", + options=["en", "fr"]) + self.feed_config = { + services_constants.CONFIG_COINDESK_TOPICS: [services_constants.COINDESK_TOPIC_NEWS], + services_constants.CONFIG_COINDESK_LANGUAGE: self.language + } + + @classmethod + def get_is_cryptocurrencies_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency dependant else False + """ + return True + + @classmethod + def get_is_cryptocurrency_name_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency name dependant else False + """ + return True + + async def _feed_callback(self, data): + if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]): + latest_news = self.get_data_cache(self.get_current_exchange_time(), key=services_constants.COINDESK_TOPIC_NEWS) + if latest_news is not None and len(latest_news) > 0: + sentiment_sum = 0 + news_count = 0 + for news in latest_news: + sentiment = news.sentiment + sentiment_sum += 0 if sentiment is None else -1 if sentiment == "NEGATIVE" else 1 if sentiment == "POSITIVE" else 0 + news_count += 1 + + if news_count > 0: + self.eval_note = sentiment_sum / news_count + await self.evaluation_completed(eval_time=self.get_current_exchange_time()) + else: + self.debug(f"No news found") + + def _is_interested_by_this_notification(self, notification_description): + return notification_description == services_constants.COINDESK_TOPIC_NEWS + + async def prepare(self): + self.sentiment_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.TextAnalysis)() diff --git a/Evaluator/Social/news_evaluator/resources/CryptoNewsEvaluator.md b/Evaluator/Social/news_evaluator/resources/CryptoNewsEvaluator.md new file mode 100644 index 000000000..4473083bf --- /dev/null +++ b/Evaluator/Social/news_evaluator/resources/CryptoNewsEvaluator.md @@ -0,0 +1,7 @@ +Analyzes overall crypto market sentiment through cryptocurrency news articles. + +This evaluator interprets aggregated news signals (e.g., article headlines, content, +and sentiment classifications) to produce a normalized score +indicating bullish or bearish market sentiment based on recent news coverage. + +Data source: ([CoinDesk Data API](https://developers.coindesk.com/documentation/data-api/news)) diff --git a/Evaluator/Social/sentiment_evaluator/__init__.py b/Evaluator/Social/sentiment_evaluator/__init__.py new file mode 100644 index 000000000..2bb7b4cd2 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/__init__.py @@ -0,0 +1 @@ +from .sentiment import FearAndGreedIndexEvaluator, SocialScoreEvaluator diff --git a/Evaluator/Social/sentiment_evaluator/config/FearAndGreedIndexEvaluator.json b/Evaluator/Social/sentiment_evaluator/config/FearAndGreedIndexEvaluator.json new file mode 100644 index 000000000..dcb8b2121 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/config/FearAndGreedIndexEvaluator.json @@ -0,0 +1,3 @@ +{ + "trend_averages" : [40, 30, 20, 15, 10] +} diff --git a/Evaluator/Social/sentiment_evaluator/config/SocialScoreEvaluator.json b/Evaluator/Social/sentiment_evaluator/config/SocialScoreEvaluator.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/config/SocialScoreEvaluator.json @@ -0,0 +1,2 @@ +{ +} diff --git a/Evaluator/Social/sentiment_evaluator/metadata.json b/Evaluator/Social/sentiment_evaluator/metadata.json new file mode 100644 index 000000000..bf8c5a831 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["FearAndGreedIndexEvaluator", "SocialScoreEvaluator"], + "tentacles-requirements": ["alternative_me_service_feed", "lunarcrush_service_feed"] +} \ No newline at end of file diff --git a/Evaluator/Social/sentiment_evaluator/resources/FearAndGreedIndexEvaluator.md b/Evaluator/Social/sentiment_evaluator/resources/FearAndGreedIndexEvaluator.md new file mode 100644 index 000000000..e15f7a803 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/resources/FearAndGreedIndexEvaluator.md @@ -0,0 +1,7 @@ +Analyzes overall crypto market sentiment through a Fear & Greed Index. + +This evaluator interprets aggregated market signals (e.g., volatility, volume/momentum, +social media sentiment, dominance, and trends) to produce a normalized score +indicating prevailing fear or greed. + +Data source: ([alternative.me](https://alternative.me/crypto/fear-and-greed-index/)) \ No newline at end of file diff --git a/Evaluator/Social/sentiment_evaluator/resources/SocialScoreEvaluator.md b/Evaluator/Social/sentiment_evaluator/resources/SocialScoreEvaluator.md new file mode 100644 index 000000000..fa45b13a8 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/resources/SocialScoreEvaluator.md @@ -0,0 +1,7 @@ +Analyzes cryptocurrency-specific social sentiment through LunarCrush social metrics. + +This evaluator interprets aggregated social signals (e.g., social volume, social engagement, +social dominance, and community sentiment) to produce a normalized score +indicating bullish or bearish social sentiment for a specific cryptocurrency. + +Data source: ([LunarCrush](https://lunarcrush.com/faq/what-metrics-are-available-on-lunarcrush)) diff --git a/Evaluator/Social/sentiment_evaluator/sentiment.py b/Evaluator/Social/sentiment_evaluator/sentiment.py new file mode 100644 index 000000000..80a334f18 --- /dev/null +++ b/Evaluator/Social/sentiment_evaluator/sentiment.py @@ -0,0 +1,111 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import octobot_commons.enums as commons_enums +import octobot_commons.tentacles_management as tentacles_management +import octobot_evaluators.evaluators as evaluators +import octobot_services.constants as services_constants +import tentacles.Evaluator.Util as EvaluatorUtil +import tentacles.Services.Services_feeds as Services_feeds + +CONFIG_TREND_AVERAGES = "trend_averages" + +class FearAndGreedIndexEvaluator(evaluators.SocialEvaluator): + SERVICE_FEED_CLASS = Services_feeds.AlternativeMeServiceFeed + + def __init__(self, tentacles_setup_config): + evaluators.SocialEvaluator.__init__(self, tentacles_setup_config) + self.stats_analyser = None + self.history_data = None + self.feed_config = { + services_constants.CONFIG_ALTERNATIVE_ME_TOPICS: [services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED] + } + self.trend_averages = [40, 30, 20, 15, 10] + + def init_user_inputs(self, inputs: dict) -> None: + self.trend_averages = self.UI.user_input(CONFIG_TREND_AVERAGES, + commons_enums.UserInputTypes.OBJECT_ARRAY, + self.trend_averages, inputs, + title="Averages to use to compute the trend evaluation.") + + @classmethod + def get_is_cryptocurrencies_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency dependant else False + """ + return True + + @classmethod + def get_is_cryptocurrency_name_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency name dependant else False + """ + return True + + async def _feed_callback(self, data): + if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]): + fear_and_greed_history = self.get_data_cache(self.get_current_exchange_time(), key=services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED) + if fear_and_greed_history is not None and len(fear_and_greed_history) > 0: + fear_and_greed_history_values = [item.value for item in fear_and_greed_history] + self.eval_note = self.stats_analyser.get_trend(fear_and_greed_history_values, self.trend_averages) + await self.evaluation_completed(eval_time=self.get_current_exchange_time()) + + def _is_interested_by_this_notification(self, notification_description): + return notification_description == services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED + + async def prepare(self): + self.stats_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.TrendAnalysis)() + +class SocialScoreEvaluator(evaluators.SocialEvaluator): + SERVICE_FEED_CLASS = Services_feeds.LunarCrushServiceFeed + + def __init__(self, tentacles_setup_config): + evaluators.SocialEvaluator.__init__(self, tentacles_setup_config) + self.stats_analyser = None + + def init_user_inputs(self, inputs: dict) -> None: + self.feed_config = { + services_constants.CONFIG_LUNARCRUSH_COINS: [self.cryptocurrency] + } + + @classmethod + def get_is_cryptocurrencies_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency dependant else False + """ + return False + + @classmethod + def get_is_cryptocurrency_name_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency name dependant else False + """ + return False + + async def _feed_callback(self, data): + if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]): + coin, _ = data[services_constants.FEED_METADATA].split(";") + coin_data = self.get_data_cache(self.get_current_exchange_time(), key=f"{coin};{services_constants.LUNARCRUSH_COIN_METRICS}") + if coin_data is not None and len(coin_data) > 0: + self.eval_note = coin_data[-1].sentiment + await self.evaluation_completed(cryptocurrency=self.cryptocurrency, eval_time=self.get_current_exchange_time()) + + def _is_interested_by_this_notification(self, notification_description): + try: + coin, topic = notification_description.split(";") + return coin == self.cryptocurrency and topic == services_constants.LUNARCRUSH_COIN_METRICS + except KeyError: + pass + return False diff --git a/Evaluator/Social/trends_evaluator/__init__.py b/Evaluator/Social/trends_evaluator/__init__.py index 70be43c7b..f4ce9d300 100644 --- a/Evaluator/Social/trends_evaluator/__init__.py +++ b/Evaluator/Social/trends_evaluator/__init__.py @@ -1 +1 @@ -from .trends import GoogleTrendsEvaluator \ No newline at end of file +from .trends import GoogleTrendsEvaluator, MarketCapEvaluator \ No newline at end of file diff --git a/Evaluator/Social/trends_evaluator/config/MarketCapEvaluator.json b/Evaluator/Social/trends_evaluator/config/MarketCapEvaluator.json new file mode 100644 index 000000000..582e79d70 --- /dev/null +++ b/Evaluator/Social/trends_evaluator/config/MarketCapEvaluator.json @@ -0,0 +1,4 @@ +{ + "trend_averages" : [40, 30, 20, 15, 10] + } + \ No newline at end of file diff --git a/Evaluator/Social/trends_evaluator/metadata.json b/Evaluator/Social/trends_evaluator/metadata.json index 83b1a83fb..79f3e902a 100644 --- a/Evaluator/Social/trends_evaluator/metadata.json +++ b/Evaluator/Social/trends_evaluator/metadata.json @@ -1,6 +1,6 @@ { "version": "1.2.0", "origin_package": "OctoBot-Default-Tentacles", - "tentacles": ["GoogleTrendsEvaluator"], - "tentacles-requirements": ["statistics_analysis", "google_service_feed"] + "tentacles": ["GoogleTrendsEvaluator", "MarketCapEvaluator"], + "tentacles-requirements": ["statistics_analysis", "google_service_feed", "coindesk_service_feed"] } \ No newline at end of file diff --git a/Evaluator/Social/trends_evaluator/resources/MarketCapEvaluator.md b/Evaluator/Social/trends_evaluator/resources/MarketCapEvaluator.md new file mode 100644 index 000000000..1bcadca7e --- /dev/null +++ b/Evaluator/Social/trends_evaluator/resources/MarketCapEvaluator.md @@ -0,0 +1,7 @@ +Analyzes overall crypto market trends through total market capitalization data. + +This evaluator interprets aggregated market cap signals (e.g., historical market cap values, +trend changes, and long-term averages) to produce a normalized score +indicating bullish or bearish market trends based on capitalization movements. + +Data source: ([CoinDesk Data API](https://developers.coindesk.com/documentation/data-api/overview_v1_latest_marketcap_all_tick)) diff --git a/Evaluator/Social/trends_evaluator/trends.py b/Evaluator/Social/trends_evaluator/trends.py index 88afc23c2..797ac475f 100644 --- a/Evaluator/Social/trends_evaluator/trends.py +++ b/Evaluator/Social/trends_evaluator/trends.py @@ -23,7 +23,6 @@ import tentacles.Evaluator.Util as EvaluatorUtil import tentacles.Services.Services_feeds as Services_feeds - class GoogleTrendsEvaluator(evaluators.SocialEvaluator): SERVICE_FEED_CLASS = Services_feeds.GoogleServiceFeed @@ -81,3 +80,50 @@ def _build_trend_topics(self): async def prepare(self): self.stats_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.StatisticAnalysis)() + +CONFIG_TREND_AVERAGES = "trend_averages" + +class MarketCapEvaluator(evaluators.SocialEvaluator): + SERVICE_FEED_CLASS = Services_feeds.CoindeskServiceFeed + + def __init__(self, tentacles_setup_config): + evaluators.SocialEvaluator.__init__(self, tentacles_setup_config) + self.stats_analyser = None + self.feed_config = { + services_constants.CONFIG_COINDESK_TOPICS: [services_constants.COINDESK_TOPIC_MARKETCAP] + } + self.trend_averages = [40, 30, 20, 15, 10] + + def init_user_inputs(self, inputs: dict) -> None: + self.trend_averages = self.UI.user_input(CONFIG_TREND_AVERAGES, + commons_enums.UserInputTypes.OBJECT_ARRAY, + self.trend_averages, inputs, + title="Averages to use to compute the trend evaluation.") + + @classmethod + def get_is_cryptocurrencies_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency dependant else False + """ + return True + + @classmethod + def get_is_cryptocurrency_name_wildcard(cls) -> bool: + """ + :return: True if the evaluator is not cryptocurrency name dependant else False + """ + return True + + async def _feed_callback(self, data): + if self._is_interested_by_this_notification(data[services_constants.FEED_METADATA]): + marketcap_data = self.get_data_cache(self.get_current_exchange_time(), key=services_constants.COINDESK_TOPIC_MARKETCAP) + if marketcap_data is not None and len(marketcap_data) > 0: + marketcap_history = [item.close for item in marketcap_data] + self.eval_note = self.stats_analyser.get_trend(marketcap_history, self.trend_averages) + await self.evaluation_completed(eval_time=self.get_current_exchange_time()) + + def _is_interested_by_this_notification(self, notification_description): + return notification_description == services_constants.COINDESK_TOPIC_MARKETCAP + + async def prepare(self): + self.stats_analyser = tentacles_management.get_single_deepest_child_class(EvaluatorUtil.TrendAnalysis)() diff --git a/Evaluator/Strategies/mixed_strategies_evaluator/mixed_strategies.py b/Evaluator/Strategies/mixed_strategies_evaluator/mixed_strategies.py index c12ab99e2..9e906548e 100644 --- a/Evaluator/Strategies/mixed_strategies_evaluator/mixed_strategies.py +++ b/Evaluator/Strategies/mixed_strategies_evaluator/mixed_strategies.py @@ -78,7 +78,9 @@ def init_user_inputs(self, inputs: dict) -> None: default_config[self.BACKGROUND_SOCIAL_EVALUATORS], inputs, other_schema_values={"minItems": 0, "uniqueItems": True}, options=["RedditForumEvaluator", "TwitterNewsEvaluator", - "TelegramSignalEvaluator", "GoogleTrendsEvaluator"], + "TelegramSignalEvaluator", "GoogleTrendsEvaluator", + "FearAndGreedIndexEvaluator", "SocialScoreEvaluator", + "CryptoNewsEvaluator", "MarketCapEvaluator"], title="Social evaluator to consider as background evaluators: they won't trigger technical " "evaluators re-evaluation when updated. Avoiding unnecessary updates increases " "performances.") diff --git a/Services/Services_bases/alternative_me_service/__init__.py b/Services/Services_bases/alternative_me_service/__init__.py new file mode 100644 index 000000000..1331b1a8c --- /dev/null +++ b/Services/Services_bases/alternative_me_service/__init__.py @@ -0,0 +1 @@ +from .alternative_me import AlternativeMeService \ No newline at end of file diff --git a/Services/Services_bases/alternative_me_service/alternative_me.py b/Services/Services_bases/alternative_me_service/alternative_me.py new file mode 100644 index 000000000..13d9ba186 --- /dev/null +++ b/Services/Services_bases/alternative_me_service/alternative_me.py @@ -0,0 +1,42 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import octobot_services.constants as services_constants +import octobot_services.services as services + + +class AlternativeMeService(services.AbstractService): + @staticmethod + def is_setup_correctly(config): + return True + + @staticmethod + def get_is_enabled(config): + return True + + def has_required_configuration(self): + return True + + def get_endpoint(self) -> None: + return None + + def get_type(self) -> None: + return services_constants.CONFIG_ALTERNATIVE_ME + + async def prepare(self) -> None: + pass + + def get_successful_startup_message(self): + return "", True diff --git a/Services/Services_bases/alternative_me_service/metadata.json b/Services/Services_bases/alternative_me_service/metadata.json new file mode 100644 index 000000000..826dca428 --- /dev/null +++ b/Services/Services_bases/alternative_me_service/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["AlternativeMeService"], + "tentacles-requirements": [] +} \ No newline at end of file diff --git a/Services/Services_bases/coindesk_service/__init__.py b/Services/Services_bases/coindesk_service/__init__.py new file mode 100644 index 000000000..daffc1144 --- /dev/null +++ b/Services/Services_bases/coindesk_service/__init__.py @@ -0,0 +1 @@ +from .coindesk import CoindeskService \ No newline at end of file diff --git a/Services/Services_bases/coindesk_service/coindesk.py b/Services/Services_bases/coindesk_service/coindesk.py new file mode 100644 index 000000000..c1f5b74a7 --- /dev/null +++ b/Services/Services_bases/coindesk_service/coindesk.py @@ -0,0 +1,42 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import octobot_services.constants as services_constants +import octobot_services.services as services + + +class CoindeskService(services.AbstractService): + @staticmethod + def is_setup_correctly(config): + return True + + @staticmethod + def get_is_enabled(config): + return True + + def has_required_configuration(self): + return True + + def get_endpoint(self) -> None: + return None + + def get_type(self) -> None: + return services_constants.CONFIG_COINDESK + + async def prepare(self) -> None: + pass + + def get_successful_startup_message(self): + return "", True diff --git a/Services/Services_bases/coindesk_service/metadata.json b/Services/Services_bases/coindesk_service/metadata.json new file mode 100644 index 000000000..cf90b334c --- /dev/null +++ b/Services/Services_bases/coindesk_service/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["CoindeskService"], + "tentacles-requirements": [] +} \ No newline at end of file diff --git a/Services/Services_bases/lunarcrush_service/__init__.py b/Services/Services_bases/lunarcrush_service/__init__.py new file mode 100644 index 000000000..4d81dc242 --- /dev/null +++ b/Services/Services_bases/lunarcrush_service/__init__.py @@ -0,0 +1 @@ +from .lunarcrush import LunarCrushService \ No newline at end of file diff --git a/Services/Services_bases/lunarcrush_service/lunarcrush.py b/Services/Services_bases/lunarcrush_service/lunarcrush.py new file mode 100644 index 000000000..d72984ae5 --- /dev/null +++ b/Services/Services_bases/lunarcrush_service/lunarcrush.py @@ -0,0 +1,62 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import typing +import octobot_services.constants as services_constants +import octobot_services.services as services + + +class LunarCrushService(services.AbstractService): + @staticmethod + def is_setup_correctly(config): + return True + + @staticmethod + def get_is_enabled(config): + return True + + def has_required_configuration(self): + return True + + def get_endpoint(self) -> None: + return None + + def get_type(self) -> None: + return services_constants.CONFIG_LUNARCRUSH + + async def prepare(self) -> None: + pass + + def get_successful_startup_message(self): + return "", True + + def get_fields_description(self): + return { + services_constants.CONFIG_LUNARCRUSH_API_KEY: "Api key.", + } + + def get_default_value(self): + return { + services_constants.CONFIG_LUNARCRUSH_API_KEY: "" + } + + def get_required_config(self): + return [services_constants.CONFIG_LUNARCRUSH_API_KEY] + + def get_authentication_headers(self) -> typing.Optional[dict]: + api_key = self.config[services_constants.CONFIG_CATEGORY_SERVICES].get(services_constants.CONFIG_LUNARCRUSH, {}).get(services_constants.CONFIG_LUNARCRUSH_API_KEY, None) + return { + "Authorization": f"Bearer {api_key}" + } if api_key else None diff --git a/Services/Services_bases/lunarcrush_service/metadata.json b/Services/Services_bases/lunarcrush_service/metadata.json new file mode 100644 index 000000000..211d51cba --- /dev/null +++ b/Services/Services_bases/lunarcrush_service/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["LunarCrushService"], + "tentacles-requirements": [] +} \ No newline at end of file diff --git a/Services/Services_feeds/alternative_me_service_feed/__init__.py b/Services/Services_feeds/alternative_me_service_feed/__init__.py new file mode 100644 index 000000000..7a15f5cb8 --- /dev/null +++ b/Services/Services_feeds/alternative_me_service_feed/__init__.py @@ -0,0 +1,2 @@ +from .alternative_me_feed import AlternativeMeServiceFeed +from .alternative_me_feed import AlternativeMeFearAndGreed diff --git a/Services/Services_feeds/alternative_me_service_feed/alternative_me_feed.py b/Services/Services_feeds/alternative_me_service_feed/alternative_me_feed.py new file mode 100644 index 000000000..fdcb9111e --- /dev/null +++ b/Services/Services_feeds/alternative_me_service_feed/alternative_me_feed.py @@ -0,0 +1,136 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import asyncio +import aiohttp +import typing +import dataclasses +import datetime + +import octobot_commons.enums as commons_enums +import octobot_commons.constants as commons_constants +import octobot_services.channel as services_channel +import octobot_services.constants as services_constants +import octobot_services.service_feeds as service_feeds +import tentacles.Services.Services_bases as Services_bases + + +class AlternativeMeServiceFeedChannel(services_channel.AbstractServiceFeedChannel): + pass + + +@dataclasses.dataclass +class AlternativeMeFearAndGreed: + timestamp: float + value: float + value_classification: str + +class AlternativeMeServiceFeed(service_feeds.AbstractServiceFeed): + FEED_CHANNEL = AlternativeMeServiceFeedChannel + REQUIRED_SERVICES = [Services_bases.AlternativeMeService] + + API_RATE_LIMIT_SECONDS = 10 + + def __init__(self, config, main_async_loop, bot_id): + super().__init__(config, main_async_loop, bot_id) + self.alternative_me_topics = [] + self.data_cache = {} + self.refresh_time_frame = commons_enums.TimeFrames.ONE_DAY + self.listener_task = None + + # merge new config into existing config + def update_feed_config(self, config): + self.alternative_me_topics.extend(topic + for topic in config.get(services_constants.CONFIG_ALTERNATIVE_ME_TOPICS, []) + if topic not in self.alternative_me_topics) + self.refresh_time_frame = config.get(services_constants.CONFIG_ALTERNATIVE_ME_REFRESH_TIME_FRAME, commons_enums.TimeFrames.ONE_DAY) + + def _initialize(self): + pass # Nothing to do + + def _something_to_watch(self): + return bool(self.alternative_me_topics) + + def _get_sleep_time_before_next_wakeup(self): + return commons_enums.TimeFramesMinutes[self.refresh_time_frame] * commons_constants.MINUTE_TO_SECONDS + + async def _get_fear_and_greed_data(self, session: aiohttp.ClientSession, limit: typing.Optional[int] = 100) -> bool: + api_url = f"https://api.alternative.me/fng/?limit={limit}&format=json&date_format=us" + async with session.get(api_url) as response: + if response.status != 200: + self.logger.error(f"Alternative.me API request failed with status: {response.status}") + return False + + fear_and_greed_data = await response.json() + data = fear_and_greed_data["data"] + self.data_cache[services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED] = [ + AlternativeMeFearAndGreed( + timestamp=datetime.datetime.strptime(entry["timestamp"], '%m-%d-%Y').timestamp(), + value=float(entry["value"]), + value_classification=entry["value_classification"] + ) for entry in data] + return True + + def get_data_cache(self, current_time: float, key: typing.Optional[str] = None): + if self.data_cache is None: + return None + + if key is None: + return self.data_cache + + if key == services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED and self.data_cache.get(services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED) is not None: + return [item for item in self.data_cache.get(services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED) if item.timestamp <= current_time] + return None + + async def _push_update_and_wait(self, session: aiohttp.ClientSession): + for topic in self.alternative_me_topics: + self.logger.debug(f"Fetching alternative.me {topic} topic data...") + result = False + if topic == services_constants.ALTERNATIVE_ME_TOPIC_FEAR_AND_GREED: + result = await self._get_fear_and_greed_data(session) + + if result: + await self._async_notify_consumers( + { + services_constants.FEED_METADATA: topic, + } + ) + await asyncio.sleep(self.API_RATE_LIMIT_SECONDS) + await asyncio.sleep(self._get_sleep_time_before_next_wakeup()) + + async def _update_loop(self): + async with aiohttp.ClientSession() as session: + while not self.should_stop: + try: + await self._push_update_and_wait(session) + except Exception as e: + self.logger.exception(e, True, f"Error when receiving Alternative.me data: ({e})") + self.should_stop = True + return False + + async def _start_service_feed(self): + try: + self.listener_task = asyncio.create_task(self._update_loop()) + return True + except Exception as e: + self.logger.exception(e, True, f"Error when initializing Alternative.me feed: {e}") + return False + + async def stop(self): + await super().stop() + if self.listener_task is not None: + self.listener_task.cancel() + self.listener_task = None + diff --git a/Services/Services_feeds/alternative_me_service_feed/metadata.json b/Services/Services_feeds/alternative_me_service_feed/metadata.json new file mode 100644 index 000000000..ca5b23c10 --- /dev/null +++ b/Services/Services_feeds/alternative_me_service_feed/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["AlternativeMeServiceFeed"], + "tentacles-requirements": ["alternative_me_service"] +} \ No newline at end of file diff --git a/Services/Services_feeds/coindesk_service_feed/__init__.py b/Services/Services_feeds/coindesk_service_feed/__init__.py new file mode 100644 index 000000000..8ceaf4bb0 --- /dev/null +++ b/Services/Services_feeds/coindesk_service_feed/__init__.py @@ -0,0 +1,3 @@ +from .coindesk_feed import CoindeskServiceFeed +from .coindesk_feed import CoindeskNews +from .coindesk_feed import CoindeskMarketcap diff --git a/Services/Services_feeds/coindesk_service_feed/coindesk_feed.py b/Services/Services_feeds/coindesk_service_feed/coindesk_feed.py new file mode 100644 index 000000000..5ac383bcc --- /dev/null +++ b/Services/Services_feeds/coindesk_service_feed/coindesk_feed.py @@ -0,0 +1,227 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import asyncio +import aiohttp +import typing +import datetime +import dataclasses + +import octobot_commons.enums as commons_enums +import octobot_commons.constants as commons_constants +import octobot_services.channel as services_channel +import octobot_services.constants as services_constants +import octobot_services.service_feeds as service_feeds +import tentacles.Services.Services_bases as Services_bases + + +class CoindeskServiceFeedChannel(services_channel.AbstractServiceFeedChannel): + pass + + +@dataclasses.dataclass +class CoindeskNews: + id: str + guid: str + published_on: datetime.datetime + image_url: str + title: str + url: str + source_id: str + body: str + keywords: str + lang: str + upvotes: int + downvotes: int + score: int + sentiment: str # POSITIVE, NEGATIVE, NEUTRAL + status: str + source_name: str + source_key: str + source_url: str + source_lang: str + source_type: str + categories: str + +@dataclasses.dataclass +class CoindeskMarketcap: + timestamp: datetime.datetime + open: float + close: float + high: float + low: float + top_tier_volume: float + +class CoindeskServiceFeed(service_feeds.AbstractServiceFeed): + FEED_CHANNEL = CoindeskServiceFeedChannel + REQUIRED_SERVICES = [Services_bases.CoindeskService] + + API_RATE_LIMIT_SECONDS = 10 + + def __init__(self, config, main_async_loop, bot_id): + super().__init__(config, main_async_loop, bot_id) + self.coindesk_api_key = config.get(services_constants.CONFIG_COINDESK_API_KEY, None) + self.coindesk_language = config.get(services_constants.CONFIG_COINDESK_LANGUAGE, "en") + self.coindesk_topics = [] + self.data_cache = {} + self.refresh_time_frame = commons_enums.TimeFrames.ONE_DAY + self.listener_task = None + + # merge new config into existing config + def update_feed_config(self, config): + self.coindesk_topics.extend(topic + for topic in config.get(services_constants.CONFIG_COINDESK_TOPICS, []) + if topic not in self.coindesk_topics) + self.refresh_time_frame = config.get(services_constants.CONFIG_COINDESK_REFRESH_TIME_FRAME, commons_enums.TimeFrames.ONE_DAY) + self.coindesk_language = config.get(services_constants.CONFIG_COINDESK_LANGUAGE, "en") + + def _initialize(self): + pass # Nothing to do + + def _something_to_watch(self): + return bool(self.coindesk_topics) + + def _get_sleep_time_before_next_wakeup(self): + return commons_enums.TimeFramesMinutes[self.refresh_time_frame] * commons_constants.MINUTE_TO_SECONDS + + def _get_marketcap_api_url(self, limit: typing.Optional[int] = 2000): + return f"https://data-api.coindesk.com/overview/v1/historical/marketcap/all/assets/days?limit={limit}&response_format=JSON" + + async def _get_marketcap_data(self, session: aiohttp.ClientSession) -> bool: + async with session.get(self._get_marketcap_api_url()) as response: + if response.status != 200: + self.logger.error(f"Coindesk API request failed with status: {response.status}") + return False + + market_cap_data = await response.json() + + # We should append and check duplicates + self.data_cache[services_constants.COINDESK_TOPIC_MARKETCAP] = [ + CoindeskMarketcap( + timestamp=entry["TIMESTAMP"], + open=entry["OPEN"], + close=entry["CLOSE"], + high=entry["HIGH"], + low=entry["LOW"], + top_tier_volume=entry["TOP_TIER_VOLUME"] + ) for entry in market_cap_data["Data"] + ] + return True + + + def _get_news_api_url(self, limit: typing.Optional[int] = 10): + return f"https://data-api.coindesk.com/news/v1/article/list?lang={self.coindesk_language}&limit={limit}" + + async def _get_news_data(self, session: aiohttp.ClientSession) -> bool: + async with session.get(self._get_news_api_url()) as response: + if response.status != 200: + self.logger.error(f"API request failed with status: {response.status}") + return False + + news_data = await response.json() + articles = news_data.get("Data", []) + + if not articles: + self.logger.error("No articles found in API response") + return False + + values = [] + for article in articles: + source_data = article.get("SOURCE_DATA", {}) + category_data = article.get("CATEGORY_DATA", []) + categories_str = str([cat["NAME"] for cat in category_data]) + + values.append(CoindeskNews( + id=article["ID"], + guid=article["GUID"], + published_on=article["PUBLISHED_ON"], + image_url=article.get("IMAGE_URL", ""), + title=article["TITLE"], + url=article["URL"], + source_id=article["SOURCE_ID"], + body=article.get("BODY", ""), + keywords=article.get("KEYWORDS", ""), + lang=article["LANG"], + upvotes=article.get("UPVOTES", 0), + downvotes=article.get("DOWNVOTES", 0), + score=article.get("SCORE", 0), + sentiment=article.get("SENTIMENT", ""), + status=article.get("STATUS", "ACTIVE"), + source_name=source_data.get("NAME", ""), + source_key=source_data.get("SOURCE_KEY", ""), + source_url=source_data.get("URL", ""), + source_lang=source_data.get("LANG", ""), + source_type=source_data.get("SOURCE_TYPE", ""), + categories=categories_str + )) + + # We should append and check duplicates + self.data_cache[services_constants.COINDESK_TOPIC_NEWS] = values + return True + + def get_data_cache(self, current_time: float, key: typing.Optional[str] = None): + if self.data_cache is None: + return None + + if key is None: + return self.data_cache + + if key == services_constants.COINDESK_TOPIC_NEWS and self.data_cache.get(services_constants.COINDESK_TOPIC_NEWS) is not None: + return [item for item in self.data_cache.get(services_constants.COINDESK_TOPIC_NEWS) if item.published_on <= current_time] + elif key == services_constants.COINDESK_TOPIC_MARKETCAP and self.data_cache.get(services_constants.COINDESK_TOPIC_MARKETCAP) is not None: + return [item for item in self.data_cache.get(services_constants.COINDESK_TOPIC_MARKETCAP) if item.timestamp <= current_time] + return None + + async def _push_update_and_wait(self, session: aiohttp.ClientSession): + for topic in self.coindesk_topics: + self.logger.debug(f"Fetching coindesk {topic} topic data...") + result = False + if topic == services_constants.COINDESK_TOPIC_NEWS: + result = await self._get_news_data(session) + elif topic == services_constants.COINDESK_TOPIC_MARKETCAP: + result = await self._get_marketcap_data(session) + + if result: + await self._async_notify_consumers( + { + services_constants.FEED_METADATA: topic, + } + ) + await asyncio.sleep(self.API_RATE_LIMIT_SECONDS) + await asyncio.sleep(self._get_sleep_time_before_next_wakeup()) + + async def _update_loop(self): + async with aiohttp.ClientSession() as session: + while not self.should_stop: + try: + await self._push_update_and_wait(session) + except Exception as e: + self.logger.exception(e, True, f"Error when receiving Coindesk feed: ({e})") + self.should_stop = True + return False + + async def _start_service_feed(self): + try: + self.listener_task = asyncio.create_task(self._update_loop()) + return True + except Exception as e: + self.logger.exception(e, True, f"Error when initializing Coindesk feed: {e}") + return False + + async def stop(self): + await super().stop() + if self.listener_task is not None: + self.listener_task.cancel() + self.listener_task = None diff --git a/Services/Services_feeds/coindesk_service_feed/metadata.json b/Services/Services_feeds/coindesk_service_feed/metadata.json new file mode 100644 index 000000000..c332735a0 --- /dev/null +++ b/Services/Services_feeds/coindesk_service_feed/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["CoindeskServiceFeed"], + "tentacles-requirements": ["coindesk_service"] +} \ No newline at end of file diff --git a/Services/Services_feeds/google_service_feed/google_feed.py b/Services/Services_feeds/google_service_feed/google_feed.py index b2d6d107b..7ae8d9c15 100644 --- a/Services/Services_feeds/google_service_feed/google_feed.py +++ b/Services/Services_feeds/google_service_feed/google_feed.py @@ -56,6 +56,7 @@ def __init__(self, config, main_async_loop, bot_id): super().__init__(config, main_async_loop, bot_id) self.trends_req_builder = None self.trends_topics = [] + self.listener_task = None def _initialize(self): # if the url changes (google sometimes changes it), use the following line: @@ -116,8 +117,15 @@ async def _update_loop(self): async def _start_service_feed(self): try: - asyncio.create_task(self._update_loop()) + self.listener_task = asyncio.create_task(self._update_loop()) + return True except Exception as e: self.logger.exception(e, True, f"Error when initializing Google trends feed: {e}") return False - return True + + async def stop(self): + await super().stop() + if self.listener_task is not None: + self.listener_task.cancel() + self.listener_task = None + diff --git a/Services/Services_feeds/lunarcrush_service_feed/__init__.py b/Services/Services_feeds/lunarcrush_service_feed/__init__.py new file mode 100644 index 000000000..fd3dc3774 --- /dev/null +++ b/Services/Services_feeds/lunarcrush_service_feed/__init__.py @@ -0,0 +1 @@ +from .lunarcrush_feed import LunarCrushServiceFeed diff --git a/Services/Services_feeds/lunarcrush_service_feed/lunarcrush_feed.py b/Services/Services_feeds/lunarcrush_service_feed/lunarcrush_feed.py new file mode 100644 index 000000000..e33c44c2f --- /dev/null +++ b/Services/Services_feeds/lunarcrush_service_feed/lunarcrush_feed.py @@ -0,0 +1,151 @@ +# Drakkar-Software OctoBot-Tentacles +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. +import asyncio +import datetime +import aiohttp +import json +import dataclasses +import typing + +import octobot_commons.enums as commons_enums +import octobot_commons.constants as commons_constants +import octobot_commons.dataclasses as commons_dataclasses +import octobot_services.channel as services_channel +import octobot_services.constants as services_constants +import octobot_services.service_feeds as service_feeds +import tentacles.Services.Services_bases as Services_bases + + +class LunarCrushServiceFeedChannel(services_channel.AbstractServiceFeedChannel): + pass + + +@dataclasses.dataclass +class LunarCrushCoinMetrics(commons_dataclasses.FlexibleDataclass): + time: int # A unix timestamp (in seconds) + contributors_active: int # number of unique social accounts with posts that have interactions + contributors_created: int # number of unique social accounts that created new posts + interactions: int # number of all publicly measurable interactions on a social post (views, likes, comments, thumbs up, upvote, share etc) + posts_active: int # number of unique social posts with interactions + posts_created: int # number of unique social posts created + sentiment: float # % of posts (weighted by interactions) that are positive. 100% means all posts are positive, 50% is half positive and half negative, and 0% is all negative posts. + spam: int # The number of posts created that are considered spam + alt_rank: float # A proprietary score based on how an asset is performing relative to all other assets supported + circulating_supply: int # Circulating Supply is the total number of coins or tokens that are actively available for trade and are being used in the market and in general public + close: float # Close price for the time period + galaxy_score: float # A proprietary score based on technical indicators of price, average social sentiment, relative social activity, and a factor of how closely social indicators correlate with price and volume + high: float # Higest price fo rthe time period + low: float # Lowest price for the time period + market_cap: int # Total dollar market value of all circulating supply or outstanding shares + market_dominance: float # The percent of the total market cap that this asset represents + open: float # Open price for the time period + social_dominance: float # The percent of the total social volume that this topic represents + volume_24h: float # Volume in USD for 24 hours up to this data point + +class LunarCrushServiceFeed(service_feeds.AbstractServiceFeed): + FEED_CHANNEL = LunarCrushServiceFeedChannel + REQUIRED_SERVICES = [Services_bases.LunarCrushService] + + def __init__(self, config, main_async_loop, bot_id): + super().__init__(config, main_async_loop, bot_id) + self.lunarcrush_coins = [] + self.data_cache = {} + self.refresh_time_frame = commons_enums.TimeFrames.ONE_DAY + self.listener_task = None + + # merge new config into existing config + def update_feed_config(self, config): + self.lunarcrush_coins.extend(coin + for coin in config.get(services_constants.CONFIG_LUNARCRUSH_COINS, []) + if coin not in self.lunarcrush_coins) + self.refresh_time_frame = config.get(services_constants.CONFIG_LUNARCRUSH_REFRESH_TIME_FRAME, commons_enums.TimeFrames.ONE_DAY) + + def _initialize(self): + pass # Nothing to do + + def _something_to_watch(self): + return bool(self.lunarcrush_coins) + + def _get_sleep_time_before_next_wakeup(self): + return commons_enums.TimeFramesMinutes[self.refresh_time_frame] * commons_constants.MINUTE_TO_SECONDS + + def get_data_cache(self, current_time: float, key: typing.Optional[str] = None): + if self.data_cache is None: + return None + + if key is None: + return self.data_cache + + coin, topic = key.split(";") + if topic == services_constants.LUNARCRUSH_COIN_METRICS and self.data_cache.get(services_constants.LUNARCRUSH_COIN_METRICS) is not None: + return [item for item in self.data_cache.get(services_constants.LUNARCRUSH_COIN_METRICS).get(coin) if item.time <= current_time] + return None + + async def _get_coin_data(self, session: aiohttp.ClientSession, coin: str, start_date: datetime.datetime, end_date: datetime.datetime) -> bool: + self.logger.debug(f"Getting lunarcrush coin data for {coin} from {start_date.timestamp()} to {end_date.timestamp()}...") + api_url = f"https://lunarcrush.com/api3/public/coins/{coin}/time-series/v2?bucket=day&interval=1d&start={int(start_date.timestamp())}&end={int(end_date.timestamp())}" + async with session.get(api_url) as response: + if response.status != 200: + self.logger.error(f"Lunarcrush API request failed with status: {response.status}") + return False + + coin_metrics_data = await response.json() + data = coin_metrics_data["data"] + self.data_cache[services_constants.LUNARCRUSH_COIN_METRICS] = { + coin: [ + LunarCrushCoinMetrics.from_dict(entry) + for entry in data + ] + } + return True + + async def _push_update_and_wait(self, session: aiohttp.ClientSession): + end_date = datetime.datetime.now(datetime.timezone.utc) + start_date = end_date - datetime.timedelta(days=30) + for coin in self.lunarcrush_coins: + result = await self._get_coin_data(session, coin, start_date, end_date) + if result: + await self._async_notify_consumers( + { + services_constants.FEED_METADATA: f"{coin};{services_constants.LUNARCRUSH_COIN_METRICS}", + } + ) + await asyncio.sleep(self._get_sleep_time_before_next_wakeup()) + + async def _update_loop(self): + async with aiohttp.ClientSession(headers=self.services[0].get_authentication_headers()) as session: + while not self.should_stop: + try: + await self._push_update_and_wait(session) + except Exception as e: + self.logger.exception(e, True, f"Error when receiving LunarCrush data: ({e})") + self.should_stop = True + return False + + async def _start_service_feed(self): + try: + self.listener_task = asyncio.create_task(self._update_loop()) + return True + except Exception as e: + self.logger.exception(e, True, f"Error when initializing LunarCrush feed: {e}") + return False + + async def stop(self): + await super().stop() + if self.listener_task is not None: + self.listener_task.cancel() + self.listener_task = None + diff --git a/Services/Services_feeds/lunarcrush_service_feed/metadata.json b/Services/Services_feeds/lunarcrush_service_feed/metadata.json new file mode 100644 index 000000000..ad7c73f8c --- /dev/null +++ b/Services/Services_feeds/lunarcrush_service_feed/metadata.json @@ -0,0 +1,6 @@ +{ + "version": "1.2.0", + "origin_package": "OctoBot-Default-Tentacles", + "tentacles": ["LunarCrushServiceFeed"], + "tentacles-requirements": ["lunarcrush_service"] +} \ No newline at end of file