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
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
|
+
)
|