polymarket proxy wallet redeem
Project description
poly-position-watcher
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
TRADEandORDER(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(...)andget_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,
)
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
Important:
- When
enable_fee_calc=True, you must register market fee metadata withset_market_fee_schedule(...)orset_market_fee_schedules(...). get_position()does not fetch/marketsautomatically.- If you need strategy-level positions, use
get_position_by_order_ids(...)orget_positions_by_order_ids(...); these resolveorder.associate_tradesfirst and then fall back to the internal trade index built from live trades. - If order WS updates may arrive before trade aggregation, use
get_effective_position_size(...)to compareposition.original_sizeandorder.size_matchedsafely. - Use
wait_for_orders_filled(...)when you care about order fill progress; usewait_for_orders_pos_filled(...)when you need the position aggregate (position.original_size) to be synchronized before continuing. - If multiple callers share one watcher, pass
group="..."toadd_http_listen(...),remove_http_listen(...),set_http_listen(...),set_market_http_listen(...),set_order_http_listen(...), orclear_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.marketas the marketconditionId, so register fee metadata withconditionIdas 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)
⚠️ 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 -> feeSchedulethroughservice.set_market_fee_schedule(...)orservice.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) |
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file poly_position_watcher-0.3.9.tar.gz.
File metadata
- Download URL: poly_position_watcher-0.3.9.tar.gz
- Upload date:
- Size: 39.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64a32a1e776b2f14ba54fc2e0c9c88c3857628981dbf75c6c34040d671d521fc
|
|
| MD5 |
de0479c2c18521daead841fab3b7a506
|
|
| BLAKE2b-256 |
54cf480e2caa6c1b8002baf1120e5a2e080aec3b09db8e8cfa47044c7a6ebf35
|
File details
Details for the file poly_position_watcher-0.3.9-py3-none-any.whl.
File metadata
- Download URL: poly_position_watcher-0.3.9-py3-none-any.whl
- Upload date:
- Size: 34.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95427eca3818da0d4ae8c79319302b27a50081fcd525e871de2ade9ca459760c
|
|
| MD5 |
ebdf63cade90dc80888418639ee00a09
|
|
| BLAKE2b-256 |
c35d58b7cebf9193c9772e0297f3956e2097bef3c863919e18b4069cb026a007
|