Qubx 0.6.16__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.18__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
@@ -49,6 +49,8 @@ class OrdersManagementEngine:
49
49
  __order_id: int
50
50
  __trade_id: int
51
51
  _fill_stops_at_price: bool
52
+ _tick_size: float
53
+ _last_update_time: dt_64
52
54
 
53
55
  def __init__(
54
56
  self,
@@ -69,6 +71,9 @@ class OrdersManagementEngine:
69
71
  self.__order_id = 100000
70
72
  self.__trade_id = 100000
71
73
  self._fill_stops_at_price = fill_stop_order_at_price
74
+ self._tick_size = instrument.tick_size
75
+ self._last_update_time = np.datetime64(0, "ns")
76
+
72
77
  if not debug:
73
78
  self._dbg = lambda message, **kwargs: None
74
79
 
@@ -104,13 +109,15 @@ class OrdersManagementEngine:
104
109
 
105
110
  # - bunch of trades
106
111
  elif isinstance(mdata, TradeArray):
107
- _b = mdata.max_buy_price
108
- _a = mdata.min_sell_price
112
+ # - to prevent triggering of orders on past trades in array
113
+ _, max_buy_price, min_sell_price, _ = mdata.traded_range_from(self._last_update_time)
114
+ _b = max_buy_price - self._tick_size
115
+ _a = min_sell_price + self._tick_size
109
116
  _bs, _as = _a, _b
110
117
 
111
118
  # - single trade
112
119
  elif isinstance(mdata, Trade):
113
- _b, _a = mdata.price, mdata.price
120
+ _b, _a = mdata.price - self._tick_size, mdata.price + self._tick_size
114
121
  _bs, _as = _b, _a
115
122
 
116
123
  # - order book
@@ -154,6 +161,7 @@ class OrdersManagementEngine:
154
161
  self.stop_orders.pop(soid)
155
162
  _exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
156
163
 
164
+ self._last_update_time = timestamp
157
165
  return _exec_report
158
166
 
159
167
  def place_order(
qubx/cli/commands.py CHANGED
@@ -140,16 +140,16 @@ def ls(directory: str):
140
140
  callback=lambda ctx, param, value: os.path.abspath(os.path.expanduser(value)),
141
141
  )
142
142
  @click.option(
143
- "--strategy",
144
- "-s",
145
- type=click.STRING,
146
- help="Strategy name to release (should match the strategy class name) or path to a config YAML file",
143
+ "--config",
144
+ "-c",
145
+ type=click.Path(exists=True, resolve_path=True),
146
+ help="Path to a config YAML file",
147
147
  required=True,
148
148
  )
149
149
  @click.option(
150
150
  "--output-dir",
151
151
  "-o",
152
- type=click.STRING,
152
+ type=click.Path(exists=False),
153
153
  help="Output directory to put zip file.",
154
154
  default=".releases",
155
155
  show_default=True,
@@ -172,7 +172,6 @@ def ls(directory: str):
172
172
  )
173
173
  @click.option(
174
174
  "--commit",
175
- "-c",
176
175
  is_flag=True,
177
176
  default=False,
178
177
  help="Commit changes and create tag in repo (default: False)",
@@ -180,7 +179,7 @@ def ls(directory: str):
180
179
  )
181
180
  def release(
182
181
  directory: str,
183
- strategy: str,
182
+ config: str,
184
183
  tag: str | None,
185
184
  message: str | None,
186
185
  commit: bool,
@@ -189,16 +188,9 @@ def release(
189
188
  """
190
189
  Releases the strategy to a zip file.
191
190
 
192
- The strategy can be specified in two ways:
193
- 1. As a strategy name (class name) - strategies are scanned in the given directory (NOT SUPPORTED ANYMORE !)
194
- 2. As a path to a config YAML file containing the strategy configuration in StrategyConfig format
195
-
196
- If a strategy name is provided, a default configuration will be generated with:
197
- - The strategy parameters from the strategy class
198
- - Default exchange, connector, and instruments from the command options
199
- - Standard logging configuration
191
+ The strategy is specified by a path to a config YAML file containing the strategy configuration in StrategyConfig format.
200
192
 
201
- If a config file is provided, it must follow the StrategyConfig structure with:
193
+ The config file must follow the StrategyConfig structure with:
202
194
  - strategy: The strategy name or path
203
195
  - parameters: Dictionary of strategy parameters
204
196
  - exchanges: Dictionary of exchange configurations
@@ -211,7 +203,7 @@ def release(
211
203
 
212
204
  release_strategy(
213
205
  directory=directory,
214
- strategy_name=strategy,
206
+ config_file=config,
215
207
  tag=tag,
216
208
  message=message,
217
209
  commit=commit,
qubx/cli/release.py CHANGED
@@ -16,7 +16,6 @@ from qubx import logger
16
16
  from qubx.utils.misc import (
17
17
  cyan,
18
18
  generate_name,
19
- get_local_qubx_folder,
20
19
  green,
21
20
  load_qubx_resources_as_text,
22
21
  magenta,
@@ -265,7 +264,7 @@ def load_strategy_from_config(config_path: Path, directory: str) -> StrategyInfo
265
264
 
266
265
  def release_strategy(
267
266
  directory: str,
268
- strategy_name: str,
267
+ config_file: str,
269
268
  tag: str | None,
270
269
  message: str | None,
271
270
  commit: bool,
@@ -276,7 +275,7 @@ def release_strategy(
276
275
 
277
276
  Args:
278
277
  directory: str - directory to scan for strategies
279
- strategy_name: str - strategy name to release or path to config file
278
+ config_file: str - path to config file
280
279
  tag: str - additional tag for this release
281
280
  message: str - release message
282
281
  commit: bool - commit changes and create tag in repo
@@ -288,29 +287,12 @@ def release_strategy(
288
287
 
289
288
  try:
290
289
  # - determine if strategy_name is a config file or a strategy name
291
- if is_config_file(strategy_name):
292
- # - load strategy from config file
293
- logger.info(f"Loading strategy from config file: {strategy_name}")
294
- stg_info = load_strategy_from_config(Path(strategy_name), directory)
295
- else:
296
- # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
297
- # TODO: generate default config from strategy class ? Do we really need it at all ?
298
- # - find strategy by name
299
- # logger.info(f"Looking for '{strategy_name}' strategy")
300
-
301
- # strat_name = "_".join([x.split(".")[-1] for x in strategy_class_names])
302
- # stg_info = StrategyInfo(name=strategy_name, classes=[find_class_by_name(directory, strategy_name)])
303
-
304
- # stg_info = find_class_by_name(directory, strategy_name)
290
+ if not is_config_file(config_file):
291
+ raise ValueError("Try using yaml config file path")
305
292
 
306
- # - generate default config
307
- # strategy_config = generate_default_config(
308
- # stg_info, default_exchange, default_connector, default_instruments
309
- # )
310
- # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
311
- raise ValueError(
312
- "!!! Release of strategy by name is not supported anymore ! Try to use config file instead !!!"
313
- )
293
+ # - load strategy from config file
294
+ logger.info(f"Loading strategy from config file: {config_file}")
295
+ stg_info = load_strategy_from_config(Path(config_file), directory)
314
296
 
315
297
  # - process git repo and pyproject.toml for each strategy component
316
298
  repos_paths = set()
@@ -413,9 +395,8 @@ def _save_strategy_config(stg_name: str, strategy_config: StrategyConfig, releas
413
395
 
414
396
  def _copy_strategy_file(strategy_path: str, pyproject_root: str, release_dir: str) -> None:
415
397
  """Copy the strategy file to the release directory."""
416
- src_dir = os.path.basename(pyproject_root)
417
398
  rel_path = os.path.relpath(strategy_path, pyproject_root)
418
- dest_file_path = os.path.join(release_dir, src_dir, rel_path)
399
+ dest_file_path = os.path.join(release_dir, rel_path)
419
400
 
420
401
  # Ensure the destination directory exists
421
402
  os.makedirs(os.path.dirname(dest_file_path), exist_ok=True)
@@ -428,11 +409,9 @@ def _copy_strategy_file(strategy_path: str, pyproject_root: str, release_dir: st
428
409
  def _try_copy_file(src_file: str, dest_dir: str, pyproject_root: str) -> None:
429
410
  """Try to copy the file to the release directory."""
430
411
  if os.path.exists(src_file):
431
- _src_dir = os.path.basename(pyproject_root)
432
-
433
412
  # Get the relative path from pyproject_root
434
413
  _rel_import_path = os.path.relpath(src_file, pyproject_root)
435
- _dest_import_path = os.path.join(dest_dir, _src_dir, _rel_import_path)
414
+ _dest_import_path = os.path.join(dest_dir, _rel_import_path)
436
415
 
437
416
  # Ensure the destination directory exists
438
417
  os.makedirs(os.path.dirname(_dest_import_path), exist_ok=True)
@@ -446,10 +425,22 @@ def _copy_dependencies(strategy_path: str, pyproject_root: str, release_dir: str
446
425
  """Copy all dependencies required by the strategy."""
447
426
  _src_dir = os.path.basename(pyproject_root)
448
427
  _imports = _get_imports(strategy_path, pyproject_root, [_src_dir])
428
+ # find inside of the pyproject_root a folder with the same name as the _src_dir
429
+ # for instance it could be like macd_crossover/src/macd_crossover
430
+ # or macd_crossover/macd_crossover
431
+ # and assign this folder to _src_root
432
+ _src_root = None
433
+ for root, dirs, files in os.walk(pyproject_root):
434
+ if _src_dir in dirs:
435
+ _src_root = os.path.join(root, _src_dir)
436
+ break
437
+
438
+ if _src_root is None:
439
+ raise ValueError(f"Could not find the source root for {_src_dir} in {pyproject_root}")
449
440
 
450
441
  for _imp in _imports:
451
442
  # Construct source path
452
- _base = os.path.join(pyproject_root, *[s for s in _imp.module if s != _src_dir])
443
+ _base = os.path.join(_src_root, *[s for s in _imp.module if s != _src_dir])
453
444
 
454
445
  # - try to copy all available files for satisfying the import
455
446
  if os.path.isdir(_base):
@@ -521,7 +512,7 @@ def _modify_pyproject_toml(pyproject_path: str, package_name: str) -> None:
521
512
  deps[d] = f">={version(d)}"
522
513
 
523
514
  # Replace the packages section with the new one
524
- pyproject_data["tool"]["poetry"]["packages"] = [{"include": package_name}]
515
+ # pyproject_data["tool"]["poetry"]["packages"] = [{"include": package_name}]
525
516
 
526
517
  # Check if build section exists
527
518
  if "build" not in pyproject_data["tool"]["poetry"]:
@@ -632,7 +623,7 @@ def _handle_project_files(pyproject_root: str, release_dir: str) -> None:
632
623
  # Copy build.py if it exists
633
624
  build_src = os.path.join(pyproject_root, "build.py")
634
625
  if not os.path.exists(build_src):
635
- logger.warning(f"build.py not found in {pyproject_root} using default one")
626
+ logger.info(f"build.py not found in {pyproject_root} using default one")
636
627
  build_src = load_qubx_resources_as_text("_build.py")
637
628
 
638
629
  # - setup project's name in default build.py
@@ -14,7 +14,7 @@ from qubx.core.basics import (
14
14
  Order,
15
15
  )
16
16
  from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
17
- from qubx.core.exceptions import InvalidOrderParameters
17
+ from qubx.core.exceptions import BadRequest, InvalidOrderParameters
18
18
  from qubx.core.interfaces import (
19
19
  IAccountProcessor,
20
20
  IBroker,
@@ -37,11 +37,11 @@ class CcxtBroker(IBroker):
37
37
  time_provider: ITimeProvider,
38
38
  account: IAccountProcessor,
39
39
  data_provider: IDataProvider,
40
- enable_price_match: bool = False,
41
- price_match_ticks: int = 5,
42
40
  cancel_timeout: int = 30,
43
41
  cancel_retry_interval: int = 2,
44
42
  max_cancel_retries: int = 10,
43
+ enable_create_order_ws: bool = False,
44
+ enable_cancel_order_ws: bool = False,
45
45
  ):
46
46
  self._exchange = exchange
47
47
  self.ccxt_exchange_id = str(exchange.name)
@@ -49,12 +49,12 @@ class CcxtBroker(IBroker):
49
49
  self.time_provider = time_provider
50
50
  self.account = account
51
51
  self.data_provider = data_provider
52
- self.enable_price_match = enable_price_match
53
- self.price_match_ticks = price_match_ticks
54
52
  self._loop = AsyncThreadLoop(exchange.asyncio_loop)
55
53
  self.cancel_timeout = cancel_timeout
56
54
  self.cancel_retry_interval = cancel_retry_interval
57
55
  self.max_cancel_retries = max_cancel_retries
56
+ self.enable_create_order_ws = enable_create_order_ws
57
+ self.enable_cancel_order_ws = enable_cancel_order_ws
58
58
 
59
59
  @property
60
60
  def is_simulated_trading(self) -> bool:
@@ -209,61 +209,14 @@ class CcxtBroker(IBroker):
209
209
  Returns:
210
210
  tuple: (Order object if successful, Exception if failed)
211
211
  """
212
- params = {}
213
- _is_trigger_order = order_type.startswith("stop_")
214
-
215
- if order_type == "limit" or _is_trigger_order:
216
- params["timeInForce"] = time_in_force.upper()
217
- if price is None:
218
- return None, InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
219
-
220
- quote = self.data_provider.get_quote(instrument)
221
-
222
- # TODO: think about automatically setting reduce only when needed
223
- if not options.get("reduceOnly", False):
224
- min_notional = instrument.min_notional
225
- if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
226
- return None, InvalidOrderParameters(
227
- f"[{instrument.symbol}] Order amount {amount} is too small. Minimum notional is {min_notional}"
228
- )
229
-
230
- # - handle trigger (stop) orders
231
- if _is_trigger_order:
232
- params["triggerPrice"] = price
233
- order_type = order_type.split("_")[1]
234
-
235
- if client_id:
236
- params["newClientOrderId"] = client_id
237
-
238
- if "priceMatch" in options:
239
- params["priceMatch"] = options["priceMatch"]
240
-
241
- if instrument.is_futures():
242
- params["type"] = "swap"
243
-
244
- if time_in_force == "gtx" and price is not None and self.enable_price_match:
245
- if (order_side == "buy" and quote.bid - price < self.price_match_ticks * instrument.tick_size) or (
246
- order_side == "sell" and price - quote.ask < self.price_match_ticks * instrument.tick_size
247
- ):
248
- params["priceMatch"] = "QUEUE"
249
- logger.debug(f"[<y>{instrument.symbol}</y>] :: Price match is set to QUEUE. Price will be ignored.")
250
-
251
- if "priceMatch" in params:
252
- # - if price match is set, we don't need to specify the price
253
- price = None
254
-
255
- ccxt_symbol = instrument_to_ccxt_symbol(instrument)
256
-
257
212
  try:
258
- # Type annotation issue: We need to use type ignore for CCXT API compatibility
259
- r = await self._exchange.create_order(
260
- symbol=ccxt_symbol,
261
- type=order_type, # type: ignore
262
- side=order_side, # type: ignore
263
- amount=amount,
264
- price=price,
265
- params=params,
213
+ payload = self._prepare_order_payload(
214
+ instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
266
215
  )
216
+ if self.enable_create_order_ws:
217
+ r = await self._exchange.create_order_ws(**payload)
218
+ else:
219
+ r = await self._exchange.create_order(**payload)
267
220
 
268
221
  if r is None:
269
222
  msg = "(::_create_order) No response from exchange"
@@ -278,25 +231,6 @@ class CcxtBroker(IBroker):
278
231
  logger.error(
279
232
  f"(::_create_order) [{instrument.symbol}] ORDER NOT FILLEABLE for {order_side} {amount} {order_type} : {exc}"
280
233
  )
281
- exc_msg = str(exc)
282
- if (
283
- self.enable_price_match
284
- and "priceMatch" not in options
285
- and ("-5022" in exc_msg or "Post Only order will be rejected" in exc_msg)
286
- ):
287
- logger.debug(f"(::_create_order) [{instrument.symbol}] Trying again with price match ...")
288
- options_with_price_match = options.copy()
289
- options_with_price_match["priceMatch"] = "QUEUE"
290
- return await self._create_order(
291
- instrument=instrument,
292
- order_side=order_side,
293
- order_type=order_type,
294
- amount=amount,
295
- price=price,
296
- client_id=client_id,
297
- time_in_force=time_in_force,
298
- **options_with_price_match,
299
- )
300
234
  return None, exc
301
235
  except ccxt.InvalidOrder as exc:
302
236
  logger.error(
@@ -315,6 +249,59 @@ class CcxtBroker(IBroker):
315
249
  logger.error(traceback.format_exc())
316
250
  return None, err
317
251
 
252
+ def _prepare_order_payload(
253
+ self,
254
+ instrument: Instrument,
255
+ order_side: str,
256
+ order_type: str,
257
+ amount: float,
258
+ price: float | None = None,
259
+ client_id: str | None = None,
260
+ time_in_force: str = "gtc",
261
+ **options,
262
+ ) -> dict[str, Any]:
263
+ params = {}
264
+ _is_trigger_order = order_type.startswith("stop_")
265
+
266
+ if order_type == "limit" or _is_trigger_order:
267
+ params["timeInForce"] = time_in_force.upper()
268
+ if price is None:
269
+ raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
270
+
271
+ quote = self.data_provider.get_quote(instrument)
272
+ if quote is None:
273
+ logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
274
+ raise BadRequest(f"Quote is not available for order creation for {instrument.symbol}")
275
+
276
+ # TODO: think about automatically setting reduce only when needed
277
+ if not options.get("reduceOnly", False):
278
+ min_notional = instrument.min_notional
279
+ if min_notional > 0 and abs(amount) * quote.mid_price() < min_notional:
280
+ raise InvalidOrderParameters(
281
+ f"[{instrument.symbol}] Order amount {amount} is too small. Minimum notional is {min_notional}"
282
+ )
283
+
284
+ # - handle trigger (stop) orders
285
+ if _is_trigger_order:
286
+ params["triggerPrice"] = price
287
+ order_type = order_type.split("_")[1]
288
+
289
+ if client_id:
290
+ params["newClientOrderId"] = client_id
291
+
292
+ if instrument.is_futures():
293
+ params["type"] = "swap"
294
+
295
+ ccxt_symbol = instrument_to_ccxt_symbol(instrument)
296
+ return {
297
+ "symbol": ccxt_symbol,
298
+ "type": order_type,
299
+ "side": order_side,
300
+ "amount": amount,
301
+ "price": price,
302
+ "params": params,
303
+ }
304
+
318
305
  async def _cancel_order_with_retry(self, order_id: str, instrument: Instrument) -> bool:
319
306
  """
320
307
  Attempts to cancel an order with retries.
@@ -332,7 +319,10 @@ class CcxtBroker(IBroker):
332
319
 
333
320
  while True:
334
321
  try:
335
- await self._exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
322
+ if self.enable_cancel_order_ws:
323
+ await self._exchange.cancel_order_ws(order_id, symbol=instrument_to_ccxt_symbol(instrument))
324
+ else:
325
+ await self._exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(instrument))
336
326
  return True
337
327
  except ccxt.OperationRejected as err:
338
328
  err_msg = str(err).lower()
@@ -224,7 +224,8 @@ class CcxtDataProvider(IDataProvider):
224
224
 
225
225
  if sub_type in self._sub_to_coro:
226
226
  logger.debug(f"Canceling existing {sub_type} subscription for {self._subscriptions[_sub_type]}")
227
- self._loop.submit(self._stop_subscriber(sub_type, self._sub_to_name[sub_type]))
227
+ # - wait for the subscriber to stop
228
+ self._loop.submit(self._stop_subscriber(sub_type, self._sub_to_name[sub_type])).result()
228
229
  del self._sub_to_coro[sub_type]
229
230
  del self._sub_to_name[sub_type]
230
231
  del self._subscriptions[_sub_type]
@@ -533,7 +534,10 @@ class CcxtDataProvider(IDataProvider):
533
534
 
534
535
  async def un_watch_quote(instruments: list[Instrument]):
535
536
  symbols = [_instr_to_ccxt_symbol[i] for i in instruments]
536
- await self._exchange.un_watch_tickers(symbols)
537
+ if hasattr(self._exchange, "un_watch_bids_asks"):
538
+ await getattr(self._exchange, "un_watch_bids_asks")(symbols)
539
+ else:
540
+ await self._exchange.un_watch_tickers(symbols)
537
541
 
538
542
  await self._listen_to_stream(
539
543
  subscriber=self._call_by_market_type(watch_quote, instruments),
@@ -0,0 +1,37 @@
1
+ """
2
+ This module contains the CCXT connectors for the exchanges.
3
+ """
4
+
5
+ from functools import partial
6
+
7
+ import ccxt.pro as cxp
8
+
9
+ from .binance.broker import BinanceCcxtBroker
10
+ from .binance.exchange import BINANCE_UM_MM, BinancePortfolioMargin, BinanceQV, BinanceQVUSDM
11
+
12
+ EXCHANGE_ALIASES = {
13
+ "binance": "binanceqv",
14
+ "binance.um": "binanceqv_usdm",
15
+ "binance.cm": "binancecoinm",
16
+ "binance.pm": "binancepm",
17
+ "kraken.f": "krakenfutures",
18
+ "binance.um.mm": "binance_um_mm",
19
+ }
20
+
21
+ CUSTOM_BROKERS = {
22
+ "binance": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
23
+ "binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
24
+ "binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
25
+ "binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
26
+ }
27
+
28
+ cxp.binanceqv = BinanceQV # type: ignore
29
+ cxp.binanceqv_usdm = BinanceQVUSDM # type: ignore
30
+ cxp.binancepm = BinancePortfolioMargin # type: ignore
31
+ cxp.binance_um_mm = BINANCE_UM_MM # type: ignore
32
+
33
+ cxp.exchanges.append("binanceqv")
34
+ cxp.exchanges.append("binanceqv_usdm")
35
+ cxp.exchanges.append("binancepm")
36
+ cxp.exchanges.append("binancepm_usdm")
37
+ cxp.exchanges.append("binance_um_mm")
@@ -0,0 +1,56 @@
1
+ from typing import Any
2
+
3
+ from qubx import logger
4
+ from qubx.connectors.ccxt.broker import CcxtBroker
5
+ from qubx.core.basics import Instrument
6
+ from qubx.core.exceptions import BadRequest
7
+
8
+
9
+ class BinanceCcxtBroker(CcxtBroker):
10
+ def __init__(
11
+ self,
12
+ *args,
13
+ enable_price_match: bool = False,
14
+ price_match_ticks: int = 5,
15
+ **kwargs,
16
+ ):
17
+ super().__init__(*args, **kwargs)
18
+ self.enable_price_match = enable_price_match
19
+ self.price_match_ticks = price_match_ticks
20
+
21
+ def _prepare_order_payload(
22
+ self,
23
+ instrument: Instrument,
24
+ order_side: str,
25
+ order_type: str,
26
+ amount: float,
27
+ price: float | None = None,
28
+ client_id: str | None = None,
29
+ time_in_force: str = "gtc",
30
+ **options,
31
+ ) -> dict[str, Any]:
32
+ payload = super()._prepare_order_payload(
33
+ instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
34
+ )
35
+ params = payload.get("params", {})
36
+ if "priceMatch" in options:
37
+ params["priceMatch"] = options["priceMatch"]
38
+
39
+ quote = self.data_provider.get_quote(instrument)
40
+ if quote is None:
41
+ logger.warning(f"[<y>{instrument.symbol}</y>] :: Quote is not available for order creation.")
42
+ raise BadRequest(f"Quote is not available for price match for {instrument.symbol}")
43
+
44
+ if time_in_force == "gtx" and price is not None and self.enable_price_match:
45
+ if (order_side == "buy" and quote.bid - price < self.price_match_ticks * instrument.tick_size) or (
46
+ order_side == "sell" and price - quote.ask < self.price_match_ticks * instrument.tick_size
47
+ ):
48
+ params["priceMatch"] = "QUEUE"
49
+ logger.debug(f"[<y>{instrument.symbol}</y>] :: Price match is set to QUEUE. Price will be ignored.")
50
+
51
+ if "priceMatch" in params:
52
+ # - if price match is set, we don't need to specify the price
53
+ payload["price"] = None
54
+
55
+ payload["params"] = params
56
+ return payload