poly-position-watcher 0.3.2__tar.gz → 0.3.4__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.
Files changed (27) hide show
  1. {poly_position_watcher-0.3.2/poly_position_watcher.egg-info → poly_position_watcher-0.3.4}/PKG-INFO +1 -1
  2. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/_version.py +1 -1
  3. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/api_worker.py +4 -4
  4. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/position_service.py +18 -5
  5. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/position_model.py +12 -2
  6. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/trade_calculator.py +7 -5
  7. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4/poly_position_watcher.egg-info}/PKG-INFO +1 -1
  8. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/pyproject.toml +1 -1
  9. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/tests/test_position_model.py +23 -4
  10. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/tests/test_trade_calculator.py +39 -0
  11. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/LICENSE +0 -0
  12. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/MANIFEST.in +0 -0
  13. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/README.md +0 -0
  14. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/__init__.py +0 -0
  15. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/__init__.py +0 -0
  16. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/enums.py +0 -0
  17. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/logger.py +0 -0
  18. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/__init__.py +0 -0
  19. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/base.py +0 -0
  20. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/common_model.py +0 -0
  21. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/wss_worker.py +0 -0
  22. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/SOURCES.txt +0 -0
  23. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/dependency_links.txt +0 -0
  24. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/requires.txt +0 -0
  25. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/top_level.txt +0 -0
  26. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/setup.cfg +0 -0
  27. {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poly-position-watcher
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: polymarket proxy wallet redeem
5
5
  Home-page: https://github.com/tosmart01/polymarket-position-watcher
6
6
  Author: pinbar
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.3.4"
@@ -117,7 +117,7 @@ class APIWorker:
117
117
  Uses taker mode (maker_orders=[]) so trade_calculator will use outer fields directly.
118
118
 
119
119
  :param user_address: User wallet address
120
- :return: List of TradeMessage instances sorted by match_time
120
+ :return: List of TradeMessage instances sorted by effective event time
121
121
  """
122
122
  positions = self.fetch_positions(user_address)
123
123
  if not positions:
@@ -315,7 +315,7 @@ class HttpFallbackManager:
315
315
  logger.error(f"Failed to http fetch trades market {task._market_id}: {e}")
316
316
  continue
317
317
 
318
- for trade in sorted(trades, key=lambda x: x.match_time or 0):
318
+ for trade in sorted(trades, key=lambda x: x.event_time):
319
319
  self.service._ingest_trade(trade)
320
320
  except Exception as e:
321
321
  logger.error(f"Error in trade loop: {e}")
@@ -563,9 +563,9 @@ class HttpListenerContext:
563
563
  logger.error(f"Failed to http fetch trades market {task._market_id}: {e}")
564
564
  continue
565
565
  if is_init:
566
- self.service._init_trades(sorted(trades, key=lambda x: x.match_time))
566
+ self.service._init_trades(sorted(trades, key=lambda x: x.event_time))
567
567
  else:
568
- for trade in sorted(trades, key=lambda x: x.match_time):
568
+ for trade in sorted(trades, key=lambda x: x.event_time):
569
569
  self.service._ingest_trade(trade)
570
570
 
571
571
  def sync_order_from_http(self):
@@ -56,6 +56,7 @@ class PositionStore:
56
56
  self.trades_by_token: Dict[str, Dict[str, TradeMessage]] = defaultdict(dict)
57
57
  self.positions: Dict[str, UserPosition] = {}
58
58
  self.orders: Dict[str, OrderMessage] = {}
59
+ self._warned_failed_trade_keys: set[tuple[str, str]] = set()
59
60
  self._lock = threading.RLock()
60
61
  self.queue_dict: Dict[str, Queue] = {}
61
62
 
@@ -96,7 +97,7 @@ class PositionStore:
96
97
  outcome, token_id = result
97
98
  trades_map = self.trades_by_token[token_id]
98
99
  existing = trades_map.get(trade.id)
99
- if existing and trade.last_update < existing.last_update:
100
+ if existing and trade.event_time < existing.event_time:
100
101
  return
101
102
  else:
102
103
  trades_map[trade.id] = trade
@@ -201,9 +202,17 @@ class PositionStore:
201
202
  i for i in success_trades if _status_is(i, TradeStatus.CONFIRMED)
202
203
  ]
203
204
  has_failed = bool(len(failed_trades))
204
- if has_failed:
205
- failed_size = sum(i.size for i in failed_trades)
206
- failed_trade_ids = [trade.id for trade in failed_trades]
205
+ new_failed_trades = [
206
+ trade
207
+ for trade in failed_trades
208
+ if (token_id, trade.id) not in self._warned_failed_trade_keys
209
+ ]
210
+ if new_failed_trades:
211
+ self._warned_failed_trade_keys.update(
212
+ (token_id, trade.id) for trade in new_failed_trades
213
+ )
214
+ failed_size = sum(i.size for i in new_failed_trades)
215
+ failed_trade_ids = [trade.id for trade in new_failed_trades]
207
216
  logger.warning(
208
217
  "Found failed trades, total size: {}, ids: {}",
209
218
  failed_size,
@@ -240,7 +249,11 @@ class PositionStore:
240
249
  last_update=position_result.last_update,
241
250
  market_id=trades[0].market,
242
251
  outcome=outcome,
243
- created_at=datetime.fromtimestamp(position_result.last_update),
252
+ created_at=(
253
+ datetime.fromtimestamp(position_result.last_update)
254
+ if position_result.last_update > 0
255
+ else None
256
+ ),
244
257
  has_failed=has_failed,
245
258
  market_slug=market_slug,
246
259
  failed_trades=failed_trades,
@@ -63,10 +63,20 @@ class TradeMessage(BaseModel):
63
63
  created_at: datetime | None = None
64
64
  market_slug: Optional[str] = ""
65
65
 
66
+ @property
67
+ def event_time(self) -> int:
68
+ return self.match_time or self.last_update or self.timestamp or 0
69
+
66
70
  @model_validator(mode="after")
67
71
  def validate_datetime(self):
68
- base_ts = self.match_time or self.last_update
69
- if base_ts:
72
+ base_ts = self.event_time
73
+ if self.match_time is None:
74
+ self.match_time = base_ts or None
75
+ if self.last_update is None:
76
+ self.last_update = base_ts or None
77
+ if self.timestamp is None:
78
+ self.timestamp = base_ts or None
79
+ if base_ts > 0:
70
80
  self.created_at = datetime.fromtimestamp(base_ts)
71
81
  return self
72
82
 
@@ -102,6 +102,7 @@ def calculate_position_from_trades(
102
102
  if order.maker_address.upper() != user_address.upper():
103
103
  continue
104
104
  is_maker_order = True
105
+ event_time = trade.event_time
105
106
  size, fee_amount = apply_fee(
106
107
  order.size,
107
108
  order.price,
@@ -113,7 +114,7 @@ def calculate_position_from_trades(
113
114
 
114
115
  if order.side == Side.BUY:
115
116
  buy_events.append(
116
- (size, order.price, trade.match_time, order.size * order.price)
117
+ (size, order.price, event_time, order.size * order.price)
117
118
  )
118
119
  total_original_size += order.size
119
120
  else:
@@ -121,7 +122,7 @@ def calculate_position_from_trades(
121
122
  (
122
123
  -size,
123
124
  order.price,
124
- trade.match_time,
125
+ event_time,
125
126
  size * order.price - fee_amount,
126
127
  )
127
128
  )
@@ -129,6 +130,7 @@ def calculate_position_from_trades(
129
130
 
130
131
  # taker 部分
131
132
  if not is_maker_order and trade.maker_address.upper() == user_address.upper():
133
+ event_time = trade.event_time
132
134
  size, fee_amount = apply_fee(
133
135
  trade.size,
134
136
  trade.price,
@@ -139,7 +141,7 @@ def calculate_position_from_trades(
139
141
  total_fee_amount += fee_amount
140
142
  if trade.side == Side.BUY:
141
143
  buy_events.append(
142
- (size, trade.price, trade.match_time, trade.size * trade.price)
144
+ (size, trade.price, event_time, trade.size * trade.price)
143
145
  )
144
146
  total_original_size += trade.size
145
147
  else:
@@ -147,7 +149,7 @@ def calculate_position_from_trades(
147
149
  (
148
150
  -size,
149
151
  trade.price,
150
- trade.match_time,
152
+ event_time,
151
153
  size * trade.price - fee_amount,
152
154
  )
153
155
  )
@@ -219,7 +221,7 @@ def calculate_position_from_trades(
219
221
  sell_events=len(sell_events),
220
222
  total_trades=len(all_events),
221
223
  ),
222
- last_update=max([i.match_time for i in trades]) if trades else None,
224
+ last_update=max((trade.event_time for trade in trades), default=0),
223
225
  )
224
226
 
225
227
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poly-position-watcher
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: polymarket proxy wallet redeem
5
5
  Home-page: https://github.com/tosmart01/polymarket-position-watcher
6
6
  Author: pinbar
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "poly-position-watcher"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "polymarket proxy wallet redeem"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import unittest
4
+ from unittest.mock import patch
4
5
 
6
+ from poly_position_watcher.position_service import PositionStore
5
7
  from poly_position_watcher.schema.position_model import TradeMessage, UserPosition
6
8
 
7
9
 
8
- def build_failed_trade(trade_id: str) -> TradeMessage:
10
+ def build_trade(trade_id: str, status: str = "FAILED", size: float = 10.0) -> TradeMessage:
9
11
  return TradeMessage(
10
12
  type="TRADE",
11
13
  event_type="trade",
@@ -19,8 +21,8 @@ def build_failed_trade(trade_id: str) -> TradeMessage:
19
21
  owner="0xuser",
20
22
  price=0.25,
21
23
  side="BUY",
22
- size=10.0,
23
- status="FAILED",
24
+ size=size,
25
+ status=status,
24
26
  taker_order_id=f"0xorder-{trade_id}",
25
27
  timestamp=1,
26
28
  match_time=1,
@@ -46,7 +48,7 @@ class UserPositionTests(unittest.TestCase):
46
48
  market_id="0xmarket",
47
49
  outcome="YES",
48
50
  has_failed=True,
49
- failed_trades=[build_failed_trade("failed-1"), build_failed_trade("failed-2")],
51
+ failed_trades=[build_trade("failed-1"), build_trade("failed-2")],
50
52
  )
51
53
 
52
54
  self.assertEqual(position.failed_trade_ids, ["failed-1", "failed-2"])
@@ -54,6 +56,23 @@ class UserPositionTests(unittest.TestCase):
54
56
  self.assertIn("failed_trades: ['failed-1', 'failed-2']", rendered)
55
57
  self.assertNotIn("transaction_hash", rendered)
56
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
+
57
76
 
58
77
  if __name__ == "__main__":
59
78
  unittest.main()
@@ -212,6 +212,45 @@ class TradeCalculatorFeeTests(unittest.TestCase):
212
212
  math.isclose(result.fee_amount, buy_fee + sell_fee, rel_tol=0, abs_tol=1e-9)
213
213
  )
214
214
 
215
+ def test_last_update_falls_back_to_last_update_when_match_time_is_missing(self) -> None:
216
+ trade = build_taker_trade(
217
+ trade_id="buy-missing-match-time",
218
+ side=Side.BUY.value,
219
+ size=10.0,
220
+ price=0.25,
221
+ match_time=1,
222
+ )
223
+ trade.match_time = None
224
+ trade.last_update = 123
225
+
226
+ result = calculate_position_from_trades(
227
+ [trade],
228
+ user_address=USER_ADDRESS,
229
+ enable_fee_calc=False,
230
+ )
231
+
232
+ self.assertEqual(result.last_update, 123)
233
+
234
+ def test_last_update_defaults_to_zero_when_all_trade_timestamps_are_missing(self) -> None:
235
+ trade = build_taker_trade(
236
+ trade_id="buy-missing-all-times",
237
+ side=Side.BUY.value,
238
+ size=10.0,
239
+ price=0.25,
240
+ match_time=1,
241
+ )
242
+ trade.match_time = None
243
+ trade.last_update = None
244
+ trade.timestamp = None
245
+
246
+ result = calculate_position_from_trades(
247
+ [trade],
248
+ user_address=USER_ADDRESS,
249
+ enable_fee_calc=False,
250
+ )
251
+
252
+ self.assertEqual(result.last_update, 0)
253
+
215
254
 
216
255
  if __name__ == "__main__":
217
256
  unittest.main()