Files
polymarket-insider-tracker/tests/ingestor/test_models.py
T
Patrick SelamyandClaude Opus 4.5 bb5fc086ae 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>
2026-01-04 14:40:43 -05:00

270 lines
8.3 KiB
Python

"""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