Skip to content

Commit 060ca0e

Browse files
committed
[Flow] add bot process keywords
1 parent 64868c6 commit 060ca0e

57 files changed

Lines changed: 3422 additions & 155 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

octobot/cli.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616
import argparse
1717
import os
18+
import pathlib
1819
import sys
1920
import multiprocessing
2021
import asyncio
@@ -30,6 +31,7 @@
3031
import octobot_commons.authentication as authentication
3132
import octobot_commons.constants as common_constants
3233
import octobot_commons.errors as errors
34+
import octobot_commons.user_root_folder_provider as user_root_folder_provider
3335

3436
import octobot_services.api as service_api
3537

@@ -103,6 +105,14 @@ def _disable_interface_from_param(interface_identifier, param_value, logger):
103105
logger.info(interface_identifier.capitalize() + " interface disabled")
104106

105107

108+
def _set_user_root_from_cli(user_folder: str) -> None:
109+
if not (user_folder and str(user_folder).strip()):
110+
raise errors.ConfigError("User folder must be a non-empty path.")
111+
if ".." in pathlib.PurePath(user_folder).parts:
112+
raise errors.ConfigError("Invalid user folder: parent directory segments are not allowed.")
113+
user_root_folder_provider.instance().set_root(os.path.normpath(user_folder))
114+
115+
106116
def _log_environment(logger):
107117
try:
108118
bot_type = "cloud" if constants.IS_CLOUD_ENV else "self-hosted"
@@ -115,10 +125,12 @@ def _log_environment(logger):
115125

116126
def _create_configuration():
117127
config_path = configuration.get_user_config()
118-
config = configuration.Configuration(config_path,
119-
common_constants.USER_PROFILES_FOLDER,
120-
constants.CONFIG_FILE_SCHEMA,
121-
constants.PROFILE_FILE_SCHEMA)
128+
config = configuration.Configuration(
129+
config_path,
130+
user_root_folder_provider.get_user_profiles_folder(),
131+
constants.CONFIG_FILE_SCHEMA,
132+
constants.PROFILE_FILE_SCHEMA,
133+
)
122134
return config
123135

124136

@@ -297,7 +309,9 @@ def _load_or_create_tentacles(community_auth, config, logger):
297309
# add tentacles folder to Python path
298310
sys.path.append(os.path.realpath(os.getcwd()))
299311

300-
if os.path.isfile(tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_FILE_PATH):
312+
if os.path.isfile(
313+
user_root_folder_provider.get_user_reference_tentacle_config_file_path()
314+
):
301315
# when tentacles folder already exists
302316
config.load_profiles_if_possible_and_necessary()
303317
tentacles_setup_config = tentacles_manager_api.get_tentacles_setup_config(
@@ -318,9 +332,13 @@ def start_octobot(args, default_config_file=None):
318332
print(constants.LONG_VERSION)
319333
return
320334

321-
# log folder can be overridden by the LOGS_FOLDER environment variable,
322-
# useful to run multiple bots from the same folder
323-
logger = octobot_logger.init_logger(logs_folder=constants.LOGS_FOLDER)
335+
user_folder = getattr(args, "user_folder", None)
336+
if user_folder:
337+
_set_user_root_from_cli(user_folder)
338+
339+
# log folder: --log-folder overrides default (from LOGS_FOLDER env at import + default "logs")
340+
logs_folder = getattr(args, "log_folder", None) or constants.LOGS_FOLDER
341+
logger = octobot_logger.init_logger(logs_folder=logs_folder)
324342
startup_messages = []
325343

326344
# Version
@@ -384,6 +402,11 @@ def start_octobot(args, default_config_file=None):
384402
reset_trading_history=args.reset_trading_history,
385403
startup_messages=startup_messages)
386404

405+
if not args.backtesting:
406+
path = getattr(args, "dump_state", None)
407+
if path:
408+
bot.dump_state_path = os.path.normpath(path)
409+
387410
# set global bot instance
388411
commands.set_global_bot_instance(bot)
389412

@@ -473,6 +496,18 @@ def octobot_parser(parser, default_config_file=None):
473496
'When disabled, the backtesting run will not be interrupted during execution',
474497
action='store_true')
475498
parser.add_argument('-r', '--risk', type=float, help='Force a specific risk configuration (between 0 and 1).')
499+
parser.add_argument(
500+
'--user-folder',
501+
type=str,
502+
default=None,
503+
help='User data root (config, profiles, reference tentacles). Relative to the current working directory.',
504+
)
505+
parser.add_argument(
506+
'--log-folder',
507+
type=str,
508+
default=None,
509+
help='Log files directory. When set, overrides the LOGS_FOLDER environment variable and default "logs".',
510+
)
476511
parser.add_argument('-nw', '--no_web', help="Don't start OctoBot web interface.",
477512
action='store_true')
478513
parser.add_argument('-nl', '--no_logs', help="Disable OctoBot logs in backtesting.",
@@ -486,6 +521,13 @@ def octobot_parser(parser, default_config_file=None):
486521
" exchanges configuration in your config.json without using any interface "
487522
"(ie the web interface that handle encryption automatically).",
488523
action='store_true')
524+
parser.add_argument(
525+
"--dump-state",
526+
type=str,
527+
default=None,
528+
help="Absolute path of the JSON file where OctoBot periodically writes ProcessBotState (liveness, "
529+
"next to the user config directory). Omitted in normal use; spawned DSL children pass this explicitly.",
530+
)
489531
parser.add_argument('--identifier', help="OctoBot community identifier.", type=str, nargs=1)
490532
parser.add_argument('-o', '--strategy_optimizer', help='Start Octobot strategy optimizer. This mode will make '
491533
'octobot play backtesting scenarii located in '
@@ -603,6 +645,8 @@ def start_background_octobot_with_args(
603645
in_subprocess=False,
604646
reset_trading_history=False,
605647
default_config_file=None,
648+
user_folder=None,
649+
log_folder=None,
606650
):
607651
if backtesting_files is None:
608652
backtesting_files = []
@@ -621,7 +665,9 @@ def start_background_octobot_with_args(
621665
enable_backtesting_timeout=enable_backtesting_timeout,
622666
simulate=simulate,
623667
risk=risk,
624-
reset_trading_history=reset_trading_history)
668+
reset_trading_history=reset_trading_history,
669+
user_folder=user_folder,
670+
log_folder=log_folder)
625671
if in_subprocess:
626672
bot_process = multiprocessing.Process(target=start_octobot, args=(args, default_config_file))
627673
bot_process.start()

octobot/commands.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import octobot.community.tentacles_packages as community_tentacles_packages
4040
import octobot.configuration_manager as configuration_manager
4141

42+
import octobot.storage.process_bot_state_dumper as process_bot_state_dumper
43+
4244
COMMANDS_LOGGER_NAME = "Commands"
4345
IGNORED_COMMAND_WHEN_RESTART = ["-u", "--update"]
4446

@@ -324,6 +326,12 @@ async def start_bot(bot, logger, catch=False):
324326
await bot.initialize()
325327
except asyncio.CancelledError:
326328
logger.info("Core engine tasks cancelled.")
329+
else:
330+
if bot.dump_state_path:
331+
332+
bot._process_bot_state_dump_task = asyncio.create_task(
333+
process_bot_state_dumper.run_periodic_dump_loop(bot.dump_state_path, logger, bot)
334+
)
327335

328336
except Exception as e:
329337
logger.exception(e)

octobot/configuration_manager.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import octobot.constants as constants
2121
import octobot_commons.configuration as configuration
2222
import octobot_commons.constants as common_constants
23+
import octobot_commons.user_root_folder_provider as user_root_folder_provider
2324
import octobot_commons.logging as logging
2425
import octobot_commons.json_util as json_util
2526
import octobot_tentacles_manager.constants as tentacles_manager_constants
@@ -123,8 +124,9 @@ def init_config(
123124
:param from_config_file: the default config file path
124125
"""
125126
try:
126-
if not os.path.exists(common_constants.USER_FOLDER):
127-
os.makedirs(common_constants.USER_FOLDER)
127+
user_root = user_root_folder_provider.get_user_root_folder()
128+
if not os.path.exists(user_root):
129+
os.makedirs(user_root)
128130

129131
shutil.copyfile(from_config_file, config_file)
130132
except Exception as global_exception:
@@ -169,35 +171,48 @@ def get_default_tentacles_url(version=None):
169171

170172
def get_user_local_config_file():
171173
try:
172-
import octobot_commons.constants as commons_constants
173-
return f"{commons_constants.USER_FOLDER}/logging_config.ini"
174+
import octobot_commons.user_root_folder_provider as user_root_folder_provider
175+
176+
return os.path.join(
177+
user_root_folder_provider.get_user_root_folder(), "logging_config.ini"
178+
)
174179
except ImportError:
175180
return None
176181

177182

178183
def load_default_tentacles_config(profile_folder):
179-
if os.path.isdir(tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_PATH):
180-
shutil.copyfile(tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_FILE_PATH,
181-
os.path.join(profile_folder, tentacles_manager_constants.constants.CONFIG_TENTACLES_FILE))
182-
shutil.copytree(tentacles_manager_constants.USER_REFERENCE_TENTACLE_SPECIFIC_CONFIG_PATH,
183-
os.path.join(profile_folder, tentacles_manager_constants.TENTACLES_SPECIFIC_CONFIG_FOLDER))
184+
ref_path = user_root_folder_provider.get_user_reference_tentacle_config_path()
185+
ref_file = user_root_folder_provider.get_user_reference_tentacle_config_file_path()
186+
ref_spec = user_root_folder_provider.get_user_reference_tentacle_specific_config_path()
187+
if os.path.isdir(ref_path):
188+
shutil.copyfile(
189+
ref_file,
190+
os.path.join(profile_folder, tentacles_manager_constants.constants.CONFIG_TENTACLES_FILE),
191+
)
192+
shutil.copytree(
193+
ref_spec,
194+
os.path.join(profile_folder, tentacles_manager_constants.TENTACLES_SPECIFIC_CONFIG_FOLDER),
195+
)
184196

185197

186198
def migrate_from_previous_config(config):
187199
logger = logging.get_logger(LOGGER_NAME)
188200
# migrate tentacles configuration if necessary
189-
previous_tentacles_config = os.path.join(common_constants.USER_FOLDER, "tentacles_config")
190-
previous_tentacles_config_save = os.path.join(common_constants.USER_FOLDER, "tentacles_config.back")
191-
if os.path.isdir(previous_tentacles_config) and \
192-
not os.path.isdir(tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_PATH):
201+
user_root = user_root_folder_provider.get_user_root_folder()
202+
ref_tent_path = user_root_folder_provider.get_user_reference_tentacle_config_path()
203+
previous_tentacles_config = os.path.join(user_root, "tentacles_config")
204+
previous_tentacles_config_save = os.path.join(user_root, "tentacles_config.back")
205+
if os.path.isdir(previous_tentacles_config) and not os.path.isdir(ref_tent_path):
193206
logger.info(
194207
f"Updating your tentacles configuration located in {previous_tentacles_config} into the new format. "
195208
f"A save of your previous tentacles config is available in {previous_tentacles_config_save}")
196-
shutil.copytree(previous_tentacles_config,
197-
tentacles_manager_constants.USER_REFERENCE_TENTACLE_CONFIG_PATH)
209+
shutil.copytree(previous_tentacles_config, ref_tent_path)
198210
shutil.move(previous_tentacles_config, previous_tentacles_config_save)
199211
load_default_tentacles_config(
200-
os.path.join(common_constants.USER_PROFILES_FOLDER, common_constants.DEFAULT_PROFILE)
212+
os.path.join(
213+
user_root_folder_provider.get_user_profiles_folder(),
214+
common_constants.DEFAULT_PROFILE,
215+
)
201216
)
202217
# migrate global configuration if necessary
203218
config_path = configuration.get_user_config()

octobot/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@
202202
# logs
203203
DEFAULT_LOGS_FOLDER = "logs"
204204
LOGS_FOLDER = os.getenv("LOGS_FOLDER", DEFAULT_LOGS_FOLDER)
205+
206+
# Web automation: child process sets OCTOBOT_WEB_API_KEY
207+
ENV_WEB_API_KEY = "OCTOBOT_WEB_API_KEY"
208+
WEB_API_KEY_HEADER = "X-Octobot-Api-Key"
209+
# Process bot state JSON next to user config (--dump-state); liveness for run_octobot_process
210+
PROCESS_BOT_STATE_FILE_NAME = "process_bot_state.json"
211+
ENV_PROCESS_BOT_STATE_DUMP_INTERVAL_SECONDS = "OCTOBOT_PROCESS_BOT_STATE_DUMP_INTERVAL_SECONDS"
212+
PROCESS_BOT_STATE_DUMP_INTERVAL_SECONDS = float(
213+
os.getenv(ENV_PROCESS_BOT_STATE_DUMP_INTERVAL_SECONDS, "30")
214+
)
205215
FORCED_LOG_LEVEL = os.getenv("FORCED_LOG_LEVEL", "")
206216
ENV_TRADING_ENABLE_DEBUG_LOGS = os_util.parse_boolean_environment_var("ENV_TRADING_ENABLE_DEBUG_LOGS", "False")
207217

octobot/logger.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
import octobot.constants as constants
4040
import octobot.configuration_manager as configuration_manager
41+
import octobot_commons.user_root_folder_provider as user_root_folder_provider
4142

4243
BOT_CHANNEL_LOGGER = None
4344
LOGGER_PRIORITY_LEVEL = channel_enums.ChannelConsumerPriorityLevels.OPTIONAL.value
@@ -51,7 +52,7 @@ def _log_uncaught_exceptions(ex_cls, ex, tb):
5152
def init_logger(logs_folder: str = constants.DEFAULT_LOGS_FOLDER):
5253
try:
5354
if not os.path.exists(logs_folder):
54-
os.mkdir(logs_folder)
55+
os.makedirs(logs_folder)
5556
_load_logger_config(logs_folder)
5657
init_bot_channel_logger()
5758
except KeyError:
@@ -88,8 +89,9 @@ def _load_logger_config(logs_folder: str):
8889
try:
8990
# use local logging file to allow users to customize the log level
9091
if not os.path.isfile(configuration_manager.get_user_local_config_file()):
91-
if not os.path.exists(commons_constants.USER_FOLDER):
92-
os.mkdir(commons_constants.USER_FOLDER)
92+
user_root = user_root_folder_provider.get_user_root_folder()
93+
if not os.path.exists(user_root):
94+
os.mkdir(user_root)
9395
shutil.copyfile(constants.LOGGING_CONFIG_FILE, configuration_manager.get_user_local_config_file())
9496
logging.config.fileConfig(configuration_manager.get_user_local_config_file())
9597
logger = logging.getLogger("Logging Configuration")

octobot/octobot.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def __init__(self, config: configuration.Configuration, community_authenticator=
105105
self.initializer = initializer.Initializer(self)
106106
self.task_manager = task_manager.TaskManager(self)
107107
self._init_metadata_run_task = None
108+
# optional path for periodic ProcessBotState JSON (see cli --dump-state)
109+
self.dump_state_path = None
110+
self._process_bot_state_dump_task = None
108111

109112
# Producers
110113
self.exchange_producer = None
@@ -211,6 +214,9 @@ async def stop(self):
211214
self.logger.debug("Stopping ...")
212215
if self._init_metadata_run_task is not None and not self._init_metadata_run_task.done():
213216
self._init_metadata_run_task.cancel()
217+
if self._process_bot_state_dump_task is not None and not self._process_bot_state_dump_task.done():
218+
self._process_bot_state_dump_task.cancel()
219+
self._process_bot_state_dump_task = None
214220
signals.SignalPublisher.instance().stop()
215221
if self.evaluator_producer is not None:
216222
await self.evaluator_producer.stop()

0 commit comments

Comments
 (0)