Qubx 0.6.54__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.57__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/backtester/runner.py +1 -1
- qubx/core/basics.py +111 -5
- qubx/core/context.py +7 -0
- qubx/core/helpers.py +23 -9
- qubx/core/interfaces.py +14 -0
- qubx/core/lookups.py +226 -304
- qubx/core/mixins/market.py +21 -0
- qubx/core/mixins/processing.py +23 -3
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pyx +1 -1
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/resources/crypto-fees.ini +98 -0
- qubx/resources/instruments/symbols-binance-spot.json +1 -0
- qubx/resources/instruments/symbols-binance.cm-future.json +1 -0
- qubx/resources/instruments/symbols-binance.cm-perpetual.json +1 -0
- qubx/resources/instruments/symbols-binance.um-future.json +1 -0
- qubx/resources/instruments/symbols-binance.um-perpetual.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +1 -0
- qubx/resources/instruments/symbols-hyperliquid-spot.json +1 -0
- qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +1 -0
- qubx/resources/instruments/symbols-kraken-spot.json +1 -0
- qubx/resources/instruments/symbols-kraken.f-future.json +1 -0
- qubx/resources/instruments/symbols-kraken.f-perpetual.json +1 -0
- qubx/restorers/factory.py +3 -3
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/marketdata/ccxt.py +51 -6
- qubx/utils/marketdata/dukas.py +1 -1
- qubx/utils/runner/runner.py +1 -1
- {qubx-0.6.54.dist-info → qubx-0.6.57.dist-info}/METADATA +1 -1
- {qubx-0.6.54.dist-info → qubx-0.6.57.dist-info}/RECORD +33 -28
- qubx/resources/instruments/symbols-binance.cm.json +0 -1
- qubx/resources/instruments/symbols-binance.json +0 -1
- qubx/resources/instruments/symbols-binance.um.json +0 -1
- qubx/resources/instruments/symbols-bitfinex.f.json +0 -1
- qubx/resources/instruments/symbols-bitfinex.json +0 -1
- qubx/resources/instruments/symbols-kraken.f.json +0 -1
- qubx/resources/instruments/symbols-kraken.json +0 -1
- {qubx-0.6.54.dist-info → qubx-0.6.57.dist-info}/LICENSE +0 -0
- {qubx-0.6.54.dist-info → qubx-0.6.57.dist-info}/WHEEL +0 -0
- {qubx-0.6.54.dist-info → qubx-0.6.57.dist-info}/entry_points.txt +0 -0
qubx/backtester/runner.py
CHANGED
|
@@ -439,7 +439,7 @@ class SimulationRunner:
|
|
|
439
439
|
if isinstance(commissions, (str, type(None))):
|
|
440
440
|
commissions = {e: commissions for e in exchanges}
|
|
441
441
|
for exchange in exchanges:
|
|
442
|
-
_exchange_to_tcc[exchange] = lookup.
|
|
442
|
+
_exchange_to_tcc[exchange] = lookup.find_fees(exchange.lower(), commissions.get(exchange))
|
|
443
443
|
return _exchange_to_tcc
|
|
444
444
|
|
|
445
445
|
def _construct_account_processor(
|
qubx/core/basics.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from enum import StrEnum
|
|
@@ -176,6 +177,14 @@ class MarketType(StrEnum):
|
|
|
176
177
|
|
|
177
178
|
@dataclass
|
|
178
179
|
class Instrument:
|
|
180
|
+
"""
|
|
181
|
+
Instrument class.
|
|
182
|
+
|
|
183
|
+
- 2025-06-11: Important change for FUTURE type: now instrument's symbol contains delivery date in format YYYYMMDD.
|
|
184
|
+
So now for let's say september's BTCUSDT future, symbol would be BTCUSD.20250914
|
|
185
|
+
and full id is `BINANCE.UM:FUTURE:BTCUSD.20250914`
|
|
186
|
+
"""
|
|
187
|
+
|
|
179
188
|
symbol: str
|
|
180
189
|
asset_type: AssetType
|
|
181
190
|
market_type: MarketType
|
|
@@ -192,8 +201,10 @@ class Instrument:
|
|
|
192
201
|
maint_margin: float = 0.0 # maintenance margin
|
|
193
202
|
liquidation_fee: float = 0.0 # liquidation fee
|
|
194
203
|
contract_size: float = 1.0 # contract size
|
|
195
|
-
onboard_date: datetime | None = None
|
|
196
|
-
delivery_date: datetime | None = None
|
|
204
|
+
onboard_date: datetime | None = None # date when instrument was listed on the exchange
|
|
205
|
+
delivery_date: datetime | None = None # date when instrument is delivered
|
|
206
|
+
delist_date: datetime | None = None # date when instrument is delisted
|
|
207
|
+
inverse: bool = False # if true, then the future is inverse
|
|
197
208
|
|
|
198
209
|
@property
|
|
199
210
|
def price_precision(self):
|
|
@@ -437,9 +448,6 @@ class AssetBalance:
|
|
|
437
448
|
return self
|
|
438
449
|
|
|
439
450
|
|
|
440
|
-
MARKET_TYPE = Literal["SPOT", "MARGIN", "SWAP", "FUTURES", "OPTION"]
|
|
441
|
-
|
|
442
|
-
|
|
443
451
|
class Position:
|
|
444
452
|
instrument: Instrument # instrument for this position
|
|
445
453
|
quantity: float = 0.0 # quantity positive for long and negative for short
|
|
@@ -878,3 +886,101 @@ class RestoredState:
|
|
|
878
886
|
balances: dict[str, AssetBalance]
|
|
879
887
|
instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
|
|
880
888
|
positions: dict[Instrument, Position]
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class InstrumentsLookup:
|
|
892
|
+
def get_lookup(self) -> dict[str, Instrument]: ...
|
|
893
|
+
|
|
894
|
+
def find(
|
|
895
|
+
self,
|
|
896
|
+
exchange: str,
|
|
897
|
+
base: str,
|
|
898
|
+
quote: str,
|
|
899
|
+
settle: str | None = None,
|
|
900
|
+
market_type: MarketType | None = None,
|
|
901
|
+
) -> Instrument | None:
|
|
902
|
+
for i in self.get_lookup().values():
|
|
903
|
+
if (
|
|
904
|
+
i.exchange == exchange
|
|
905
|
+
and ((i.base == base and i.quote == quote) or (i.base == quote and i.quote == base))
|
|
906
|
+
and (market_type is None or i.market_type == market_type)
|
|
907
|
+
):
|
|
908
|
+
if settle is not None and i.settle is not None:
|
|
909
|
+
if i.settle == settle:
|
|
910
|
+
return i
|
|
911
|
+
else:
|
|
912
|
+
return i
|
|
913
|
+
return None
|
|
914
|
+
|
|
915
|
+
def find_symbol(self, exchange: str, symbol: str, market_type: MarketType | None = None) -> Instrument | None:
|
|
916
|
+
for i in self.get_lookup().values():
|
|
917
|
+
if (
|
|
918
|
+
(i.exchange == exchange)
|
|
919
|
+
and (i.symbol == symbol)
|
|
920
|
+
and (market_type is None or i.market_type == market_type)
|
|
921
|
+
):
|
|
922
|
+
return i
|
|
923
|
+
|
|
924
|
+
return None
|
|
925
|
+
|
|
926
|
+
def find_instruments(
|
|
927
|
+
self,
|
|
928
|
+
exchange: str,
|
|
929
|
+
quote: str | None = None,
|
|
930
|
+
market_type: MarketType | None = None,
|
|
931
|
+
as_of: str | pd.Timestamp | None = None,
|
|
932
|
+
) -> list[Instrument]:
|
|
933
|
+
"""
|
|
934
|
+
Find instruments by exchange, quote, market type and as of date.
|
|
935
|
+
If as_of is not None, then only instruments that are not delisted after as_of date will be returned.
|
|
936
|
+
- exchange: str - exchange name
|
|
937
|
+
- quote: str | None - quote currency
|
|
938
|
+
- market_type: MarketType | None - market type
|
|
939
|
+
- as_of is a string in format YYYY-MM-DD or pd.Timestamp or None
|
|
940
|
+
"""
|
|
941
|
+
_limit_time = pd.Timestamp(as_of) if as_of else None
|
|
942
|
+
return [
|
|
943
|
+
i
|
|
944
|
+
for i in self.get_lookup().values()
|
|
945
|
+
if i.exchange == exchange
|
|
946
|
+
and (quote is None or i.quote == quote)
|
|
947
|
+
and (market_type is None or i.market_type == market_type)
|
|
948
|
+
and (
|
|
949
|
+
_limit_time is None
|
|
950
|
+
or (i.delist_date is None)
|
|
951
|
+
or (pd.Timestamp(i.delist_date).tz_localize(None) >= _limit_time)
|
|
952
|
+
)
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
def find_aux_instrument_for(
|
|
956
|
+
self, instrument: Instrument, base_currency: str, market_type: MarketType | None = None
|
|
957
|
+
) -> Instrument | None:
|
|
958
|
+
"""
|
|
959
|
+
Tries to find aux instrument (for conversions to funded currency)
|
|
960
|
+
for example:
|
|
961
|
+
ETHBTC -> BTCUSDT for base_currency USDT
|
|
962
|
+
EURGBP -> GBPUSD for base_currency USD
|
|
963
|
+
...
|
|
964
|
+
"""
|
|
965
|
+
if market_type is None:
|
|
966
|
+
market_type = instrument.market_type
|
|
967
|
+
base_currency = base_currency.upper()
|
|
968
|
+
if instrument.quote != base_currency:
|
|
969
|
+
return self.find(instrument.exchange, instrument.quote, base_currency, market_type=market_type)
|
|
970
|
+
return None
|
|
971
|
+
|
|
972
|
+
def __getitem__(self, spath: str) -> list[Instrument]:
|
|
973
|
+
"""
|
|
974
|
+
Helper method for finding instruments by pattern.
|
|
975
|
+
It's convenient to use in research mode.
|
|
976
|
+
"""
|
|
977
|
+
res = []
|
|
978
|
+
c = re.compile(spath)
|
|
979
|
+
for k, v in self.get_lookup().items():
|
|
980
|
+
if re.match(c, k):
|
|
981
|
+
res.append(v)
|
|
982
|
+
return res
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
class FeesLookup:
|
|
986
|
+
def find_fees(self, exchange: str, spec: str | None) -> TransactionCostsCalculator: ...
|
qubx/core/context.py
CHANGED
|
@@ -2,6 +2,8 @@ import traceback
|
|
|
2
2
|
from threading import Thread
|
|
3
3
|
from typing import Any, Callable
|
|
4
4
|
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
5
7
|
from qubx import logger
|
|
6
8
|
from qubx.core.basics import (
|
|
7
9
|
AssetBalance,
|
|
@@ -396,6 +398,11 @@ class StrategyContext(IStrategyContext):
|
|
|
396
398
|
def ohlc(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None):
|
|
397
399
|
return self._market_data_provider.ohlc(instrument, timeframe, length)
|
|
398
400
|
|
|
401
|
+
def ohlc_pd(
|
|
402
|
+
self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True
|
|
403
|
+
) -> pd.DataFrame:
|
|
404
|
+
return self._market_data_provider.ohlc_pd(instrument, timeframe, length, consolidated)
|
|
405
|
+
|
|
399
406
|
def quote(self, instrument: Instrument):
|
|
400
407
|
return self._market_data_provider.quote(instrument)
|
|
401
408
|
|
qubx/core/helpers.py
CHANGED
|
@@ -6,15 +6,15 @@ import time
|
|
|
6
6
|
from collections import defaultdict, deque
|
|
7
7
|
from inspect import isbuiltin, isclass, isfunction, ismethod, ismethoddescriptor
|
|
8
8
|
from threading import Thread
|
|
9
|
-
from typing import Any, Callable
|
|
9
|
+
from typing import Any, Callable
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
13
13
|
from croniter import croniter
|
|
14
14
|
|
|
15
15
|
from qubx import logger
|
|
16
|
-
from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped
|
|
17
|
-
from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade
|
|
16
|
+
from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped, dt_64
|
|
17
|
+
from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade, time_as_nsec
|
|
18
18
|
from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64, interval_to_cron
|
|
19
19
|
|
|
20
20
|
|
|
@@ -26,9 +26,9 @@ class CachedMarketDataHolder:
|
|
|
26
26
|
default_timeframe: np.timedelta64
|
|
27
27
|
_last_bar: dict[Instrument, Bar | None]
|
|
28
28
|
_ohlcvs: dict[Instrument, dict[np.timedelta64, OHLCV]]
|
|
29
|
-
_updates: dict[Instrument,
|
|
29
|
+
_updates: dict[Instrument, Bar | Quote | Trade]
|
|
30
30
|
|
|
31
|
-
_instr_to_sub_to_buffer:
|
|
31
|
+
_instr_to_sub_to_buffer: dict[Instrument, dict[str, deque]]
|
|
32
32
|
|
|
33
33
|
def __init__(self, default_timeframe: str | None = None, max_buffer_size: int = 10_000) -> None:
|
|
34
34
|
self._ohlcvs = dict()
|
|
@@ -93,7 +93,7 @@ class CachedMarketDataHolder:
|
|
|
93
93
|
|
|
94
94
|
return self._ohlcvs[instrument][tf]
|
|
95
95
|
|
|
96
|
-
def get_data(self, instrument: Instrument, event_type: str) ->
|
|
96
|
+
def get_data(self, instrument: Instrument, event_type: str) -> list[Any]:
|
|
97
97
|
return list(self._instr_to_sub_to_buffer[instrument][event_type])
|
|
98
98
|
|
|
99
99
|
def update(
|
|
@@ -126,7 +126,7 @@ class CachedMarketDataHolder:
|
|
|
126
126
|
pass
|
|
127
127
|
|
|
128
128
|
@SW.watch("CachedMarketDataHolder")
|
|
129
|
-
def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars:
|
|
129
|
+
def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars: list[Bar]) -> OHLCV:
|
|
130
130
|
"""
|
|
131
131
|
Update or create OHLCV series with the provided historical bars.
|
|
132
132
|
|
|
@@ -156,7 +156,7 @@ class CachedMarketDataHolder:
|
|
|
156
156
|
|
|
157
157
|
# Update the last update for this instrument
|
|
158
158
|
if bars:
|
|
159
|
-
self._updates[instrument] = bars[
|
|
159
|
+
self._updates[instrument] = bars[-1] # Use the last bar as the last update
|
|
160
160
|
|
|
161
161
|
return ohlc
|
|
162
162
|
|
|
@@ -179,7 +179,10 @@ class CachedMarketDataHolder:
|
|
|
179
179
|
if instrument in self._ohlcvs:
|
|
180
180
|
self._last_bar[instrument] = bar
|
|
181
181
|
for ser in self._ohlcvs[instrument].values():
|
|
182
|
-
|
|
182
|
+
try:
|
|
183
|
+
ser.update_by_bar(bar.time, bar.open, bar.high, bar.low, bar.close, v_tot_inc, v_buy_inc)
|
|
184
|
+
except ValueError as e:
|
|
185
|
+
logger.warning(f"Can't update ohlc series for [{instrument.symbol}] ::: {str(e)}")
|
|
183
186
|
|
|
184
187
|
@SW.watch("CachedMarketDataHolder")
|
|
185
188
|
def update_by_quote(self, instrument: Instrument, quote: Quote):
|
|
@@ -203,6 +206,17 @@ class CachedMarketDataHolder:
|
|
|
203
206
|
continue
|
|
204
207
|
ser.update(trade.time, trade.price, total_vol, bought_vol)
|
|
205
208
|
|
|
209
|
+
def finalize_ohlc_for_instruments(self, time: dt_64, instruments: list[Instrument]):
|
|
210
|
+
"""
|
|
211
|
+
Finalize all OHLCV series at the given time for the given instruments.
|
|
212
|
+
FIXME: (2025-06-17) This is part of urgent live fix and must be removed in future !!!.
|
|
213
|
+
"""
|
|
214
|
+
for instrument in instruments:
|
|
215
|
+
# - use most recent update
|
|
216
|
+
if (_u := self._updates.get(instrument)) is not None:
|
|
217
|
+
_px = extract_price(_u)
|
|
218
|
+
self.update_by_bar(instrument, Bar(time_as_nsec(time), _px, _px, _px, _px, 0, 0))
|
|
219
|
+
|
|
206
220
|
|
|
207
221
|
SPEC_REGEX = re.compile(
|
|
208
222
|
r"((?P<type>[A-Za-z]+)(\.?(?P<timeframe>[0-9A-Za-z]+))?\ *:)?"
|
qubx/core/interfaces.py
CHANGED
|
@@ -508,6 +508,20 @@ class IMarketManager(ITimeProvider):
|
|
|
508
508
|
"""
|
|
509
509
|
...
|
|
510
510
|
|
|
511
|
+
def ohlc_pd(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True) -> pd.DataFrame:
|
|
512
|
+
"""Get OHLCV data for an instrument as pandas DataFrame.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
instrument: The instrument to get data for
|
|
516
|
+
timeframe (optional): The timeframe of the data. If None, the default timeframe is used.
|
|
517
|
+
length (optional): Number of bars to retrieve. If None, full cached data is returned.
|
|
518
|
+
consolidated (optional): If True, only finished bars are returned.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
pd.DataFrame: The OHLCV data as pandas DataFrame
|
|
522
|
+
"""
|
|
523
|
+
...
|
|
524
|
+
|
|
511
525
|
def quote(self, instrument: Instrument) -> Quote | None:
|
|
512
526
|
"""Get latest quote for an instrument.
|
|
513
527
|
|