Qubx 0.2.61__tar.gz → 0.2.62__tar.gz

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 (56) hide show
  1. {qubx-0.2.61 → qubx-0.2.62}/PKG-INFO +1 -1
  2. {qubx-0.2.61 → qubx-0.2.62}/pyproject.toml +1 -1
  3. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/simulator.py +11 -6
  4. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/basics.py +44 -9
  5. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/context.py +5 -5
  6. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/utils.pyi +2 -0
  7. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/utils.pyx +9 -1
  8. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_utils.py +4 -1
  9. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/sizers.py +1 -4
  10. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/misc.py +0 -4
  11. {qubx-0.2.61 → qubx-0.2.62}/README.md +0 -0
  12. {qubx-0.2.61 → qubx-0.2.62}/build.py +0 -0
  13. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/__init__.py +0 -0
  14. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/_nb_magic.py +0 -0
  15. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/__init__.py +0 -0
  16. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/ome.py +0 -0
  17. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/optimization.py +0 -0
  18. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/queue.py +0 -0
  19. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/__init__.py +0 -0
  20. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/account.py +0 -0
  21. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/exceptions.py +0 -0
  22. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/helpers.py +0 -0
  23. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/loggers.py +0 -0
  24. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/lookups.py +0 -0
  25. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/metrics.py +0 -0
  26. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pxd +0 -0
  27. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pyi +0 -0
  28. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pyx +0 -0
  29. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/strategy.py +0 -0
  30. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/data/helpers.py +0 -0
  31. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/data/readers.py +0 -0
  32. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/gathering/simplest.py +0 -0
  33. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_connector.py +0 -0
  34. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_customizations.py +0 -0
  35. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_trading.py +0 -0
  36. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/math/__init__.py +0 -0
  37. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/math/stats.py +0 -0
  38. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/__init__.py +0 -0
  39. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/ta.py +0 -0
  40. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/utils.py +0 -0
  41. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/__init__.py +0 -0
  42. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pxd +0 -0
  43. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pyi +0 -0
  44. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pyx +0 -0
  45. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/__init__.py +0 -0
  46. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/composite.py +0 -0
  47. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/rebalancers.py +0 -0
  48. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/riskctrl.py +0 -0
  49. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/__init__.py +0 -0
  50. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/_pyxreloader.py +0 -0
  51. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/charting/lookinglass.py +0 -0
  52. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  53. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/marketdata/binance.py +0 -0
  54. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/ntp.py +0 -0
  55. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/runner.py +0 -0
  56. {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.2.61
3
+ Version: 0.2.62
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.2.61"
3
+ version = "0.2.62"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = ["Dmitry Marienko <dmitry@gmail.com>", "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>"]
6
6
  readme = "README.md"
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pandas as pd
3
3
  from collections import defaultdict
4
4
  from dataclasses import dataclass
5
- from typing import Any, Dict, List, Optional, Tuple, TypeAlias, Callable
5
+ from typing import Any, Dict, List, Optional, Tuple, TypeAlias, Callable, Literal
6
6
  from enum import Enum
7
7
  from tqdm.auto import tqdm
8
8
  from itertools import chain
@@ -187,7 +187,7 @@ class SimulatedTrading(ITradingServiceProvider):
187
187
  price: float | None = None,
188
188
  client_id: str | None = None,
189
189
  time_in_force: str = "gtc",
190
- **optional,
190
+ **options,
191
191
  ) -> Order:
192
192
  ome = self._ome.get(instrument.symbol)
193
193
  if ome is None:
@@ -201,7 +201,7 @@ class SimulatedTrading(ITradingServiceProvider):
201
201
  price,
202
202
  client_id,
203
203
  time_in_force,
204
- fill_at_price=optional.get("fill_at_price", False),
204
+ fill_at_price=options.get("fill_at_price", False),
205
205
  )
206
206
  order = report.order
207
207
  self._order_to_symbol[order.id] = instrument.symbol
@@ -685,7 +685,12 @@ def simulate(
685
685
  n_jobs: int = 1,
686
686
  silent: bool = False,
687
687
  enable_event_batching: bool = True,
688
+ debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
688
689
  ) -> list[TradingSessionResult]:
690
+
691
+ # - setup logging
692
+ QubxLogConfig.set_log_level(debug.upper() if debug else "WARNING")
693
+
689
694
  # - recognize provided data
690
695
  if isinstance(data, dict):
691
696
  data_reader = InMemoryDataFrameReader(data)
@@ -707,13 +712,13 @@ def simulate(
707
712
  f"No exchange iformation provided - you can specify it by exchange parameter or use <yellow>EXCHANGE:SYMBOL</yellow> format for symbols"
708
713
  )
709
714
  # - TODO: probably we need to raise exceptions here ?
710
- return None
715
+ return []
711
716
 
712
717
  # - check exchanges
713
718
  if len(set(_exchanges)) > 1:
714
719
  logger.error(f"Multiple exchanges found: {', '.join(_exchanges)} - this mode is not supported yet in Qubx !")
715
720
  # - TODO: probably we need to raise exceptions here ?
716
- return None
721
+ return []
717
722
 
718
723
  exchange = list(set(_exchanges))[0]
719
724
 
@@ -724,7 +729,7 @@ def simulate(
724
729
  f"Can't recognize setup - it should be a strategy, a set of signals or list of signals/strategies + tracker !"
725
730
  )
726
731
  # - TODO: probably we need to raise exceptions here ?
727
- return None
732
+ return []
728
733
 
729
734
  # - check stop time : here we try to backtest till now (may be we need to get max available time from data reader ?)
730
735
  if stop is None:
@@ -4,11 +4,11 @@ import numpy as np
4
4
  import pandas as pd
5
5
  from dataclasses import dataclass, field
6
6
 
7
- from threading import Thread, Event, Lock, Condition
7
+ from threading import Event, Lock
8
8
  from queue import Queue
9
9
 
10
10
  from qubx.core.series import Quote, Trade, time_as_nsec
11
- from qubx.core.utils import time_to_str, time_delta_to_str, recognize_timeframe
11
+ from qubx.core.utils import prec_ceil, prec_floor
12
12
 
13
13
 
14
14
  dt_64 = np.datetime64
@@ -138,6 +138,42 @@ class Instrument:
138
138
  self._size_precision = int(abs(np.log10(self.min_size_step)))
139
139
  return self._size_precision
140
140
 
141
+ def round_size_down(self, size: float) -> float:
142
+ """
143
+ Round down size to specified precision
144
+
145
+ i.size_precision == 3
146
+ i.round_size_up(0.1234) -> 0.123
147
+ """
148
+ return prec_floor(size, self.size_precision)
149
+
150
+ def round_size_up(self, size: float) -> float:
151
+ """
152
+ Round up size to specified precision
153
+
154
+ i.size_precision == 3
155
+ i.round_size_up(0.1234) -> 0.124
156
+ """
157
+ return prec_ceil(size, self.size_precision)
158
+
159
+ def round_price_down(self, price: float) -> float:
160
+ """
161
+ Round down price to specified precision
162
+
163
+ i.price_precision == 3
164
+ i.round_price_down(1.234999, 3) -> 1.234
165
+ """
166
+ return prec_floor(price, self.price_precision)
167
+
168
+ def round_price_up(self, price: float) -> float:
169
+ """
170
+ Round up price to specified precision
171
+
172
+ i.price_precision == 3
173
+ i.round_price_up(1.234999) -> 1.235
174
+ """
175
+ return prec_ceil(price, self.price_precision)
176
+
141
177
  def signal(
142
178
  self,
143
179
  signal: float,
@@ -266,11 +302,6 @@ class Order:
266
302
  return f"[{self.id}] {self.type} {self.side} {self.quantity} of {self.symbol} {('@ ' + str(self.price)) if self.price > 0 else ''} ({self.time_in_force}) [{self.status}]"
267
303
 
268
304
 
269
- def round_down(x, n):
270
- dvz = 10 ** (-n)
271
- return (int(x / dvz)) * dvz
272
-
273
-
274
305
  class Position:
275
306
  instrument: Instrument # instrument for this poisition
276
307
  quantity: float = 0.0 # quantity positive for long and negative for short
@@ -338,7 +369,11 @@ class Position:
338
369
  self, timestamp: dt_64, amount: float, exec_price: float, fee_amount: float = 0, conversion_rate: float = 1
339
370
  ) -> tuple[float, float]:
340
371
  return self.update_position(
341
- timestamp, self.quantity + amount, exec_price, fee_amount, conversion_rate=conversion_rate
372
+ timestamp,
373
+ self.instrument.round_size_down(self.quantity + amount),
374
+ exec_price,
375
+ fee_amount,
376
+ conversion_rate=conversion_rate,
342
377
  )
343
378
 
344
379
  def update_position(
@@ -374,7 +409,7 @@ class Position:
374
409
  self.__pos_incr_qty + _abs_qty_open
375
410
  )
376
411
  # - round position average price to be in line with how it's calculated by broker
377
- self.position_avg_price = round_down(pos_avg_price_raw, self.instrument.price_precision)
412
+ self.position_avg_price = self.instrument.round_price_down(pos_avg_price_raw)
378
413
  self.__pos_incr_qty += _abs_qty_open
379
414
 
380
415
  # - update position and position's price
@@ -38,7 +38,7 @@ from qubx.core.strategy import (
38
38
  from qubx.core.series import Trade, Quote, Bar, OHLCV
39
39
  from qubx.gathering.simplest import SimplePositionGatherer
40
40
  from qubx.trackers.sizers import FixedSizer
41
- from qubx.utils.misc import Stopwatch, round_down_at_min_qty
41
+ from qubx.utils.misc import Stopwatch
42
42
  from qubx.utils.time import convert_seconds_to_str
43
43
 
44
44
 
@@ -702,7 +702,7 @@ class StrategyContextImpl(StrategyContext):
702
702
  amount: float,
703
703
  price: float | None = None,
704
704
  time_in_force="gtc",
705
- **optional,
705
+ **options,
706
706
  ) -> Order:
707
707
  instrument: Instrument | None = (
708
708
  self._symb_to_instr.get(instr_or_symbol) if isinstance(instr_or_symbol, str) else instr_or_symbol
@@ -711,7 +711,7 @@ class StrategyContextImpl(StrategyContext):
711
711
  raise ValueError(f"Can't find instrument for symbol {instr_or_symbol}")
712
712
 
713
713
  # - adjust size
714
- size_adj = round_down_at_min_qty(abs(amount), instrument.min_size_step)
714
+ size_adj = instrument.round_size_down(abs(amount))
715
715
  if size_adj < instrument.min_size:
716
716
  raise ValueError(f"Attempt to trade size {abs(amount)} less than minimal allowed {instrument.min_size} !")
717
717
 
@@ -720,13 +720,13 @@ class StrategyContextImpl(StrategyContext):
720
720
  logger.debug(f"(StrategyContext) sending {type} {side} for {size_adj} of {instrument.symbol} ...")
721
721
  client_id = self._generate_order_client_id(instrument.symbol)
722
722
 
723
- if self.broker_provider.is_simulated_trading and optional.get("fill_at_price", False):
723
+ if self.broker_provider.is_simulated_trading and options.get("fill_at_price", False):
724
724
  # assume worst case, if we force execution and certain price, assume it's via market
725
725
  # TODO: add an additional flag besides price to indicate order type
726
726
  type = "market"
727
727
 
728
728
  order = self.trading_service.send_order(
729
- instrument, side, type, size_adj, price, time_in_force=time_in_force, client_id=client_id, **optional
729
+ instrument, side, type, size_adj, price, time_in_force=time_in_force, client_id=client_id, **options
730
730
  )
731
731
 
732
732
  return order
@@ -2,3 +2,5 @@ def recognize_time(time): ...
2
2
  def time_to_str(t: int, units: str = "ns") -> str: ...
3
3
  def time_delta_to_str(d: int) -> str: ...
4
4
  def recognize_timeframe(timeframe): ...
5
+ def prec_ceil(a: float, precision: int) -> float: ...
6
+ def prec_floor(a: float, precision: int) -> float: ...
@@ -51,4 +51,12 @@ cpdef recognize_timeframe(timeframe):
51
51
 
52
52
  else:
53
53
  raise ValueError(f'Unknown timeframe type: {timeframe} !')
54
- return tf
54
+ return tf
55
+
56
+
57
+ cpdef double prec_ceil(double a, int precision):
58
+ return np.sign(a) * np.true_divide(np.ceil(abs(a) * 10**precision), 10**precision)
59
+
60
+
61
+ cpdef double prec_floor(double a, int precision):
62
+ return np.sign(a) * np.true_divide(np.floor(abs(a) * 10**precision), 10**precision)
@@ -101,9 +101,12 @@ def ccxt_restore_position_from_deals(
101
101
  else:
102
102
  for d in _last_deals:
103
103
  pos.update_position_by_deal(d)
104
+ fees = 0.0
104
105
  if d.fee_amount is not None:
105
106
  if instr.base == d.fee_currency:
106
- pos.quantity -= d.fee_amount
107
+ fees += d.fee_amount
108
+ # - we round fees up in case of fees in base currency
109
+ pos.quantity -= pos.instrument.round_size_up(fees)
107
110
  return pos
108
111
 
109
112
 
@@ -4,7 +4,6 @@ import numpy as np
4
4
  from qubx import logger
5
5
  from qubx.core.basics import Position, Signal, TargetPosition
6
6
  from qubx.core.strategy import IPositionSizer, StrategyContext
7
- from qubx.utils.misc import round_down_at_min_qty
8
7
 
9
8
 
10
9
  class FixedSizer(IPositionSizer):
@@ -140,9 +139,7 @@ class WeightedPortfolioSizer(IPositionSizer):
140
139
  TargetPosition(
141
140
  ctx.time(),
142
141
  signal,
143
- round_down_at_min_qty(
144
- cap * max(signal.signal, 0) / sw / _q.mid_price(), signal.instrument.min_size_step
145
- ),
142
+ signal.instrument.round_size_down(cap * max(signal.signal, 0) / sw / _q.mid_price()),
146
143
  )
147
144
  )
148
145
  else:
@@ -339,10 +339,6 @@ def dequotify(sx: Union[str, List[str]], quote="USDT"):
339
339
  raise ValueError("Can't process input data !")
340
340
 
341
341
 
342
- def round_down_at_min_qty(x: float, min_size: float) -> float:
343
- return (int(x / min_size)) * min_size
344
-
345
-
346
342
  class ProgressParallel(joblib.Parallel):
347
343
  def __init__(self, *args, **kwargs):
348
344
  self.total = kwargs.pop("total", None)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes