Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,470 @@
1
+ import inspect
2
+ import socket
3
+ import time
4
+ from functools import reduce
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+
9
+ from qubx import formatter, logger, lookup
10
+ from qubx.backtester.account import SimulatedAccountProcessor
11
+ from qubx.backtester.optimization import variate
12
+ from qubx.backtester.simulator import SimulatedBroker, simulate
13
+ from qubx.connectors.ccxt.account import CcxtAccountProcessor
14
+ from qubx.connectors.ccxt.broker import CcxtBroker
15
+ from qubx.connectors.ccxt.data import CcxtDataProvider
16
+ from qubx.connectors.ccxt.factory import get_ccxt_exchange
17
+ from qubx.core.basics import CtrlChannel, Instrument, ITimeProvider, LiveTimeProvider, TransactionCostsCalculator
18
+ from qubx.core.context import StrategyContext
19
+ from qubx.core.exceptions import SimulationConfigError
20
+ from qubx.core.helpers import BasicScheduler
21
+ from qubx.core.interfaces import IAccountProcessor, IBroker, IDataProvider, IStrategyContext
22
+ from qubx.core.loggers import StrategyLogging
23
+ from qubx.data import DataReader
24
+ from qubx.utils.misc import blue, class_import, cyan, green, magenta, makedirs, red, yellow
25
+ from qubx.utils.runner.configs import ExchangeConfig, load_simulation_config_from_yaml, load_strategy_config_from_yaml
26
+
27
+ from .accounts import AccountConfigurationManager
28
+ from .configs import AuxConfig, LoggingConfig, StrategyConfig
29
+
30
+
31
+ def run_strategy_yaml(
32
+ config_file: Path,
33
+ account_file: Path | None = None,
34
+ paper: bool = False,
35
+ blocking: bool = False,
36
+ ) -> IStrategyContext:
37
+ """
38
+ Run the strategy with the given configuration file.
39
+
40
+ Args:
41
+ config_file (Path): The path to the configuration file.
42
+ account_file (Path, optional): The path to the account configuration file. Defaults to None.
43
+ paper (bool, optional): Whether to run in paper trading mode. Defaults to False.
44
+ jupyter (bool, optional): Whether to run in a Jupyter console. Defaults to False.
45
+ """
46
+ if not config_file.exists():
47
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
48
+ if account_file is not None and not account_file.exists():
49
+ raise FileNotFoundError(f"Account configuration file not found: {account_file}")
50
+
51
+ acc_manager = AccountConfigurationManager(account_file, config_file.parent, search_qubx_dir=True)
52
+ stg_config = load_strategy_config_from_yaml(config_file)
53
+ return run_strategy(stg_config, acc_manager, paper=paper, blocking=blocking)
54
+
55
+
56
+ def run_strategy_yaml_in_jupyter(config_file: Path, account_file: Path | None = None, paper: bool = False) -> None:
57
+ """
58
+ Helper for run this in jupyter console
59
+ """
60
+ try:
61
+ from jupyter_console.app import ZMQTerminalIPythonApp
62
+ except ImportError:
63
+ logger.error(
64
+ "Can't find <r>ZMQTerminalIPythonApp</r> module - try to install <g>jupyter-console</g> package first"
65
+ )
66
+ return
67
+ try:
68
+ import nest_asyncio
69
+ except ImportError:
70
+ logger.error("Can't find <r>nest_asyncio</r> module - try to install it first")
71
+ return
72
+
73
+ class TerminalRunner(ZMQTerminalIPythonApp):
74
+ def __init__(self, **kwargs) -> None:
75
+ self.init_code = kwargs.pop("init_code")
76
+ super().__init__(**kwargs)
77
+
78
+ def init_banner(self):
79
+ pass
80
+
81
+ def initialize(self, argv=None):
82
+ super().initialize(argv=[])
83
+ self.shell.run_cell(self.init_code)
84
+
85
+ _base = Path(__file__).parent.absolute()
86
+ with open(_base / "_jupyter_runner.pyt", "r") as f:
87
+ content = f.read()
88
+
89
+ content_with_values = content.format_map({"config_file": config_file, "account_file": account_file, "paper": paper})
90
+ logger.info("Running in Jupyter console")
91
+ TerminalRunner.launch_instance(init_code=content_with_values)
92
+
93
+
94
+ def run_strategy(
95
+ config: StrategyConfig,
96
+ account_manager: AccountConfigurationManager,
97
+ paper: bool = False,
98
+ blocking: bool = False,
99
+ ) -> IStrategyContext:
100
+ """
101
+ Run the strategy with the given configuration.
102
+
103
+ Args:
104
+ config (StrategyConfig): The configuration of the strategy.
105
+ account_manager (AccountManager): The account manager to use.
106
+ paper (bool, optional): Whether to run in paper trading mode. Defaults to False.
107
+ jupyter (bool, optional): Whether to run in a Jupyter console. Defaults to False.
108
+ """
109
+ ctx = create_strategy_context(config, account_manager, paper)
110
+ if blocking:
111
+ try:
112
+ ctx.start(blocking=True)
113
+ except KeyboardInterrupt:
114
+ logger.info("Stopped by user")
115
+ finally:
116
+ ctx.stop()
117
+ else:
118
+ ctx.start()
119
+
120
+ return ctx
121
+
122
+
123
+ def create_strategy_context(
124
+ config: StrategyConfig,
125
+ account_manager: AccountConfigurationManager,
126
+ paper: bool = False,
127
+ ) -> IStrategyContext:
128
+ """
129
+ Create a strategy context from the given configuration.
130
+ """
131
+ stg_name = _get_strategy_name(config)
132
+ _run_mode = "paper" if paper else "live"
133
+
134
+ _strategy_class = class_import(config.strategy)
135
+
136
+ _logging = _setup_strategy_logging(stg_name, config.logging)
137
+ _aux_reader = _get_aux_reader(config.aux)
138
+
139
+ _time = LiveTimeProvider()
140
+ _chan = CtrlChannel("databus", sentinel=(None, None, None, None))
141
+ _sched = BasicScheduler(_chan, lambda: _time.time().item())
142
+
143
+ exchanges = list(config.exchanges.keys())
144
+ if len(exchanges) > 1:
145
+ raise ValueError("Multiple exchanges are not supported yet !")
146
+
147
+ _exchange_to_tcc = {}
148
+ _exchange_to_broker = {}
149
+ _exchange_to_data_provider = {}
150
+ _exchange_to_account = {}
151
+ _instruments = []
152
+ for exchange_name, exchange_config in config.exchanges.items():
153
+ _exchange_to_tcc[exchange_name] = (tcc := _create_tcc(exchange_name, account_manager))
154
+ _exchange_to_data_provider[exchange_name] = _create_data_provider(
155
+ exchange_name,
156
+ exchange_config,
157
+ time_provider=_time,
158
+ channel=_chan,
159
+ account_manager=account_manager,
160
+ )
161
+ _exchange_to_account[exchange_name] = (
162
+ account := _create_account_processor(
163
+ exchange_name,
164
+ exchange_config,
165
+ channel=_chan,
166
+ time_provider=_time,
167
+ account_manager=account_manager,
168
+ tcc=tcc,
169
+ paper=paper,
170
+ )
171
+ )
172
+ _exchange_to_broker[exchange_name] = _create_broker(
173
+ exchange_name,
174
+ exchange_config,
175
+ _chan,
176
+ time_provider=_time,
177
+ account=account,
178
+ account_manager=account_manager,
179
+ paper=paper,
180
+ )
181
+ _instruments.extend(_create_instruments_for_exchange(exchange_name, exchange_config))
182
+
183
+ # TODO: rework strategy context to support multiple exchanges
184
+ _broker = _exchange_to_broker[exchanges[0]]
185
+ _data_provider = _exchange_to_data_provider[exchanges[0]]
186
+ _account = _exchange_to_account[exchanges[0]]
187
+
188
+ logger.info(f"- Strategy: <blue>{stg_name}</blue>\n- Mode: {_run_mode}\n- Parameters: {config.parameters}")
189
+ ctx = StrategyContext(
190
+ strategy=_strategy_class,
191
+ broker=_broker,
192
+ data_provider=_data_provider,
193
+ account=_account,
194
+ scheduler=_sched,
195
+ time_provider=_time,
196
+ instruments=_instruments,
197
+ logging=_logging,
198
+ config=config.parameters,
199
+ aux_data_provider=_aux_reader,
200
+ )
201
+
202
+ return ctx
203
+
204
+
205
+ def _get_strategy_name(cfg: StrategyConfig) -> str:
206
+ return cfg.strategy.split(".")[-1]
207
+
208
+
209
+ def _setup_strategy_logging(stg_name: str, log_config: LoggingConfig) -> StrategyLogging:
210
+ log_id = time.strftime("%Y%m%d%H%M%S", time.gmtime())
211
+ run_folder = f"logs/run_{log_id}"
212
+ logger.add(f"{run_folder}/strategy/{stg_name}_{{time}}.log", format=formatter, rotation="100 MB", colorize=False)
213
+
214
+ run_id = f"{socket.gethostname()}-{str(int(time.time() * 10**9))}"
215
+
216
+ _log_writer_name = log_config.logger
217
+ if "." not in _log_writer_name:
218
+ _log_writer_name = f"qubx.core.loggers.{_log_writer_name}"
219
+
220
+ logger.debug(f"Setup <g>{_log_writer_name}</g> logger...")
221
+ _log_writer_class = class_import(_log_writer_name)
222
+ _log_writer_params = {
223
+ "account_id": "account",
224
+ "strategy_id": stg_name,
225
+ "run_id": run_id,
226
+ "log_folder": run_folder,
227
+ }
228
+ _log_writer_sig_params = inspect.signature(_log_writer_class).parameters
229
+ _log_writer_params = {k: v for k, v in _log_writer_params.items() if k in _log_writer_sig_params}
230
+ _log_writer = _log_writer_class(**_log_writer_params)
231
+ stg_logging = StrategyLogging(_log_writer, heartbeat_freq=log_config.heartbeat_interval)
232
+ return stg_logging
233
+
234
+
235
+ def _get_aux_reader(aux_config: AuxConfig | None) -> DataReader | None:
236
+ if aux_config is None:
237
+ return None
238
+
239
+ from qubx.data.helpers import __KNOWN_READERS # TODO: we need to get rid of using explicit readers lookup here !!!
240
+
241
+ _reader_name = aux_config.reader
242
+ _is_uri = "::" in _reader_name
243
+ if _is_uri:
244
+ # like: mqdb::nebula or csv::/data/rawdata/
245
+ db_conn, db_name = _reader_name.split("::")
246
+ return __KNOWN_READERS[db_conn](db_name, **aux_config.args)
247
+ else:
248
+ # like: sty.data.readers.MyCustomDataReader
249
+ return class_import(_reader_name)(**aux_config.args)
250
+
251
+
252
+ def _create_tcc(exchange_name: str, account_manager: AccountConfigurationManager) -> TransactionCostsCalculator:
253
+ if exchange_name == "BINANCE.PM":
254
+ # TODO: clean this up
255
+ exchange_name = "BINANCE.UM"
256
+ settings = account_manager.get_exchange_settings(exchange_name)
257
+ tcc = lookup.fees.find(exchange_name, settings.commissions)
258
+ assert tcc is not None, f"Can't find fees calculator for {exchange_name} exchange"
259
+ return tcc
260
+
261
+
262
+ def _create_data_provider(
263
+ exchange_name: str,
264
+ exchange_config: ExchangeConfig,
265
+ time_provider: ITimeProvider,
266
+ channel: CtrlChannel,
267
+ account_manager: AccountConfigurationManager,
268
+ ) -> IDataProvider:
269
+ settings = account_manager.get_exchange_settings(exchange_name)
270
+ match exchange_config.connector.lower():
271
+ case "ccxt":
272
+ exchange = get_ccxt_exchange(exchange_name, use_testnet=settings.testnet)
273
+ return CcxtDataProvider(exchange, time_provider, channel)
274
+ case _:
275
+ raise ValueError(f"Connector {exchange_config.connector} is not supported yet !")
276
+
277
+
278
+ def _create_account_processor(
279
+ exchange_name: str,
280
+ exchange_config: ExchangeConfig,
281
+ channel: CtrlChannel,
282
+ time_provider: ITimeProvider,
283
+ account_manager: AccountConfigurationManager,
284
+ tcc: TransactionCostsCalculator,
285
+ paper: bool,
286
+ ) -> IAccountProcessor:
287
+ if paper:
288
+ settings = account_manager.get_exchange_settings(exchange_name)
289
+ return SimulatedAccountProcessor(
290
+ account_id=exchange_name,
291
+ channel=channel,
292
+ base_currency=settings.base_currency,
293
+ time_provider=time_provider,
294
+ tcc=tcc,
295
+ initial_capital=settings.initial_capital,
296
+ )
297
+
298
+ creds = account_manager.get_exchange_credentials(exchange_name)
299
+ match exchange_config.connector.lower():
300
+ case "ccxt":
301
+ exchange = get_ccxt_exchange(
302
+ exchange_name, use_testnet=creds.testnet, api_key=creds.api_key, secret=creds.secret
303
+ )
304
+ return CcxtAccountProcessor(
305
+ exchange_name,
306
+ exchange,
307
+ channel,
308
+ time_provider,
309
+ base_currency=creds.base_currency,
310
+ tcc=tcc,
311
+ )
312
+ case _:
313
+ raise ValueError(f"Connector {exchange_config.connector} is not supported yet !")
314
+
315
+
316
+ def _create_broker(
317
+ exchange_name: str,
318
+ exchange_config: ExchangeConfig,
319
+ channel: CtrlChannel,
320
+ time_provider: ITimeProvider,
321
+ account: IAccountProcessor,
322
+ account_manager: AccountConfigurationManager,
323
+ paper: bool,
324
+ ) -> IBroker:
325
+ if paper:
326
+ assert isinstance(account, SimulatedAccountProcessor)
327
+ return SimulatedBroker(channel=channel, account=account, exchange_id=exchange_name)
328
+
329
+ creds = account_manager.get_exchange_credentials(exchange_name)
330
+
331
+ match exchange_config.connector.lower():
332
+ case "ccxt":
333
+ exchange = get_ccxt_exchange(
334
+ exchange_name, use_testnet=creds.testnet, api_key=creds.api_key, secret=creds.secret
335
+ )
336
+ return CcxtBroker(exchange, channel, time_provider, account)
337
+ case _:
338
+ raise ValueError(f"Connector {exchange_config.connector} is not supported yet !")
339
+
340
+
341
+ def _create_instruments_for_exchange(exchange_name: str, exchange_config: ExchangeConfig) -> list[Instrument]:
342
+ exchange_name = exchange_name.upper()
343
+ if exchange_name == "BINANCE.PM":
344
+ # TODO: clean this up
345
+ exchange_name = "BINANCE.UM"
346
+ symbols = exchange_config.universe
347
+ instruments = [lookup.find_symbol(exchange_name, symbol.upper()) for symbol in symbols]
348
+ instruments = [i for i in instruments if i is not None]
349
+ return instruments
350
+
351
+
352
+ def simulate_strategy(
353
+ config_file: Path, save_path: str | None = None, start: str | None = None, stop: str | None = None
354
+ ):
355
+ """
356
+ Run a backtest simulation of a trading strategy using configuration from a YAML file.
357
+
358
+ Args:
359
+ config_file (Path): Path to the YAML configuration file containing strategy and simulation parameters
360
+ save_path (str, optional): Directory to save simulation results. Defaults to "results/" if None.
361
+ start (str, optional): Override simulation start date from config. Format: "YYYY-MM-DD". Defaults to None.
362
+ stop (str, optional): Override simulation end date from config. Format: "YYYY-MM-DD". Defaults to None.
363
+
364
+ Returns:
365
+ The simulation results object containing performance metrics and trade history.
366
+
367
+ Raises:
368
+ FileNotFoundError: If config_file does not exist
369
+ SimulationConfigError: If strategy configuration is invalid
370
+ ValueError: If required simulation parameters are missing
371
+
372
+ The configuration file should contain:
373
+ - strategy: Strategy class path(s) as string or list
374
+ - parameters: Strategy initialization parameters
375
+ - data: Data source configurations
376
+ - simulation: Backtest parameters (instruments, capital, commissions, start/stop dates)
377
+ """
378
+ from qubx.data.helpers import loader
379
+
380
+ if not config_file.exists():
381
+ raise FileNotFoundError(f"Configuration file for simualtion not found: {config_file}")
382
+
383
+ cfg = load_simulation_config_from_yaml(config_file)
384
+ stg = cfg.strategy
385
+ simulation_name = config_file.stem
386
+ _v_id = pd.Timestamp("now").strftime("%Y%m%d%H%M%S")
387
+
388
+ match stg:
389
+ case list():
390
+ stg_cls = reduce(lambda x, y: x + y, [class_import(x) for x in stg])
391
+ case str():
392
+ stg_cls = class_import(stg)
393
+ case _:
394
+ raise SimulationConfigError(f"Invalid strategy type: {stg}")
395
+
396
+ # - create simulation setup
397
+ if cfg.variate:
398
+ # - get conditions for variations if exists
399
+ cond = cfg.variate.pop("with", None)
400
+ conditions = []
401
+ dict2lambda = lambda a, d: eval(f"lambda {a}: {d}") # noqa: E731
402
+ if cond:
403
+ for a, c in cond.items():
404
+ conditions.append(dict2lambda(a, c))
405
+
406
+ experiments = variate(stg_cls, **(cfg.parameters | cfg.variate), conditions=conditions)
407
+ experiments = {f"{simulation_name}.{_v_id}.[{k}]": v for k, v in experiments.items()}
408
+ print(f"Parameters variation is configured. There are {len(experiments)} simulations to run.")
409
+ _n_jobs = -1
410
+ else:
411
+ strategy = stg_cls(**cfg.parameters)
412
+ experiments = {simulation_name: strategy}
413
+ _n_jobs = 1
414
+
415
+ data_i = {}
416
+
417
+ for k, v in cfg.data.items():
418
+ data_i[k] = eval(v)
419
+
420
+ sim_params = cfg.simulation
421
+ for mp in ["instruments", "capital", "commissions", "start", "stop"]:
422
+ if mp not in sim_params:
423
+ raise ValueError(f"Simulation parameter {mp} is required")
424
+
425
+ if start is not None:
426
+ sim_params["start"] = start
427
+ logger.info(f"Start date set to {start}")
428
+
429
+ if stop is not None:
430
+ sim_params["stop"] = stop
431
+ logger.info(f"Stop date set to {stop}")
432
+
433
+ # - check for aux_data parameter
434
+ if "aux_data" in sim_params:
435
+ aux_data = sim_params.pop("aux_data")
436
+ if aux_data is not None:
437
+ try:
438
+ sim_params["aux_data"] = eval(aux_data)
439
+ except Exception as e:
440
+ raise ValueError(f"Invalid aux_data parameter: {aux_data}") from e
441
+
442
+ # - run simulation
443
+ print(f" > Run simulation for [{red(simulation_name)}] ::: {sim_params['start']} - {sim_params['stop']}")
444
+ sim_params["n_jobs"] = sim_params.get("n_jobs", _n_jobs)
445
+ test_res = simulate(experiments, data=data_i, **sim_params)
446
+
447
+ _where_to_save = save_path if save_path is not None else Path("results/")
448
+ s_path = Path(makedirs(str(_where_to_save))) / simulation_name
449
+
450
+ # logger.info(f"Saving simulation results to <g>{s_path}</g> ...")
451
+ if cfg.description is not None:
452
+ _descr = cfg.description
453
+ if isinstance(cfg.description, list):
454
+ _descr = "\n".join(cfg.description)
455
+ else:
456
+ _descr = str(cfg.description)
457
+
458
+ if len(test_res) > 1:
459
+ # - TODO: think how to deal with variations !
460
+ s_path = s_path / f"variations.{_v_id}"
461
+ print(f" > Saving variations results to <g>{s_path}</g> ...")
462
+ for k, t in enumerate(test_res):
463
+ # - set variation name
464
+ t.variation_name = f"{simulation_name}.{_v_id}"
465
+ t.to_file(str(s_path), description=_descr, suffix=f".{k}", attachments=[str(config_file)])
466
+ else:
467
+ print(f" > Saving simulation results to <g>{s_path}</g> ...")
468
+ test_res[0].to_file(str(s_path), description=_descr, attachments=[str(config_file)])
469
+
470
+ return test_res