Skip to main content

polymarket proxy wallet redeem

Project description

poly-position-watcher

PyPI Python License

English | 中文

Overview

poly-position-watcher is built for real trading on Polymarket, where order fills, trade updates, position sync, and sellable on-chain balance may not arrive at the same time.

In practice, order filled, trade confirmed, position updated, and sellable on-chain are different states. Strategy code that reacts to only one of them can easily overestimate inventory, mis-time hedges, or place exits before the position is actually ready.

This library acts as an execution reliability layer for Polymarket strategies. It helps you observe and reconcile these states in one place, so your strategy does not mistake "order filled" for "position synchronized" or "position visible" for "actually sellable".

Core capabilities:

  • WSS real-time tracking for TRADE and ORDER (positions + orders)
  • HTTP polling fallback for reliability
  • Optional fee calculation using market feeSchedule
  • Position fields for fill checks: size (net position shares), original_size (gross position shares), sellable_size (on-chain confirmed size), fee_amount (accumulated fee amount in USDC)
  • Failed trades are detected and returned on positions (has_failed, failed_trades)
  • Strategy-scoped position queries by order_ids: get_position_by_order_ids(...) and get_positions_by_order_ids(...)
  • Order-aware fill helpers: get_effective_position_size(...), wait_for_orders_filled(...), wait_for_orders_pos_filled(...)
  • HTTP fallback namespaces via group=..., so multiple callers can share one watcher without overwriting each other's monitored order/market sets

Note: WSS disconnects are auto-detected and reconnected.

Installation

pip install poly-position-watcher
# pip install poly-position-watcher --index-url https://pypi.org/simple

If installing from source, clone this repo and run pip install -e ..

Quick start

from py_clob_client.client import ClobClient
from poly_position_watcher import PositionWatcherService, OrderMessage, UserPosition

client = ClobClient(
    base_url="https://clob.polymarket.com",
    key="<wallet-key>",
    secret="<wallet-secret>",
)

with PositionWatcherService(
    client=client,
    init_positions=True,  # Initialize positions via official API
    enable_http_fallback=True,  # Enable HTTP polling fallback
    add_init_positions_to_http=True,  # Auto-add condition_ids from init positions to HTTP monitoring
    enable_fee_calc=True,  # Optional: enable fee adjustments
) as service:
    service.set_market_fee_schedule(
        "<condition_id>",
        {"rate": 0.0175, "exponent": 1, "takerOnly": True, "rebateRate": 0.25},
    )

    # Non-blocking: Get current positions and orders (returns immediately)
    position: UserPosition = service.get_position("<token_id>")
    strategy_position: UserPosition | None = service.get_position_by_order_ids(["<order_id>"])
    strategy_positions: dict[str, UserPosition] = service.get_positions_by_order_ids(
        ["<order_id_1>", "<order_id_2>"]
    )
    effective_size: float = service.get_effective_position_size(
        token_id="<token_id>",
        order_ids=["<order_id_1>", "<order_id_2>"],
    )
    order: OrderMessage = service.get_order("<order_id>")
    fill_result = service.wait_for_orders_filled(
        ["<order_id_1>", "<order_id_2>"],
        any_filled=True,
        timeout=3,
    )
    pos_fill_result = service.wait_for_orders_pos_filled(
        ["<order_id_1>", "<order_id_2>"],
        any_filled=True,
        timeout=3,
    )
    sellable_fill_result = service.wait_for_orders_pos_filled(
        ["<order_id_1>", "<order_id_2>"],
        any_filled=True,
        timeout=3,
        use_sellable_size=True,
    )
    print(position)
    print(strategy_position)
    print(strategy_positions)
    print(effective_size)
    print(fill_result)
    print(fill_result.is_filled("<order_id_1>"))
    print(fill_result.get("<order_id_1>"))
    print(pos_fill_result)
    print(order)
    if position:
        print("size(net shares):", position.size)
        print("original_size(gross shares):", position.original_size)
        print("fee_amount:", position.fee_amount)
    service.show_positions(limit=10)
    service.show_orders(limit=10)
    
    # Blocking: Wait for position/order updates (with timeout)
    position: UserPosition = service.blocking_get_position("<token_id>", timeout=5)
    order: OrderMessage = service.blocking_get_order("<order_id>", timeout=3)
    print(position)
    print(order)
    
    # Optional: If you open new positions/orders and want to monitor them via HTTP fallback
    # service.add_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
    # service.set_http_listen(
    #     market_ids=["<condition_id>"],
    #     order_ids=["<order_id>"],
    #     group="strategy-a",
    # )
    # service.clear_http(group="strategy-a")
    # service.remove_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
    # service.clear_http()  # Clear all monitoring items, threads continue running
    # service.remove_token_data("<token_id>")  # Optional: purge all cached in-memory data for this token

Important:

  • When enable_fee_calc=True, you must register market fee metadata with set_market_fee_schedule(...) or set_market_fee_schedules(...).
  • get_position() does not fetch /markets automatically.
  • If you need strategy-level positions, use get_position_by_order_ids(...) or get_positions_by_order_ids(...); these resolve order.associate_trades first and then fall back to the internal trade index built from live trades.
  • A single trade event can include multiple maker orders from the same user; order-scoped APIs automatically filter those maker legs back down to the requested order_id set before aggregating positions.
  • If order WS updates may arrive before trade aggregation, use get_effective_position_size(...) to compare position.original_size and order.size_matched safely.
  • If a token's trade/order/position state only needs to be retained temporarily, call remove_token_data(token_id) to atomically purge that token's in-memory data, including trade indexes, order indexes, cached positions, and wait queues.
  • Use wait_for_orders_filled(...) when you care about order fill progress; use wait_for_orders_pos_filled(...) when you need the position aggregate (position.original_size) to be synchronized before continuing. Pass use_sellable_size=True when you need to wait until the position becomes fully sellable from confirmed trades.
  • If multiple callers share one watcher, pass group="..." to add_http_listen(...), remove_http_listen(...), set_http_listen(...), set_market_http_listen(...), set_order_http_listen(...), or clear_http(...) so each caller manages its own HTTP fallback namespace without overwriting others.
  • If a market is missing feeSchedule, fee calculation is skipped for that market and a warning is logged once.

Where does feeSchedule come from:

  • Fetch a market or event from the Gamma API, then read the market object's feeSchedule.
  • Your trade payload uses trade.market as the market conditionId, so register fee metadata with conditionId as the key.
  • Official docs: Fees, Get event by id, List markets, Get market by slug

Example: fetch an event and register all nested market fee schedules

import requests

event = requests.get(
    "https://gamma-api.polymarket.com/events/<event_id>",
    timeout=10,
).json()

fee_schedule_map = {
    market["conditionId"]: market.get("feeSchedule")
    for market in event.get("markets", [])
    if market.get("feeSchedule")
}

service.set_market_fee_schedules(fee_schedule_map)

Example: fetch a single market and register its fee schedule

import requests

market = requests.get(
    "https://gamma-api.polymarket.com/markets/slug/<market-slug>",
    timeout=10,
).json()

service.set_market_fee_schedule(
    market["conditionId"],
    market.get("feeSchedule"),
)

Example output:

OrderMessage(
  type: 'update',
  event_type: 'order',
  asset_id: '7718951783559279583290056782453440...',
  associate_trades: ['8bf02a75-5...'],
  id: '0x74a71abb9efe59c994e0...',
  market: '0x3b7e9926575eb7fae2...',
  order_owner: None,
  original_size: 37.5,
  outcome: 'Up',
  owner: '',
  price: 0.52,
  side: 'BUY',
  size_matched: 37.5,
  timestamp: 0.0,
  filled: True,
  status: 'MATCHED',
  created_at: datetime.datetime(2025, 12, 8, 9, 44, 50, tzinfo=TzInfo(0))
)
UserPosition(
  price: 0.0,
  size: 0.0,
  original_size: 0.0,
  volume: 0.0,
  fee_amount: 0.0,
  sellable_size: 0.0,
  token_id: '',
  last_update: 0.0,
  market_id: None,
  outcome: None,
  created_at: None,
  has_failed: False,
  failed_trades: []
)

Full example (examples/example.py)

Pretty printing

service.show_positions(limit=10)
service.show_orders(limit=10)

Positions Table

⚠️ Fee notice (taker fee / maker rebate)


Some Polymarket markets enable taker fee / maker rebate. This library supports fee calculation from market feeSchedule data:

  • Enable with enable_fee_calc=True
  • Register condition_id -> feeSchedule through service.set_market_fee_schedule(...) or service.set_market_fee_schedules(...)
  • This registration step is required if you want fee-aware positions; the watcher does not auto-fetch /markets
  • In practice, use the Gamma market/event response's market.get("feeSchedule")
  • Optionally override the fee handler with fee_calc_fn
  • Disable (default) if you prefer pre-fee positions
  • Returned position fields: size = net position shares, original_size = gross position shares, fee_amount = accumulated fee amount in USDC

Default fee formula (when fee_calc_fn is not provided): fee = size * rate * price * (1 - price).

According to the current Polymarket fees documentation, taker fees are charged in USDC. Under the default calculator, both taker buys and taker sells keep share quantity unchanged; fee impact is tracked in fee_amount instead of reducing size.


Position Initialization

When init_positions=True, the service will:

  • Fetch current positions via the official Polymarket API (/positions)
  • Create fake trades from position data to maintain compatibility with existing trade-based calculations
  • Skip positions with currentValue = 0 (empty positions)
  • Optionally add condition IDs to HTTP monitoring if add_init_positions_to_http=True

The HTTP fallback polling threads run persistently throughout the with statement lifecycle. You can dynamically add/remove markets and orders without restarting threads.

Note: If you start the watcher before any positions exist, set init_positions=False. The HTTP fallback can be enabled independently and will start with empty monitoring sets if needed.

Configuration

Service Parameters

Parameter Type Default Description
init_positions bool False Initialize positions via official Polymarket API on startup
enable_http_fallback bool False Enable persistent HTTP polling threads as WebSocket fallback
http_poll_interval float 3.0 HTTP polling interval in seconds
add_init_positions_to_http bool False Automatically add condition IDs from initialized positions to HTTP monitoring
enable_fee_calc bool False Apply fee adjustments using registered market feeSchedule data
market_fee_schedules mapping None Optional initial condition_id -> feeSchedule mapping
fee_calc_fn callable None Custom fee function: (size, price, side, fee_schedule) -> (new_size, fee_amount)
print_first_sellable_size_equal_original bool False Log once when an all-buy position first has sellable_size == original_size
sellable_size_equal_tolerance float 0.02 Tolerance for the sellable_size / original_size comparison

Environment Variables

Environment variable Description
poly_position_watcher_LOG_LEVEL Log level, default INFO

To set a proxy for WebSocket connections, build a dict before creating PositionWatcherService and pass it as wss_proxies:

PROXY = {"http_proxy_host": "127.0.0.1", "http_proxy_port": 7890}
service = PositionWatcherService(client, wss_proxies=PROXY)

Dependencies

Layout

poly_position_watcher/
├── api_worker.py          # HTTP backfill and context management
├── position_service.py    # Core entry; maintains position/order caches
├── trade_calculator.py    # Position calculation utils
├── wss_worker.py          # WebSocket client implementation
├── common/                # Logging and enums
└── schema/                # Pydantic models

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

poly_position_watcher-0.3.13.tar.gz (42.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

poly_position_watcher-0.3.13-py3-none-any.whl (36.0 kB view details)

Uploaded Python 3

File details

Details for the file poly_position_watcher-0.3.13.tar.gz.

File metadata

  • Download URL: poly_position_watcher-0.3.13.tar.gz
  • Upload date:
  • Size: 42.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for poly_position_watcher-0.3.13.tar.gz
Algorithm Hash digest
SHA256 cade0db67fb1e85c7fdce9200143df9a8c7e88d88fd528351394d783dbcf4a74
MD5 3c45dcf1c6118c7b61a3372006612e1e
BLAKE2b-256 5c8bec447f8bc14d51e72496903a1ed133b0eaa99ccec57a134893ac4e3eed73

See more details on using hashes here.

File details

Details for the file poly_position_watcher-0.3.13-py3-none-any.whl.

File metadata

File hashes

Hashes for poly_position_watcher-0.3.13-py3-none-any.whl
Algorithm Hash digest
SHA256 7fe242252ba31cea301bb50b976426ca761f59c6ac31e9f1950e379f68fbc230
MD5 d2997f750e75e690a28a5b48fc37c15d
BLAKE2b-256 aa26f29c245fe3b52231f4ff4857e6aac89598e6b80ec1b4fbe0e969caf9a45c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page