diff --git a/pyproject.toml b/pyproject.toml index f6e78eb..2053a0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "x10-python-trading-starknet" -version = "2.5.0" +version = "2.6.0" description = "Python client for X10 API" authors = ["X10 "] repository = "https://github.com/x10xchange/python_sdk" diff --git a/tests/signing/order_object/test_conditional_order_object.py b/tests/signing/order_object/test_conditional_order_object.py index 15f78a8..28fdc1b 100644 --- a/tests/signing/order_object/test_conditional_order_object.py +++ b/tests/signing/order_object/test_conditional_order_object.py @@ -55,6 +55,7 @@ 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 ab4ee50..f643167 100644 --- a/tests/signing/order_object/test_limit_order_object.py +++ b/tests/signing/order_object/test_limit_order_object.py @@ -51,6 +51,7 @@ 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", @@ -109,6 +110,7 @@ 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", @@ -167,6 +169,7 @@ 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", @@ -238,6 +241,7 @@ 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", @@ -347,6 +351,7 @@ 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 9c13d2f..26c6046 100644 --- a/tests/signing/order_object/test_market_order_object.py +++ b/tests/signing/order_object/test_market_order_object.py @@ -54,6 +54,7 @@ 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", @@ -121,6 +122,7 @@ 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_tpsl_order_object.py b/tests/signing/order_object/test_tpsl_order_object.py index 624236f..99662eb 100644 --- a/tests/signing/order_object/test_tpsl_order_object.py +++ b/tests/signing/order_object/test_tpsl_order_object.py @@ -65,6 +65,7 @@ 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", @@ -159,6 +160,7 @@ 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 a4f93b9..69830f5 100644 --- a/x10/clients/blocking/blocking_trading_client.py +++ b/x10/clients/blocking/blocking_trading_client.py @@ -207,6 +207,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, ) -> TimedOpenOrderModel: market = (await self.get_markets()).get(market_name) @@ -219,6 +220,7 @@ 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, diff --git a/x10/clients/rest/rest_api_client.py b/x10/clients/rest/rest_api_client.py index 7e81102..2a1b76a 100644 --- a/x10/clients/rest/rest_api_client.py +++ b/x10/clients/rest/rest_api_client.py @@ -63,6 +63,7 @@ async def place_order( tp_sl_type: Optional[OrderTpslType] = None, take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, + rfq_start_price: Optional[Decimal] = None, ) -> WrappedApiResponseModel[PlacedOrderModel]: # FIXME: Remove all the checks, should proxy the request? if not self.__stark_account: @@ -84,6 +85,7 @@ async def place_order( market=market, amount_of_synthetic=amount_of_synthetic, price=price, + rfq_start_price=rfq_start_price, side=side, post_only=post_only, previous_order_external_id=previous_order_id, diff --git a/x10/models/market.py b/x10/models/market.py index 17cb6df..5f7a44b 100644 --- a/x10/models/market.py +++ b/x10/models/market.py @@ -104,6 +104,7 @@ class MarketTradingHours(StrEnum): CONTINUOUS = "CONTINUOUS" WEEKDAYS = "WEEKDAYS" REGULAR = "REGULAR" + NO_OVERNIGHT = "NO_OVERNIGHT" class MarketModel(X10BaseModel): diff --git a/x10/models/order.py b/x10/models/order.py index c200169..9b26019 100644 --- a/x10/models/order.py +++ b/x10/models/order.py @@ -153,12 +153,18 @@ class CreateOrderTpslTriggerModel(X10BaseModel): 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 @@ -206,11 +212,13 @@ class OpenOrderModel(X10BaseModel): status_reason: Optional[OrderStatusReason] = None price: Optional[Decimal] = None average_price: Optional[Decimal] = None + rfq_start_price: Optional[Decimal] = None qty: Decimal filled_qty: Optional[Decimal] = None cancelled_qty: Optional[Decimal] = None reduce_only: bool post_only: bool + chase_order: Optional[bool] = None payed_fee: Optional[Decimal] = None created_time: int updated_time: int diff --git a/x10/signing/order_object.py b/x10/signing/order_object.py index 2693af6..181e4c8 100644 --- a/x10/signing/order_object.py +++ b/x10/signing/order_object.py @@ -53,6 +53,7 @@ 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, @@ -84,6 +85,7 @@ 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, @@ -154,6 +156,7 @@ 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]], @@ -187,6 +190,13 @@ 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_tpsl_order(): if not reduce_only: raise ValidationError("TPSL orders must be reduce-only") @@ -222,6 +232,8 @@ def validate_tpsl_order(): elif order_type == OrderType.TPSL: validate_tpsl_order() + validate_rfq_start_price() + if nonce is None: nonce = generate_nonce() @@ -268,6 +280,7 @@ 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), diff --git a/x10/tools/mcp/place_order_tool.py b/x10/tools/mcp/place_order_tool.py index 263cda2..3f6a67c 100644 --- a/x10/tools/mcp/place_order_tool.py +++ b/x10/tools/mcp/place_order_tool.py @@ -111,22 +111,18 @@ async def _get_top_of_book(market_name: str) -> tuple[OrderbookQuantityModel, Or return None -async def _get_order_price( - *, client: RestApiClient, market: MarketModel, side: OrderSide, price: Decimal | None -) -> Decimal: - if price is not None: - return price - +async def _get_best_market_price(*, client: RestApiClient, market: MarketModel, side: OrderSide) -> Decimal: best_bid_and_ask = await _get_top_of_book(market.name) if best_bid_and_ask is None: raise ValidationError(f"Failed to fetch top of book for {market.name}") best_bid, best_ask = best_bid_and_ask + best_price = best_ask.price if side == OrderSide.BUY else best_bid.price return get_price_with_slippage( side=side, - price=best_ask.price if side == OrderSide.BUY else best_bid.price, + price=best_price, min_price_change=market.trading_config.min_price_change, slippage=client.config.defaults.market_price_slippage, ) @@ -174,14 +170,22 @@ async def place_order( if order_type == OrderType.TPSL: price = Decimal(0) - elif order_type != OrderType.MARKET and not price: - raise ValidationError("Price is required for non-MARKET orders") async with create_private_rest_api_client() as client: markets = await client.info.get_markets_dict() market = markets[market_name] - order_price = await _get_order_price(client=client, market=market, side=side, price=price) + if market.is_rfq: + raise ValidationError("RFQ markets are not supported by MCP") + + order_price = ( + await _get_best_market_price(client=client, market=market, side=side) + if order_type == OrderType.MARKET and price is None + else price + ) + + if order_price is None: + raise ValidationError("`order_price` is required") close_side = OrderSide.SELL if side == OrderSide.BUY else OrderSide.BUY min_price_change = market.trading_config.min_price_change