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
83 changes: 83 additions & 0 deletions examples/create_limit_order_with_position_tpsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import logging
from asyncio import run

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_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 LIMIT order object with TPSL for market: %s", market.name)

new_order = create_order_object(
account=stark_account,
starknet_domain=ENDPOINT_CONFIG.starknet_domain,
market=market,
side=OrderSide.BUY,
amount_of_synthetic=order_size,
price=market.trading_config.round_price(order_price),
time_in_force=TimeInForce.GTT,
reduce_only=False,
post_only=True,
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,
),
)

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())
115 changes: 113 additions & 2 deletions tests/perpetual/test_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from x10.perpetual.orders import (
OrderPriceType,
OrderSide,
OrderTpslType,
OrderTriggerPriceType,
SelfTradeProtectionLevel,
)
Expand Down Expand Up @@ -196,7 +197,7 @@ async def test_create_buy_order(mocker: MockerFixture, create_trading_account, c

@freeze_time("2024-01-05 01:08:56.860694")
@pytest.mark.asyncio
async def test_create_buy_order_with_tpsl(mocker: MockerFixture, create_trading_account, create_btc_usd_market):
async def test_create_buy_order_with_order_tpsl(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
Expand All @@ -212,6 +213,7 @@ async def test_create_buy_order_with_tpsl(mocker: MockerFixture, create_trading_
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,
Expand Down Expand Up @@ -253,7 +255,7 @@ async def test_create_buy_order_with_tpsl(mocker: MockerFixture, create_trading_
"collateralPosition": "10002",
},
"trigger": None,
"tpSlType": None,
"tpSlType": "ORDER",
"takeProfit": {
"triggerPrice": "49000",
"triggerPriceType": "MARK",
Expand Down Expand Up @@ -300,6 +302,115 @@ async def test_create_buy_order_with_tpsl(mocker: MockerFixture, create_trading_
)


@freeze_time("2024-01-05 01:08:56.860694")
@pytest.mark.asyncio
async def test_create_buy_order_with_position_tpsl(
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,
amount_of_synthetic=Decimal("0.00100000"),
price=Decimal("43445.11680000"),
side=OrderSide.BUY,
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": "2495374044666992118771096772295242242651427695217815113349321039194683172848",
"market": "BTC-USD",
"type": "LIMIT",
"side": "BUY",
"qty": "0.00100000",
"price": "43445.11680000",
"reduceOnly": False,
"postOnly": False,
"timeInForce": "GTT",
"expiryEpochMillis": 1705626536861,
"fee": "0.0005",
"nonce": "1473459052",
"selfTradeProtectionLevel": "CLIENT",
"cancelId": None,
"settlement": {
"signature": {
"r": "0xa55625c7d5f1b85bed22556fc805224b8363074979cf918091d9ddb1403e13",
"s": "0x504caf634d859e643569743642ccf244434322859b2421d76f853af43ae7a46",
},
"starkKey": "0x61c5e7e8339b7d56f197f54ea91b776776690e3232313de0f2ecbd0ef76f466",
"collateralPosition": "10002",
},
"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": "-43445117", "feeAmount": "21723", "syntheticAmount": "1000"},
"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):
Expand Down
16 changes: 16 additions & 0 deletions tests/utils/test_tpsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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")),
)
83 changes: 46 additions & 37 deletions x10/perpetual/order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from x10.perpetual.fees import DEFAULT_FEES, TradingFeeModel
from x10.perpetual.markets import MarketModel
from x10.perpetual.order_object_settlement import (
OrderSettlementData,
SettlementDataCtx,
create_order_settlement_data,
)
Expand All @@ -25,6 +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


@dataclass(kw_only=True)
Expand Down Expand Up @@ -92,7 +92,31 @@ def create_order_object(
)


def __create_order_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam, settlement_data: OrderSettlementData):
def __create_order_tpsl_trigger_model(
*,
trigger_param: OrderTpslTriggerParam,
side: OrderSide,
synthetic_amount: Decimal,
tp_sl_type: OrderTpslType,
market: MarketModel,
settlement_data_ctx: SettlementDataCtx,
):
settlement_synthetic_amount = (
synthetic_amount
if tp_sl_type == OrderTpslType.ORDER
else calc_entire_position_size(
price=trigger_param.price,
quantity_precision=market.trading_config.quantity_precision,
max_position_value=market.trading_config.max_position_value,
)
)
settlement_data = create_order_settlement_data(
side=__get_opposite_side(side),
synthetic_amount=settlement_synthetic_amount,
price=trigger_param.price,
ctx=settlement_data_ctx,
)

return CreateOrderTpslTriggerModel(
trigger_price=trigger_param.trigger_price,
trigger_price_type=trigger_param.trigger_price_type,
Expand Down Expand Up @@ -145,14 +169,6 @@ def __create_order_object(
if exact_only:
raise NotImplementedError("`exact_only` option is not supported yet")

if tp_sl_type == OrderTpslType.POSITION:
raise NotImplementedError("`POSITION` TPSL type is not supported yet")

if (take_profit and take_profit.price_type == OrderPriceType.MARKET) or (
stop_loss and stop_loss.price_type == OrderPriceType.MARKET
):
raise NotImplementedError("TPSL `MARKET` price type is not supported yet")

if nonce is None:
nonce = generate_nonce()

Expand All @@ -172,32 +188,25 @@ def __create_order_object(
settlement_data = create_order_settlement_data(
side=side, synthetic_amount=synthetic_amount, price=price, ctx=settlement_data_ctx
)
tp_trigger_model = (
__create_order_tpsl_trigger_model(
take_profit,
create_order_settlement_data(
side=__get_opposite_side(side),
synthetic_amount=synthetic_amount,
price=take_profit.price,
ctx=settlement_data_ctx,
),
)
if take_profit
else None
)
sl_trigger_model = (
__create_order_tpsl_trigger_model(
stop_loss,
create_order_settlement_data(
side=__get_opposite_side(side),
synthetic_amount=synthetic_amount,
price=stop_loss.price,
ctx=settlement_data_ctx,
),

def create_tpsl_trigger_model(trigger_param: OrderTpslTriggerParam | None):
if not trigger_param:
return None

if tp_sl_type is None:
raise ValueError("`tp_sl_type` must be provided if `take_profit` or `stop_loss` is specified")

if trigger_param.price_type == OrderPriceType.MARKET:
raise NotImplementedError("TPSL `MARKET` price type is not supported yet")

return __create_order_tpsl_trigger_model(
trigger_param=trigger_param,
side=side,
synthetic_amount=synthetic_amount,
tp_sl_type=tp_sl_type,
market=market,
settlement_data_ctx=settlement_data_ctx,
)
if stop_loss
else None
)

order_id = str(settlement_data.order_hash) if order_external_id is None else order_external_id
order = NewOrderModel(
Expand All @@ -216,8 +225,8 @@ def __create_order_object(
cancel_id=previous_order_external_id,
settlement=settlement_data.settlement,
tp_sl_type=tp_sl_type,
take_profit=tp_trigger_model,
stop_loss=sl_trigger_model,
take_profit=create_tpsl_trigger_model(take_profit),
stop_loss=create_tpsl_trigger_model(stop_loss),
debugging_amounts=settlement_data.debugging_amounts,
builderFee=builder_fee,
builderId=builder_id,
Expand Down
17 changes: 17 additions & 0 deletions x10/utils/tpsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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)
Loading