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
qubx/backtester/ome.py ADDED
@@ -0,0 +1,296 @@
1
+ from dataclasses import dataclass
2
+ from operator import neg
3
+
4
+ import numpy as np
5
+ from sortedcontainers import SortedDict
6
+
7
+ from qubx import logger
8
+ from qubx.core.basics import (
9
+ OPTION_FILL_AT_SIGNAL_PRICE,
10
+ Deal,
11
+ Instrument,
12
+ ITimeProvider,
13
+ Order,
14
+ OrderSide,
15
+ OrderType,
16
+ TransactionCostsCalculator,
17
+ dt_64,
18
+ )
19
+ from qubx.core.exceptions import (
20
+ ExchangeError,
21
+ InvalidOrder,
22
+ )
23
+ from qubx.core.series import Quote
24
+
25
+
26
+ @dataclass
27
+ class OmeReport:
28
+ timestamp: dt_64
29
+ order: Order
30
+ exec: Deal | None
31
+
32
+
33
+ class OrdersManagementEngine:
34
+ instrument: Instrument
35
+ time_service: ITimeProvider
36
+ active_orders: dict[str, Order]
37
+ stop_orders: dict[str, Order]
38
+ asks: SortedDict[float, list[str]]
39
+ bids: SortedDict[float, list[str]]
40
+ bbo: Quote | None # current best bid/ask order book (simplest impl)
41
+ __order_id: int
42
+ __trade_id: int
43
+ _fill_stops_at_price: bool
44
+
45
+ def __init__(
46
+ self,
47
+ instrument: Instrument,
48
+ time_provider: ITimeProvider,
49
+ tcc: TransactionCostsCalculator,
50
+ fill_stop_order_at_price: bool = False, # emulate stop orders execution at order's exact limit price
51
+ debug: bool = True,
52
+ ) -> None:
53
+ self.instrument = instrument
54
+ self.time_service = time_provider
55
+ self.tcc = tcc
56
+ self.asks = SortedDict()
57
+ self.bids = SortedDict(neg)
58
+ self.active_orders = dict()
59
+ self.stop_orders = dict()
60
+ self.bbo = None
61
+ self.__order_id = 100000
62
+ self.__trade_id = 100000
63
+ self._fill_stops_at_price = fill_stop_order_at_price
64
+ if not debug:
65
+ self._dbg = lambda message, **kwargs: None
66
+
67
+ def _generate_order_id(self) -> str:
68
+ self.__order_id += 1
69
+ return "SIM-ORDER-" + self.instrument.symbol + "-" + str(self.__order_id)
70
+
71
+ def _generate_trade_id(self) -> str:
72
+ self.__trade_id += 1
73
+ return "SIM-EXEC-" + self.instrument.symbol + "-" + str(self.__trade_id)
74
+
75
+ def get_quote(self) -> Quote:
76
+ return self.bbo
77
+
78
+ def get_open_orders(self) -> list[Order]:
79
+ return list(self.active_orders.values()) + list(self.stop_orders.values())
80
+
81
+ def update_bbo(self, quote: Quote) -> list[OmeReport]:
82
+ timestamp = self.time_service.time()
83
+ rep = []
84
+
85
+ if self.bbo is not None:
86
+ if quote.bid >= self.bbo.ask:
87
+ for level in self.asks.irange(0, quote.bid):
88
+ for order_id in self.asks[level]:
89
+ order = self.active_orders.pop(order_id)
90
+ rep.append(self._execute_order(timestamp, order.price, order, False))
91
+ self.asks.pop(level)
92
+
93
+ if quote.ask <= self.bbo.bid:
94
+ for level in self.bids.irange(np.inf, quote.ask):
95
+ for order_id in self.bids[level]:
96
+ order = self.active_orders.pop(order_id)
97
+ rep.append(self._execute_order(timestamp, order.price, order, False))
98
+ self.bids.pop(level)
99
+
100
+ # - processing stop orders
101
+ for soid in list(self.stop_orders.keys()):
102
+ so = self.stop_orders[soid]
103
+ _emulate_price_exec = self._fill_stops_at_price or so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
104
+
105
+ if so.side == "BUY" and quote.ask >= so.price:
106
+ _exec_price = quote.ask if not _emulate_price_exec else so.price
107
+ self.stop_orders.pop(soid)
108
+ rep.append(self._execute_order(timestamp, _exec_price, so, True))
109
+ elif so.side == "SELL" and quote.bid <= so.price:
110
+ _exec_price = quote.bid if not _emulate_price_exec else so.price
111
+ self.stop_orders.pop(soid)
112
+ rep.append(self._execute_order(timestamp, _exec_price, so, True))
113
+
114
+ self.bbo = quote
115
+ return rep
116
+
117
+ def place_order(
118
+ self,
119
+ order_side: OrderSide,
120
+ order_type: OrderType,
121
+ amount: float,
122
+ price: float | None = None,
123
+ client_id: str | None = None,
124
+ time_in_force: str = "gtc",
125
+ **options,
126
+ ) -> OmeReport:
127
+ if self.bbo is None:
128
+ raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
129
+
130
+ # - validate order parameters
131
+ self._validate_order(order_side, order_type, amount, price, time_in_force)
132
+
133
+ timestamp = self.time_service.time()
134
+ order = Order(
135
+ self._generate_order_id(),
136
+ order_type,
137
+ self.instrument,
138
+ timestamp,
139
+ amount,
140
+ price if price is not None else 0,
141
+ order_side,
142
+ "NEW",
143
+ time_in_force,
144
+ client_id,
145
+ options=options,
146
+ )
147
+
148
+ return self._process_order(timestamp, order)
149
+
150
+ def _dbg(self, message, **kwargs) -> None:
151
+ logger.debug(f" [<y>OME</y>(<g>{self.instrument}</g>)] :: {message}", **kwargs)
152
+
153
+ def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
154
+ if order.status in ["CLOSED", "CANCELED"]:
155
+ raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
156
+
157
+ buy_side = order.side == "BUY"
158
+ c_ask = self.bbo.ask
159
+ c_bid = self.bbo.bid
160
+
161
+ # - check if order can be "executed" immediately
162
+ exec_price = None
163
+ _need_update_book = False
164
+
165
+ if order.type == "MARKET":
166
+ exec_price = c_ask if buy_side else c_bid
167
+
168
+ elif order.type == "LIMIT":
169
+ _need_update_book = True
170
+ if (buy_side and order.price >= c_ask) or (not buy_side and order.price <= c_bid):
171
+ exec_price = c_ask if buy_side else c_bid
172
+
173
+ elif order.type == "STOP_MARKET":
174
+ # - it processes stop orders separately without adding to orderbook (as on real exchanges)
175
+ order.status = "OPEN"
176
+ self.stop_orders[order.id] = order
177
+
178
+ elif order.type == "STOP_LIMIT":
179
+ # TODO: (OME) check trigger conditions in options etc
180
+ raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
181
+
182
+ # - if order must be "executed" immediately
183
+ if exec_price is not None:
184
+ return self._execute_order(timestamp, exec_price, order, True)
185
+
186
+ # - processing limit orders
187
+ if _need_update_book:
188
+ if buy_side:
189
+ self.bids.setdefault(order.price, list()).append(order.id)
190
+ else:
191
+ self.asks.setdefault(order.price, list()).append(order.id)
192
+
193
+ order.status = "OPEN"
194
+ self.active_orders[order.id] = order
195
+
196
+ self._dbg(f"registered {order.id} {order.type} {order.side} {order.quantity} {order.price}")
197
+ return OmeReport(timestamp, order, None)
198
+
199
+ def _execute_order(self, timestamp: dt_64, exec_price: float, order: Order, taker: bool) -> OmeReport:
200
+ order.status = "CLOSED"
201
+ self._dbg(f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price}")
202
+ return OmeReport(
203
+ timestamp,
204
+ order,
205
+ Deal(
206
+ id=self._generate_trade_id(),
207
+ order_id=order.id,
208
+ time=timestamp,
209
+ amount=order.quantity if order.side == "BUY" else -order.quantity,
210
+ price=exec_price,
211
+ aggressive=taker,
212
+ fee_amount=self.tcc.get_execution_fees(
213
+ instrument=self.instrument, exec_price=exec_price, amount=order.quantity, crossed_market=taker
214
+ ),
215
+ fee_currency=self.instrument.quote,
216
+ ),
217
+ )
218
+
219
+ def _validate_order(
220
+ self, order_side: str, order_type: str, amount: float, price: float | None, time_in_force: str
221
+ ) -> None:
222
+ if order_side.upper() not in ["BUY", "SELL"]:
223
+ raise InvalidOrder("Invalid order side. Only BUY or SELL is allowed.")
224
+
225
+ _ot = order_type.upper()
226
+ if _ot not in ["LIMIT", "MARKET", "STOP_MARKET", "STOP_LIMIT"]:
227
+ raise InvalidOrder("Invalid order type. Only LIMIT, MARKET, STOP_MARKET, STOP_LIMIT are supported.")
228
+
229
+ if amount <= 0:
230
+ raise InvalidOrder("Invalid order amount. Amount must be positive.")
231
+
232
+ if (_ot == "LIMIT" or _ot.startswith("STOP")) and (price is None or price <= 0):
233
+ raise InvalidOrder("Invalid order price. Price must be positively defined for LIMIT or STOP orders.")
234
+
235
+ if time_in_force.upper() not in ["GTC", "IOC"]:
236
+ raise InvalidOrder("Invalid time in force. Only GTC or IOC is supported for now.")
237
+
238
+ if _ot.startswith("STOP"):
239
+ assert price is not None
240
+ c_ask, c_bid = self.bbo.ask, self.bbo.bid
241
+ if (order_side == "BUY" and c_ask >= price) or (order_side == "SELL" and c_bid <= price):
242
+ raise ExchangeError(
243
+ f"Stop price would trigger immediately: STOP_MARKET {order_side} {amount} of {self.instrument.symbol} at {price} | market: {c_ask} / {c_bid}"
244
+ )
245
+
246
+ def cancel_order(self, order_id: str) -> OmeReport:
247
+ # - check limit orders
248
+ if order_id in self.active_orders:
249
+ order = self.active_orders.pop(order_id)
250
+ if order.side == "BUY":
251
+ oids = self.bids[order.price]
252
+ oids.remove(order_id)
253
+ if not oids:
254
+ self.bids.pop(order.price)
255
+ else:
256
+ oids = self.asks[order.price]
257
+ oids.remove(order_id)
258
+ if not oids:
259
+ self.asks.pop(order.price)
260
+ # - check stop orders
261
+ elif order_id in self.stop_orders:
262
+ order = self.stop_orders.pop(order_id)
263
+ # - wrong order_id
264
+ else:
265
+ raise InvalidOrder(f"Order {order_id} not found for {self.instrument.symbol}")
266
+
267
+ order.status = "CANCELED"
268
+ self._dbg(f"{order.id} {order.type} {order.side} {order.quantity} canceled")
269
+ return OmeReport(self.time_service.time(), order, None)
270
+
271
+ def __str__(self) -> str:
272
+ _a, _b = True, True
273
+
274
+ timestamp = self.time_service.time()
275
+ _s = f"= = ({np.datetime64(timestamp, 'ns')}) = =\n"
276
+ for k, v in reversed(self.asks.items()):
277
+ _sizes = ",".join([f"{self.active_orders[o].quantity}" for o in v])
278
+ _s += f" {k} : [{_sizes}]\n"
279
+ if k == self.bbo.ask:
280
+ _a = False
281
+
282
+ if _a:
283
+ _s += f" {self.bbo.ask} : \n"
284
+ _s += "- - - - - - - - - - - - - - - - - - - -\n"
285
+
286
+ _s1 = ""
287
+ for k, v in self.bids.items():
288
+ _sizes = ",".join([f"{self.active_orders[o].quantity}" for o in v])
289
+ _s1 += f" {k} : [{_sizes}]\n"
290
+ if k == self.bbo.bid:
291
+ _b = False
292
+ _s1 += "= = = = = = = = = = = = = = = = = = = =\n"
293
+
294
+ _s1 = f" {self.bbo.bid} : \n" + _s1 if _b else _s1
295
+
296
+ return _s + _s1
@@ -0,0 +1,201 @@
1
+ import re
2
+ from itertools import product
3
+ from types import FunctionType
4
+ from typing import Any, Callable, Type
5
+
6
+ import numpy as np
7
+
8
+ from qubx.utils.misc import generate_name
9
+
10
+
11
+ def _wrap_single_list(param_grid: list | dict) -> dict[str, Any] | list:
12
+ """
13
+ Wraps all non list values as single
14
+ :param param_grid:
15
+ :return:
16
+ """
17
+ as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x] # noqa: E731
18
+ if isinstance(param_grid, list):
19
+ return [_wrap_single_list(ps) for ps in param_grid]
20
+ return {k: as_list(v) for k, v in param_grid.items()}
21
+
22
+
23
+ def permutate_params(
24
+ parameters: dict[str, list | tuple | Any],
25
+ conditions: FunctionType | list | tuple | None = None,
26
+ wrap_as_list=False,
27
+ ) -> list[dict]:
28
+ """
29
+ Generate list of all permutations for given parameters and theirs possible values
30
+
31
+ Example:
32
+
33
+ >>> def foo(par1, par2):
34
+ >>> print(par1)
35
+ >>> print(par2)
36
+ >>>
37
+ >>> # permutate all values and call function for every permutation
38
+ >>> [foo(**z) for z in permutate_params({
39
+ >>> 'par1' : [1,2,3],
40
+ >>> 'par2' : [True, False]
41
+ >>> }, conditions=lambda par1, par2: par1<=2 and par2==True)]
42
+
43
+ 1
44
+ True
45
+ 2
46
+ True
47
+
48
+ :param conditions: list of filtering functions
49
+ :param parameters: dictionary
50
+ :param wrap_as_list: if True (default) it wraps all non list values as single lists (required for sklearn)
51
+ :return: list of permutations
52
+ """
53
+ if conditions is None:
54
+ conditions = []
55
+ elif isinstance(conditions, FunctionType):
56
+ conditions = [conditions]
57
+ elif isinstance(conditions, (tuple, list)):
58
+ if not all([isinstance(e, FunctionType) for e in conditions]):
59
+ raise ValueError("every condition must be a function")
60
+ else:
61
+ raise ValueError("conditions must be of type of function, list or tuple")
62
+
63
+ args = []
64
+ vals = []
65
+ for k, v in parameters.items():
66
+ args.append(k)
67
+ # vals.append([v] if not isinstance(v, (list, tuple)) else list(v) if isinstance(v, range) else v)
68
+ match v:
69
+ case list() | tuple():
70
+ vals.append(v)
71
+ case range():
72
+ vals.append(list(v))
73
+ case str():
74
+ vals.append([v])
75
+ case _:
76
+ vals.append([v])
77
+ # vals.append(v if isinstance(v, (List, Tuple)) else list(v) if isinstance(v, range) else [v])
78
+ d = [dict(zip(args, p)) for p in product(*vals)]
79
+ result = []
80
+ for params_set in d:
81
+ conditions_met = True
82
+ for cond_func in conditions:
83
+ func_param_args = cond_func.__code__.co_varnames
84
+ func_param_values = [params_set[arg] for arg in func_param_args]
85
+ if not cond_func(*func_param_values):
86
+ conditions_met = False
87
+ break
88
+ if conditions_met:
89
+ result.append(params_set)
90
+
91
+ # if we need to follow sklearn rules we should wrap every noniterable as list
92
+ return _wrap_single_list(result) if wrap_as_list else result
93
+
94
+
95
+ def dicts_product(d1: dict, d2: dict) -> dict:
96
+ """
97
+ Product of two dictionaries.
98
+
99
+ Example:
100
+ -------
101
+
102
+ dicts_product({
103
+ 'A': 1,
104
+ 'B': 2,
105
+ }, {
106
+ 'C': 3,
107
+ 'D': 4,
108
+ })
109
+
110
+ Output:
111
+ ------
112
+ {
113
+ 'A + C': [1, 3],
114
+ 'A + D': [1, 4],
115
+ 'B + C': [2, 3],
116
+ 'B + D': [2, 4]
117
+ }
118
+
119
+ """
120
+
121
+ def flatten(lst):
122
+ return [item for sublist in lst for item in (sublist if isinstance(sublist, list) else [sublist])]
123
+
124
+ return {(a + " + " + b): flatten([d1[a], d2[b]]) for a, b in product(d1.keys(), d2.keys())}
125
+
126
+
127
+ class _dict(dict):
128
+ def __add__(self, other: dict) -> dict:
129
+ return _dict(dicts_product(self, other))
130
+
131
+
132
+ def variate(clz: Type[Any] | list[Type[Any]], *args, conditions=None, **kwargs) -> _dict:
133
+ """
134
+ Make variations of parameters for simulations (micro optimizer)
135
+
136
+ Example:
137
+
138
+ >>> class MomentumStrategy_Ex1_test:
139
+ >>> def __init__(self, p1, lookback_period=10, filter_type='sma', skip_entries_flag=False):
140
+ >>> self.p1, self.lookback_period, self.filter_type, self.skip_entries_flag = p1, lookback_period, filter_type, skip_entries_flag
141
+ >>>
142
+ >>> def __repr__(self):
143
+ >>> return self.__class__.__name__ + f"({self.p1},{self.lookback_period},{self.filter_type},{self.skip_entries_flag})"
144
+ >>>
145
+ >>> variate(MomentumStrategy_Ex1_test, 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False])
146
+
147
+ Output:
148
+ >>> {
149
+ >>> 'MSE1t_(lp=1,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,1,ema,True),
150
+ >>> 'MSE1t_(lp=1,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,1,ema,False),
151
+ >>> 'MSE1t_(lp=1,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,1,sma,True),
152
+ >>> 'MSE1t_(lp=1,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,1,sma,False),
153
+ >>> 'MSE1t_(lp=2,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,2,ema,True),
154
+ >>> 'MSE1t_(lp=2,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,2,ema,False),
155
+ >>> 'MSE1t_(lp=2,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,2,sma,True),
156
+ >>> 'MSE1t_(lp=2,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,2,sma,False),
157
+ >>> 'MSE1t_(lp=3,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,3,ema,True),
158
+ >>> 'MSE1t_(lp=3,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,3,ema,False),
159
+ >>> 'MSE1t_(lp=3,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,3,sma,True),
160
+ >>> 'MSE1t_(lp=3,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,3,sma,False)
161
+ >>> }
162
+
163
+ and using in simuation:
164
+
165
+ >>> r = simulate(
166
+ >>> variate(MomentumStrategy_Ex1_test, 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False]),
167
+ >>> data, capital, ["BINANCE.UM:BTCUSDT"], dict(type="ohlc", timeframe="5Min", nback=0), "5Min -1Sec", "vip0_usdt", "2024-01-01", "2024-01-02"
168
+ >>> )
169
+
170
+ Also it's possible to pass a class with tracker:
171
+ >>> variate([MomentumStrategy_Ex1_test, AtrTracker(2, 1)], 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False])
172
+ """
173
+
174
+ def _cmprss(xs: str):
175
+ return "".join([x[0] for x in re.split(r"((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
176
+
177
+ if isinstance(clz, (type, Callable)):
178
+ sfx = _cmprss(clz.__name__)
179
+ _mk = lambda k, *args, **kwargs: k(*args, **kwargs) # noqa: E731
180
+ elif isinstance(clz, (list, tuple)) and clz and isinstance(clz[0], type):
181
+ sfx = _cmprss(clz[0].__name__)
182
+ _mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]] # noqa: E731
183
+ else:
184
+ raise ValueError(
185
+ "Can't recognize data for variating: must be either a class type or a list where first element is class type"
186
+ )
187
+
188
+ def _v_to_str(x: Any) -> str:
189
+ if isinstance(x, (list, tuple, dict, set, np.ndarray)) and len(xs := str(x)) > 15:
190
+ return "[" + generate_name(xs, 8).lower() + "]"
191
+ return str(x)
192
+
193
+ to_excl = [s for s, v in kwargs.items() if not isinstance(v, (list, set, tuple, range))]
194
+ dic2str = lambda ds: [_cmprss(k) + "=" + _v_to_str(v) for k, v in ds.items() if k not in to_excl] # noqa: E731
195
+
196
+ return _dict(
197
+ {
198
+ f"{sfx}_({','.join(dic2str(z))})": _mk(clz, *args, **z)
199
+ for z in permutate_params(kwargs, conditions=conditions)
200
+ }
201
+ )