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.
- qubx/__init__.py +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- 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
|