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/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