diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..be245b24 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python + +install: + - pip install -r requirements.txt + +script: pytest # run test + +after_success: + - codecov # submit coverage \ No newline at end of file diff --git a/README.md b/README.md index d9b58d0a..88d45387 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,32 @@ -# Investing bot +[![Build Status](https://travis-ci.org/investingbots/investing-bot-framework.svg?branch=master)](https://travis-ci.org/investingbots/investing-bot-framework) -The investing bot is a free and open source investing bot written in Python. The goal is to give you a configurable bot -where you can decide on how you implement your data providers, strategies, and brokers/exchanges. Also we want to allow -you to let your bot facilitate multiple users. +# Investing Algorithm Framework -It is designed to be controlled via Telegram. As of now, we are aiming to make the configuration of the different -components by the use of plugins. Please see the documentation on how to make your own plugin. +The Investing Algorithm Framework is a free and open source Python framework that encourages rapid development and clean, +pragmatic design. -### Disclaimer -This software is for educational purposes only. Do not risk money which you are afraid to lose. We can't stress this -enough: BEFORE YOU START USING MONEY WITH THE BOT, MAKE SURE THAT YOU TESTED YOU STRATEGIES AND DATA PROVIDERS. -USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. +The goal is to give you a configurable investing algorithm where you can decide how you implement your data providers, +strategies, and order executors. -Always start by running a investing bot in Dry-run and do not engage money before you understand how it works and what profit/loss you should expect. +#####Disclaimer +If you use this framework for your investments, do not risk money which you are afraid to lose. We can't stress this +enough: -We strongly recommend you to have coding and Python knowledge, or trust the people that created the plugins your using. -Do not hesitate to read the source code and understand the mechanism of this bot or the plugin you're using. - -Brokers/Exchange marketplaces supported ------- -Will be updated in the future +BEFORE YOU START USING MONEY WITH THE FRAMEWORK, MAKE SURE THAT YOU TESTED YOUR COMPONENTS THOROUGHLY. USE THE SOFTWARE AT +YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR INVESTMENT RESULTS. +Also, make sure that you read the source code of any plugin you use or implementation of an algorithm made with this +framework. Documentation ------ -Will be updated in the future - -## Features - -- [x] **Based on Python 3.6+**: Support for all operating systems - Windows, macOS and Linux. -- [x] **Persistence**: Persistence is achieved through sqlite. -- [ ] **Dry-run**: Run the bot without playing money. -- [ ] **REST API**: Manage the bot with the use of a REST API. -- [ ] **Backtesting**: Run a simulation of your buy/sell strategy. -- [ ] **Manageable via Telegram**: Manage the bot with Telegram. -- [ ] **Display profit/loss**: Display your profit/loss. -- [ ] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. -- [ ] **Performance status report**: Provide a performance status of your current trades. - -## Quick start - -The investing bot provides a Linux/macOS script to install all dependencies and help you to configure the bot. - -The script will come as a future update - -### Bot commands - - -``` -usage: main.py [-h] [-V] [-c PATH] - -Trading bot based on value principles - -optional arguments: - -h, --help show this help message and exit - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: `config.json`). - -``` - -### Telegram RPC commands - -Telegram is not mandatory. However, this is a great way to control your bot. +All documentation is in the "docs" directory and online at "". If you're just getting started, here's how we recommend +you read the docs: +* First, read install for instructions on installing Investing Algorithm Framework. +* Next, work through the tutorials in order. ("Quickstart", "Template algorithm", "Custom algorithm"). +* For concrete algorithm examples you probably want to read through the topical guides. + ## Development branches @@ -74,7 +37,6 @@ The project is currently setup in two main branches: - `feature/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - `hotfix/*` - These are hot fix branches, which are being worked on heavily. Please don't use these unless you really need to. -## Support ### Help / Slack diff --git a/ci/test.sh b/ci/test.sh new file mode 100755 index 00000000..3b72dcf3 --- /dev/null +++ b/ci/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +python3 -m unittest discover -s ../ diff --git a/investing_bot_framework/__init__.py b/investing_bot_framework/__init__.py index e69de29b..df5df79d 100644 --- a/investing_bot_framework/__init__.py +++ b/investing_bot_framework/__init__.py @@ -0,0 +1,4 @@ +from investing_bot_framework.utils.version import get_version + +VERSION = (1, 0, 0, 'alpha', 0) + diff --git a/investing_bot_framework/core/configuration/__init__.py b/investing_bot_framework/core/configuration/__init__.py index 0a6481bd..562ffdc5 100644 --- a/investing_bot_framework/core/configuration/__init__.py +++ b/investing_bot_framework/core/configuration/__init__.py @@ -1,12 +1,13 @@ import os +import logging.config from typing import Any from importlib import import_module from enum import Enum from investing_bot_framework.core.exceptions import ImproperlyConfigured, OperationalException from investing_bot_framework.core.configuration.template import Template -from investing_bot_framework.core.configuration.config_constants import SETTINGS_MODULE_PATH_ENV_NAME, SETTINGS_STRATEGY_REGISTERED_APPS, \ - SETTINGS_DATA_PROVIDER_REGISTERED_APPS +from investing_bot_framework.core.configuration.config_constants import SETTINGS_MODULE_PATH_ENV_NAME, \ + SETTINGS_STRATEGY_REGISTERED_APPS, SETTINGS_DATA_PROVIDER_REGISTERED_APPS, BASE_DIR, SETTINGS_LOGGING_CONFIG class TimeUnit(Enum): @@ -59,14 +60,17 @@ class BaseSettings: Base wrapper for settings module. It will load all the default settings for a given settings module """ - def __init__(self, settings_module: str = None) -> None: + def __init__(self) -> None: self._configured = False - self._settings_module = settings_module + self._settings_module = None - if self._settings_module is not None: - self.configure() + def configure(self, settings_module: str = None) -> None: + self._settings_module = settings_module - def configure(self) -> None: + if settings_module is None: + self.settings_module = os.environ.get(SETTINGS_MODULE_PATH_ENV_NAME) + else: + self.settings_module = settings_module if self.settings_module is None: raise ImproperlyConfigured("There is no settings module defined") @@ -93,6 +97,8 @@ def configure(self) -> None: self._configured = True + logging.config.dictConfig(self[SETTINGS_LOGGING_CONFIG]) + @property def settings_module(self) -> str: return self._settings_module @@ -100,7 +106,6 @@ def settings_module(self) -> str: @settings_module.setter def settings_module(self, settings_module: str) -> None: self._settings_module = settings_module - self.configure() @property def configured(self) -> bool: @@ -131,8 +136,4 @@ def get(self, key: str, default: Any = None) -> Any: return default -def resolve_settings(): - return BaseSettings(os.environ.get(SETTINGS_MODULE_PATH_ENV_NAME)) - - -settings = resolve_settings() +settings = BaseSettings() diff --git a/investing_bot_framework/core/configuration/config_constants.py b/investing_bot_framework/core/configuration/config_constants.py index 81345b23..de89dd8f 100644 --- a/investing_bot_framework/core/configuration/config_constants.py +++ b/investing_bot_framework/core/configuration/config_constants.py @@ -8,6 +8,12 @@ SETTINGS_DATA_PROVIDER_REGISTERED_APPS = 'INSTALLED_DATA_PROVIDER_APPS' SETTINGS_STRATEGY_REGISTERED_APPS = 'INSTALLED_STRATEGY_APPS' SETTINGS_MAX_WORKERS = 'DEFAULT_MAX_WORKERS' +SETTINGS_BOT_CONTEXT_CONFIGURATION = 'BOT_CONTEXT_CONFIGURATION' +SETTINGS_LOGGING_CONFIG = 'LOGGING' # Operational constants DEFAULT_MAX_WORKERS = 2 + +# Database related constants +BASE_DIR = 'BASE_DIR' +DATABASE_NAME = 'DATABASE_NAME' diff --git a/investing_bot_framework/core/context/bot_context.py b/investing_bot_framework/core/context/bot_context.py index d429fea9..38cc6ba6 100644 --- a/investing_bot_framework/core/context/bot_context.py +++ b/investing_bot_framework/core/context/bot_context.py @@ -3,7 +3,7 @@ from investing_bot_framework.core.configuration import settings from investing_bot_framework.core.exceptions import OperationalException from investing_bot_framework.core.utils import Singleton -from investing_bot_framework.core.context.states import BotState +from investing_bot_framework.core.states import BotState class BotContext(metaclass=Singleton): @@ -18,19 +18,13 @@ class BotContext(metaclass=Singleton): # Settings reference settings = settings - def initialize(self, bot_state: Type[BotState]) -> None: - - # Stop the current state of the investing_bot_framework - if self._state: - self._state.stop() - + def register_initial_state(self, bot_state: Type[BotState]) -> None: self._state = bot_state(context=self) def transition_to(self, bot_state: Type[BotState]) -> None: """ Function to change the running BotState at runtime. """ - self._state = bot_state(context=self) def _check_state(self, raise_exception: bool = False) -> bool: @@ -42,7 +36,7 @@ def _check_state(self, raise_exception: bool = False) -> bool: if raise_exception: raise OperationalException( - "Bot context doesn't have a state, Make sure that you set the state of bot either " + "Bot context doesn't have a state. Make sure that you set the state of bot either " "by initializing it or making sure that you transition to a new valid state." ) else: @@ -66,18 +60,3 @@ def _run_state(self) -> None: transition_state = self._state.get_transition_state_class() self.transition_to(transition_state) - def stop(self) -> None: - """ - Stop the current state of the investing_bot_framework - """ - - self._check_state(raise_exception=True) - self._state.stop() - - def reconfigure(self) -> None: - """ - Reconfigure the current state of the investing_bot_framework - """ - - self._check_state(raise_exception=True) - self._state.reconfigure() diff --git a/investing_bot_framework/core/context/state_validator.py b/investing_bot_framework/core/context/state_validator.py index 0316ba93..8c111ede 100644 --- a/investing_bot_framework/core/context/state_validator.py +++ b/investing_bot_framework/core/context/state_validator.py @@ -4,8 +4,8 @@ class StateValidator(ABC): """ Class StateValidator: validates the given state. Use this class to change the transition process of a state. - Use it as a hook to decide if a state must transition, e.g. only change to a strategy state if all the - provided data meets a certain threshold. + Use it as a hook to decide if a state must transition, e.g. only change to a strategies state if all the + provided data_providers meets a certain threshold. """ @abstractmethod diff --git a/investing_bot_framework/core/context/states/__init__.py b/investing_bot_framework/core/context/states/__init__.py deleted file mode 100644 index 9511ad24..00000000 --- a/investing_bot_framework/core/context/states/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from investing_bot_framework.core.context.states.bot_state import BotState diff --git a/investing_bot_framework/core/context/states/data_state.py b/investing_bot_framework/core/context/states/data_state.py deleted file mode 100644 index d4d5192a..00000000 --- a/investing_bot_framework/core/context/states/data_state.py +++ /dev/null @@ -1,158 +0,0 @@ -import time -from typing import List -from wrapt import synchronized - -from investing_bot_framework.core.exceptions import ImproperlyConfigured, OperationalException -from investing_bot_framework.core.events import Observer -from investing_bot_framework.core.context.bot_context import BotContext -from investing_bot_framework.core.context.states import BotState -from investing_bot_framework.core.executors import ExecutionScheduler -from investing_bot_framework.core.data.data_providers import DataProvider, DataProviderExecutor -from investing_bot_framework.core.utils import Singleton, TimeUnit -from investing_bot_framework.core.configuration.config_constants import DEFAULT_MAX_WORKERS, SETTINGS_MAX_WORKERS, \ - SETTINGS_DATA_PROVIDER_REGISTERED_APPS - - -def import_class(cl): - d = cl.rfind(".") - classname = cl[d+1:len(cl)] - m = __import__(cl[0:d], globals(), locals(), [classname]) - return getattr(m, classname) - - -class DataProviderScheduler(ExecutionScheduler, metaclass=Singleton): - """ - Data Provider scheduler that will function as a scheduler to make sure it keeps it state across multiple states, - it is defined as a Singleton. - """ - - def __init__(self): - self._configured = False - - super(DataProviderScheduler, self).__init__() - - def configure(self, data_providers: List[DataProvider]) -> None: - self._planning = {} - - for data_provider in data_providers: - self.add_execution_task(execution_id=data_provider.get_id(), time_unit=TimeUnit.ALWAYS) - - self._configured = True - - @property - def configured(self) -> bool: - return self._configured - - -class DataState(BotState, Observer): - """ - Represent the data state of a bot. This state will load all the defined data providers and will - run them. - - If you want to validate the state before transitioning, provide a state validator. - """ - - from investing_bot_framework.core.context.states.strategy_state import StrategyState - transition_state_class = StrategyState - - data_providers: List[DataProvider] = [] - _data_provider_executor: DataProviderExecutor - - def __init__(self, context: BotContext) -> None: - super(DataState, self).__init__(context) - - self._updated = False - self._configured = False - self._initialize() - - def _initialize(self) -> None: - """ - Initializes the data providers, loads them dynamically from the specified settings - """ - self._clean_up() - - for data_provider_app_class in self.context.settings[SETTINGS_DATA_PROVIDER_REGISTERED_APPS]: - - instance = import_class(data_provider_app_class)() - - if not isinstance(instance, DataProvider): - raise ImproperlyConfigured( - "Specified data provider {} is not a instance of DataProvider".format(data_provider_app_class) - ) - - self.data_providers.append(instance) - - self._configured = True - - def _clean_up(self) -> None: - """ - Cleans up all the data state resources - """ - - self._data_provider_executor = None - self.data_providers = [] - - def _schedule_data_providers(self) -> List[DataProvider]: - data_provider_scheduler = DataProviderScheduler() - - if not data_provider_scheduler.configured: - data_provider_scheduler.configure(self.data_providers) - - planning = data_provider_scheduler.schedule_executions() - planned_data_providers = [] - - for data_provider in self.data_providers: - - if data_provider.get_id() in planning: - planned_data_providers.append(data_provider) - - return planned_data_providers - - def _start_data_providers(self, data_providers: List[DataProvider]) -> None: - - self._data_provider_executor = DataProviderExecutor( - data_providers=data_providers, - max_workers=self.context.settings.get(SETTINGS_MAX_WORKERS, DEFAULT_MAX_WORKERS) - ) - - self._data_provider_executor.add_observer(self) - self._data_provider_executor.start() - - def run(self) -> None: - - if self._configured: - # Schedule the data providers - planned_data_providers = self._schedule_data_providers() - - # Execute all the data providers - self._start_data_providers(planned_data_providers) - - # Sleep till updated - while not self._updated: - time.sleep(1) - - # Collect all data from the data providers - for data_provider in self._data_provider_executor.registered_data_providers: - print("Data provider: {} finished running".format(data_provider.get_id())) - - else: - raise OperationalException("Data state started without any configuration") - - def stop(self) -> None: - """ - Stop all data providers - """ - - if self._configured: - - if self._data_provider_executor.processing: - self._data_provider_executor.stop() - - def reconfigure(self) -> None: - self._clean_up() - self._initialize() - - @synchronized - def update(self, observable, **kwargs) -> None: - self._updated = True - diff --git a/investing_bot_framework/core/context/states/setup_state.py b/investing_bot_framework/core/context/states/setup_state.py deleted file mode 100644 index 9e6ce13a..00000000 --- a/investing_bot_framework/core/context/states/setup_state.py +++ /dev/null @@ -1,79 +0,0 @@ -from pydoc import locate - -from investing_bot_framework.core.exceptions import ImproperlyConfigured -from investing_bot_framework.core.context.states import BotState -from investing_bot_framework.core.resolvers import ClassCollector -from investing_bot_framework.core.data.data_providers import DataProvider -from investing_bot_framework.core.configuration.config_constants import SETTINGS_DATA_PROVIDER_REGISTERED_APPS - - -class SetupState(BotState): - - from investing_bot_framework.core.context.states.data_state import DataState - transition_state_class = DataState - - def __init__(self, context): - super(SetupState, self).__init__(context) - - def run(self) -> None: - """ - Running the setup state. - - During execution a validation will be performed on: - - - DataProviders - """ - - # Load the settings - if not self.context.settings.configured: - raise ImproperlyConfigured( - "Settings module is not specified, make sure you have setup a investing_bot_framework project and the investing_bot_framework is valid or that " - "you have specified the settings module in your manage.py file" - ) - - # Initialize all data provider executors - self._validate_data_providers() - - def _validate_data_providers(self) -> None: - """ - Validates if all the data providers are correctly configured and can be loaded. - """ - - data_provider_apps_config = self.context.settings.get(SETTINGS_DATA_PROVIDER_REGISTERED_APPS, None) - - # Check if any data providers are configured - if data_provider_apps_config is None or len(data_provider_apps_config) < 1: - raise ImproperlyConfigured( - "You have not configured any data provider apps in your settings file. Please define your data " - "provider apps in your settings file. If you have difficulties configuring data providers, consider " - "looking at the documentation." - ) - - # Try to load all the specified data provider modules - for data_provider_app in data_provider_apps_config: - instance = locate(data_provider_app) - print(instance) - # class_collector = ClassCollector(package_path=data_provider_app, class_type=DataProvider) - # - # if len(class_collector.instances) == 0: - # raise ImproperlyConfigured( - # "Could not load data providers from package {}, are they implemented correctly?. Please make sure " - # "that you defined the right package or module. In the case of referring to your own defined data " - # "providers make sure that they can be imported. If you have difficulties configuring data " - # "providers, consider looking at the documentation.".format(data_provider_app) - # ) - - def stop(self) -> None: - # Stopping all services - pass - - def reconfigure(self) -> None: - # Clean up and reconfigure all the services - pass - - def transition(self) -> None: - - # Transition to data state - from investing_bot_framework.core.context.states.data_state import DataState - self.context.transition_to(DataState) - self.context.run() diff --git a/investing_bot_framework/core/context/states/strategy_state.py b/investing_bot_framework/core/context/states/strategy_state.py deleted file mode 100644 index 661f14ba..00000000 --- a/investing_bot_framework/core/context/states/strategy_state.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Type -from investing_bot_framework.core.context.states import BotState - - -class StrategyState(BotState): - - def run(self) -> None: - pass - - def stop(self) -> None: - pass - - def reconfigure(self) -> None: - pass - - def update(self, observable, **kwargs) -> None: - pass - - def get_transition_state_class(self) -> Type: - from investing_bot_framework.core.context.states.data_state import DataState - return DataState diff --git a/investing_bot_framework/core/data/data_providers/__init__.py b/investing_bot_framework/core/data/data_providers/__init__.py deleted file mode 100644 index ddad2889..00000000 --- a/investing_bot_framework/core/data/data_providers/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from investing_bot_framework.core.data.data_providers.data_provider import DataProvider, RestApiDataProvider -from investing_bot_framework.core.data.data_providers.data_provider_executor import DataProviderExecutor diff --git a/investing_bot_framework/core/data/data_providers/data_provider.py b/investing_bot_framework/core/data/data_providers/data_provider.py deleted file mode 100644 index 297412b2..00000000 --- a/investing_bot_framework/core/data/data_providers/data_provider.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Dict, Any -from abc import abstractmethod, ABC - -from investing_bot_framework.core.workers import Worker -from investing_bot_framework.core.data.data_providers.mixins import RestApiClientMixin - - -class DataProviderException(Exception): - """ - Should be raised when an data_provider related error occurs, for example if an authorization for an API fails, - i.e.: raise DataProviderException('Provided api token is false') - """ - - def __init__(self, message: str) -> None: - super().__init__(self) - self.message = message - - def __str__(self) -> str: - return self.message - - -class DataProvider(Worker): - """ - Class DataProvider: An entity which responsibility is to provide data from an external data source. Where a data - source is defined as any third party service that provides data, e.g cloud storage, REST API, or website. - - A data provider must always be run with the start function from it´s super class. Otherwise depend observers will - not be updated. - """ - - def __init__(self): - super(DataProvider, self).__init__() - self._data: Any = None - - @abstractmethod - def provide_data(self, **kwargs: Dict[str, Any]) -> Any: - pass - - def work(self, **kwargs: Dict[str, Any]) -> None: - self._data = self.provide_data() - - @property - def data(self) -> Any: - - if self._data is None: - raise DataProviderException("Could not provide data, data is not set by {}".format(self.get_id())) - - return self._data - - def clean_up(self) -> None: - self._data = None - - -class RestApiDataProvider(RestApiClientMixin, DataProvider, ABC): - pass diff --git a/investing_bot_framework/core/data/data_providers/data_provider_executor.py b/investing_bot_framework/core/data/data_providers/data_provider_executor.py deleted file mode 100644 index 919a4bde..00000000 --- a/investing_bot_framework/core/data/data_providers/data_provider_executor.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List - -from investing_bot_framework.core.workers import Worker -from investing_bot_framework.core.data.data_providers import DataProvider -from investing_bot_framework.core.executors import Executor -from investing_bot_framework.core.configuration.config_constants import DEFAULT_MAX_WORKERS - - -class DataProviderExecutor(Executor): - """ - Class DataProviderExecutor: is an executor for DataProvider instances. - """ - - def __init__(self, data_providers: List[DataProvider] = None, max_workers: int = DEFAULT_MAX_WORKERS): - super(DataProviderExecutor, self).__init__(max_workers=max_workers) - - self._registered_data_providers: List[DataProvider] = [] - - if data_providers is not None: - self._registered_data_providers = data_providers - - def create_workers(self) -> List[Worker]: - return self._registered_data_providers - - @property - def registered_data_providers(self) -> List[DataProvider]: - return self._registered_data_providers diff --git a/investing_bot_framework/core/data/data_providers/mixins/__init__.py b/investing_bot_framework/core/data/data_providers/mixins/__init__.py deleted file mode 100644 index d459f32a..00000000 --- a/investing_bot_framework/core/data/data_providers/mixins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from investing_bot_framework.core.data.data_providers.mixins.rest_api_mixin import RestApiClientMixin diff --git a/investing_bot_framework/core/data_providers/__init__.py b/investing_bot_framework/core/data_providers/__init__.py new file mode 100644 index 00000000..1090a6f3 --- /dev/null +++ b/investing_bot_framework/core/data_providers/__init__.py @@ -0,0 +1 @@ +from investing_bot_framework.core.data_providers.data_provider import DataProvider diff --git a/investing_bot_framework/core/data_providers/data_provider.py b/investing_bot_framework/core/data_providers/data_provider.py new file mode 100644 index 00000000..eab26b75 --- /dev/null +++ b/investing_bot_framework/core/data_providers/data_provider.py @@ -0,0 +1,42 @@ +import logging +from typing import Dict, Any +from abc import abstractmethod + +from investing_bot_framework.core.workers import ScheduledWorker + +logger = logging.getLogger(__name__) + + +class DataProviderException(Exception): + """ + Should be raised when an data_provider related error occurs, for example if an authorization for an API fails, + i.e.: raise DataProviderException('Provided api token is false') + """ + + def __init__(self, message: str) -> None: + super().__init__(self) + self.message = message + + def __str__(self) -> str: + return self.message + + +class DataProvider(ScheduledWorker): + """ + Class DataProvider: An entity which responsibility is to provide data_providers from an external data_providers + source. Where a data_providers source is defined as any third party service that provides data_providers, + e.g cloud storage, REST API, or website. + + A data_providers provider must always be run with the start function from it´s super class. Otherwise depend + observers will not be updated. + """ + + @abstractmethod + def provide_data(self) -> None: + pass + + def work(self, **kwargs: Dict[str, Any]) -> None: + logger.info("Starting data provider {}".format(self.get_id())) + self.provide_data() + + diff --git a/investing_bot_framework/core/data_providers/mixins/__init__.py b/investing_bot_framework/core/data_providers/mixins/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/investing_bot_framework/core/data_providers/mixins/__init__.py @@ -0,0 +1 @@ + diff --git a/investing_bot_framework/core/data/data_providers/mixins/rest_api_mixin.py b/investing_bot_framework/core/data_providers/mixins/rest_api_mixin.py similarity index 100% rename from investing_bot_framework/core/data/data_providers/mixins/rest_api_mixin.py rename to investing_bot_framework/core/data_providers/mixins/rest_api_mixin.py diff --git a/investing_bot_framework/core/events/observable.py b/investing_bot_framework/core/events/observable.py index 802dd16a..27c766df 100644 --- a/investing_bot_framework/core/events/observable.py +++ b/investing_bot_framework/core/events/observable.py @@ -30,4 +30,4 @@ def notify_observers(self, **kwargs) -> None: @property def observers(self) -> List[Observer]: - return self._observers \ No newline at end of file + return self._observers diff --git a/investing_bot_framework/core/exceptions.py b/investing_bot_framework/core/exceptions.py index 15ed74a8..c20ee3cf 100644 --- a/investing_bot_framework/core/exceptions.py +++ b/investing_bot_framework/core/exceptions.py @@ -12,3 +12,11 @@ class OperationalException(Exception): """ def __init__(self, message) -> None: super(OperationalException, self).__init__(message) + + +class DatabaseOperationalException(Exception): + """ + Class DatabaseOperationalException: Exception class indicating a problem occurred during usage of the database + """ + def __init__(self, message) -> None: + super(DatabaseOperationalException, self).__init__(message) diff --git a/investing_bot_framework/core/executors/__init__.py b/investing_bot_framework/core/executors/__init__.py index 0349763c..93f5ebe1 100644 --- a/investing_bot_framework/core/executors/__init__.py +++ b/investing_bot_framework/core/executors/__init__.py @@ -1,2 +1,2 @@ from investing_bot_framework.core.executors.executor import Executor -from investing_bot_framework.core.executors.execution_scheduler import ExecutionScheduler \ No newline at end of file +from investing_bot_framework.core.executors.execution_scheduler import ExecutionScheduler diff --git a/investing_bot_framework/core/extensions.py b/investing_bot_framework/core/extensions.py new file mode 100644 index 00000000..60881642 --- /dev/null +++ b/investing_bot_framework/core/extensions.py @@ -0,0 +1,3 @@ +from investing_bot_framework.core.resolvers import DatabaseResolver + +db = DatabaseResolver() \ No newline at end of file diff --git a/investing_bot_framework/core/management/commands/runbot.py b/investing_bot_framework/core/management/commands/runbot.py index bebe4389..0b45964b 100644 --- a/investing_bot_framework/core/management/commands/runbot.py +++ b/investing_bot_framework/core/management/commands/runbot.py @@ -2,13 +2,14 @@ from investing_bot_framework.core.management.command import BaseCommand, CommandError from investing_bot_framework.core.context import BotContext -from investing_bot_framework.core.context.states.setup_state import SetupState +from investing_bot_framework.core.configuration.config_constants import SETTINGS_BOT_CONTEXT_CONFIGURATION +from investing_bot_framework.core.configuration import settings class RunBotCommand(BaseCommand): help_message = ( - "Runs a investing_bot_framework, by default it will run until stopped, if cycles is specified it will run the according to " - "the amount of cycles" + "Runs a investing_bot_framework, by default it will run until stopped, if cycles is specified it will run " + "the according to the amount of cycles" ) success_message = ( @@ -23,11 +24,16 @@ def add_arguments(self, parser) -> None: def handle(self, *args, **options) -> Any: + # configure settings + settings.configure() + + # Load the context configuration + __import__(settings[SETTINGS_BOT_CONTEXT_CONFIGURATION]) + cycles = options.get('name', None) # Create an investing_bot_framework context of the investing_bot_framework and run it context = BotContext() - context.initialize(SetupState) context.start() @staticmethod diff --git a/investing_bot_framework/core/order_executors/__init__.py b/investing_bot_framework/core/order_executors/__init__.py new file mode 100644 index 00000000..1af709d0 --- /dev/null +++ b/investing_bot_framework/core/order_executors/__init__.py @@ -0,0 +1 @@ +from investing_bot_framework.core.order_executors.order_executor import OrderExecutor diff --git a/investing_bot_framework/core/order_executors/order_executor.py b/investing_bot_framework/core/order_executors/order_executor.py new file mode 100644 index 00000000..552f20d8 --- /dev/null +++ b/investing_bot_framework/core/order_executors/order_executor.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class OrderExecutor(ABC): + + @abstractmethod + def execute_orders(self) -> None: + pass diff --git a/investing_bot_framework/core/resolvers/__init__.py b/investing_bot_framework/core/resolvers/__init__.py index d265cc44..d087e7f9 100644 --- a/investing_bot_framework/core/resolvers/__init__.py +++ b/investing_bot_framework/core/resolvers/__init__.py @@ -1 +1,3 @@ from investing_bot_framework.core.resolvers.class_collector import ClassCollector +from investing_bot_framework.core.resolvers.database_resolver import DatabaseResolver + diff --git a/investing_bot_framework/core/resolvers/database_resolver.py b/investing_bot_framework/core/resolvers/database_resolver.py new file mode 100644 index 00000000..12c57d16 --- /dev/null +++ b/investing_bot_framework/core/resolvers/database_resolver.py @@ -0,0 +1,180 @@ +import os +from typing import Any + +from sqlalchemy import create_engine +from sqlalchemy.orm import Query, class_mapper, sessionmaker, scoped_session, Session +from sqlalchemy.orm.exc import UnmappedClassError +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm.exc import DetachedInstanceError +from sqlalchemy.exc import DatabaseError + + +from investing_bot_framework.core.configuration import settings +from investing_bot_framework.core.configuration.config_constants import BASE_DIR, DATABASE_NAME +from investing_bot_framework.core.exceptions import DatabaseOperationalException + + +class _SessionProperty: + """ + Wrapper for session property of a Model + + To make sure that each thread gets an scoped session, a new scoped session is created if a new thread + accesses the session property of a Model. + """ + def __init__(self, db): + self.db = db + + def __get__(self, instance, owner): + return self.db.session + + +class _QueryProperty: + """ + Wrapper for query property of a Model + + This wrapper makes sure that each model gets a Query object with a correct session corresponding to its thread. + """ + def __init__(self, db): + self.db = db + + def __get__(self, instance, owner): + + try: + mapper = class_mapper(owner) + if mapper: + return owner.query_class(mapper, session=self.db.session) + + except UnmappedClassError: + return None + + +class Model(object): + table_name = None + session = None + query_class = None + _query = None + + @property + def query(self) -> Query: + return self._query + + @declared_attr + def __tablename__(cls): + + if cls.table_name is None: + return cls.__name__.lower() + return cls.table_name + + def save(self): + self.session.add(self) + self._flush() + return self + + def update(self, **kwargs): + + for attr, value in kwargs.items(): + setattr(self, attr, value) + return self.save() + + def delete(self): + self.session.delete(self) + self._flush() + + def _flush(self): + try: + self.session.flush() + except DatabaseError: + self.session.rollback() + raise + + def _repr(self, **fields: Any) -> str: + """ + Helper for __repr__ + """ + + field_strings = [] + at_least_one_attached_attribute = False + + for key, field in fields.items(): + try: + field_strings.append(f'{key}={field!r}') + except DetachedInstanceError: + field_strings.append(f'{key}=DetachedInstanceError') + else: + at_least_one_attached_attribute = True + + if at_least_one_attached_attribute: + return f"<{self.__class__.__name__}({','.join(field_strings)})>" + + return f"<{self.__class__.__name__} {id(self)}>" + + +class DatabaseResolver: + + def __init__(self, query_class=Query, model_class=Model): + self._configured = False + self.Query = query_class + self._model = self.make_declarative_base(model_class) + self.engine = None + self.session_factory = None + self.Session = None + self.database_path = None + + def configure(self): + self.initialize() + self.session_factory = sessionmaker(bind=self.engine) + self.Session = scoped_session(self.session_factory) + + if self._model is None: + raise DatabaseOperationalException("Model is not defined") + + self._model.session = _SessionProperty(self) + + if not getattr(self._model, 'query_class', None): + self._model.query_class = self.Query + + self._model.query = _QueryProperty(self) + + def initialize(self): + base_dir = settings.get(BASE_DIR) + database_name = settings.get(DATABASE_NAME) + + if database_name is not None: + self.database_path = os.path.join(base_dir, database_name, '.sqlite3') + else: + self.database_path = os.path.join(base_dir, 'db.sqlite3') + + # Only create the database if not exist + if not os.path.isfile(self.database_path): + os.mknod(self.database_path) + + self.engine = create_engine('sqlite:////{}'.format(self.database_path)) + + @staticmethod + def make_declarative_base(model_class): + """ + Creates the declarative base that all models will inherit from. + + :param model_class: base model class to pass to :func:`~sqlalchemy.ext.declarative.declarative_base`. + """ + + model = declarative_base(cls=model_class) + return model + + @property + def session(self) -> Session: + """ + Returns scoped session of an Session object + """ + return self.Session() + + @property + def model(self) -> Model: + return self._model + + def initialize_tables(self): + self._model.metadata.create_all(self.engine) + + + + diff --git a/investing_bot_framework/core/resolvers/module_loaders/data_providers_loader.py b/investing_bot_framework/core/resolvers/module_loaders/data_providers_loader.py index 58eafcf9..84e79d20 100644 --- a/investing_bot_framework/core/resolvers/module_loaders/data_providers_loader.py +++ b/investing_bot_framework/core/resolvers/module_loaders/data_providers_loader.py @@ -3,7 +3,7 @@ from investing_bot_framework.core.exceptions import ImproperlyConfigured from investing_bot_framework.core.configuration import settings from investing_bot_framework.core.resolvers import ClassCollector -from investing_bot_framework.core.data.data_providers import DataProvider +from investing_bot_framework.core.data_providers import DataProvider class DataProvidersLoader: @@ -28,7 +28,8 @@ def load_modules(self) -> List[DataProvider]: if len(class_collector.instances) == 0: raise ImproperlyConfigured( - "There are no data providers configured. Make sure you implement data providers or use a plugin" + "There are no data_providers providers configured. Make sure you implement data_providers providers " + "or use a plugin" ) return class_collector.instances diff --git a/investing_bot_framework/core/states/__init__.py b/investing_bot_framework/core/states/__init__.py new file mode 100644 index 00000000..43b869e2 --- /dev/null +++ b/investing_bot_framework/core/states/__init__.py @@ -0,0 +1,2 @@ +from investing_bot_framework.core.states.bot_state import BotState + diff --git a/investing_bot_framework/core/context/states/bot_state.py b/investing_bot_framework/core/states/bot_state.py similarity index 62% rename from investing_bot_framework/core/context/states/bot_state.py rename to investing_bot_framework/core/states/bot_state.py index de777382..884d4d89 100644 --- a/investing_bot_framework/core/context/states/bot_state.py +++ b/investing_bot_framework/core/states/bot_state.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Type, List +from typing import List from investing_bot_framework.core.context.state_validator import StateValidator @@ -14,14 +14,18 @@ class BotState(ABC): transition_state_class = None # Validator for the current state - state_validators = None + pre_state_validators: List[StateValidator] = None + post_state_validators: List[StateValidator] = None - def __init__(self, context, state_validator: StateValidator = None) -> None: + def __init__(self, context) -> None: self._bot_context = context - self._state_validator = state_validator def start(self): + # Will stop the state if pre-conditions are not met + if not self.validate_state(): + return + while True: self.run() @@ -33,24 +37,19 @@ def start(self): def run(self) -> None: pass - @abstractmethod - def stop(self) -> None: - pass - @property def context(self): return self._bot_context - @abstractmethod - def reconfigure(self) -> None: - pass - - def validate_state(self) -> bool: + def validate_state(self, pre_state: bool = False) -> bool: """ Function that will validate the state """ - state_validators = self.get_state_validators() + if pre_state: + state_validators = self.get_pre_state_validators() + else: + state_validators = self.get_post_state_validators() if state_validators is None: return True @@ -71,12 +70,18 @@ def get_transition_state_class(self): return self.transition_state_class - def get_state_validators(self) -> List[StateValidator]: + def get_pre_state_validators(self) -> List[StateValidator]: - if self.state_validators is not None: + if self.pre_state_validators is not None: return [ - state_validator() for state_validator in getattr(self, 'state_validators') + state_validator() for state_validator in getattr(self, 'pre_state_validators') if issubclass(state_validator, StateValidator) ] + def get_post_state_validators(self) -> List[StateValidator]: + if self.post_state_validators is not None: + return [ + state_validator() for state_validator in getattr(self, 'post_state_validators') + if issubclass(state_validator, StateValidator) + ] diff --git a/investing_bot_framework/core/data/__init__.py b/investing_bot_framework/core/states/templates/__init__.py similarity index 100% rename from investing_bot_framework/core/data/__init__.py rename to investing_bot_framework/core/states/templates/__init__.py diff --git a/investing_bot_framework/core/states/templates/data_providing_state.py b/investing_bot_framework/core/states/templates/data_providing_state.py new file mode 100644 index 00000000..074caa1a --- /dev/null +++ b/investing_bot_framework/core/states/templates/data_providing_state.py @@ -0,0 +1,151 @@ +import time +import logging +from typing import List +from wrapt import synchronized + +from investing_bot_framework.core.events import Observer +from investing_bot_framework.core.context.bot_context import BotContext +from investing_bot_framework.core.exceptions import OperationalException +from investing_bot_framework.core.states import BotState +from investing_bot_framework.core.executors import ExecutionScheduler +from investing_bot_framework.core.workers import Worker +from investing_bot_framework.core.data_providers import DataProvider +from investing_bot_framework.core.executors import Executor +from investing_bot_framework.core.configuration.config_constants import DEFAULT_MAX_WORKERS, SETTINGS_MAX_WORKERS + +logger = logging.getLogger(__name__) + + +class DataProviderExecutor(Executor): + """ + Class DataProviderExecutor: is an executor for DataProvider instances. + """ + + def __init__(self, data_providers: List[DataProvider] = None, max_workers: int = DEFAULT_MAX_WORKERS): + super(DataProviderExecutor, self).__init__(max_workers=max_workers) + + self._registered_data_providers: List[DataProvider] = [] + + if data_providers is not None and len(data_providers) > 0: + self._registered_data_providers = data_providers + + def create_workers(self) -> List[Worker]: + return self._registered_data_providers + + @property + def registered_data_providers(self) -> List[DataProvider]: + return self._registered_data_providers + + @property + def configured(self): + return self._registered_data_providers is not None and len(self._registered_data_providers) > 0 + + +class DataProviderScheduler(ExecutionScheduler): + """ + Data Provider scheduler that will function as a scheduler to make sure it keeps it state across multiple states, + it is defined as a Singleton. + """ + + def __init__(self): + self._configured = False + super(DataProviderScheduler, self).__init__() + + def configure(self, data_providers: List[DataProvider]) -> None: + self._planning = {} + + for data_provider in data_providers: + self.add_execution_task( + execution_id=data_provider.get_id(), + time_unit=data_provider.get_time_unit(), + interval=data_provider.get_time_interval() + ) + + self._configured = True + + @property + def configured(self) -> bool: + return self._configured + + +class DataProvidingState(BotState, Observer): + """ + Represent the data_providers state of a bot. This state will load all the defined data_providers providers and will + run them. + + If you want to validate the state before transitioning, provide a state validator. + """ + + registered_data_providers: List = None + + from investing_bot_framework.core.states.templates.strategy_state import StrategyState + transition_state_class = StrategyState + + data_provider_scheduler: DataProviderScheduler = None + + def __init__(self, context: BotContext) -> None: + super(DataProvidingState, self).__init__(context) + self._updated = False + self.data_provider_executor = None + + if self.registered_data_providers is None or len(self.registered_data_providers) < 1: + raise OperationalException("Data providing state has not any data providers configured") + + def _schedule_data_providers(self) -> List[DataProvider]: + + if not DataProvidingState.data_provider_scheduler: + DataProvidingState.data_provider_scheduler = DataProviderScheduler() + + if not DataProvidingState.data_provider_scheduler.configured: + DataProvidingState.data_provider_scheduler.configure(self.registered_data_providers) + + planning = DataProvidingState.data_provider_scheduler.schedule_executions() + planned_data_providers = [] + + for data_provider in self.registered_data_providers: + + if data_provider.get_id() in planning: + planned_data_providers.append(data_provider) + + return planned_data_providers + + def _start_data_providers(self, data_providers: List[DataProvider]) -> None: + + self.data_provider_executor = DataProviderExecutor( + data_providers=data_providers, + max_workers=self.context.settings.get(SETTINGS_MAX_WORKERS, DEFAULT_MAX_WORKERS) + ) + + if self.data_provider_executor.configured: + self.data_provider_executor.add_observer(self) + self.data_provider_executor.start() + else: + # Skip the execution + self._updated = True + + def run(self) -> None: + + # Schedule the data_providers providers + planned_data_providers = self._schedule_data_providers() + + # Execute all the data_providers providers + self._start_data_providers(planned_data_providers) + + # Sleep till updated + while not self._updated: + time.sleep(1) + + # Collect all data_providers from the data_providers providers + for data_provider in self.data_provider_executor.registered_data_providers: + logger.info("Data provider: {} finished running".format(data_provider.get_id())) + + @synchronized + def update(self, observable, **kwargs) -> None: + self._updated = True + + @staticmethod + def register_data_providers(data_providers: List) -> None: + DataProvidingState.registered_data_providers = data_providers + + + diff --git a/investing_bot_framework/core/states/templates/ordering_state.py b/investing_bot_framework/core/states/templates/ordering_state.py new file mode 100644 index 00000000..c1e5763d --- /dev/null +++ b/investing_bot_framework/core/states/templates/ordering_state.py @@ -0,0 +1,30 @@ +from typing import List + +from investing_bot_framework.core.states import BotState +from investing_bot_framework.core.exceptions import OperationalException +from investing_bot_framework.core.order_executors import OrderExecutor + + +class OrderingState(BotState): + + from investing_bot_framework.core.states.templates.data_providing_state import DataProvidingState + transition_state_class = DataProvidingState + + order_executors: List[OrderExecutor] = None + + def __init__(self, context) -> None: + super(OrderingState, self).__init__(context) + + if self.order_executors is None or len(self.order_executors) < 1: + raise OperationalException("OrderingState state has not any order executors configured") + + def run(self) -> None: + + for order_executor in OrderingState.order_executors: + order_executor.execute_orders() + + @staticmethod + def register_order_executors(order_executors: List[OrderExecutor]) -> None: + OrderingState.order_executors = order_executors + + diff --git a/investing_bot_framework/core/states/templates/setup_state.py b/investing_bot_framework/core/states/templates/setup_state.py new file mode 100644 index 00000000..96ff6df8 --- /dev/null +++ b/investing_bot_framework/core/states/templates/setup_state.py @@ -0,0 +1,34 @@ +import logging + +from investing_bot_framework.core.exceptions import ImproperlyConfigured +from investing_bot_framework.core.states import BotState + +logger = logging.getLogger(__name__) + + +class SetupState(BotState): + + from investing_bot_framework.core.states.templates.data_providing_state import DataProvidingState + transition_state_class = DataProvidingState + + def __init__(self, context): + super(SetupState, self).__init__(context) + + def run(self) -> None: + """ + Running the setup state. + + During execution a validation will be performed on: + + - DataProviders + """ + + # Load the settings + if not self.context.settings.configured: + raise ImproperlyConfigured( + "Settings module is not specified, make sure you have setup a investing_bot_framework project and " + "the investing_bot_framework is valid or that you have specified the settings module in your " + "manage.py file" + ) + + diff --git a/investing_bot_framework/core/states/templates/strategy_state.py b/investing_bot_framework/core/states/templates/strategy_state.py new file mode 100644 index 00000000..fb01c241 --- /dev/null +++ b/investing_bot_framework/core/states/templates/strategy_state.py @@ -0,0 +1,138 @@ +import logging +import time +from typing import List + +from investing_bot_framework.core.states import BotState +from investing_bot_framework.core.executors import ExecutionScheduler +from investing_bot_framework.core.exceptions import OperationalException +from investing_bot_framework.core.workers import Worker +from investing_bot_framework.core.executors import Executor +from investing_bot_framework.core.events import Observer +from investing_bot_framework.core.configuration.config_constants import DEFAULT_MAX_WORKERS, SETTINGS_MAX_WORKERS + +logger = logging.getLogger(__name__) + + +class StrategyExecutor(Executor): + """ + Class StrategyExecutor: is an executor for Strategy instances. + """ + + def __init__(self, strategies: List = None, max_workers: int = DEFAULT_MAX_WORKERS): + super(StrategyExecutor, self).__init__(max_workers=max_workers) + + self._registered_strategies: List = [] + + if strategies is not None and len(strategies) > 0: + self._registered_strategies = strategies + + def create_workers(self) -> List[Worker]: + return self._registered_strategies + + @property + def registered_strategies(self) -> List: + return self._registered_strategies + + @property + def configured(self): + return self._registered_strategies is not None and len(self._registered_strategies) > 0 + + +class StrategyScheduler(ExecutionScheduler): + """ + Strategy scheduler that will function as a scheduler. + """ + + def __init__(self): + self._configured = False + super(StrategyScheduler, self).__init__() + + def configure(self, strategies: List) -> None: + self._planning = {} + + for strategy in strategies: + self.add_execution_task( + execution_id=strategy.get_id(), + time_unit=strategy.get_time_unit(), + interval=strategy.get_time_interval() + ) + + self._configured = True + + @property + def configured(self) -> bool: + return self._configured + + +class StrategyState(BotState, Observer): + + registered_strategies: List = None + strategy_scheduler: StrategyScheduler = None + + def __init__(self, context): + super(StrategyState, self).__init__(context) + self._updated = False + self.strategy_executor = None + + def _schedule_strategies(self) -> List: + + if not StrategyState.strategy_scheduler: + StrategyState.strategy_scheduler = StrategyScheduler() + + if not StrategyState.strategy_scheduler.configured: + StrategyState.strategy_scheduler.configure(self.registered_strategies) + + planning = StrategyState.strategy_scheduler.schedule_executions() + planned_strategies = [] + + for strategy in self.registered_strategies: + + if strategy.get_id() in planning: + planned_strategies.append(strategy) + + return planned_strategies + + def _start_strategies(self, strategies: List) -> None: + + self.strategy_executor = StrategyExecutor( + strategies=strategies, + max_workers=self.context.settings.get(SETTINGS_MAX_WORKERS, DEFAULT_MAX_WORKERS) + ) + + if self.strategy_executor.configured: + self.strategy_executor.add_observer(self) + self.strategy_executor.start() + else: + # Skip the execution + self._updated = True + + def run(self) -> None: + + if self.registered_strategies is None: + raise OperationalException("Data providing state has not any data providers configured") + + # Schedule the strategies providers + planned_strategies = self._schedule_strategies() + + # Execute all the strategies providers + self._start_strategies(planned_strategies) + + # Sleep till updated + while not self._updated: + time.sleep(1) + + # Collect all strategies from the strategies providers + for strategies in self.strategy_executor.registered_strategies: + logger.info("Strategy: {} finished running".format(strategies.get_id())) + + def update(self, observable, **kwargs) -> None: + self._updated = True + + @staticmethod + def register_strategies(strategies: List) -> None: + StrategyState.registered_strategies = strategies + + def get_transition_state_class(self): + from investing_bot_framework.core.states.templates.ordering_state import OrderingState + return OrderingState + diff --git a/investing_bot_framework/core/strategies/__init__.py b/investing_bot_framework/core/strategies/__init__.py new file mode 100644 index 00000000..b971a42d --- /dev/null +++ b/investing_bot_framework/core/strategies/__init__.py @@ -0,0 +1 @@ +from investing_bot_framework.core.strategies.strategy import Strategy diff --git a/investing_bot_framework/core/strategies/strategy.py b/investing_bot_framework/core/strategies/strategy.py new file mode 100644 index 00000000..21fea0ef --- /dev/null +++ b/investing_bot_framework/core/strategies/strategy.py @@ -0,0 +1,23 @@ +import logging +from typing import Dict, Any +from abc import abstractmethod + +from investing_bot_framework.core.workers import ScheduledWorker + +logger = logging.getLogger(__name__) + + +class Strategy(ScheduledWorker): + """ + Class Strategy + """ + + @abstractmethod + def apply_strategy(self) -> None: + pass + + def work(self, **kwargs: Dict[str, Any]) -> None: + logger.info("Starting strategy {}".format(self.get_id())) + self.apply_strategy() + + diff --git a/investing_bot_framework/core/workers/__init__.py b/investing_bot_framework/core/workers/__init__.py index 511deca6..12d5b55b 100644 --- a/investing_bot_framework/core/workers/__init__.py +++ b/investing_bot_framework/core/workers/__init__.py @@ -1 +1,3 @@ from investing_bot_framework.core.workers.worker import Worker +from investing_bot_framework.core.workers.scheduled_worker import ScheduledWorker + diff --git a/investing_bot_framework/core/workers/scheduled_worker.py b/investing_bot_framework/core/workers/scheduled_worker.py new file mode 100644 index 00000000..722bfa45 --- /dev/null +++ b/investing_bot_framework/core/workers/scheduled_worker.py @@ -0,0 +1,23 @@ +from abc import ABC + +from investing_bot_framework.core.utils import TimeUnit +from investing_bot_framework.core.workers.worker import Worker + + +class ScheduledWorker(Worker, ABC): + + def get_time_unit(self) -> TimeUnit: + assert getattr(self, 'time_unit', None) is not None, ( + "{} should either include a time_unit attribute, or override the " + "`get_time_unit()`, method.".format(self.__class__.__name__) + ) + + return getattr(self, 'time_unit') + + def get_time_interval(self) -> int: + assert getattr(self, 'time_interval', None) is not None, ( + "{} should either include a time_interval attribute, or override the " + "`get_time_interval()`, method.".format(self.__class__.__name__) + ) + + return getattr(self, 'time_interval') diff --git a/investing_bot_framework/settings.py b/investing_bot_framework/settings.py deleted file mode 100644 index 4a3d1cb0..00000000 --- a/investing_bot_framework/settings.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -import logging.config - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - -# Change this when not in development, feature or hot-fix branch -DEBUG = int(os.environ.get('DEBUG', True)) - -# Setup logging -# make sure that the log dir exists -log_dir = os.path.abspath(os.path.join(BASE_DIR, 'logs')) -main_log_file = os.path.join(log_dir, 'main.log') - -if not os.path.isdir(log_dir): - os.mkdir(log_dir) - -if not os.path.isfile(main_log_file): - os.mknod(main_log_file) - -if DEBUG: - logging_level = "DEBUG" -else: - logging_level = "INFO" - -logging.config.dictConfig({ - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': '%(levelname)s %(asctime)s - [thread: %(threadName)-4s %(name)s] %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'standard', - }, - 'file': { - 'formatter': 'standard', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'logs/main.log'), - 'backupCount': 10, - 'maxBytes': 10000, - }, - }, - 'loggers': { - '': { - 'level': logging_level, - 'handlers': ['console', 'file'], - }, - }, -}) diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/context.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/context.py-template new file mode 100644 index 00000000..270d3346 --- /dev/null +++ b/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/context.py-template @@ -0,0 +1,23 @@ +from investing_bot_framework.core.context import BotContext +from investing_bot_framework.core.states.templates.setup_state import SetupState +from investing_bot_framework.core.states.templates.data_providing_state import DataProvidingState +from investing_bot_framework.core.states.templates.strategy_state import StrategyState +from investing_bot_framework.core.states.templates.ordering_state import OrderingState + +# Import custom components +from bot.data_providers.data_providers import MyDataProvider +from bot.strategies.strategies import MyStrategy +from bot.order_executors.order_executors import MyOrderExecutor + +# Register Initial state +context = BotContext() +context.register_initial_state(SetupState) + +# Register all data providers +DataProvidingState.register_data_providers([MyDataProvider()]) + +# Register all strategies +StrategyState.register_strategies([MyStrategy()]) + +# Register all order executors +OrderingState.register_order_executors([MyOrderExecutor()]) diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/settings.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/settings.py-template index e0968492..3aeaef9f 100755 --- a/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/settings.py-template +++ b/investing_bot_framework/templates/bot_project_directory/bot_project_template/configuration/settings.py-template @@ -1,10 +1,56 @@ -BOT_PROJECT_NAME = '{{ bot_project_name }}' +import os +from pathlib import Path -INSTALLED_DATA_PROVIDER_APPS = [ - '{{ bot_project_name }}.data', -] +BOT_PROJECT_NAME = 'bot' -INSTALLED_STRATEGY_APPS = [ - '{{ bot_project_name }}.strategy', -] +BOT_CONTEXT_CONFIGURATION = 'bot.configuration.context' +# Change this when not in development, feature or hot-fix branch +DEBUG = int(os.environ.get('DEBUG', True)) + +BASE_DIR = str(Path(__file__).parent.parent) + +LOG_FILE_NAME = 'log' + +LOG_DIR = '{}/logs'.format(BASE_DIR) + +LOG_PATH = "{}/{}.log".format(LOG_DIR, LOG_FILE_NAME) + +if not os.path.isdir(LOG_DIR): + os.mkdir(LOG_DIR) + +if DEBUG: + logging_level = "DEBUG" +else: + logging_level = "INFO" + +# Logging configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(levelname)s %(asctime)s - [thread: %(threadName)-4s %(name)s] %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'standard', + }, + 'file': { + 'formatter': 'standard', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': LOG_PATH, + 'backupCount': 10, + 'maxBytes': 10000, + }, + }, + 'loggers': { + '': { + 'level': logging_level, + 'handlers': ['console', 'file'], + }, + }, +} \ No newline at end of file diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/data/data_providers.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/data/data_providers.py-template deleted file mode 100755 index b18f9061..00000000 --- a/investing_bot_framework/templates/bot_project_directory/bot_project_template/data/data_providers.py-template +++ /dev/null @@ -1,5 +0,0 @@ -from investing_bot_framework.core.data.data_providers import DataProvider - -""" -Define here all you data providers, e.g. Rest API client -""" diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/data/__init__.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/data_providers/__init__.py-template similarity index 100% rename from investing_bot_framework/templates/bot_project_directory/bot_project_template/data/__init__.py-template rename to investing_bot_framework/templates/bot_project_directory/bot_project_template/data_providers/__init__.py-template diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/data_providers/data_providers.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/data_providers/data_providers.py-template new file mode 100755 index 00000000..3262f658 --- /dev/null +++ b/investing_bot_framework/templates/bot_project_directory/bot_project_template/data_providers/data_providers.py-template @@ -0,0 +1,15 @@ +from investing_bot_framework.core.data_providers import DataProvider +from investing_bot_framework.core.utils import TimeUnit + +""" +Define here all your data providers +""" + + +class MyDataProvider(DataProvider): + time_unit = TimeUnit.SECOND + time_interval = 10 + id = 'my_data_provider' + + def provide_data(self) -> None: + pass diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategy/__init__.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/order_executors/__init__.py-template old mode 100755 new mode 100644 similarity index 100% rename from investing_bot_framework/templates/bot_project_directory/bot_project_template/strategy/__init__.py-template rename to investing_bot_framework/templates/bot_project_directory/bot_project_template/order_executors/__init__.py-template diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/order_executors/order_executors.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/order_executors/order_executors.py-template new file mode 100644 index 00000000..e6e8d42a --- /dev/null +++ b/investing_bot_framework/templates/bot_project_directory/bot_project_template/order_executors/order_executors.py-template @@ -0,0 +1,7 @@ +from investing_bot_framework.core.order_executors import OrderExecutor + + +class MyOrderExecutor(OrderExecutor): + + def execute_orders(self) -> None: + pass diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategies/__init__.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategies/__init__.py-template new file mode 100755 index 00000000..e69de29b diff --git a/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategies/strategies.py-template b/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategies/strategies.py-template new file mode 100644 index 00000000..56c34fb9 --- /dev/null +++ b/investing_bot_framework/templates/bot_project_directory/bot_project_template/strategies/strategies.py-template @@ -0,0 +1,15 @@ +from investing_bot_framework.core.strategies import Strategy +from investing_bot_framework.core.utils import TimeUnit + +""" +Define here all your strategies +""" + + +class MyStrategy(Strategy): + time_unit = TimeUnit.SECOND + time_interval = 10 + id = 'my_strategy' + + def apply_strategy(self) -> None: + pass diff --git a/investing_bot_framework/tests/core/data/data_providers/data_provider.py b/investing_bot_framework/tests/core/data/data_providers/data_provider.py index cef1059a..d94326cd 100644 --- a/investing_bot_framework/tests/core/data/data_providers/data_provider.py +++ b/investing_bot_framework/tests/core/data/data_providers/data_provider.py @@ -1,64 +1,34 @@ -from typing import Dict, Any -from unittest import TestCase +from investing_bot_framework.tests.core.data.data_providers.resources import TestDataProviderOne, \ + TestDataProviderTwo, TestObserver -from investing_bot_framework.core.data.data_providers import DataProvider -from investing_bot_framework.core.events import Observer +def test(): + data_provider_one = TestDataProviderOne() -class TestDataProviderOne(DataProvider): + assert data_provider_one.id is not None + assert data_provider_one.get_id() == TestDataProviderOne.id - id = 'TestDataProviderOne' + observer = TestObserver() + data_provider_one.add_observer(observer) - def provide_data(self, **kwargs: Dict[str, Any]) -> Any: - return "data" + # Run the data_providers provider + data_provider_one.start() + # Observer must have been updated + assert observer.update_count == 1 -class TestDataProviderTwo(DataProvider): + data_provider_two = TestDataProviderTwo() - id = 'TestDataProviderTwo' + assert data_provider_two.id is not None + assert data_provider_two.get_id() == TestDataProviderTwo.id - def provide_data(self, **kwargs: Dict[str, Any]) -> Any: - return "data" + data_provider_two.add_observer(observer) + # Run the data_providers provider + data_provider_two.start() -class TestObserver(Observer): + # Observer must have been updated + assert observer.update_count == 2 - def __init__(self) -> None: - self.update_count = 0 - - def update(self, observable, **kwargs) -> None: - self.update_count += 1 - - -class DataProviderSetup(TestCase): - - def test(self): - data_provider_one = TestDataProviderOne() - - self.assertIsNotNone(data_provider_one.id) - self.assertEqual(data_provider_one.get_id(), TestDataProviderOne.id) - - observer = TestObserver() - data_provider_one.add_observer(observer) - - # Run the data provider - data_provider_one.start() - - # Observer must have been updated - self.assertEqual(observer.update_count, 1) - - data_provider_two = TestDataProviderTwo() - - self.assertIsNotNone(data_provider_two.id) - self.assertEqual(data_provider_two.get_id(), TestDataProviderTwo.id) - - data_provider_two.add_observer(observer) - - # Run the data provider - data_provider_two.start() - - # Observer must have been updated - self.assertEqual(observer.update_count, 2) - - # Id´s must be different - self.assertNotEqual(TestDataProviderOne.id, TestDataProviderTwo.id) \ No newline at end of file + # Id´s must be different + assert TestDataProviderOne.id != TestDataProviderTwo.id diff --git a/investing_bot_framework/tests/core/data/data_providers/resources.py b/investing_bot_framework/tests/core/data/data_providers/resources.py new file mode 100644 index 00000000..6a8754a2 --- /dev/null +++ b/investing_bot_framework/tests/core/data/data_providers/resources.py @@ -0,0 +1,29 @@ +from typing import Dict, Any + +from investing_bot_framework.core.events import Observer +from investing_bot_framework.core.data_providers import DataProvider + + +class TestDataProviderOne(DataProvider): + + id = 'TestDataProviderOne' + + def provide_data(self, **kwargs: Dict[str, Any]) -> Any: + return "data_providers" + + +class TestDataProviderTwo(DataProvider): + + id = 'TestDataProviderTwo' + + def provide_data(self, **kwargs: Dict[str, Any]) -> Any: + return "data_providers" + + +class TestObserver(Observer): + + def __init__(self) -> None: + self.update_count = 0 + + def update(self, observable, **kwargs) -> None: + self.update_count += 1 diff --git a/investing_bot_framework/tests/core/executors/resources.py b/investing_bot_framework/tests/core/executors/resources.py new file mode 100644 index 00000000..522f6b42 --- /dev/null +++ b/investing_bot_framework/tests/core/executors/resources.py @@ -0,0 +1,56 @@ +from typing import Dict, Any, List +from time import sleep +from wrapt import synchronized + +from investing_bot_framework.core.workers import Worker +from investing_bot_framework.core.events.observer import Observer +from investing_bot_framework.core.executors import Executor + + +class TestObserver(Observer): + + def __init__(self) -> None: + self.update_count = 0 + + @synchronized + def update(self, observable, **kwargs) -> None: + self.update_count += 1 + + +class TestWorkerOne(Worker): + id = 'TestWorkerOne' + + def work(self, **kwargs: Dict[str, Any]) -> None: + # Simulate some work + sleep(1) + + +class TestWorkerTwo(Worker): + id = 'TestWorkerTwo' + + def work(self, **kwargs: Dict[str, Any]) -> None: + # Simulate some work + sleep(1) + + +class TestWorkerThree(Worker): + id = 'TestWorkerThree' + + def work(self, **kwargs: Dict[str, Any]) -> None: + # Simulate some work + sleep(1) + + +class TestExecutor(Executor): + + def __init__(self, workers: List[Worker] = None): + super(TestExecutor, self).__init__(max_workers=2) + + self._registered_workers = workers + + def create_workers(self) -> List[Worker]: + return self.registered_workers + + @property + def registered_workers(self) -> List[Worker]: + return self._registered_workers diff --git a/investing_bot_framework/tests/core/executors/test_executor.py b/investing_bot_framework/tests/core/executors/test_executor.py index f4a507c4..72f2854a 100644 --- a/investing_bot_framework/tests/core/executors/test_executor.py +++ b/investing_bot_framework/tests/core/executors/test_executor.py @@ -1,64 +1,12 @@ from threading import active_count -from typing import Dict, Any, List from unittest import TestCase from time import sleep -from wrapt import synchronized -from investing_bot_framework.core.workers import Worker -from investing_bot_framework.core.executors import Executor -from investing_bot_framework.core.events.observer import Observer +from investing_bot_framework.tests.core.executors.resources import TestExecutor, TestWorkerOne, TestWorkerTwo, \ + TestObserver, TestWorkerThree -class TestObserver(Observer): - - def __init__(self) -> None: - self.update_count = 0 - - @synchronized - def update(self, observable, **kwargs) -> None: - self.update_count += 1 - - -class TestWorkerOne(Worker): - id = 'TestWorkerOne' - - def work(self, **kwargs: Dict[str, Any]) -> None: - # Simulate some work - sleep(1) - - -class TestWorkerTwo(Worker): - id = 'TestWorkerTwo' - - def work(self, **kwargs: Dict[str, Any]) -> None: - # Simulate some work - sleep(1) - - -class TestWorkerThree(Worker): - id = 'TestWorkerThree' - - def work(self, **kwargs: Dict[str, Any]) -> None: - # Simulate some work - sleep(1) - - -class TestExecutor(Executor): - - def __init__(self, workers: List[Worker] = None): - super(TestExecutor, self).__init__(max_workers=2) - - self._registered_workers = workers - - def create_workers(self) -> List[Worker]: - return self.registered_workers - - @property - def registered_workers(self) -> List[Worker]: - return self._registered_workers - - -class TestStandardExecutor(TestCase): +class TestStandardExecutor: def test(self) -> None: executor = TestExecutor(workers=[TestWorkerOne(), TestWorkerTwo()]) @@ -66,40 +14,40 @@ def test(self) -> None: executor.add_observer(observer) # Make sure the initialization is correct - self.assertEqual(len(executor.registered_workers), 2) - self.assertEqual(active_count(), 1) + assert len(executor.registered_workers) == 2 + assert active_count() == 1 # Start the executor executor.start() # 3 Threads must be running - self.assertTrue(executor.processing) - self.assertEqual(active_count(), 3) + assert executor.processing + assert active_count() == 3 sleep(2) - # After finishing only 1 thread must be active - self.assertEqual(active_count(), 1) - self.assertFalse(executor.processing) + # # After finishing only 1 thread must be active + assert active_count(), 1 + assert not executor.processing # Observer must have been updated by the executor - self.assertEqual(observer.update_count, 1) + assert observer.update_count == 1 # Start the executor executor.start() # 3 Threads must be running - self.assertTrue(executor.processing) - self.assertEqual(active_count(), 3) + assert executor.processing + assert active_count() == 3 sleep(2) # After finishing only 1 thread must be active - self.assertEqual(active_count(), 1) - self.assertFalse(executor.processing) + assert active_count() == 1 + assert not executor.processing # Observer must have been updated by the executor - self.assertEqual(observer.update_count, 2) + assert observer.update_count == 2 executor = TestExecutor(workers=[TestWorkerOne(), TestWorkerTwo(), TestWorkerThree()]) executor.add_observer(observer) @@ -108,81 +56,20 @@ def test(self) -> None: executor.start() # 3 Threads must be running - self.assertTrue(executor.processing) - self.assertEqual(active_count(), 3) + assert executor.processing + assert active_count() == 3 sleep(2) # After finishing only two threads must be active (main + last worker, because max workers is 2) - self.assertEqual(active_count(), 2) - self.assertTrue(executor.processing) + assert active_count() == 2 + assert executor.processing sleep(1) # After finishing only 1 thread must be active - self.assertEqual(active_count(), 1) - self.assertFalse(executor.processing) + assert active_count(), 1 + assert not executor.processing # Observer must have been updated by the executor - self.assertEqual(observer.update_count, 3) - - - - - - # def test_execution_executor(): -# logger.info("TEST: test DataProviderExecutor execution") -# -# observer = DummyObserver() -# -# data_provider_one = DummyDataProviderWorker() -# data_provider_three = DummyDataProviderWorker() -# -# executor = DataProviderExecutor( -# [ -# data_provider_one, -# data_provider_three -# ] -# ) -# -# executor.add_observer(observer) -# -# assert active_count() == 1 -# -# -# assert active_count() == 3 -# -# sleep(2) -# -# # Check if the observer is updated by the executor -# assert observer.update_count == 1 -# -# data_provider_one = DummyDataProviderWorker() -# -# executor = DataProviderExecutor( -# [ -# data_provider_one, -# ] -# ) -# -# executor.add_observer(observer) -# -# assert active_count() == 1 -# -# executor.start() -# -# assert active_count() == 2 -# -# sleep(2) -# -# # Check if the observer is updated by the executor -# assert observer.update_count == 2 -# -# executor.start() -# -# sleep(2) -# -# # Check if the observer is updated by the executor -# assert observer.update_count == 3 -# -# logger.info("TEST FINISHED") \ No newline at end of file + assert observer.update_count == 3 diff --git a/investing_bot_framework/tests/core/executors/test_scheduler.py b/investing_bot_framework/tests/core/executors/test_scheduler.py index 2ac02073..ffa2f9ed 100644 --- a/investing_bot_framework/tests/core/executors/test_scheduler.py +++ b/investing_bot_framework/tests/core/executors/test_scheduler.py @@ -1,15 +1,15 @@ import random +import pytest from uuid import uuid4 -from unittest import TestCase from datetime import datetime, timedelta from investing_bot_framework.core.executors.execution_scheduler import ExecutionScheduler, ExecutionTask from investing_bot_framework.core.utils import TimeUnit -class TestExecutionScheduler(TestCase): +class TestExecutionScheduler(object): - def setUp(self) -> None: + def setup_method(self) -> None: self.execution_task_one = { 'execution_id': uuid4().__str__(), @@ -54,17 +54,17 @@ def test(self): # All tasks must be scheduled the first planning planning = scheduler.schedule_executions() - self.assertTrue(self.execution_task_one['execution_id'] in planning) - self.assertTrue(self.execution_task_two['execution_id'] in planning) - self.assertTrue(self.execution_task_three['execution_id'] in planning) - self.assertTrue(self.execution_task_four['execution_id'] in planning) + assert self.execution_task_one['execution_id'] in planning + assert self.execution_task_two['execution_id'] in planning + assert self.execution_task_three['execution_id'] in planning + assert self.execution_task_four['execution_id'] in planning # Only Task 1 must be in the planning planning = scheduler.schedule_executions() - self.assertTrue(self.execution_task_one['execution_id'] in planning) - self.assertFalse(self.execution_task_two['execution_id'] in planning) - self.assertFalse(self.execution_task_three['execution_id'] in planning) - self.assertFalse(self.execution_task_four['execution_id'] in planning) + assert self.execution_task_one['execution_id'] in planning + assert self.execution_task_two['execution_id'] not in planning + assert self.execution_task_three['execution_id'] not in planning + assert self.execution_task_four['execution_id'] not in planning minus_time_delta = datetime.now() - timedelta(seconds=self.execution_task_two['interval']) appointments = scheduler._planning.keys() @@ -79,10 +79,10 @@ def test(self): # Task 1, 2 must be in the planning planning = scheduler.schedule_executions() - self.assertTrue(self.execution_task_one['execution_id'] in planning) - self.assertTrue(self.execution_task_two['execution_id'] in planning) - self.assertFalse(self.execution_task_three['execution_id'] in planning) - self.assertFalse(self.execution_task_four['execution_id'] in planning) + assert self.execution_task_one['execution_id'] in planning + assert self.execution_task_two['execution_id'] in planning + assert self.execution_task_three['execution_id'] not in planning + assert self.execution_task_four['execution_id'] not in planning minus_time_delta = datetime.now() - timedelta(minutes=self.execution_task_three['interval']) appointments = scheduler._planning.keys() @@ -97,10 +97,10 @@ def test(self): # Task 1, 2 and 3 must be in the planning planning = scheduler.schedule_executions() - self.assertTrue(self.execution_task_one['execution_id'] in planning) - self.assertTrue(self.execution_task_two['execution_id'] in planning) - self.assertTrue(self.execution_task_three['execution_id'] in planning) - self.assertFalse(self.execution_task_four['execution_id'] in planning) + assert self.execution_task_one['execution_id'] in planning + assert self.execution_task_two['execution_id'] in planning + assert self.execution_task_three['execution_id'] in planning + assert self.execution_task_four['execution_id'] not in planning minus_time_delta = datetime.now() - timedelta(hours=self.execution_task_four['interval']) appointments = scheduler._planning.keys() @@ -112,33 +112,33 @@ def test(self): scheduler._planning[appointment].interval, last_run=minus_time_delta ) - + # # Task 1, 2 and 3 must be in the planning planning = scheduler.schedule_executions() - self.assertTrue(self.execution_task_one['execution_id'] in planning) - self.assertTrue(self.execution_task_two['execution_id'] in planning) - self.assertTrue(self.execution_task_three['execution_id'] in planning) - self.assertTrue(self.execution_task_four['execution_id'] in planning) + assert self.execution_task_one['execution_id'] in planning + assert self.execution_task_two['execution_id'] in planning + assert self.execution_task_three['execution_id'] in planning + assert self.execution_task_four['execution_id'] in planning def test_exceptions(self) -> None: scheduler = ExecutionScheduler() - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as e_info: scheduler.add_execution_task(**self.wrong_execution_task_one) - self.assertTrue("Interval for task time unit is smaller then 1" in str(context.exception)) + assert "Interval for task time unit is smaller then 1" in str(e_info.value) - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as e_info: scheduler.add_execution_task(**self.wrong_execution_task_two) - self.assertTrue("Appoint must set an interval with the corresponding time unit" in str(context.exception)) + assert "Appoint must set an interval with the corresponding time unit" in str(e_info.value) scheduler.add_execution_task(**self.execution_task_one) - with self.assertRaises(Exception) as context: + with pytest.raises(Exception) as e_info: scheduler.add_execution_task(**self.execution_task_one) - self.assertTrue("Can't add execution task, execution id is already taken" in str(context.exception)) + assert "Can't add execution task, execution id is already taken" in str(e_info.value) diff --git a/investing_bot_framework/tests/core/resolvers/__init__.py b/investing_bot_framework/tests/core/resolvers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/investing_bot_framework/tests/core/resolvers/test_database_resolver.py b/investing_bot_framework/tests/core/resolvers/test_database_resolver.py new file mode 100644 index 00000000..4fac1ad9 --- /dev/null +++ b/investing_bot_framework/tests/core/resolvers/test_database_resolver.py @@ -0,0 +1,63 @@ +import os +from sqlalchemy import Column, String, Integer + +from investing_bot_framework.tests.resources import BaseTestMixin, utils +from investing_bot_framework.core.configuration import settings +from investing_bot_framework.core.extensions import db + + +class TestModel(db.model): + id = Column(Integer, primary_key=True) + name = Column(String()) + + +class TestDatabaseResolverConfiguration(BaseTestMixin): + + def setup_method(self) -> None: + self.initialize_environment() + + def test_configuration(self): + settings.configure() + db.configure() + + # Check if all properties are configured + assert db.Session is not None + assert db.engine is not None + assert db.session_factory is not None + assert db.database_path is not None + assert os.path.isfile(db.database_path) == True + + def teardown_method(self) -> None: + + if os.path.isfile(db.database_path): + os.remove(db.database_path) + + +class TestDatabaseResolverModel(BaseTestMixin): + + def setup_method(self) -> None: + self.initialize_environment() + settings.configure() + db.configure() + db.initialize_tables() + + def test(self) -> None: + + model = TestModel(name=utils.random_string(10)) + model.save() + db.session.commit() + assert 1 == len(TestModel.query.all()) + + model = TestModel(name=utils.random_string(10)) + model.save() + db.session.commit() + assert 2 == len(TestModel.query.all()) + + def teardown_method(self) -> None: + + if os.path.isfile(db.database_path): + os.remove(db.database_path) + + + + diff --git a/investing_bot_framework/tests/resources/__init__.py b/investing_bot_framework/tests/resources/__init__.py new file mode 100644 index 00000000..0013e49a --- /dev/null +++ b/investing_bot_framework/tests/resources/__init__.py @@ -0,0 +1,13 @@ +import os + +import investing_bot_framework.tests.resources.standard_settings + + +class BaseTestMixin: + + @staticmethod + def initialize_environment(): + os.environ.setdefault( + 'INVESTING_BOT_FRAMEWORK_SETTINGS_MODULE', 'investing_bot_framework.tests.resources.standard_settings' + ) + diff --git a/investing_bot_framework/tests/resources/standard_settings.py b/investing_bot_framework/tests/resources/standard_settings.py new file mode 100644 index 00000000..71c220d8 --- /dev/null +++ b/investing_bot_framework/tests/resources/standard_settings.py @@ -0,0 +1,49 @@ +import os +from pathlib import Path + +BOT_PROJECT_NAME = 'bot' + +BOT_CONTEXT_CONFIGURATION = 'bot.configuration.context' + +# Change this when not in development, feature or hot-fix branch +DEBUG = int(os.environ.get('DEBUG', True)) + +BASE_DIR = str(Path(__file__).parent.parent) + +LOG_FILE_NAME = 'log' + +LOG_DIR = '{}/logs'.format(BASE_DIR) + +LOG_PATH = "{}/{}.log".format(LOG_DIR, LOG_FILE_NAME) + +# if not os.path.isdir(LOG_DIR): +# os.mkdir(LOG_DIR) + +if DEBUG: + logging_level = "DEBUG" +else: + logging_level = "INFO" + +# Logging configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '%(levelname)s %(asctime)s - [thread: %(threadName)-4s %(name)s] %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'standard', + }, + }, + 'loggers': { + '': { + 'level': logging_level, + 'handlers': ['console'], + }, + }, +} \ No newline at end of file diff --git a/investing_bot_framework/tests/resources/utils.py b/investing_bot_framework/tests/resources/utils.py new file mode 100644 index 00000000..7fb043b7 --- /dev/null +++ b/investing_bot_framework/tests/resources/utils.py @@ -0,0 +1,10 @@ +import random +import string + +def random_string(n, spaces: bool = False): + + if spaces: + return ''.join(random.choice(string.ascii_lowercase + ' ') for _ in range(n)) + + return ''.join(random.choice(string.ascii_lowercase) for _ in range(n)) + diff --git a/investing_bot_framework/tests/utils/__init__.py b/investing_bot_framework/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/investing_bot_framework/tests/utils/test_version.py b/investing_bot_framework/tests/utils/test_version.py new file mode 100644 index 00000000..9902dc07 --- /dev/null +++ b/investing_bot_framework/tests/utils/test_version.py @@ -0,0 +1,13 @@ +from investing_bot_framework.utils.version import get_version, get_complete_version, get_main_version + + +def test(): + assert get_version() is not None + assert type(get_version()) == str + + version = (1, 0, 0, 'alpha', 0) + assert get_version(version) == '1.0' + assert get_main_version(version) == '1.0' + assert get_complete_version(version) == version + + diff --git a/investing_bot_framework/utils/__init__.py b/investing_bot_framework/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/investing_bot_framework/utils/version.py b/investing_bot_framework/utils/version.py new file mode 100644 index 00000000..c908e667 --- /dev/null +++ b/investing_bot_framework/utils/version.py @@ -0,0 +1,25 @@ +def get_version(version=None): + version = get_complete_version(version) + main = get_main_version(version) + return main + + +def get_main_version(version=None): + """Return main version (X.Y[.Z]) from VERSION.""" + version = get_complete_version(version) + parts = 2 if version[2] == 0 else 3 + return '.'.join(str(x) for x in version[:parts]) + + +def get_complete_version(version=None): + """ + Return a tuple of the investing algorithm framework version. If version argument is non-empty, + check for correctness of the tuple provided. + """ + if version is None: + from investing_bot_framework import VERSION as version + else: + assert len(version) == 5 + assert version[3] in ('alpha', 'beta', 'rc', 'final') + + return version diff --git a/requirements.txt b/requirements.txt index 92cd37ba..dc54f685 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,6 @@ requests==2.22.0 pandas==0.25.3 wrapt==1.11.2 colorama==0.4.3 - +SQLAlchemy==1.3.13 +pytest==5.4.3