Qubx 0.1.89__tar.gz → 0.2.2__tar.gz
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-0.1.89 → qubx-0.2.2}/PKG-INFO +2 -1
- {qubx-0.1.89 → qubx-0.2.2}/pyproject.toml +3 -2
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/__init__.py +6 -12
- qubx-0.2.2/src/qubx/backtester/__init__.py +2 -0
- qubx-0.2.2/src/qubx/backtester/ome.py +237 -0
- qubx-0.2.2/src/qubx/backtester/optimization.py +141 -0
- qubx-0.2.2/src/qubx/backtester/queue.py +243 -0
- qubx-0.2.2/src/qubx/backtester/simulator.py +896 -0
- qubx-0.2.2/src/qubx/core/account.py +229 -0
- qubx-0.2.2/src/qubx/core/basics.py +547 -0
- qubx-0.2.2/src/qubx/core/context.py +760 -0
- qubx-0.2.2/src/qubx/core/exceptions.py +22 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/helpers.py +110 -78
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/loggers.py +146 -75
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/lookups.py +110 -82
- qubx-0.2.2/src/qubx/core/metrics.py +901 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/series.pxd +5 -0
- qubx-0.2.2/src/qubx/core/series.pyi +29 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/series.pyx +67 -13
- qubx-0.2.2/src/qubx/core/strategy.py +307 -0
- qubx-0.2.2/src/qubx/core/utils.pyi +4 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/data/readers.py +177 -110
- qubx-0.2.2/src/qubx/gathering/simplest.py +41 -0
- qubx-0.2.2/src/qubx/impl/ccxt_connector.py +311 -0
- qubx-0.2.2/src/qubx/impl/ccxt_customizations.py +150 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/impl/ccxt_trading.py +107 -72
- qubx-0.2.2/src/qubx/impl/ccxt_utils.py +114 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/pandaz/utils.py +145 -111
- qubx-0.2.2/src/qubx/ta/indicators.pyi +16 -0
- qubx-0.2.2/src/qubx/trackers/__init__.py +3 -0
- qubx-0.2.2/src/qubx/trackers/rebalancers.py +141 -0
- qubx-0.2.2/src/qubx/trackers/riskctrl.py +152 -0
- qubx-0.2.2/src/qubx/trackers/sizers.py +104 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/__init__.py +2 -1
- qubx-0.2.2/src/qubx/utils/charting/lookinglass.py +1088 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/misc.py +28 -9
- qubx-0.2.2/src/qubx/utils/ntp.py +58 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/runner.py +79 -61
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/time.py +49 -31
- qubx-0.1.89/src/qubx/core/account.py +0 -166
- qubx-0.1.89/src/qubx/core/basics.py +0 -355
- qubx-0.1.89/src/qubx/core/strategy.py +0 -707
- qubx-0.1.89/src/qubx/impl/ccxt_connector.py +0 -215
- qubx-0.1.89/src/qubx/impl/ccxt_customizations.py +0 -76
- qubx-0.1.89/src/qubx/impl/ccxt_utils.py +0 -108
- qubx-0.1.89/src/qubx/trackers/__init__.py +0 -1
- qubx-0.1.89/src/qubx/trackers/rebalancers.py +0 -116
- {qubx-0.1.89 → qubx-0.2.2}/README.md +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/build.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/__init__.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/math/__init__.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/math/stats.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.1.89 → qubx-0.2.2}/src/qubx/utils/marketdata/binance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Qubx - quantitative trading framework
|
|
5
5
|
Home-page: https://github.com/dmarienko/Qubx
|
|
6
6
|
Author: Dmitry Marienko
|
|
@@ -32,6 +32,7 @@ Requires-Dist: python-binance (>=1.0.19,<2.0.0)
|
|
|
32
32
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
33
33
|
Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
|
|
34
34
|
Requires-Dist: scipy (>=1.12.0,<2.0.0)
|
|
35
|
+
Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
35
36
|
Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
|
|
36
37
|
Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
37
38
|
Requires-Dist: tqdm
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Qubx"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Qubx - quantitative trading framework"
|
|
5
|
-
authors = ["Dmitry Marienko <dmitry@gmail.com>"]
|
|
5
|
+
authors = ["Dmitry Marienko <dmitry@gmail.com>", "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
packages = [
|
|
8
8
|
{ include = "qubx", from = "src" },
|
|
@@ -41,6 +41,7 @@ scikit-learn = "^1.4.2"
|
|
|
41
41
|
plotly = "^5.22.0"
|
|
42
42
|
psycopg-binary = "^3.1.19"
|
|
43
43
|
psycopg-pool = "^3.2.2"
|
|
44
|
+
sortedcontainers = "^2.4.0"
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Callable
|
|
1
2
|
from qubx.utils import set_mpl_theme, runtime_env
|
|
2
3
|
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
3
4
|
|
|
@@ -10,20 +11,13 @@ def formatter(record):
|
|
|
10
11
|
end = record["extra"].get("end", "\n")
|
|
11
12
|
fmt = "<lvl>{message}</lvl>%s" % end
|
|
12
13
|
if record["level"].name in {"WARNING", "SNAKY"}:
|
|
13
|
-
fmt =
|
|
14
|
-
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
|
|
15
|
-
)
|
|
14
|
+
fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
|
|
16
15
|
|
|
17
|
-
prefix =
|
|
18
|
-
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] "
|
|
19
|
-
% record["level"].icon
|
|
20
|
-
)
|
|
16
|
+
prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
|
|
21
17
|
|
|
22
18
|
if record["exception"] is not None:
|
|
23
19
|
# stackprinter.set_excepthook(style='darkbg2')
|
|
24
|
-
record["extra"]["stack"] = stackprinter.format(
|
|
25
|
-
record["exception"], style="darkbg"
|
|
26
|
-
)
|
|
20
|
+
record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg3")
|
|
27
21
|
fmt += "\n{extra[stack]}\n"
|
|
28
22
|
|
|
29
23
|
if record["level"].name in {"TEXT"}:
|
|
@@ -44,7 +38,7 @@ class QubxLogConfig:
|
|
|
44
38
|
QubxLogConfig.setup_logger(level)
|
|
45
39
|
|
|
46
40
|
@staticmethod
|
|
47
|
-
def setup_logger(level: str | None = None):
|
|
41
|
+
def setup_logger(level: str | None = None, custom_formatter: Callable | None = None):
|
|
48
42
|
global logger
|
|
49
43
|
config = {
|
|
50
44
|
"handlers": [
|
|
@@ -55,7 +49,7 @@ class QubxLogConfig:
|
|
|
55
49
|
logger.configure(**config)
|
|
56
50
|
logger.remove(None)
|
|
57
51
|
level = level or QubxLogConfig.get_log_level()
|
|
58
|
-
logger.add(sys.stdout, format=formatter, colorize=True, level=level)
|
|
52
|
+
logger.add(sys.stdout, format=custom_formatter or formatter, colorize=True, level=level, enqueue=True)
|
|
59
53
|
logger = logger.opt(colors=True)
|
|
60
54
|
|
|
61
55
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from typing import List, Dict
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from operator import neg
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from sortedcontainers import SortedDict
|
|
7
|
+
|
|
8
|
+
from qubx import logger
|
|
9
|
+
from qubx.core.basics import Deal, Instrument, Order, Position, Signal, TransactionCostsCalculator, dt_64, ITimeProvider
|
|
10
|
+
from qubx.core.series import Quote, Trade
|
|
11
|
+
from qubx.core.exceptions import (
|
|
12
|
+
ExchangeError,
|
|
13
|
+
InvalidOrder,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class OmeReport:
|
|
19
|
+
timestamp: dt_64
|
|
20
|
+
order: Order
|
|
21
|
+
exec: Deal | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OrdersManagementEngine:
|
|
25
|
+
instrument: Instrument
|
|
26
|
+
time_service: ITimeProvider
|
|
27
|
+
active_orders: Dict[str, Order]
|
|
28
|
+
asks: SortedDict[float, List[str]]
|
|
29
|
+
bids: SortedDict[float, List[str]]
|
|
30
|
+
bbo: Quote | None # current best bid/ask order book (simplest impl)
|
|
31
|
+
__order_id: int
|
|
32
|
+
__trade_id: int
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, instrument: Instrument, time_provider: ITimeProvider, tcc: TransactionCostsCalculator, debug: bool = True
|
|
36
|
+
) -> None:
|
|
37
|
+
self.instrument = instrument
|
|
38
|
+
self.time_service = time_provider
|
|
39
|
+
self.tcc = tcc
|
|
40
|
+
self.asks = SortedDict()
|
|
41
|
+
self.bids = SortedDict(neg)
|
|
42
|
+
self.active_orders = dict()
|
|
43
|
+
self.bbo = None
|
|
44
|
+
self.__order_id = 100000
|
|
45
|
+
self.__trade_id = 100000
|
|
46
|
+
if not debug:
|
|
47
|
+
self._dbg = lambda message, **kwargs: None
|
|
48
|
+
|
|
49
|
+
def _generate_order_id(self) -> str:
|
|
50
|
+
self.__order_id += 1
|
|
51
|
+
return "SIM-ORDER-" + self.instrument.symbol + "-" + str(self.__order_id)
|
|
52
|
+
|
|
53
|
+
def _generate_trade_id(self) -> str:
|
|
54
|
+
self.__trade_id += 1
|
|
55
|
+
return "SIM-EXEC-" + self.instrument.symbol + "-" + str(self.__trade_id)
|
|
56
|
+
|
|
57
|
+
def get_quote(self) -> Quote:
|
|
58
|
+
return self.bbo
|
|
59
|
+
|
|
60
|
+
def get_open_orders(self) -> List[Order]:
|
|
61
|
+
return list(self.active_orders.values())
|
|
62
|
+
|
|
63
|
+
def update_bbo(self, quote: Quote) -> List[OmeReport]:
|
|
64
|
+
timestamp = self.time_service.time()
|
|
65
|
+
rep = []
|
|
66
|
+
|
|
67
|
+
if self.bbo is not None:
|
|
68
|
+
if quote.bid >= self.bbo.ask:
|
|
69
|
+
for level in self.asks.irange(0, quote.bid):
|
|
70
|
+
for order_id in self.asks[level]:
|
|
71
|
+
order = self.active_orders.pop(order_id)
|
|
72
|
+
rep.append(self._execute_order(timestamp, order.price, order, False))
|
|
73
|
+
self.asks.pop(level)
|
|
74
|
+
|
|
75
|
+
if quote.ask <= self.bbo.bid:
|
|
76
|
+
for level in self.bids.irange(np.inf, quote.ask):
|
|
77
|
+
for order_id in self.bids[level]:
|
|
78
|
+
order = self.active_orders.pop(order_id)
|
|
79
|
+
rep.append(self._execute_order(timestamp, order.price, order, False))
|
|
80
|
+
self.bids.pop(level)
|
|
81
|
+
|
|
82
|
+
self.bbo = quote
|
|
83
|
+
return rep
|
|
84
|
+
|
|
85
|
+
def place_order(
|
|
86
|
+
self,
|
|
87
|
+
order_side: str,
|
|
88
|
+
order_type: str,
|
|
89
|
+
amount: float,
|
|
90
|
+
price: float | None = None,
|
|
91
|
+
client_id: str | None = None,
|
|
92
|
+
time_in_force: str = "gtc",
|
|
93
|
+
) -> OmeReport:
|
|
94
|
+
|
|
95
|
+
if self.bbo is None:
|
|
96
|
+
raise ExchangeError(
|
|
97
|
+
f"Simulator is not ready for order management - no any quote for {self.instrument.symbol}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# - validate order parameters
|
|
101
|
+
self._validate_order(order_side, order_type, amount, price, time_in_force)
|
|
102
|
+
|
|
103
|
+
timestamp = self.time_service.time()
|
|
104
|
+
order = Order(
|
|
105
|
+
self._generate_order_id(),
|
|
106
|
+
order_type,
|
|
107
|
+
self.instrument.symbol,
|
|
108
|
+
timestamp,
|
|
109
|
+
amount,
|
|
110
|
+
price if price is not None else 0,
|
|
111
|
+
order_side,
|
|
112
|
+
"NEW",
|
|
113
|
+
time_in_force,
|
|
114
|
+
client_id,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return self._process_order(timestamp, order)
|
|
118
|
+
|
|
119
|
+
def _dbg(self, message, **kwargs) -> None:
|
|
120
|
+
logger.debug(f"[OMS] {self.instrument.symbol} - {message}", **kwargs)
|
|
121
|
+
|
|
122
|
+
def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
|
|
123
|
+
if order.status in ["CLOSED", "CANCELED"]:
|
|
124
|
+
raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
|
|
125
|
+
|
|
126
|
+
buy_side = order.side == "BUY"
|
|
127
|
+
c_ask = self.bbo.ask
|
|
128
|
+
c_bid = self.bbo.bid
|
|
129
|
+
|
|
130
|
+
# - check if order can be "executed" immediately
|
|
131
|
+
exec_price = None
|
|
132
|
+
if order.type == "MARKET":
|
|
133
|
+
exec_price = c_ask if buy_side else c_bid
|
|
134
|
+
|
|
135
|
+
elif order.type == "LIMIT":
|
|
136
|
+
if (buy_side and order.price >= c_ask) or (not buy_side and order.price <= c_bid):
|
|
137
|
+
exec_price = c_ask if buy_side else c_bid
|
|
138
|
+
|
|
139
|
+
# - if order must be "executed" immediately
|
|
140
|
+
if exec_price is not None:
|
|
141
|
+
return self._execute_order(timestamp, exec_price, order, True)
|
|
142
|
+
|
|
143
|
+
# - processing limit orders
|
|
144
|
+
if buy_side:
|
|
145
|
+
self.bids.setdefault(order.price, list()).append(order.id)
|
|
146
|
+
else:
|
|
147
|
+
self.asks.setdefault(order.price, list()).append(order.id)
|
|
148
|
+
order.status = "OPEN"
|
|
149
|
+
self._dbg(f"registered {order.id} {order.type} {order.side} {order.quantity} {order.price}")
|
|
150
|
+
self.active_orders[order.id] = order
|
|
151
|
+
return OmeReport(timestamp, order, None)
|
|
152
|
+
|
|
153
|
+
def _execute_order(self, timestamp: dt_64, exec_price: float, order: Order, taker: bool) -> OmeReport:
|
|
154
|
+
order.status = "CLOSED"
|
|
155
|
+
self._dbg(f"{order.id} {order.type} {order.side} {order.quantity} executed at {exec_price}")
|
|
156
|
+
return OmeReport(
|
|
157
|
+
timestamp,
|
|
158
|
+
order,
|
|
159
|
+
Deal(
|
|
160
|
+
id=self._generate_trade_id(),
|
|
161
|
+
order_id=order.id,
|
|
162
|
+
time=timestamp,
|
|
163
|
+
amount=order.quantity if order.side == "BUY" else -order.quantity,
|
|
164
|
+
price=exec_price,
|
|
165
|
+
aggressive=taker,
|
|
166
|
+
fee_amount=self.tcc.get_execution_fees(
|
|
167
|
+
instrument=self.instrument, exec_price=exec_price, amount=order.quantity, crossed_market=taker
|
|
168
|
+
),
|
|
169
|
+
fee_currency=self.instrument.quote,
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _validate_order(
|
|
174
|
+
self, order_side: str, order_type: str, amount: float, price: float | None, time_in_force: str
|
|
175
|
+
) -> None:
|
|
176
|
+
if order_side.upper() not in ["BUY", "SELL"]:
|
|
177
|
+
raise InvalidOrder("Invalid order side. Only BUY or SELL is allowed.")
|
|
178
|
+
|
|
179
|
+
if order_type.upper() not in ["LIMIT", "MARKET"]:
|
|
180
|
+
raise InvalidOrder("Invalid order type. Only LIMIT or MARKET is supported.")
|
|
181
|
+
|
|
182
|
+
if amount <= 0:
|
|
183
|
+
raise InvalidOrder("Invalid order amount. Amount must be positive.")
|
|
184
|
+
|
|
185
|
+
if order_type.upper() == "LIMIT" and (price is None or price <= 0):
|
|
186
|
+
raise InvalidOrder("Invalid order price. Price must be positively defined for LIMIT orders.")
|
|
187
|
+
|
|
188
|
+
if time_in_force.upper() not in ["GTC", "IOC"]:
|
|
189
|
+
raise InvalidOrder("Invalid time in force. Only GTC or IOC is supported for now.")
|
|
190
|
+
|
|
191
|
+
def cancel_order(self, order_id: str) -> OmeReport:
|
|
192
|
+
if order_id not in self.active_orders:
|
|
193
|
+
raise InvalidOrder(f"Order {order_id} not found for {self.instrument.symbol}")
|
|
194
|
+
|
|
195
|
+
timestamp = self.time_service.time()
|
|
196
|
+
order = self.active_orders.pop(order_id)
|
|
197
|
+
if order.side == "BUY":
|
|
198
|
+
oids = self.bids[order.price]
|
|
199
|
+
oids.remove(order_id)
|
|
200
|
+
if not oids:
|
|
201
|
+
self.bids.pop(order.price)
|
|
202
|
+
else:
|
|
203
|
+
oids = self.asks[order.price]
|
|
204
|
+
oids.remove(order_id)
|
|
205
|
+
if not oids:
|
|
206
|
+
self.asks.pop(order.price)
|
|
207
|
+
|
|
208
|
+
order.status = "CANCELED"
|
|
209
|
+
self._dbg(f"{order.id} {order.type} {order.side} {order.quantity} canceled")
|
|
210
|
+
return OmeReport(timestamp, order, None)
|
|
211
|
+
|
|
212
|
+
def __str__(self) -> str:
|
|
213
|
+
_a, _b = True, True
|
|
214
|
+
|
|
215
|
+
timestamp = self.time_service.time()
|
|
216
|
+
_s = f"= = ({np.datetime64(timestamp, 'ns')}) = =\n"
|
|
217
|
+
for k, v in reversed(self.asks.items()):
|
|
218
|
+
_sizes = ",".join([f"{self.active_orders[o].quantity}" for o in v])
|
|
219
|
+
_s += f" {k} : [{ _sizes }]\n"
|
|
220
|
+
if k == self.bbo.ask:
|
|
221
|
+
_a = False
|
|
222
|
+
|
|
223
|
+
if _a:
|
|
224
|
+
_s += f" {self.bbo.ask} : \n"
|
|
225
|
+
_s += "- - - - - - - - - - - - - - - - - - - -\n"
|
|
226
|
+
|
|
227
|
+
_s1 = ""
|
|
228
|
+
for k, v in self.bids.items():
|
|
229
|
+
_sizes = ",".join([f"{self.active_orders[o].quantity}" for o in v])
|
|
230
|
+
_s1 += f" {k} : [{ _sizes }]\n"
|
|
231
|
+
if k == self.bbo.bid:
|
|
232
|
+
_b = False
|
|
233
|
+
_s1 += "= = = = = = = = = = = = = = = = = = = =\n"
|
|
234
|
+
|
|
235
|
+
_s1 = f" {self.bbo.bid} : \n" + _s1 if _b else _s1
|
|
236
|
+
|
|
237
|
+
return _s + _s1
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Sequence, Tuple
|
|
2
|
+
import numpy as np
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from types import FunctionType
|
|
6
|
+
from itertools import product
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _wrap_single_list(param_grid: List | Dict) -> Dict[str, Any] | List:
|
|
10
|
+
"""
|
|
11
|
+
Wraps all non list values as single
|
|
12
|
+
:param param_grid:
|
|
13
|
+
:return:
|
|
14
|
+
"""
|
|
15
|
+
as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x]
|
|
16
|
+
if isinstance(param_grid, list):
|
|
17
|
+
return [_wrap_single_list(ps) for ps in param_grid]
|
|
18
|
+
return {k: as_list(v) for k, v in param_grid.items()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def permutate_params(
|
|
22
|
+
parameters: Dict[str, List | Tuple | Any],
|
|
23
|
+
conditions: FunctionType | List | Tuple | None = None,
|
|
24
|
+
wrap_as_list=False,
|
|
25
|
+
) -> List[Dict]:
|
|
26
|
+
"""
|
|
27
|
+
Generate list of all permutations for given parameters and theirs possible values
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
|
|
31
|
+
>>> def foo(par1, par2):
|
|
32
|
+
>>> print(par1)
|
|
33
|
+
>>> print(par2)
|
|
34
|
+
>>>
|
|
35
|
+
>>> # permutate all values and call function for every permutation
|
|
36
|
+
>>> [foo(**z) for z in permutate_params({
|
|
37
|
+
>>> 'par1' : [1,2,3],
|
|
38
|
+
>>> 'par2' : [True, False]
|
|
39
|
+
>>> }, conditions=lambda par1, par2: par1<=2 and par2==True)]
|
|
40
|
+
|
|
41
|
+
1
|
|
42
|
+
True
|
|
43
|
+
2
|
|
44
|
+
True
|
|
45
|
+
|
|
46
|
+
:param conditions: list of filtering functions
|
|
47
|
+
:param parameters: dictionary
|
|
48
|
+
:param wrap_as_list: if True (default) it wraps all non list values as single lists (required for sklearn)
|
|
49
|
+
:return: list of permutations
|
|
50
|
+
"""
|
|
51
|
+
if conditions is None:
|
|
52
|
+
conditions = []
|
|
53
|
+
elif isinstance(conditions, FunctionType):
|
|
54
|
+
conditions = [conditions]
|
|
55
|
+
elif isinstance(conditions, (tuple, list)):
|
|
56
|
+
if not all([isinstance(e, FunctionType) for e in conditions]):
|
|
57
|
+
raise ValueError("every condition must be a function")
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError("conditions must be of type of function, list or tuple")
|
|
60
|
+
|
|
61
|
+
args = []
|
|
62
|
+
vals = []
|
|
63
|
+
for k, v in parameters.items():
|
|
64
|
+
args.append(k)
|
|
65
|
+
# vals.append([v] if not isinstance(v, (list, tuple)) else list(v) if isinstance(v, range) else v)
|
|
66
|
+
match v:
|
|
67
|
+
case list() | tuple():
|
|
68
|
+
vals.append(v)
|
|
69
|
+
case range():
|
|
70
|
+
vals.append(list(v))
|
|
71
|
+
case str():
|
|
72
|
+
vals.append([v])
|
|
73
|
+
case _:
|
|
74
|
+
vals.append(list(v))
|
|
75
|
+
# vals.append(v if isinstance(v, (List, Tuple)) else list(v) if isinstance(v, range) else [v])
|
|
76
|
+
d = [dict(zip(args, p)) for p in product(*vals)]
|
|
77
|
+
result = []
|
|
78
|
+
for params_set in d:
|
|
79
|
+
conditions_met = True
|
|
80
|
+
for cond_func in conditions:
|
|
81
|
+
func_param_args = cond_func.__code__.co_varnames
|
|
82
|
+
func_param_values = [params_set[arg] for arg in func_param_args]
|
|
83
|
+
if not cond_func(*func_param_values):
|
|
84
|
+
conditions_met = False
|
|
85
|
+
break
|
|
86
|
+
if conditions_met:
|
|
87
|
+
result.append(params_set)
|
|
88
|
+
|
|
89
|
+
# if we need to follow sklearn rules we should wrap every noniterable as list
|
|
90
|
+
return _wrap_single_list(result) if wrap_as_list else result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def variate(clz, *args, conditions=None, **kwargs) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Make variations of parameters for simulations (micro optimizer)
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
>>> class MomentumStrategy_Ex1_test:
|
|
100
|
+
>>> def __init__(self, p1, lookback_period=10, filter_type='sma', skip_entries_flag=False):
|
|
101
|
+
>>> self.p1, self.lookback_period, self.filter_type, self.skip_entries_flag = p1, lookback_period, filter_type, skip_entries_flag
|
|
102
|
+
>>>
|
|
103
|
+
>>> def __repr__(self):
|
|
104
|
+
>>> return self.__class__.__name__ + f"({self.p1},{self.lookback_period},{self.filter_type},{self.skip_entries_flag})"
|
|
105
|
+
>>>
|
|
106
|
+
>>> variate(MomentumStrategy_Ex1_test, 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False])
|
|
107
|
+
|
|
108
|
+
Output:
|
|
109
|
+
>>> {
|
|
110
|
+
>>> 'MSE1t_(lp=1,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,1,ema,True),
|
|
111
|
+
>>> 'MSE1t_(lp=1,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,1,ema,False),
|
|
112
|
+
>>> 'MSE1t_(lp=1,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,1,sma,True),
|
|
113
|
+
>>> 'MSE1t_(lp=1,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,1,sma,False),
|
|
114
|
+
>>> 'MSE1t_(lp=2,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,2,ema,True),
|
|
115
|
+
>>> 'MSE1t_(lp=2,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,2,ema,False),
|
|
116
|
+
>>> 'MSE1t_(lp=2,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,2,sma,True),
|
|
117
|
+
>>> 'MSE1t_(lp=2,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,2,sma,False),
|
|
118
|
+
>>> 'MSE1t_(lp=3,ft=ema,sef=True)': MomentumStrategy_Ex1_test(10,3,ema,True),
|
|
119
|
+
>>> 'MSE1t_(lp=3,ft=ema,sef=False)': MomentumStrategy_Ex1_test(10,3,ema,False),
|
|
120
|
+
>>> 'MSE1t_(lp=3,ft=sma,sef=True)': MomentumStrategy_Ex1_test(10,3,sma,True),
|
|
121
|
+
>>> 'MSE1t_(lp=3,ft=sma,sef=False)': MomentumStrategy_Ex1_test(10,3,sma,False)
|
|
122
|
+
>>> }
|
|
123
|
+
|
|
124
|
+
and using in simuation:
|
|
125
|
+
|
|
126
|
+
>>> r = simulate(
|
|
127
|
+
>>> variate(MomentumStrategy_Ex1_test, 10, lookback_period=[1,2,3], filter_type=['ema', 'sma'], skip_entries_flag=[True, False]),
|
|
128
|
+
>>> data, capital, ["BINANCE.UM:BTCUSDT"], dict(type="ohlc", timeframe="5Min", nback=0), "5Min -1Sec", "vip0_usdt", "2024-01-01", "2024-01-02"
|
|
129
|
+
>>> )
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def _cmprss(xs: str):
|
|
133
|
+
return "".join([x[0] for x in re.split("((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
|
|
134
|
+
|
|
135
|
+
sfx = _cmprss(clz.__name__)
|
|
136
|
+
to_excl = [s for s, v in kwargs.items() if not isinstance(v, (list, set, tuple, range))]
|
|
137
|
+
dic2str = lambda ds: [_cmprss(k) + "=" + str(v) for k, v in ds.items() if k not in to_excl]
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
f"{sfx}_({ ','.join(dic2str(z)) })": clz(*args, **z) for z in permutate_params(kwargs, conditions=conditions)
|
|
141
|
+
}
|