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
11 changes: 6 additions & 5 deletions examples/market_maker_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ async def build_markets_cache(trading_client: PerpetualTradingClient):


# flake8: noqa
async def stupid_market_maker_example():
async def on_board_example():
environment_config = STARKNET_TESTNET_CONFIG
eth_account_1: LocalAccount = Account.from_key("<you_eth_private_key>") # Replace with your actual private key
eth_account_1: LocalAccount = Account.from_key("<YOUR_ETH_PRIVATE_KEY>")
onboarding_client = UserClient(endpoint_config=environment_config, l1_private_key=eth_account_1.key.hex)
root_account = await onboarding_client.onboard()

Expand Down Expand Up @@ -51,8 +51,8 @@ async def stupid_market_maker_example():
markets=[market.name],
)

if "SOL" in market.name:
print("Skipping SOL market")
if "BTC" not in market.name: # Example for a specific market
print(f"Skipping {market.name} market")
continue

mark_price = market.market_stats.mark_price
Expand Down Expand Up @@ -109,8 +109,9 @@ async def stupid_market_maker_example():
)
except Exception as e:
print(f"Error: {e}")
await asyncio.sleep(30)
except Exception as e:
print(f"Error: {e}")


asyncio.run(stupid_market_maker_example())
asyncio.run(on_board_example())
5 changes: 3 additions & 2 deletions examples/simple_client_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ async def setup_and_run():
api_key=api_key,
)

client = BlockingTradingClient(endpoint_config=TESTNET_CONFIG, account=stark_account)
client = await BlockingTradingClient.create(endpoint_config=TESTNET_CONFIG, account=stark_account)

placed_order = await client.create_and_place_order(
amount_of_synthetic=Decimal("1"),
price=Decimal("62133.6"),
market_name="BTC-USD",
side=OrderSide.BUY,
post_only=False,
external_id="test_order_123",
)

print(placed_order)

await client.cancel_order(placed_order.id)
await client.cancel_order(order_external_id=placed_order.external_id)


if __name__ == "__main__":
Expand Down
137 changes: 137 additions & 0 deletions examples/simple_market_maker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import asyncio
import math
import random
import traceback
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
from typing import List, Tuple

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

from x10.perpetual.accounts import StarkPerpetualAccount
from x10.perpetual.configuration import STARKNET_TESTNET_CONFIG
from x10.perpetual.orderbook import OrderBook
from x10.perpetual.orders import OrderSide
from x10.perpetual.simple_client.simple_trading_client import BlockingTradingClient
from x10.perpetual.trading_client.trading_client import PerpetualTradingClient
from x10.perpetual.user_client.user_client import UserClient


async def build_markets_cache(trading_client: PerpetualTradingClient):
markets = await trading_client.markets_info.get_markets()
assert markets.data is not None
return {m.name: m for m in markets.data}


# flake8: noqa
async def on_board_example():
environment_config = STARKNET_TESTNET_CONFIG
eth_account_1: LocalAccount = Account.from_key("<YOUR_ETH_PRIVATE_KEY>")
onboarding_client = UserClient(endpoint_config=environment_config, l1_private_key=eth_account_1.key.hex)
root_account = await onboarding_client.onboard()
trading_key = await onboarding_client.create_account_api_key(root_account.account, "trading_key")

root_trading_client = await BlockingTradingClient.create(
environment_config,
StarkPerpetualAccount(
vault=root_account.account.l2_vault,
private_key=root_account.l2_key_pair.private_hex,
public_key=root_account.l2_key_pair.public_hex,
api_key=trading_key,
),
)

print(f"User vault: {root_account.account.l2_vault}")
print(f"User pub: {root_account.l2_key_pair.public_hex}")
print(f"User priv: {root_account.l2_key_pair.private_hex}")

available_markets = await root_trading_client.get_markets()
target_order_amount_usd = Decimal("50")
market = available_markets["BTC-USD"]
await root_trading_client.mass_cancel(markets=[market.name])
buy_orders_infoz: List[Tuple[str, Decimal] | None] = [None, None, None]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't we reuse OrderBookEntry?

sell_order_infoz: List[Tuple[str, Decimal] | None] = [None, None, None]

pending_sell_job: asyncio.Future[list[BaseException | None]] | None = None
pending_buy_job: asyncio.Future[list[BaseException | None]] | None = None

def update_sell_orders(best_ask: Decimal | None):
print(f"Best ask: {best_ask}")
nonlocal pending_sell_job
if pending_sell_job and not pending_sell_job.done():
print("Pending sell job is still running, skipping update.")
return
tasks = [
asyncio.create_task(place_order(best_ask, idx, OrderSide.SELL)) for idx in range(len(sell_order_infoz))
]
pending_sell_job = asyncio.gather(*tasks, return_exceptions=True)

def update_buy_orders(best_bid: Decimal | None):
print(f"Best bid: {best_bid}")
nonlocal pending_buy_job
if pending_buy_job and not pending_buy_job.done():
print("Pending buy job is still running, skipping update.")
return
tasks = [asyncio.create_task(place_order(best_bid, idx, OrderSide.BUY)) for idx in range(len(buy_orders_infoz))]
pending_buy_job = asyncio.gather(*tasks, return_exceptions=True)

async def place_order(best_price: Decimal | None, idx: int, side: OrderSide):
order_holders = sell_order_infoz if side == OrderSide.SELL else buy_orders_infoz
try:
previous_order_info = order_holders[idx]
if previous_order_info is not None:
previous_order_id, previous_order_price = previous_order_info
else:
previous_order_id, previous_order_price = None, None
print(f"Previous order ID: {previous_order_id}, Price: {previous_order_price}")

if previous_order_id and previous_order_price and previous_order_price == best_price:
print(f"Order at index {idx} with price {previous_order_price} is at top of the book, cancelling.")
await root_trading_client.cancel_order(previous_order_id)
order_holders[idx] = None
previous_order_id = None

if best_price is None:
print(f"No best price available for index {idx}, skipping {side} order placement.")
return
new_external_id = (
f"mm_{side}_order_{idx}_{random.randint(1,10000000000000000000000000000000000000000000000000000000000)}"
)

adjustment_direction = Decimal(1 if side == OrderSide.SELL else -1)

adjusted_price = market.trading_config.round_price(
best_price * (Decimal(1) + (adjustment_direction * (Decimal(1) + Decimal(idx)) / Decimal(400))),
ROUND_CEILING,
)
print(f"Placing {side} order at {adjusted_price} for index {idx}")
synthetic_amount = market.trading_config.calculate_order_size_from_value(
target_order_amount_usd, adjusted_price
)
place_response = await root_trading_client.create_and_place_order(
market_name=market.name,
amount_of_synthetic=synthetic_amount,
price=adjusted_price,
side=side,
post_only=True,
previous_order_external_id=previous_order_id,
external_id=new_external_id,
)
order_holders[idx] = (new_external_id, adjusted_price)
print(f"Placed sell order at {adjusted_price} with ID {place_response.external_id}")
except Exception as e:
print(traceback.format_exc())

order_book = await OrderBook.create(
STARKNET_TESTNET_CONFIG,
market_name="BTC-USD",
start=True,
best_ask_change_callback=lambda best_ask: update_buy_orders(best_ask.price if best_ask else None),
best_bid_change_callback=lambda best_bid: update_sell_orders(best_bid.price if best_bid else None),
)

while True:
await asyncio.sleep(1)


asyncio.run(on_board_example())
2 changes: 1 addition & 1 deletion tests/perpetual/test_order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def test_cancel_previous_order(mocker: MockerFixture, create_trading_accou
price=Decimal("43445.11680000"),
side=OrderSide.BUY,
expire_time=utc_now() + timedelta(days=14),
previous_order_id="previous_custom_id",
previous_order_external_id="previous_custom_id",
starknet_domain=STARKNET_TESTNET_CONFIG.starknet_domain,
)

Expand Down
2 changes: 1 addition & 1 deletion x10/perpetual/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class EndpointConfig:
STARKNET_TESTNET_CONFIG = EndpointConfig(
chain_rpc_url="https://rpc.sepolia.org",
api_base_url="https://api.starknet.sepolia.extended.exchange/api/v1",
stream_url="wss://api.starknet.sepolia.extended.exchange/stream.extended.exchange/v1",
stream_url="wss://starknet.sepolia.extended.exchange/stream.extended.exchange/v1",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for UI, pls revert

onboarding_url="https://api.starknet.sepolia.extended.exchange",
signing_domain="starknet.sepolia.extended.exchange",
collateral_asset_contract="0x0C9165046063B7bCD05C6924Bbe05ed535c140a1",
Expand Down
3 changes: 3 additions & 0 deletions x10/perpetual/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ def calculate_order_size_from_value(
else:
return Decimal(0)

def round_price(self, price: Decimal, rounding_direction: str = ROUND_CEILING) -> Decimal:
return self.price_precision * (price / self.price_precision).to_integral_exact(rounding_direction)


class L2ConfigModel(X10BaseModel):
type: str
Expand Down
4 changes: 2 additions & 2 deletions x10/perpetual/order_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def create_order_object(
side: OrderSide,
starknet_domain: StarknetDomain,
post_only: bool = False,
previous_order_id: Optional[str] = None,
previous_order_external_id: Optional[str] = None,
expire_time: Optional[datetime] = None,
order_external_id: Optional[str] = None,
time_in_force: TimeInForce = TimeInForce.GTT,
Expand Down Expand Up @@ -66,7 +66,7 @@ def create_order_object(
exact_only=False,
expire_time=expire_time,
post_only=post_only,
previous_order_external_id=previous_order_id,
previous_order_external_id=previous_order_external_id,
order_external_id=order_external_id,
time_in_force=time_in_force,
self_trade_protection_level=self_trade_protection_level,
Expand Down
37 changes: 23 additions & 14 deletions x10/perpetual/orderbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ class OrderBook:
async def create(
endpoint_config: EndpointConfig,
market_name: str,
best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
best_ask_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
best_bid_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
start=False,
depth_1: bool = False,
) -> "OrderBook":
ob = OrderBook(
endpoint_config,
Expand All @@ -49,8 +50,8 @@ def __init__(
self,
endpoint_config: EndpointConfig,
market_name: str,
best_ask_change_callback: Callable[[OrderBookEntry], None] | None = None,
best_bid_change_callback: Callable[[OrderBookEntry], None] | None = None,
best_ask_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
best_bid_change_callback: Callable[[OrderBookEntry | None], None] | None = None,
) -> None:
self.__stream_client = PerpetualStreamClient(api_url=endpoint_config.stream_url)
self.__market_name = market_name
Expand All @@ -64,7 +65,7 @@ def update_orderbook(self, data: OrderbookUpdateModel):
best_bid_before_update = self.best_bid()
for bid in data.bid:
if bid.price in self._bid_prices:
existing_bid_entry: OrderBookEntry = self._bid_prices.get(bid.price)
existing_bid_entry: OrderBookEntry = self._bid_prices[bid.price]
existing_bid_entry.amount = existing_bid_entry.amount + bid.qty
if existing_bid_entry.amount == 0:
del self._bid_prices[bid.price]
Expand All @@ -74,14 +75,14 @@ def update_orderbook(self, data: OrderbookUpdateModel):
amount=bid.qty,
)
now_best_bid = self.best_bid()
if now_best_bid and best_bid_before_update != now_best_bid:
if best_bid_before_update != now_best_bid:
if self.best_bid_change_callback:
self.best_bid_change_callback(now_best_bid)

best_ask_before_update = self.best_ask()
for ask in data.ask:
if ask.price in self._ask_prices:
existing_ask_entry: OrderBookEntry = self._ask_prices.get(ask.price)
existing_ask_entry: OrderBookEntry = self._ask_prices[ask.price]
existing_ask_entry.amount = existing_ask_entry.amount + ask.qty
if existing_ask_entry.amount == 0:
del self._ask_prices[ask.price]
Expand All @@ -91,7 +92,7 @@ def update_orderbook(self, data: OrderbookUpdateModel):
amount=ask.qty,
)
now_best_ask = self.best_ask()
if now_best_ask and best_ask_before_update != now_best_ask:
if best_ask_before_update != now_best_ask:
if self.best_ask_change_callback:
self.best_ask_change_callback(now_best_ask)

Expand All @@ -111,12 +112,20 @@ async def start_orderbook(self) -> asyncio.Task:
loop = asyncio.get_running_loop()

async def inner():
async with self.__stream_client.subscribe_to_orderbooks(self.__market_name) as stream:
async for event in stream:
if event.type == StreamDataType.SNAPSHOT.value:
self.init_orderbook(event.data)
elif event.type == StreamDataType.DELTA.value:
self.update_orderbook(event.data)
while True:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for reconnection?

print(f"Connecting to orderbook stream for market: {self.__market_name}")
async with self.__stream_client.subscribe_to_orderbooks(self.__market_name) as stream:
async for event in stream:
if event.type == StreamDataType.SNAPSHOT.value:
if not event.data:
continue
self.init_orderbook(event.data)
elif event.type == StreamDataType.DELTA.value:
if not event.data:
continue
self.update_orderbook(event.data)
print("Orderbook stream disconnected, reconnecting...")
await asyncio.sleep(1)

self.__task = loop.create_task(inner())
return self.__task
Expand Down
Loading