Files

245 lines
7.8 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""Analyze a Polymarket order book for a given token ID.
Calculates spread, depth, bid-ask imbalance, and classifies book thickness.
Requires: py-clob-client (pip install py-clob-client)
"""
import argparse
import json
import sys
from py_clob_client.client import ClobClient
CLOB_HOST = "https://clob.polymarket.com"
def fetch_orderbook(token_id: str) -> object:
"""Fetch order book from CLOB API."""
client = ClobClient(CLOB_HOST)
return client.get_order_book(token_id)
def analyze(book, depth: int = 5) -> dict:
"""Analyze an order book and return metrics."""
bids = [(float(b.price), float(b.size)) for b in (book.bids or [])]
asks = [(float(a.price), float(a.size)) for a in (book.asks or [])]
# Sort: bids descending by price, asks ascending by price
bids.sort(key=lambda x: x[0], reverse=True)
asks.sort(key=lambda x: x[0])
result = {
"token_id": book.asset_id,
"total_bid_levels": len(bids),
"total_ask_levels": len(asks),
}
if not bids and not asks:
result["status"] = "EMPTY_BOOK"
return result
# Best bid / best ask
best_bid = bids[0][0] if bids else 0.0
best_ask = asks[0][0] if asks else 1.0
spread = best_ask - best_bid
mid_price = (best_bid + best_ask) / 2.0 if (bids and asks) else None
result["best_bid"] = best_bid
result["best_ask"] = best_ask
result["spread"] = round(spread, 6)
result["spread_pct"] = round((spread / mid_price * 100) if mid_price else 0, 4)
result["mid_price"] = round(mid_price, 6) if mid_price else None
# Depth at top N levels
top_bids = bids[:depth]
top_asks = asks[:depth]
bid_depth = sum(size for _, size in top_bids)
ask_depth = sum(size for _, size in top_asks)
total_depth = bid_depth + ask_depth
result["bid_depth"] = round(bid_depth, 2)
result["ask_depth"] = round(ask_depth, 2)
result["total_depth"] = round(total_depth, 2)
result["depth_levels_used"] = depth
# Bid-ask imbalance ratio: positive = more bids (buying pressure)
if total_depth > 0:
imbalance = (bid_depth - ask_depth) / total_depth
else:
imbalance = 0.0
result["imbalance_ratio"] = round(imbalance, 4)
# Classify the imbalance
if imbalance > 0.3:
result["imbalance_signal"] = "STRONG_BUY_PRESSURE"
elif imbalance > 0.1:
result["imbalance_signal"] = "MODERATE_BUY_PRESSURE"
elif imbalance < -0.3:
result["imbalance_signal"] = "STRONG_SELL_PRESSURE"
elif imbalance < -0.1:
result["imbalance_signal"] = "MODERATE_SELL_PRESSURE"
else:
result["imbalance_signal"] = "BALANCED"
# Book thickness classification
if total_depth < 500:
result["book_class"] = "THIN"
result["book_note"] = "Easy to move price; high slippage risk"
elif total_depth < 5000:
result["book_class"] = "MODERATE"
result["book_note"] = "Normal depth; moderate slippage on large orders"
else:
result["book_class"] = "THICK"
result["book_note"] = "Stable book; low slippage for most order sizes"
# Bid levels detail
result["bid_levels"] = [
{"price": p, "size": round(s, 2), "cumulative": round(sum(sz for _, sz in top_bids[:i+1]), 2)}
for i, (p, s) in enumerate(top_bids)
]
result["ask_levels"] = [
{"price": p, "size": round(s, 2), "cumulative": round(sum(sz for _, sz in top_asks[:i+1]), 2)}
for i, (p, s) in enumerate(top_asks)
]
# Slippage estimate: cost to buy/sell $100 worth
slippage_size = 100.0
result["buy_slippage"] = _estimate_slippage(asks, slippage_size)
result["sell_slippage"] = _estimate_slippage(
[(p, s) for p, s in bids], slippage_size, selling=True
)
return result
def _estimate_slippage(
levels: list[tuple[float, float]], target_size: float, selling: bool = False
) -> dict | None:
"""Estimate average fill price and slippage for a target size."""
if not levels:
return None
filled = 0.0
cost = 0.0
for price, size in levels:
remaining = target_size - filled
fill_qty = min(size, remaining)
cost += fill_qty * price
filled += fill_qty
if filled >= target_size:
break
if filled == 0:
return None
avg_price = cost / filled
best_price = levels[0][0]
slippage = abs(avg_price - best_price)
return {
"target_size": target_size,
"filled": round(filled, 2),
"avg_price": round(avg_price, 6),
"best_price": best_price,
"slippage": round(slippage, 6),
"slippage_pct": round(slippage / best_price * 100 if best_price else 0, 4),
"fully_filled": filled >= target_size,
}
def format_output(result: dict) -> str:
"""Format analysis result for display."""
lines = []
lines.append(f"Order Book Analysis for {result['token_id'][:30]}...")
lines.append("=" * 70)
if result.get("status") == "EMPTY_BOOK":
lines.append("Order book is empty -- no bids or asks.")
return "\n".join(lines)
lines.append(f" Best Bid: ${result['best_bid']:.4f}")
lines.append(f" Best Ask: ${result['best_ask']:.4f}")
lines.append(f" Mid Price: ${result['mid_price']:.4f}" if result['mid_price'] else " Mid Price: N/A")
lines.append(f" Spread: ${result['spread']:.4f} ({result['spread_pct']:.2f}%)")
lines.append("")
lines.append(f"Depth (top {result['depth_levels_used']} levels):")
lines.append(f" Bid Depth: {result['bid_depth']:,.2f} shares")
lines.append(f" Ask Depth: {result['ask_depth']:,.2f} shares")
lines.append(f" Total: {result['total_depth']:,.2f} shares")
lines.append(f" Imbalance: {result['imbalance_ratio']:+.4f} ({result['imbalance_signal']})")
lines.append(f" Book Class: {result['book_class']} -- {result['book_note']}")
lines.append("")
lines.append("Bid Levels:")
lines.append(f" {'Price':>8} {'Size':>10} {'Cumulative':>12}")
for lvl in result.get("bid_levels", []):
lines.append(f" ${lvl['price']:<7.4f} {lvl['size']:>10,.2f} {lvl['cumulative']:>12,.2f}")
lines.append("")
lines.append("Ask Levels:")
lines.append(f" {'Price':>8} {'Size':>10} {'Cumulative':>12}")
for lvl in result.get("ask_levels", []):
lines.append(f" ${lvl['price']:<7.4f} {lvl['size']:>10,.2f} {lvl['cumulative']:>12,.2f}")
for label, key in [("Buy", "buy_slippage"), ("Sell", "sell_slippage")]:
slip = result.get(key)
lines.append("")
if slip:
status = "YES" if slip["fully_filled"] else "PARTIAL"
lines.append(
f"{label} Slippage ({slip['target_size']:.0f} shares): "
f"avg ${slip['avg_price']:.4f}, "
f"slippage ${slip['slippage']:.4f} ({slip['slippage_pct']:.2f}%), "
f"filled: {status}"
)
else:
lines.append(f"{label} Slippage: No liquidity on this side")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Analyze Polymarket order book"
)
parser.add_argument(
"--token-id",
required=True,
help="CLOB token ID to analyze",
)
parser.add_argument(
"--depth",
type=int,
default=5,
help="Number of price levels to analyze (default: 5)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON",
)
args = parser.parse_args()
try:
book = fetch_orderbook(args.token_id)
except Exception as e:
print(f"Error fetching order book: {e}", file=sys.stderr)
sys.exit(1)
result = analyze(book, depth=args.depth)
if args.json:
print(json.dumps(result, indent=2))
else:
print(format_output(result))
if __name__ == "__main__":
main()