From 06432116c9123b1979bdb6529800c92cc31c0d0c Mon Sep 17 00:00:00 2001 From: "sergei.bulavintsev" Date: Tue, 1 Sep 2026 16:16:03 +0100 Subject: [PATCH 1/2] Added rfq param model --- .../order_object/test_conditional_order_object.py | 1 + tests/signing/order_object/test_limit_order_object.py | 5 +++++ tests/signing/order_object/test_market_order_object.py | 2 ++ tests/signing/order_object/test_tpsl_order_object.py | 2 ++ x10/clients/rest/rest_api_client.py | 9 +++++++++ x10/models/order.py | 6 ++++++ x10/signing/order_object.py | 5 +++++ 7 files changed, 30 insertions(+) diff --git a/tests/signing/order_object/test_conditional_order_object.py b/tests/signing/order_object/test_conditional_order_object.py index 15f78a8..a028a92 100644 --- a/tests/signing/order_object/test_conditional_order_object.py +++ b/tests/signing/order_object/test_conditional_order_object.py @@ -83,6 +83,7 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "debuggingAmounts": {"collateralAmount": "-43445117", "feeAmount": "21723", "syntheticAmount": "1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) diff --git a/tests/signing/order_object/test_limit_order_object.py b/tests/signing/order_object/test_limit_order_object.py index ab4ee50..38c554b 100644 --- a/tests/signing/order_object/test_limit_order_object.py +++ b/tests/signing/order_object/test_limit_order_object.py @@ -74,6 +74,7 @@ async def test_create_sell_order_with_default_expiration( "debuggingAmounts": {"collateralAmount": "43445116", "feeAmount": "21723", "syntheticAmount": "-1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -132,6 +133,7 @@ async def test_create_sell_order(mocker: MockerFixture, create_trading_account, "debuggingAmounts": {"collateralAmount": "43445116", "feeAmount": "21723", "syntheticAmount": "-1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -190,6 +192,7 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "debuggingAmounts": {"collateralAmount": "-43445117", "feeAmount": "21723", "syntheticAmount": "1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -297,6 +300,7 @@ async def test_create_buy_order_with_order_tpsl(mocker: MockerFixture, create_tr "debuggingAmounts": {"collateralAmount": "-43445117", "feeAmount": "21723", "syntheticAmount": "1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -406,6 +410,7 @@ async def test_create_buy_order_with_position_tpsl( "debuggingAmounts": {"collateralAmount": "-43445117", "feeAmount": "21723", "syntheticAmount": "1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) diff --git a/tests/signing/order_object/test_market_order_object.py b/tests/signing/order_object/test_market_order_object.py index 9c13d2f..a056a46 100644 --- a/tests/signing/order_object/test_market_order_object.py +++ b/tests/signing/order_object/test_market_order_object.py @@ -77,6 +77,7 @@ async def test_create_sell_order(mocker: MockerFixture, create_trading_account, "debuggingAmounts": {"collateralAmount": "49625000", "feeAmount": "24813", "syntheticAmount": "-1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -144,6 +145,7 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "debuggingAmounts": {"collateralAmount": "-50375000", "feeAmount": "25188", "syntheticAmount": "1000"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) diff --git a/tests/signing/order_object/test_tpsl_order_object.py b/tests/signing/order_object/test_tpsl_order_object.py index 624236f..0b6ba33 100644 --- a/tests/signing/order_object/test_tpsl_order_object.py +++ b/tests/signing/order_object/test_tpsl_order_object.py @@ -109,6 +109,7 @@ async def test_create_buy_partial_tpsl_order(mocker: MockerFixture, create_tradi "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "-100"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) @@ -211,6 +212,7 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "0"}, "builderFee": None, "builderId": None, + "rfq": None, } ), ) diff --git a/x10/clients/rest/rest_api_client.py b/x10/clients/rest/rest_api_client.py index ce66297..4c35114 100644 --- a/x10/clients/rest/rest_api_client.py +++ b/x10/clients/rest/rest_api_client.py @@ -15,7 +15,9 @@ from x10.models.order import ( OrderSide, OrderTpslType, + OrderType, PlacedOrderModel, + CreateOrderRfqModel, SelfTradeProtectionLevel, TimeInForce, ) @@ -51,6 +53,7 @@ async def place_order( price: Decimal, side: OrderSide, taker_fee: Decimal, + order_type: OrderType = OrderType.LIMIT, post_only: bool = False, previous_order_id=None, expire_time: Optional[datetime] = None, @@ -63,6 +66,7 @@ async def place_order( tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, + rfq: Optional[CreateOrderRfqModel] = None, ) -> WrappedApiResponseModel[PlacedOrderModel]: # FIXME: Remove all the checks, should proxy the request? if not self.__stark_account: @@ -76,6 +80,9 @@ async def place_order( if not market: raise ValidationError(f"Market {market_name} not found") + if rfq is not None and not market.is_rfq: + raise ValidationError(f"`rfq` is only supported for RFQ markets, {market_name} is not one") + if expire_time is None: expire_time = utc_now() + timedelta(hours=1) @@ -85,6 +92,7 @@ async def place_order( amount_of_synthetic=amount_of_synthetic, price=price, side=side, + order_type=order_type, post_only=post_only, previous_order_external_id=previous_order_id, expire_time=expire_time, @@ -99,6 +107,7 @@ async def place_order( tp_sl_type=tp_sl_type, take_profit=take_profit, stop_loss=stop_loss, + rfq=rfq, ) if market.is_rfq: return await self.__order_management_module.place_rfq_order(order) diff --git a/x10/models/order.py b/x10/models/order.py index 4cd02f4..55364b4 100644 --- a/x10/models/order.py +++ b/x10/models/order.py @@ -151,6 +151,11 @@ class CreateOrderTpslTriggerModel(X10BaseModel): debugging_amounts: Optional[StarkDebuggingOrderAmountsModel] = None +class CreateOrderRfqModel(X10BaseModel): + start_price: Optional[Decimal] = None + max_reprice_steps: Optional[int] = None + + class NewOrderModel(X10BaseModel): id: str market: str @@ -174,6 +179,7 @@ class NewOrderModel(X10BaseModel): debugging_amounts: Optional[StarkDebuggingOrderAmountsModel] = None builder_fee: Optional[Decimal] = None builder_id: Optional[int] = None + rfq: Optional[CreateOrderRfqModel] = None class PlacedOrderModel(X10BaseModel): diff --git a/x10/signing/order_object.py b/x10/signing/order_object.py index db64c23..e5757a8 100644 --- a/x10/signing/order_object.py +++ b/x10/signing/order_object.py @@ -17,6 +17,7 @@ OrderTriggerDirection, OrderTriggerPriceType, OrderType, + CreateOrderRfqModel, SelfTradeProtectionLevel, TimeInForce, ) @@ -71,6 +72,7 @@ def create_order_object( tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, + rfq: Optional[CreateOrderRfqModel] = None, ) -> NewOrderModel: """ Creates an order object to be placed on the exchange using the `place_order` method. @@ -105,6 +107,7 @@ def create_order_object( tp_sl_type=tp_sl_type, take_profit=take_profit, stop_loss=stop_loss, + rfq=rfq, ) @@ -175,6 +178,7 @@ def __create_order_object( tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, + rfq: Optional[CreateOrderRfqModel] = None, ) -> NewOrderModel: def validate_market_order(): if post_only: @@ -288,6 +292,7 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): builder_fee=builder_fee, builder_id=builder_id, reduce_only=reduce_only, + rfq=rfq, ) return order From 34189ced8fb39de33a063f2f2be77d8bfcfe3dd8 Mon Sep 17 00:00:00 2001 From: "sergei.bulavintsev" Date: Thu, 3 Sep 2026 11:47:28 +0100 Subject: [PATCH 2/2] Added rfq param model --- MIGRATION.md | 6 ++++++ .../test_conditional_order_object.py | 1 - .../order_object/test_limit_order_object.py | 5 ----- .../order_object/test_market_order_object.py | 2 -- .../order_object/test_order_object_attrs.py | 21 ++++++++++++++++++- .../order_object/test_tpsl_order_object.py | 2 -- .../blocking/blocking_trading_client.py | 5 +++-- x10/clients/rest/rest_api_client.py | 5 +---- x10/models/order.py | 14 +++++++------ x10/signing/order_object.py | 17 +++++---------- 10 files changed, 43 insertions(+), 35 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 7f7cf66..c41e879 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,11 @@ # Migration Guide +## 2.6.0 -> 2.7.0 + +- The `rfq_start_price` argument of `RestApiClient.place_order`, `BlockingTradingClient.create_and_place_order` + and `create_order_object` has been replaced with `rfq: CreateOrderRfqModel` (`x10.models.order`), which carries + `start_price` and the new `max_reprice_steps`. `NewOrderModel.rfq_start_price` has been replaced with `NewOrderModel.rfq`. + ## 2.0.0 -> 2.3.0 - `Config` dataclass has been renamed to `ClientConfig` and moved to `x10.core.client_config`. diff --git a/tests/signing/order_object/test_conditional_order_object.py b/tests/signing/order_object/test_conditional_order_object.py index 1ab15e3..a028a92 100644 --- a/tests/signing/order_object/test_conditional_order_object.py +++ b/tests/signing/order_object/test_conditional_order_object.py @@ -55,7 +55,6 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "side": "BUY", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", diff --git a/tests/signing/order_object/test_limit_order_object.py b/tests/signing/order_object/test_limit_order_object.py index 900bdfe..38c554b 100644 --- a/tests/signing/order_object/test_limit_order_object.py +++ b/tests/signing/order_object/test_limit_order_object.py @@ -51,7 +51,6 @@ async def test_create_sell_order_with_default_expiration( "side": "SELL", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", @@ -111,7 +110,6 @@ async def test_create_sell_order(mocker: MockerFixture, create_trading_account, "side": "SELL", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", @@ -171,7 +169,6 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "side": "BUY", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", @@ -244,7 +241,6 @@ async def test_create_buy_order_with_order_tpsl(mocker: MockerFixture, create_tr "side": "BUY", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", @@ -355,7 +351,6 @@ async def test_create_buy_order_with_position_tpsl( "side": "BUY", "qty": "0.00100000", "price": "43445.11680000", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "GTT", diff --git a/tests/signing/order_object/test_market_order_object.py b/tests/signing/order_object/test_market_order_object.py index d996fd6..a056a46 100644 --- a/tests/signing/order_object/test_market_order_object.py +++ b/tests/signing/order_object/test_market_order_object.py @@ -54,7 +54,6 @@ async def test_create_sell_order(mocker: MockerFixture, create_trading_account, "side": "SELL", "qty": "0.00100000", "price": "49625.0", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "IOC", @@ -123,7 +122,6 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c "side": "BUY", "qty": "0.00100000", "price": "50375.0", - "rfqStartPrice": None, "reduceOnly": False, "postOnly": False, "timeInForce": "IOC", diff --git a/tests/signing/order_object/test_order_object_attrs.py b/tests/signing/order_object/test_order_object_attrs.py index 8fb8e5c..c4d2b89 100644 --- a/tests/signing/order_object/test_order_object_attrs.py +++ b/tests/signing/order_object/test_order_object_attrs.py @@ -7,7 +7,8 @@ from pytest_mock import MockerFixture from x10.config import TESTNET_CONFIG -from x10.models.order import OrderSide +from x10.errors import ValidationError +from x10.models.order import CreateOrderRfqModel, OrderSide, OrderType, TimeInForce from x10.utils.date import utc_now FROZEN_NONCE = 1473459052 @@ -71,3 +72,21 @@ async def test_external_order_id(mocker: MockerFixture, create_trading_account, } ), ) + + +@pytest.mark.asyncio +async def test_rfq_not_allowed_for_non_rfq_market(create_trading_account, create_btc_usd_market): + from x10.signing.order_object import create_order_object + + with pytest.raises(ValidationError, match="only supported for RFQ markets"): + create_order_object( + account=create_trading_account(), + market=create_btc_usd_market(), + order_type=OrderType.MARKET, + time_in_force=TimeInForce.IOC, + amount_of_synthetic=Decimal("0.00100000"), + price=Decimal("43445.11680000"), + side=OrderSide.BUY, + starknet_domain=TESTNET_CONFIG.signing.starknet_domain, + rfq=CreateOrderRfqModel(start_price=Decimal("43000"), max_reprice_steps=3), + ) diff --git a/tests/signing/order_object/test_tpsl_order_object.py b/tests/signing/order_object/test_tpsl_order_object.py index 1388a89..0b6ba33 100644 --- a/tests/signing/order_object/test_tpsl_order_object.py +++ b/tests/signing/order_object/test_tpsl_order_object.py @@ -65,7 +65,6 @@ async def test_create_buy_partial_tpsl_order(mocker: MockerFixture, create_tradi "side": "SELL", "qty": "0.0001", "price": "0", - "rfqStartPrice": None, "reduceOnly": True, "postOnly": False, "timeInForce": "GTT", @@ -161,7 +160,6 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "side": "SELL", "qty": "0", "price": "0", - "rfqStartPrice": None, "reduceOnly": True, "postOnly": False, "timeInForce": "GTT", diff --git a/x10/clients/blocking/blocking_trading_client.py b/x10/clients/blocking/blocking_trading_client.py index 69830f5..2aeabc2 100644 --- a/x10/clients/blocking/blocking_trading_client.py +++ b/x10/clients/blocking/blocking_trading_client.py @@ -14,6 +14,7 @@ from x10.models.http import WrappedStreamResponseModel from x10.models.market import MarketModel from x10.models.order import ( + CreateOrderRfqModel, NewOrderModel, OpenOrderModel, OrderSide, @@ -207,7 +208,7 @@ async def create_and_place_order( time_in_force: TimeInForce = TimeInForce.GTT, reduce_only: bool = False, order_type: OrderType = OrderType.LIMIT, - rfq_start_price: Optional[Decimal] = None, + rfq: Optional[CreateOrderRfqModel] = None, ) -> TimedOpenOrderModel: market = (await self.get_markets()).get(market_name) @@ -220,7 +221,6 @@ async def create_and_place_order( order_type=order_type, amount_of_synthetic=amount_of_synthetic, price=price, - rfq_start_price=rfq_start_price, side=side, post_only=post_only, reduce_only=reduce_only, @@ -231,6 +231,7 @@ async def create_and_place_order( builder_id=builder_id, time_in_force=time_in_force, taker_fee=taker_fee, + rfq=rfq, ) if order.id in self.__order_waiters: diff --git a/x10/clients/rest/rest_api_client.py b/x10/clients/rest/rest_api_client.py index a90daeb..9ebd6bc 100644 --- a/x10/clients/rest/rest_api_client.py +++ b/x10/clients/rest/rest_api_client.py @@ -13,11 +13,11 @@ from x10.errors import ValidationError from x10.models.market import MarketModel from x10.models.order import ( + CreateOrderRfqModel, OrderSide, OrderTpslType, OrderType, PlacedOrderModel, - CreateOrderRfqModel, SelfTradeProtectionLevel, TimeInForce, ) @@ -80,9 +80,6 @@ async def place_order( if not market: raise ValidationError(f"Market {market_name} not found") - if rfq is not None and not market.is_rfq: - raise ValidationError(f"`rfq` is only supported for RFQ markets, {market_name} is not one") - if expire_time is None: expire_time = utc_now() + timedelta(hours=1) diff --git a/x10/models/order.py b/x10/models/order.py index 807cfbe..7044f6d 100644 --- a/x10/models/order.py +++ b/x10/models/order.py @@ -153,23 +153,25 @@ class CreateOrderTpslTriggerModel(X10BaseModel): class CreateOrderRfqModel(X10BaseModel): + """ + RFQ quote walk parameters. Supported for RFQ markets only. + + Attributes: + start_price: The start price of the quote walk. + max_reprice_steps: The maximum number of repricing steps of the quote walk. + """ + start_price: Optional[Decimal] = None max_reprice_steps: Optional[int] = None class NewOrderModel(X10BaseModel): - """ - Attributes: - rfq_start_price: The start price of the quote walk. Supported for RFQ markets MARKET orders only. - """ - id: str market: str type: OrderType side: OrderSide qty: Decimal price: Decimal - rfq_start_price: Optional[Decimal] = None reduce_only: bool = False post_only: bool = False time_in_force: TimeInForce diff --git a/x10/signing/order_object.py b/x10/signing/order_object.py index 7082ad3..3cd5fdc 100644 --- a/x10/signing/order_object.py +++ b/x10/signing/order_object.py @@ -9,6 +9,7 @@ from x10.models.market import MarketModel from x10.models.order import ( CreateOrderConditionalTriggerModel, + CreateOrderRfqModel, CreateOrderTpslTriggerModel, NewOrderModel, OrderPriceType, @@ -17,7 +18,6 @@ OrderTriggerDirection, OrderTriggerPriceType, OrderType, - CreateOrderRfqModel, SelfTradeProtectionLevel, TimeInForce, ) @@ -54,7 +54,6 @@ def create_order_object( market: MarketModel, amount_of_synthetic: Decimal, price: Decimal, - rfq_start_price: Optional[Decimal] = None, side: OrderSide, starknet_domain: StarknetDomain, order_type: OrderType = OrderType.LIMIT, @@ -87,7 +86,6 @@ def create_order_object( order_type=order_type, synthetic_amount=amount_of_synthetic, price=price, - rfq_start_price=rfq_start_price, side=side, collateral_position_id=account.vault, signer=account.sign, @@ -159,7 +157,6 @@ def __create_order_object( order_type: OrderType, synthetic_amount: Decimal, price: Decimal, - rfq_start_price: Optional[Decimal] = None, side: OrderSide, collateral_position_id: int, signer: Callable[[int], Tuple[int, int]], @@ -194,12 +191,9 @@ def validate_conditional_order(): if not trigger: raise ValidationError("CONDITIONAL orders must have `trigger` specified") - def validate_rfq_start_price(): - if rfq_start_price and not market.is_rfq: - raise ValidationError("`rfq_start_price` must not be provided for non-RFQ markets") - - if rfq_start_price and order_type != OrderType.MARKET: - raise ValidationError("`rfq_start_price` must not be provided for non-MARKET orders") + def validate_rfq(): + if rfq is not None and not market.is_rfq: + raise ValidationError(f"`rfq` is only supported for RFQ markets, {market.name} is not one") def validate_tpsl_order(): if not reduce_only: @@ -236,7 +230,7 @@ def validate_tpsl_order(): elif order_type == OrderType.TPSL: validate_tpsl_order() - validate_rfq_start_price() + validate_rfq() if nonce is None: nonce = generate_nonce() @@ -284,7 +278,6 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): side=side, qty=settlement_data.synthetic_amount_human.value, price=price, - rfq_start_price=rfq_start_price, post_only=post_only, time_in_force=time_in_force, expiry_epoch_millis=to_epoch_millis(expire_time),