poly-position-watcher 0.3.4__tar.gz → 0.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {poly_position_watcher-0.3.4/poly_position_watcher.egg-info → poly_position_watcher-0.3.6}/PKG-INFO +15 -1
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/README.md +14 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/_version.py +1 -1
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/api_worker.py +166 -60
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/position_service.py +216 -38
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6/poly_position_watcher.egg-info}/PKG-INFO +15 -1
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher.egg-info/SOURCES.txt +1 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/pyproject.toml +1 -1
- poly_position_watcher-0.3.6/tests/test_http_fallback_manager.py +69 -0
- poly_position_watcher-0.3.6/tests/test_position_model.py +201 -0
- poly_position_watcher-0.3.4/tests/test_position_model.py +0 -78
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/LICENSE +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/MANIFEST.in +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/__init__.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/__init__.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/enums.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/logger.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/__init__.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/base.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/common_model.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/position_model.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/trade_calculator.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/wss_worker.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher.egg-info/dependency_links.txt +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher.egg-info/requires.txt +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher.egg-info/top_level.txt +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/setup.cfg +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/setup.py +0 -0
- {poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/tests/test_trade_calculator.py +0 -0
{poly_position_watcher-0.3.4/poly_position_watcher.egg-info → poly_position_watcher-0.3.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: poly-position-watcher
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: polymarket proxy wallet redeem
|
|
5
5
|
Home-page: https://github.com/tosmart01/polymarket-position-watcher
|
|
6
6
|
Author: pinbar
|
|
@@ -73,8 +73,14 @@ with PositionWatcherService(
|
|
|
73
73
|
|
|
74
74
|
# Non-blocking: Get current positions and orders (returns immediately)
|
|
75
75
|
position: UserPosition = service.get_position("<token_id>")
|
|
76
|
+
strategy_position: UserPosition | None = service.get_position_by_order_ids(["<order_id>"])
|
|
77
|
+
strategy_positions: dict[str, UserPosition] = service.get_positions_by_order_ids(
|
|
78
|
+
["<order_id_1>", "<order_id_2>"]
|
|
79
|
+
)
|
|
76
80
|
order: OrderMessage = service.get_order("<order_id>")
|
|
77
81
|
print(position)
|
|
82
|
+
print(strategy_position)
|
|
83
|
+
print(strategy_positions)
|
|
78
84
|
print(order)
|
|
79
85
|
if position:
|
|
80
86
|
print("size(post-fee):", position.size)
|
|
@@ -91,6 +97,12 @@ with PositionWatcherService(
|
|
|
91
97
|
|
|
92
98
|
# Optional: If you open new positions/orders and want to monitor them via HTTP fallback
|
|
93
99
|
# service.add_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
100
|
+
# service.set_http_listen(
|
|
101
|
+
# market_ids=["<condition_id>"],
|
|
102
|
+
# order_ids=["<order_id>"],
|
|
103
|
+
# group="strategy-a",
|
|
104
|
+
# )
|
|
105
|
+
# service.clear_http(group="strategy-a")
|
|
94
106
|
# service.remove_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
95
107
|
# service.clear_http() # Clear all monitoring items, threads continue running
|
|
96
108
|
```
|
|
@@ -98,6 +110,8 @@ with PositionWatcherService(
|
|
|
98
110
|
Important:
|
|
99
111
|
- When `enable_fee_calc=True`, you must register market fee metadata with `set_market_fee_schedule(...)` or `set_market_fee_schedules(...)`.
|
|
100
112
|
- `get_position()` does not fetch `/markets` automatically.
|
|
113
|
+
- 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.
|
|
114
|
+
- 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.
|
|
101
115
|
- If a market is missing `feeSchedule`, fee calculation is skipped for that market and a warning is logged once.
|
|
102
116
|
|
|
103
117
|
Where does `feeSchedule` come from:
|
|
@@ -54,8 +54,14 @@ with PositionWatcherService(
|
|
|
54
54
|
|
|
55
55
|
# Non-blocking: Get current positions and orders (returns immediately)
|
|
56
56
|
position: UserPosition = service.get_position("<token_id>")
|
|
57
|
+
strategy_position: UserPosition | None = service.get_position_by_order_ids(["<order_id>"])
|
|
58
|
+
strategy_positions: dict[str, UserPosition] = service.get_positions_by_order_ids(
|
|
59
|
+
["<order_id_1>", "<order_id_2>"]
|
|
60
|
+
)
|
|
57
61
|
order: OrderMessage = service.get_order("<order_id>")
|
|
58
62
|
print(position)
|
|
63
|
+
print(strategy_position)
|
|
64
|
+
print(strategy_positions)
|
|
59
65
|
print(order)
|
|
60
66
|
if position:
|
|
61
67
|
print("size(post-fee):", position.size)
|
|
@@ -72,6 +78,12 @@ with PositionWatcherService(
|
|
|
72
78
|
|
|
73
79
|
# Optional: If you open new positions/orders and want to monitor them via HTTP fallback
|
|
74
80
|
# service.add_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
81
|
+
# service.set_http_listen(
|
|
82
|
+
# market_ids=["<condition_id>"],
|
|
83
|
+
# order_ids=["<order_id>"],
|
|
84
|
+
# group="strategy-a",
|
|
85
|
+
# )
|
|
86
|
+
# service.clear_http(group="strategy-a")
|
|
75
87
|
# service.remove_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
76
88
|
# service.clear_http() # Clear all monitoring items, threads continue running
|
|
77
89
|
```
|
|
@@ -79,6 +91,8 @@ with PositionWatcherService(
|
|
|
79
91
|
Important:
|
|
80
92
|
- When `enable_fee_calc=True`, you must register market fee metadata with `set_market_fee_schedule(...)` or `set_market_fee_schedules(...)`.
|
|
81
93
|
- `get_position()` does not fetch `/markets` automatically.
|
|
94
|
+
- 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.
|
|
95
|
+
- 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.
|
|
82
96
|
- If a market is missing `feeSchedule`, fee calculation is skipped for that market and a warning is logged once.
|
|
83
97
|
|
|
84
98
|
Where does `feeSchedule` come from:
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/api_worker.py
RENAMED
|
@@ -196,34 +196,103 @@ class HttpFallbackManager:
|
|
|
196
196
|
Manages HTTP fallback polling with persistent threads.
|
|
197
197
|
Markets and orders sets can be modified dynamically while threads keep running.
|
|
198
198
|
"""
|
|
199
|
-
|
|
199
|
+
|
|
200
|
+
DEFAULT_GROUP = "__default__"
|
|
201
|
+
|
|
200
202
|
def __init__(self, service: "PositionWatcherService", http_poll_interval: float = 3):
|
|
201
203
|
self.service = service
|
|
202
204
|
self.http_poll_interval = http_poll_interval
|
|
203
205
|
# Use service's existing APIWorker instance
|
|
204
206
|
self.api_worker = service.api_worker
|
|
205
207
|
self._slug_cache: dict[str, str] = {}
|
|
206
|
-
|
|
207
|
-
# Thread-safe
|
|
208
|
+
|
|
209
|
+
# Thread-safe grouped monitoring state
|
|
208
210
|
self._lock = threading.RLock()
|
|
209
|
-
self.
|
|
210
|
-
self.
|
|
211
|
-
|
|
211
|
+
self.market_groups: dict[str, set[str]] = {}
|
|
212
|
+
self.order_groups: dict[str, set[str]] = {}
|
|
213
|
+
|
|
212
214
|
# Thread control
|
|
213
215
|
self._stop_event = threading.Event()
|
|
214
216
|
self._trade_thread = None
|
|
215
217
|
self._order_thread = None
|
|
216
218
|
self._running = False
|
|
217
|
-
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _normalize_group(group: str | None) -> str:
|
|
222
|
+
return group or HttpFallbackManager.DEFAULT_GROUP
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def _clean_ids(values: list[str] | None) -> set[str]:
|
|
226
|
+
return {value for value in (values or []) if value}
|
|
227
|
+
|
|
228
|
+
def _aggregated_markets_locked(self) -> set[str]:
|
|
229
|
+
markets: set[str] = set()
|
|
230
|
+
for group_markets in self.market_groups.values():
|
|
231
|
+
markets.update(group_markets)
|
|
232
|
+
return markets
|
|
233
|
+
|
|
234
|
+
def _aggregated_orders_locked(self) -> set[str]:
|
|
235
|
+
order_ids: set[str] = set()
|
|
236
|
+
for group_orders in self.order_groups.values():
|
|
237
|
+
order_ids.update(group_orders)
|
|
238
|
+
return order_ids
|
|
239
|
+
|
|
240
|
+
def add(
|
|
241
|
+
self,
|
|
242
|
+
market_ids: list[str] = None,
|
|
243
|
+
order_ids: list[str] = None,
|
|
244
|
+
group: str | None = None,
|
|
245
|
+
):
|
|
246
|
+
"""Add markets/orders to monitor for a specific group."""
|
|
247
|
+
group_name = self._normalize_group(group)
|
|
248
|
+
with self._lock:
|
|
249
|
+
if clean_market_ids := self._clean_ids(market_ids):
|
|
250
|
+
self.market_groups.setdefault(group_name, set()).update(clean_market_ids)
|
|
251
|
+
if clean_order_ids := self._clean_ids(order_ids):
|
|
252
|
+
self.order_groups.setdefault(group_name, set()).update(clean_order_ids)
|
|
253
|
+
logger.info(
|
|
254
|
+
"Added HTTP monitoring for group {}: {} markets, {} orders",
|
|
255
|
+
group_name,
|
|
256
|
+
len(market_ids or []),
|
|
257
|
+
len(order_ids or []),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def set_group(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
group: str,
|
|
264
|
+
market_ids: list[str] | None = None,
|
|
265
|
+
order_ids: list[str] | None = None,
|
|
266
|
+
):
|
|
267
|
+
"""Replace monitored markets/orders for a specific group."""
|
|
268
|
+
group_name = self._normalize_group(group)
|
|
269
|
+
clean_market_ids = self._clean_ids(market_ids)
|
|
270
|
+
clean_order_ids = self._clean_ids(order_ids)
|
|
271
|
+
with self._lock:
|
|
272
|
+
if clean_market_ids:
|
|
273
|
+
self.market_groups[group_name] = clean_market_ids
|
|
274
|
+
else:
|
|
275
|
+
self.market_groups.pop(group_name, None)
|
|
276
|
+
if clean_order_ids:
|
|
277
|
+
self.order_groups[group_name] = clean_order_ids
|
|
278
|
+
else:
|
|
279
|
+
self.order_groups.pop(group_name, None)
|
|
280
|
+
logger.info(
|
|
281
|
+
"Set HTTP monitoring group {}: {} markets, {} orders",
|
|
282
|
+
group_name,
|
|
283
|
+
len(clean_market_ids),
|
|
284
|
+
len(clean_order_ids),
|
|
285
|
+
)
|
|
286
|
+
|
|
218
287
|
def start(self):
|
|
219
288
|
"""Start HTTP polling threads (persistent, runs until stop is called)."""
|
|
220
289
|
with self._lock:
|
|
221
290
|
if self._running:
|
|
222
291
|
return
|
|
223
|
-
|
|
292
|
+
|
|
224
293
|
self._stop_event.clear()
|
|
225
294
|
self._running = True
|
|
226
|
-
|
|
295
|
+
|
|
227
296
|
self._trade_thread = threading.Thread(
|
|
228
297
|
target=self._trade_loop,
|
|
229
298
|
daemon=True,
|
|
@@ -232,120 +301,157 @@ class HttpFallbackManager:
|
|
|
232
301
|
target=self._order_loop,
|
|
233
302
|
daemon=True,
|
|
234
303
|
)
|
|
235
|
-
|
|
304
|
+
|
|
236
305
|
self._trade_thread.start()
|
|
237
306
|
self._order_thread.start()
|
|
238
307
|
logger.info("Started HTTP fallback polling threads")
|
|
239
|
-
|
|
308
|
+
|
|
240
309
|
def stop(self):
|
|
241
310
|
"""Stop HTTP polling threads."""
|
|
242
311
|
with self._lock:
|
|
243
312
|
if not self._running:
|
|
244
313
|
return
|
|
245
|
-
|
|
314
|
+
|
|
246
315
|
self._stop_event.set()
|
|
247
316
|
self._running = False
|
|
248
|
-
|
|
317
|
+
|
|
249
318
|
# Wait for threads to finish
|
|
250
319
|
if self._trade_thread:
|
|
251
320
|
self._trade_thread.join(timeout=2)
|
|
252
321
|
if self._order_thread:
|
|
253
322
|
self._order_thread.join(timeout=2)
|
|
254
|
-
|
|
323
|
+
|
|
255
324
|
logger.info("Stopped HTTP fallback polling threads")
|
|
256
|
-
|
|
257
|
-
def
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
if market_ids:
|
|
261
|
-
self.markets.update(market_ids)
|
|
262
|
-
if order_ids:
|
|
263
|
-
self.orders.update(order_ids)
|
|
264
|
-
logger.info(f"Added HTTP monitoring: {len(market_ids or [])} markets, {len(order_ids or [])} orders")
|
|
265
|
-
|
|
266
|
-
def set_markets(self, market_ids: list[str] | None = None):
|
|
267
|
-
"""Replace monitored markets with the provided list."""
|
|
325
|
+
|
|
326
|
+
def set_markets(self, market_ids: list[str] | None = None, group: str | None = None):
|
|
327
|
+
"""Replace monitored markets for a specific group."""
|
|
328
|
+
group_name = self._normalize_group(group)
|
|
268
329
|
with self._lock:
|
|
269
|
-
|
|
270
|
-
|
|
330
|
+
clean_market_ids = self._clean_ids(market_ids)
|
|
331
|
+
if clean_market_ids:
|
|
332
|
+
self.market_groups[group_name] = clean_market_ids
|
|
333
|
+
else:
|
|
334
|
+
self.market_groups.pop(group_name, None)
|
|
335
|
+
logger.info(
|
|
336
|
+
"Set HTTP monitoring markets for group {}: {} total",
|
|
337
|
+
group_name,
|
|
338
|
+
len(clean_market_ids),
|
|
339
|
+
)
|
|
271
340
|
|
|
272
|
-
def set_orders(self, order_ids: list[str] | None = None):
|
|
273
|
-
"""Replace monitored orders
|
|
341
|
+
def set_orders(self, order_ids: list[str] | None = None, group: str | None = None):
|
|
342
|
+
"""Replace monitored orders for a specific group."""
|
|
343
|
+
group_name = self._normalize_group(group)
|
|
274
344
|
with self._lock:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
345
|
+
clean_order_ids = self._clean_ids(order_ids)
|
|
346
|
+
if clean_order_ids:
|
|
347
|
+
self.order_groups[group_name] = clean_order_ids
|
|
348
|
+
else:
|
|
349
|
+
self.order_groups.pop(group_name, None)
|
|
350
|
+
logger.info(
|
|
351
|
+
"Set HTTP monitoring orders for group {}: {} total",
|
|
352
|
+
group_name,
|
|
353
|
+
len(clean_order_ids),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def remove(
|
|
357
|
+
self,
|
|
358
|
+
market_ids: list[str] = None,
|
|
359
|
+
order_ids: list[str] = None,
|
|
360
|
+
group: str | None = None,
|
|
361
|
+
):
|
|
362
|
+
"""Remove markets/orders from monitoring for a specific group."""
|
|
363
|
+
group_name = self._normalize_group(group)
|
|
280
364
|
with self._lock:
|
|
281
|
-
if market_ids:
|
|
282
|
-
self.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
365
|
+
if clean_market_ids := self._clean_ids(market_ids):
|
|
366
|
+
group_markets = self.market_groups.get(group_name)
|
|
367
|
+
if group_markets is not None:
|
|
368
|
+
group_markets -= clean_market_ids
|
|
369
|
+
if not group_markets:
|
|
370
|
+
self.market_groups.pop(group_name, None)
|
|
371
|
+
if clean_order_ids := self._clean_ids(order_ids):
|
|
372
|
+
group_orders = self.order_groups.get(group_name)
|
|
373
|
+
if group_orders is not None:
|
|
374
|
+
group_orders -= clean_order_ids
|
|
375
|
+
if not group_orders:
|
|
376
|
+
self.order_groups.pop(group_name, None)
|
|
377
|
+
logger.info(
|
|
378
|
+
"Removed HTTP monitoring for group {}: {} markets, {} orders",
|
|
379
|
+
group_name,
|
|
380
|
+
len(market_ids or []),
|
|
381
|
+
len(order_ids or []),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def clear(self, group: str | None = None):
|
|
385
|
+
"""Clear monitoring state for one group, or all groups when group is omitted."""
|
|
289
386
|
with self._lock:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
387
|
+
if group is None:
|
|
388
|
+
self.market_groups.clear()
|
|
389
|
+
self.order_groups.clear()
|
|
390
|
+
logger.info("Cleared all HTTP monitoring groups (threads continue running)")
|
|
391
|
+
return
|
|
392
|
+
group_name = self._normalize_group(group)
|
|
393
|
+
self.market_groups.pop(group_name, None)
|
|
394
|
+
self.order_groups.pop(group_name, None)
|
|
395
|
+
logger.info(
|
|
396
|
+
"Cleared HTTP monitoring group {} (threads continue running)",
|
|
397
|
+
group_name,
|
|
398
|
+
)
|
|
399
|
+
|
|
294
400
|
def _trade_loop(self):
|
|
295
401
|
"""Trade polling loop - runs continuously until stopped."""
|
|
296
402
|
while not self._stop_event.wait(self.http_poll_interval):
|
|
297
403
|
try:
|
|
298
404
|
self._update_missing_market_slugs()
|
|
299
405
|
with self._lock:
|
|
300
|
-
markets = list(self.
|
|
301
|
-
|
|
406
|
+
markets = list(self._aggregated_markets_locked())
|
|
407
|
+
|
|
302
408
|
if not markets:
|
|
303
409
|
continue # No markets to poll, continue waiting
|
|
304
|
-
|
|
410
|
+
|
|
305
411
|
tasks = []
|
|
306
412
|
for market in markets:
|
|
307
413
|
task = executor.submit(self.api_worker.fetch_trades, market)
|
|
308
414
|
task._market_id = market
|
|
309
415
|
tasks.append(task)
|
|
310
|
-
|
|
416
|
+
|
|
311
417
|
for task in as_completed(tasks):
|
|
312
418
|
try:
|
|
313
419
|
trades = task.result()
|
|
314
420
|
except Exception as e:
|
|
315
421
|
logger.error(f"Failed to http fetch trades market {task._market_id}: {e}")
|
|
316
422
|
continue
|
|
317
|
-
|
|
423
|
+
|
|
318
424
|
for trade in sorted(trades, key=lambda x: x.event_time):
|
|
319
425
|
self.service._ingest_trade(trade)
|
|
320
426
|
except Exception as e:
|
|
321
427
|
logger.error(f"Error in trade loop: {e}")
|
|
322
|
-
|
|
428
|
+
|
|
323
429
|
logger.info("Trade polling loop stopped")
|
|
324
|
-
|
|
430
|
+
|
|
325
431
|
def _order_loop(self):
|
|
326
432
|
"""Order polling loop - runs continuously until stopped."""
|
|
327
433
|
while not self._stop_event.wait(self.http_poll_interval):
|
|
328
434
|
try:
|
|
329
435
|
self._update_missing_market_slugs()
|
|
330
436
|
with self._lock:
|
|
331
|
-
order_ids = list(self.
|
|
332
|
-
|
|
437
|
+
order_ids = list(self._aggregated_orders_locked())
|
|
438
|
+
|
|
333
439
|
if not order_ids:
|
|
334
440
|
continue # No orders to poll, continue waiting
|
|
335
|
-
|
|
441
|
+
|
|
336
442
|
tasks = []
|
|
337
443
|
for order_id in order_ids:
|
|
338
444
|
task = executor.submit(self.api_worker.fetch_order, order_id)
|
|
339
445
|
task._order_id = order_id
|
|
340
446
|
tasks.append(task)
|
|
341
|
-
|
|
447
|
+
|
|
342
448
|
for task in as_completed(tasks):
|
|
343
449
|
try:
|
|
344
450
|
order = task.result()
|
|
345
451
|
except Exception as e:
|
|
346
452
|
logger.error(f"Failed to fetch order {task._order_id}: {e}")
|
|
347
453
|
continue
|
|
348
|
-
|
|
454
|
+
|
|
349
455
|
if order is None:
|
|
350
456
|
exists = self.service.position_store.orders.get(task._order_id)
|
|
351
457
|
if exists:
|
|
@@ -355,7 +461,7 @@ class HttpFallbackManager:
|
|
|
355
461
|
self.service._ingest_order(order)
|
|
356
462
|
except Exception as e:
|
|
357
463
|
logger.error(f"Error in order loop: {e}")
|
|
358
|
-
|
|
464
|
+
|
|
359
465
|
logger.info("Order polling loop stopped")
|
|
360
466
|
|
|
361
467
|
def _update_missing_market_slugs(self):
|
|
@@ -54,18 +54,27 @@ class PositionStore:
|
|
|
54
54
|
if fee_schedule
|
|
55
55
|
}
|
|
56
56
|
self.trades_by_token: Dict[str, Dict[str, TradeMessage]] = defaultdict(dict)
|
|
57
|
+
self.trades_by_id: Dict[str, TradeMessage] = {}
|
|
58
|
+
self.trade_ids_by_order: Dict[str, set[str]] = defaultdict(set)
|
|
57
59
|
self.positions: Dict[str, UserPosition] = {}
|
|
58
60
|
self.orders: Dict[str, OrderMessage] = {}
|
|
59
61
|
self._warned_failed_trade_keys: set[tuple[str, str]] = set()
|
|
60
62
|
self._lock = threading.RLock()
|
|
61
63
|
self.queue_dict: Dict[str, Queue] = {}
|
|
62
64
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
def _is_user_outer_trade(self, trade: TradeMessage) -> bool:
|
|
66
|
+
return trade.maker_address.upper() == self.user_address.upper()
|
|
67
|
+
|
|
68
|
+
def _iter_user_maker_orders(self, trade: TradeMessage):
|
|
66
69
|
for order in trade.maker_orders:
|
|
67
70
|
if order.maker_address.upper() == self.user_address.upper():
|
|
68
|
-
|
|
71
|
+
yield order
|
|
72
|
+
|
|
73
|
+
def get_token_id_from_trade(self, trade: TradeMessage) -> tuple[str, str] | None:
|
|
74
|
+
if self._is_user_outer_trade(trade):
|
|
75
|
+
return trade.outcome, trade.asset_id
|
|
76
|
+
for order in self._iter_user_maker_orders(trade):
|
|
77
|
+
return order.outcome, order.asset_id
|
|
69
78
|
|
|
70
79
|
def _put(self, _id: str, item: UserPosition | OrderMessage) -> None:
|
|
71
80
|
if _id not in self.queue_dict:
|
|
@@ -99,8 +108,10 @@ class PositionStore:
|
|
|
99
108
|
existing = trades_map.get(trade.id)
|
|
100
109
|
if existing and trade.event_time < existing.event_time:
|
|
101
110
|
return
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
if existing:
|
|
112
|
+
self._remove_trade_indexes(existing)
|
|
113
|
+
trades_map[trade.id] = trade
|
|
114
|
+
self._index_trade(trade)
|
|
104
115
|
user_pos = self.build_position(
|
|
105
116
|
trades=list(trades_map.values()), token_id=token_id, outcome=outcome
|
|
106
117
|
)
|
|
@@ -120,7 +131,10 @@ class PositionStore:
|
|
|
120
131
|
outcome, token_id = result
|
|
121
132
|
trades_map = self.trades_by_token[token_id]
|
|
122
133
|
for trade in trades:
|
|
134
|
+
if existing := trades_map.get(trade.id):
|
|
135
|
+
self._remove_trade_indexes(existing)
|
|
123
136
|
trades_map[trade.id] = trade
|
|
137
|
+
self._index_trade(trade)
|
|
124
138
|
user_pos = self.build_position(
|
|
125
139
|
trades=list(trades_map.values()), token_id=token_id, outcome=outcome
|
|
126
140
|
)
|
|
@@ -141,7 +155,10 @@ class PositionStore:
|
|
|
141
155
|
and order.status == existing.status
|
|
142
156
|
):
|
|
143
157
|
return
|
|
144
|
-
if
|
|
158
|
+
if (
|
|
159
|
+
order.original_size is not None
|
|
160
|
+
and abs(order.size_matched - order.original_size) < 0.5
|
|
161
|
+
):
|
|
145
162
|
order.filled = True
|
|
146
163
|
self.orders[order.id] = order
|
|
147
164
|
self._put(order.id, order)
|
|
@@ -190,9 +207,43 @@ class PositionStore:
|
|
|
190
207
|
self.positions[token_id] = user_pos
|
|
191
208
|
self._put(token_id, user_pos)
|
|
192
209
|
|
|
193
|
-
def
|
|
194
|
-
self
|
|
210
|
+
def _iter_user_order_ids_for_trade(self, trade: TradeMessage):
|
|
211
|
+
if self._is_user_outer_trade(trade) and trade.taker_order_id:
|
|
212
|
+
yield trade.taker_order_id
|
|
213
|
+
for order in self._iter_user_maker_orders(trade):
|
|
214
|
+
yield order.order_id
|
|
215
|
+
|
|
216
|
+
def _index_trade(self, trade: TradeMessage) -> None:
|
|
217
|
+
self.trades_by_id[trade.id] = trade
|
|
218
|
+
for order_id in self._iter_user_order_ids_for_trade(trade):
|
|
219
|
+
if order_id:
|
|
220
|
+
self.trade_ids_by_order[order_id].add(trade.id)
|
|
221
|
+
|
|
222
|
+
def _remove_trade_indexes(self, trade: TradeMessage) -> None:
|
|
223
|
+
existing = self.trades_by_id.get(trade.id)
|
|
224
|
+
if existing is trade or existing is not None:
|
|
225
|
+
self.trades_by_id.pop(trade.id, None)
|
|
226
|
+
for order_id in self._iter_user_order_ids_for_trade(trade):
|
|
227
|
+
if not order_id:
|
|
228
|
+
continue
|
|
229
|
+
trade_ids = self.trade_ids_by_order.get(order_id)
|
|
230
|
+
if not trade_ids:
|
|
231
|
+
continue
|
|
232
|
+
trade_ids.discard(trade.id)
|
|
233
|
+
if not trade_ids:
|
|
234
|
+
self.trade_ids_by_order.pop(order_id, None)
|
|
235
|
+
|
|
236
|
+
def _build_user_position(
|
|
237
|
+
self,
|
|
238
|
+
trades: list[TradeMessage],
|
|
239
|
+
token_id: str,
|
|
240
|
+
outcome: str,
|
|
241
|
+
*,
|
|
242
|
+
warn_failed: bool,
|
|
195
243
|
) -> UserPosition | None:
|
|
244
|
+
if not trades:
|
|
245
|
+
return None
|
|
246
|
+
|
|
196
247
|
def _status_is(trade: TradeMessage, status: TradeStatus) -> bool:
|
|
197
248
|
return trade.status == status or trade.status == status.value
|
|
198
249
|
|
|
@@ -202,22 +253,23 @@ class PositionStore:
|
|
|
202
253
|
i for i in success_trades if _status_is(i, TradeStatus.CONFIRMED)
|
|
203
254
|
]
|
|
204
255
|
has_failed = bool(len(failed_trades))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
if warn_failed:
|
|
257
|
+
new_failed_trades = [
|
|
258
|
+
trade
|
|
259
|
+
for trade in failed_trades
|
|
260
|
+
if (token_id, trade.id) not in self._warned_failed_trade_keys
|
|
261
|
+
]
|
|
262
|
+
if new_failed_trades:
|
|
263
|
+
self._warned_failed_trade_keys.update(
|
|
264
|
+
(token_id, trade.id) for trade in new_failed_trades
|
|
265
|
+
)
|
|
266
|
+
failed_size = sum(i.size for i in new_failed_trades)
|
|
267
|
+
failed_trade_ids = [trade.id for trade in new_failed_trades]
|
|
268
|
+
logger.warning(
|
|
269
|
+
"Found failed trades, total size: {}, ids: {}",
|
|
270
|
+
failed_size,
|
|
271
|
+
failed_trade_ids,
|
|
272
|
+
)
|
|
221
273
|
market_slug = next(
|
|
222
274
|
(trade.market_slug for trade in trades if trade.market_slug), ""
|
|
223
275
|
)
|
|
@@ -238,7 +290,7 @@ class PositionStore:
|
|
|
238
290
|
fee_calc_fn=self.fee_calc_fn,
|
|
239
291
|
)
|
|
240
292
|
sellable_size = confirmed_result.size
|
|
241
|
-
|
|
293
|
+
return UserPosition(
|
|
242
294
|
price=position_result.avg_price,
|
|
243
295
|
size=position_result.size,
|
|
244
296
|
original_size=position_result.original_size,
|
|
@@ -258,7 +310,16 @@ class PositionStore:
|
|
|
258
310
|
market_slug=market_slug,
|
|
259
311
|
failed_trades=failed_trades,
|
|
260
312
|
)
|
|
261
|
-
|
|
313
|
+
|
|
314
|
+
def build_position(
|
|
315
|
+
self, trades: list[TradeMessage], token_id, outcome: str
|
|
316
|
+
) -> UserPosition | None:
|
|
317
|
+
return self._build_user_position(
|
|
318
|
+
trades=trades,
|
|
319
|
+
token_id=token_id,
|
|
320
|
+
outcome=outcome,
|
|
321
|
+
warn_failed=True,
|
|
322
|
+
)
|
|
262
323
|
# if exists_pos := self.positions.get(token_id):
|
|
263
324
|
# if exists_pos.last_update < current.last_update:
|
|
264
325
|
# return current
|
|
@@ -278,6 +339,53 @@ class PositionStore:
|
|
|
278
339
|
def get_order_by_id(self, order_id: str) -> OrderMessage:
|
|
279
340
|
return self.orders.get(order_id)
|
|
280
341
|
|
|
342
|
+
def get_positions_by_order_ids(
|
|
343
|
+
self, order_ids: list[str]
|
|
344
|
+
) -> Dict[str, UserPosition]:
|
|
345
|
+
with self._lock:
|
|
346
|
+
trade_ids: set[str] = set()
|
|
347
|
+
for order_id in order_ids:
|
|
348
|
+
if order := self.orders.get(order_id):
|
|
349
|
+
trade_ids.update(order.associate_trades or [])
|
|
350
|
+
trade_ids.update(self.trade_ids_by_order.get(order_id, set()))
|
|
351
|
+
|
|
352
|
+
grouped_trades: Dict[str, list[TradeMessage]] = defaultdict(list)
|
|
353
|
+
outcomes: Dict[str, str] = {}
|
|
354
|
+
for trade_id in trade_ids:
|
|
355
|
+
trade = self.trades_by_id.get(trade_id)
|
|
356
|
+
if trade is None:
|
|
357
|
+
continue
|
|
358
|
+
result = self.get_token_id_from_trade(trade)
|
|
359
|
+
if result is None:
|
|
360
|
+
continue
|
|
361
|
+
outcome, token_id = result
|
|
362
|
+
grouped_trades[token_id].append(trade)
|
|
363
|
+
outcomes[token_id] = outcome
|
|
364
|
+
|
|
365
|
+
positions: Dict[str, UserPosition] = {}
|
|
366
|
+
for token_id, trades in grouped_trades.items():
|
|
367
|
+
position = self._build_user_position(
|
|
368
|
+
trades=trades,
|
|
369
|
+
token_id=token_id,
|
|
370
|
+
outcome=outcomes[token_id],
|
|
371
|
+
warn_failed=False,
|
|
372
|
+
)
|
|
373
|
+
if position is not None:
|
|
374
|
+
positions[token_id] = position
|
|
375
|
+
return positions
|
|
376
|
+
|
|
377
|
+
def get_position_by_order_ids(
|
|
378
|
+
self, order_ids: list[str]
|
|
379
|
+
) -> UserPosition | None:
|
|
380
|
+
positions = self.get_positions_by_order_ids(order_ids)
|
|
381
|
+
if not positions:
|
|
382
|
+
return None
|
|
383
|
+
if len(positions) > 1:
|
|
384
|
+
raise ValueError(
|
|
385
|
+
"order_ids resolve to multiple token positions; use get_positions_by_order_ids instead."
|
|
386
|
+
)
|
|
387
|
+
return next(iter(positions.values()))
|
|
388
|
+
|
|
281
389
|
def blocking_get_token_position(
|
|
282
390
|
self, token_id: str, timeout: float = None
|
|
283
391
|
) -> UserPosition:
|
|
@@ -662,6 +770,16 @@ class PositionWatcherService:
|
|
|
662
770
|
def get_order(self, order_id: str) -> OrderMessage:
|
|
663
771
|
return self.position_store.get_order_by_id(order_id)
|
|
664
772
|
|
|
773
|
+
def get_positions_by_order_ids(
|
|
774
|
+
self, order_ids: list[str]
|
|
775
|
+
) -> Dict[str, UserPosition]:
|
|
776
|
+
return self.position_store.get_positions_by_order_ids(order_ids)
|
|
777
|
+
|
|
778
|
+
def get_position_by_order_ids(
|
|
779
|
+
self, order_ids: list[str]
|
|
780
|
+
) -> UserPosition | None:
|
|
781
|
+
return self.position_store.get_position_by_order_ids(order_ids)
|
|
782
|
+
|
|
665
783
|
def blocking_get_position(
|
|
666
784
|
self, token_id: str, timeout: float = None
|
|
667
785
|
) -> UserPosition | None:
|
|
@@ -689,14 +807,20 @@ class PositionWatcherService:
|
|
|
689
807
|
# -------------------------------------------------------------------------
|
|
690
808
|
# HTTP Fallback Management (delegates to HttpFallbackManager)
|
|
691
809
|
# -------------------------------------------------------------------------
|
|
810
|
+
DEFAULT_HTTP_LISTEN_GROUP = HttpFallbackManager.DEFAULT_GROUP
|
|
811
|
+
|
|
692
812
|
def add_http_listen(
|
|
693
|
-
self,
|
|
813
|
+
self,
|
|
814
|
+
order_ids: list[str] = None,
|
|
815
|
+
market_ids: list[str] = None,
|
|
816
|
+
group: str = DEFAULT_HTTP_LISTEN_GROUP,
|
|
694
817
|
):
|
|
695
818
|
"""
|
|
696
819
|
Add markets/orders to HTTP fallback polling.
|
|
697
820
|
|
|
698
821
|
:param order_ids: List of order IDs to monitor
|
|
699
822
|
:param market_ids: List of market (condition) IDs to monitor
|
|
823
|
+
:param group: Optional namespace/group name for this caller
|
|
700
824
|
"""
|
|
701
825
|
if not self.enable_http_fallback or not self._http_fallback:
|
|
702
826
|
logger.warning(
|
|
@@ -704,47 +828,101 @@ class PositionWatcherService:
|
|
|
704
828
|
)
|
|
705
829
|
return
|
|
706
830
|
|
|
707
|
-
self._http_fallback.add(
|
|
831
|
+
self._http_fallback.add(
|
|
832
|
+
market_ids=market_ids,
|
|
833
|
+
order_ids=order_ids,
|
|
834
|
+
group=group,
|
|
835
|
+
)
|
|
708
836
|
|
|
709
837
|
def remove_http_listen(
|
|
710
|
-
self,
|
|
838
|
+
self,
|
|
839
|
+
order_ids: list[str] = None,
|
|
840
|
+
market_ids: list[str] = None,
|
|
841
|
+
group: str = DEFAULT_HTTP_LISTEN_GROUP,
|
|
711
842
|
):
|
|
712
843
|
"""
|
|
713
844
|
Remove markets/orders from HTTP fallback polling.
|
|
714
845
|
|
|
715
846
|
:param order_ids: List of order IDs to remove
|
|
716
847
|
:param market_ids: List of market (condition) IDs to remove
|
|
848
|
+
:param group: Optional namespace/group name for this caller
|
|
849
|
+
"""
|
|
850
|
+
if not self.enable_http_fallback or not self._http_fallback:
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
self._http_fallback.remove(
|
|
854
|
+
market_ids=market_ids,
|
|
855
|
+
order_ids=order_ids,
|
|
856
|
+
group=group,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
def set_http_listen(
|
|
860
|
+
self,
|
|
861
|
+
order_ids: list[str] = None,
|
|
862
|
+
market_ids: list[str] = None,
|
|
863
|
+
group: str = DEFAULT_HTTP_LISTEN_GROUP,
|
|
864
|
+
):
|
|
865
|
+
"""
|
|
866
|
+
Replace HTTP fallback monitoring for one group atomically.
|
|
867
|
+
|
|
868
|
+
:param order_ids: List of order IDs to monitor
|
|
869
|
+
:param market_ids: List of market (condition) IDs to monitor
|
|
870
|
+
:param group: Optional namespace/group name for this caller
|
|
717
871
|
"""
|
|
718
872
|
if not self.enable_http_fallback or not self._http_fallback:
|
|
719
873
|
return
|
|
720
874
|
|
|
721
|
-
self._http_fallback.
|
|
875
|
+
self._http_fallback.set_group(
|
|
876
|
+
group=group,
|
|
877
|
+
market_ids=market_ids,
|
|
878
|
+
order_ids=order_ids,
|
|
879
|
+
)
|
|
722
880
|
|
|
723
|
-
def set_market_http_listen(
|
|
881
|
+
def set_market_http_listen(
|
|
882
|
+
self,
|
|
883
|
+
market_ids: list[str] = None,
|
|
884
|
+
group: str = DEFAULT_HTTP_LISTEN_GROUP,
|
|
885
|
+
):
|
|
724
886
|
"""
|
|
725
887
|
Replace HTTP fallback market monitoring list.
|
|
726
888
|
|
|
727
889
|
:param market_ids: List of market (condition) IDs to monitor
|
|
890
|
+
:param group: Optional namespace/group name for this caller
|
|
728
891
|
"""
|
|
729
892
|
if not self.enable_http_fallback or not self._http_fallback:
|
|
730
893
|
return
|
|
731
894
|
|
|
732
|
-
self._http_fallback.set_markets(
|
|
895
|
+
self._http_fallback.set_markets(
|
|
896
|
+
market_ids=market_ids,
|
|
897
|
+
group=group,
|
|
898
|
+
)
|
|
733
899
|
|
|
734
|
-
def set_order_http_listen(
|
|
900
|
+
def set_order_http_listen(
|
|
901
|
+
self,
|
|
902
|
+
order_ids: list[str] = None,
|
|
903
|
+
group: str = DEFAULT_HTTP_LISTEN_GROUP,
|
|
904
|
+
):
|
|
735
905
|
"""
|
|
736
906
|
Replace HTTP fallback order monitoring list.
|
|
737
907
|
|
|
738
908
|
:param order_ids: List of order IDs to monitor
|
|
909
|
+
:param group: Optional namespace/group name for this caller
|
|
739
910
|
"""
|
|
740
911
|
if not self.enable_http_fallback or not self._http_fallback:
|
|
741
912
|
return
|
|
742
913
|
|
|
743
|
-
self._http_fallback.set_orders(
|
|
914
|
+
self._http_fallback.set_orders(
|
|
915
|
+
order_ids=order_ids,
|
|
916
|
+
group=group,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
def clear_http(self, group: str | None = None):
|
|
920
|
+
"""
|
|
921
|
+
Clear HTTP fallback monitoring.
|
|
744
922
|
|
|
745
|
-
|
|
746
|
-
"""
|
|
923
|
+
:param group: When provided, clear only that group; otherwise clear all groups.
|
|
924
|
+
"""
|
|
747
925
|
if not self.enable_http_fallback or not self._http_fallback:
|
|
748
926
|
return
|
|
749
927
|
|
|
750
|
-
self._http_fallback.clear()
|
|
928
|
+
self._http_fallback.clear(group=group)
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6/poly_position_watcher.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: poly-position-watcher
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: polymarket proxy wallet redeem
|
|
5
5
|
Home-page: https://github.com/tosmart01/polymarket-position-watcher
|
|
6
6
|
Author: pinbar
|
|
@@ -73,8 +73,14 @@ with PositionWatcherService(
|
|
|
73
73
|
|
|
74
74
|
# Non-blocking: Get current positions and orders (returns immediately)
|
|
75
75
|
position: UserPosition = service.get_position("<token_id>")
|
|
76
|
+
strategy_position: UserPosition | None = service.get_position_by_order_ids(["<order_id>"])
|
|
77
|
+
strategy_positions: dict[str, UserPosition] = service.get_positions_by_order_ids(
|
|
78
|
+
["<order_id_1>", "<order_id_2>"]
|
|
79
|
+
)
|
|
76
80
|
order: OrderMessage = service.get_order("<order_id>")
|
|
77
81
|
print(position)
|
|
82
|
+
print(strategy_position)
|
|
83
|
+
print(strategy_positions)
|
|
78
84
|
print(order)
|
|
79
85
|
if position:
|
|
80
86
|
print("size(post-fee):", position.size)
|
|
@@ -91,6 +97,12 @@ with PositionWatcherService(
|
|
|
91
97
|
|
|
92
98
|
# Optional: If you open new positions/orders and want to monitor them via HTTP fallback
|
|
93
99
|
# service.add_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
100
|
+
# service.set_http_listen(
|
|
101
|
+
# market_ids=["<condition_id>"],
|
|
102
|
+
# order_ids=["<order_id>"],
|
|
103
|
+
# group="strategy-a",
|
|
104
|
+
# )
|
|
105
|
+
# service.clear_http(group="strategy-a")
|
|
94
106
|
# service.remove_http_listen(market_ids=["<condition_id>"], order_ids=["<order_id>"])
|
|
95
107
|
# service.clear_http() # Clear all monitoring items, threads continue running
|
|
96
108
|
```
|
|
@@ -98,6 +110,8 @@ with PositionWatcherService(
|
|
|
98
110
|
Important:
|
|
99
111
|
- When `enable_fee_calc=True`, you must register market fee metadata with `set_market_fee_schedule(...)` or `set_market_fee_schedules(...)`.
|
|
100
112
|
- `get_position()` does not fetch `/markets` automatically.
|
|
113
|
+
- 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.
|
|
114
|
+
- 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.
|
|
101
115
|
- If a market is missing `feeSchedule`, fee calculation is skipped for that market and a warning is logged once.
|
|
102
116
|
|
|
103
117
|
Where does `feeSchedule` come from:
|
|
@@ -21,5 +21,6 @@ poly_position_watcher/schema/__init__.py
|
|
|
21
21
|
poly_position_watcher/schema/base.py
|
|
22
22
|
poly_position_watcher/schema/common_model.py
|
|
23
23
|
poly_position_watcher/schema/position_model.py
|
|
24
|
+
tests/test_http_fallback_manager.py
|
|
24
25
|
tests/test_position_model.py
|
|
25
26
|
tests/test_trade_calculator.py
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from poly_position_watcher.api_worker import APIWorker, HttpFallbackManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DummyPositionStore:
|
|
9
|
+
positions = {}
|
|
10
|
+
orders = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DummyService:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.api_worker = object()
|
|
16
|
+
self.position_store = DummyPositionStore()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HttpFallbackManagerTests(unittest.TestCase):
|
|
20
|
+
def test_default_group_keeps_old_api_compatible(self) -> None:
|
|
21
|
+
manager = HttpFallbackManager(DummyService(), http_poll_interval=1)
|
|
22
|
+
|
|
23
|
+
manager.add(market_ids=["market-1"], order_ids=["order-1"])
|
|
24
|
+
|
|
25
|
+
self.assertEqual(
|
|
26
|
+
manager.market_groups[HttpFallbackManager.DEFAULT_GROUP],
|
|
27
|
+
{"market-1"},
|
|
28
|
+
)
|
|
29
|
+
self.assertEqual(
|
|
30
|
+
manager.order_groups[HttpFallbackManager.DEFAULT_GROUP],
|
|
31
|
+
{"order-1"},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_group_sets_are_aggregated_across_namespaces(self) -> None:
|
|
35
|
+
manager = HttpFallbackManager(DummyService(), http_poll_interval=1)
|
|
36
|
+
|
|
37
|
+
manager.set_group(group="strategy-a", market_ids=["market-a"], order_ids=["order-a"])
|
|
38
|
+
manager.set_group(group="strategy-b", market_ids=["market-b"], order_ids=["order-b"])
|
|
39
|
+
|
|
40
|
+
self.assertEqual(manager._aggregated_markets_locked(), {"market-a", "market-b"})
|
|
41
|
+
self.assertEqual(manager._aggregated_orders_locked(), {"order-a", "order-b"})
|
|
42
|
+
|
|
43
|
+
def test_clear_one_group_does_not_affect_others(self) -> None:
|
|
44
|
+
manager = HttpFallbackManager(DummyService(), http_poll_interval=1)
|
|
45
|
+
|
|
46
|
+
manager.set_group(group="strategy-a", market_ids=["market-a"], order_ids=["order-a"])
|
|
47
|
+
manager.set_group(group="strategy-b", market_ids=["market-b"], order_ids=["order-b"])
|
|
48
|
+
manager.clear(group="strategy-a")
|
|
49
|
+
|
|
50
|
+
self.assertNotIn("strategy-a", manager.market_groups)
|
|
51
|
+
self.assertNotIn("strategy-a", manager.order_groups)
|
|
52
|
+
self.assertEqual(manager.market_groups["strategy-b"], {"market-b"})
|
|
53
|
+
self.assertEqual(manager.order_groups["strategy-b"], {"order-b"})
|
|
54
|
+
|
|
55
|
+
def test_remove_only_touches_target_group(self) -> None:
|
|
56
|
+
manager = HttpFallbackManager(DummyService(), http_poll_interval=1)
|
|
57
|
+
|
|
58
|
+
manager.add(group="strategy-a", market_ids=["market-a"], order_ids=["order-a"])
|
|
59
|
+
manager.add(group="strategy-b", market_ids=["market-a"], order_ids=["order-a"])
|
|
60
|
+
manager.remove(group="strategy-a", market_ids=["market-a"], order_ids=["order-a"])
|
|
61
|
+
|
|
62
|
+
self.assertNotIn("strategy-a", manager.market_groups)
|
|
63
|
+
self.assertNotIn("strategy-a", manager.order_groups)
|
|
64
|
+
self.assertEqual(manager.market_groups["strategy-b"], {"market-a"})
|
|
65
|
+
self.assertEqual(manager.order_groups["strategy-b"], {"order-a"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
unittest.main()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from poly_position_watcher.position_service import PositionStore
|
|
7
|
+
from poly_position_watcher.schema.position_model import OrderMessage, TradeMessage, UserPosition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_trade(trade_id: str, status: str = "FAILED", size: float = 10.0) -> TradeMessage:
|
|
11
|
+
return TradeMessage(
|
|
12
|
+
type="TRADE",
|
|
13
|
+
event_type="trade",
|
|
14
|
+
asset_id="0xtoken",
|
|
15
|
+
id=trade_id,
|
|
16
|
+
maker_orders=[],
|
|
17
|
+
transaction_hash=f"0xhash-{trade_id}",
|
|
18
|
+
market="0xmarket",
|
|
19
|
+
maker_address="0xuser",
|
|
20
|
+
outcome="YES",
|
|
21
|
+
owner="0xuser",
|
|
22
|
+
price=0.25,
|
|
23
|
+
side="BUY",
|
|
24
|
+
size=size,
|
|
25
|
+
status=status,
|
|
26
|
+
taker_order_id=f"0xorder-{trade_id}",
|
|
27
|
+
timestamp=1,
|
|
28
|
+
match_time=1,
|
|
29
|
+
last_update=1,
|
|
30
|
+
trade_owner="0xuser",
|
|
31
|
+
trader_side="TAKER",
|
|
32
|
+
fee_rate_bps=0,
|
|
33
|
+
market_slug="test-market",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_order(
|
|
38
|
+
order_id: str,
|
|
39
|
+
*,
|
|
40
|
+
asset_id: str = "0xtoken",
|
|
41
|
+
associate_trades: list[str] | None = None,
|
|
42
|
+
) -> OrderMessage:
|
|
43
|
+
return OrderMessage(
|
|
44
|
+
type="update",
|
|
45
|
+
event_type="order",
|
|
46
|
+
asset_id=asset_id,
|
|
47
|
+
associate_trades=associate_trades,
|
|
48
|
+
id=order_id,
|
|
49
|
+
market="0xmarket",
|
|
50
|
+
outcome="YES",
|
|
51
|
+
owner="0xuser",
|
|
52
|
+
price=0.25,
|
|
53
|
+
side="BUY",
|
|
54
|
+
size_matched=10.0,
|
|
55
|
+
timestamp=1,
|
|
56
|
+
status="LIVE",
|
|
57
|
+
market_slug="test-market",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UserPositionTests(unittest.TestCase):
|
|
62
|
+
def test_failed_trade_ids_are_exposed_in_string_output(self) -> None:
|
|
63
|
+
position = UserPosition(
|
|
64
|
+
price=0.25,
|
|
65
|
+
size=10.0,
|
|
66
|
+
original_size=10.0,
|
|
67
|
+
volume=2.5,
|
|
68
|
+
fee_amount=0.0,
|
|
69
|
+
sellable_size=10.0,
|
|
70
|
+
token_id="0xtoken",
|
|
71
|
+
last_update=1,
|
|
72
|
+
market_id="0xmarket",
|
|
73
|
+
outcome="YES",
|
|
74
|
+
has_failed=True,
|
|
75
|
+
failed_trades=[build_trade("failed-1"), build_trade("failed-2")],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.assertEqual(position.failed_trade_ids, ["failed-1", "failed-2"])
|
|
79
|
+
rendered = str(position)
|
|
80
|
+
self.assertIn("failed_trades: ['failed-1', 'failed-2']", rendered)
|
|
81
|
+
self.assertNotIn("transaction_hash", rendered)
|
|
82
|
+
|
|
83
|
+
def test_failed_trade_warning_logs_once_per_token_and_trade_id(self) -> None:
|
|
84
|
+
store = PositionStore(user_address="0xuser")
|
|
85
|
+
trades = [
|
|
86
|
+
build_trade("confirmed-1", status="CONFIRMED", size=5.0),
|
|
87
|
+
build_trade("failed-1", status="FAILED", size=10.0),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
with patch("poly_position_watcher.position_service.logger.warning") as warning:
|
|
91
|
+
store.build_position(trades=trades, token_id="0xtoken", outcome="YES")
|
|
92
|
+
store.build_position(trades=trades, token_id="0xtoken", outcome="YES")
|
|
93
|
+
|
|
94
|
+
warning.assert_called_once_with(
|
|
95
|
+
"Found failed trades, total size: {}, ids: {}",
|
|
96
|
+
10.0,
|
|
97
|
+
["failed-1"],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def test_get_position_by_order_ids_uses_order_trade_links(self) -> None:
|
|
101
|
+
store = PositionStore(user_address="0xuser")
|
|
102
|
+
trade_a = build_trade("trade-a", status="CONFIRMED", size=3.0)
|
|
103
|
+
trade_a.taker_order_id = "order-a"
|
|
104
|
+
trade_b = build_trade("trade-b", status="CONFIRMED", size=5.0)
|
|
105
|
+
trade_b.taker_order_id = "order-b"
|
|
106
|
+
store.append_trade(trade_a)
|
|
107
|
+
store.append_trade(trade_b)
|
|
108
|
+
store.append_order(build_order("order-a", associate_trades=["trade-a"]))
|
|
109
|
+
store.append_order(build_order("order-b", associate_trades=["trade-b"]))
|
|
110
|
+
|
|
111
|
+
position_a = store.get_position_by_order_ids(["order-a"])
|
|
112
|
+
combined_positions = store.get_positions_by_order_ids(["order-a", "order-b"])
|
|
113
|
+
|
|
114
|
+
self.assertIsNotNone(position_a)
|
|
115
|
+
self.assertEqual(position_a.token_id, "0xtoken")
|
|
116
|
+
self.assertEqual(position_a.size, 3.0)
|
|
117
|
+
self.assertIn("0xtoken", combined_positions)
|
|
118
|
+
self.assertEqual(combined_positions["0xtoken"].size, 8.0)
|
|
119
|
+
|
|
120
|
+
def test_get_position_by_order_ids_falls_back_to_trade_index_when_order_missing_associate_trades(self) -> None:
|
|
121
|
+
store = PositionStore(user_address="0xuser")
|
|
122
|
+
trade = build_trade("trade-a", status="CONFIRMED", size=4.0)
|
|
123
|
+
trade.taker_order_id = "order-a"
|
|
124
|
+
store.append_trade(trade)
|
|
125
|
+
store.append_order(build_order("order-a", associate_trades=None))
|
|
126
|
+
|
|
127
|
+
position = store.get_position_by_order_ids(["order-a"])
|
|
128
|
+
|
|
129
|
+
self.assertIsNotNone(position)
|
|
130
|
+
self.assertEqual(position.size, 4.0)
|
|
131
|
+
|
|
132
|
+
def test_trade_index_only_uses_current_user_related_order_ids(self) -> None:
|
|
133
|
+
store = PositionStore(user_address="0xuser")
|
|
134
|
+
trade = TradeMessage(
|
|
135
|
+
type="TRADE",
|
|
136
|
+
event_type="trade",
|
|
137
|
+
asset_id="0xup-token",
|
|
138
|
+
id="trade-realistic",
|
|
139
|
+
maker_orders=[
|
|
140
|
+
{
|
|
141
|
+
"order_id": "irrelevant-order-1",
|
|
142
|
+
"owner": "other-owner",
|
|
143
|
+
"maker_address": "0xother1",
|
|
144
|
+
"matched_amount": "20",
|
|
145
|
+
"price": "0.45",
|
|
146
|
+
"fee_rate_bps": "1000",
|
|
147
|
+
"asset_id": "0xdown-token",
|
|
148
|
+
"outcome": "Down",
|
|
149
|
+
"side": "BUY",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"order_id": "user-maker-order",
|
|
153
|
+
"owner": "user-owner",
|
|
154
|
+
"maker_address": "0xuser",
|
|
155
|
+
"matched_amount": "6",
|
|
156
|
+
"price": "0.55",
|
|
157
|
+
"fee_rate_bps": "1000",
|
|
158
|
+
"asset_id": "0xup-token",
|
|
159
|
+
"outcome": "Up",
|
|
160
|
+
"side": "SELL",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"order_id": "irrelevant-order-2",
|
|
164
|
+
"owner": "other-owner-2",
|
|
165
|
+
"maker_address": "0xother2",
|
|
166
|
+
"matched_amount": "40",
|
|
167
|
+
"price": "0.41",
|
|
168
|
+
"fee_rate_bps": "1000",
|
|
169
|
+
"asset_id": "0xdown-token",
|
|
170
|
+
"outcome": "Down",
|
|
171
|
+
"side": "BUY",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
transaction_hash="0xhash-realistic",
|
|
175
|
+
market="0xmarket",
|
|
176
|
+
maker_address="0xouter-not-user",
|
|
177
|
+
outcome="Up",
|
|
178
|
+
owner="outer-owner",
|
|
179
|
+
price=0.59,
|
|
180
|
+
side="BUY",
|
|
181
|
+
size=6.0,
|
|
182
|
+
status="CONFIRMED",
|
|
183
|
+
taker_order_id="outer-taker-order",
|
|
184
|
+
timestamp=1,
|
|
185
|
+
match_time=1,
|
|
186
|
+
last_update=1,
|
|
187
|
+
trade_owner="outer-owner",
|
|
188
|
+
trader_side="MAKER",
|
|
189
|
+
fee_rate_bps=1000,
|
|
190
|
+
market_slug="test-market",
|
|
191
|
+
)
|
|
192
|
+
store.append_trade(trade)
|
|
193
|
+
|
|
194
|
+
self.assertEqual(store.get_position_by_order_ids(["user-maker-order"]).size, -6.0)
|
|
195
|
+
self.assertIsNone(store.get_position_by_order_ids(["irrelevant-order-1"]))
|
|
196
|
+
self.assertIsNone(store.get_position_by_order_ids(["irrelevant-order-2"]))
|
|
197
|
+
self.assertIsNone(store.get_position_by_order_ids(["outer-taker-order"]))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
unittest.main()
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import unittest
|
|
4
|
-
from unittest.mock import patch
|
|
5
|
-
|
|
6
|
-
from poly_position_watcher.position_service import PositionStore
|
|
7
|
-
from poly_position_watcher.schema.position_model import TradeMessage, UserPosition
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def build_trade(trade_id: str, status: str = "FAILED", size: float = 10.0) -> TradeMessage:
|
|
11
|
-
return TradeMessage(
|
|
12
|
-
type="TRADE",
|
|
13
|
-
event_type="trade",
|
|
14
|
-
asset_id="0xtoken",
|
|
15
|
-
id=trade_id,
|
|
16
|
-
maker_orders=[],
|
|
17
|
-
transaction_hash=f"0xhash-{trade_id}",
|
|
18
|
-
market="0xmarket",
|
|
19
|
-
maker_address="0xuser",
|
|
20
|
-
outcome="YES",
|
|
21
|
-
owner="0xuser",
|
|
22
|
-
price=0.25,
|
|
23
|
-
side="BUY",
|
|
24
|
-
size=size,
|
|
25
|
-
status=status,
|
|
26
|
-
taker_order_id=f"0xorder-{trade_id}",
|
|
27
|
-
timestamp=1,
|
|
28
|
-
match_time=1,
|
|
29
|
-
last_update=1,
|
|
30
|
-
trade_owner="0xuser",
|
|
31
|
-
trader_side="TAKER",
|
|
32
|
-
fee_rate_bps=0,
|
|
33
|
-
market_slug="test-market",
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class UserPositionTests(unittest.TestCase):
|
|
38
|
-
def test_failed_trade_ids_are_exposed_in_string_output(self) -> None:
|
|
39
|
-
position = UserPosition(
|
|
40
|
-
price=0.25,
|
|
41
|
-
size=10.0,
|
|
42
|
-
original_size=10.0,
|
|
43
|
-
volume=2.5,
|
|
44
|
-
fee_amount=0.0,
|
|
45
|
-
sellable_size=10.0,
|
|
46
|
-
token_id="0xtoken",
|
|
47
|
-
last_update=1,
|
|
48
|
-
market_id="0xmarket",
|
|
49
|
-
outcome="YES",
|
|
50
|
-
has_failed=True,
|
|
51
|
-
failed_trades=[build_trade("failed-1"), build_trade("failed-2")],
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
self.assertEqual(position.failed_trade_ids, ["failed-1", "failed-2"])
|
|
55
|
-
rendered = str(position)
|
|
56
|
-
self.assertIn("failed_trades: ['failed-1', 'failed-2']", rendered)
|
|
57
|
-
self.assertNotIn("transaction_hash", rendered)
|
|
58
|
-
|
|
59
|
-
def test_failed_trade_warning_logs_once_per_token_and_trade_id(self) -> None:
|
|
60
|
-
store = PositionStore(user_address="0xuser")
|
|
61
|
-
trades = [
|
|
62
|
-
build_trade("confirmed-1", status="CONFIRMED", size=5.0),
|
|
63
|
-
build_trade("failed-1", status="FAILED", size=10.0),
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
with patch("poly_position_watcher.position_service.logger.warning") as warning:
|
|
67
|
-
store.build_position(trades=trades, token_id="0xtoken", outcome="YES")
|
|
68
|
-
store.build_position(trades=trades, token_id="0xtoken", outcome="YES")
|
|
69
|
-
|
|
70
|
-
warning.assert_called_once_with(
|
|
71
|
-
"Found failed trades, total size: {}, ids: {}",
|
|
72
|
-
10.0,
|
|
73
|
-
["failed-1"],
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if __name__ == "__main__":
|
|
78
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/enums.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/common/logger.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/schema/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{poly_position_watcher-0.3.4 → poly_position_watcher-0.3.6}/poly_position_watcher/wss_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|