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/core/lookups.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import dataclasses
|
|
3
|
+
import glob
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import stackprinter
|
|
11
|
+
|
|
12
|
+
from qubx import logger
|
|
13
|
+
from qubx.core.basics import ZERO_COSTS, AssetType, Instrument, MarketType, TransactionCostsCalculator
|
|
14
|
+
from qubx.utils.marketdata.dukas import SAMPLE_INSTRUMENTS
|
|
15
|
+
from qubx.utils.misc import get_local_qubx_folder, makedirs
|
|
16
|
+
|
|
17
|
+
_DEF_INSTRUMENTS_FOLDER = "instruments"
|
|
18
|
+
_DEF_FEES_FOLDER = "fees"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _InstrumentEncoder(json.JSONEncoder):
|
|
22
|
+
def default(self, obj):
|
|
23
|
+
if dataclasses.is_dataclass(obj):
|
|
24
|
+
return {k: v for k, v in dataclasses.asdict(obj).items() if not k.startswith("_")}
|
|
25
|
+
if isinstance(obj, (datetime)):
|
|
26
|
+
return obj.isoformat()
|
|
27
|
+
return super().default(obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _InstrumentDecoder(json.JSONDecoder):
|
|
31
|
+
def decode(self, json_string):
|
|
32
|
+
obj = super(_InstrumentDecoder, self).decode(json_string)
|
|
33
|
+
if isinstance(obj, dict):
|
|
34
|
+
# Convert delivery_date and onboard_date strings to datetime
|
|
35
|
+
delivery_date = obj.get("delivery_date")
|
|
36
|
+
onboard_date = obj.get("onboard_date")
|
|
37
|
+
if delivery_date:
|
|
38
|
+
obj["delivery_date"] = datetime.strptime(delivery_date.split(".")[0], "%Y-%m-%dT%H:%M:%S")
|
|
39
|
+
if onboard_date:
|
|
40
|
+
obj["onboard_date"] = datetime.strptime(onboard_date.split(".")[0], "%Y-%m-%dT%H:%M:%S")
|
|
41
|
+
return Instrument(
|
|
42
|
+
symbol=obj["symbol"],
|
|
43
|
+
asset_type=AssetType[obj["asset_type"]],
|
|
44
|
+
market_type=MarketType[obj["market_type"]],
|
|
45
|
+
exchange=obj["exchange"],
|
|
46
|
+
base=obj["base"],
|
|
47
|
+
quote=obj["quote"],
|
|
48
|
+
settle=obj["settle"],
|
|
49
|
+
exchange_symbol=obj.get("exchange_symbol", obj["symbol"]),
|
|
50
|
+
tick_size=float(obj["tick_size"]),
|
|
51
|
+
lot_size=float(obj["lot_size"]),
|
|
52
|
+
min_size=float(obj["min_size"]),
|
|
53
|
+
min_notional=float(obj.get("min_notional", 0.0)),
|
|
54
|
+
initial_margin=float(obj.get("initial_margin", 0.0)),
|
|
55
|
+
maint_margin=float(obj.get("maint_margin", 0.0)),
|
|
56
|
+
liquidation_fee=float(obj.get("liquidation_fee", 0.0)),
|
|
57
|
+
contract_size=float(obj.get("contract_size", 1.0)),
|
|
58
|
+
onboard_date=obj.get("onboard_date"),
|
|
59
|
+
delivery_date=obj.get("delivery_date"),
|
|
60
|
+
)
|
|
61
|
+
elif isinstance(obj, list):
|
|
62
|
+
return [self.decode(json.dumps(item)) for item in obj]
|
|
63
|
+
return obj
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InstrumentsLookup:
|
|
67
|
+
_lookup: dict[str, Instrument]
|
|
68
|
+
_path: str
|
|
69
|
+
|
|
70
|
+
def __init__(self, path: str = makedirs(get_local_qubx_folder(), _DEF_INSTRUMENTS_FOLDER)) -> None:
|
|
71
|
+
self._path = path
|
|
72
|
+
if not self.load():
|
|
73
|
+
self.refresh()
|
|
74
|
+
self.load()
|
|
75
|
+
|
|
76
|
+
def load(self) -> bool:
|
|
77
|
+
self._lookup = {}
|
|
78
|
+
data_exists = False
|
|
79
|
+
for fs in glob.glob(self._path + "/*.json"):
|
|
80
|
+
try:
|
|
81
|
+
with open(fs, "r") as f:
|
|
82
|
+
instrs: list[Instrument] = json.load(f, cls=_InstrumentDecoder)
|
|
83
|
+
for i in instrs:
|
|
84
|
+
self._lookup[f"{i.exchange}:{i.symbol}"] = i
|
|
85
|
+
data_exists = True
|
|
86
|
+
except Exception as ex:
|
|
87
|
+
stackprinter.show_current_exception()
|
|
88
|
+
logger.warning(ex)
|
|
89
|
+
|
|
90
|
+
return data_exists
|
|
91
|
+
|
|
92
|
+
def find(self, exchange: str, base: str, quote: str, settle: str | None = None) -> Instrument | None:
|
|
93
|
+
for i in self._lookup.values():
|
|
94
|
+
if i.exchange == exchange and (
|
|
95
|
+
(i.base == base and i.quote == quote) or (i.base == quote and i.quote == base)
|
|
96
|
+
):
|
|
97
|
+
if settle is not None and i.settle is not None:
|
|
98
|
+
if i.settle == settle:
|
|
99
|
+
return i
|
|
100
|
+
else:
|
|
101
|
+
return i
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def find_symbol(self, exchange: str, symbol: str) -> Instrument | None:
|
|
105
|
+
for i in self._lookup.values():
|
|
106
|
+
if (i.exchange == exchange) and (i.symbol == symbol):
|
|
107
|
+
return i
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def find_instruments(self, exchange: str, quote: str | None = None) -> list[Instrument]:
|
|
111
|
+
return [i for i in self._lookup.values() if i.exchange == exchange and (quote is None or i.quote == quote)]
|
|
112
|
+
|
|
113
|
+
def _save_to_json(self, path, instruments: list[Instrument]):
|
|
114
|
+
with open(path, "w") as f:
|
|
115
|
+
json.dump(instruments, f, cls=_InstrumentEncoder, indent=4)
|
|
116
|
+
logger.info(f"Saved {len(instruments)} to {path}")
|
|
117
|
+
|
|
118
|
+
def find_aux_instrument_for(self, instrument: Instrument, base_currency: str) -> Instrument | None:
|
|
119
|
+
"""
|
|
120
|
+
Tries to find aux instrument (for conversions to funded currency)
|
|
121
|
+
for example:
|
|
122
|
+
ETHBTC -> BTCUSDT for base_currency USDT
|
|
123
|
+
EURGBP -> GBPUSD for base_currency USD
|
|
124
|
+
...
|
|
125
|
+
"""
|
|
126
|
+
base_currency = base_currency.upper()
|
|
127
|
+
if instrument.quote != base_currency:
|
|
128
|
+
return self.find(instrument.exchange, instrument.quote, base_currency)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def __getitem__(self, spath: str) -> list[Instrument]:
|
|
132
|
+
res = []
|
|
133
|
+
c = re.compile(spath)
|
|
134
|
+
for k, v in self._lookup.items():
|
|
135
|
+
if re.match(c, k):
|
|
136
|
+
res.append(v)
|
|
137
|
+
return res
|
|
138
|
+
|
|
139
|
+
def refresh(self, query_exchanges: bool = False):
|
|
140
|
+
for mn in dir(self):
|
|
141
|
+
if mn.startswith("_update_"):
|
|
142
|
+
getattr(self, mn)(self._path, query_exchanges)
|
|
143
|
+
|
|
144
|
+
def _ccxt_update(
|
|
145
|
+
self,
|
|
146
|
+
path: str,
|
|
147
|
+
file_name: str,
|
|
148
|
+
exchange_to_ccxt_name: dict[str, str],
|
|
149
|
+
keep_types: list[MarketType] | None = None,
|
|
150
|
+
query_exchanges: bool = False,
|
|
151
|
+
):
|
|
152
|
+
import ccxt as cx
|
|
153
|
+
|
|
154
|
+
from qubx.utils.marketdata.ccxt import ccxt_symbol_to_instrument
|
|
155
|
+
|
|
156
|
+
# - first we try to load packed data from QUBX resources
|
|
157
|
+
instruments = {}
|
|
158
|
+
_packed_data = _load_qubx_resources_as_json(f"instruments/symbols-{file_name}")
|
|
159
|
+
if _packed_data:
|
|
160
|
+
for i in _convert_instruments_metadata_to_qubx(_packed_data):
|
|
161
|
+
instruments[i] = i
|
|
162
|
+
|
|
163
|
+
if query_exchanges:
|
|
164
|
+
# - replace defaults with data from CCXT
|
|
165
|
+
for exch, ccxt_name in exchange_to_ccxt_name.items():
|
|
166
|
+
exch = exch.upper()
|
|
167
|
+
ccxt_name = ccxt_name.lower()
|
|
168
|
+
ex: cx.Exchange = getattr(cx, ccxt_name)()
|
|
169
|
+
mkts = ex.load_markets()
|
|
170
|
+
for v in mkts.values():
|
|
171
|
+
if v["index"]:
|
|
172
|
+
continue
|
|
173
|
+
instr = ccxt_symbol_to_instrument(exch, v)
|
|
174
|
+
if not keep_types or instr.market_type in keep_types:
|
|
175
|
+
instruments[instr] = instr
|
|
176
|
+
|
|
177
|
+
# - drop to file
|
|
178
|
+
self._save_to_json(os.path.join(path, f"{file_name}.json"), list(instruments.values()))
|
|
179
|
+
|
|
180
|
+
def _update_kraken(self, path: str, query_exchanges: bool = False):
|
|
181
|
+
self._ccxt_update(path, "kraken.f", {"kraken.f": "krakenfutures"}, query_exchanges=query_exchanges)
|
|
182
|
+
self._ccxt_update(path, "kraken", {"kraken": "kraken"}, query_exchanges=query_exchanges)
|
|
183
|
+
|
|
184
|
+
def _update_hyperliquid(self, path: str, query_exchanges: bool = False):
|
|
185
|
+
self._ccxt_update(
|
|
186
|
+
path,
|
|
187
|
+
"hyperliquid",
|
|
188
|
+
{"hyperliquid": "hyperliquid"},
|
|
189
|
+
keep_types=[MarketType.SPOT],
|
|
190
|
+
query_exchanges=query_exchanges,
|
|
191
|
+
)
|
|
192
|
+
self._ccxt_update(
|
|
193
|
+
path,
|
|
194
|
+
"hyperliquid.f",
|
|
195
|
+
{"hyperliquid.f": "hyperliquid"},
|
|
196
|
+
keep_types=[MarketType.SWAP],
|
|
197
|
+
query_exchanges=query_exchanges,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _update_binance(self, path: str, query_exchanges: bool = False):
|
|
201
|
+
self._ccxt_update(
|
|
202
|
+
path,
|
|
203
|
+
"binance",
|
|
204
|
+
{"binance": "binance"},
|
|
205
|
+
keep_types=[MarketType.SPOT, MarketType.MARGIN],
|
|
206
|
+
query_exchanges=query_exchanges,
|
|
207
|
+
)
|
|
208
|
+
self._ccxt_update(path, "binance.um", {"binance.um": "binanceusdm"}, query_exchanges=query_exchanges)
|
|
209
|
+
self._ccxt_update(path, "binance.cm", {"binance.cm": "binancecoinm"}, query_exchanges=query_exchanges)
|
|
210
|
+
|
|
211
|
+
def _update_bitfinex(self, path: str, query_exchanges: bool = False):
|
|
212
|
+
self._ccxt_update(
|
|
213
|
+
path,
|
|
214
|
+
"bitfinex.f",
|
|
215
|
+
{"bitfinex.f": "bitfinex"},
|
|
216
|
+
keep_types=[MarketType.SWAP],
|
|
217
|
+
query_exchanges=query_exchanges,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _update_dukas(self, path: str, query_exchanges: bool = False):
|
|
221
|
+
self._save_to_json(os.path.join(path, "dukas.json"), SAMPLE_INSTRUMENTS)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# - TODO: need to find better way to extract actual data !!
|
|
225
|
+
_DEFAULT_FEES = """
|
|
226
|
+
[binance]
|
|
227
|
+
# SPOT (maker, taker)
|
|
228
|
+
vip0_usdt = 0.1000,0.1000
|
|
229
|
+
vip1_usdt = 0.0900,0.1000
|
|
230
|
+
vip2_usdt = 0.0800,0.1000
|
|
231
|
+
vip3_usdt = 0.0420,0.0600
|
|
232
|
+
vip4_usdt = 0.0420,0.0540
|
|
233
|
+
vip5_usdt = 0.0360,0.0480
|
|
234
|
+
vip6_usdt = 0.0300,0.0420
|
|
235
|
+
vip7_usdt = 0.0240,0.0360
|
|
236
|
+
vip8_usdt = 0.0180,0.0300
|
|
237
|
+
vip9_usdt = 0.0120,0.0240
|
|
238
|
+
|
|
239
|
+
# SPOT (maker, taker)
|
|
240
|
+
vip0_bnb = 0.0750,0.0750
|
|
241
|
+
vip1_bnb = 0.0675,0.0750
|
|
242
|
+
vip2_bnb = 0.0600,0.0750
|
|
243
|
+
vip3_bnb = 0.0315,0.0450
|
|
244
|
+
vip4_bnb = 0.0315,0.0405
|
|
245
|
+
vip5_bnb = 0.0270,0.0360
|
|
246
|
+
vip6_bnb = 0.0225,0.0315
|
|
247
|
+
vip7_bnb = 0.0180,0.0270
|
|
248
|
+
vip8_bnb = 0.0135,0.0225
|
|
249
|
+
vip9_bnb = 0.0090,0.0180
|
|
250
|
+
|
|
251
|
+
# UM futures (maker, taker)
|
|
252
|
+
[binance.um]
|
|
253
|
+
vip0_usdt = 0.0200,0.0500
|
|
254
|
+
vip1_usdt = 0.0160,0.0400
|
|
255
|
+
vip2_usdt = 0.0140,0.0350
|
|
256
|
+
vip3_usdt = 0.0120,0.0320
|
|
257
|
+
vip4_usdt = 0.0100,0.0300
|
|
258
|
+
vip5_usdt = 0.0080,0.0270
|
|
259
|
+
vip6_usdt = 0.0060,0.0250
|
|
260
|
+
vip7_usdt = 0.0040,0.0220
|
|
261
|
+
vip8_usdt = 0.0020,0.0200
|
|
262
|
+
vip9_usdt = 0.0000,0.0170
|
|
263
|
+
|
|
264
|
+
# CM futures (maker, taker)
|
|
265
|
+
[binance.cm]
|
|
266
|
+
vip0 = 0.0200,0.0500
|
|
267
|
+
vip1 = 0.0160,0.0400
|
|
268
|
+
vip2 = 0.0140,0.0350
|
|
269
|
+
vip3 = 0.0120,0.0320
|
|
270
|
+
vip4 = 0.0100,0.0300
|
|
271
|
+
vip5 = 0.0080,0.0270
|
|
272
|
+
vip6 = 0.0060,0.0250
|
|
273
|
+
vip7 = 0.0040,0.0220
|
|
274
|
+
vip8 = 0.0020,0.0200
|
|
275
|
+
vip9 = 0.0000,0.0170
|
|
276
|
+
|
|
277
|
+
[bitmex]
|
|
278
|
+
tierb_xbt=0.02,0.075
|
|
279
|
+
tierb_usdt=-0.015,0.075
|
|
280
|
+
tieri_xbt=0.01,0.05
|
|
281
|
+
tieri_usdt=-0.015,0.05
|
|
282
|
+
tiert_xbt=0.0,0.04
|
|
283
|
+
tiert_usdt=-0.015,0.04
|
|
284
|
+
tierm_xbt=0.0,0.035
|
|
285
|
+
tierm_usdt=-0.015,0.035
|
|
286
|
+
tiere_xbt=0.0,0.03
|
|
287
|
+
tiere_usdt=-0.015,0.03
|
|
288
|
+
tierx_xbt=0.0,0.025
|
|
289
|
+
tierx_usdt=-0.015,0.025
|
|
290
|
+
tierd_xbt=-0.003,0.024
|
|
291
|
+
tierd_usdt=-0.015,0.024
|
|
292
|
+
tierw_xbt=-0.005,0.023
|
|
293
|
+
tierw_usdt=-0.015,0.023
|
|
294
|
+
tierk_xbt=-0.008,0.022
|
|
295
|
+
tierk_usdt=-0.015,0.022
|
|
296
|
+
tiers_xbt=-0.01,0.0175
|
|
297
|
+
tiers_usdt=-0.015,0.02
|
|
298
|
+
|
|
299
|
+
[dukas]
|
|
300
|
+
regular=0.0035,0.0035
|
|
301
|
+
premium=0.0017,0.0017
|
|
302
|
+
|
|
303
|
+
[kraken]
|
|
304
|
+
K0=0.25,0.40
|
|
305
|
+
K10=0.20,0.35
|
|
306
|
+
K50=0.14,0.24
|
|
307
|
+
K100=0.12,0.22
|
|
308
|
+
K250=0.10,0.20
|
|
309
|
+
K500=0.08,0.18
|
|
310
|
+
M1=0.06,0.16
|
|
311
|
+
M2.5=0.04,0.14
|
|
312
|
+
M5=0.02,0.12
|
|
313
|
+
M10=0.0,0.10
|
|
314
|
+
|
|
315
|
+
[kraken.f]
|
|
316
|
+
K0=0.0200,0.0500
|
|
317
|
+
K100=0.0150,0.0400
|
|
318
|
+
M1=0.0125,0.0300
|
|
319
|
+
M5=0.0100,0.0250
|
|
320
|
+
M10=0.0075,0.0200
|
|
321
|
+
M20=0.0050,0.0150
|
|
322
|
+
M50=0.0025,0.0125
|
|
323
|
+
M100=0.0000,0.0100
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class FeesLookup:
|
|
328
|
+
"""
|
|
329
|
+
Fees lookup
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
_lookup: dict[str, TransactionCostsCalculator]
|
|
333
|
+
_path: str
|
|
334
|
+
|
|
335
|
+
def __init__(self, path: str = makedirs(get_local_qubx_folder(), _DEF_FEES_FOLDER)) -> None:
|
|
336
|
+
self._path = path
|
|
337
|
+
if not self.load():
|
|
338
|
+
self.refresh()
|
|
339
|
+
self.load()
|
|
340
|
+
|
|
341
|
+
def load(self) -> bool:
|
|
342
|
+
self._lookup = {}
|
|
343
|
+
data_exists = False
|
|
344
|
+
parser = configparser.ConfigParser()
|
|
345
|
+
# - load all avaliable configs
|
|
346
|
+
for fs in glob.glob(self._path + "/*.ini"):
|
|
347
|
+
parser.read(fs)
|
|
348
|
+
data_exists = True
|
|
349
|
+
|
|
350
|
+
for exch in parser.sections():
|
|
351
|
+
for spec, info in parser[exch].items():
|
|
352
|
+
try:
|
|
353
|
+
maker, taker = info.split(",")
|
|
354
|
+
self._lookup[f"{exch}_{spec}"] = (float(maker), float(taker))
|
|
355
|
+
except (ValueError, TypeError) as e:
|
|
356
|
+
logger.warning(f'Wrong spec format for {exch}: "{info}". Should be spec=maker,taker. Error: {e}')
|
|
357
|
+
|
|
358
|
+
return data_exists
|
|
359
|
+
|
|
360
|
+
def __getitem__(self, spath: str) -> list[Instrument]:
|
|
361
|
+
res = []
|
|
362
|
+
c = re.compile(spath)
|
|
363
|
+
for k, v in self._lookup.items():
|
|
364
|
+
if re.match(c, k):
|
|
365
|
+
res.append((k, v))
|
|
366
|
+
return res
|
|
367
|
+
|
|
368
|
+
def refresh(self):
|
|
369
|
+
with open(os.path.join(self._path, "default.ini"), "w") as f:
|
|
370
|
+
f.write(_DEFAULT_FEES)
|
|
371
|
+
|
|
372
|
+
def find(self, exchange: str, spec: str | None) -> TransactionCostsCalculator | None:
|
|
373
|
+
if spec is None:
|
|
374
|
+
return ZERO_COSTS
|
|
375
|
+
key = f"{exchange}_{spec}"
|
|
376
|
+
vals = self._lookup.get(key)
|
|
377
|
+
return TransactionCostsCalculator(key, *self._lookup.get(key)) if vals is not None else None
|
|
378
|
+
|
|
379
|
+
def __repr__(self) -> str:
|
|
380
|
+
s = "Name:\t\t\t(maker, taker)\n"
|
|
381
|
+
for k, v in self._lookup.items():
|
|
382
|
+
s += f"{k.ljust(25)}: {v}\n"
|
|
383
|
+
return s
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclasses.dataclass(frozen=True)
|
|
387
|
+
class GlobalLookup:
|
|
388
|
+
instruments: InstrumentsLookup
|
|
389
|
+
fees: FeesLookup
|
|
390
|
+
|
|
391
|
+
def find_fees(self, exchange: str, spec: str | None) -> TransactionCostsCalculator | None:
|
|
392
|
+
return self.fees.find(exchange, spec)
|
|
393
|
+
|
|
394
|
+
def find_aux_instrument_for(self, instrument: Instrument, base_currency: str) -> Instrument | None:
|
|
395
|
+
return self.instruments.find_aux_instrument_for(instrument, base_currency)
|
|
396
|
+
|
|
397
|
+
def find_instrument(self, exchange: str, base: str, quote: str) -> Instrument | None:
|
|
398
|
+
return self.instruments.find(exchange, base, quote)
|
|
399
|
+
|
|
400
|
+
def find_instruments(self, exchange: str, quote: str | None = None) -> list[Instrument]:
|
|
401
|
+
return self.instruments.find_instruments(exchange, quote)
|
|
402
|
+
|
|
403
|
+
def find_symbol(self, exchange: str, symbol: str) -> Instrument | None:
|
|
404
|
+
return self.instruments.find_symbol(exchange, symbol)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _load_qubx_resources_as_json(path: Path | str) -> list[dict]:
|
|
408
|
+
"""
|
|
409
|
+
Reads a JSON file from resource module
|
|
410
|
+
"""
|
|
411
|
+
import importlib.resources
|
|
412
|
+
import json
|
|
413
|
+
|
|
414
|
+
if isinstance(path, str):
|
|
415
|
+
path = Path(path)
|
|
416
|
+
|
|
417
|
+
if path.suffix != ".json":
|
|
418
|
+
path = path.with_suffix(path.suffix + ".json")
|
|
419
|
+
|
|
420
|
+
data = []
|
|
421
|
+
try:
|
|
422
|
+
res_path = importlib.resources.files("qubx.resources") / path
|
|
423
|
+
with res_path.open() as f:
|
|
424
|
+
data = json.load(f)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.warning(f"Can't load resource file from {path} - {str(e)}")
|
|
427
|
+
|
|
428
|
+
return data
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _convert_instruments_metadata_to_qubx(data: list[dict]):
|
|
432
|
+
"""
|
|
433
|
+
Converting tardis symbols meta-data to Qubx instruments
|
|
434
|
+
"""
|
|
435
|
+
_excs = {
|
|
436
|
+
"binance": "BINANCE",
|
|
437
|
+
"binance-delivery": "BINANCE.CM",
|
|
438
|
+
"binance-futures": "BINANCE.UM",
|
|
439
|
+
"kraken": "KRAKEN",
|
|
440
|
+
"cryptofacilities": "KRAKEN.F",
|
|
441
|
+
"bitfinex": "BITFINEX",
|
|
442
|
+
"bitfinex-derivatives": "BITFINEX.F",
|
|
443
|
+
"hyperliquid": "HYPERLIQUID",
|
|
444
|
+
}
|
|
445
|
+
r = []
|
|
446
|
+
for s in data:
|
|
447
|
+
match s["type"]:
|
|
448
|
+
case "perpetual":
|
|
449
|
+
_type = MarketType.SWAP
|
|
450
|
+
case "spot":
|
|
451
|
+
_type = MarketType.SPOT
|
|
452
|
+
case "future":
|
|
453
|
+
_type = MarketType.FUTURE
|
|
454
|
+
case _:
|
|
455
|
+
raise ValueError(f" -> Unknown type {s['type']}")
|
|
456
|
+
r.append(
|
|
457
|
+
Instrument(
|
|
458
|
+
s["baseCurrency"] + s["quoteCurrency"],
|
|
459
|
+
AssetType.CRYPTO,
|
|
460
|
+
_type,
|
|
461
|
+
_excs.get(s["exchange"], s["exchange"].upper()),
|
|
462
|
+
s["baseCurrency"],
|
|
463
|
+
s["quoteCurrency"],
|
|
464
|
+
s["quoteCurrency"],
|
|
465
|
+
s["id"],
|
|
466
|
+
tick_size=s["priceIncrement"],
|
|
467
|
+
lot_size=s["minTradeAmount"],
|
|
468
|
+
min_size=s["amountIncrement"],
|
|
469
|
+
min_notional=0, # we don't have this info from tardis
|
|
470
|
+
contract_size=s.get("contractMultiplier", 1.0),
|
|
471
|
+
onboard_date=s.get("availableSince", None),
|
|
472
|
+
delivery_date=s.get("availableTo", None),
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
return r
|