Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.8.0] - 2026-02-02
### Added
- pydantic to requirements
- add AbstractAIService
- add AbstractWebSearchService
- MCP server interface
### Updated
- OpenAI dependency

## [1.7.3] - 2026-01-29
### Added
- Tavily service constants
Expand Down Expand Up @@ -63,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.6.21] - 2023-10-21
### Added
[Constant] add CONIG_LLM_CUSTOM_BASE_URL
[Constant] add CONFIG_LLM_CUSTOM_BASE_URL

## [1.6.20] - 2023-10-07
### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# OctoBot-Services [1.7.3](https://github.com/Drakkar-Software/OctoBot-Services/tree/master/docs/CHANGELOG.md)
# OctoBot-Services [1.8.0](https://github.com/Drakkar-Software/OctoBot-Services/tree/master/docs/CHANGELOG.md)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/31a1caa6e5384d80bf890dba5c9b5e4b)](https://app.codacy.com/gh/Drakkar-Software/OctoBot-Services?utm_source=github.com&utm_medium=referral&utm_content=Drakkar-Software/OctoBot-Services&utm_campaign=Badge_Grade_Dashboard)
[![PyPI](https://img.shields.io/pypi/v/OctoBot-Services.svg)](https://pypi.python.org/pypi/OctoBot-Services/)
[![Github-Action-CI](https://github.com/Drakkar-Software/OctoBot-Services/workflows/OctoBot-Services-CI/badge.svg)](https://github.com/Drakkar-Software/OctoBot-Services/actions)
Expand Down
9 changes: 5 additions & 4 deletions full_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ gevent==25.5.1
### used by flask-socketio with gevent (listed here because multiple libs are usable, force this one)
gevent-websocket==0.10.1
flask-socketio==5.5.1
# chatgpt
openai==1.99.9

# openai
openai==2.15.0
# agents
pydantic==2.12.5
mcp==1.26.0
# Coingecko
coingecko-openapi-client>=1.3.0

# Analysis tools
vaderSentiment==3.3.2
2 changes: 1 addition & 1 deletion octobot_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
# License along with this library.

PROJECT_NAME = "OctoBot-Services"
VERSION = "1.7.3" # major.minor.revision
VERSION = "1.8.0" # major.minor.revision
16 changes: 16 additions & 0 deletions octobot_services/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

from octobot_services.api.services import (
get_available_services,
get_available_backtestable_services,
get_available_ai_services,
get_available_web_search_services,
get_ai_service,
get_web_search_service,
is_service_available_in_backtesting,
get_service,
create_service_factory,
stop_services,
Expand All @@ -39,6 +45,8 @@
from octobot_services.api.service_feeds import (
create_service_feed_factory,
get_service_feed,
get_available_backtestable_feeds,
is_service_used_by_backtestable_feed,
start_service_feed,
stop_service_feed,
clear_bot_id_feeds,
Expand All @@ -59,6 +67,12 @@

__all__ = [
"get_available_services",
"get_available_backtestable_services",
"get_available_ai_services",
"get_available_web_search_services",
"get_ai_service",
"get_web_search_service",
"is_service_available_in_backtesting",
"get_service",
"create_service_factory",
"stop_services",
Expand All @@ -73,6 +87,8 @@
"stop_interfaces",
"create_service_feed_factory",
"get_service_feed",
"get_available_backtestable_feeds",
"is_service_used_by_backtestable_feed",
"start_service_feed",
"stop_service_feed",
"clear_bot_id_feeds",
Expand Down
8 changes: 7 additions & 1 deletion octobot_services/api/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import async_channel.channels as channels
import octobot_services.interfaces as interfaces
import octobot_services.managers as managers
import octobot_services.api.service_feeds as service_feeds_api


def initialize_global_project_data(bot_api: object, project_name: str, project_version: str) -> None:
Expand Down Expand Up @@ -53,7 +54,12 @@ async def send_user_command(bot_id, subject, action, data, wait_for_processing=F


def is_enabled_in_backtesting(interface_class) -> bool:
return all(service.BACKTESTING_ENABLED for service in interface_class.REQUIRED_SERVICES)
if not interface_class.REQUIRED_SERVICES:
return True
return all(
service_feeds_api.is_service_used_by_backtestable_feed(service)
for service in interface_class.REQUIRED_SERVICES
)


def is_interface_relevant(config, interface_class, backtesting_enabled):
Expand Down
15 changes: 15 additions & 0 deletions octobot_services/api/service_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,25 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import octobot_commons.tentacles_management as tentacles_management

import octobot_services.managers as managers
import octobot_services.service_feeds as service_feeds


def get_available_backtestable_feeds() -> list:
feeds = tentacles_management.get_all_classes_from_parent(service_feeds.AbstractServiceFeed)
return [feed for feed in feeds if feed.BACKTESTING_ENABLED]


def is_service_used_by_backtestable_feed(service_class) -> bool:
backtestable = get_available_backtestable_feeds()
for feed in backtestable:
if feed.REQUIRED_SERVICES and service_class in feed.REQUIRED_SERVICES:
return True
return False


def create_service_feed_factory(config, main_async_loop, bot_id) -> service_feeds.ServiceFeedFactory:
return service_feeds.ServiceFeedFactory(config, main_async_loop, bot_id)

Expand Down
53 changes: 51 additions & 2 deletions octobot_services/api/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import octobot_services.api.service_feeds as service_feeds_api
import octobot_services.managers as managers
import octobot_services.services as services
import octobot_services.interfaces as interfaces
Expand All @@ -31,9 +32,57 @@ def _service_async_lock(service_class):
return _SERVICE_ASYNC_LOCKS[service_class.__name__]


def get_available_services() -> list:
def get_available_services() -> list[type[services.AbstractService]]:
return services.ServiceFactory.get_available_services()

def get_available_ai_services() -> list[type[services.AbstractAIService]]:
return services.ServiceFactory.get_available_ai_services()

def get_available_web_search_services() -> list[type[services.AbstractWebSearchService]]:
return services.ServiceFactory.get_available_web_search_services()


def get_available_backtestable_services() -> list:
return [
service_class for service_class in services.ServiceFactory.get_available_services()
if service_class.BACKTESTING_ENABLED
]

async def _get_available_service_instance(
get_available_services_func,
service_type_name: str,
is_backtesting: bool = False
):
available_services = get_available_services_func()
for service_class in available_services:
try:
return await get_service(service_class, is_backtesting, None)
except errors.CreationError:
# Service is not running/initialized, skip it
continue
raise errors.CreationError(f"No {service_type_name} is currently running or available.")

async def get_ai_service(is_backtesting=False) -> services.AbstractAIService:
return await _get_available_service_instance(
get_available_ai_services,
"AI service",
is_backtesting
)

async def get_web_search_service(is_backtesting=False) -> services.AbstractWebSearchService:
return await _get_available_service_instance(
get_available_web_search_services,
"web search service",
is_backtesting
)


def is_service_available_in_backtesting(service_class) -> bool:
return (
service_class.BACKTESTING_ENABLED
or service_feeds_api.is_service_used_by_backtestable_feed(service_class)
)


async def get_service(service_class, is_backtesting, config=None):
# prevent concurrent access when creating a service
Expand All @@ -47,7 +96,7 @@ async def get_service(service_class, is_backtesting, config=None):
)
if created:
service = service_class.instance()
if is_backtesting and not service.BACKTESTING_ENABLED:
if is_backtesting and not is_service_available_in_backtesting(service_class):
raise errors.UnavailableInBacktestingError(
f"{service_class.__name__} service is not available in backtesting"
)
Expand Down
25 changes: 25 additions & 0 deletions octobot_services/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,26 @@
CONFIG_OPENAI_SECRET_KEY = "openai-secret-key"
CONFIG_LLM_CUSTOM_BASE_URL = "llm-custom-base-url"
CONFIG_LLM_MODEL = "llm-model"
CONFIG_LLM_MODEL_FAST = "llm-model-fast"
CONFIG_LLM_MODEL_REASONING = "llm-model-reasoning"
CONFIG_LLM_DAILY_TOKENS_LIMIT = "llm-daily-tokens-limit"
CONFIG_LLM_SHOW_REASONING = "llm-show-reasoning"
CONFIG_LLM_REASONING_EFFORT = "llm-reasoning-effort"
CONFIG_LLM_MCP_SERVERS = "llm-mcp-servers"
CONFIG_LLM_AUTO_INJECT_MCP_TOOLS = "llm-auto-inject-mcp-tools"
ENV_OPENAI_SECRET_KEY = "OPENAI_SECRET_KEY"
ENV_GPT_MODEL = "GPT_MODEL"
ENV_GPT_DAILY_TOKENS_LIMIT = "GPT_DAILY_TOKEN_LIMIT"

# MCP
CONFIG_MCP = "mcp"
CONFIG_MCP_IP = "ip"
CONFIG_MCP_PORT = "port"
ENV_MCP_PORT = "MCP_PORT"
ENV_MCP_ADDRESS = "MCP_ADDRESS"
DEFAULT_MCP_IP = '127.0.0.1'
DEFAULT_MCP_PORT = 3001

# Google
CONFIG_GOOGLE = "google"
CONFIG_TREND_TOPICS = "trends"
Expand Down Expand Up @@ -123,6 +138,16 @@
CONFIG_TAVILY_API_KEY = "api-key"
CONFIG_TAVILY_PROJECT_ID = "project-id"

# SearXNG (self-hosted web search)
CONFIG_SEARXNG = "searxng"
CONFIG_SEARXNG_URL = "url"
CONFIG_SEARXNG_PORT = "port"
CONFIG_SEARXNG_CATEGORIES = "categories"
CONFIG_SEARXNG_LANGUAGE = "language"
CONFIG_SEARXNG_TIME_RANGE = "time_range"
CONFIG_SEARXNG_SAFE_SEARCH = "safe_search"
CONFIG_SEARXNG_ENGINES = "engines"

# Reddit
CONFIG_REDDIT = "reddit"
CONFIG_REDDIT_SUBREDDITS = "subreddits"
Expand Down
5 changes: 5 additions & 0 deletions octobot_services/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ class ReadOnlyInfoType(enum.Enum):
CLICKABLE = "clickable"
CTA = "cta"
READONLY = "readonly"


class AIModelPolicy(enum.Enum):
FAST = "fast"
REASONING = "reasoning"
39 changes: 39 additions & 0 deletions octobot_services/service_feeds/abstract_service_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class AbstractServiceFeed(abstract_service_user.AbstractServiceUser,
# Set simulator class when available in order to use it in backtesting for this feed
SIMULATOR_CLASS = None

# Whether this feed supports historical data collection for backtesting
BACKTESTING_ENABLED = False

_SLEEPING_TIME_BEFORE_RECONNECT_ATTEMPT_SEC = 10
DELAY_BETWEEN_STREAMS_QUERIES = 5
REQUIRED_SERVICE_ERROR_MESSAGE = "Required services are not ready, service feed can't start"
Expand Down Expand Up @@ -145,3 +148,39 @@ async def stop(self):
if self.is_running:
self.should_stop = True
self.is_running = False

async def get_historical_data(
self,
start_timestamp,
end_timestamp,
symbols=None,
source=None,
**kwargs
) -> typing.AsyncIterator[list[dict]]:
"""
Fetch historical data from the feed for the given time range.
Override this method in feeds that support historical data collection.

:param start_timestamp: milliseconds timestamp (int/float) for start of range
:param end_timestamp: milliseconds timestamp (int/float) for end of range
:param symbols: optional list of symbols to filter by
:param source: optional source/topic to fetch
:param kwargs: additional feed-specific parameters
:return: async generator yielding batches (lists) of event dicts
:rtype: typing.AsyncIterator[list[dict]]

Each event dict should have at least:
- timestamp: milliseconds timestamp (int/float)
- payload: dict with event data
- channel: optional str
- symbol: optional str
"""
raise NotImplementedError("get_historical_data is not implemented for this feed")

@classmethod
def get_historical_sources(cls) -> list:
"""
Return the list of source/topic ids supported by get_historical_data.
Override in feeds that support historical data to return their source ids.
"""
return []
14 changes: 14 additions & 0 deletions octobot_services/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from octobot_services.services import service_factory
from octobot_services.services import abstract_service
from octobot_services.services import abstract_ai_service
from octobot_services.services import abstract_web_search_service
from octobot_services.services import read_only_info

from octobot_services.services.service_factory import (
Expand All @@ -24,12 +26,24 @@
from octobot_services.services.abstract_service import (
AbstractService,
)
from octobot_services.services.abstract_ai_service import (
AbstractAIService,
)
from octobot_services.services.abstract_web_search_service import (
AbstractWebSearchService,
WebSearchResult,
WebSearchResponse,
)
from octobot_services.services.read_only_info import (
ReadOnlyInfo,
)

__all__ = [
"ServiceFactory",
"AbstractService",
"AbstractAIService",
"AbstractWebSearchService",
"WebSearchResult",
"WebSearchResponse",
"ReadOnlyInfo",
]
Loading