Qubx 0.0.1__cp311-cp311-manylinux_2_35_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 ADDED
@@ -0,0 +1,164 @@
1
+ from qubx.utils import set_mpl_theme, runtime_env, install_pyx_recompiler_for_dev
2
+ install_pyx_recompiler_for_dev()
3
+
4
+ from loguru import logger
5
+ import os, sys, stackprinter
6
+ from qubx.core.lookups import InstrumentsLookup
7
+
8
+
9
+ def formatter(record):
10
+ end = record["extra"].get("end", "\n")
11
+ fmt = "<lvl>{message}</lvl>%s" % end
12
+ if record["level"].name in {"WARNING", "SNAKY"}:
13
+ fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
14
+
15
+ prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
16
+
17
+ if record["exception"] is not None:
18
+ # stackprinter.set_excepthook(style='darkbg2')
19
+ record["extra"]["stack"] = stackprinter.format(record["exception"], style="darkbg")
20
+ fmt += "\n{extra[stack]}\n"
21
+
22
+ if record["level"].name in {"TEXT"}:
23
+ prefix = ""
24
+
25
+ return prefix + fmt
26
+
27
+
28
+ config = {
29
+ "handlers": [ {"sink": sys.stdout, "format": "{time} - {message}"}, ],
30
+ "extra": {"user": "someone"},
31
+ }
32
+
33
+
34
+ logger.configure(**config)
35
+ logger.remove(None)
36
+ logger.add(sys.stdout, format=formatter, colorize=True)
37
+ logger = logger.opt(colors=True)
38
+
39
+ lookup = InstrumentsLookup()
40
+
41
+ # registering magic for jupyter notebook
42
+ if runtime_env() in ['notebook', 'shell']:
43
+ from IPython.core.magic import (Magics, magics_class, line_magic, line_cell_magic)
44
+ from IPython import get_ipython
45
+
46
+ @magics_class
47
+ class QubxMagics(Magics):
48
+ # process data manager
49
+ __manager = None
50
+
51
+ @line_magic
52
+ def qubxd(self, line: str):
53
+ self.qubx_setup('dark')
54
+
55
+ @line_magic
56
+ def qubxl(self, line: str):
57
+ self.qubx_setup('light')
58
+
59
+ @line_magic
60
+ def qubx_setup(self, line: str):
61
+ """
62
+ QUBE framework initialization
63
+ """
64
+ import os
65
+
66
+ tpl_path = os.path.join(os.path.dirname(__file__), "_nb_magic.py")
67
+ # print("TPL:", tpl_path)
68
+ with open(tpl_path, 'r', encoding="utf8") as myfile:
69
+ s = myfile.read()
70
+
71
+ exec(s, self.shell.user_ns)
72
+
73
+ # setup more funcy mpl theme instead of ugly default
74
+ if line:
75
+ if 'dark' in line.lower():
76
+ set_mpl_theme('dark')
77
+
78
+ elif 'light' in line.lower():
79
+ set_mpl_theme('light')
80
+
81
+ # install additional plotly helpers
82
+ # from qube.charting.plot_helpers import install_plotly_helpers
83
+ # install_plotly_helpers()
84
+
85
+ def _get_manager(self):
86
+ if self.__manager is None:
87
+ import multiprocessing as m
88
+ self.__manager = m.Manager()
89
+ return self.__manager
90
+
91
+ @line_cell_magic
92
+ def proc(self, line, cell=None):
93
+ """
94
+ Run cell in separate process
95
+
96
+ >>> %%proc x, y as MyProc1
97
+ >>> x.set('Hello')
98
+ >>> y.set([1,2,3,4])
99
+
100
+ """
101
+ import multiprocessing as m
102
+ import time, re
103
+
104
+ # create ext args
105
+ name = None
106
+ if line:
107
+ # check if custom process name was provided
108
+ if ' as ' in line:
109
+ line, name = line.split('as')
110
+ if not name.isspace():
111
+ name = name.strip()
112
+ else:
113
+ print('>>> Process name must be specified afer "as" keyword !')
114
+ return
115
+
116
+ ipy = get_ipython()
117
+ for a in [x for x in re.split('[\ ,;]', line.strip()) if x]:
118
+ ipy.push({a: self._get_manager().Value(None, None)})
119
+
120
+ # code to run
121
+ lines = '\n'.join([' %s' % x for x in cell.split('\n')])
122
+
123
+ def fn():
124
+ result = get_ipython().run_cell(lines)
125
+
126
+ # send errors to parent
127
+ if result.error_before_exec:
128
+ raise result.error_before_exec
129
+
130
+ if result.error_in_exec:
131
+ raise result.error_in_exec
132
+
133
+ t_start = str(time.time()).replace('.', '_')
134
+ f_id = f'proc_{t_start}' if name is None else name
135
+ if self._is_task_name_already_used(f_id):
136
+ f_id = f"{f_id}_{t_start}"
137
+
138
+ task = m.Process(target=fn, name=f_id)
139
+ task.start()
140
+ print(' -> Task %s is started' % f_id)
141
+
142
+ def _is_task_name_already_used(self, name):
143
+ import multiprocessing as m
144
+ for p in m.active_children():
145
+ if p.name == name:
146
+ return True
147
+ return False
148
+
149
+ @line_magic
150
+ def list_proc(self, line):
151
+ import multiprocessing as m
152
+ for p in m.active_children():
153
+ print(p.name)
154
+
155
+ @line_magic
156
+ def kill_proc(self, line):
157
+ import multiprocessing as m
158
+ for p in m.active_children():
159
+ if line and p.name.startswith(line):
160
+ p.terminate()
161
+
162
+
163
+ # - registering magic here
164
+ get_ipython().register_magics(QubxMagics)
qubx/_nb_magic.py ADDED
@@ -0,0 +1,69 @@
1
+ """"
2
+ Here stuff we want to have in every Jupyter notebook after calling %qube magic
3
+ """
4
+ import importlib_metadata
5
+
6
+ import qubx
7
+ from qubx.utils import runtime_env
8
+ from qubx.utils.misc import add_project_to_system_path
9
+
10
+
11
+ def np_fmt_short():
12
+ # default np output is 75 columns so extend it a bit and suppress scientific fmt for small floats
13
+ np.set_printoptions(linewidth=240, suppress=True)
14
+
15
+
16
+ def np_fmt_reset():
17
+ # reset default np printing options
18
+ np.set_printoptions(edgeitems=3, infstr='inf', linewidth=75, nanstr='nan', precision=8,
19
+ suppress=False, threshold=1000, formatter=None)
20
+
21
+
22
+ if runtime_env() in ['notebook', 'shell']:
23
+
24
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
25
+ # -- all imports below will appear in notebook after calling %%alphalab magic ---
26
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
27
+
28
+ # - - - - Common stuff - - - -
29
+ import numpy as np
30
+ import pandas as pd
31
+ from datetime import time, timedelta
32
+ from tqdm.auto import tqdm
33
+
34
+ # - - - - TA stuff and indicators - - - -
35
+ # - - - - Portfolio analysis - - - -
36
+ # - - - - Simulator stuff - - - -
37
+ # - - - - Learn stuff - - - -
38
+ # - - - - Charting stuff - - - -
39
+ from matplotlib import pyplot as plt
40
+ from qubx.utils.charting.mpl_helpers import fig, subplot, sbp
41
+ # - - - - Utils - - - -
42
+
43
+ # - setup short numpy output format
44
+ np_fmt_short()
45
+
46
+ # - add project home to system path
47
+ add_project_to_system_path()
48
+
49
+ # - check current version
50
+ try:
51
+ version = importlib_metadata.version('qube2')
52
+ except:
53
+ version = 'Dev'
54
+
55
+ # some new logo
56
+ if not hasattr(qubx.QubxMagics, '__already_initialized__'):
57
+ from qubx.utils.misc import (green, yellow, cyan, magenta, white, blue, red)
58
+
59
+ print(
60
+ f"""
61
+ {red("╻")}
62
+ {green("┏┓ ╻ ")} {red("┃")} {yellow("┏┓")} {cyan("Quantitative Backtesting Environment")}
63
+ {green("┃┃ ┓┏ ┣┓ ┏┓")} {red("┃")} {yellow("┏┛")}
64
+ {green("┗┻ ┗┻ ┗┛ ┗ ")} {red("┃")} {yellow("┗━")} (c) 2024, ver. {magenta(version.rstrip())}
65
+ {red("╹")}
66
+ """
67
+ )
68
+ qubx.QubxMagics.__already_initialized__ = True
69
+
qubx/core/__init__.py ADDED
File without changes
qubx/core/basics.py ADDED
@@ -0,0 +1,224 @@
1
+ from datetime import datetime
2
+ from typing import Callable, Dict, List, Optional, Union
3
+ import numpy as np
4
+ import math
5
+ from dataclasses import dataclass, field
6
+ from qubx.core.series import Quote, Trade, time_as_nsec
7
+ from qubx.core.utils import time_to_str, time_delta_to_str, recognize_timeframe
8
+
9
+
10
+ dt_64 = np.datetime64
11
+
12
+
13
+ @dataclass
14
+ class FuturesInfo:
15
+ contract_type: Optional[str] = None # contract type
16
+ delivery_date: Optional[datetime] = None # delivery date
17
+ onboard_date: Optional[datetime] = None # futures contract size
18
+ contract_size: float = 1.0 # futures contract size
19
+ maint_margin: float = 0.0 # maintanance margin
20
+ required_margin: float = 0.0 # required margin
21
+ liquidation_fee: float = 0.0 # liquidation cost
22
+
23
+ def __str__(self) -> str:
24
+ return f"{self.contract_type} ({self.contract_size}) {self.onboard_date.isoformat()} -> {self.delivery_date.isoformat()}"
25
+
26
+
27
+ @dataclass
28
+ class Instrument:
29
+ symbol: str # instrument's name
30
+ market_type: str # market type (CRYPTO, STOCK, FX, etc)
31
+ exchange: str # exchange id
32
+ base: str # base symbol
33
+ quote: str # quote symbol
34
+ margin_symbol: str # margin asset
35
+ min_tick: float = 0.0 # tick size - minimal price change
36
+ min_size_step: float = 0.0 # minimal position change step size
37
+ min_size: float = 0.0 # minimal allowed position size
38
+
39
+ # - futures section
40
+ futures_info: Optional[FuturesInfo] = None
41
+
42
+ _aux_instrument: Optional['Instrument'] = None # instrument used for conversion to main asset basis
43
+ # | let's say we trade BTC/ETH with main account in USDT
44
+ # | so we need to use ETH/USDT for convert profits/losses to USDT
45
+ _tick_precision: int = field(repr=False ,default=-1) #type: check
46
+ _size_precision: int = field(repr=False ,default=-1)
47
+
48
+ @property
49
+ def is_futures(self) -> bool:
50
+ return self.futures_info is not None
51
+
52
+ @property
53
+ def price_precision(self):
54
+ if self._tick_precision < 0:
55
+ self._tick_precision = int(abs(np.log10(self.min_tick)))
56
+ return self._tick_precision
57
+
58
+ @property
59
+ def size_precision(self):
60
+ if self._size_precision < 0:
61
+ self._size_precision = int(abs(np.log10(self.min_size_step)))
62
+ return self._size_precision
63
+
64
+ def __str__(self) -> str:
65
+ return f"{self.exchange}:{self.symbol} [{self.market_type} {str(self.futures_info) if self.futures_info else 'SPOT ' + self.base + '/' + self.quote }]"
66
+
67
+
68
+ @dataclass
69
+ class Signal:
70
+ """
71
+ Class for presenting signals generated by strategy
72
+ """
73
+ instrument: Instrument
74
+ signal: float
75
+ price: Optional[float] = None
76
+ stop: Optional[float] = None
77
+ take: Optional[float] = None
78
+ group: Optional[str] = None
79
+ comment: Optional[str] = None
80
+
81
+
82
+ class TransactionCostsCalculator:
83
+ def __init__(self, maker: float, taker: float):
84
+ self.maker = maker
85
+ self.taker = taker
86
+
87
+ def get_execution_fees(self, instrument: Instrument, exec_price: float, amount: float, crossed_market=False, conversion_rate=1.0):
88
+ if crossed_market:
89
+ return abs(amount * exec_price) * self.taker / conversion_rate
90
+ else:
91
+ return abs(amount * exec_price) * self.maker / conversion_rate
92
+
93
+ def get_overnight_fees(self, instrument: Instrument, amount: float):
94
+ return 0.0
95
+
96
+ def get_funding_rates_fees(self, instrument: Instrument, amount: float):
97
+ return 0.0
98
+
99
+ def __repr__(self):
100
+ return f'<TCC: {self.maker * 100:.4f} / {self.taker * 100:.4f}>'
101
+
102
+
103
+ ZERO_COSTS = TransactionCostsCalculator(0.0, 0.0)
104
+
105
+
106
+ class Position:
107
+ instrument: Instrument # instrument for this poisition
108
+ quantity: float = 0.0 # quantity positive for long and negative for short
109
+ tcc: TransactionCostsCalculator # transaction costs calculator
110
+ pnl: float = 0.0 # total cumulative position PnL in portfolio basic funds currency
111
+ r_pnl: float = 0.0 # total cumulative position PnL in portfolio basic funds currency
112
+ market_value: float = 0.0 # position's market value in quote currency
113
+ market_value_funds: float = 0.0 # position market value in portfolio funded currency
114
+ position_avg_price: float = 0.0 # average position price
115
+ position_avg_price_funds: float = 0.0 # average position price
116
+ commissions: float = 0.0 # cumulative commissions paid for this position
117
+
118
+ last_update_time: Optional[int] = None # when price updated or position changed
119
+ last_update_price: Optional[float] = None # last update price (actually instrument's price) in quoted currency
120
+
121
+ # - helpers for position processing
122
+ _formatter: str
123
+ _prc_formatter: str
124
+ _qty_multiplier: float = 1.0
125
+
126
+ def __init__(self, instrument: Instrument, tcc: TransactionCostsCalculator,
127
+ quantity=0.0, average_price=0.0, aux_price=1.0,
128
+ ) -> None:
129
+ self.instrument = instrument
130
+ self.tcc = tcc
131
+
132
+ # - size/price formaters
133
+ # time [symbol] qty
134
+ self._formatter = f'%s [{instrument.exchange}:{instrument.symbol}] %{instrument.size_precision+8}.{instrument.size_precision}f'
135
+ # pos_avg_px pnl | mkt_price mkt_value
136
+ self._formatter += f'%10.{instrument.price_precision}f %+10.4f | %s %10.2f'
137
+ self._prc_formatter = f"%.{instrument.price_precision}f"
138
+ if instrument.is_futures:
139
+ self._qty_multiplier = instrument.futures_info.contract_size
140
+
141
+ if quantity != 0.0 and average_price > 0.0:
142
+ self.quantity = quantity
143
+ raise ValueError("[TODO] Position: restore state by quantity and avg price !!!!")
144
+
145
+ def _price(self, update: Union[Quote, Trade]) -> float:
146
+ if isinstance(update, Quote):
147
+ return update.bid if np.sign(self.quantity) > 0 else update.ask
148
+ elif isinstance(update, Trade):
149
+ return update.price
150
+ raise ValueError(f"Unknown update type: {type(update)}")
151
+
152
+ def update_position(self, timestamp: dt_64, position: float, exec_price: float, aggressive=True, conversion_rate:float=1) -> float:
153
+ # - realized PnL of this fill
154
+ deal_pnl = 0
155
+ quantity = self.quantity
156
+
157
+ if quantity != position:
158
+ pos_change = position - quantity
159
+ direction = np.sign(pos_change)
160
+ prev_direction = np.sign(quantity)
161
+
162
+ # how many shares are closed/open
163
+ qty_closing = min(abs(self.quantity), abs(pos_change)) * direction if prev_direction != direction else 0
164
+ qty_opening = pos_change if prev_direction == direction else pos_change - qty_closing
165
+
166
+ # - extract realized part of PnL
167
+ if qty_closing != 0:
168
+ deal_pnl = qty_closing * (self.position_avg_price - exec_price)
169
+ quantity += qty_closing
170
+ # - reset average price to 0 if smaller than minimal price change to avoid cumulative error
171
+ if abs(quantity) < self.instrument.min_size_step:
172
+ quantity = 0.0
173
+ self.position_avg_price = 0.0
174
+
175
+ # - if it has something to add to position let's update price and cost
176
+ if qty_opening != 0:
177
+ qa_open, qas = abs(qty_opening), abs(quantity)
178
+ self.position_avg_price = (qa_open * exec_price + qas * self.position_avg_price) / (qas + qa_open)
179
+
180
+ # - update position and position's price
181
+ self.position_avg_price_funds = self.position_avg_price / conversion_rate
182
+ self.quantity = position
183
+
184
+ # - convert PnL to fund currency
185
+ self.r_pnl += deal_pnl / conversion_rate
186
+
187
+ # - update pnl
188
+ self._update_market_price(time_as_nsec(timestamp), exec_price, conversion_rate)
189
+
190
+ # - calculate transaction costs
191
+ comms = self.tcc.get_execution_fees(self.instrument, exec_price, pos_change, aggressive, conversion_rate)
192
+ self.commissions += comms
193
+
194
+ return deal_pnl
195
+
196
+ def update_market_price(self, price: Union[Quote, Trade], conversion_rate:float=1) -> float:
197
+ return self._update_market_price(price.time, self._price(price), conversion_rate)
198
+
199
+ def _update_market_price(self, timestamp: dt_64, price: float, conversion_rate:float) -> float:
200
+ self.last_update_time = timestamp
201
+ self.last_update_price = price
202
+
203
+ if not np.isnan(price):
204
+ self.pnl = self.quantity * (price - self.position_avg_price) / conversion_rate + self.r_pnl
205
+ self.market_value = self.quantity * self.last_update_price * self._qty_multiplier
206
+
207
+ # calculate mkt value in funded currency
208
+ self.market_value_funds = self.market_value / conversion_rate
209
+ return self.pnl
210
+
211
+ def total_pnl(self, conversion_rate:float=1.0) -> float:
212
+ pnl = self.r_pnl
213
+ if not np.isnan(self.last_update_price):
214
+ pnl += self.quantity * (self.last_update_price - self.position_avg_price) / conversion_rate
215
+ return pnl
216
+
217
+ @staticmethod
218
+ def _t2s(t) -> str:
219
+ return np.datetime64(t, 'ns').astype('datetime64[ms]').item().strftime('%Y-%m-%d %H:%M:%S') if t else '---'
220
+
221
+ def __str__(self):
222
+ _mkt_price = (self._prc_formatter % self.last_update_price) if self.last_update_price else "---"
223
+ return self._formatter % (Position._t2s(self.last_update_time), self.quantity, self.position_avg_price_funds,self.pnl, _mkt_price, self.market_value_funds)
224
+
qubx/core/lookups.py ADDED
@@ -0,0 +1,152 @@
1
+ import glob, re
2
+ import json, os, dataclasses
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional
5
+
6
+ from qubx.core.basics import Instrument, FuturesInfo
7
+ from qubx.utils.marketdata.binance import get_binance_symbol_info_for_type
8
+ from qubx import logger
9
+ from qubx.utils.misc import makedirs, get_local_qubx_folder
10
+
11
+
12
+ class _InstrumentEncoder(json.JSONEncoder):
13
+ def default(self, obj):
14
+ if dataclasses.is_dataclass(obj):
15
+ return {k:v for k,v in dataclasses.asdict(obj).items() if not k.startswith('_')}
16
+ if isinstance(obj, (datetime)):
17
+ return obj.isoformat()
18
+ return super().default(obj)
19
+
20
+
21
+ class _InstrumentDecoder(json.JSONDecoder):
22
+ def _preprocess(d, ks):
23
+ fi = d.get(ks)
24
+ if fi:
25
+ fi['delivery_date'] = datetime.strptime(fi.get('delivery_date', '5000-01-01T00:00:00'),'%Y-%m-%dT%H:%M:%S')
26
+ fi['onboard_date'] = datetime.strptime(fi.get('onboard_date', '1970-01-01T00:00:00'),'%Y-%m-%dT%H:%M:%S')
27
+ return d | {ks: FuturesInfo(**fi) if fi else None}
28
+
29
+ def decode(self, json_string):
30
+ obj = super(_InstrumentDecoder, self).decode(json_string)
31
+ if isinstance(obj, dict):
32
+ return Instrument(**_InstrumentDecoder._preprocess(obj, 'futures_info'))
33
+ elif isinstance(obj, list):
34
+ return [Instrument(**_InstrumentDecoder._preprocess(o, 'futures_info')) for o in obj]
35
+ return obj
36
+
37
+
38
+ class InstrumentsLookup:
39
+ lookup: Dict[str, Instrument]
40
+ path: str
41
+
42
+ def __init__(self, path: str=makedirs(get_local_qubx_folder(), 'instruments')) -> None:
43
+ self.path = path
44
+ if not self.load():
45
+ self.refresh()
46
+ self.load()
47
+
48
+ def load(self) -> bool:
49
+ self.lookup = {}
50
+ data_exists = False
51
+ for fs in glob.glob(self.path + '/*.json'):
52
+ try:
53
+ with open(fs, 'r') as f:
54
+ instrs = json.load(f, cls=_InstrumentDecoder)
55
+ for i in instrs:
56
+ self.lookup[f"{i.exchange}:{i.symbol}"] = i
57
+ data_exists = True
58
+ except Exception as ex:
59
+ logger.warning(ex)
60
+
61
+ return data_exists
62
+
63
+ def find(self, exchange: str, base: str, quote: str) -> Optional[Instrument]:
64
+ for i in self.lookup.values():
65
+ if i.exchange == exchange and (
66
+ (i.base == base and i.quote == quote) or (i.base == quote and i.quote == base)
67
+ ):
68
+ return i
69
+ return None
70
+
71
+ def find_aux_instrument_for(self, instrument: Instrument, base_currency: str) -> Optional[Instrument]:
72
+ """
73
+ Tries to find aux instrument (for conversions to funded currency)
74
+ for example:
75
+ ETHBTC -> BTCUSDT for base_currency USDT
76
+ EURGBP -> GBPUSD for base_currency USD
77
+ ...
78
+ """
79
+ base_currency = base_currency.upper()
80
+ if instrument.quote != base_currency and instrument._aux_instrument is None:
81
+ return self.find(instrument.exchange, instrument.quote, base_currency)
82
+ return instrument._aux_instrument
83
+
84
+ def __getitem__(self, spath: str) -> List[Instrument]:
85
+ res = []
86
+ c = re.compile(spath)
87
+ for k, v in self.lookup.items():
88
+ if re.match(c, k):
89
+ res.append(v)
90
+ return res
91
+
92
+ def refresh(self):
93
+ for mn in dir(self):
94
+ if mn.startswith('_update_'):
95
+ getattr(self, mn)(self.path)
96
+
97
+ def _update_kraken(self, path: str):
98
+ # TODO
99
+ pass
100
+
101
+ def _update_dukas(self, path: str):
102
+ instruments = [
103
+ Instrument('EURUSD', 'FX', 'DUKAS', 'EUR', 'USD', 'USD', 0.00001, 1, 1000),
104
+ Instrument('GBPUSD', 'FX', 'DUKAS', 'GBP', 'USD', 'USD', 0.00001, 1, 1000),
105
+ Instrument('USDJPY', 'FX', 'DUKAS', 'USD', 'JPY', 'USD', 0.001, 1, 1000),
106
+ Instrument('USDCAD', 'FX', 'DUKAS', 'USD', 'CAD', 'USD', 0.00001, 1, 1000),
107
+ Instrument('AUDUSD', 'FX', 'DUKAS', 'AUD', 'USD', 'USD', 0.00001, 1, 1000),
108
+ Instrument('USDPLN', 'FX', 'DUKAS', 'USD', 'PLN', 'USD', 0.00001, 1, 1000),
109
+ Instrument('EURGBP', 'FX', 'DUKAS', 'EUR', 'GBP', 'USD', 0.00001, 1, 1000),
110
+ # TODO: addd all or find how to get it from site
111
+ ]
112
+ logger.info(f'Updates {len(instruments)} for DUKASCOPY')
113
+ with open(os.path.join(path, f'dukas.json'), 'w') as f:
114
+ json.dump(instruments, f, cls=_InstrumentEncoder)
115
+
116
+ def _update_binance(self, path: str):
117
+ infos = get_binance_symbol_info_for_type(['UM', 'CM', 'SPOT'])
118
+ for exchange, info in infos.items():
119
+ instruments = []
120
+ for s in info['symbols']:
121
+ tick_size, size_step = None, None
122
+ for i in s['filters']:
123
+ if i['filterType'] == 'PRICE_FILTER':
124
+ tick_size = float(i['tickSize'])
125
+ if i['filterType'] == 'LOT_SIZE':
126
+ size_step = float(i['stepSize'])
127
+
128
+ fut_info = None
129
+ if 'contractType' in s:
130
+ fut_info = FuturesInfo(
131
+ s.get('contractType', 'UNKNOWN'),
132
+ datetime.fromtimestamp(s.get('deliveryDate', 0)/1000.0),
133
+ datetime.fromtimestamp(s.get('onboardDate', 0)/1000.0),
134
+ float(s.get('contractSize', 1)),
135
+ float(s.get('maintMarginPercent', 0)),
136
+ float(s.get('requiredMarginPercent', 0)),
137
+ float(s.get('liquidationFee', 0)),
138
+ )
139
+
140
+ instruments.append(
141
+ Instrument(
142
+ s['symbol'], 'CRYPTO', exchange.upper(), s['baseAsset'], s['quoteAsset'], s.get('marginAsset', None),
143
+ tick_size, size_step,
144
+ min_size=size_step, # TODO: not sure about minimal position for Binance
145
+ futures_info=fut_info
146
+ ))
147
+
148
+ logger.info(f'Loaded {len(instruments)} for {exchange}')
149
+
150
+ with open(os.path.join(path, f'{exchange}.json'), 'w') as f:
151
+ json.dump(instruments, f, cls=_InstrumentEncoder)
152
+