Qubx 0.6.74__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.75__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.

@@ -89,7 +89,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
89
89
  for instrument in instruments:
90
90
  start = self._data_provider._time_msec_nbars_back(timeframe, nbarsback)
91
91
  ccxt_symbol = instrument_to_ccxt_symbol(instrument)
92
- ohlcv = await self._exchange_manager.exchange.fetch_ohlcv(ccxt_symbol, exch_timeframe, since=start, limit=nbarsback + 1)
92
+ ohlcv = await self._exchange_manager.exchange.fetch_ohlcv(
93
+ ccxt_symbol, exch_timeframe, since=start, limit=nbarsback + 1
94
+ )
93
95
 
94
96
  logger.debug(f"<yellow>{self._exchange_id}</yellow> {instrument}: loaded {len(ohlcv)} {timeframe} bars")
95
97
 
@@ -102,6 +104,12 @@ class OhlcDataHandler(BaseDataTypeHandler):
102
104
  )
103
105
  )
104
106
 
107
+ if len(ohlcv) > 0:
108
+ # Send a quote update to the context at the end of warmup
109
+ channel.send((instrument, DataType.QUOTE, self._convert_ohlcv_to_quote(ohlcv, instrument), False))
110
+
111
+ self._update_quote(instrument, ohlcv)
112
+
105
113
  async def get_historical_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
106
114
  """
107
115
  Get historical OHLC data for a single instrument (used by get_ohlc method).
@@ -121,7 +129,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
121
129
 
122
130
  # Retrieve OHLC data from exchange
123
131
  # TODO: check if nbarsback > max_limit (1000) we need to do more requests
124
- ohlcv_data = await self._exchange_manager.exchange.fetch_ohlcv(ccxt_symbol, exch_timeframe, since=since, limit=nbarsback + 1)
132
+ ohlcv_data = await self._exchange_manager.exchange.fetch_ohlcv(
133
+ ccxt_symbol, exch_timeframe, since=since, limit=nbarsback + 1
134
+ )
125
135
 
126
136
  # Convert to Bar objects using utility method
127
137
  bars = []
@@ -153,7 +163,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
153
163
 
154
164
  # ohlcv is symbol -> timeframe -> list[timestamp, open, high, low, close, volume]
155
165
  for exch_symbol, _data in ohlcv.items():
156
- instrument = ccxt_find_instrument(exch_symbol, self._exchange_manager.exchange, _symbol_to_instrument)
166
+ instrument = ccxt_find_instrument(
167
+ exch_symbol, self._exchange_manager.exchange, _symbol_to_instrument
168
+ )
157
169
  for _, ohlcvs in _data.items():
158
170
  for oh in ohlcvs:
159
171
  # Use private processing method to avoid duplication
@@ -261,7 +273,9 @@ class OhlcDataHandler(BaseDataTypeHandler):
261
273
  individual_subscribers[instrument] = create_individual_subscriber()
262
274
 
263
275
  # Create individual unsubscriber if exchange supports it
264
- if hasattr(self._exchange_manager.exchange, "un_watch_ohlcv") and callable(getattr(self._exchange_manager.exchange, "un_watch_ohlcv", None)):
276
+ if hasattr(self._exchange_manager.exchange, "un_watch_ohlcv") and callable(
277
+ getattr(self._exchange_manager.exchange, "un_watch_ohlcv", None)
278
+ ):
265
279
 
266
280
  def create_individual_unsubscriber(symbol=ccxt_symbol, exchange_id=self._exchange_id):
267
281
  async def individual_unsubscriber():
@@ -314,7 +328,7 @@ class OhlcDataHandler(BaseDataTypeHandler):
314
328
  # Use current time for health monitoring with robust conversion
315
329
  current_timestamp_ms = current_timestamp_ns // 1_000_000
316
330
  health_timestamp = pd.Timestamp(current_timestamp_ms, unit="ms").asm8
317
-
331
+
318
332
  # Notify all listeners
319
333
  self._data_provider.notify_data_arrival(sub_type, health_timestamp)
320
334
 
@@ -329,10 +343,19 @@ class OhlcDataHandler(BaseDataTypeHandler):
329
343
  # Use provided OHLCV data or fall back to current bar
330
344
  quote_data = ohlcv_data_for_quotes or [oh]
331
345
  if quote_data:
332
- _price = quote_data[-1][4] # Close price
333
- _s2 = instrument.tick_size / 2.0
334
- _bid, _ask = _price - _s2, _price + _s2
335
- self._data_provider._last_quotes[instrument] = Quote(current_timestamp_ns, _bid, _ask, 0.0, 0.0)
346
+ self._update_quote(instrument, quote_data)
347
+
348
+ def _update_quote(self, instrument: Instrument, quote_data: list):
349
+ quote = self._convert_ohlcv_to_quote(quote_data, instrument)
350
+ self._data_provider._last_quotes[instrument] = quote
351
+
352
+ def _convert_ohlcv_to_quote(self, oh: list, instrument: Instrument) -> Quote:
353
+ current_time = self._data_provider.time_provider.time()
354
+ current_timestamp_ns = current_time.astype("datetime64[ns]").view("int64")
355
+ _price = oh[-1][4] # Close price
356
+ _s2 = instrument.tick_size / 2.0
357
+ _bid, _ask = _price - _s2, _price + _s2
358
+ return Quote(current_timestamp_ns, _bid, _ask, 0.0, 0.0)
336
359
 
337
360
  def _convert_ohlcv_to_bar(self, oh: list) -> Bar:
338
361
  """
@@ -452,7 +452,7 @@ class CcxtDataReader(DataReader):
452
452
 
453
453
  # Use exchange-specific funding interval (lookup from cache or default)
454
454
  exchange_caps = self._capabilities.get(exchange.lower(), ReaderCapabilities())
455
- funding_interval_hours = self._get_funding_interval_for_symbol(
455
+ funding_interval_hours = self._get_funding_interval_hours_for_symbol(
456
456
  exchange.upper(), ccxt_symbol, exchange_caps.default_funding_interval_hours
457
457
  )
458
458
 
@@ -602,7 +602,7 @@ class CcxtDataReader(DataReader):
602
602
 
603
603
  # Use exchange-specific funding interval (lookup from cache or default)
604
604
  exchange_caps = self._capabilities.get(exchange.lower(), ReaderCapabilities())
605
- funding_interval_hours = self._get_funding_interval_for_symbol(
605
+ funding_interval_hours = self._get_funding_interval_hours_for_symbol(
606
606
  exchange.upper(), ccxt_symbol, exchange_caps.default_funding_interval_hours
607
607
  )
608
608
 
@@ -652,12 +652,17 @@ class CcxtDataReader(DataReader):
652
652
  self._funding_intervals_cache[exchange_name] = intervals
653
653
  return intervals
654
654
 
655
- def _get_funding_interval_for_symbol(self, exchange_name: str, ccxt_symbol: str, default_hours: float) -> float:
655
+ def _get_funding_interval_hours_for_symbol(
656
+ self, exchange_name: str, ccxt_symbol: str, default_hours: float
657
+ ) -> float:
656
658
  """
657
659
  Get funding interval for a specific symbol, with exchange-specific lookup and fallback.
658
660
  """
659
661
  intervals_dict = self._get_funding_intervals_for_exchange(exchange_name)
660
- return intervals_dict.get(ccxt_symbol, default_hours)
662
+ interval = intervals_dict.get(ccxt_symbol, default_hours)
663
+ if isinstance(interval, str):
664
+ return pd.Timedelta(interval).total_seconds() / 3600
665
+ return float(interval)
661
666
 
662
667
  def _get_column_names(self, data_type: str) -> list[str]:
663
668
  match data_type:
@@ -110,7 +110,19 @@ class MarketManager(IMarketManager):
110
110
 
111
111
  def quote(self, instrument: Instrument) -> Quote | None:
112
112
  _data_provider = self._get_data_provider(instrument.exchange)
113
- return _data_provider.get_quote(instrument)
113
+ quote = _data_provider.get_quote(instrument)
114
+ if quote is None:
115
+ ohlcv = self._cache.get_ohlcv(instrument)
116
+ if len(ohlcv) > 0:
117
+ last_bar = ohlcv[0]
118
+ quote = Quote(
119
+ last_bar.time,
120
+ last_bar.close - instrument.tick_size / 2,
121
+ last_bar.close + instrument.tick_size / 2,
122
+ 0,
123
+ 0,
124
+ )
125
+ return quote
114
126
 
115
127
  def get_data(self, instrument: Instrument, sub_type: str) -> list[Any]:
116
128
  return self._cache.get_data(instrument, sub_type)
@@ -393,13 +393,13 @@ class ProcessingManager(IProcessingManager):
393
393
  else:
394
394
  _init_signals.append(signal)
395
395
  self._instruments_in_init_stage.add(instr)
396
- logger.info(f"Switching tracker for <g>{instr}</g> to post-warmup initialization")
396
+ logger.debug(f"Switching tracker for <g>{instr}</g> to post-warmup initialization")
397
397
  else:
398
398
  _std_signals.append(signal)
399
399
  if instr in self._instruments_in_init_stage:
400
400
  _cancel_init_stage_instruments_tracker.add(instr)
401
401
  self._instruments_in_init_stage.remove(instr)
402
- logger.info(f"Switching tracker for <g>{instr}</g> back to defined position tracker")
402
+ logger.debug(f"Switching tracker for <g>{instr}</g> back to defined position tracker")
403
403
 
404
404
  return _std_signals, _init_signals, _cancel_init_stage_instruments_tracker
405
405
 
@@ -643,6 +643,8 @@ class ProcessingManager(IProcessingManager):
643
643
  # - update tracker
644
644
  _targets_from_tracker = self._get_tracker_for(instrument).update(self._context, instrument, _update)
645
645
 
646
+ # TODO: add gatherer update
647
+
646
648
  # - notify position gatherer for the new target positions
647
649
  if _targets_from_tracker:
648
650
  # - tracker generated new targets on update, notify position gatherer
qubx/ta/indicators.pxd CHANGED
@@ -157,3 +157,12 @@ cdef class Swings(IndicatorOHLC):
157
157
  cdef public TimeSeries middles, deltas
158
158
 
159
159
  cpdef double calculate(self, long long time, Bar bar, short new_item_started)
160
+
161
+ cdef class Pivots(IndicatorOHLC):
162
+ cdef int before, after
163
+ cdef object bars_buffer
164
+ cdef Bar current_bar
165
+ cdef long long current_bar_time
166
+ cdef public TimeSeries tops, bottoms, tops_detection_lag, bottoms_detection_lag
167
+
168
+ cpdef double calculate(self, long long time, Bar bar, short new_item_started)
qubx/ta/indicators.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import pandas as pd
1
2
  from qubx.core.series import OHLCV, Indicator, IndicatorOHLC, TimeSeries
2
3
 
3
4
  def sma(series: TimeSeries, period: int): ...
@@ -16,6 +17,7 @@ def psar(series: OHLCV, iaf: float = 0.02, maxaf: float = 0.2): ...
16
17
  def smooth(series: TimeSeries, smoother: str, *args, **kwargs) -> Indicator: ...
17
18
  def atr(series: OHLCV, period: int = 14, smoother="sma", percentage: bool = False): ...
18
19
  def swings(series: OHLCV, trend_indicator, **indicator_args) -> Indicator: ...
20
+ def pivots(series: OHLCV, before: int = 5, after: int = 5) -> Indicator: ...
19
21
 
20
22
  class Sma(Indicator):
21
23
  def __init__(self, name: str, series: TimeSeries, period: int): ...
@@ -45,3 +47,11 @@ class Swings(IndicatorOHLC):
45
47
  bottoms: TimeSeries
46
48
  middles: TimeSeries
47
49
  deltas: TimeSeries
50
+
51
+ class Pivots(IndicatorOHLC):
52
+ tops: TimeSeries
53
+ bottoms: TimeSeries
54
+ tops_detection_lag: TimeSeries
55
+ bottoms_detection_lag: TimeSeries
56
+ def __init__(self, name: str, series: OHLCV, before: int, after: int): ...
57
+ def pd(self) -> pd.DataFrame: ...
qubx/ta/indicators.pyx CHANGED
@@ -858,4 +858,156 @@ def swings(series: OHLCV, trend_indicator, **indicator_args):
858
858
  """
859
859
  if not isinstance(series, OHLCV):
860
860
  raise ValueError("Series must be OHLCV !")
861
- return Swings.wrap(series, trend_indicator, **indicator_args)
861
+ return Swings.wrap(series, trend_indicator, **indicator_args)
862
+
863
+
864
+ cdef class Pivots(IndicatorOHLC):
865
+ """
866
+ Pivot points detector that identifies local highs and lows using
867
+ lookback (before) and lookahead (after) windows.
868
+ """
869
+
870
+ def __init__(self, str name, OHLCV series, int before, int after):
871
+ self.before = before
872
+ self.after = after
873
+
874
+ # Deque to store completed bars for pivot detection
875
+ self.bars_buffer = deque(maxlen=before + after + 1)
876
+
877
+ # Keep track of the current unfinished bar separately
878
+ self.current_bar = None
879
+ self.current_bar_time = 0
880
+
881
+ # TimeSeries for pivot points
882
+ self.tops = TimeSeries("tops", series.timeframe, series.max_series_length)
883
+ self.bottoms = TimeSeries("bottoms", series.timeframe, series.max_series_length)
884
+ self.tops_detection_lag = TimeSeries("tops_lag", series.timeframe, series.max_series_length)
885
+ self.bottoms_detection_lag = TimeSeries("bottoms_lag", series.timeframe, series.max_series_length)
886
+
887
+ super().__init__(name, series)
888
+
889
+ cpdef double calculate(self, long long time, Bar bar, short new_item_started):
890
+ cdef int pivot_idx, i
891
+ cdef double pivot_high, pivot_low
892
+ cdef long long pivot_time
893
+ cdef short is_pivot_high, is_pivot_low
894
+
895
+ if new_item_started:
896
+ # If we have a previous bar that was being updated, add it to the buffer as completed
897
+ if self.current_bar is not None and self.current_bar_time > 0:
898
+ self.bars_buffer.append((self.current_bar_time, self.current_bar))
899
+
900
+ # Start tracking the new unfinished bar
901
+ self.current_bar = bar
902
+ self.current_bar_time = time
903
+
904
+ # Check if we have enough completed bars to detect a pivot
905
+ # We need exactly before + after + 1 bars in the buffer
906
+ if len(self.bars_buffer) < self.before + self.after + 1:
907
+ return np.nan
908
+
909
+ # The pivot candidate is at index 'before'
910
+ pivot_idx = self.before
911
+ pivot_time = self.bars_buffer[pivot_idx][0]
912
+ pivot_bar = self.bars_buffer[pivot_idx][1]
913
+ pivot_high = pivot_bar.high
914
+ pivot_low = pivot_bar.low
915
+
916
+ # Check for pivot high: pivot high must be > all other highs in window
917
+ is_pivot_high = 1
918
+ for i in range(len(self.bars_buffer)):
919
+ if i != pivot_idx:
920
+ if self.bars_buffer[i][1].high >= pivot_high:
921
+ is_pivot_high = 0
922
+ break
923
+
924
+ # Check for pivot low: pivot low must be < all other lows in window
925
+ is_pivot_low = 1
926
+ for i in range(len(self.bars_buffer)):
927
+ if i != pivot_idx:
928
+ if self.bars_buffer[i][1].low <= pivot_low:
929
+ is_pivot_low = 0
930
+ break
931
+
932
+ # Record pivot high if found
933
+ if is_pivot_high:
934
+ self.tops.update(pivot_time, pivot_high)
935
+ # Detection time is now (when we actually detect it)
936
+ self.tops_detection_lag.update(pivot_time, time - pivot_time)
937
+
938
+ # Record pivot low if found
939
+ if is_pivot_low:
940
+ self.bottoms.update(pivot_time, pivot_low)
941
+ # Detection time is now (when we actually detect it)
942
+ self.bottoms_detection_lag.update(pivot_time, time - pivot_time)
943
+
944
+ # Return 1 for pivot high, -1 for pivot low, 0 for both, nan for neither
945
+ if is_pivot_high and is_pivot_low:
946
+ return 0
947
+ elif is_pivot_high:
948
+ return 1
949
+ elif is_pivot_low:
950
+ return -1
951
+ else:
952
+ return np.nan
953
+ else:
954
+ # Just update the current unfinished bar
955
+ self.current_bar = bar
956
+ # Note: current_bar_time stays the same since we're updating the same bar
957
+ return np.nan
958
+
959
+ def pd(self) -> pd.DataFrame:
960
+ """
961
+ Return DataFrame with pivot points and detection lags.
962
+
963
+ Returns a multi-column DataFrame with:
964
+ - Tops: price, detection_lag, spotted (time when pivot was detected)
965
+ - Bottoms: price, detection_lag, spotted
966
+ """
967
+ from qubx.pandaz.utils import scols
968
+
969
+ tps = self.tops.pd()
970
+ bts = self.bottoms.pd()
971
+ tpl = self.tops_detection_lag.pd()
972
+ btl = self.bottoms_detection_lag.pd()
973
+
974
+ # Convert lags to timedeltas
975
+ if len(tpl) > 0:
976
+ tpl = tpl.apply(lambda x: pd.Timedelta(x, unit='ns'))
977
+ if len(btl) > 0:
978
+ btl = btl.apply(lambda x: pd.Timedelta(x, unit='ns'))
979
+
980
+ # Create DataFrames for tops and bottoms
981
+ if len(tps) > 0:
982
+ tops_df = pd.DataFrame({
983
+ 'price': tps,
984
+ 'detection_lag': tpl,
985
+ 'spotted': pd.Series(tps.index + tpl.values, index=tps.index)
986
+ })
987
+ else:
988
+ tops_df = pd.DataFrame(columns=['price', 'detection_lag', 'spotted'])
989
+
990
+ if len(bts) > 0:
991
+ bottoms_df = pd.DataFrame({
992
+ 'price': bts,
993
+ 'detection_lag': btl,
994
+ 'spotted': pd.Series(bts.index + btl.values, index=bts.index)
995
+ })
996
+ else:
997
+ bottoms_df = pd.DataFrame(columns=['price', 'detection_lag', 'spotted'])
998
+
999
+ return scols(tops_df, bottoms_df, keys=["Tops", "Bottoms"])
1000
+
1001
+
1002
+ def pivots(series: OHLCV, before: int = 5, after: int = 5):
1003
+ """
1004
+ Pivot points detector using lookback/lookahead windows.
1005
+
1006
+ :param series: OHLCV series
1007
+ :param before: Number of bars to look back
1008
+ :param after: Number of bars to look ahead
1009
+ :return: Pivots indicator with tops and bottoms
1010
+ """
1011
+ if not isinstance(series, OHLCV):
1012
+ raise ValueError("Series must be OHLCV!")
1013
+ return Pivots.wrap(series, before, after)
qubx/trackers/riskctrl.py CHANGED
@@ -989,7 +989,7 @@ class _InitializationStageTracker(GenericRiskControllerDecorator, IPositionSizer
989
989
  continue
990
990
 
991
991
  _current_pos = ctx.get_position(s.instrument).quantity
992
- logger.info(
992
+ logger.debug(
993
993
  f"[<y>{self.__class__.__name__}</y>] :: <y>Processing init signal</y> :: {s} :: Position is {_current_pos}"
994
994
  )
995
995
  _to_proceed.append(s)
@@ -1183,13 +1183,25 @@ def plot_trends(trends: pd.DataFrame | Struct, uc="w--", dc="m--", lw=2, ms=6, f
1183
1183
  raise ValueError("trends must be a DataFrame or Struct with 'trends' attribute")
1184
1184
 
1185
1185
 
1186
- def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel="Items",
1187
- figsize=(16, 10), positive_color='#2E8B57', negative_color='#DC143C',
1188
- value_formatter=None, label_transformer=None, show_values=True,
1189
- zero_line=True, positive_label=None, negative_label=None):
1186
+ def plot_quantiles(
1187
+ data,
1188
+ top_n=10,
1189
+ bottom_n=10,
1190
+ title=None,
1191
+ ylabel=None,
1192
+ xlabel="Items",
1193
+ figsize=(16, 10),
1194
+ positive_color="#2E8B57",
1195
+ negative_color="#DC143C",
1196
+ value_formatter=None,
1197
+ label_transformer=None,
1198
+ zero_line=True,
1199
+ positive_label=None,
1200
+ negative_label=None,
1201
+ ):
1190
1202
  """
1191
1203
  Plot top and bottom quantiles of data as a bar chart with customizable styling.
1192
-
1204
+
1193
1205
  Parameters:
1194
1206
  -----------
1195
1207
  data : pd.Series
@@ -1214,28 +1226,26 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
1214
1226
  Function to format values for display (e.g., lambda x: f'{x:.1f}%')
1215
1227
  label_transformer : callable, optional
1216
1228
  Function to transform index labels (e.g., lambda x: x.replace('USDC', ''))
1217
- show_values : bool, default True
1218
- Whether to show values on bars
1219
1229
  zero_line : bool, default True
1220
1230
  Whether to show horizontal line at zero
1221
1231
  positive_label : str, optional
1222
1232
  Legend label for positive values
1223
1233
  negative_label : str, optional
1224
1234
  Legend label for negative values
1225
-
1235
+
1226
1236
  Returns:
1227
1237
  --------
1228
1238
  fig, ax : matplotlib figure and axes objects
1229
-
1239
+
1230
1240
  Examples:
1231
1241
  ---------
1232
1242
  # Basic usage
1233
1243
  plot_quantiles(data_series)
1234
-
1244
+
1235
1245
  # Funding rates example
1236
- plot_quantiles(funding_rates,
1246
+ plot_quantiles(funding_rates,
1237
1247
  title="Annualized Funding Rates",
1238
- ylabel="Rate (%)",
1248
+ ylabel="Rate (%)",
1239
1249
  value_formatter=lambda x: f'{x:.1f}%',
1240
1250
  label_transformer=lambda x: x.replace('USDC', ''),
1241
1251
  positive_label="Longs pay Shorts",
@@ -1243,65 +1253,49 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
1243
1253
  """
1244
1254
  import pandas as pd
1245
1255
  from matplotlib.patches import Patch
1246
-
1256
+
1247
1257
  # Get top and bottom quantiles
1248
1258
  top_data = data.head(top_n)
1249
1259
  bottom_data = data.tail(bottom_n)
1250
-
1260
+
1251
1261
  # Combine them
1252
1262
  combined_data = pd.concat([top_data, bottom_data])
1253
-
1263
+
1254
1264
  # Create the plot
1255
1265
  fig, ax = plt.subplots(figsize=figsize)
1256
-
1266
+
1257
1267
  # Create colors: positive_color for positive, negative_color for negative
1258
1268
  colors = [positive_color if value >= 0 else negative_color for value in combined_data.values]
1259
-
1269
+
1260
1270
  # Create bar plot
1261
- bars = ax.bar(range(len(combined_data)), combined_data.values, color=colors, alpha=0.8,
1262
- edgecolor='white', linewidth=0.5)
1263
-
1271
+ bars = ax.bar(
1272
+ range(len(combined_data)), combined_data.values, color=colors, alpha=0.8, edgecolor="white", linewidth=0.5
1273
+ )
1274
+
1264
1275
  # Customize the plot
1265
1276
  if title:
1266
- ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
1277
+ ax.set_title(title, fontsize=16, fontweight="bold", pad=20)
1267
1278
  if ylabel:
1268
- ax.set_ylabel(ylabel, fontsize=12, fontweight='bold')
1269
- ax.set_xlabel(xlabel, fontsize=12, fontweight='bold')
1270
-
1279
+ ax.set_ylabel(ylabel, fontsize=12, fontweight="bold")
1280
+ ax.set_xlabel(xlabel, fontsize=12, fontweight="bold")
1281
+
1271
1282
  # Set x-axis labels
1272
1283
  if label_transformer:
1273
1284
  labels = [label_transformer(str(label)) for label in combined_data.index]
1274
1285
  else:
1275
1286
  labels = [str(label) for label in combined_data.index]
1276
-
1287
+
1277
1288
  ax.set_xticks(range(len(combined_data)))
1278
- ax.set_xticklabels(labels, rotation=45, ha='right')
1279
-
1289
+ ax.set_xticklabels(labels, rotation=45, ha="right")
1290
+
1280
1291
  # Add horizontal line at zero
1281
1292
  if zero_line:
1282
- ax.axhline(y=0, color='black', linestyle='-', alpha=0.4, linewidth=1.5)
1283
-
1293
+ ax.axhline(y=0, color="black", linestyle="-", alpha=0.4, linewidth=1.5)
1294
+
1284
1295
  # Add grid
1285
- ax.grid(True, alpha=0.25, linestyle=':', linewidth=0.8, color='#666666')
1296
+ ax.grid(True, alpha=0.25, linestyle=":", linewidth=0.8, color="#666666")
1286
1297
  ax.set_axisbelow(True)
1287
-
1288
- # Add value labels on bars
1289
- if show_values:
1290
- if value_formatter is None:
1291
- value_formatter = lambda x: f'{x:.1f}'
1292
-
1293
- for bar, value in zip(bars, combined_data.values):
1294
- height = bar.get_height()
1295
- label_offset = max(abs(height) * 0.02, 5) # Dynamic offset based on value magnitude
1296
-
1297
- ax.text(bar.get_x() + bar.get_width()/2.,
1298
- height + (label_offset if height >= 0 else -label_offset),
1299
- value_formatter(value),
1300
- ha='center',
1301
- va='bottom' if height >= 0 else 'top',
1302
- fontsize=10, fontweight='bold',
1303
- color='white' if abs(height) > max(abs(combined_data.min()), abs(combined_data.max())) * 0.3 else 'black')
1304
-
1298
+
1305
1299
  # Add legend if labels provided
1306
1300
  if positive_label or negative_label:
1307
1301
  legend_elements = []
@@ -1310,8 +1304,8 @@ def plot_quantiles(data, top_n=10, bottom_n=10, title=None, ylabel=None, xlabel=
1310
1304
  if negative_label:
1311
1305
  legend_elements.append(Patch(facecolor=negative_color, label=negative_label))
1312
1306
  if legend_elements:
1313
- ax.legend(handles=legend_elements, loc='upper right', fontsize=10, framealpha=0.9)
1314
-
1307
+ ax.legend(handles=legend_elements, loc="upper right", fontsize=10, framealpha=0.9)
1308
+
1315
1309
  plt.tight_layout()
1316
-
1310
+
1317
1311
  return fig, ax
qubx/utils/questdb.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Union
3
3
  import pandas as pd
4
4
  import psycopg as pg
5
5
  from psycopg.sql import SQL, Composed
6
+ from questdb.ingress import IngressError, Sender
6
7
 
7
8
 
8
9
  class QuestDBClient:
@@ -34,6 +35,17 @@ class QuestDBClient:
34
35
 
35
36
  self.conn_str = conn_str
36
37
 
38
+ @property
39
+ def http_connection_string(self) -> str:
40
+ """Get HTTP connection string for QuestDB ingress."""
41
+ # Extract host from conn_str
42
+ host = "localhost" # default
43
+ for part in self.conn_str.split():
44
+ if part.startswith("host="):
45
+ host = part.split("=", 1)[1]
46
+ break
47
+ return f"http::addr={host}:9000;"
48
+
37
49
  def query(self, query: str, params: Optional[Dict[str, Any]] = None) -> pd.DataFrame:
38
50
  """
39
51
  Execute a SQL query and return the results as a pandas DataFrame.
@@ -67,10 +79,61 @@ class QuestDBClient:
67
79
  """
68
80
  with pg.connect(self.conn_str) as conn:
69
81
  with conn.cursor() as cursor:
70
- cursor.execute(query, params)
82
+ cursor.execute(query, params) # type: ignore
71
83
  conn.commit()
72
84
  return cursor.rowcount
73
85
 
86
+ def insert_dataframe(self, df: pd.DataFrame, table_name: str) -> None:
87
+ """
88
+ Insert DataFrame into QuestDB table using the HTTP ingress API.
89
+
90
+ Args:
91
+ df: DataFrame to insert. Must have a timestamp index or 'timestamp' column
92
+ table_name: Name of the target QuestDB table
93
+
94
+ Raises:
95
+ IngressError: If ingestion fails
96
+ ValueError: If DataFrame doesn't have proper timestamp information
97
+ """
98
+ try:
99
+ with Sender.from_conf(self.http_connection_string) as sender:
100
+ # Check if DataFrame has a proper timestamp index
101
+ if isinstance(df.index, pd.DatetimeIndex) and df.index.name:
102
+ # Use the timestamp index directly
103
+ sender.dataframe(df.reset_index(), table_name=table_name, at=df.index.name)
104
+ elif "timestamp" in df.columns:
105
+ # If timestamp is a column, use it directly
106
+ sender.dataframe(df, table_name=table_name, at="timestamp")
107
+ else:
108
+ # Try to use index name if it exists
109
+ if df.index.name:
110
+ sender.dataframe(df.reset_index(), table_name=table_name, at=df.index.name)
111
+ else:
112
+ raise ValueError("DataFrame must have either a named timestamp index or a 'timestamp' column")
113
+ sender.flush()
114
+ except IngressError as e:
115
+ raise IngressError(f"Failed to insert DataFrame into {table_name}: {e}")
116
+
117
+ @staticmethod
118
+ def get_table_name(exchange: str, market: str, symbol: Optional[str], table_type: str) -> str:
119
+ """
120
+ Generate table name following the exchange.market.symbol.type pattern.
121
+
122
+ Args:
123
+ exchange: Exchange name (e.g., 'binance')
124
+ market: Market type (e.g., 'umfutures', 'spot')
125
+ symbol: Symbol name (e.g., 'btcusdt'), can be None for market-wide tables
126
+ table_type: Type of data (e.g., 'candle_1m', 'trade', 'orderbook')
127
+
128
+ Returns:
129
+ Formatted table name
130
+ """
131
+ parts = [exchange.lower(), market.lower()]
132
+ if symbol:
133
+ parts.append(symbol.lower())
134
+ parts.append(table_type.lower())
135
+ return ".".join(parts)
136
+
74
137
  def __enter__(self):
75
138
  """Context manager entry point."""
76
139
  return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.74
3
+ Version: 0.6.75
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -44,12 +44,12 @@ qubx/connectors/ccxt/handlers/base.py,sha256=1QS9uhFsisWvGwDmoAc83kLcSd1Lf3hSlnr
44
44
  qubx/connectors/ccxt/handlers/factory.py,sha256=SlMAOmaUy1dQ-q5ZzrjODj_XMcLr1BORGGZ7klLNndk,4250
45
45
  qubx/connectors/ccxt/handlers/funding_rate.py,sha256=OLlocxRBEMxQ7LAtxZgQcdn3LIZwAqxZ7Q69TUYNyT4,8918
46
46
  qubx/connectors/ccxt/handlers/liquidation.py,sha256=0QbLeZwpdEgd30PwQoWXUBKG25rrNQ0OgPz8fPLRA6U,3915
47
- qubx/connectors/ccxt/handlers/ohlc.py,sha256=bHAEAkjtJ9TLcevsrf5iYIZGB3Z-yKfW66E9IEBLRiE,16903
47
+ qubx/connectors/ccxt/handlers/ohlc.py,sha256=668mHEuLb-sT-Rk0miNsvKTETfoo0XK9aAAqNa4uTCY,17706
48
48
  qubx/connectors/ccxt/handlers/open_interest.py,sha256=K3gPI51zUtxvED3V-sfFmK1Lsa-vx7k-Q-UE_eLsoRM,9957
49
49
  qubx/connectors/ccxt/handlers/orderbook.py,sha256=VuRgIAshKoHhRrI9DKW7xuVnzsOTNvboz3y5QRkvcII,9147
50
50
  qubx/connectors/ccxt/handlers/quote.py,sha256=JwQ8mXMpFMdFEpQTx3x_Xaj6VHZanC6_JI6tB9l_yDw,4464
51
51
  qubx/connectors/ccxt/handlers/trade.py,sha256=VspCqw13r9SyvF0N3a31YKIVTzUx5IjFtMDaQeSxblM,4519
52
- qubx/connectors/ccxt/reader.py,sha256=htCfQQHD8oP7U8248L9f0iD7RxL5wWX-NkLXYUm7H0k,26432
52
+ qubx/connectors/ccxt/reader.py,sha256=uUG1I_ejzTf0f4bCAHpLhBzTUqtNX-JvJGFA4bi7-WU,26602
53
53
  qubx/connectors/ccxt/subscription_config.py,sha256=jbMZ_9US3nvrp6LCVmMXLQnAjXH0xIltzUSPqXJZvgs,3865
54
54
  qubx/connectors/ccxt/subscription_manager.py,sha256=ZGWf-j4ZTJm-flhVvCs2kLRGqt0zci3p4PSf4nvTKzI,12662
55
55
  qubx/connectors/ccxt/subscription_orchestrator.py,sha256=CbZMTRhmgcJZd8cofQbyBDI__N2Lbo1loYfh9_-EkFA,16512
@@ -71,18 +71,18 @@ qubx/core/loggers.py,sha256=pa28UYLTfRibhDzcbtPfbtNb3jpMZ8catTMikA0RFlc,14268
71
71
  qubx/core/lookups.py,sha256=Bed30kPZvbTGjZ8exojhIMOIVfB46j6741yF3fXGTiM,18313
72
72
  qubx/core/metrics.py,sha256=eMsg9qaL86pkKVKrQOyF9u7yf27feLRxOvJj-6qwmj8,75551
73
73
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
74
- qubx/core/mixins/market.py,sha256=-_C2AlpgTMIknVA-AzhQWVS5jOpAvcKcZSJFlJPEAEo,5931
75
- qubx/core/mixins/processing.py,sha256=rbso204IcaRmOxqL5Gfqj57K6yxnhBTK3MB4RNMERds,42169
74
+ qubx/core/mixins/market.py,sha256=w9OEDfy0r9xnb4KdKA-PuFsCQugNo4pMLQivGFeyqGw,6356
75
+ qubx/core/mixins/processing.py,sha256=OgmrQfnUQKfHJ77BYrXQIu4AOz3xGlnuP8H6UvCiWhA,42216
76
76
  qubx/core/mixins/subscription.py,sha256=GcZKHHzjPyYFLAEpE7j4fpLDOlAhFKojQEYfFO3QarY,11064
77
77
  qubx/core/mixins/trading.py,sha256=zq3JO8UDwbkYgmTHy1W8Ccn9Adeo1xw3vnyFAUFU0R4,9047
78
78
  qubx/core/mixins/universe.py,sha256=mzZJA7Me6HNFbAMGg1XOpnYCMtcFKHESTiozjaXyKXY,10100
79
79
  qubx/core/mixins/utils.py,sha256=P71cLuqKjId8989MwOL_BtvvCnnwOFMkZyB1SY-0Ork,147
80
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=GLf8S0xWchTbcCyaVFEdY12acsqX9IgqLSCLgaIU6zk,1019592
80
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=HuFJpTNo4QYAcj9MIJlKnMR86_MPC5vq0t9JfqWZRE4,1019592
81
81
  qubx/core/series.pxd,sha256=PvnUEupOsZg8u81U5Amd-nbfmWQ0-PwZwc7yUoaZpoQ,4739
82
82
  qubx/core/series.pyi,sha256=RkM-F3AyrzT7m1H2UmOvZmmcOzU2eBeEWf2c0GUZe2o,5437
83
83
  qubx/core/series.pyx,sha256=wAn7L9HIkvVl-1Tt7bgdWhec7xy4AiHSXyDsrA4a29U,51703
84
84
  qubx/core/stale_data_detector.py,sha256=NHnnG9NkcivC93n8QMwJUzFVQv2ziUaN-fg76ppng_c,17118
85
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=5QMCfncQEUHIHa_h1qqzN4Vd_evnrTlSNbECmtzk3Qc,86568
85
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=wdcP158YeC3iUevMSdtdslbkl-02EBluUHgwFkUVOP4,86568
86
86
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
87
87
  qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
88
88
  qubx/data/__init__.py,sha256=BlyZ99esHLDmFA6ktNkuIce9RZO99TA1IMKWe94aI8M,599
@@ -156,10 +156,10 @@ qubx/restorers/signal.py,sha256=7n7eeRhWGUBPbg179GxFH_ifywcl3pQJbwrcDklw0N0,1460
156
156
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
157
157
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
158
158
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=cxYoV2RdBpqvPJYdK68Mqfbqtji2SXB7uf5mLec1ulY,704392
160
- qubx/ta/indicators.pxd,sha256=oEBJ-S6JTgBqEC81edGlf2uNyGJliVgCEHX5NzlQIV0,4521
161
- qubx/ta/indicators.pyi,sha256=j_XArS3e99fIzKiaKhXYg9OLf7syWS3lUF8oUbzsWtU,2268
162
- qubx/ta/indicators.pyx,sha256=7WhkkSik1ww4k_WvRoblZ4FUYc-jtyMDJbsd9TnsDOQ,29067
159
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=CQ1-s8YDcEcUupwMyLxtEmdTI1nFxJtCgzZIoFHKBbs,762760
160
+ qubx/ta/indicators.pxd,sha256=l4JgvNdlWBuLqMzqTZVaYYn4XyZ9-c222YCyBXVv8MA,4843
161
+ qubx/ta/indicators.pyi,sha256=kHoHONhzI7aq0qs-wP5cxyDPj96ZvQLlThEC8yQj6U4,2630
162
+ qubx/ta/indicators.pyx,sha256=rT6OJ7xygZdTF4-pT6vr6g9MRhvbi6nYBlkTzzZYA_U,35126
163
163
  qubx/templates/__init__.py,sha256=L2U6tX5DGhtIDRarIo3tHT7ZVQKNJMlpWn_sinJPevk,153
164
164
  qubx/templates/base.py,sha256=cY9Lnvs8hvuqyyBwMN-p7rXWGETrbAx0zmE6Px19Ym4,6844
165
165
  qubx/templates/project/accounts.toml.j2,sha256=LlaYWqOalMimGf7n_4sIzJk9p28tNbhZFogg3SHplSQ,637
@@ -181,12 +181,12 @@ qubx/trackers/__init__.py,sha256=zvIahF8MwSffBMOX2BDFFNKJCWtL8TyFLQLL_DR22JM,106
181
181
  qubx/trackers/advanced.py,sha256=Q9r8g2nfx5PU3JOY2Mpyes0UDFLf078ACN89x-vB3mM,14325
182
182
  qubx/trackers/composite.py,sha256=Tjupx78SraXmRKkWhu8n81RkPjOgsDbXLd8yz6PhbaA,6318
183
183
  qubx/trackers/rebalancers.py,sha256=KFY7xuD4fGALiSLMas8MZ3ueRzlWt5wDT9329dlmNng,5150
184
- qubx/trackers/riskctrl.py,sha256=wLeAI5v8Z6ZZrWneQdpj4QPoOMcVyYE0UyFPEsPmrPw,52343
184
+ qubx/trackers/riskctrl.py,sha256=xI6x1QO6ZEHBkBD1fOhOQuqTeK091fwYaT8eelUp_dg,52344
185
185
  qubx/trackers/sizers.py,sha256=j1C7YRvTf_qT3kstpeHiagMHZESFqRWFTgce4rZdy88,11841
186
186
  qubx/utils/__init__.py,sha256=FEPBtU3dhfLawBkAfm9FEUW4RuOY7pGCBfzDCtKjn9A,481
187
187
  qubx/utils/_pyxreloader.py,sha256=34kNd8kQi2ey_ZrGdVVUHbPrO1PEiHZDLEDBscIkT_s,12292
188
188
  qubx/utils/charting/lookinglass.py,sha256=PdIFQ6HrjmurIq8ZOOkpPtYsjQjWxGEkrwTpW060-Ng,41848
189
- qubx/utils/charting/mpl_helpers.py,sha256=4uPSmKs6gypFxT6L6gHzpFKfudrtTB_M3JUilkAGZjg,41127
189
+ qubx/utils/charting/mpl_helpers.py,sha256=oG9aaz1jFkRHk2g-jiaLlto6mLjhFtq3KTyDUEdMGow,40147
190
190
  qubx/utils/charting/orderbook.py,sha256=NmeXxru3CUiKLtl1DzCBbQgSdL4qmTDIxV0u81p2tTw,11083
191
191
  qubx/utils/collections.py,sha256=go2sH_q2TlXqI3Vxq8GHLfNGlYL4JwS3X1lwJWbpFLE,7425
192
192
  qubx/utils/marketdata/binance.py,sha256=hWX3noZj704JIMBqlwsXA5IzUP7EgiLiC2r12dJAKQA,11565
@@ -202,7 +202,7 @@ qubx/utils/plotting/data.py,sha256=ZOg8rHAq4NVmfsyhvzFHtey4HaXywAHufxhv1IExRqg,4
202
202
  qubx/utils/plotting/interfaces.py,sha256=mtRcoWIMt2xkv-Tc2ZgXZQpr5HRiWhPcqkIslzZTeig,493
203
203
  qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
204
204
  qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
- qubx/utils/questdb.py,sha256=TdjmlGPoZXdjidZ_evcBIkFtoL4nGQXPR4IQSUc6IvA,2509
205
+ qubx/utils/questdb.py,sha256=bxlWiCyYf8IspsvXrs58tn5iXYBUtv6ojeYwOj8EXI0,5269
206
206
  qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
207
207
  qubx/utils/runner/_jupyter_runner.pyt,sha256=1bo06ql_wlZ7ng6go_zvemySzngrM8Uqzj-_xeOdiFg,10030
208
208
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
@@ -211,8 +211,8 @@ qubx/utils/runner/factory.py,sha256=hmtUDYNFQwVQffHEfxgrlmKwOGLcFQ6uJIH_ZLscpIY,
211
211
  qubx/utils/runner/runner.py,sha256=T2V6KSKcLNQVXYjxw4zrF7n_AhKEuHJBXjFGU0u1DQY,33252
212
212
  qubx/utils/time.py,sha256=xOWl_F6dOLFCmbB4xccLIx5yVt5HOH-I8ZcuowXjtBQ,11797
213
213
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
214
- qubx-0.6.74.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
215
- qubx-0.6.74.dist-info/METADATA,sha256=mIF4kPt9lE1tHHASINkmBPxzXdyDN_Tu4OU6PUtvWD8,5836
216
- qubx-0.6.74.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
217
- qubx-0.6.74.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
- qubx-0.6.74.dist-info/RECORD,,
214
+ qubx-0.6.75.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
215
+ qubx-0.6.75.dist-info/METADATA,sha256=EtZt5Ge9ebKLsxiBUS0xT2bWr4Fpr4dCSaI9nnepZdU,5836
216
+ qubx-0.6.75.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
217
+ qubx-0.6.75.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
+ qubx-0.6.75.dist-info/RECORD,,
File without changes
File without changes