Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tech@ex10.org>"]
repository = "https://github.com/x10xchange/python_sdk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions tests/signing/order_object/test_limit_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions tests/signing/order_object/test_market_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions tests/signing/order_object/test_tpsl_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions x10/clients/blocking/blocking_trading_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions x10/clients/rest/rest_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions x10/models/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class MarketTradingHours(StrEnum):
CONTINUOUS = "CONTINUOUS"
WEEKDAYS = "WEEKDAYS"
REGULAR = "REGULAR"
NO_OVERNIGHT = "NO_OVERNIGHT"


class MarketModel(X10BaseModel):
Expand Down
8 changes: 8 additions & 0 deletions x10/models/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions x10/signing/order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -154,6 +156,7 @@ def __create_order_object(
order_type: OrderType,
synthetic_amount: Decimal,
price: Decimal,
rfq_start_price: Optional[Decimal] = None,
side: OrderSide,
Comment thread
alexex10 marked this conversation as resolved.
collateral_position_id: int,
signer: Callable[[int], Tuple[int, int]],
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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),
Expand Down
24 changes: 14 additions & 10 deletions x10/tools/mcp/place_order_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
)
Comment thread
Copilot marked this conversation as resolved.

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
Expand Down
Loading