Skip to content
Open
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
136 changes: 136 additions & 0 deletions examples/placed_market_order_example_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import asyncio
import logging
import logging.config
import logging.handlers
import os
import random
from asyncio import run
from decimal import ROUND_DOWN, Decimal

from dotenv import load_dotenv

from x10.perpetual.accounts import StarkPerpetualAccount
from x10.perpetual.configuration import TESTNET_CONFIG
from x10.perpetual.orderbook import OrderBook
from x10.perpetual.orders import OrderSide, OrderType, TimeInForce
from x10.perpetual.trading_client import PerpetualTradingClient

load_dotenv()
MARKET_NAME = "BTC-USD"
ORDER_QTY = Decimal("0.0001")

API_KEY = os.getenv("X10_API_KEY")
PUBLIC_KEY = os.getenv("X10_PUBLIC_KEY")
PRIVATE_KEY = os.getenv("X10_PRIVATE_KEY")
VAULT_ID = int(os.environ["X10_VAULT_ID"])


def round_to_step(value: Decimal, step: Decimal) -> Decimal:
"""
Round a Decimal down to the nearest multiple of `step`.
This matches typical "tick size" / "min price change" and size step constraints.
"""
if step <= 0:
return value
return (value / step).to_integral_value(rounding=ROUND_DOWN) * step


def marketable_price(side: OrderSide, best_bid: Decimal, best_ask: Decimal) -> Decimal:
"""
Create a marketable price using a fixed offset from best bid/ask.
Extended requires a price field even for MARKET+IOC.
"""
if side == OrderSide.BUY:
return best_ask * (Decimal("1") + Decimal("0.002"))
return best_bid * (Decimal("1") - Decimal("0.002"))


async def clean_it(trading_client: PerpetualTradingClient):
logger = logging.getLogger("placed_order_example")
positions = await trading_client.account.get_positions()
logger.info("Positions: %s", positions.to_pretty_json())
balance = await trading_client.account.get_balance()
logger.info("Balance: %s", balance.to_pretty_json())
open_orders = await trading_client.account.get_open_orders()
await trading_client.orders.mass_cancel(order_ids=[order.id for order in open_orders.data])


async def setup_and_run():
assert API_KEY is not None
assert PUBLIC_KEY is not None
assert PRIVATE_KEY is not None
assert VAULT_ID is not None

stark_account = StarkPerpetualAccount(
vault=VAULT_ID,
private_key=PRIVATE_KEY,
public_key=PUBLIC_KEY,
api_key=API_KEY,
)
trading_client = PerpetualTradingClient(
endpoint_config=TESTNET_CONFIG,
stark_account=stark_account,
)
positions = await trading_client.account.get_positions()
for position in positions.data:
print(
f"market: {position.market} \
side: {position.side} \
size: {position.size} \
mark_price: ${position.mark_price} \
leverage: {position.leverage}"
)
print(f"consumed im: ${round((position.size * position.mark_price) / position.leverage, 2)}")

await clean_it(trading_client)

markets = await trading_client.markets_info.get_markets_dict()
market = markets.get(MARKET_NAME)
tick_size = market.trading_config.min_price_change
size_step = market.trading_config.min_order_size_change

orderbook = await OrderBook.create(endpoint_config=TESTNET_CONFIG, market_name=MARKET_NAME)

await orderbook.start_orderbook()

# Place a single MARKET+IOC order (venue requirement) using a marketable price.
# Note: this can open a real position on the venue. Use tiny size and close manually if needed.
while True:
bid = orderbook.best_bid()
ask = orderbook.best_ask()
if bid and ask:
best_bid = bid.price
best_ask = ask.price
break
await asyncio.sleep(0.5)

side = OrderSide.BUY
raw_price = marketable_price(side=side, best_bid=best_bid, best_ask=best_ask)
price = round_to_step(raw_price, tick_size)
qty = round_to_step(ORDER_QTY, size_step)
if qty <= 0:
raise ValueError(f"Order qty rounds to 0 (ORDER_QTY={ORDER_QTY}, step={size_step})")

external_id = str(random.randint(1, 10**40))
placed = await trading_client.place_order(
market_name=MARKET_NAME,
amount_of_synthetic=qty,
price=price,
side=side,
order_type=OrderType.MARKET,
time_in_force=TimeInForce.IOC,
post_only=False,
external_id=external_id,
)
placed_id = placed.data.id if placed.data is not None else None
print(
f"placed: market={MARKET_NAME} side={side.value} qty={qty} price={price} "
f"tif=IOC type=MARKET external_id={external_id} => id={placed_id}"
)

positions = await trading_client.account.get_positions()
print("positions:", positions.to_pretty_json())


if __name__ == "__main__":
run(main=setup_and_run())
8 changes: 7 additions & 1 deletion x10/perpetual/order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def create_order_object(
price: Decimal,
side: OrderSide,
starknet_domain: StarknetDomain,
order_type: OrderType = OrderType.LIMIT,
post_only: bool = False,
previous_order_external_id: Optional[str] = None,
expire_time: Optional[datetime] = None,
Expand Down Expand Up @@ -76,6 +77,7 @@ def create_order_object(
public_key=account.public_key,
exact_only=False,
expire_time=expire_time,
order_type=order_type,
post_only=post_only,
previous_order_external_id=previous_order_external_id,
order_external_id=order_external_id,
Expand Down Expand Up @@ -120,6 +122,7 @@ def __create_order_object(
starknet_domain: StarknetDomain,
exact_only: bool = False,
expire_time: Optional[datetime] = None,
order_type: OrderType = OrderType.LIMIT,
post_only: bool = False,
previous_order_external_id: Optional[str] = None,
order_external_id: Optional[str] = None,
Expand All @@ -139,6 +142,9 @@ def __create_order_object(
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 order_type not in OrderType:
raise ValueError(f"Unexpected order type value: {order_type}")

if expire_time is None:
raise ValueError("`expire_time` must be provided")

Expand Down Expand Up @@ -203,7 +209,7 @@ def __create_order_object(
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,
Expand Down
3 changes: 3 additions & 0 deletions x10/perpetual/simple_client/simple_trading_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
OpenOrderModel,
OrderSide,
OrderStatus,
OrderType,
TimeInForce,
)
from x10.perpetual.stream_client.perpetual_stream_connection import (
Expand Down Expand Up @@ -197,6 +198,7 @@ async def create_and_place_order(
amount_of_synthetic: Decimal,
price: Decimal,
side: OrderSide,
order_type: OrderType = OrderType.LIMIT,
post_only: bool = False,
previous_order_external_id: str | None = None,
external_id: str | None = None,
Expand All @@ -214,6 +216,7 @@ async def create_and_place_order(
amount_of_synthetic=amount_of_synthetic,
price=price,
side=side,
order_type=order_type,
post_only=post_only,
previous_order_external_id=previous_order_external_id,
starknet_domain=self.__endpoint_config.starknet_domain,
Expand Down
3 changes: 3 additions & 0 deletions x10/perpetual/trading_client/trading_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from x10.perpetual.orders import (
OrderSide,
OrderTpslType,
OrderType,
PlacedOrderModel,
SelfTradeProtectionLevel,
TimeInForce,
Expand Down Expand Up @@ -48,6 +49,7 @@ async def place_order(
amount_of_synthetic: Decimal,
price: Decimal,
side: OrderSide,
order_type: OrderType = OrderType.LIMIT,
post_only: bool = False,
previous_order_id=None,
expire_time: Optional[datetime] = None,
Expand Down Expand Up @@ -81,6 +83,7 @@ async def place_order(
amount_of_synthetic=amount_of_synthetic,
price=price,
side=side,
order_type=order_type,
post_only=post_only,
previous_order_external_id=previous_order_id,
expire_time=expire_time,
Expand Down