Skip to content

Commit

Permalink
[Profile] handle auto-update
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Jul 3, 2024
1 parent f7147c0 commit cba6322
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 27 deletions.
3 changes: 3 additions & 0 deletions octobot_commons/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@
DEFAULT_PROFILE = "default"
DEFAULT_PROFILE_FILE = f"{CONFIG_PROFILE}.json"
CONFIG_NAME = "name"
CONFIG_SLUG = "slug"
CONFIG_DESCRIPTION = "description"
CONFIG_AVATAR = "avatar"
CONFIG_ORIGIN_URL = "origin_url"
CONFIG_AUTO_UPDATE = "auto_update"
CONFIG_READ_ONLY = "read_only"
CONFIG_IMPORTED = "imported"
CONFIG_EXTRA_BACKTESTING_TIME_FRAMES = "extra_backtesting_time_frames"
Expand All @@ -76,6 +78,7 @@
PROFILE_EXPORT_FORMAT = "zip"
IMPORTED_PROFILE_PREFIX = "imported"
USE_CURRENT_PROFILE = "use_current_profile"
PROFILE_REFRESH_HOURS_INTERVAL = int(os.getenv("PROFILE_REFRESH_HOURS_INTERVAL", "24"))

# Config currencies
CONFIG_CRYPTO_CURRENCIES = "crypto-currencies"
Expand Down
10 changes: 10 additions & 0 deletions octobot_commons/json_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ def validate(config, schema_file) -> None:
jsonschema.validate(instance=config, schema=loaded_schema)


def has_same_content(file_path: str, expected_content: dict) -> bool:
"""
:return: True if the content of the parsed json file at file_path equals the given expected_content
"""
if os.path.isfile(file_path):
content = read_file(file_path, raise_errors=False)
return content == expected_content
return False


def read_file(
file_path: str,
raise_errors: bool = True,
Expand Down
11 changes: 11 additions & 0 deletions octobot_commons/profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
install_profile,
import_profile,
import_profile_data_as_profile,
update_profile,
download_profile,
download_and_install_profile,
)
Expand All @@ -40,17 +41,27 @@
OptionsData,
)

from octobot_commons.profiles import profile_sync

from octobot_commons.profiles.profile_sync import (
start_profile_synchronizer,
stop_profile_synchronizer,
)


__all__ = [
"Profile",
"export_profile",
"install_profile",
"import_profile",
"import_profile_data_as_profile",
"update_profile",
"download_profile",
"download_and_install_profile",
"ProfileData",
"ExchangeData",
"MinimalFund",
"OptionsData",
"start_profile_synchronizer",
"stop_profile_synchronizer",
]
7 changes: 7 additions & 0 deletions octobot_commons/profiles/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ def __init__(self, profile_path: str, schema_path: str = None):
self.path: str = profile_path
self.schema_path: str = schema_path or constants.PROFILE_FILE_SCHEMA
self.name: str = None
self.slug: str = None
self.description: str = None
self.avatar: str = None
self.avatar_path: str = None
self.origin_url: str = None
self.auto_update: bool = False
self.read_only: bool = False
self.imported: bool = False
self.complexity: enums.ProfileComplexity = enums.ProfileComplexity.MEDIUM
Expand All @@ -91,9 +93,11 @@ def from_dict(self, profile_dict: dict):
profile_config = profile_dict.get(constants.CONFIG_PROFILE, {})
self.profile_id = profile_config.get(constants.CONFIG_ID, str(uuid.uuid4()))
self.name = profile_config.get(constants.CONFIG_NAME, "")
self.slug = profile_config.get(constants.CONFIG_SLUG, "")
self.description = profile_config.get(constants.CONFIG_DESCRIPTION, "")
self.avatar = profile_config.get(constants.CONFIG_AVATAR, "")
self.origin_url = profile_config.get(constants.CONFIG_ORIGIN_URL, None)
self.auto_update = profile_config.get(constants.CONFIG_AUTO_UPDATE, False)
self.read_only = profile_config.get(constants.CONFIG_READ_ONLY, False)
self.imported = profile_config.get(constants.CONFIG_IMPORTED, False)
self.complexity = enums.ProfileComplexity(
Expand Down Expand Up @@ -221,6 +225,7 @@ def duplicate(self, name: str = None, description: str = None):
clone.read_only = False
clone.imported = False
clone.origin_url = None
clone.auto_update = False
try:
clone.path = os.path.join(
os.path.split(self.path)[0], f"{clone.name}_{clone.profile_id}"
Expand All @@ -247,9 +252,11 @@ def as_dict(self) -> dict:
constants.CONFIG_PROFILE: {
constants.CONFIG_ID: self.profile_id,
constants.CONFIG_NAME: self.name,
constants.CONFIG_SLUG: self.slug,
constants.CONFIG_DESCRIPTION: self.description,
constants.CONFIG_AVATAR: self.avatar,
constants.CONFIG_ORIGIN_URL: self.origin_url,
constants.CONFIG_AUTO_UPDATE: self.auto_update,
constants.CONFIG_READ_ONLY: self.read_only,
constants.CONFIG_IMPORTED: self.imported,
constants.CONFIG_COMPLEXITY: self.complexity.value
Expand Down
140 changes: 114 additions & 26 deletions octobot_commons/profiles/profile_data_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import copy
import os
import uuid

import octobot_commons.profiles.profile_data as profile_data_import
import octobot_commons.profiles.profile as profile_import
import octobot_commons.logging as bot_logging
import octobot_commons.json_util as json_util
import octobot_commons.constants as constants
Expand All @@ -30,56 +32,88 @@
)


def init_profile_directory(
output_path: str,
):
"""
:param output_path: profile folder path
"""
if os.path.exists(output_path):
raise OSError(f"{output_path} already exists")
os.mkdir(output_path)


async def convert_profile_data_to_profile_directory(
profile_data: profile_data_import.ProfileData,
description: str,
risk: enums.ProfileRisk,
avatar_url: str,
output_path: str,
aiohttp_session,
):
description: str = None,
risk: enums.ProfileRisk = None,
auto_update: bool = False,
slug: str = None,
avatar_url: str = None,
aiohttp_session=None,
profile_to_update: profile_import.Profile = None,
changed: bool = False,
) -> bool:
"""
Creates a profile folder from the given ProfileData
:param profile_data: path to the profile zipped archive
:param description: profile description
:param risk: profile risk
:param slug: slug of the associated strategy
:param auto_update: True if the profile should be kept up-to-date
:param avatar_url: profile avatar_url
:param output_path: profile folder path
:param aiohttp_session: session to use
:param profile_to_update: profile to update instead of creating a new one
:param changed: if True, profile will be saved even if no change are identified
"""
logger = bot_logging.get_logger(__name__)
if os.path.exists(output_path):
raise OSError(f"{output_path} already exists")
os.mkdir(output_path)
profile = _get_profile(profile_data, description, risk, output_path)
profile = (
profile_to_update
if profile_to_update
else _get_profile(
profile_data, description, risk, output_path, auto_update, slug
)
)
# tentacles_config.json
tentacles_setup_config = _get_tentacles_setup_config(profile_data, output_path)
tentacles_setup_config.save_config()
if tentacles_setup_config.save_config(is_config_update=True):
changed = True
# specific_config
_save_specific_config(profile_data, output_path)
if _save_specific_config(profile_data, output_path, bool(profile_to_update)):
changed = True
# avatar file
try:
await _download_and_set_avatar(
profile, avatar_url, output_path, aiohttp_session
)
except Exception as err:
logger.exception(err, True, f"Error when downloading profile avatar: {err}")
if avatar_url:
try:
await _download_and_set_avatar(
profile, avatar_url, output_path, aiohttp_session
)
except Exception as err:
bot_logging.get_logger(__name__).exception(
err, True, f"Error when downloading profile avatar: {err}"
)
# finish with profile.json to include edits from previous methods
profile.save()
if changed:
profile.save()
return changed


def _get_profile(
profile_data: profile_data_import.ProfileData,
description: str,
risk: enums.ProfileRisk,
output_path: str,
auto_update: bool,
slug: str,
):
profile = profile_data.to_profile(output_path)
# use trading simulator by default
profile.config[constants.CONFIG_TRADER][constants.CONFIG_ENABLED_OPTION] = False
profile.config[constants.CONFIG_SIMULATOR][constants.CONFIG_ENABLED_OPTION] = True
profile.description = description
profile.risk = risk
profile.auto_update = auto_update
profile.slug = slug
profile.profile_id = str(uuid.uuid4().hex)
profile.read_only = True
profile.extra_backtesting_time_frames = [
Expand All @@ -88,6 +122,48 @@ def _get_profile(
return profile


def get_updated_profile(
profile_to_update: profile_import.Profile,
profile_data: profile_data_import.ProfileData,
) -> bool:
"""
:param profile_to_update: the profile to be updated
:param profile_data: the profile_data to get the update from
:return: True if something changed in the updated profile
"""
updated_profile = profile_data.to_profile("")
changed = False
# update traded currencies (add new currencies)
origin_currencies = copy.deepcopy(
profile_to_update.config[constants.CONFIG_CRYPTO_CURRENCIES]
)
profile_to_update.config[constants.CONFIG_CRYPTO_CURRENCIES] = {
**origin_currencies,
**updated_profile.config[constants.CONFIG_CRYPTO_CURRENCIES],
}
if (
origin_currencies
!= profile_to_update.config[constants.CONFIG_CRYPTO_CURRENCIES]
):
changed = True
# update ref market
origin_ref_market = profile_to_update.config[constants.CONFIG_TRADING][
constants.CONFIG_TRADER_REFERENCE_MARKET
]
profile_to_update.config[constants.CONFIG_TRADING][
constants.CONFIG_TRADER_REFERENCE_MARKET
] = profile_data.trading.reference_market
if (
origin_ref_market
!= profile_to_update.config[constants.CONFIG_TRADING][
constants.CONFIG_TRADER_REFERENCE_MARKET
]
):
changed = True
# leave other fields as is (tentacles config will be updated)
return changed


def _get_tentacles_setup_config(
profile_data: profile_data_import.ProfileData, output_path: str
):
Expand Down Expand Up @@ -115,26 +191,38 @@ def _get_tentacles_setup_config(


def _save_specific_config(
profile_data: profile_data_import.ProfileData, output_path: str
):
profile_data: profile_data_import.ProfileData,
output_path: str,
is_config_update: bool,
) -> bool:
changed = False
try:
import octobot_tentacles_manager.constants

specific_config_dir = os.path.join(
output_path,
octobot_tentacles_manager.constants.TENTACLES_SPECIFIC_CONFIG_FOLDER,
)
os.mkdir(specific_config_dir)
if not os.path.exists(specific_config_dir):
os.mkdir(specific_config_dir)
for tentacle_config in profile_data.tentacles:
file_path = os.path.join(
specific_config_dir,
f"{tentacle_config.name}{octobot_tentacles_manager.constants.CONFIG_EXT}",
)
if is_config_update and json_util.has_same_content(
file_path, tentacle_config.config
):
# nothing to do
continue
changed = True
json_util.safe_dump(
tentacle_config.config,
os.path.join(
specific_config_dir,
f"{tentacle_config.name}{octobot_tentacles_manager.constants.CONFIG_EXT}",
),
file_path,
)
except ImportError:
raise
return changed


async def _download_and_set_avatar(
Expand Down
Loading

0 comments on commit cba6322

Please sign in to comment.