From 3210527244d84d22cd0f7294009d42a128bd4bf0 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Tue, 17 Mar 2026 12:09:40 +0400 Subject: [PATCH 01/12] Support TPSL order type --- examples/create_partial_tpsl_order.py | 86 +++++++++++++++++++ examples/create_position_tpsl_order.py | 88 ++++++++++++++++++++ pyproject.toml | 2 +- tests/perpetual/test_order_object.py | 110 +++++++++++++++++++++++++ x10/perpetual/order_object.py | 28 +++++-- 5 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 examples/create_partial_tpsl_order.py create mode 100644 examples/create_position_tpsl_order.py diff --git a/examples/create_partial_tpsl_order.py b/examples/create_partial_tpsl_order.py new file mode 100644 index 0000000..68f0e51 --- /dev/null +++ b/examples/create_partial_tpsl_order.py @@ -0,0 +1,86 @@ +import logging +from asyncio import run +from decimal import Decimal + +from examples.init_env import init_env +from examples.utils import find_order_and_cancel, get_adjust_price_by_pct +from x10.config import ETH_USD_MARKET +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object +from x10.perpetual.orders import ( + OrderPriceType, + OrderSide, + OrderTpslType, + OrderTriggerPriceType, + OrderType, + TimeInForce, +) +from x10.perpetual.trading_client import PerpetualTradingClient + +LOGGER = logging.getLogger() +MARKET_NAME = ETH_USD_MARKET +ENDPOINT_CONFIG = TESTNET_CONFIG + + +async def run_example(): + env_config = init_env() + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + markets_dict = await trading_client.markets_info.get_markets_dict() + + market = markets_dict[MARKET_NAME] + adjust_price_by_pct = get_adjust_price_by_pct(market.trading_config) + + order_size = market.trading_config.min_order_size + + order_price = adjust_price_by_pct(market.market_stats.bid_price, -10.0) + tp_trigger_price = adjust_price_by_pct(order_price, 0.5) + tp_price = adjust_price_by_pct(order_price, 1.0) + sl_trigger_price = adjust_price_by_pct(order_price, -0.5) + sl_price = adjust_price_by_pct(order_price, -1.0) + + LOGGER.info("Creating partial TPSL order object for market: %s", market.name) + + new_order = create_order_object( + account=stark_account, + starknet_domain=ENDPOINT_CONFIG.starknet_domain, + market=market, + order_type=OrderType.TPSL, + side=OrderSide.BUY, + amount_of_synthetic=order_size, + price=Decimal(0), + time_in_force=TimeInForce.GTT, + reduce_only=True, + post_only=False, + tp_sl_type=OrderTpslType.ORDER, + take_profit=OrderTpslTriggerParam( + trigger_price=tp_trigger_price, + trigger_price_type=OrderTriggerPriceType.LAST, + price=tp_price, + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=sl_trigger_price, + trigger_price_type=OrderTriggerPriceType.LAST, + price=sl_price, + price_type=OrderPriceType.LIMIT, + ), + ) + + LOGGER.info("Placing order...") + + placed_order = await trading_client.orders.place_order(order=new_order) + + LOGGER.info(f"Order is placed: {placed_order.to_pretty_json()}") + + await find_order_and_cancel(trading_client=trading_client, logger=LOGGER, order_id=placed_order.data.id) + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/examples/create_position_tpsl_order.py b/examples/create_position_tpsl_order.py new file mode 100644 index 0000000..9063757 --- /dev/null +++ b/examples/create_position_tpsl_order.py @@ -0,0 +1,88 @@ +import logging +from asyncio import run +from decimal import Decimal + +from perpetual.orders import OrderType + +from examples.init_env import init_env +from examples.utils import find_order_and_cancel, get_adjust_price_by_pct +from x10.config import ETH_USD_MARKET +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object +from x10.perpetual.orders import ( + OrderPriceType, + OrderSide, + OrderTpslType, + OrderTriggerPriceType, + TimeInForce, +) +from x10.perpetual.trading_client import PerpetualTradingClient + +LOGGER = logging.getLogger() +MARKET_NAME = ETH_USD_MARKET +ENDPOINT_CONFIG = TESTNET_CONFIG + + +async def run_example(): + env_config = init_env() + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + markets_dict = await trading_client.markets_info.get_markets_dict() + + market = markets_dict[MARKET_NAME] + adjust_price_by_pct = get_adjust_price_by_pct(market.trading_config) + + order_price = adjust_price_by_pct(market.market_stats.bid_price, -10.0) + tp_trigger_price = adjust_price_by_pct(order_price, 0.5) + tp_price = adjust_price_by_pct(order_price, 1.0) + sl_trigger_price = adjust_price_by_pct(order_price, -0.5) + sl_price = adjust_price_by_pct(order_price, -1.0) + + LOGGER.info("Creating entire position TPSL order object for market: %s", market.name) + + new_order = create_order_object( + account=stark_account, + starknet_domain=ENDPOINT_CONFIG.starknet_domain, + market=market, + order_type=OrderType.TPSL, + side=OrderSide.BUY, + amount_of_synthetic=Decimal(0), + price=Decimal(0), + time_in_force=TimeInForce.GTT, + reduce_only=True, + post_only=False, + tp_sl_type=OrderTpslType.POSITION, + take_profit=OrderTpslTriggerParam( + trigger_price=tp_trigger_price, + trigger_price_type=OrderTriggerPriceType.LAST, + price=tp_price, + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=sl_trigger_price, + trigger_price_type=OrderTriggerPriceType.LAST, + price=sl_price, + price_type=OrderPriceType.LIMIT, + ), + ) + + # FIXME + print(new_order.to_pretty_json()) + + LOGGER.info("Placing order...") + + placed_order = await trading_client.orders.place_order(order=new_order) + + LOGGER.info(f"Order is placed: {placed_order.to_pretty_json()}") + + await find_order_and_cancel(trading_client=trading_client, logger=LOGGER, order_id=placed_order.data.id) + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/pyproject.toml b/pyproject.toml index c80b80a..1c556c6 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 = "0.0.17" +version = "0.1.0" description = "Python client for X10 API" authors = ["X10 "] repository = "https://github.com/x10xchange/python_sdk" diff --git a/tests/perpetual/test_order_object.py b/tests/perpetual/test_order_object.py index b6a48c6..b8051cd 100644 --- a/tests/perpetual/test_order_object.py +++ b/tests/perpetual/test_order_object.py @@ -12,6 +12,7 @@ OrderSide, OrderTpslType, OrderTriggerPriceType, + OrderType, SelfTradeProtectionLevel, ) from x10.utils.date import utc_now @@ -411,6 +412,115 @@ async def test_create_buy_order_with_position_tpsl( ) +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.TPSL, + amount_of_synthetic=Decimal("0"), + price=Decimal("0"), + side=OrderSide.BUY, + reduce_only=True, + expire_time=utc_now() + timedelta(days=14), + self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, + starknet_domain=TESTNET_CONFIG.starknet_domain, + tp_sl_type=OrderTpslType.POSITION, + take_profit=OrderTpslTriggerParam( + trigger_price=Decimal("49000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("50000"), + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=Decimal("40000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("39000"), + price_type=OrderPriceType.LIMIT, + ), + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "2740618205716882280724217633173981437193188033910023585411792989580464995593", + "market": "BTC-USD", + "type": "TPSL", + "side": "BUY", + "qty": "0", + "price": "0", + "reduceOnly": True, + "postOnly": False, + "timeInForce": "GTT", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "CLIENT", + "cancelId": None, + "settlement": { + "signature": { + "r": "0x721145761c282c6bfe6437cb03452223212e060eb64656bb71893b65915f040", + "s": "0x7f5e5aaea50d60777e3d75ee198e2ea61d7663ea2b532178092f422ee6e8eb8", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "trigger": None, + "tpSlType": "POSITION", + "takeProfit": { + "triggerPrice": "49000", + "triggerPriceType": "MARK", + "price": "50000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x58b3d48793b1f4d30aa16c6d4a03c15ecdd7e226c6a7a86fe7ac35366a82713", + "s": "0xf16659adb76a0624e7f5cc97564d7872d16b58d699411640328114441045cc", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": { + "collateralAmount": "-500000000000000", + "feeAmount": "250000000000", + "syntheticAmount": "10000000000", + }, + }, + "stopLoss": { + "triggerPrice": "40000", + "triggerPriceType": "MARK", + "price": "39000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x54c50c1f6e110ff6d057a58244e499a66f6d4ad68a50a8851164706d8531a02", + "s": "0x1cc6a2b3850dc811a94581108190c6695bc5390b3cb694e597d7a82289c9352", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": { + "collateralAmount": "-499999999980000", + "feeAmount": "249999999990", + "syntheticAmount": "12820512820", + }, + }, + "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "0"}, + "builderFee": None, + "builderId": None, + } + ), + ) + + @freeze_time("2024-01-05 01:08:56.860694") @pytest.mark.asyncio async def test_cancel_previous_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): diff --git a/x10/perpetual/order_object.py b/x10/perpetual/order_object.py index 8508ead..b85de57 100644 --- a/x10/perpetual/order_object.py +++ b/x10/perpetual/order_object.py @@ -36,12 +36,14 @@ class OrderTpslTriggerParam: def create_order_object( + *, account: StarkPerpetualAccount, market: MarketModel, amount_of_synthetic: Decimal, price: Decimal, side: OrderSide, starknet_domain: StarknetDomain, + order_type: Optional[OrderType] = OrderType.LIMIT, post_only: bool = False, previous_order_external_id: Optional[str] = None, expire_time: Optional[datetime] = None, @@ -67,6 +69,7 @@ def create_order_object( return __create_order_object( market=market, + order_type=order_type, synthetic_amount=amount_of_synthetic, price=price, side=side, @@ -95,6 +98,7 @@ def create_order_object( def __create_order_tpsl_trigger_model( *, trigger_param: OrderTpslTriggerParam, + order_type: OrderType, side: OrderSide, synthetic_amount: Decimal, tp_sl_type: OrderTpslType, @@ -111,7 +115,7 @@ def __create_order_tpsl_trigger_model( ) ) settlement_data = create_order_settlement_data( - side=__get_opposite_side(side), + side=side if order_type == OrderType.TPSL else __get_opposite_side(side), synthetic_amount=settlement_synthetic_amount, price=trigger_param.price, ctx=settlement_data_ctx, @@ -134,6 +138,7 @@ def __get_opposite_side(side: OrderSide) -> OrderSide: def __create_order_object( *, market: MarketModel, + order_type: OrderType, synthetic_amount: Decimal, price: Decimal, side: OrderSide, @@ -157,8 +162,11 @@ def __create_order_object( take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, ) -> NewOrderModel: - if side not in OrderSide: - raise ValueError(f"Unexpected order side value: {side}") + if order_type not in [OrderType.LIMIT, OrderType.TPSL]: + raise NotImplementedError(f"{order_type} order type is not supported yet") + + if exact_only: + raise NotImplementedError("`exact_only` option is not supported yet") if time_in_force not in TimeInForce or time_in_force == TimeInForce.FOK: raise ValueError(f"Unexpected time in force value: {time_in_force}") @@ -166,8 +174,15 @@ def __create_order_object( if expire_time is None: raise ValueError("`expire_time` must be provided") - if exact_only: - raise NotImplementedError("`exact_only` option is not supported yet") + if order_type == OrderType.TPSL: + if not reduce_only: + raise ValueError("TPSL orders must be reduce-only") + + if tp_sl_type == OrderTpslType.POSITION and synthetic_amount != Decimal(0): + raise ValueError("`amount_of_synthetic` must be 0 for entire position TPSL orders") + + if price != Decimal(0): + raise ValueError("`price` must be 0 for TPSL orders") if nonce is None: nonce = generate_nonce() @@ -201,6 +216,7 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): return __create_order_tpsl_trigger_model( trigger_param=trigger_param, + order_type=order_type, side=side, synthetic_amount=synthetic_amount, tp_sl_type=tp_sl_type, @@ -212,7 +228,7 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): order = NewOrderModel( id=order_id, market=market.name, - type=OrderType.LIMIT, + type=order_type, side=side, qty=settlement_data.synthetic_amount_human.value, price=price, From b5da96f3a0cb68bf46c9026eb78be7bd7ba80e50 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Tue, 17 Mar 2026 13:17:38 +0400 Subject: [PATCH 02/12] Support TPSL order type --- examples/create_partial_tpsl_order.py | 16 +-- examples/create_position_tpsl_order.py | 21 ++-- examples/placed_order_example_advanced.py | 7 +- tests/perpetual/test_order_object.py | 123 ++++++++++++++++++---- x10/perpetual/order_object.py | 7 +- x10/perpetual/orders.py | 8 +- 6 files changed, 140 insertions(+), 42 deletions(-) diff --git a/examples/create_partial_tpsl_order.py b/examples/create_partial_tpsl_order.py index 68f0e51..a1476b3 100644 --- a/examples/create_partial_tpsl_order.py +++ b/examples/create_partial_tpsl_order.py @@ -4,7 +4,7 @@ from examples.init_env import init_env from examples.utils import find_order_and_cancel, get_adjust_price_by_pct -from x10.config import ETH_USD_MARKET +from x10.config import BTC_USD_MARKET from x10.perpetual.accounts import StarkPerpetualAccount from x10.perpetual.configuration import TESTNET_CONFIG from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object @@ -19,7 +19,7 @@ from x10.perpetual.trading_client import PerpetualTradingClient LOGGER = logging.getLogger() -MARKET_NAME = ETH_USD_MARKET +MARKET_NAME = BTC_USD_MARKET ENDPOINT_CONFIG = TESTNET_CONFIG @@ -39,11 +39,11 @@ async def run_example(): order_size = market.trading_config.min_order_size - order_price = adjust_price_by_pct(market.market_stats.bid_price, -10.0) - tp_trigger_price = adjust_price_by_pct(order_price, 0.5) - tp_price = adjust_price_by_pct(order_price, 1.0) - sl_trigger_price = adjust_price_by_pct(order_price, -0.5) - sl_price = adjust_price_by_pct(order_price, -1.0) + last_price = market.market_stats.last_price + tp_trigger_price = adjust_price_by_pct(last_price, -5) + tp_price = adjust_price_by_pct(last_price, -10) + sl_trigger_price = adjust_price_by_pct(last_price, 5) + sl_price = adjust_price_by_pct(last_price, 10) LOGGER.info("Creating partial TPSL order object for market: %s", market.name) @@ -52,7 +52,7 @@ async def run_example(): starknet_domain=ENDPOINT_CONFIG.starknet_domain, market=market, order_type=OrderType.TPSL, - side=OrderSide.BUY, + side=OrderSide.SELL, amount_of_synthetic=order_size, price=Decimal(0), time_in_force=TimeInForce.GTT, diff --git a/examples/create_position_tpsl_order.py b/examples/create_position_tpsl_order.py index 9063757..b34a0fd 100644 --- a/examples/create_position_tpsl_order.py +++ b/examples/create_position_tpsl_order.py @@ -2,11 +2,11 @@ from asyncio import run from decimal import Decimal -from perpetual.orders import OrderType +from x10.perpetual.orders import OrderType from examples.init_env import init_env from examples.utils import find_order_and_cancel, get_adjust_price_by_pct -from x10.config import ETH_USD_MARKET +from x10.config import BTC_USD_MARKET from x10.perpetual.accounts import StarkPerpetualAccount from x10.perpetual.configuration import TESTNET_CONFIG from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object @@ -20,7 +20,7 @@ from x10.perpetual.trading_client import PerpetualTradingClient LOGGER = logging.getLogger() -MARKET_NAME = ETH_USD_MARKET +MARKET_NAME = BTC_USD_MARKET ENDPOINT_CONFIG = TESTNET_CONFIG @@ -38,11 +38,11 @@ async def run_example(): market = markets_dict[MARKET_NAME] adjust_price_by_pct = get_adjust_price_by_pct(market.trading_config) - order_price = adjust_price_by_pct(market.market_stats.bid_price, -10.0) - tp_trigger_price = adjust_price_by_pct(order_price, 0.5) - tp_price = adjust_price_by_pct(order_price, 1.0) - sl_trigger_price = adjust_price_by_pct(order_price, -0.5) - sl_price = adjust_price_by_pct(order_price, -1.0) + last_price = market.market_stats.last_price + tp_trigger_price = adjust_price_by_pct(last_price, -5) + tp_price = adjust_price_by_pct(last_price, -10) + sl_trigger_price = adjust_price_by_pct(last_price, 5) + sl_price = adjust_price_by_pct(last_price, 10) LOGGER.info("Creating entire position TPSL order object for market: %s", market.name) @@ -51,7 +51,7 @@ async def run_example(): starknet_domain=ENDPOINT_CONFIG.starknet_domain, market=market, order_type=OrderType.TPSL, - side=OrderSide.BUY, + side=OrderSide.SELL, amount_of_synthetic=Decimal(0), price=Decimal(0), time_in_force=TimeInForce.GTT, @@ -72,9 +72,6 @@ async def run_example(): ), ) - # FIXME - print(new_order.to_pretty_json()) - LOGGER.info("Placing order...") placed_order = await trading_client.orders.place_order(order=new_order) diff --git a/examples/placed_order_example_advanced.py b/examples/placed_order_example_advanced.py index fcea625..8f4faf1 100644 --- a/examples/placed_order_example_advanced.py +++ b/examples/placed_order_example_advanced.py @@ -106,7 +106,12 @@ async def place_order( order_side = OrderSide.BUY if should_buy else OrderSide.SELL market = markets_cache[ADA_USD_MARKET] new_order = create_order_object( - stark_account, market, Decimal("100"), price, order_side, starknet_domain=TESTNET_CONFIG.starknet_domain + account=stark_account, + market=market, + amount_of_synthetic=Decimal("100"), + price=price, + side=order_side, + starknet_domain=TESTNET_CONFIG.starknet_domain, ) order_condtions[new_order.id] = asyncio.Condition() return new_order.id, await trading_client.orders.place_order(order=new_order) diff --git a/tests/perpetual/test_order_object.py b/tests/perpetual/test_order_object.py index b8051cd..9b7affb 100644 --- a/tests/perpetual/test_order_object.py +++ b/tests/perpetual/test_order_object.py @@ -412,6 +412,100 @@ async def test_create_buy_order_with_position_tpsl( ) +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_buy_partial_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.TPSL, + amount_of_synthetic=btc_usd_market.trading_config.min_order_size, + price=Decimal("0"), + side=OrderSide.SELL, + reduce_only=True, + expire_time=utc_now() + timedelta(days=14), + self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, + starknet_domain=TESTNET_CONFIG.starknet_domain, + tp_sl_type=OrderTpslType.ORDER, + take_profit=OrderTpslTriggerParam( + trigger_price=Decimal("49000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("50000"), + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=Decimal("40000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("39000"), + price_type=OrderPriceType.LIMIT, + ), + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "2302927936354168859785231329337424926774195696889032018790365367779325970821", + "market": "BTC-USD", + "type": "TPSL", + "side": "SELL", + "qty": "0.0001", + "price": "0", + "reduceOnly": True, + "postOnly": False, + "timeInForce": "GTT", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "CLIENT", + "cancelId": None, + "settlement": None, + "trigger": None, + "tpSlType": "ORDER", + "takeProfit": { + "triggerPrice": "49000", + "triggerPriceType": "MARK", + "price": "50000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x513b8c6540b171c6e85e2992046ce697b6056793ace2ea0e46c04cba1b72aa0", + "s": "0x25684458e72e276bbaa87a6787bfab11ed4b117c4f6ffa7cea2781c13648667", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": {"collateralAmount": "5000000", "feeAmount": "2500", "syntheticAmount": "-100"}, + }, + "stopLoss": { + "triggerPrice": "40000", + "triggerPriceType": "MARK", + "price": "39000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x30227b00607a0f671db245c734a9d9152c58af0ec0fbc83ac8c50d41b6dc2e5", + "s": "0xb355fcce280b7c82c8e6b23106df57d98aba5b3616a791a4db226063693b5d", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": {"collateralAmount": "3900000", "feeAmount": "1950", "syntheticAmount": "-100"}, + }, + "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "-100"}, + "builderFee": None, + "builderId": None, + } + ), + ) + + @freeze_time("2024-01-05 01:08:56.860694") @pytest.mark.asyncio async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): @@ -427,7 +521,7 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad order_type=OrderType.TPSL, amount_of_synthetic=Decimal("0"), price=Decimal("0"), - side=OrderSide.BUY, + side=OrderSide.SELL, reduce_only=True, expire_time=utc_now() + timedelta(days=14), self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, @@ -454,7 +548,7 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "id": "2740618205716882280724217633173981437193188033910023585411792989580464995593", "market": "BTC-USD", "type": "TPSL", - "side": "BUY", + "side": "SELL", "qty": "0", "price": "0", "reduceOnly": True, @@ -465,14 +559,7 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "nonce": "1473459052", "selfTradeProtectionLevel": "CLIENT", "cancelId": None, - "settlement": { - "signature": { - "r": "0x721145761c282c6bfe6437cb03452223212e060eb64656bb71893b65915f040", - "s": "0x7f5e5aaea50d60777e3d75ee198e2ea61d7663ea2b532178092f422ee6e8eb8", - }, - "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", - "collateralPosition": "10002", - }, + "settlement": None, "trigger": None, "tpSlType": "POSITION", "takeProfit": { @@ -482,16 +569,16 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "priceType": "LIMIT", "settlement": { "signature": { - "r": "0x58b3d48793b1f4d30aa16c6d4a03c15ecdd7e226c6a7a86fe7ac35366a82713", - "s": "0xf16659adb76a0624e7f5cc97564d7872d16b58d699411640328114441045cc", + "r": "0x9ec78c951d0397e31873bb1f5ede330dffc05a6be918007fac3299a122bc37", + "s": "0x1962207a67b6b6d77216d363af31ca5ea37d371ac762aa13bd5366634337b86", }, "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", "collateralPosition": "10002", }, "debuggingAmounts": { - "collateralAmount": "-500000000000000", + "collateralAmount": "500000000000000", "feeAmount": "250000000000", - "syntheticAmount": "10000000000", + "syntheticAmount": "-10000000000", }, }, "stopLoss": { @@ -501,16 +588,16 @@ async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trad "priceType": "LIMIT", "settlement": { "signature": { - "r": "0x54c50c1f6e110ff6d057a58244e499a66f6d4ad68a50a8851164706d8531a02", - "s": "0x1cc6a2b3850dc811a94581108190c6695bc5390b3cb694e597d7a82289c9352", + "r": "0x4b5a05de5d0c07956398de50b846037250dee8cd85c35944fd97f0b3dad3e5b", + "s": "0x76aa8c382b73c0f516c8398e4d648d2dad411e8b6a9a7e81fbae3656bed6d78", }, "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", "collateralPosition": "10002", }, "debuggingAmounts": { - "collateralAmount": "-499999999980000", + "collateralAmount": "499999999980000", "feeAmount": "249999999990", - "syntheticAmount": "12820512820", + "syntheticAmount": "-12820512820", }, }, "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "0"}, diff --git a/x10/perpetual/order_object.py b/x10/perpetual/order_object.py index b85de57..653021c 100644 --- a/x10/perpetual/order_object.py +++ b/x10/perpetual/order_object.py @@ -43,7 +43,7 @@ def create_order_object( price: Decimal, side: OrderSide, starknet_domain: StarknetDomain, - order_type: Optional[OrderType] = OrderType.LIMIT, + order_type: OrderType = OrderType.LIMIT, post_only: bool = False, previous_order_external_id: Optional[str] = None, expire_time: Optional[datetime] = None, @@ -178,6 +178,9 @@ def __create_order_object( if not reduce_only: raise ValueError("TPSL orders must be reduce-only") + if post_only: + raise ValueError("TPSL orders must not be post-only") + if tp_sl_type == OrderTpslType.POSITION and synthetic_amount != Decimal(0): raise ValueError("`amount_of_synthetic` must be 0 for entire position TPSL orders") @@ -239,7 +242,7 @@ def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None): self_trade_protection_level=self_trade_protection_level, nonce=Decimal(nonce), cancel_id=previous_order_external_id, - settlement=settlement_data.settlement, + settlement=settlement_data.settlement if order_type != OrderType.TPSL else None, tp_sl_type=tp_sl_type, take_profit=create_tpsl_trigger_model(take_profit), stop_loss=create_tpsl_trigger_model(stop_loss), diff --git a/x10/perpetual/orders.py b/x10/perpetual/orders.py index 146b253..3dc8a4a 100644 --- a/x10/perpetual/orders.py +++ b/x10/perpetual/orders.py @@ -170,6 +170,11 @@ class OpenOrderTpslTriggerModel(X10BaseModel): class OpenOrderModel(X10BaseModel): + """ + Attributes: + price: Price of the order. If it's a TP/SL order, it will be null. + """ + id: int account_id: int external_id: str @@ -178,10 +183,11 @@ class OpenOrderModel(X10BaseModel): side: OrderSide status: OrderStatus status_reason: Optional[OrderStatusReason] = None - price: Decimal + price: Optional[Decimal] = None average_price: Optional[Decimal] = None qty: Decimal filled_qty: Optional[Decimal] = None + cancelled_qty: Optional[Decimal] = None reduce_only: bool post_only: bool payed_fee: Optional[Decimal] = None From b01bae520aab78fd311f7be5af0c0e3b5ec79e0c Mon Sep 17 00:00:00 2001 From: alex101xela Date: Tue, 17 Mar 2026 17:36:24 +0400 Subject: [PATCH 03/12] Support TPSL order type --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1c556c6..5a8895c 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 = "0.1.0" +version = "1.0.0" description = "Python client for X10 API" authors = ["X10 "] repository = "https://github.com/x10xchange/python_sdk" From 6deb2b0841e6ade519fb4f5aaaa3b96f5a3cb1b9 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Tue, 17 Mar 2026 17:38:01 +0400 Subject: [PATCH 04/12] Support TPSL order type --- examples/create_position_tpsl_order.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/create_position_tpsl_order.py b/examples/create_position_tpsl_order.py index b34a0fd..01a3f29 100644 --- a/examples/create_position_tpsl_order.py +++ b/examples/create_position_tpsl_order.py @@ -2,8 +2,6 @@ from asyncio import run from decimal import Decimal -from x10.perpetual.orders import OrderType - from examples.init_env import init_env from examples.utils import find_order_and_cancel, get_adjust_price_by_pct from x10.config import BTC_USD_MARKET @@ -15,6 +13,7 @@ OrderSide, OrderTpslType, OrderTriggerPriceType, + OrderType, TimeInForce, ) from x10.perpetual.trading_client import PerpetualTradingClient From 010827655c4c9ef39d002cf458d7c7b337ab65da Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 15:29:04 +0400 Subject: [PATCH 05/12] Support MARKET order type --- examples/create_market_order.py | 0 .../test_limit_order_object.py} | 256 ------------------ .../order_object/test_order_object_attrs.py | 80 ++++++ .../order_object/test_tpsl_order_object.py | 216 +++++++++++++++ tests/utils/test_order.py | 54 ++++ tests/utils/test_tpsl.py | 16 -- x10/perpetual/order_object.py | 8 +- x10/utils/order.py | 31 +++ x10/utils/tpsl.py | 17 -- 9 files changed, 385 insertions(+), 293 deletions(-) create mode 100644 examples/create_market_order.py rename tests/perpetual/{test_order_object.py => order_object/test_limit_order_object.py} (61%) create mode 100644 tests/perpetual/order_object/test_order_object_attrs.py create mode 100644 tests/perpetual/order_object/test_tpsl_order_object.py create mode 100644 tests/utils/test_order.py delete mode 100644 tests/utils/test_tpsl.py create mode 100644 x10/utils/order.py delete mode 100644 x10/utils/tpsl.py diff --git a/examples/create_market_order.py b/examples/create_market_order.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/perpetual/test_order_object.py b/tests/perpetual/order_object/test_limit_order_object.py similarity index 61% rename from tests/perpetual/test_order_object.py rename to tests/perpetual/order_object/test_limit_order_object.py index 9b7affb..ecaee61 100644 --- a/tests/perpetual/test_order_object.py +++ b/tests/perpetual/order_object/test_limit_order_object.py @@ -410,259 +410,3 @@ async def test_create_buy_order_with_position_tpsl( } ), ) - - -@freeze_time("2024-01-05 01:08:56.860694") -@pytest.mark.asyncio -async def test_create_buy_partial_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): - mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) - - from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object - - trading_account = create_trading_account() - btc_usd_market = create_btc_usd_market() - order_obj = create_order_object( - account=trading_account, - market=btc_usd_market, - order_type=OrderType.TPSL, - amount_of_synthetic=btc_usd_market.trading_config.min_order_size, - price=Decimal("0"), - side=OrderSide.SELL, - reduce_only=True, - expire_time=utc_now() + timedelta(days=14), - self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, - starknet_domain=TESTNET_CONFIG.starknet_domain, - tp_sl_type=OrderTpslType.ORDER, - take_profit=OrderTpslTriggerParam( - trigger_price=Decimal("49000"), - trigger_price_type=OrderTriggerPriceType.MARK, - price=Decimal("50000"), - price_type=OrderPriceType.LIMIT, - ), - stop_loss=OrderTpslTriggerParam( - trigger_price=Decimal("40000"), - trigger_price_type=OrderTriggerPriceType.MARK, - price=Decimal("39000"), - price_type=OrderPriceType.LIMIT, - ), - ) - - assert_that( - order_obj.to_api_request_json(), - equal_to( - { - "id": "2302927936354168859785231329337424926774195696889032018790365367779325970821", - "market": "BTC-USD", - "type": "TPSL", - "side": "SELL", - "qty": "0.0001", - "price": "0", - "reduceOnly": True, - "postOnly": False, - "timeInForce": "GTT", - "expiryEpochMillis": 1705626536861, - "fee": "0.0005", - "nonce": "1473459052", - "selfTradeProtectionLevel": "CLIENT", - "cancelId": None, - "settlement": None, - "trigger": None, - "tpSlType": "ORDER", - "takeProfit": { - "triggerPrice": "49000", - "triggerPriceType": "MARK", - "price": "50000", - "priceType": "LIMIT", - "settlement": { - "signature": { - "r": "0x513b8c6540b171c6e85e2992046ce697b6056793ace2ea0e46c04cba1b72aa0", - "s": "0x25684458e72e276bbaa87a6787bfab11ed4b117c4f6ffa7cea2781c13648667", - }, - "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", - "collateralPosition": "10002", - }, - "debuggingAmounts": {"collateralAmount": "5000000", "feeAmount": "2500", "syntheticAmount": "-100"}, - }, - "stopLoss": { - "triggerPrice": "40000", - "triggerPriceType": "MARK", - "price": "39000", - "priceType": "LIMIT", - "settlement": { - "signature": { - "r": "0x30227b00607a0f671db245c734a9d9152c58af0ec0fbc83ac8c50d41b6dc2e5", - "s": "0xb355fcce280b7c82c8e6b23106df57d98aba5b3616a791a4db226063693b5d", - }, - "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", - "collateralPosition": "10002", - }, - "debuggingAmounts": {"collateralAmount": "3900000", "feeAmount": "1950", "syntheticAmount": "-100"}, - }, - "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "-100"}, - "builderFee": None, - "builderId": None, - } - ), - ) - - -@freeze_time("2024-01-05 01:08:56.860694") -@pytest.mark.asyncio -async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): - mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) - - from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object - - trading_account = create_trading_account() - btc_usd_market = create_btc_usd_market() - order_obj = create_order_object( - account=trading_account, - market=btc_usd_market, - order_type=OrderType.TPSL, - amount_of_synthetic=Decimal("0"), - price=Decimal("0"), - side=OrderSide.SELL, - reduce_only=True, - expire_time=utc_now() + timedelta(days=14), - self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, - starknet_domain=TESTNET_CONFIG.starknet_domain, - tp_sl_type=OrderTpslType.POSITION, - take_profit=OrderTpslTriggerParam( - trigger_price=Decimal("49000"), - trigger_price_type=OrderTriggerPriceType.MARK, - price=Decimal("50000"), - price_type=OrderPriceType.LIMIT, - ), - stop_loss=OrderTpslTriggerParam( - trigger_price=Decimal("40000"), - trigger_price_type=OrderTriggerPriceType.MARK, - price=Decimal("39000"), - price_type=OrderPriceType.LIMIT, - ), - ) - - assert_that( - order_obj.to_api_request_json(), - equal_to( - { - "id": "2740618205716882280724217633173981437193188033910023585411792989580464995593", - "market": "BTC-USD", - "type": "TPSL", - "side": "SELL", - "qty": "0", - "price": "0", - "reduceOnly": True, - "postOnly": False, - "timeInForce": "GTT", - "expiryEpochMillis": 1705626536861, - "fee": "0.0005", - "nonce": "1473459052", - "selfTradeProtectionLevel": "CLIENT", - "cancelId": None, - "settlement": None, - "trigger": None, - "tpSlType": "POSITION", - "takeProfit": { - "triggerPrice": "49000", - "triggerPriceType": "MARK", - "price": "50000", - "priceType": "LIMIT", - "settlement": { - "signature": { - "r": "0x9ec78c951d0397e31873bb1f5ede330dffc05a6be918007fac3299a122bc37", - "s": "0x1962207a67b6b6d77216d363af31ca5ea37d371ac762aa13bd5366634337b86", - }, - "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", - "collateralPosition": "10002", - }, - "debuggingAmounts": { - "collateralAmount": "500000000000000", - "feeAmount": "250000000000", - "syntheticAmount": "-10000000000", - }, - }, - "stopLoss": { - "triggerPrice": "40000", - "triggerPriceType": "MARK", - "price": "39000", - "priceType": "LIMIT", - "settlement": { - "signature": { - "r": "0x4b5a05de5d0c07956398de50b846037250dee8cd85c35944fd97f0b3dad3e5b", - "s": "0x76aa8c382b73c0f516c8398e4d648d2dad411e8b6a9a7e81fbae3656bed6d78", - }, - "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", - "collateralPosition": "10002", - }, - "debuggingAmounts": { - "collateralAmount": "499999999980000", - "feeAmount": "249999999990", - "syntheticAmount": "-12820512820", - }, - }, - "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "0"}, - "builderFee": None, - "builderId": None, - } - ), - ) - - -@freeze_time("2024-01-05 01:08:56.860694") -@pytest.mark.asyncio -async def test_cancel_previous_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): - mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) - - from x10.perpetual.order_object import create_order_object - - trading_account = create_trading_account() - btc_usd_market = create_btc_usd_market() - order_obj = create_order_object( - account=trading_account, - market=btc_usd_market, - amount_of_synthetic=Decimal("0.00100000"), - price=Decimal("43445.11680000"), - side=OrderSide.BUY, - expire_time=utc_now() + timedelta(days=14), - previous_order_external_id="previous_custom_id", - starknet_domain=TESTNET_CONFIG.starknet_domain, - ) - - assert_that( - order_obj.to_api_request_json(), - has_entries( - { - "cancelId": equal_to("previous_custom_id"), - } - ), - ) - - -@freeze_time("2024-01-05 01:08:56.860694") -@pytest.mark.asyncio -async def test_external_order_id(mocker: MockerFixture, create_trading_account, create_btc_usd_market): - mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) - - from x10.perpetual.order_object import create_order_object - - trading_account = create_trading_account() - btc_usd_market = create_btc_usd_market() - order_obj = create_order_object( - account=trading_account, - market=btc_usd_market, - amount_of_synthetic=Decimal("0.00100000"), - price=Decimal("43445.11680000"), - side=OrderSide.BUY, - expire_time=utc_now() + timedelta(days=14), - order_external_id="custom_id", - starknet_domain=TESTNET_CONFIG.starknet_domain, - ) - - assert_that( - order_obj.to_api_request_json(), - has_entries( - { - "id": equal_to("custom_id"), - } - ), - ) diff --git a/tests/perpetual/order_object/test_order_object_attrs.py b/tests/perpetual/order_object/test_order_object_attrs.py new file mode 100644 index 0000000..bc06a1c --- /dev/null +++ b/tests/perpetual/order_object/test_order_object_attrs.py @@ -0,0 +1,80 @@ +from datetime import timedelta +from decimal import Decimal + +import pytest +from freezegun import freeze_time +from hamcrest import assert_that, equal_to, has_entries +from pytest_mock import MockerFixture + +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.orders import ( + OrderPriceType, + OrderSide, + OrderTpslType, + OrderTriggerPriceType, + OrderType, + SelfTradeProtectionLevel, +) +from x10.utils.date import utc_now + +FROZEN_NONCE = 1473459052 + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_cancel_previous_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + amount_of_synthetic=Decimal("0.00100000"), + price=Decimal("43445.11680000"), + side=OrderSide.BUY, + expire_time=utc_now() + timedelta(days=14), + previous_order_external_id="previous_custom_id", + starknet_domain=TESTNET_CONFIG.starknet_domain, + ) + + assert_that( + order_obj.to_api_request_json(), + has_entries( + { + "cancelId": equal_to("previous_custom_id"), + } + ), + ) + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_external_order_id(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + amount_of_synthetic=Decimal("0.00100000"), + price=Decimal("43445.11680000"), + side=OrderSide.BUY, + expire_time=utc_now() + timedelta(days=14), + order_external_id="custom_id", + starknet_domain=TESTNET_CONFIG.starknet_domain, + ) + + assert_that( + order_obj.to_api_request_json(), + has_entries( + { + "id": equal_to("custom_id"), + } + ), + ) diff --git a/tests/perpetual/order_object/test_tpsl_order_object.py b/tests/perpetual/order_object/test_tpsl_order_object.py new file mode 100644 index 0000000..59efdb2 --- /dev/null +++ b/tests/perpetual/order_object/test_tpsl_order_object.py @@ -0,0 +1,216 @@ +from datetime import timedelta +from decimal import Decimal + +import pytest +from freezegun import freeze_time +from hamcrest import assert_that, equal_to, has_entries +from pytest_mock import MockerFixture + +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.orders import ( + OrderPriceType, + OrderSide, + OrderTpslType, + OrderTriggerPriceType, + OrderType, + SelfTradeProtectionLevel, +) +from x10.utils.date import utc_now + +FROZEN_NONCE = 1473459052 + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_buy_partial_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.TPSL, + amount_of_synthetic=btc_usd_market.trading_config.min_order_size, + price=Decimal("0"), + side=OrderSide.SELL, + reduce_only=True, + expire_time=utc_now() + timedelta(days=14), + self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, + starknet_domain=TESTNET_CONFIG.starknet_domain, + tp_sl_type=OrderTpslType.ORDER, + take_profit=OrderTpslTriggerParam( + trigger_price=Decimal("49000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("50000"), + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=Decimal("40000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("39000"), + price_type=OrderPriceType.LIMIT, + ), + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "2302927936354168859785231329337424926774195696889032018790365367779325970821", + "market": "BTC-USD", + "type": "TPSL", + "side": "SELL", + "qty": "0.0001", + "price": "0", + "reduceOnly": True, + "postOnly": False, + "timeInForce": "GTT", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "CLIENT", + "cancelId": None, + "settlement": None, + "trigger": None, + "tpSlType": "ORDER", + "takeProfit": { + "triggerPrice": "49000", + "triggerPriceType": "MARK", + "price": "50000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x513b8c6540b171c6e85e2992046ce697b6056793ace2ea0e46c04cba1b72aa0", + "s": "0x25684458e72e276bbaa87a6787bfab11ed4b117c4f6ffa7cea2781c13648667", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": {"collateralAmount": "5000000", "feeAmount": "2500", "syntheticAmount": "-100"}, + }, + "stopLoss": { + "triggerPrice": "40000", + "triggerPriceType": "MARK", + "price": "39000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x30227b00607a0f671db245c734a9d9152c58af0ec0fbc83ac8c50d41b6dc2e5", + "s": "0xb355fcce280b7c82c8e6b23106df57d98aba5b3616a791a4db226063693b5d", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": {"collateralAmount": "3900000", "feeAmount": "1950", "syntheticAmount": "-100"}, + }, + "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "-100"}, + "builderFee": None, + "builderId": None, + } + ), + ) + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_buy_position_tpsl_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import OrderTpslTriggerParam, create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.TPSL, + amount_of_synthetic=Decimal("0"), + price=Decimal("0"), + side=OrderSide.SELL, + reduce_only=True, + expire_time=utc_now() + timedelta(days=14), + self_trade_protection_level=SelfTradeProtectionLevel.CLIENT, + starknet_domain=TESTNET_CONFIG.starknet_domain, + tp_sl_type=OrderTpslType.POSITION, + take_profit=OrderTpslTriggerParam( + trigger_price=Decimal("49000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("50000"), + price_type=OrderPriceType.LIMIT, + ), + stop_loss=OrderTpslTriggerParam( + trigger_price=Decimal("40000"), + trigger_price_type=OrderTriggerPriceType.MARK, + price=Decimal("39000"), + price_type=OrderPriceType.LIMIT, + ), + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "2740618205716882280724217633173981437193188033910023585411792989580464995593", + "market": "BTC-USD", + "type": "TPSL", + "side": "SELL", + "qty": "0", + "price": "0", + "reduceOnly": True, + "postOnly": False, + "timeInForce": "GTT", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "CLIENT", + "cancelId": None, + "settlement": None, + "trigger": None, + "tpSlType": "POSITION", + "takeProfit": { + "triggerPrice": "49000", + "triggerPriceType": "MARK", + "price": "50000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x9ec78c951d0397e31873bb1f5ede330dffc05a6be918007fac3299a122bc37", + "s": "0x1962207a67b6b6d77216d363af31ca5ea37d371ac762aa13bd5366634337b86", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": { + "collateralAmount": "500000000000000", + "feeAmount": "250000000000", + "syntheticAmount": "-10000000000", + }, + }, + "stopLoss": { + "triggerPrice": "40000", + "triggerPriceType": "MARK", + "price": "39000", + "priceType": "LIMIT", + "settlement": { + "signature": { + "r": "0x4b5a05de5d0c07956398de50b846037250dee8cd85c35944fd97f0b3dad3e5b", + "s": "0x76aa8c382b73c0f516c8398e4d648d2dad411e8b6a9a7e81fbae3656bed6d78", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "debuggingAmounts": { + "collateralAmount": "499999999980000", + "feeAmount": "249999999990", + "syntheticAmount": "-12820512820", + }, + }, + "debuggingAmounts": {"collateralAmount": "0", "feeAmount": "0", "syntheticAmount": "0"}, + "builderFee": None, + "builderId": None, + } + ), + ) diff --git a/tests/utils/test_order.py b/tests/utils/test_order.py new file mode 100644 index 0000000..5ef414a --- /dev/null +++ b/tests/utils/test_order.py @@ -0,0 +1,54 @@ +from decimal import Decimal + +from hamcrest import assert_that, equal_to +from perpetual.orders import OrderSide + +from x10.utils.order import calc_entire_position_size, get_price_with_slippage + + +def test_calc_entire_position_size(): + assert_that( + calc_entire_position_size( + price=Decimal("24580.3412"), + quantity_precision=4, + max_position_value=Decimal("10000000"), + ), + equal_to(Decimal("20341.4588")), + ) + + +def test_get_price_with_slippage(create_btc_usd_market): + # given + market = create_btc_usd_market() + slippage = Decimal("0.0075") + best_ask = Decimal("66841.6") + best_bid = Decimal("66774.7") + + # then + assert_that( + get_price_with_slippage( + OrderSide.BUY, + best_ask, + market, + slippage, + ), + equal_to(Decimal("67343")), + ) + assert_that( + get_price_with_slippage( + OrderSide.SELL, + best_bid, + market, + slippage, + ), + equal_to(Decimal("66273.8")), + ) + assert_that( + get_price_with_slippage( + OrderSide.SELL, + market.trading_config.min_price_change, + market, + slippage, + ), + equal_to(market.trading_config.min_price_change), + ) diff --git a/tests/utils/test_tpsl.py b/tests/utils/test_tpsl.py deleted file mode 100644 index ac1e016..0000000 --- a/tests/utils/test_tpsl.py +++ /dev/null @@ -1,16 +0,0 @@ -from decimal import Decimal - -from hamcrest import assert_that, equal_to - -from x10.utils.tpsl import calc_entire_position_size - - -def test_calc_entire_position_size(): - assert_that( - calc_entire_position_size( - price=Decimal("24580.3412"), - quantity_precision=4, - max_position_value=Decimal("10000000"), - ), - equal_to(Decimal("20341.4588")), - ) diff --git a/x10/perpetual/order_object.py b/x10/perpetual/order_object.py index 653021c..223aa1d 100644 --- a/x10/perpetual/order_object.py +++ b/x10/perpetual/order_object.py @@ -24,7 +24,7 @@ ) from x10.utils.date import to_epoch_millis, utc_now from x10.utils.nonce import generate_nonce -from x10.utils.tpsl import calc_entire_position_size +from x10.utils.order import calc_entire_position_size @dataclass(kw_only=True) @@ -162,14 +162,14 @@ def __create_order_object( take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, ) -> NewOrderModel: - if order_type not in [OrderType.LIMIT, OrderType.TPSL]: + if order_type not in [OrderType.LIMIT, OrderType.MARKET, OrderType.TPSL]: raise NotImplementedError(f"{order_type} order type is not supported yet") if exact_only: raise NotImplementedError("`exact_only` option is not supported yet") - if time_in_force not in TimeInForce or time_in_force == TimeInForce.FOK: - raise ValueError(f"Unexpected time in force value: {time_in_force}") + if time_in_force == TimeInForce.FOK: + raise ValueError(f"FOK time in force value is deprecated") if expire_time is None: raise ValueError("`expire_time` must be provided") diff --git a/x10/utils/order.py b/x10/utils/order.py new file mode 100644 index 0000000..6fca493 --- /dev/null +++ b/x10/utils/order.py @@ -0,0 +1,31 @@ +from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal + +from perpetual.markets import MarketModel +from perpetual.orders import OrderSide + + +def calc_entire_position_size( + *, + price: Decimal, + quantity_precision: int, + max_position_value: Decimal, +): + """ + This calculation is required to avoid a case when the position at + the time of TPSL execution has a bigger size than a signed TPSL order size. + """ + + assert price > 0, "`price` must be greater than 0" + + return (max_position_value * 50 / price).quantize(Decimal(10) ** -quantity_precision, rounding=ROUND_FLOOR) + + +def get_price_with_slippage(side: OrderSide, price: Decimal, market: MarketModel, slippage: Decimal) -> Decimal: + slippage_collateral = price * slippage + price_with_slippage = price + slippage_collateral if side == OrderSide.BUY else price - slippage_collateral + rounding_direction = ROUND_CEILING if side == OrderSide.BUY else ROUND_FLOOR + + return Decimal.max( + market.trading_config.min_price_change, + market.trading_config.round_price(price_with_slippage, rounding_direction=rounding_direction), + ) diff --git a/x10/utils/tpsl.py b/x10/utils/tpsl.py deleted file mode 100644 index 6a1b196..0000000 --- a/x10/utils/tpsl.py +++ /dev/null @@ -1,17 +0,0 @@ -from decimal import ROUND_FLOOR, Decimal - - -def calc_entire_position_size( - *, - price: Decimal, - quantity_precision: int, - max_position_value: Decimal, -): - """ - This calculation is required to avoid a case when the position at - the time of TPSL execution has a bigger size than a signed TPSL order size. - """ - - assert price > 0, "`price` must be greater than 0" - - return (max_position_value * 50 / price).quantize(Decimal(10) ** -quantity_precision, rounding=ROUND_FLOOR) From c927bf96246a68c7d782c781511cfc1b591fe403 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 16:43:33 +0400 Subject: [PATCH 06/12] Support MARKET order type --- .../order_object/test_limit_order_object.py | 3 +- .../order_object/test_market_order_object.py | 144 ++++++++++++++++++ .../order_object/test_order_object_attrs.py | 9 +- .../order_object/test_tpsl_order_object.py | 2 +- x10/perpetual/markets.py | 3 +- x10/perpetual/order_object.py | 36 +++-- x10/utils/order.py | 11 +- 7 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 tests/perpetual/order_object/test_market_order_object.py diff --git a/tests/perpetual/order_object/test_limit_order_object.py b/tests/perpetual/order_object/test_limit_order_object.py index ecaee61..afea0b1 100644 --- a/tests/perpetual/order_object/test_limit_order_object.py +++ b/tests/perpetual/order_object/test_limit_order_object.py @@ -3,7 +3,7 @@ import pytest from freezegun import freeze_time -from hamcrest import assert_that, equal_to, has_entries +from hamcrest import assert_that, equal_to from pytest_mock import MockerFixture from x10.perpetual.configuration import TESTNET_CONFIG @@ -12,7 +12,6 @@ OrderSide, OrderTpslType, OrderTriggerPriceType, - OrderType, SelfTradeProtectionLevel, ) from x10.utils.date import utc_now diff --git a/tests/perpetual/order_object/test_market_order_object.py b/tests/perpetual/order_object/test_market_order_object.py new file mode 100644 index 0000000..ab88de4 --- /dev/null +++ b/tests/perpetual/order_object/test_market_order_object.py @@ -0,0 +1,144 @@ +from datetime import timedelta +from decimal import Decimal + +import pytest +from freezegun import freeze_time +from hamcrest import assert_that, equal_to +from perpetual.orders import TimeInForce +from pytest_mock import MockerFixture +from utils.order import get_price_with_slippage + +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.orders import OrderSide, OrderType +from x10.utils.date import utc_now + +FROZEN_NONCE = 1473459052 +SLIPPAGE = Decimal("0.0075") + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_sell_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_side = OrderSide.SELL + order_price = get_price_with_slippage( + order_side, Decimal("50000"), btc_usd_market.trading_config.min_price_change, SLIPPAGE + ) + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.MARKET, + amount_of_synthetic=Decimal("0.00100000"), + price=order_price, + side=order_side, + expire_time=utc_now() + timedelta(days=14), + time_in_force=TimeInForce.IOC, + starknet_domain=TESTNET_CONFIG.starknet_domain, + nonce=FROZEN_NONCE, + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "2580220688642480426946040763258220762106230673118492731878319591751617419967", + "market": "BTC-USD", + "type": "MARKET", + "side": "SELL", + "qty": "0.00100000", + "price": "49625.0", + "reduceOnly": False, + "postOnly": False, + "timeInForce": "IOC", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "ACCOUNT", + "cancelId": None, + "settlement": { + "signature": { + "r": "0x28af719b8c9619fadd151a0f9c269058b3240ae2e08ab14e6fa15b8ea081dc6", + "s": "0x78c518768fe71c8583aee78e756de66ffed2170171fec10da03edc5e9a3d241", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "trigger": None, + "tpSlType": None, + "takeProfit": None, + "stopLoss": None, + "debuggingAmounts": {"collateralAmount": "49625000", "feeAmount": "24813", "syntheticAmount": "-1000"}, + "builderFee": None, + "builderId": None, + } + ), + ) + + +@freeze_time("2024-01-05 01:08:56.860694") +@pytest.mark.asyncio +async def test_create_buy_order(mocker: MockerFixture, create_trading_account, create_btc_usd_market): + mocker.patch("x10.utils.nonce.generate_nonce", return_value=FROZEN_NONCE) + + from x10.perpetual.order_object import create_order_object + + trading_account = create_trading_account() + btc_usd_market = create_btc_usd_market() + order_side = OrderSide.BUY + order_price = get_price_with_slippage( + order_side, Decimal("50000"), btc_usd_market.trading_config.min_price_change, SLIPPAGE + ) + order_obj = create_order_object( + account=trading_account, + market=btc_usd_market, + order_type=OrderType.MARKET, + amount_of_synthetic=Decimal("0.00100000"), + price=order_price, + side=order_side, + expire_time=utc_now() + timedelta(days=14), + time_in_force=TimeInForce.IOC, + starknet_domain=TESTNET_CONFIG.starknet_domain, + nonce=FROZEN_NONCE, + ) + + assert_that( + order_obj.to_api_request_json(), + equal_to( + { + "id": "3168487898969135762904713190835173941926260364920267758857147425797045990747", + "market": "BTC-USD", + "type": "MARKET", + "side": "BUY", + "qty": "0.00100000", + "price": "50375.0", + "reduceOnly": False, + "postOnly": False, + "timeInForce": "IOC", + "expiryEpochMillis": 1705626536861, + "fee": "0.0005", + "nonce": "1473459052", + "selfTradeProtectionLevel": "ACCOUNT", + "cancelId": None, + "settlement": { + "signature": { + "r": "0x4baa519bdf416e3563eb9c6d52d48d6adaf8926e4840a76d242c1ba62be6587", + "s": "0x87342653d415d4f47f876bcc34d11b2a16c9220b6a8e1dc7fb9225d9e7e9f9", + }, + "starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466", + "collateralPosition": "10002", + }, + "trigger": None, + "tpSlType": None, + "takeProfit": None, + "stopLoss": None, + "debuggingAmounts": {"collateralAmount": "-50375000", "feeAmount": "25188", "syntheticAmount": "1000"}, + "builderFee": None, + "builderId": None, + } + ), + ) diff --git a/tests/perpetual/order_object/test_order_object_attrs.py b/tests/perpetual/order_object/test_order_object_attrs.py index bc06a1c..e898f72 100644 --- a/tests/perpetual/order_object/test_order_object_attrs.py +++ b/tests/perpetual/order_object/test_order_object_attrs.py @@ -7,14 +7,7 @@ from pytest_mock import MockerFixture from x10.perpetual.configuration import TESTNET_CONFIG -from x10.perpetual.orders import ( - OrderPriceType, - OrderSide, - OrderTpslType, - OrderTriggerPriceType, - OrderType, - SelfTradeProtectionLevel, -) +from x10.perpetual.orders import OrderSide from x10.utils.date import utc_now FROZEN_NONCE = 1473459052 diff --git a/tests/perpetual/order_object/test_tpsl_order_object.py b/tests/perpetual/order_object/test_tpsl_order_object.py index 59efdb2..fa572c8 100644 --- a/tests/perpetual/order_object/test_tpsl_order_object.py +++ b/tests/perpetual/order_object/test_tpsl_order_object.py @@ -3,7 +3,7 @@ import pytest from freezegun import freeze_time -from hamcrest import assert_that, equal_to, has_entries +from hamcrest import assert_that, equal_to from pytest_mock import MockerFixture from x10.perpetual.configuration import TESTNET_CONFIG diff --git a/x10/perpetual/markets.py b/x10/perpetual/markets.py index 640c32b..85bf1dd 100644 --- a/x10/perpetual/markets.py +++ b/x10/perpetual/markets.py @@ -4,6 +4,7 @@ from x10.perpetual.assets import Asset from x10.utils.model import X10BaseModel +from x10.utils.order import round_price as round_order_price_util class RiskFactorConfig(X10BaseModel): @@ -77,7 +78,7 @@ def calculate_order_size_from_value( return Decimal(0) def round_price(self, price: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal: - return price.quantize(self.min_price_change, rounding=rounding_direction) + return round_order_price_util(price, self.min_price_change, rounding_direction) class L2ConfigModel(X10BaseModel): diff --git a/x10/perpetual/order_object.py b/x10/perpetual/order_object.py index 223aa1d..6cbf1d6 100644 --- a/x10/perpetual/order_object.py +++ b/x10/perpetual/order_object.py @@ -162,19 +162,14 @@ def __create_order_object( take_profit: Optional[OrderTpslTriggerParam] = None, stop_loss: Optional[OrderTpslTriggerParam] = None, ) -> NewOrderModel: - if order_type not in [OrderType.LIMIT, OrderType.MARKET, OrderType.TPSL]: - raise NotImplementedError(f"{order_type} order type is not supported yet") - - if exact_only: - raise NotImplementedError("`exact_only` option is not supported yet") - - if time_in_force == TimeInForce.FOK: - raise ValueError(f"FOK time in force value is deprecated") + def validate_market_order(): + if post_only: + raise ValueError("MARKET orders must not be post-only") - if expire_time is None: - raise ValueError("`expire_time` must be provided") + if time_in_force != TimeInForce.IOC: + raise ValueError("MARKET orders must have `time_in_force` set to IOC") - if order_type == OrderType.TPSL: + def validate_tpsl_order(): if not reduce_only: raise ValueError("TPSL orders must be reduce-only") @@ -182,11 +177,28 @@ def __create_order_object( raise ValueError("TPSL orders must not be post-only") if tp_sl_type == OrderTpslType.POSITION and synthetic_amount != Decimal(0): - raise ValueError("`amount_of_synthetic` must be 0 for entire position TPSL orders") + raise ValueError("`synthetic_amount` must be 0 for entire position TPSL orders") if price != Decimal(0): raise ValueError("`price` must be 0 for TPSL orders") + if order_type not in [OrderType.LIMIT, OrderType.MARKET, OrderType.TPSL]: + raise NotImplementedError(f"{order_type} order type is not supported yet") + + if exact_only: + raise NotImplementedError("`exact_only` option is not supported yet") + + if time_in_force == TimeInForce.FOK: + raise ValueError("FOK `time_in_force` value is deprecated") + + if expire_time is None: + raise ValueError("`expire_time` must be provided") + + if order_type == OrderType.MARKET: + validate_market_order() + elif order_type == OrderType.TPSL: + validate_tpsl_order() + if nonce is None: nonce = generate_nonce() diff --git a/x10/utils/order.py b/x10/utils/order.py index 6fca493..b5f5a06 100644 --- a/x10/utils/order.py +++ b/x10/utils/order.py @@ -1,6 +1,5 @@ from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal -from perpetual.markets import MarketModel from perpetual.orders import OrderSide @@ -20,12 +19,16 @@ def calc_entire_position_size( return (max_position_value * 50 / price).quantize(Decimal(10) ** -quantity_precision, rounding=ROUND_FLOOR) -def get_price_with_slippage(side: OrderSide, price: Decimal, market: MarketModel, slippage: Decimal) -> Decimal: +def round_price(price: Decimal, min_price_change: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal: + return price.quantize(min_price_change, rounding=rounding_direction) + + +def get_price_with_slippage(side: OrderSide, price: Decimal, min_price_change: Decimal, slippage: Decimal) -> Decimal: slippage_collateral = price * slippage price_with_slippage = price + slippage_collateral if side == OrderSide.BUY else price - slippage_collateral rounding_direction = ROUND_CEILING if side == OrderSide.BUY else ROUND_FLOOR return Decimal.max( - market.trading_config.min_price_change, - market.trading_config.round_price(price_with_slippage, rounding_direction=rounding_direction), + min_price_change, + round_price(price_with_slippage, min_price_change, rounding_direction), ) From 7e426c1688af7e905209f09aea8434ad677b461b Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 16:56:10 +0400 Subject: [PATCH 07/12] Support MARKET order type --- examples/create_market_order.py | 68 +++++++++++++++++++++++++++++++++ x10/perpetual/markets.py | 6 ++- x10/utils/order.py | 14 +++++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/examples/create_market_order.py b/examples/create_market_order.py index e69de29..50d4f17 100644 --- a/examples/create_market_order.py +++ b/examples/create_market_order.py @@ -0,0 +1,68 @@ +import logging +from asyncio import run +from decimal import Decimal + +from examples.init_env import init_env +from x10.config import BTC_USD_MARKET +from x10.perpetual.accounts import StarkPerpetualAccount +from x10.perpetual.configuration import TESTNET_CONFIG +from x10.perpetual.order_object import create_order_object +from x10.perpetual.orders import OrderSide, OrderType, TimeInForce +from x10.perpetual.trading_client import PerpetualTradingClient +from x10.utils.order import get_price_with_slippage + +LOGGER = logging.getLogger() +MARKET_NAME = BTC_USD_MARKET +ENDPOINT_CONFIG = TESTNET_CONFIG +SLIPPAGE = Decimal(0.0075) + + +async def run_example(): + env_config = init_env() + stark_account = StarkPerpetualAccount( + api_key=env_config.api_key, + public_key=env_config.public_key, + private_key=env_config.private_key, + vault=env_config.vault_id, + ) + trading_client = PerpetualTradingClient(ENDPOINT_CONFIG, stark_account) + markets_dict = await trading_client.markets_info.get_markets_dict() + market_stats = await trading_client.markets_info.get_market_statistics(market_name=MARKET_NAME) + + market = markets_dict[MARKET_NAME] + + order_side = OrderSide.SELL + order_size = market.trading_config.min_order_size + + best_market_price = market_stats.data.ask_price if order_side == OrderSide.BUY else market_stats.data.bid_price + order_price = get_price_with_slippage( + side=order_side, + price=best_market_price, + min_price_change=market.trading_config.min_price_change, + slippage=SLIPPAGE, + ) + + LOGGER.info("Creating MARKET order object for market: %s", market.name) + + new_order = create_order_object( + account=stark_account, + order_type=OrderType.MARKET, + starknet_domain=ENDPOINT_CONFIG.starknet_domain, + market=market, + side=order_side, + amount_of_synthetic=order_size, + price=order_price, + time_in_force=TimeInForce.IOC, + reduce_only=False, + post_only=False, + ) + + LOGGER.info("Placing order...") + + placed_order = await trading_client.orders.place_order(order=new_order) + + LOGGER.info("Order is placed: %s", placed_order.to_pretty_json()) + + +if __name__ == "__main__": + run(main=run_example()) diff --git a/x10/perpetual/markets.py b/x10/perpetual/markets.py index 85bf1dd..bae7d97 100644 --- a/x10/perpetual/markets.py +++ b/x10/perpetual/markets.py @@ -78,7 +78,11 @@ def calculate_order_size_from_value( return Decimal(0) def round_price(self, price: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal: - return round_order_price_util(price, self.min_price_change, rounding_direction) + return round_order_price_util( + price=price, + min_price_change=self.min_price_change, + rounding_direction=rounding_direction, + ) class L2ConfigModel(X10BaseModel): diff --git a/x10/utils/order.py b/x10/utils/order.py index b5f5a06..fa5eb74 100644 --- a/x10/utils/order.py +++ b/x10/utils/order.py @@ -1,6 +1,6 @@ from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal -from perpetual.orders import OrderSide +from x10.perpetual.orders import OrderSide def calc_entire_position_size( @@ -19,16 +19,22 @@ def calc_entire_position_size( return (max_position_value * 50 / price).quantize(Decimal(10) ** -quantity_precision, rounding=ROUND_FLOOR) -def round_price(price: Decimal, min_price_change: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal: +def round_price(*, price: Decimal, min_price_change: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal: return price.quantize(min_price_change, rounding=rounding_direction) -def get_price_with_slippage(side: OrderSide, price: Decimal, min_price_change: Decimal, slippage: Decimal) -> Decimal: +def get_price_with_slippage( + *, side: OrderSide, price: Decimal, min_price_change: Decimal, slippage: Decimal +) -> Decimal: slippage_collateral = price * slippage price_with_slippage = price + slippage_collateral if side == OrderSide.BUY else price - slippage_collateral rounding_direction = ROUND_CEILING if side == OrderSide.BUY else ROUND_FLOOR return Decimal.max( min_price_change, - round_price(price_with_slippage, min_price_change, rounding_direction), + round_price( + price=price_with_slippage, + min_price_change=min_price_change, + rounding_direction=rounding_direction, + ), ) From 29dc38899cff0092a938ad0469b098c861518065 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 16:59:51 +0400 Subject: [PATCH 08/12] Support MARKET order type --- .../order_object/test_market_order_object.py | 15 +++++--- tests/utils/test_order.py | 34 +++++++++---------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/tests/perpetual/order_object/test_market_order_object.py b/tests/perpetual/order_object/test_market_order_object.py index ab88de4..6370fdf 100644 --- a/tests/perpetual/order_object/test_market_order_object.py +++ b/tests/perpetual/order_object/test_market_order_object.py @@ -4,13 +4,12 @@ import pytest from freezegun import freeze_time from hamcrest import assert_that, equal_to -from perpetual.orders import TimeInForce from pytest_mock import MockerFixture -from utils.order import get_price_with_slippage from x10.perpetual.configuration import TESTNET_CONFIG -from x10.perpetual.orders import OrderSide, OrderType +from x10.perpetual.orders import OrderSide, OrderType, TimeInForce from x10.utils.date import utc_now +from x10.utils.order import get_price_with_slippage FROZEN_NONCE = 1473459052 SLIPPAGE = Decimal("0.0075") @@ -27,7 +26,10 @@ async def test_create_sell_order(mocker: MockerFixture, create_trading_account, btc_usd_market = create_btc_usd_market() order_side = OrderSide.SELL order_price = get_price_with_slippage( - order_side, Decimal("50000"), btc_usd_market.trading_config.min_price_change, SLIPPAGE + side=order_side, + price=Decimal("50000"), + min_price_change=btc_usd_market.trading_config.min_price_change, + slippage=SLIPPAGE, ) order_obj = create_order_object( account=trading_account, @@ -91,7 +93,10 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c btc_usd_market = create_btc_usd_market() order_side = OrderSide.BUY order_price = get_price_with_slippage( - order_side, Decimal("50000"), btc_usd_market.trading_config.min_price_change, SLIPPAGE + side=order_side, + price=Decimal("50000"), + min_price_change=btc_usd_market.trading_config.min_price_change, + slippage=SLIPPAGE, ) order_obj = create_order_object( account=trading_account, diff --git a/tests/utils/test_order.py b/tests/utils/test_order.py index 5ef414a..b9085da 100644 --- a/tests/utils/test_order.py +++ b/tests/utils/test_order.py @@ -1,8 +1,8 @@ from decimal import Decimal from hamcrest import assert_that, equal_to -from perpetual.orders import OrderSide +from x10.perpetual.orders import OrderSide from x10.utils.order import calc_entire_position_size, get_price_with_slippage @@ -17,38 +17,38 @@ def test_calc_entire_position_size(): ) -def test_get_price_with_slippage(create_btc_usd_market): +def test_get_price_with_slippage(): # given - market = create_btc_usd_market() - slippage = Decimal("0.0075") best_ask = Decimal("66841.6") best_bid = Decimal("66774.7") + min_price_change = Decimal("0.1") + slippage = Decimal("0.0075") # then assert_that( get_price_with_slippage( - OrderSide.BUY, - best_ask, - market, - slippage, + side=OrderSide.BUY, + price=best_ask, + min_price_change=min_price_change, + slippage=slippage, ), equal_to(Decimal("67343")), ) assert_that( get_price_with_slippage( - OrderSide.SELL, - best_bid, - market, - slippage, + side=OrderSide.SELL, + price=best_bid, + min_price_change=min_price_change, + slippage=slippage, ), equal_to(Decimal("66273.8")), ) assert_that( get_price_with_slippage( - OrderSide.SELL, - market.trading_config.min_price_change, - market, - slippage, + side=OrderSide.SELL, + price=min_price_change, + min_price_change=min_price_change, + slippage=slippage, ), - equal_to(market.trading_config.min_price_change), + equal_to(min_price_change), ) From 823249b564cc871bc3f5e9578953146bdf753af4 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 17:12:41 +0400 Subject: [PATCH 09/12] Support MARKET order type --- examples/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/__init__.py diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29..0000000 From 4ff9dcdacb1458fce65483730f6fbfe44e2126c8 Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 17:14:30 +0400 Subject: [PATCH 10/12] Support MARKET order type --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a8895c..2446fe3 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 = "1.0.0" +version = "1.1.0" description = "Python client for X10 API" authors = ["X10 "] repository = "https://github.com/x10xchange/python_sdk" From 46e5fcd241fa7ddde3d0ce7c1bc9878658c351bf Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 17:18:45 +0400 Subject: [PATCH 11/12] Support MARKET order type --- tests/perpetual/order_object/test_limit_order_object.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/perpetual/order_object/test_limit_order_object.py b/tests/perpetual/order_object/test_limit_order_object.py index ea39d6e..afea0b1 100644 --- a/tests/perpetual/order_object/test_limit_order_object.py +++ b/tests/perpetual/order_object/test_limit_order_object.py @@ -12,7 +12,6 @@ OrderSide, OrderTpslType, OrderTriggerPriceType, - OrderType, SelfTradeProtectionLevel, ) from x10.utils.date import utc_now From 40ed9e9bcfe4cedd7689121faa3b5f2fd923093f Mon Sep 17 00:00:00 2001 From: alex101xela Date: Fri, 27 Mar 2026 17:21:50 +0400 Subject: [PATCH 12/12] Support MARKET order type --- examples/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/__init__.py diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29