feat: implement Polymarket CLOB client wrapper with rate limiting
- Add ClobClient class wrapping py-clob-client library - Implement rate limiting (10 requests/second) with token bucket - Add retry logic with exponential backoff (3 retries) - Load API key from POLYMARKET_API_KEY environment variable - Create Market, Orderbook, Token dataclass models - Add comprehensive unit tests with mocked responses Acceptance Criteria: - [x] ClobClient class that wraps py-clob-client - [x] Loads POLYMARKET_API_KEY from environment - [x] Implements get_markets() returning all active markets - [x] Implements get_market(market_id) returning market details - [x] Implements get_orderbook(market_id) returning current book - [x] Rate limiting: max 10 requests/second with automatic throttling - [x] Retry logic: 3 retries with exponential backoff on errors - [x] Unit tests with mocked responses - [x] Type hints for all public methods Closes #2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
bc05af2fd7
commit
bb5fc086ae
@@ -0,0 +1,356 @@
|
||||
"""Tests for ClobClient wrapper."""
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from polymarket_insider_tracker.ingestor.clob_client import (
|
||||
ClobClient,
|
||||
ClobClientError,
|
||||
RateLimiter,
|
||||
RetryError,
|
||||
with_retry,
|
||||
)
|
||||
from polymarket_insider_tracker.ingestor.models import Market, Orderbook
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
"""Tests for RateLimiter."""
|
||||
|
||||
def test_acquire_sync_no_wait_first_call(self) -> None:
|
||||
"""First call should not wait."""
|
||||
limiter = RateLimiter(max_requests_per_second=10)
|
||||
start = time.monotonic()
|
||||
limiter.acquire_sync()
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
# Should be nearly instant
|
||||
assert elapsed < 0.05
|
||||
|
||||
def test_acquire_sync_enforces_rate(self) -> None:
|
||||
"""Subsequent calls should be rate limited."""
|
||||
limiter = RateLimiter(max_requests_per_second=10) # 100ms between calls
|
||||
|
||||
# First call
|
||||
limiter.acquire_sync()
|
||||
|
||||
# Second call should wait
|
||||
start = time.monotonic()
|
||||
limiter.acquire_sync()
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
# Should wait at least 90ms (allowing some tolerance)
|
||||
assert elapsed >= 0.08
|
||||
|
||||
|
||||
class TestWithRetry:
|
||||
"""Tests for retry decorator."""
|
||||
|
||||
def test_success_first_try(self) -> None:
|
||||
"""Function succeeds on first try."""
|
||||
call_count = 0
|
||||
|
||||
@with_retry(max_retries=3)
|
||||
def succeed() -> str:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return "success"
|
||||
|
||||
result = succeed()
|
||||
|
||||
assert result == "success"
|
||||
assert call_count == 1
|
||||
|
||||
def test_success_after_retries(self) -> None:
|
||||
"""Function succeeds after some retries."""
|
||||
call_count = 0
|
||||
|
||||
@with_retry(max_retries=3, base_delay=0.01)
|
||||
def succeed_eventually() -> str:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 3:
|
||||
raise ValueError("Not yet")
|
||||
return "success"
|
||||
|
||||
result = succeed_eventually()
|
||||
|
||||
assert result == "success"
|
||||
assert call_count == 3
|
||||
|
||||
def test_exhausted_retries(self) -> None:
|
||||
"""Raises RetryError after exhausting retries."""
|
||||
|
||||
@with_retry(max_retries=2, base_delay=0.01)
|
||||
def always_fails() -> str:
|
||||
raise ValueError("Always fails")
|
||||
|
||||
with pytest.raises(RetryError) as exc_info:
|
||||
always_fails()
|
||||
|
||||
assert "3 attempts failed" in str(exc_info.value)
|
||||
assert isinstance(exc_info.value.last_exception, ValueError)
|
||||
|
||||
def test_specific_exception_types(self) -> None:
|
||||
"""Only retries on specified exception types."""
|
||||
call_count = 0
|
||||
|
||||
@with_retry(max_retries=3, base_delay=0.01, retry_on=(ValueError,))
|
||||
def raise_type_error() -> str:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
raise TypeError("Not retried")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
raise_type_error()
|
||||
|
||||
# Should only be called once since TypeError is not in retry_on
|
||||
assert call_count == 1
|
||||
|
||||
|
||||
class TestClobClient:
|
||||
"""Tests for ClobClient wrapper."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_base_client(self) -> MagicMock:
|
||||
"""Create a mock base CLOB client."""
|
||||
with patch(
|
||||
"polymarket_insider_tracker.ingestor.clob_client.BaseClobClient"
|
||||
) as mock:
|
||||
yield mock.return_value
|
||||
|
||||
def test_init_defaults(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test client initialization with defaults."""
|
||||
client = ClobClient()
|
||||
|
||||
assert client._host == "https://clob.polymarket.com"
|
||||
assert client._max_retries == 3
|
||||
|
||||
def test_init_with_env_api_key(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test client reads API key from environment."""
|
||||
with patch.dict("os.environ", {"POLYMARKET_API_KEY": "test-key"}):
|
||||
client = ClobClient()
|
||||
assert client._api_key == "test-key"
|
||||
|
||||
def test_init_with_explicit_api_key(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test client uses explicitly provided API key."""
|
||||
client = ClobClient(api_key="explicit-key")
|
||||
assert client._api_key == "explicit-key"
|
||||
|
||||
def test_health_check_success(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test health check returns True when API responds OK."""
|
||||
mock_base_client.get_ok.return_value = "OK"
|
||||
|
||||
client = ClobClient()
|
||||
result = client.health_check()
|
||||
|
||||
assert result is True
|
||||
mock_base_client.get_ok.assert_called_once()
|
||||
|
||||
def test_health_check_failure(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test health check returns False on error."""
|
||||
mock_base_client.get_ok.side_effect = Exception("Connection failed")
|
||||
|
||||
client = ClobClient()
|
||||
result = client.health_check()
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_get_server_time(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test getting server time."""
|
||||
mock_base_client.get_server_time.return_value = 1704067200000
|
||||
|
||||
client = ClobClient()
|
||||
result = client.get_server_time()
|
||||
|
||||
assert result == 1704067200000
|
||||
|
||||
def test_get_markets(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching markets."""
|
||||
mock_base_client.get_simplified_markets.return_value = {
|
||||
"data": [
|
||||
{
|
||||
"condition_id": "0x123",
|
||||
"question": "Test market?",
|
||||
"tokens": [],
|
||||
"closed": False,
|
||||
},
|
||||
],
|
||||
"next_cursor": "LTE=",
|
||||
}
|
||||
|
||||
client = ClobClient()
|
||||
markets = client.get_markets()
|
||||
|
||||
assert len(markets) == 1
|
||||
assert isinstance(markets[0], Market)
|
||||
assert markets[0].condition_id == "0x123"
|
||||
|
||||
def test_get_markets_filters_closed(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test that closed markets are filtered when active_only=True."""
|
||||
mock_base_client.get_simplified_markets.return_value = {
|
||||
"data": [
|
||||
{"condition_id": "0x1", "closed": False},
|
||||
{"condition_id": "0x2", "closed": True},
|
||||
],
|
||||
"next_cursor": "LTE=",
|
||||
}
|
||||
|
||||
client = ClobClient()
|
||||
markets = client.get_markets(active_only=True)
|
||||
|
||||
assert len(markets) == 1
|
||||
assert markets[0].condition_id == "0x1"
|
||||
|
||||
def test_get_markets_includes_closed(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test that closed markets are included when active_only=False."""
|
||||
mock_base_client.get_simplified_markets.return_value = {
|
||||
"data": [
|
||||
{"condition_id": "0x1", "closed": False},
|
||||
{"condition_id": "0x2", "closed": True},
|
||||
],
|
||||
"next_cursor": "LTE=",
|
||||
}
|
||||
|
||||
client = ClobClient()
|
||||
markets = client.get_markets(active_only=False)
|
||||
|
||||
assert len(markets) == 2
|
||||
|
||||
def test_get_markets_pagination(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test that pagination is handled correctly."""
|
||||
mock_base_client.get_simplified_markets.side_effect = [
|
||||
{
|
||||
"data": [{"condition_id": "0x1"}],
|
||||
"next_cursor": "cursor2",
|
||||
},
|
||||
{
|
||||
"data": [{"condition_id": "0x2"}],
|
||||
"next_cursor": "LTE=",
|
||||
},
|
||||
]
|
||||
|
||||
client = ClobClient()
|
||||
markets = client.get_markets()
|
||||
|
||||
assert len(markets) == 2
|
||||
assert mock_base_client.get_simplified_markets.call_count == 2
|
||||
|
||||
def test_get_market(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching a single market."""
|
||||
mock_base_client.get_market.return_value = {
|
||||
"condition_id": "0xabc",
|
||||
"question": "Will it happen?",
|
||||
"tokens": [
|
||||
{"token_id": "t1", "outcome": "Yes"},
|
||||
{"token_id": "t2", "outcome": "No"},
|
||||
],
|
||||
}
|
||||
|
||||
client = ClobClient()
|
||||
market = client.get_market("0xabc")
|
||||
|
||||
assert isinstance(market, Market)
|
||||
assert market.condition_id == "0xabc"
|
||||
assert len(market.tokens) == 2
|
||||
|
||||
def test_get_market_not_found(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test error handling when market not found."""
|
||||
mock_base_client.get_market.side_effect = Exception("Not found")
|
||||
|
||||
client = ClobClient()
|
||||
|
||||
with pytest.raises(ClobClientError) as exc_info:
|
||||
client.get_market("0xnotfound")
|
||||
|
||||
assert "0xnotfound" in str(exc_info.value)
|
||||
|
||||
def test_get_orderbook(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching an orderbook."""
|
||||
mock_bid = MagicMock()
|
||||
mock_bid.price = "0.50"
|
||||
mock_bid.size = "100"
|
||||
|
||||
mock_ask = MagicMock()
|
||||
mock_ask.price = "0.52"
|
||||
mock_ask.size = "150"
|
||||
|
||||
mock_orderbook = MagicMock()
|
||||
mock_orderbook.market = "0xmarket"
|
||||
mock_orderbook.asset_id = "token123"
|
||||
mock_orderbook.tick_size = "0.01"
|
||||
mock_orderbook.bids = [mock_bid]
|
||||
mock_orderbook.asks = [mock_ask]
|
||||
|
||||
mock_base_client.get_order_book.return_value = mock_orderbook
|
||||
|
||||
client = ClobClient()
|
||||
orderbook = client.get_orderbook("token123")
|
||||
|
||||
assert isinstance(orderbook, Orderbook)
|
||||
assert orderbook.asset_id == "token123"
|
||||
assert len(orderbook.bids) == 1
|
||||
assert len(orderbook.asks) == 1
|
||||
|
||||
def test_get_orderbooks(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching multiple orderbooks."""
|
||||
mock_ob1 = MagicMock()
|
||||
mock_ob1.market = "m1"
|
||||
mock_ob1.asset_id = "t1"
|
||||
mock_ob1.tick_size = "0.01"
|
||||
mock_ob1.bids = []
|
||||
mock_ob1.asks = []
|
||||
|
||||
mock_ob2 = MagicMock()
|
||||
mock_ob2.market = "m2"
|
||||
mock_ob2.asset_id = "t2"
|
||||
mock_ob2.tick_size = "0.01"
|
||||
mock_ob2.bids = []
|
||||
mock_ob2.asks = []
|
||||
|
||||
mock_base_client.get_order_books.return_value = [mock_ob1, mock_ob2]
|
||||
|
||||
client = ClobClient()
|
||||
orderbooks = client.get_orderbooks(["t1", "t2"])
|
||||
|
||||
assert len(orderbooks) == 2
|
||||
assert all(isinstance(ob, Orderbook) for ob in orderbooks)
|
||||
|
||||
def test_get_midpoint(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching midpoint price."""
|
||||
mock_base_client.get_midpoint.return_value = {"mid": "0.55"}
|
||||
|
||||
client = ClobClient()
|
||||
result = client.get_midpoint("token123")
|
||||
|
||||
assert result == "0.55"
|
||||
|
||||
def test_get_midpoint_error(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test midpoint returns None on error."""
|
||||
mock_base_client.get_midpoint.side_effect = Exception("API error")
|
||||
|
||||
client = ClobClient()
|
||||
result = client.get_midpoint("token123")
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_get_price_buy(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching buy price."""
|
||||
mock_base_client.get_price.return_value = {"price": "0.53"}
|
||||
|
||||
client = ClobClient()
|
||||
result = client.get_price("token123", side="BUY")
|
||||
|
||||
assert result == "0.53"
|
||||
mock_base_client.get_price.assert_called_with("token123", side="BUY")
|
||||
|
||||
def test_get_price_sell(self, mock_base_client: MagicMock) -> None:
|
||||
"""Test fetching sell price."""
|
||||
mock_base_client.get_price.return_value = {"price": "0.51"}
|
||||
|
||||
client = ClobClient()
|
||||
result = client.get_price("token123", side="SELL")
|
||||
|
||||
assert result == "0.51"
|
||||
mock_base_client.get_price.assert_called_with("token123", side="SELL")
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Tests for ingestor data models."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from polymarket_insider_tracker.ingestor.models import (
|
||||
Market,
|
||||
Orderbook,
|
||||
OrderbookLevel,
|
||||
Token,
|
||||
)
|
||||
|
||||
|
||||
class TestToken:
|
||||
"""Tests for Token model."""
|
||||
|
||||
def test_from_dict_with_price(self) -> None:
|
||||
"""Test creating Token from dict with price."""
|
||||
data = {
|
||||
"token_id": "123abc",
|
||||
"outcome": "Yes",
|
||||
"price": "0.65",
|
||||
}
|
||||
token = Token.from_dict(data)
|
||||
|
||||
assert token.token_id == "123abc"
|
||||
assert token.outcome == "Yes"
|
||||
assert token.price == Decimal("0.65")
|
||||
|
||||
def test_from_dict_without_price(self) -> None:
|
||||
"""Test creating Token from dict without price."""
|
||||
data = {
|
||||
"token_id": "456def",
|
||||
"outcome": "No",
|
||||
}
|
||||
token = Token.from_dict(data)
|
||||
|
||||
assert token.token_id == "456def"
|
||||
assert token.outcome == "No"
|
||||
assert token.price is None
|
||||
|
||||
def test_frozen(self) -> None:
|
||||
"""Test that Token is immutable."""
|
||||
token = Token(token_id="123", outcome="Yes", price=Decimal("0.5"))
|
||||
with pytest.raises(AttributeError):
|
||||
token.token_id = "456" # type: ignore[misc]
|
||||
|
||||
|
||||
class TestMarket:
|
||||
"""Tests for Market model."""
|
||||
|
||||
def test_from_dict_full(self) -> None:
|
||||
"""Test creating Market from complete dict."""
|
||||
data = {
|
||||
"condition_id": "0xabc123",
|
||||
"question": "Will it rain tomorrow?",
|
||||
"description": "Market for weather prediction",
|
||||
"tokens": [
|
||||
{"token_id": "t1", "outcome": "Yes", "price": "0.7"},
|
||||
{"token_id": "t2", "outcome": "No", "price": "0.3"},
|
||||
],
|
||||
"end_date_iso": "2024-12-31T23:59:59Z",
|
||||
"active": True,
|
||||
"closed": False,
|
||||
}
|
||||
market = Market.from_dict(data)
|
||||
|
||||
assert market.condition_id == "0xabc123"
|
||||
assert market.question == "Will it rain tomorrow?"
|
||||
assert market.description == "Market for weather prediction"
|
||||
assert len(market.tokens) == 2
|
||||
assert market.tokens[0].outcome == "Yes"
|
||||
assert market.tokens[1].outcome == "No"
|
||||
assert market.end_date == datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
|
||||
assert market.active is True
|
||||
assert market.closed is False
|
||||
|
||||
def test_from_dict_minimal(self) -> None:
|
||||
"""Test creating Market from minimal dict."""
|
||||
data = {
|
||||
"condition_id": "0xdef456",
|
||||
}
|
||||
market = Market.from_dict(data)
|
||||
|
||||
assert market.condition_id == "0xdef456"
|
||||
assert market.question == ""
|
||||
assert market.description == ""
|
||||
assert market.tokens == ()
|
||||
assert market.end_date is None
|
||||
assert market.active is True
|
||||
assert market.closed is False
|
||||
|
||||
def test_from_dict_invalid_date(self) -> None:
|
||||
"""Test that invalid date is handled gracefully."""
|
||||
data = {
|
||||
"condition_id": "0x123",
|
||||
"end_date_iso": "not-a-valid-date",
|
||||
}
|
||||
market = Market.from_dict(data)
|
||||
|
||||
assert market.end_date is None
|
||||
|
||||
def test_frozen(self) -> None:
|
||||
"""Test that Market is immutable."""
|
||||
market = Market(
|
||||
condition_id="0x123",
|
||||
question="Test?",
|
||||
description="",
|
||||
tokens=(),
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
market.condition_id = "0x456" # type: ignore[misc]
|
||||
|
||||
|
||||
class TestOrderbookLevel:
|
||||
"""Tests for OrderbookLevel model."""
|
||||
|
||||
def test_from_dict(self) -> None:
|
||||
"""Test creating OrderbookLevel from dict."""
|
||||
data = {"price": "0.55", "size": "100.5"}
|
||||
level = OrderbookLevel.from_dict(data)
|
||||
|
||||
assert level.price == Decimal("0.55")
|
||||
assert level.size == Decimal("100.5")
|
||||
|
||||
def test_frozen(self) -> None:
|
||||
"""Test that OrderbookLevel is immutable."""
|
||||
level = OrderbookLevel(price=Decimal("0.5"), size=Decimal("10"))
|
||||
with pytest.raises(AttributeError):
|
||||
level.price = Decimal("0.6") # type: ignore[misc]
|
||||
|
||||
|
||||
class TestOrderbook:
|
||||
"""Tests for Orderbook model."""
|
||||
|
||||
def test_from_clob_orderbook(self) -> None:
|
||||
"""Test creating Orderbook from py-clob-client response."""
|
||||
# Create mock bid/ask objects
|
||||
mock_bid = MagicMock()
|
||||
mock_bid.price = "0.50"
|
||||
mock_bid.size = "100"
|
||||
|
||||
mock_ask = MagicMock()
|
||||
mock_ask.price = "0.52"
|
||||
mock_ask.size = "150"
|
||||
|
||||
mock_orderbook = MagicMock()
|
||||
mock_orderbook.market = "0xmarket123"
|
||||
mock_orderbook.asset_id = "token123"
|
||||
mock_orderbook.tick_size = "0.01"
|
||||
mock_orderbook.bids = [mock_bid]
|
||||
mock_orderbook.asks = [mock_ask]
|
||||
|
||||
orderbook = Orderbook.from_clob_orderbook(mock_orderbook)
|
||||
|
||||
assert orderbook.market == "0xmarket123"
|
||||
assert orderbook.asset_id == "token123"
|
||||
assert orderbook.tick_size == Decimal("0.01")
|
||||
assert len(orderbook.bids) == 1
|
||||
assert len(orderbook.asks) == 1
|
||||
assert orderbook.bids[0].price == Decimal("0.50")
|
||||
assert orderbook.asks[0].price == Decimal("0.52")
|
||||
|
||||
def test_from_clob_orderbook_empty(self) -> None:
|
||||
"""Test creating Orderbook with empty bids/asks."""
|
||||
mock_orderbook = MagicMock()
|
||||
mock_orderbook.market = "0xmarket"
|
||||
mock_orderbook.asset_id = "token"
|
||||
mock_orderbook.tick_size = "0.01"
|
||||
mock_orderbook.bids = None
|
||||
mock_orderbook.asks = []
|
||||
|
||||
orderbook = Orderbook.from_clob_orderbook(mock_orderbook)
|
||||
|
||||
assert orderbook.bids == ()
|
||||
assert orderbook.asks == ()
|
||||
|
||||
def test_best_bid(self) -> None:
|
||||
"""Test best_bid property."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(
|
||||
OrderbookLevel(Decimal("0.50"), Decimal("100")),
|
||||
OrderbookLevel(Decimal("0.49"), Decimal("50")),
|
||||
),
|
||||
asks=(),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.best_bid == Decimal("0.50")
|
||||
|
||||
def test_best_bid_empty(self) -> None:
|
||||
"""Test best_bid with no bids."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(),
|
||||
asks=(),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.best_bid is None
|
||||
|
||||
def test_best_ask(self) -> None:
|
||||
"""Test best_ask property."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(),
|
||||
asks=(
|
||||
OrderbookLevel(Decimal("0.52"), Decimal("100")),
|
||||
OrderbookLevel(Decimal("0.53"), Decimal("50")),
|
||||
),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.best_ask == Decimal("0.52")
|
||||
|
||||
def test_spread(self) -> None:
|
||||
"""Test spread calculation."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(OrderbookLevel(Decimal("0.50"), Decimal("100")),),
|
||||
asks=(OrderbookLevel(Decimal("0.52"), Decimal("100")),),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.spread == Decimal("0.02")
|
||||
|
||||
def test_spread_missing_data(self) -> None:
|
||||
"""Test spread with missing bid or ask."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(OrderbookLevel(Decimal("0.50"), Decimal("100")),),
|
||||
asks=(),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.spread is None
|
||||
|
||||
def test_midpoint(self) -> None:
|
||||
"""Test midpoint calculation."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(OrderbookLevel(Decimal("0.50"), Decimal("100")),),
|
||||
asks=(OrderbookLevel(Decimal("0.52"), Decimal("100")),),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.midpoint == Decimal("0.51")
|
||||
|
||||
def test_midpoint_missing_data(self) -> None:
|
||||
"""Test midpoint with missing data."""
|
||||
orderbook = Orderbook(
|
||||
market="0x",
|
||||
asset_id="t",
|
||||
bids=(),
|
||||
asks=(OrderbookLevel(Decimal("0.52"), Decimal("100")),),
|
||||
tick_size=Decimal("0.01"),
|
||||
)
|
||||
|
||||
assert orderbook.midpoint is None
|
||||
Reference in New Issue
Block a user