Qubx 0.6.29__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.31__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/ome.py CHANGED
@@ -105,11 +105,13 @@ class OrdersManagementEngine:
105
105
  if mdata.time < self._last_data_update_time_ns:
106
106
  return _exec_report
107
107
  self._last_data_update_time_ns = mdata.time
108
+ _mkt_state = "UNKNOWN"
108
109
 
109
110
  # - new quote
110
111
  if isinstance(mdata, Quote):
111
112
  _b, _a = mdata.bid, mdata.ask
112
113
  _bs, _as = _b, _a
114
+ _mkt_state = "Q: " + str(mdata)
113
115
 
114
116
  # - update BBO by new quote
115
117
  self.__prev_bbo = self.bbo
@@ -122,16 +124,19 @@ class OrdersManagementEngine:
122
124
  _b = max_buy_price - self._tick_size
123
125
  _a = min_sell_price + self._tick_size
124
126
  _bs, _as = _a, _b
127
+ _mkt_state = "TA: " + str(mdata)
125
128
 
126
129
  # - single trade
127
130
  elif isinstance(mdata, Trade):
128
131
  _b, _a = mdata.price - self._tick_size, mdata.price + self._tick_size
129
132
  _bs, _as = _b, _a
133
+ _mkt_state = "T: " + str(mdata)
130
134
 
131
135
  # - order book
132
136
  elif isinstance(mdata, OrderBook):
133
137
  _b, _a = mdata.top_bid, mdata.top_ask
134
138
  _bs, _as = _b, _a
139
+ _mkt_state = "OB: " + str(mdata)
135
140
 
136
141
  else:
137
142
  raise SimulationError(f"Invalid market data type: {type(mdata)} for update OME({self.instrument.symbol})")
@@ -142,7 +147,7 @@ class OrdersManagementEngine:
142
147
  for level in _asks_to_execute:
143
148
  for order_id in self.asks[level]:
144
149
  order = self.active_orders.pop(order_id)
145
- _exec_report.append(self._execute_order(timestamp, order.price, order, False))
150
+ _exec_report.append(self._execute_order(timestamp, order.price, order, False, _mkt_state))
146
151
  self.asks.pop(level)
147
152
 
148
153
  # - when new quote ask is lower than the highest bid order execute all affected orders
@@ -151,7 +156,7 @@ class OrdersManagementEngine:
151
156
  for level in _bids_to_execute:
152
157
  for order_id in self.bids[level]:
153
158
  order = self.active_orders.pop(order_id)
154
- _exec_report.append(self._execute_order(timestamp, order.price, order, False))
159
+ _exec_report.append(self._execute_order(timestamp, order.price, order, False, _mkt_state))
155
160
  self.bids.pop(level)
156
161
 
157
162
  # - processing stop orders
@@ -162,12 +167,12 @@ class OrdersManagementEngine:
162
167
  if so.side == "BUY" and _as >= so.price:
163
168
  _exec_price = _as if not _emulate_price_exec else so.price
164
169
  self.stop_orders.pop(soid)
165
- _exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
170
+ _exec_report.append(self._execute_order(timestamp, _exec_price, so, True, _mkt_state))
166
171
 
167
172
  elif so.side == "SELL" and _bs <= so.price:
168
173
  _exec_price = _bs if not _emulate_price_exec else so.price
169
174
  self.stop_orders.pop(soid)
170
- _exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
175
+ _exec_report.append(self._execute_order(timestamp, _exec_price, so, True, _mkt_state))
171
176
 
172
177
  self._last_update_time = timestamp
173
178
  return _exec_report
@@ -268,7 +273,7 @@ class OrdersManagementEngine:
268
273
 
269
274
  # - if order must be "executed" immediately
270
275
  if _exec_price is not None:
271
- return self._execute_order(timestamp, _exec_price, order, True)
276
+ return self._execute_order(timestamp, _exec_price, order, True, "BBO: " + str(self.bbo))
272
277
 
273
278
  # - processing limit orders
274
279
  if _need_update_book:
@@ -284,10 +289,12 @@ class OrdersManagementEngine:
284
289
  return SimulatedExecutionReport(self.instrument, timestamp, order, None)
285
290
 
286
291
  def _execute_order(
287
- self, timestamp: dt_64, exec_price: float, order: Order, taker: bool
292
+ self, timestamp: dt_64, exec_price: float, order: Order, taker: bool, market_state: str
288
293
  ) -> SimulatedExecutionReport:
289
294
  order.status = "CLOSED"
290
- self._dbg(f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price}")
295
+ self._dbg(
296
+ f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price} ::: {market_state}"
297
+ )
291
298
  return SimulatedExecutionReport(
292
299
  self.instrument,
293
300
  timestamp,
@@ -616,9 +616,10 @@ class CcxtDataProvider(IDataProvider):
616
616
  for exch_symbol, ccxt_ticker in ccxt_tickers.items(): # type: ignore
617
617
  instrument = ccxt_find_instrument(exch_symbol, self._exchange, _symbol_to_instrument)
618
618
  quote = ccxt_convert_ticker(ccxt_ticker)
619
- self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
620
- self._last_quotes[instrument] = quote
621
- channel.send((instrument, sub_type, quote, False))
619
+ if self._last_quotes[instrument] is None or quote.time > self._last_quotes[instrument].time:
620
+ self._health_monitor.record_data_arrival(sub_type, dt_64(quote.time, "ns"))
621
+ self._last_quotes[instrument] = quote
622
+ channel.send((instrument, sub_type, quote, False))
622
623
 
623
624
  async def un_watch_quote(instruments: list[Instrument]):
624
625
  symbols = [_instr_to_ccxt_symbol[i] for i in instruments]
@@ -8,6 +8,8 @@ import ccxt.pro as cxp
8
8
 
9
9
  from .binance.broker import BinanceCcxtBroker
10
10
  from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
11
+ from .bitfinex.bitfinex import BitfinexF
12
+ from .bitfinex.bitfinex_account import BitfinexAccountProcessor
11
13
  from .kraken.kraken import CustomKrakenFutures
12
14
 
13
15
  EXCHANGE_ALIASES = {
@@ -17,7 +19,7 @@ EXCHANGE_ALIASES = {
17
19
  "binance.pm": "binancepm",
18
20
  "kraken.f": "custom_krakenfutures",
19
21
  "binance.um.mm": "binance_um_mm",
20
- "bitfinex.f": "bitfinex",
22
+ "bitfinex.f": "bitfinex_f",
21
23
  }
22
24
 
23
25
  CUSTOM_BROKERS = {
@@ -27,11 +29,16 @@ CUSTOM_BROKERS = {
27
29
  "binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
28
30
  }
29
31
 
32
+ CUSTOM_ACCOUNTS = {
33
+ "bitfinex.f": BitfinexAccountProcessor,
34
+ }
35
+
30
36
  cxp.binanceqv = BinanceQV # type: ignore
31
37
  cxp.binanceqv_usdm = BinanceQVUSDM # type: ignore
32
38
  cxp.binancepm = BinancePortfolioMargin # type: ignore
33
39
  cxp.binance_um_mm = BINANCE_UM_MM # type: ignore
34
40
  cxp.custom_krakenfutures = CustomKrakenFutures # type: ignore
41
+ cxp.bitfinex_f = BitfinexF # type: ignore
35
42
 
36
43
  cxp.exchanges.append("binanceqv")
37
44
  cxp.exchanges.append("binanceqv_usdm")
@@ -39,3 +46,4 @@ cxp.exchanges.append("binancepm")
39
46
  cxp.exchanges.append("binancepm_usdm")
40
47
  cxp.exchanges.append("binance_um_mm")
41
48
  cxp.exchanges.append("custom_krakenfutures")
49
+ cxp.exchanges.append("bitfinex_f")
@@ -202,7 +202,12 @@ class BinanceQV(cxp.binance):
202
202
  executionType = self.safe_string(message, "X")
203
203
  if executionType == "INSURANCE_FUND":
204
204
  return
205
+
206
+ # - fix 2025-04-16: filter out trades with zero price
205
207
  trade = self.parse_ws_trade(message, market)
208
+ if trade["price"] == 0.0:
209
+ return
210
+
206
211
  tradesArray = self.safe_value(self.trades, symbol)
207
212
  if tradesArray is None:
208
213
  limit = self.safe_integer(self.options, "tradesLimit", 1000)
@@ -8,16 +8,19 @@ from ccxt.base.precise import Precise
8
8
  from ccxt.base.types import (
9
9
  Any,
10
10
  Balances,
11
+ Int,
12
+ Market,
11
13
  Num,
12
14
  Order,
13
15
  OrderSide,
14
16
  OrderType,
17
+ Str,
15
18
  Strings,
16
19
  Tickers,
17
20
  )
18
21
 
19
22
 
20
- class MyBitfinex(cxp.bitfinex):
23
+ class BitfinexF(cxp.bitfinex):
21
24
  """
22
25
  Extended binance exchange to provide quote asset volumes support
23
26
  """
@@ -41,3 +44,126 @@ class MyBitfinex(cxp.bitfinex):
41
44
  def un_watch_bids_asks(self, symbol: str, params: dict = {}):
42
45
  if hasattr(self, "un_watch_order_book"):
43
46
  return self.un_watch_order_book(symbol, params)
47
+
48
+ async def fetch_balance(self, params={}) -> Balances:
49
+ params["type"] = "margin"
50
+ return await super().fetch_balance(params)
51
+
52
+ async def create_order(
53
+ self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}
54
+ ):
55
+ params.pop("type", None)
56
+ if "timeInForce" in params and params["timeInForce"] == "GTX":
57
+ # GTX is not supported by bitfinex, so we need to convert it to PO
58
+ params["timeInForce"] = "PO"
59
+ params["postOnly"] = True
60
+ response = await super().create_order(symbol, type, side, amount, price, params)
61
+ return response
62
+
63
+ async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
64
+ response = await super().watch_orders(symbol, since, limit, params)
65
+ print(response)
66
+ return response
67
+
68
+ def parse_ws_order_status(self, status):
69
+ print(status)
70
+ statuses: dict = {
71
+ "ACTIVE": "open",
72
+ "CANCELED": "canceled",
73
+ "EXECUTED": "closed",
74
+ "PARTIALLY": "open",
75
+ "POSTONLY": "canceled", # this status means that a postonly order got canceled on submission
76
+ }
77
+ return self.safe_string(statuses, status, status)
78
+
79
+ def parse_order_status(self, status: Str):
80
+ if status is None:
81
+ return status
82
+ parts = status.split(" ")
83
+ state = self.safe_string(parts, 0)
84
+ statuses: dict = {
85
+ "ACTIVE": "open",
86
+ "PARTIALLY": "open",
87
+ "EXECUTED": "closed",
88
+ "CANCELED": "canceled",
89
+ "INSUFFICIENT": "canceled",
90
+ "POSTONLY CANCELED": "canceled",
91
+ "POSTONLY": "canceled",
92
+ "RSN_DUST": "rejected",
93
+ "RSN_PAUSE": "rejected",
94
+ "IOC CANCELED": "canceled",
95
+ "FILLORKILL CANCELED": "canceled",
96
+ }
97
+ return self.safe_string(statuses, state, status)
98
+
99
+ def parse_position(self, position: dict, market: Market = None):
100
+ #
101
+ # [
102
+ # "tBTCUSD", # SYMBOL
103
+ # "ACTIVE", # STATUS
104
+ # 0.0195, # AMOUNT
105
+ # 8565.0267019, # BASE_PRICE
106
+ # 0, # MARGIN_FUNDING
107
+ # 0, # MARGIN_FUNDING_TYPE
108
+ # -0.33455568705000516, # PL
109
+ # -0.0003117550117425625, # PL_PERC
110
+ # 7045.876419249083, # PRICE_LIQ
111
+ # 3.0673001895895604, # LEVERAGE
112
+ # null, # _PLACEHOLDER
113
+ # 142355652, # POSITION_ID
114
+ # 1574002216000, # MTS_CREATE
115
+ # 1574002216000, # MTS_UPDATE
116
+ # null, # _PLACEHOLDER
117
+ # 0, # TYPE
118
+ # null, # _PLACEHOLDER
119
+ # 0, # COLLATERAL
120
+ # 0, # COLLATERAL_MIN
121
+ # # META
122
+ # {
123
+ # "reason": "TRADE",
124
+ # "order_id": 34271018124,
125
+ # "liq_stage": null,
126
+ # "trade_price": "8565.0267019",
127
+ # "trade_amount": "0.0195",
128
+ # "order_id_oppo": 34277498022
129
+ # }
130
+ # ]
131
+ #
132
+ positionList = self.safe_list(position, "result")
133
+ marketId = self.safe_string(positionList, 0)
134
+ amount = self.safe_string(positionList, 2)
135
+ timestamp = self.safe_integer(positionList, 12)
136
+ meta = self.safe_string(positionList, 19)
137
+ tradePrice = self.safe_string(meta, "trade_price")
138
+ tradeAmount = self.safe_string(meta, "trade_amount")
139
+ return self.safe_position(
140
+ {
141
+ "info": positionList,
142
+ "id": self.safe_string(positionList, 11),
143
+ "symbol": self.safe_symbol(marketId, market),
144
+ "notional": None,
145
+ "marginMode": "isolated", # derivatives use isolated, margin uses cross, https://support.bitfinex.com/hc/en-us/articles/360035475374-Derivatives-Trading-on-Bitfinex
146
+ "liquidationPrice": self.safe_number(positionList, 8),
147
+ "entryPrice": self.safe_number(positionList, 3),
148
+ "unrealizedPnl": self.safe_number(positionList, 6),
149
+ "percentage": self.safe_number(positionList, 7),
150
+ "contracts": self.parse_number(amount),
151
+ "contractSize": None,
152
+ "markPrice": None,
153
+ "lastPrice": None,
154
+ "side": "long" if Precise.string_gt(amount, "0") else "short",
155
+ "hedged": None,
156
+ "timestamp": timestamp,
157
+ "datetime": self.iso8601(timestamp),
158
+ "lastUpdateTimestamp": self.safe_integer(positionList, 13),
159
+ "maintenanceMargin": self.safe_number(positionList, 18),
160
+ "maintenanceMarginPercentage": None,
161
+ "collateral": self.safe_number(positionList, 17),
162
+ "initialMargin": self.parse_number(Precise.string_mul(tradeAmount, tradePrice)),
163
+ "initialMarginPercentage": None,
164
+ "leverage": self.safe_number(positionList, 9),
165
+ "marginRatio": None,
166
+ "stopLossPrice": None,
167
+ "takeProfitPrice": None,
168
+ }
169
+ )
@@ -0,0 +1,45 @@
1
+ import asyncio
2
+
3
+ from qubx.connectors.ccxt.account import CcxtAccountProcessor
4
+ from qubx.connectors.ccxt.utils import (
5
+ ccxt_convert_order_info,
6
+ ccxt_extract_deals_from_exec,
7
+ ccxt_find_instrument,
8
+ )
9
+ from qubx.core.basics import CtrlChannel
10
+
11
+
12
+ class BitfinexAccountProcessor(CcxtAccountProcessor):
13
+ async def _subscribe_executions(self, name: str, channel: CtrlChannel):
14
+ _symbol_to_instrument = {}
15
+
16
+ async def _watch_orders():
17
+ orders = await self.exchange.watch_orders()
18
+ for order in orders:
19
+ instrument = ccxt_find_instrument(order["symbol"], self.exchange, _symbol_to_instrument)
20
+ order = ccxt_convert_order_info(instrument, order)
21
+ channel.send((instrument, "order", order, False))
22
+
23
+ async def _watch_my_trades():
24
+ trades = await self.exchange.watch_my_trades()
25
+ for trade in trades: # type: ignore
26
+ instrument = ccxt_find_instrument(trade["symbol"], self.exchange, _symbol_to_instrument)
27
+ print(trade)
28
+ deals = ccxt_extract_deals_from_exec({"trades": [trade]})
29
+ print(deals)
30
+ channel.send((instrument, "deals", deals, False))
31
+
32
+ await asyncio.gather(
33
+ self._listen_to_stream(
34
+ subscriber=_watch_orders,
35
+ exchange=self.exchange,
36
+ channel=channel,
37
+ name=name,
38
+ ),
39
+ self._listen_to_stream(
40
+ subscriber=_watch_my_trades,
41
+ exchange=self.exchange,
42
+ channel=channel,
43
+ name=name,
44
+ ),
45
+ )
@@ -7,7 +7,8 @@ from qubx.connectors.ccxt.broker import CcxtBroker
7
7
  from qubx.core.basics import CtrlChannel
8
8
  from qubx.core.interfaces import IAccountProcessor, IBroker, IDataProvider, ITimeProvider
9
9
 
10
- from .exchanges import CUSTOM_BROKERS, EXCHANGE_ALIASES
10
+ from .account import CcxtAccountProcessor
11
+ from .exchanges import CUSTOM_ACCOUNTS, CUSTOM_BROKERS, EXCHANGE_ALIASES
11
12
 
12
13
 
13
14
  def get_ccxt_exchange(
@@ -69,10 +70,18 @@ def get_ccxt_broker(
69
70
  data_provider: IDataProvider,
70
71
  **kwargs,
71
72
  ) -> IBroker:
72
- broker_cls = CUSTOM_BROKERS.get(exchange_name, CcxtBroker)
73
+ broker_cls = CUSTOM_BROKERS.get(exchange_name.lower(), CcxtBroker)
73
74
  return broker_cls(exchange, channel, time_provider, account, data_provider, **kwargs)
74
75
 
75
76
 
77
+ def get_ccxt_account(
78
+ exchange_name: str,
79
+ **kwargs,
80
+ ) -> IAccountProcessor:
81
+ account_cls = CUSTOM_ACCOUNTS.get(exchange_name.lower(), CcxtAccountProcessor)
82
+ return account_cls(**kwargs)
83
+
84
+
76
85
  def _get_api_credentials(
77
86
  api_key: str | None, secret: str | None, kwargs: dict[str, Any]
78
87
  ) -> tuple[str | None, str | None]:
@@ -38,6 +38,9 @@ def ccxt_convert_order_info(instrument: Instrument, raw: dict[str, Any]) -> Orde
38
38
  Convert CCXT excution record to Order object
39
39
  """
40
40
  ri = raw["info"]
41
+ if isinstance(ri, list):
42
+ # we don't handle case when order info is a list
43
+ ri = {}
41
44
  amnt = float(ri.get("origQty", raw.get("amount")))
42
45
  price = raw["price"]
43
46
  status = raw["status"]
@@ -157,7 +160,8 @@ def ccxt_convert_positions(
157
160
  quantity=info["contracts"] * (-1 if info["side"] == "short" else 1),
158
161
  pos_average_price=info["entryPrice"],
159
162
  )
160
- pos.update_market_price(pd.Timestamp(info["timestamp"], unit="ms").asm8, info["markPrice"], 1)
163
+ if info.get("markPrice", None) is not None:
164
+ pos.update_market_price(pd.Timestamp(info["timestamp"], unit="ms").asm8, info["markPrice"], 1)
161
165
  positions.append(pos)
162
166
  return positions
163
167
 
qubx/core/context.py CHANGED
@@ -430,8 +430,8 @@ class StrategyContext(IStrategyContext):
430
430
  def close_positions(self, market_type: MarketType | None = None) -> None:
431
431
  return self._trading_manager.close_positions(market_type)
432
432
 
433
- def cancel_order(self, order_id: str) -> None:
434
- return self._trading_manager.cancel_order(order_id)
433
+ def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
434
+ return self._trading_manager.cancel_order(order_id, exchange)
435
435
 
436
436
  def cancel_orders(self, instrument: Instrument):
437
437
  return self._trading_manager.cancel_orders(instrument)
qubx/core/helpers.py CHANGED
@@ -202,6 +202,8 @@ class CachedMarketDataHolder:
202
202
  series = self._ohlcvs.get(instrument)
203
203
  if series:
204
204
  for ser in series.values():
205
+ if len(ser) > 0 and ser[0].time > quote.time:
206
+ continue
205
207
  ser.update(quote.time, quote.mid_price(), 0)
206
208
 
207
209
  @SW.watch("CachedMarketDataHolder")
@@ -180,7 +180,9 @@ class TradingManager(ITradingManager):
180
180
  def _adjust_size(self, instrument: Instrument, amount: float) -> float:
181
181
  size_adj = instrument.round_size_down(abs(amount))
182
182
  if size_adj < instrument.min_size:
183
- raise ValueError(f"Attempt to trade size {abs(amount)} less than minimal allowed {instrument.min_size} !")
183
+ raise ValueError(
184
+ f"[{instrument.symbol}] Attempt to trade size {abs(amount)} less than minimal allowed {instrument.min_size} !"
185
+ )
184
186
  return size_adj
185
187
 
186
188
  def _adjust_price(self, instrument: Instrument, price: float | None, amount: float) -> float | None:
qubx/data/tardis.py CHANGED
@@ -529,6 +529,7 @@ class TardisMachineReader(DataReader):
529
529
  raise ValueError(f"Invalid data_id format: {data_id}. Expected format: 'exchange:symbol'")
530
530
 
531
531
  exchange = TARDIS_EXCHANGE_MAPPERS.get(exchange.lower(), exchange)
532
+ symbol = self._map_symbol(symbol, exchange)
532
533
 
533
534
  start_date, end_date = handle_start_stop(start, stop)
534
535
  if not start_date:
@@ -607,6 +608,7 @@ class TardisMachineReader(DataReader):
607
608
  try:
608
609
  chunk = chunk_queue.get()
609
610
  if chunk is None: # End of chunks
611
+ logger.info(f"End of chunks for {data_id}")
610
612
  break
611
613
 
612
614
  # Process and yield the chunk
@@ -877,3 +879,10 @@ class TardisMachineReader(DataReader):
877
879
  def _stream_data(self, url: str, line_queue: Queue, stop_event: threading.Event):
878
880
  """Submit the streaming coroutine to the asyncio loop"""
879
881
  return self._async_loop.submit(self._fetch_stream(url, line_queue, stop_event))
882
+
883
+ def _map_symbol(self, symbol: str, exchange: str) -> str:
884
+ """Map symbol to Tardis Machine API format"""
885
+ if exchange.lower() == "bitfinex-derivatives":
886
+ return f"{symbol[:3]}F0:USTF0"
887
+ else:
888
+ return symbol
@@ -21,9 +21,8 @@ from qubx.backtester.utils import (
21
21
  SimulationSetup,
22
22
  recognize_simulation_data_config,
23
23
  )
24
- from qubx.connectors.ccxt.account import CcxtAccountProcessor
25
24
  from qubx.connectors.ccxt.data import CcxtDataProvider
26
- from qubx.connectors.ccxt.factory import get_ccxt_broker, get_ccxt_exchange
25
+ from qubx.connectors.ccxt.factory import get_ccxt_account, get_ccxt_broker, get_ccxt_exchange
27
26
  from qubx.connectors.tardis.data import TardisDataProvider
28
27
  from qubx.core.account import CompositeAccountProcessor
29
28
  from qubx.core.basics import (
@@ -464,40 +463,43 @@ def _create_account_processor(
464
463
  restored_state: RestoredState | None = None,
465
464
  read_only: bool = False,
466
465
  ) -> IAccountProcessor:
467
- if paper:
468
- settings = account_manager.get_exchange_settings(exchange_name)
469
-
470
- # - TODO: here we can create different types of simulated exchanges based on it's name etc
471
- simulated_exchange = get_simulated_exchange(exchange_name, time_provider, tcc)
472
-
473
- return SimulatedAccountProcessor(
474
- account_id=exchange_name,
475
- exchange=simulated_exchange,
476
- channel=channel,
477
- base_currency=settings.base_currency,
478
- initial_capital=settings.initial_capital,
479
- restored_state=restored_state,
480
- )
481
-
482
- creds = account_manager.get_exchange_credentials(exchange_name)
483
- connector = exchange_config.connector
484
466
  if exchange_config.account is not None:
485
467
  connector = exchange_config.account.connector
468
+ elif paper:
469
+ connector = "paper"
470
+ else:
471
+ connector = exchange_config.connector
486
472
 
487
473
  match connector.lower():
488
474
  case "ccxt":
475
+ creds = account_manager.get_exchange_credentials(exchange_name)
489
476
  exchange = get_ccxt_exchange(
490
477
  exchange_name, use_testnet=creds.testnet, api_key=creds.api_key, secret=creds.secret
491
478
  )
492
- return CcxtAccountProcessor(
479
+ return get_ccxt_account(
493
480
  exchange_name,
494
- exchange,
495
- channel,
496
- time_provider,
481
+ account_id=exchange_name,
482
+ exchange=exchange,
483
+ channel=channel,
484
+ time_provider=time_provider,
497
485
  base_currency=creds.base_currency,
498
486
  tcc=tcc,
499
487
  read_only=read_only,
500
488
  )
489
+ case "paper":
490
+ settings = account_manager.get_exchange_settings(exchange_name)
491
+
492
+ # - TODO: here we can create different types of simulated exchanges based on it's name etc
493
+ simulated_exchange = get_simulated_exchange(exchange_name, time_provider, tcc)
494
+
495
+ return SimulatedAccountProcessor(
496
+ account_id=exchange_name,
497
+ exchange=simulated_exchange,
498
+ channel=channel,
499
+ base_currency=settings.base_currency,
500
+ initial_capital=settings.initial_capital,
501
+ restored_state=restored_state,
502
+ )
501
503
  case _:
502
504
  raise ValueError(f"Connector {exchange_config.connector} is not supported yet !")
503
505
 
@@ -512,19 +514,19 @@ def _create_broker(
512
514
  account_manager: AccountConfigurationManager,
513
515
  paper: bool,
514
516
  ) -> IBroker:
515
- if paper:
516
- assert isinstance(account, SimulatedAccountProcessor)
517
- return SimulatedBroker(channel=channel, account=account, simulated_exchange=account._exchange)
518
-
519
- creds = account_manager.get_exchange_credentials(exchange_name)
520
- connector = exchange_config.connector
521
- params = exchange_config.params
522
517
  if exchange_config.broker is not None:
523
518
  connector = exchange_config.broker.connector
524
519
  params = exchange_config.broker.params
520
+ elif paper:
521
+ connector = "paper"
522
+ params = {}
523
+ else:
524
+ connector = exchange_config.connector
525
+ params = exchange_config.params
525
526
 
526
527
  match connector.lower():
527
528
  case "ccxt":
529
+ creds = account_manager.get_exchange_credentials(exchange_name)
528
530
  _enable_mm = params.pop("enable_mm", False)
529
531
  exchange = get_ccxt_exchange(
530
532
  exchange_name,
@@ -536,6 +538,9 @@ def _create_broker(
536
538
  return get_ccxt_broker(
537
539
  exchange_name, exchange, channel, time_provider, account, data_provider, **exchange_config.params
538
540
  )
541
+ case "paper":
542
+ assert isinstance(account, SimulatedAccountProcessor)
543
+ return SimulatedBroker(channel=channel, account=account, simulated_exchange=account._exchange)
539
544
  case _:
540
545
  raise ValueError(f"Connector {exchange_config.connector} is not supported yet !")
541
546
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.29
3
+ Version: 0.6.31
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -5,7 +5,7 @@ qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,30
5
5
  qubx/backtester/broker.py,sha256=JMasxycLqCT99NxN50uyQ1uxtpHYL0wpp4sJ3hB6v2M,2688
6
6
  qubx/backtester/data.py,sha256=De2ioNv3Zh-cCGakzK0igb2caDcqbibZ_tsYmF7sTTQ,6601
7
7
  qubx/backtester/management.py,sha256=HuyzFsBPgR7j-ei78Ngcx34CeSn65c9atmaii1aTsYg,14900
8
- qubx/backtester/ome.py,sha256=Uus6xdcWwhb_G0a6hJSLuRTgMcbFA_zJxYkH6kF22Gw,15268
8
+ qubx/backtester/ome.py,sha256=Uf3wqyVjUEpm1jrDQ4PE77E3R3B7wJJy1vaOOmvQqWg,15610
9
9
  qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
10
10
  qubx/backtester/runner.py,sha256=zF56lXVRYJCBiwdqnylob-yf48r_aVK9gHpf8aT49cQ,20471
11
11
  qubx/backtester/simulated_data.py,sha256=niujaMRj__jf4IyzCZrSBR5ZoH1VUbvsZHSewHftdmI,17240
@@ -20,26 +20,27 @@ qubx/cli/release.py,sha256=JYdNt_ZM9jarmYiRDtKqbRqqllzm2Qwi6VggokB2j8A,28167
20
20
  qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
21
21
  qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
22
22
  qubx/connectors/ccxt/broker.py,sha256=UMAytmDCwELUv4-R4WmrEQdPnZjjF89XlgJst15WVSs,15305
23
- qubx/connectors/ccxt/data.py,sha256=-5ysiERXC6Ip5Hp9N6Lq4bTQVyMZvFFMMPcS6RfwmYk,30051
23
+ qubx/connectors/ccxt/data.py,sha256=COVUh37ZdCUjiDB0a38Cj9SNSV8P95mqG2B3Gc_fQ2U,30172
24
24
  qubx/connectors/ccxt/exceptions.py,sha256=OfZc7iMdEG8uLorcZta2NuEuJrSIqi0FG7IICmwF54M,262
25
- qubx/connectors/ccxt/exchanges/__init__.py,sha256=2Po4YfvLOcpXHCS9Q2SSy3TMUejkcd9RhQ-oyGaxcEc,1504
25
+ qubx/connectors/ccxt/exchanges/__init__.py,sha256=wtRH7g2PWyX-AT_LkM29UHpq021L5dFi2SVvdbUpLCk,1756
26
26
  qubx/connectors/ccxt/exchanges/binance/broker.py,sha256=BB2V82zaOm1EjP3GrsOqQQMeGpml6-w23iv7goKrjyU,2111
27
- qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=7r4Y1-4NOMjnhz_Sdg6mjazUHtJPc1jOIPT_d8sxuBs,24444
28
- qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=C0ZtM2MmLI_LJyUFSbww9qon7od57iAKoJJZ_3KUgNM,1150
27
+ qubx/connectors/ccxt/exchanges/binance/exchange.py,sha256=ywmf373kVkXXw0h1PTqi7dg_3-sb7ZYKC8Y3qTmDtsY,24561
28
+ qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=14Mh0H05tMAAcf6ass55YqGckEOUIPp6o750DPxAuV8,6825
29
+ qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py,sha256=TgRK5i99Fil5XgB9Hxfw53-Ngx-3L2M_xiyje2jwuDY,1661
29
30
  qubx/connectors/ccxt/exchanges/kraken/kraken.py,sha256=RFrnvr1L1NZYoKYWR5_L8vVkpMXtY7UDkWRnHeoasDU,351
30
- qubx/connectors/ccxt/factory.py,sha256=mfWjIJhoTAREkL--WiCGpD7ptDC7mHRc19s0hI2ShKM,2957
31
+ qubx/connectors/ccxt/factory.py,sha256=T0cMSH5m6-T2LXrbZHM9uCSOYOfZf-bh1fAOXAoFhF4,3226
31
32
  qubx/connectors/ccxt/reader.py,sha256=qaZIaOZkRf3Rz31ZrEqqAv4kATk5zDlSq-LK1jziBs8,8314
32
- qubx/connectors/ccxt/utils.py,sha256=hNy7jmNau6SKgztVCV-HNTRRphfVREmPCvpxKkKwPzY,11575
33
+ qubx/connectors/ccxt/utils.py,sha256=hin4NgFJCH8fwQMZsjnMCwtP4KiITm5xgkiBnWgzMkI,11733
33
34
  qubx/connectors/tardis/data.py,sha256=VJJXSe6xb4jsa88VVj1lwI7VkEYknIXuM4End3t852M,30752
34
35
  qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc5g,9092
35
36
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
37
  qubx/core/account.py,sha256=4_XskMLR9Uh-rpJBDYMrceYiGMvAZw56k1ve-unIW8w,19417
37
38
  qubx/core/basics.py,sha256=PaprFa6sscrYPZfxImWOPLfVoVlu3PtcXTWbz0ZMtEk,28864
38
- qubx/core/context.py,sha256=-kzDxzsZtu2kO1eBxi5iVUoSmWB3tIZOp4JTCiEZZE0,22277
39
+ qubx/core/context.py,sha256=R6otCA09Eqb4d0xA-HOGDAv4NjpLwJc3RzLs4QHXzg4,22316
39
40
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
40
41
  qubx/core/errors.py,sha256=kWCK6o0-mm87VUhhlGKqwTpvdDXAza7YRRjeyz-vwfI,609
41
42
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
42
- qubx/core/helpers.py,sha256=9nl9L_ZzT1HsMC9VthMqXfmuRS_37crB-9bVfIRHeOs,19631
43
+ qubx/core/helpers.py,sha256=qzRsttt4sMYMarDWMzWvc3b2W-Qp9qAQwFiQBljAsA0,19722
43
44
  qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
44
45
  qubx/core/interfaces.py,sha256=VKIgEhRaDj9EMf1UUcMlai2BLrw0UNPU1-2akU5q0Ng,57955
45
46
  qubx/core/loggers.py,sha256=eYhJANHYwz1heeFMa5V7jYCL196wkTSvj6c-8lkPj1Y,19567
@@ -49,13 +50,13 @@ qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,
49
50
  qubx/core/mixins/market.py,sha256=lBappEimPhIuI0vmUvwVlIztkYjlEjJBpP-AdpfudII,3948
50
51
  qubx/core/mixins/processing.py,sha256=dqehukrfqcLy5BeILKnkpHCvva4SbLKj1ZbQdnByu1k,24552
51
52
  qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
52
- qubx/core/mixins/trading.py,sha256=6XK712ZHECZnI-lleJfQ8taXfaBagPQBVqNoBxAkEyM,7204
53
+ qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
53
54
  qubx/core/mixins/universe.py,sha256=L3s2Jw46_J1iDh4622Gk_LvCjol4W7mflBwEHrLfZEw,9899
54
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=fsbUVYNqAS42tjo3ZbPvyXTUO9hw1d-9nZcIXepfd4U,978280
55
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=3j9cBsksD9HcJZ6NNNCaqgIFvM8q3HPDLMpT1lbCkeA,978280
55
56
  qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
56
57
  qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
57
58
  qubx/core/series.pyx,sha256=7cM3zZThW59waHiYcZmMxvYj-HYD7Ej_l7nKA4emPjE,46477
58
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=0AJgqHjRH-SBlVonHtpvhHITa0qCj-AxpT6ZHZOSHrA,86568
59
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=icPDYxV83A3kNlrgxzCnhzxtMhGqh07sfSTgsmIbdTg,86568
59
60
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
60
61
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
61
62
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
@@ -64,7 +65,7 @@ qubx/data/helpers.py,sha256=VcXBl1kfWzAOqrjadKrP9WemGjJIB0q3xascbesErh4,16268
64
65
  qubx/data/hft.py,sha256=Y1QVg3eXDt9ZtdnrNk7xreblEa0Mc4Jrprt0dVaoOn0,32797
65
66
  qubx/data/readers.py,sha256=Y6bU4jyeRGB9mPdQwi9dD0PTuiQH-OVAY9N-6cM3HVo,62527
66
67
  qubx/data/registry.py,sha256=45mjy5maBSO6cf-0zfIRRDs8b0VDW7wHSPn43aRjv-o,3883
67
- qubx/data/tardis.py,sha256=aQeNKUu2jKQpTSRyzCtTqDhxWW8hUp2If7Yr5mMaaYA,33360
68
+ qubx/data/tardis.py,sha256=VMw13tIIlrGZwermKvdFRSNtLUiJDGOKW4l6WuAMQSA,33747
68
69
  qubx/emitters/__init__.py,sha256=tpJ9OoW-gycTBXGJ0647tT8-dVBmq23T2wMX_kmk3nM,565
69
70
  qubx/emitters/base.py,sha256=z0CiEnIGkizd-4Btvq9Auxg3BpnkKN6M8-ksAH2enQc,7745
70
71
  qubx/emitters/composite.py,sha256=8DsPIUtaJ95Oww9QTVVB6LR7Wcb6TJ-c1jIHMGuttz4,2784
@@ -116,7 +117,7 @@ qubx/restorers/signal.py,sha256=DBLqA7vDhoMTAzUC4N9UerrO0GbjeHdTeMoCz7U7iI8,6621
116
117
  qubx/restorers/state.py,sha256=duXyEHQhS1zdNdo3VKscMhieZ5sYNlfE4S_pPXQ1Tuw,4109
117
118
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
118
119
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=Mss8usOoL6hmmXTTcoOH5yIUN0bRcFOEgWYsIueYlRo,654440
120
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=OYg0HP0g9sc1OFuUCw16x6L8j7xd6TFa809WGe7ni2E,654440
120
121
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
121
122
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
122
123
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
@@ -150,11 +151,11 @@ qubx/utils/runner/_jupyter_runner.pyt,sha256=41dLQeI2EL4wJjBDht2qKbgISgS5DtmJ6Xt
150
151
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
151
152
  qubx/utils/runner/configs.py,sha256=4lonQgksh4wDygsN67lIidVRIUksskWuhL25A2IZwho,3694
152
153
  qubx/utils/runner/factory.py,sha256=vQ2dBTbrQE9YH__-TvuFzGF-E1li-vt_qQum9GHa11g,11666
153
- qubx/utils/runner/runner.py,sha256=iJsxTU9ne43Seh5rMfZLrXnV-RtA4BwzZYsjHeXajLs,29067
154
+ qubx/utils/runner/runner.py,sha256=X6KxzcrnBFhS9nyyid7M9dYGiZ_2f1_b_j5Ksa3DRn8,29314
154
155
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
155
156
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
156
- qubx-0.6.29.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
157
- qubx-0.6.29.dist-info/METADATA,sha256=a7FpGiZmBlGzYdt3GrAuWr6skM_ow-yWuAkZHmCiBTM,4444
158
- qubx-0.6.29.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
159
- qubx-0.6.29.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
160
- qubx-0.6.29.dist-info/RECORD,,
157
+ qubx-0.6.31.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
158
+ qubx-0.6.31.dist-info/METADATA,sha256=xcwgxs5jjEL4HrPbEUjdKq9uUbwjfFS2YNeMWm2J3Z4,4444
159
+ qubx-0.6.31.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
160
+ qubx-0.6.31.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
161
+ qubx-0.6.31.dist-info/RECORD,,
File without changes
File without changes