1515# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616import argparse
1717import os
18+ import pathlib
1819import sys
1920import multiprocessing
2021import asyncio
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+
106116def _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
116126def _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 ()
0 commit comments