Qubx 0.4.0__cp311-cp311-manylinux_2_35_x86_64.whl → 0.4.2__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/backtester/queue.py +5 -5
- qubx/backtester/simulator.py +64 -41
- qubx/connectors/ccxt/ccxt_connector.py +523 -173
- qubx/connectors/ccxt/ccxt_exceptions.py +4 -0
- qubx/connectors/ccxt/ccxt_trading.py +8 -15
- qubx/connectors/ccxt/ccxt_utils.py +3 -3
- qubx/core/basics.py +43 -4
- qubx/core/context.py +22 -17
- qubx/core/exceptions.py +4 -0
- qubx/core/helpers.py +33 -49
- qubx/core/interfaces.py +64 -51
- qubx/core/metrics.py +99 -43
- qubx/core/mixins/market.py +38 -10
- qubx/core/mixins/processing.py +99 -69
- qubx/core/mixins/subscription.py +24 -10
- qubx/core/mixins/universe.py +5 -8
- qubx/core/series.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/data/helpers.py +27 -18
- qubx/data/readers.py +3 -2
- qubx/pandaz/utils.py +12 -8
- qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/trackers/sizers.py +1 -1
- qubx/utils/collections.py +2 -2
- qubx/utils/ntp.py +8 -3
- qubx/utils/runner.py +77 -15
- qubx/utils/threading.py +14 -0
- {qubx-0.4.0.dist-info → qubx-0.4.2.dist-info}/METADATA +1 -1
- {qubx-0.4.0.dist-info → qubx-0.4.2.dist-info}/RECORD +31 -29
- qubx-0.4.2.dist-info/entry_points.txt +3 -0
- {qubx-0.4.0.dist-info → qubx-0.4.2.dist-info}/WHEEL +0 -0
qubx/backtester/queue.py
CHANGED
|
@@ -17,7 +17,7 @@ _SW = Stopwatch()
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class DataLoader:
|
|
20
|
-
_TYPE_MAPPERS = {"agg_trade": "trade", "
|
|
20
|
+
_TYPE_MAPPERS = {"agg_trade": "trade", "bar": "ohlc", "ohlcv": "ohlc"}
|
|
21
21
|
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
@@ -25,7 +25,7 @@ class DataLoader:
|
|
|
25
25
|
reader: DataReader,
|
|
26
26
|
instrument: Instrument,
|
|
27
27
|
timeframe: str | None,
|
|
28
|
-
|
|
28
|
+
warmup_period: str | None = None,
|
|
29
29
|
data_type: str = "ohlc",
|
|
30
30
|
output_type: str | None = None, # transfomer can somtimes map to a different output type
|
|
31
31
|
chunksize: int = 5_000,
|
|
@@ -34,7 +34,7 @@ class DataLoader:
|
|
|
34
34
|
self._spec = f"{instrument.exchange}:{instrument.symbol}"
|
|
35
35
|
self._reader = reader
|
|
36
36
|
self._transformer = transformer
|
|
37
|
-
self.
|
|
37
|
+
self._warmup_period = warmup_period
|
|
38
38
|
self._timeframe = timeframe
|
|
39
39
|
self._data_type = data_type
|
|
40
40
|
self._output_type = output_type
|
|
@@ -43,8 +43,8 @@ class DataLoader:
|
|
|
43
43
|
|
|
44
44
|
def load(self, start: str | pd.Timestamp, end: str | pd.Timestamp) -> Iterator:
|
|
45
45
|
if self._first_load:
|
|
46
|
-
if self.
|
|
47
|
-
start = pd.Timestamp(start) -
|
|
46
|
+
if self._warmup_period:
|
|
47
|
+
start = pd.Timestamp(start) - pd.Timedelta(self._warmup_period)
|
|
48
48
|
self._first_load = False
|
|
49
49
|
|
|
50
50
|
args = dict(
|
qubx/backtester/simulator.py
CHANGED
|
@@ -11,7 +11,7 @@ from qubx import lookup, logger, QubxLogConfig
|
|
|
11
11
|
from qubx.core.account import AccountProcessor
|
|
12
12
|
from qubx.core.helpers import BasicScheduler
|
|
13
13
|
from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
|
|
14
|
-
from qubx.core.series import Quote
|
|
14
|
+
from qubx.core.series import Quote, time_as_nsec
|
|
15
15
|
from qubx.core.basics import (
|
|
16
16
|
ITimeProvider,
|
|
17
17
|
Instrument,
|
|
@@ -23,6 +23,7 @@ from qubx.core.basics import (
|
|
|
23
23
|
TradingSessionResult,
|
|
24
24
|
TransactionCostsCalculator,
|
|
25
25
|
dt_64,
|
|
26
|
+
SubscriptionType,
|
|
26
27
|
)
|
|
27
28
|
from qubx.core.series import TimeSeries, Trade, Quote, Bar, OHLCV
|
|
28
29
|
from qubx.core.interfaces import (
|
|
@@ -32,7 +33,6 @@ from qubx.core.interfaces import (
|
|
|
32
33
|
PositionsTracker,
|
|
33
34
|
IStrategyContext,
|
|
34
35
|
TriggerEvent,
|
|
35
|
-
SubscriptionType,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
from qubx.core.context import StrategyContext
|
|
@@ -50,6 +50,7 @@ from qubx.data.readers import (
|
|
|
50
50
|
)
|
|
51
51
|
from qubx.pandaz.utils import scols
|
|
52
52
|
from qubx.utils.misc import ProgressParallel
|
|
53
|
+
from qubx.utils.time import infer_series_frequency
|
|
53
54
|
from joblib import delayed
|
|
54
55
|
from .queue import DataLoader, SimulatedDataQueue, EventBatcher
|
|
55
56
|
|
|
@@ -287,22 +288,21 @@ class SimulatedTrading(ITradingServiceProvider):
|
|
|
287
288
|
return [o for ome in self._ome.values() for o in ome.get_open_orders()]
|
|
288
289
|
|
|
289
290
|
def get_position(self, instrument: Instrument) -> Position:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
# - initiolize empty position
|
|
302
|
-
position = Position(instrument) # type: ignore
|
|
303
|
-
self._half_tick_size[instrument] = instrument.min_tick / 2 # type: ignore
|
|
304
|
-
self.acc.attach_positions(position)
|
|
291
|
+
if instrument in self.acc.positions:
|
|
292
|
+
return self.acc.positions[instrument]
|
|
293
|
+
|
|
294
|
+
# - initiolize OME for this instrument
|
|
295
|
+
self._ome[instrument] = OrdersManagementEngine(
|
|
296
|
+
instrument=instrument,
|
|
297
|
+
time_provider=self,
|
|
298
|
+
tcc=self._fees_calculator, # type: ignore
|
|
299
|
+
fill_stop_order_at_price=self._fill_stop_order_at_price,
|
|
300
|
+
)
|
|
305
301
|
|
|
302
|
+
# - initiolize empty position
|
|
303
|
+
position = Position(instrument) # type: ignore
|
|
304
|
+
self._half_tick_size[instrument] = instrument.min_tick / 2 # type: ignore
|
|
305
|
+
self.acc.attach_positions(position)
|
|
306
306
|
return self.acc._positions[instrument]
|
|
307
307
|
|
|
308
308
|
def time(self) -> dt_64:
|
|
@@ -391,7 +391,7 @@ class SimulatedExchange(IBrokerServiceProvider):
|
|
|
391
391
|
_scheduler: BasicScheduler
|
|
392
392
|
_current_time: dt_64
|
|
393
393
|
_hist_data_type: str
|
|
394
|
-
_loaders: dict[
|
|
394
|
+
_loaders: dict[Instrument, dict[str, DataLoader]]
|
|
395
395
|
_pregenerated_signals: Dict[Instrument, pd.Series]
|
|
396
396
|
|
|
397
397
|
def __init__(
|
|
@@ -432,22 +432,22 @@ class SimulatedExchange(IBrokerServiceProvider):
|
|
|
432
432
|
self,
|
|
433
433
|
instruments: list[Instrument],
|
|
434
434
|
subscription_type: str,
|
|
435
|
+
warmup_period: str | None = None,
|
|
435
436
|
timeframe: str | None = None,
|
|
436
|
-
nback: int = 0,
|
|
437
437
|
**kwargs,
|
|
438
438
|
) -> bool:
|
|
439
439
|
units = kwargs.get("timestamp_units", "ns")
|
|
440
440
|
|
|
441
441
|
for instr in instruments:
|
|
442
442
|
logger.debug(
|
|
443
|
-
f"SimulatedExchangeService :: subscribe :: {instr.symbol}({subscription_type}, {
|
|
443
|
+
f"SimulatedExchangeService :: subscribe :: {instr.symbol}({subscription_type}, {warmup_period}, {timeframe})"
|
|
444
444
|
)
|
|
445
445
|
self._symbol_to_instrument[instr.symbol] = instr
|
|
446
446
|
|
|
447
447
|
_params: Dict[str, Any] = dict(
|
|
448
448
|
reader=self._reader,
|
|
449
449
|
instrument=instr,
|
|
450
|
-
|
|
450
|
+
warmup_period=warmup_period,
|
|
451
451
|
timeframe=timeframe,
|
|
452
452
|
)
|
|
453
453
|
|
|
@@ -470,23 +470,34 @@ class SimulatedExchange(IBrokerServiceProvider):
|
|
|
470
470
|
|
|
471
471
|
# - add loader for this instrument
|
|
472
472
|
ldr = DataLoader(**_params)
|
|
473
|
-
self._loaders[instr
|
|
473
|
+
self._loaders[instr][subscription_type] = ldr
|
|
474
474
|
self._data_queue += ldr
|
|
475
475
|
|
|
476
476
|
return True
|
|
477
477
|
|
|
478
|
-
def unsubscribe(self,
|
|
478
|
+
def unsubscribe(self, instruments: List[Instrument], subscription_type: str | None) -> bool:
|
|
479
479
|
for instr in instruments:
|
|
480
480
|
if instr.symbol in self._loaders:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
self._loaders
|
|
481
|
+
if subscription_type:
|
|
482
|
+
logger.debug(f"SimulatedExchangeService :: unsubscribe :: {instr.symbol} :: {subscription_type}")
|
|
483
|
+
self._data_queue -= self._loaders[instr].pop(subscription_type)
|
|
484
|
+
if not self._loaders[instr]:
|
|
485
|
+
self._loaders.pop(instr)
|
|
486
|
+
else:
|
|
487
|
+
logger.debug(f"SimulatedExchangeService :: unsubscribe :: {instr.symbol}")
|
|
488
|
+
for ldr in self._loaders[instr].values():
|
|
489
|
+
self._data_queue -= ldr
|
|
490
|
+
self._loaders.pop(instr)
|
|
485
491
|
return True
|
|
486
492
|
|
|
487
493
|
def has_subscription(self, subscription_type: str, instrument: Instrument) -> bool:
|
|
488
494
|
return instrument in self._loaders and subscription_type in self._loaders[instrument]
|
|
489
495
|
|
|
496
|
+
def get_subscriptions(self, instrument: Instrument) -> Dict[str, Dict[str, Any]]:
|
|
497
|
+
# TODO: implement
|
|
498
|
+
# return {k: v.params for k, v in self._loaders[instrument].items()}
|
|
499
|
+
return {}
|
|
500
|
+
|
|
490
501
|
def _try_add_process_signals(self, start: str | pd.Timestamp, end: str | pd.Timestamp) -> None:
|
|
491
502
|
if self._pregenerated_signals:
|
|
492
503
|
for s, v in self._pregenerated_signals.items():
|
|
@@ -593,17 +604,34 @@ class SimulatedExchange(IBrokerServiceProvider):
|
|
|
593
604
|
|
|
594
605
|
def get_historical_ohlcs(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
|
|
595
606
|
start = pd.Timestamp(self.time())
|
|
596
|
-
end = start - nbarsback * pd.Timedelta(timeframe)
|
|
607
|
+
end = start - nbarsback * (_timeframe := pd.Timedelta(timeframe))
|
|
597
608
|
_spec = f"{instrument.exchange}:{instrument.symbol}"
|
|
598
|
-
|
|
599
|
-
data_id=_spec, start=start, stop=end, transform=AsTimestampedRecords()
|
|
609
|
+
return self._convert_records_to_bars(
|
|
610
|
+
self._reader.read(data_id=_spec, start=start, stop=end, transform=AsTimestampedRecords()),
|
|
611
|
+
time_as_nsec(self.time()),
|
|
612
|
+
_timeframe.asm8.item()
|
|
600
613
|
)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
]
|
|
614
|
+
|
|
615
|
+
def _convert_records_to_bars(self, records: List[Dict[str, Any]], cut_time_ns: int, timeframe_ns: int) -> List[Bar]:
|
|
616
|
+
"""
|
|
617
|
+
Convert records to bars and we need to cut last bar up to the cut_time_ns
|
|
618
|
+
"""
|
|
619
|
+
bars = []
|
|
620
|
+
|
|
621
|
+
_data_tf = infer_series_frequency([r['timestamp_ns'] for r in records[:100]])
|
|
622
|
+
timeframe_ns = _data_tf.item()
|
|
623
|
+
|
|
624
|
+
if records is not None:
|
|
625
|
+
for r in records:
|
|
626
|
+
_bts_0 = np.datetime64(r["timestamp_ns"], "ns").item()
|
|
627
|
+
o, h, l, c, v = r["open"], r["high"], r["low"], r["close"], r["volume"]
|
|
628
|
+
|
|
629
|
+
if _bts_0 <= cut_time_ns and cut_time_ns < _bts_0 + timeframe_ns:
|
|
630
|
+
break
|
|
631
|
+
|
|
632
|
+
bars.append(Bar(_bts_0, o, h, l, c, v))
|
|
633
|
+
|
|
634
|
+
return bars
|
|
607
635
|
|
|
608
636
|
def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
|
|
609
637
|
logger.debug(f"Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}")
|
|
@@ -896,11 +924,6 @@ class SignalsProxy(IStrategy):
|
|
|
896
924
|
def on_init(self, ctx: IStrategyContext):
|
|
897
925
|
ctx.set_base_subscription(SubscriptionType.OHLC, timeframe=self.timeframe)
|
|
898
926
|
|
|
899
|
-
def on_fit(
|
|
900
|
-
self, ctx: IStrategyContext, fit_time: str | pd.Timestamp, previous_fit_time: str | pd.Timestamp | None = None
|
|
901
|
-
):
|
|
902
|
-
return None
|
|
903
|
-
|
|
904
927
|
def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> Optional[List[Signal]]:
|
|
905
928
|
if event.data and event.type == "event":
|
|
906
929
|
signal = event.data.get("order")
|