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.
- {qubx-0.2.61 → qubx-0.2.62}/PKG-INFO +1 -1
- {qubx-0.2.61 → qubx-0.2.62}/pyproject.toml +1 -1
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/simulator.py +11 -6
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/basics.py +44 -9
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/context.py +5 -5
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/utils.pyi +2 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/utils.pyx +9 -1
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_utils.py +4 -1
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/sizers.py +1 -4
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/misc.py +0 -4
- {qubx-0.2.61 → qubx-0.2.62}/README.md +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/build.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/backtester/queue.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/account.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/helpers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/loggers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/lookups.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/metrics.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pxd +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pyi +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/series.pyx +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/core/strategy.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/data/helpers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/data/readers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_connector.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_customizations.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/impl/ccxt_trading.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/math/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/math/stats.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/runner.py +0 -0
- {qubx-0.2.61 → qubx-0.2.62}/src/qubx/utils/time.py +0 -0
|
@@ -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
|
-
**
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
**
|
|
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 =
|
|
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
|
|
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, **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|