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
42 changes: 39 additions & 3 deletions lighter/paper_client/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from threading import RLock
from typing import Any, Dict, List, Mapping, Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

from lighter.api.order_api import OrderApi
from lighter.api_client import ApiClient
Expand Down Expand Up @@ -73,14 +74,20 @@ def __init__(
self._account_tier = account_tier

self.api_client = api_client
self.order_api = order_api if order_api is not None else OrderApi(api_client)
self.order_api = (
order_api
if order_api is not None
else OrderApi(_ReadOnlyApiClientProxy(api_client))
)
self.order_book_limit = order_book_limit
self.account = new_paper_account(initial_collateral_usdc)
self.market_configs: Dict[int, MarketConfig] = {}
self.order_books: Dict[int, InMemoryOrderBook] = {}
raw_ws_url = ws_url if ws_url is not None else self._default_ws_url(api_client, ws_path)
separator = "&" if "?" in raw_ws_url else "?"
self.ws_url = f"{raw_ws_url}{separator}encoding=json"
self.ws_url = _append_query_params(
raw_ws_url,
[("encoding", "json"), ("readonly", "true")],
)
self.initial_snapshot_timeout = initial_snapshot_timeout
self._live_listeners: Dict[int, PaperOrderBookListener] = {}
self._state_lock = asyncio.Lock()
Expand Down Expand Up @@ -353,3 +360,32 @@ def _validate_perp_market_id(market_id: int) -> None:
"paper trading only supports perp markets "
f"(market_id < 2048), got {market_id}"
)


def _with_readonly(query_params):
query_params = list(query_params or [])
if not any(name == "readonly" for name, _ in query_params):
query_params.append(("readonly", "true"))
return query_params


def _append_query_params(url: str, query_params) -> str:
parts = urlsplit(url)
params = parse_qsl(parts.query, keep_blank_values=True)
names = {name for name, _ in params}
params.extend((name, value) for name, value in query_params if name not in names)
return urlunsplit(parts._replace(query=urlencode(params)))


class _ReadOnlyApiClientProxy:
"""Lightweight proxy for ApiClient, ensuring readonly=true to generated REST URLs."""

def __init__(self, api_client: Optional[ApiClient]) -> None:
self._api_client = api_client if api_client is not None else ApiClient.get_default()

def __getattr__(self, name: str) -> Any:
return getattr(self._api_client, name)

def param_serialize(self, *args, **kwargs):
kwargs["query_params"] = _with_readonly(kwargs.get("query_params"))
return self._api_client.param_serialize(*args, **kwargs)
50 changes: 50 additions & 0 deletions test/paper_client/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import unittest

from lighter.api_client import ApiClient
from lighter.configuration import Configuration
from lighter.paper_client.accounting import apply_fill
from lighter.paper_client import (
AccountTier,
Expand All @@ -13,6 +15,54 @@


class TestPaperClient(unittest.IsolatedAsyncioTestCase):
async def test_paper_client_rest_requests_include_readonly(self) -> None:
api_client = ApiClient(Configuration(host="https://example.test"))
client = PaperClient(api_client, 5000.0)

try:
_, orders_url, _, _, _ = client.order_api._order_book_orders_serialize(
0, 100, None, None, None, 0
)
_, details_url, _, _, _ = client.order_api._order_book_details_serialize(
0, None, None, None, None, 0
)
finally:
await api_client.close()

self.assertEqual(
orders_url,
"https://example.test/api/v1/orderBookOrders"
"?market_id=0&limit=100&readonly=true",
)
self.assertEqual(
details_url,
"https://example.test/api/v1/orderBookDetails"
"?market_id=0&readonly=true",
)

def test_paper_client_ws_url_includes_readonly(self) -> None:
client = PaperClient(
None,
5000.0,
order_api=FakeOrderApi(),
ws_url="wss://example.test/stream",
)
self.assertEqual(
client.ws_url,
"wss://example.test/stream?encoding=json&readonly=true",
)

client = PaperClient(
None,
5000.0,
order_api=FakeOrderApi(),
ws_url="wss://example.test/stream?readonly=true",
)
self.assertEqual(
client.ws_url,
"wss://example.test/stream?readonly=true&encoding=json",
)

async def test_track_market_snapshot_and_market_buy_then_sell(self) -> None:
order_api = FakeOrderApi()
order_api.books[0] = book(
Expand Down
Loading