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
5 changes: 3 additions & 2 deletions examples/01_create_limit_order.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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 MAINNET_CONFIG
from x10.perpetual.order_object import create_order_object
from x10.perpetual.orders import OrderSide, TimeInForce
from x10.perpetual.trading_client import PerpetualTradingClient

from examples.init_env import init_env
from examples.utils import find_order_and_cancel, get_adjust_price_by_pct

LOGGER = logging.getLogger()
MARKET_NAME = ETH_USD_MARKET
ENDPOINT_CONFIG = MAINNET_CONFIG
Expand Down
5 changes: 3 additions & 2 deletions examples/02_create_limit_order_with_partial_tpsl.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
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 MAINNET_CONFIG
Expand All @@ -16,6 +14,9 @@
)
from x10.perpetual.trading_client import PerpetualTradingClient

from examples.init_env import init_env
from examples.utils import find_order_and_cancel, get_adjust_price_by_pct

LOGGER = logging.getLogger()
MARKET_NAME = ETH_USD_MARKET
ENDPOINT_CONFIG = MAINNET_CONFIG
Expand Down
3 changes: 2 additions & 1 deletion examples/03_subscribe_to_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from asyncio import run
from signal import SIGINT, SIGTERM

from examples.init_env import init_env
from x10.config import ETH_USD_MARKET
from x10.perpetual.configuration import MAINNET_CONFIG
from x10.perpetual.stream_client import PerpetualStreamClient

from examples.init_env import init_env

LOGGER = logging.getLogger()
MARKET_NAME = ETH_USD_MARKET
ENDPOINT_CONFIG = MAINNET_CONFIG
Expand Down
5 changes: 3 additions & 2 deletions examples/04_create_limit_order_with_builder.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import logging.handlers
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 MAINNET_CONFIG
from x10.perpetual.order_object import create_order_object
from x10.perpetual.orders import OrderSide, TimeInForce
from x10.perpetual.trading_client import PerpetualTradingClient

from examples.init_env import init_env
from examples.utils import find_order_and_cancel, get_adjust_price_by_pct

LOGGER = logging.getLogger()
MARKET_NAME = ETH_USD_MARKET
ENDPOINT_CONFIG = MAINNET_CONFIG
Expand Down
3 changes: 2 additions & 1 deletion examples/05_bridged_withdrawal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
from asyncio import run
from decimal import Decimal

from examples.init_env import init_env
from x10.perpetual.accounts import StarkPerpetualAccount
from x10.perpetual.configuration import MAINNET_CONFIG
from x10.perpetual.trading_client import PerpetualTradingClient

from examples.init_env import init_env

LOGGER = logging.getLogger()
ENDPOINT_CONFIG = MAINNET_CONFIG

Expand Down
1 change: 0 additions & 1 deletion examples/onboarding_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from eth_account import Account
from eth_account.signers.local import LocalAccount

from x10.perpetual.accounts import StarkPerpetualAccount
from x10.perpetual.configuration import TESTNET_CONFIG
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
Expand Down
135 changes: 135 additions & 0 deletions examples/placed_market_order_example_simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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())
1 change: 0 additions & 1 deletion examples/placed_order_example_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from decimal import 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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pyyaml = ">=6.0.1"
sortedcontainers = ">=2.4.0"
strenum = "^0.4.15"
tenacity = "^9.1.2"
websockets = ">=12.0,<14.0"
websockets = ">=12.0,<16.0"

[tool.poetry.group.dev.dependencies]
black = "==23.12.0"
Expand Down
1 change: 0 additions & 1 deletion tests/perpetual/test_onboarding_payload.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime

from eth_account import Account

from x10.perpetual.user_client.onboarding import get_l2_keys_from_l1_account


Expand Down
96 changes: 95 additions & 1 deletion tests/perpetual/test_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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,
Expand Down Expand Up @@ -358,3 +357,98 @@ async def test_external_order_id(mocker: MockerFixture, create_trading_account,
}
),
)


@freeze_time("2024-01-05 01:08:56.860694")
@pytest.mark.asyncio
async def test_post_only_uses_maker_fee_by_default(
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.SELL,
expire_time=utc_now() + timedelta(days=14),
post_only=True,
starknet_domain=TESTNET_CONFIG.starknet_domain,
)

assert_that(
order_obj.to_api_request_json(),
has_entries(
{
"postOnly": equal_to(True),
"fee": equal_to("0.0002"),
}
),
)


@freeze_time("2024-01-05 01:08:56.860694")
@pytest.mark.asyncio
async def test_max_fee_rate_overrides_payload_fee_and_settlement(
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),
max_fee_rate=Decimal("0.0001"),
starknet_domain=TESTNET_CONFIG.starknet_domain,
)

payload = order_obj.to_api_request_json()
assert_that(payload, has_entries({"fee": equal_to("0.0001")}))
fee_amount = Decimal(payload["debuggingAmounts"]["feeAmount"])
# Lower max-fee cap should lower settlement fee amount from default (21723)
assert fee_amount > 0
assert fee_amount < Decimal("21723")


@freeze_time("2024-01-05 01:08:56.860694")
@pytest.mark.asyncio
async def test_fee_alias_maps_to_max_fee_rate(
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),
fee=Decimal("0.00015"),
starknet_domain=TESTNET_CONFIG.starknet_domain,
)

assert_that(
order_obj.to_api_request_json(),
has_entries(
{
"fee": equal_to("0.00015"),
}
),
)
Loading