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.
- {poly_position_watcher-0.3.2/poly_position_watcher.egg-info → poly_position_watcher-0.3.4}/PKG-INFO +1 -1
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/_version.py +1 -1
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/api_worker.py +4 -4
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/position_service.py +18 -5
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/position_model.py +12 -2
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/trade_calculator.py +7 -5
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4/poly_position_watcher.egg-info}/PKG-INFO +1 -1
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/pyproject.toml +1 -1
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/tests/test_position_model.py +23 -4
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/tests/test_trade_calculator.py +39 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/LICENSE +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/MANIFEST.in +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/README.md +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/__init__.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/__init__.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/enums.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/logger.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/__init__.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/base.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/common_model.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/wss_worker.py +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/SOURCES.txt +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/dependency_links.txt +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/requires.txt +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher.egg-info/top_level.txt +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/setup.cfg +0 -0
- {poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/setup.py +0 -0
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/api_worker.py
RENAMED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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=
|
|
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.
|
|
69
|
-
if
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
224
|
+
last_update=max((trade.event_time for trade in trades), default=0),
|
|
223
225
|
)
|
|
224
226
|
|
|
225
227
|
|
|
@@ -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
|
|
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=
|
|
23
|
-
status=
|
|
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=[
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/enums.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/common/logger.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/__init__.py
RENAMED
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/poly_position_watcher/schema/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{poly_position_watcher-0.3.2 → poly_position_watcher-0.3.4}/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
|