feat: implement WebSocket trade stream handler with reconnection
- Add TradeEvent dataclass for trade data from WebSocket feed - Implement TradeStreamHandler with async WebSocket streaming - Add automatic reconnection with exponential backoff (1s-30s) - Support event/market filtering for targeted subscriptions - Include connection state management and statistics tracking - Add websockets>=12.0 dependency Acceptance Criteria: - [x] TradeStreamHandler class using websockets library - [x] Connects to Polymarket WSS endpoint - [x] Subscribes to market trade channel on connection - [x] Parses trade messages into TradeEvent dataclass - [x] Implements heartbeat/ping-pong for connection health - [x] Auto-reconnects on disconnect with exponential backoff - [x] Emits events via callback pattern - [x] Logs connection state changes Closes #3 🤖 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
1299cfe0b7
commit
85010a3ea5
@@ -1,9 +1,9 @@
|
||||
"""Data models for the ingestor module."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -138,3 +138,91 @@ class Orderbook:
|
||||
if self.best_bid is not None and self.best_ask is not None:
|
||||
return (self.best_bid + self.best_ask) / 2
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TradeEvent:
|
||||
"""Represents a trade event from the Polymarket WebSocket feed.
|
||||
|
||||
This captures all the information about a single trade execution,
|
||||
including the market, wallet, trade details, and metadata.
|
||||
"""
|
||||
|
||||
# Core trade identifiers
|
||||
market_id: str # conditionId - the market/CTF condition ID
|
||||
trade_id: str # transactionHash - unique trade identifier
|
||||
wallet_address: str # proxyWallet - trader's wallet address
|
||||
|
||||
# Trade details
|
||||
side: Literal["BUY", "SELL"]
|
||||
outcome: str # Human-readable outcome (e.g., "Yes", "No")
|
||||
outcome_index: int # Index of the outcome (0 or 1)
|
||||
price: Decimal
|
||||
size: Decimal # Number of shares traded
|
||||
timestamp: datetime
|
||||
|
||||
# Asset information
|
||||
asset_id: str # ERC1155 token ID
|
||||
|
||||
# Market metadata
|
||||
market_slug: str = ""
|
||||
event_slug: str = ""
|
||||
event_title: str = ""
|
||||
|
||||
# Trader metadata (optional - may not be available for all trades)
|
||||
trader_name: str = ""
|
||||
trader_pseudonym: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_websocket_message(cls, data: dict[str, Any]) -> "TradeEvent":
|
||||
"""Create a TradeEvent from a WebSocket activity/trade message.
|
||||
|
||||
Args:
|
||||
data: The payload from a WebSocket trade message.
|
||||
|
||||
Returns:
|
||||
TradeEvent instance.
|
||||
"""
|
||||
# Parse timestamp - it's a Unix timestamp in seconds
|
||||
raw_timestamp = data.get("timestamp", 0)
|
||||
if isinstance(raw_timestamp, int):
|
||||
timestamp = datetime.fromtimestamp(raw_timestamp, tz=timezone.utc)
|
||||
else:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
|
||||
# Parse side - normalize to uppercase
|
||||
side_raw = str(data.get("side", "BUY")).upper()
|
||||
side: Literal["BUY", "SELL"] = "BUY" if side_raw == "BUY" else "SELL"
|
||||
|
||||
return cls(
|
||||
market_id=str(data.get("conditionId", "")),
|
||||
trade_id=str(data.get("transactionHash", "")),
|
||||
wallet_address=str(data.get("proxyWallet", "")),
|
||||
side=side,
|
||||
outcome=str(data.get("outcome", "")),
|
||||
outcome_index=int(data.get("outcomeIndex", 0)),
|
||||
price=Decimal(str(data.get("price", 0))),
|
||||
size=Decimal(str(data.get("size", 0))),
|
||||
timestamp=timestamp,
|
||||
asset_id=str(data.get("asset", "")),
|
||||
market_slug=str(data.get("slug", "")),
|
||||
event_slug=str(data.get("eventSlug", "")),
|
||||
event_title=str(data.get("title", "")),
|
||||
trader_name=str(data.get("name", "")),
|
||||
trader_pseudonym=str(data.get("pseudonym", "")),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_buy(self) -> bool:
|
||||
"""Return True if this is a buy trade."""
|
||||
return self.side == "BUY"
|
||||
|
||||
@property
|
||||
def is_sell(self) -> bool:
|
||||
"""Return True if this is a sell trade."""
|
||||
return self.side == "SELL"
|
||||
|
||||
@property
|
||||
def notional_value(self) -> Decimal:
|
||||
"""Return the notional value of the trade (price * size)."""
|
||||
return self.price * self.size
|
||||
|
||||
Reference in New Issue
Block a user