poly-position-watcher 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,460 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/1 16:16
3
+ # @Author: pinbar
4
+ # @Site:
5
+ # @File: wss.py
6
+ # @Software: PyCharm
7
+ import json
8
+ import threading
9
+ import time
10
+ from typing import Callable, List, Optional
11
+
12
+ import requests
13
+
14
+ from websocket import WebSocketApp, WebSocket
15
+
16
+ from poly_position_watcher.common.enums import MarketEvent
17
+ from poly_position_watcher.common.logger import logger
18
+ from poly_position_watcher.schema.common_model import OrderBookSummary
19
+
20
+ WSS_URL = "wss://ws-subscriptions-clob.polymarket.com"
21
+ MARKET_CHANNEL = "market"
22
+ USER_CHANNEL = "user"
23
+
24
+
25
+ def json_dumps(msg: dict) -> str:
26
+ return json.dumps(msg, indent=4, ensure_ascii=False)
27
+
28
+
29
+ def fetch_order_books(asset_ids: list[str]) -> list[OrderBookSummary]:
30
+ """
31
+ Fetch an initial snapshot for every asset id before subscribing to the WS stream.
32
+ """
33
+ if not asset_ids:
34
+ return []
35
+ url = "https://clob.polymarket.com/books"
36
+ payload = [{"token_id": token_id} for token_id in asset_ids]
37
+ response = requests.post(
38
+ url, json=payload, headers={"Content-Type": "application/json"}
39
+ )
40
+ response.raise_for_status()
41
+ books = response.json()
42
+ return [OrderBookSummary(**book) for book in books]
43
+
44
+
45
+ class PolymarketUserWS:
46
+ def __init__(
47
+ self,
48
+ api_key: str,
49
+ api_secret: str,
50
+ api_passphrase: str,
51
+ markets: Optional[List[str]] = None,
52
+ ping_interval: int = 10,
53
+ ping_timeout: int = 6,
54
+ idle_timeout: Optional[int] = 60 * 60,
55
+ reconnect_delay: int = 5,
56
+ on_message_callback: Optional[Callable[[dict], None]] = None,
57
+ wss_proxies: Optional[dict] = None,
58
+ ):
59
+ """
60
+ :param markets: 订阅的 condition_ids 列表,为 None 或 [] 时表示订阅全部(视后端协议而定)
61
+ :param ping_interval: WebSocket 库自带 PING 间隔(秒)
62
+ :param ping_timeout: WebSocket 库自带 PONG 超时时间(秒),超过这个时间视为连接异常
63
+ :param idle_timeout: 业务层“长时间没有任意消息”的超时(秒);为 None / 0 时关闭此功能
64
+ :param reconnect_delay: 断线后重连间隔(秒)
65
+ :param on_message_callback: 收到业务消息时的回调函数,参数为 dict
66
+ """
67
+ self.api_key = api_key
68
+ self.api_secret = api_secret
69
+ self.api_passphrase = api_passphrase
70
+ self.markets = markets or []
71
+
72
+ self.ping_interval = ping_interval
73
+ self.ping_timeout = ping_timeout
74
+ self.idle_timeout = idle_timeout
75
+ self.reconnect_delay = reconnect_delay
76
+
77
+ self.on_message_callback = on_message_callback
78
+ self._wss_proxies = wss_proxies or {}
79
+
80
+ self.ws: Optional[WebSocketApp] = None
81
+ self._stop = False
82
+
83
+ # 业务消息 / 任意消息的最近活跃时间
84
+ self._last_activity = time.time()
85
+
86
+ # 监控“长时间无消息”的线程
87
+ self._monitor_thread: Optional[threading.Thread] = None
88
+ self._monitor_stop_evt = threading.Event()
89
+
90
+ # ---------- 公共方法 ----------
91
+
92
+ def start(self):
93
+ """
94
+ 阻塞运行,带自动重连。
95
+ 使用 websocket-client 自带 ping/pong(ping_interval & ping_timeout)。
96
+ """
97
+ while not self._stop:
98
+ try:
99
+ logger.info("[WS] Connecting...")
100
+
101
+ self.ws = WebSocketApp(
102
+ f"{WSS_URL}/ws/user",
103
+ on_open=self._on_open,
104
+ on_message=self._on_message,
105
+ on_error=self._on_error,
106
+ on_close=self._on_close,
107
+ )
108
+
109
+ # 每次新连接重置最近活跃时间
110
+ self._last_activity = time.time()
111
+ self._monitor_stop_evt.clear()
112
+
113
+ # 如果配置了 idle_timeout,则启动一个监控线程
114
+ if self.idle_timeout and self.idle_timeout > 0:
115
+ self._monitor_thread = threading.Thread(
116
+ target=self._activity_monitor_loop,
117
+ daemon=True,
118
+ )
119
+ self._monitor_thread.start()
120
+ else:
121
+ self._monitor_thread = None
122
+
123
+ # 这里使用库自带的 ping_interval / ping_timeout,负责底层心跳与超时
124
+ self.ws.run_forever(
125
+ **self._wss_proxies,
126
+ ping_interval=self.ping_interval,
127
+ ping_timeout=self.ping_timeout,
128
+ )
129
+
130
+ except Exception as e:
131
+ logger.exception("[WS] run_forever error:")
132
+
133
+ # 当前连接生命周期结束,停止监控线程
134
+ self._monitor_stop_evt.set()
135
+ if self._monitor_thread and self._monitor_thread.is_alive():
136
+ self._monitor_thread.join(timeout=1)
137
+
138
+ if self._stop:
139
+ break
140
+
141
+ logger.info(
142
+ f"[WS] Disconnected, reconnecting in {self.reconnect_delay}s..."
143
+ )
144
+ time.sleep(self.reconnect_delay)
145
+
146
+ def stop(self):
147
+ """
148
+ 手动停止
149
+ """
150
+ self._stop = True
151
+ self._monitor_stop_evt.set()
152
+ if self.ws:
153
+ try:
154
+ self.ws.close()
155
+ except Exception:
156
+ pass
157
+
158
+ def _on_open(self, ws: WebSocket):
159
+ logger.info("[WS] Opened")
160
+
161
+ auth = {
162
+ "apiKey": self.api_key,
163
+ "secret": self.api_secret,
164
+ "passphrase": self.api_passphrase,
165
+ }
166
+
167
+ sub_msg = {
168
+ "type": "USER", # 文档要求 - 订阅 user channel
169
+ "auth": auth,
170
+ }
171
+
172
+ if self.markets:
173
+ sub_msg["markets"] = self.markets
174
+ else:
175
+ sub_msg["markets"] = []
176
+
177
+ ws.send(json.dumps(sub_msg))
178
+ logger.info("[WS] Sent subscribe: \n{}", json_dumps(sub_msg))
179
+
180
+ def _on_message(self, ws: WebSocket, message: str):
181
+ # 收到任何消息都视为有“活跃”
182
+ self._last_activity = time.time()
183
+
184
+ # 对方如果返回的是纯文本心跳,可在这里过滤
185
+ # 但 websocket-client 的 ping/pong 是底层帧,不会走到这里
186
+ if str(message) == "PONG":
187
+ return
188
+
189
+ try:
190
+ data = json.loads(message)
191
+ except json.JSONDecodeError:
192
+ # 对于非 JSON 消息,按需处理或直接打印
193
+ logger.warning("[WS] Non-JSON message: {}", message)
194
+ return
195
+
196
+ if self.on_message_callback:
197
+ try:
198
+ self.on_message_callback(data)
199
+ except Exception as e:
200
+ logger.exception("[WS] on_message_callback error:")
201
+
202
+ def _on_error(self, ws: WebSocket, error):
203
+ logger.error("[WS] Error: {}", error)
204
+ # 出现错误时关闭连接,交给外层循环重连
205
+ try:
206
+ ws.close()
207
+ except Exception:
208
+ pass
209
+
210
+ def _on_close(self, ws: WebSocket, close_status_code, close_msg):
211
+ logger.info(f"[WS] Closed: code={close_status_code}, msg={close_msg}")
212
+
213
+ # ---------- 业务层“长时间无消息”监控 ----------
214
+
215
+ def _activity_monitor_loop(self):
216
+ """
217
+ 仅在配置了 idle_timeout 时启用:
218
+ 如果长时间(idle_timeout 秒)没有收到任何消息,则主动关闭连接,
219
+ 交由外层循环进行重连。
220
+ """
221
+ while not self._monitor_stop_evt.is_set():
222
+ now = time.time()
223
+ if now - self._last_activity > self.idle_timeout:
224
+ logger.info(
225
+ f"[WS] No activity for {self.idle_timeout}s, "
226
+ f"closing connection to force reconnect..."
227
+ )
228
+ try:
229
+ if self.ws:
230
+ self.ws.close()
231
+ except Exception:
232
+ pass
233
+ # 触发一次关闭即可,退出监控线程
234
+ break
235
+
236
+ # 等待 1 秒再检查,可按需要调整粒度
237
+ self._monitor_stop_evt.wait(1)
238
+
239
+
240
+ class OrderBookWS:
241
+ def __init__(
242
+ self,
243
+ asset_ids: list[str],
244
+ url: str = "wss://ws-subscriptions-clob.polymarket.com",
245
+ event_name: str = "",
246
+ ping_interval: int = 10,
247
+ ping_timeout: int = 6, # 如需用 websocket 内建 ping,可通过 run_forever 传入
248
+ idle_timeout: Optional[int] = 60 * 60,
249
+ reconnect_delay: int = 5,
250
+ callback: Callable = None,
251
+ wss_proxies: Optional[dict] = None,
252
+ ):
253
+ """
254
+ :param asset_ids: 订阅的 asset_id 列表
255
+ :param url: 基础 ws url
256
+ :param event_name: 日志里的事件名,仅用于调试
257
+ :param ping_interval: 自定义业务层 "PING" 间隔(秒)
258
+ :param ping_timeout: 预留,如果想用 websocket 内建 ping/pong,可透传给 run_forever
259
+ :param idle_timeout: 若长时间未收到任何消息(秒),则主动断开重连;为 None/0 表示关闭
260
+ :param reconnect_delay: 断线后重连间隔(秒)
261
+ """
262
+ self.url = url
263
+ self.asset_ids = asset_ids
264
+ self.event_name = event_name
265
+
266
+ self.ping_interval = ping_interval
267
+ self.ping_timeout = ping_timeout
268
+ self.idle_timeout = idle_timeout
269
+ self.reconnect_delay = reconnect_delay
270
+
271
+ self.ws: Optional[WebSocketApp] = None
272
+ self._stop = False
273
+
274
+ # 活跃时间(收到任意消息时更新)
275
+ self._last_activity = time.time()
276
+
277
+ # “长时间无消息”监控线程
278
+ self._monitor_thread: Optional[threading.Thread] = None
279
+ self._monitor_stop_evt = threading.Event()
280
+
281
+ self.order_books: dict[str, OrderBookSummary] = {}
282
+
283
+ self._furl = url.rstrip("/") + "/ws/" + MARKET_CHANNEL
284
+ self.callback = callback
285
+ self._wss_proxies = wss_proxies or {}
286
+
287
+ self.initialize()
288
+
289
+ # ---------- 初始化 order book 快照 ----------
290
+
291
+ def initialize(self):
292
+ books = fetch_order_books(self.asset_ids)
293
+ for book in books:
294
+ self.order_books[book.asset_id] = book
295
+
296
+ # ---------- 公共方法 ----------
297
+
298
+ def start(self):
299
+ """
300
+ 阻塞运行,带自动重连逻辑。
301
+ """
302
+ while not self._stop:
303
+ try:
304
+ logger.info("[OrderBookWS] Connecting to {} ...", self._furl)
305
+
306
+ self.ws = WebSocketApp(
307
+ self._furl,
308
+ on_message=self._on_message,
309
+ on_error=self._on_error,
310
+ on_close=self._on_close,
311
+ on_open=self._on_open,
312
+ )
313
+
314
+ # 重置状态
315
+ self._last_activity = time.time()
316
+ self._monitor_stop_evt.clear()
317
+
318
+ # 启动“长时间无消息”监控线程
319
+ if self.idle_timeout and self.idle_timeout > 0:
320
+ self._monitor_thread = threading.Thread(
321
+ target=self._activity_monitor_loop,
322
+ daemon=True,
323
+ )
324
+ self._monitor_thread.start()
325
+ else:
326
+ self._monitor_thread = None
327
+
328
+ self.ws.run_forever(
329
+ ping_interval=self.ping_interval,
330
+ ping_timeout=self.ping_timeout,
331
+ **self._wss_proxies,
332
+ )
333
+
334
+ except Exception as e:
335
+ logger.exception(
336
+ "[OrderBookWS] run_forever error",
337
+ )
338
+
339
+ # 连接生命周期结束,停止监控线程和 ping 线程
340
+ self._monitor_stop_evt.set()
341
+
342
+ if self._monitor_thread and self._monitor_thread.is_alive():
343
+ self._monitor_thread.join(timeout=1)
344
+
345
+ if self._stop:
346
+ break
347
+
348
+ logger.info(
349
+ f"[OrderBookWS] Disconnected, reconnecting in {self.reconnect_delay}",
350
+ )
351
+ time.sleep(self.reconnect_delay)
352
+
353
+ def stop(self):
354
+ """
355
+ 手动停止(优雅退出)
356
+ """
357
+ logger.info("[OrderBookWS] Stopping...")
358
+ self._stop = True
359
+ self._monitor_stop_evt.set()
360
+
361
+ if self.ws:
362
+ try:
363
+ self.ws.close()
364
+ except Exception:
365
+ logger.exception("[OrderBookWS] error when closing ws")
366
+
367
+ # ---------- WebSocket 回调 ----------
368
+
369
+ def _on_open(self, ws: WebSocket):
370
+ logger.info("[OrderBookWS] Opened")
371
+
372
+ sub_msg = {"assets_ids": self.asset_ids, "type": MARKET_CHANNEL}
373
+
374
+ ws.send(json.dumps(sub_msg))
375
+ logger.info("[OrderBookWS] Sent subscribe: {}", json_dumps(sub_msg))
376
+
377
+ def _on_callback(self):
378
+ if self.callback is not None:
379
+ try:
380
+ self.callback(self.order_books)
381
+ except Exception:
382
+ logger.exception(f"wss callback error")
383
+
384
+ def _on_message(self, ws: WebSocket, message: str):
385
+ # 收到任何消息都算活跃
386
+ self._last_activity = time.time()
387
+
388
+ try:
389
+ messages = json.loads(message)
390
+ if not isinstance(messages, list):
391
+ messages = [messages]
392
+ except json.decoder.JSONDecodeError:
393
+ # 非 JSON 消息直接忽略,或者按需处理
394
+ logger.debug(f"[OrderBookWS] Non-JSON message: {message}")
395
+ return
396
+
397
+ try:
398
+ for message in messages:
399
+ event = message["event_type"]
400
+ timestamp = int(message["timestamp"]) / 1000
401
+
402
+ if event == MarketEvent.PRICE_CHANGE:
403
+ for price in message["price_changes"]:
404
+ if price["asset_id"] in self.asset_ids:
405
+ self.order_books[price["asset_id"]].set_price(
406
+ price, timestamp
407
+ )
408
+ elif event == MarketEvent.TICK_SIZE_CHANGE:
409
+ self.order_books[message["asset_id"]].tick_size = message[
410
+ "new_tick_size"
411
+ ]
412
+ elif event == MarketEvent.BOOK:
413
+ order = self.order_books[message["asset_id"]]
414
+ self.order_books[message["asset_id"]] = order.model_validate(
415
+ {**order.model_dump(), **message}
416
+ )
417
+ self._on_callback()
418
+ except Exception:
419
+ logger.exception(f"[OrderBookWS] receive message error: {message}")
420
+ # 不再 exit(1),而是交给外层重连
421
+
422
+ def _on_error(self, ws: WebSocket, error):
423
+ logger.error(f"[OrderBookWS] Error: {error}")
424
+ # 出现错误时关闭连接,交给外层循环重连
425
+ try:
426
+ ws.close()
427
+ except Exception:
428
+ logger.exception("[OrderBookWS] error when closing on error")
429
+
430
+ def _on_close(self, ws: WebSocket, close_status_code, close_msg):
431
+ logger.info(f"[OrderBookWS] Closed: code={close_status_code}, msg={close_msg}")
432
+ # 不再 exit(0),让 run_forever 返回,外层 while 负责重连
433
+
434
+ # ---------- “长时间无消息”监控 ----------
435
+
436
+ def _activity_monitor_loop(self):
437
+ """
438
+ 如果长时间(idle_timeout 秒)没有收到任何消息,则主动关闭连接,
439
+ 交由外层循环进行重连。
440
+ """
441
+ while not self._monitor_stop_evt.is_set() and not self._stop:
442
+ now = time.time()
443
+ if self.idle_timeout and now - self._last_activity > self.idle_timeout:
444
+ logger.info(
445
+ f"[OrderBookWS] No activity for %ss, closing connection to force reconnect..., {self.idle_timeout}"
446
+ )
447
+ try:
448
+ if self.ws:
449
+ self.ws.close()
450
+ except Exception:
451
+ logger.exception("[OrderBookWS] error when closing in idle monitor")
452
+ break
453
+
454
+ # 每秒检查一次
455
+ self._monitor_stop_evt.wait(1)
456
+
457
+
458
+ def handle_user_message(msg: dict):
459
+ # 这里可以根据 type=TRADE / PLACEMENT / UPDATE 等自己做处理
460
+ logger.info("[USER EVENT], type: {}, msg: {}", msg.get("type"), json_dumps(msg))
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: poly-position-watcher
3
+ Version: 0.1.0
4
+ Summary: clear about domain + functionality.
5
+ Home-page: https://github.com/tosmart01/polymarket-position-watcher
6
+ Author: pinbar
7
+ Project-URL: Homepage, https://github.com/tosmart01/polymarket-position-watcher
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: py-clob-client>=0.25.0
12
+ Requires-Dist: websocket-client>=1.8.0
13
+ Dynamic: author
14
+ Dynamic: home-page
15
+ Dynamic: license-file
16
+ Dynamic: requires-python
17
+
18
+ # poly-position-watcher
19
+
20
+ `poly-position-watcher` 将我们在内部使用的仓位监控逻辑独立成了一个可复用的 Python 包,方便接入者基于 Polymarket 官方 `py-clob-client` SDK 快速搭建自己的仓位看板或做市机器人。该包负责:
21
+
22
+ - 通过 WebSocket 追踪实时 `TRADE` 与 `ORDER` 事件
23
+ - 把 HTTP API 的历史数据和 WebSocket 增量数据统一成同一套 Pydantic 模型
24
+ - 在内存中维护每个 `token_id` 的仓位、订单状态及阻塞式读取接口
25
+ - 提供易于扩展的 HTTP 轮询上下文(在 WebSocket 之外兜底同步)
26
+ - 内置 FIFO 仓位计算器,支持带市价估值与盈亏指标
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install poly-position-watcher
32
+ ```
33
+
34
+ 如果你是从源码安装,先克隆本仓库然后执行 `pip install -e .`。
35
+
36
+ ## 快速开始
37
+
38
+ ```python
39
+ from py_clob_client.client import ClobClient
40
+ from poly_position_watcher import PositionWatcherService
41
+
42
+ client = ClobClient(
43
+ base_url="https://clob.polymarket.com",
44
+ key="<wallet-key>",
45
+ secret="<wallet-secret>",
46
+ )
47
+
48
+ with PositionWatcherService(client=client) as service:
49
+ # 可选:HTTP 轮询兜底历史仓位
50
+ with service.http_listen(markets=["<condition_id>"], bootstrap_http=True):
51
+ position = service.blocking_get_position("<token_id>", timeout=5)
52
+ order = service.get_order("<order_id>")
53
+ print(position)
54
+ print(order)
55
+ ```
56
+
57
+ ### 进阶示例(`examples/http_bootstrap_example.py`)
58
+
59
+ 下面是一段更完整的示例脚本(对应 `examples/http_bootstrap_example.py`),演示如何在启动时通过 `http_listen` 获取历史订单 & 仓位,并实时追踪:
60
+
61
+ ```python
62
+ from py_clob_client.client import ClobClient
63
+ from poly_position_watcher import PositionWatcherService
64
+
65
+ client = ClobClient(...)
66
+ TARGET_MARKETS = ["0x3b7e9926575eb7fae204d27ee9d3c9db0f34d357e4b8c..."]
67
+ TARGET_ORDERS = ["0x74a71abb9efe59c994e0987fa81963aae23d7165f036afb..."]
68
+ token_id = ""
69
+
70
+ with PositionWatcherService(client=client) as service:
71
+ # 如果已经存在历史仓位,需要提前告诉 http_listen 所有关心的 markets / orders 并开启 bootstrap_http
72
+ with service.http_listen(markets=TARGET_MARKETS, orders=TARGET_ORDERS, bootstrap_http=True):
73
+ order = service.blocking_get_order(TARGET_ORDERS[0], timeout=5)
74
+ position = service.blocking_get_position(
75
+ token_id=token_id,
76
+ timeout=5,
77
+ )
78
+ print(order)
79
+ print(position)
80
+
81
+ ```
82
+
83
+ 示例输出:
84
+
85
+ ```shell
86
+ OrderMessage(
87
+ type: 'update',
88
+ event_type: 'order',
89
+ asset_id: '7718951783559279583290056782453440...',
90
+ associate_trades: ['8bf02a75-5...'],
91
+ id: '0x74a71abb9efe59c994e0...',
92
+ market: '0x3b7e9926575eb7fae2...',
93
+ order_owner: None,
94
+ original_size: 37.5,
95
+ outcome: 'Up',
96
+ owner: '',
97
+ price: 0.52,
98
+ side: 'BUY',
99
+ size_matched: 37.5,
100
+ timestamp: 0.0,
101
+ filled: True,
102
+ status: 'MATCHED',
103
+ created_at: datetime.datetime(2025, 12, 8, 9, 44, 50, tzinfo=TzInfo(0))
104
+ )
105
+ UserPosition(
106
+ price: 0.0,
107
+ size: 0.0,
108
+ volume: 0.0,
109
+ token_id: '',
110
+ last_update: 0.0,
111
+ market_id: None,
112
+ outcome: None,
113
+ created_at: None
114
+ )
115
+ ```
116
+
117
+ > ⚠️ 注意:如果你是先启动监控再产生仓位,可令 `bootstrap_http=False` 且 `markets/orders` 参数为空列表即可;只有当已经存在历史仓位/订单需要补偿时才需要提前传入,并开启 `bootstrap_http=True`。
118
+
119
+ ### 只使用 HTTP 轮询
120
+
121
+ `HttpListenerContext` 可在需要时单独使用:
122
+
123
+ ```python
124
+ with service.http_listen(markets=["<condition_id>"], http_poll_interval=2.5) as ctx:
125
+ ctx.add(markets=["other_condition_id"], orders=["<order_id>"])
126
+ ```
127
+
128
+ ## 可选配置
129
+
130
+ | 环境变量 | 说明 |
131
+ | --- | --- |
132
+ | `poly_position_watcher_LOG_LEVEL` | 调整日志级别,默认为 `INFO` |
133
+
134
+ 若需要为 WebSocket 连接设置代理,可在实例化 `PositionWatcherService` 及 `http_listen` 前自行构造一个字典并通过 `wss_proxies` 传入,例如:
135
+
136
+ ```python
137
+ PROXY = {"http_proxy_host": "127.0.0.1", "http_proxy_port": 7890}
138
+ service = PositionWatcherService(client, wss_proxies=PROXY)
139
+ ```
140
+
141
+ ## 依赖
142
+
143
+ - [`py-clob-client`](https://github.com/Polymarket/py-clob-client)
144
+ - [`pydantic`](https://docs.pydantic.dev/)
145
+ - [`websocket-client`](https://github.com/websocket-client/websocket-client)
146
+ - [`requests`](https://requests.readthedocs.io/en/latest/)
147
+
148
+ ## 目录结构
149
+
150
+ ```
151
+ poly_position_watcher/
152
+ ├── api_worker.py # HTTP 补数与上下文管理
153
+ ├── position_service.py # 核心入口,维护仓位/订单缓存
154
+ ├── trade_calculator.py # 仓位计算工具
155
+ ├── wss_worker.py # WebSocket 客户端实现
156
+ ├── common/ # 日志与枚举
157
+ └── schema/ # Pydantic 数据模型
158
+ ```
159
+
160
+ ## 许可证
161
+
162
+ MIT
@@ -0,0 +1,18 @@
1
+ poly_position_watcher/__init__.py,sha256=VR3T8B08l2YJbRIeysWzN0xPCTdh0k1m2ZPQzphhnRU,546
2
+ poly_position_watcher/_version.py,sha256=_tAoPRxHlRWnnqYQFVPJqTq0794zoiaHgxaXAAREvUI,52
3
+ poly_position_watcher/api_worker.py,sha256=05q5siawpyrNnCCqO59oskOqzbkg4cVEC_tQia2Gwcg,7939
4
+ poly_position_watcher/position_service.py,sha256=GvrNtQoASZm_lWUVPBZtlf86l8UT4kyjYwNC4skHrwo,12656
5
+ poly_position_watcher/trade_calculator.py,sha256=8AVJ16NEU7dPHvB37fa3RlbUGWA27xFSfGbgxKWFiQI,4612
6
+ poly_position_watcher/wss_worker.py,sha256=WLHyjxCs5AdXcuTZOus8e-WpLg5unY0raVED8HecQyY,16700
7
+ poly_position_watcher/common/__init__.py,sha256=8tLsESE6V-OHYvu1fHozSeLll1vO2vZ1nSMjoHGBiys,183
8
+ poly_position_watcher/common/enums.py,sha256=L7L5ehkLSOlbplfBP0uRRESF5wPqtx7tsp86nSz_4rY,430
9
+ poly_position_watcher/common/logger.py,sha256=JU8d5MhOrRUFaMtD6mDjQ2t4TaIBT6msPL09wVPl_uw,1716
10
+ poly_position_watcher/schema/__init__.py,sha256=r4fZmcqPLw7FL6kIYiUr0hdiAs5lWdbptZIpVx9i5uU,430
11
+ poly_position_watcher/schema/base.py,sha256=ZMY4YpKC6g9Zg6QJlZ0PCkVLd0KFfjjTElofqCsUacA,780
12
+ poly_position_watcher/schema/common_model.py,sha256=tCcZN-oZPZETuX6s1WBkiIkBXIhrPb_2rJituDAMHrw,4732
13
+ poly_position_watcher/schema/position_model.py,sha256=fB84lneZuO1KOHpyLXyDLAKINSe2UazVJUIOZFzNq2A,4075
14
+ poly_position_watcher-0.1.0.dist-info/licenses/LICENSE,sha256=s7XOR4h-6YPXj4pJvUjZ4OXOy13IlQuQGPcIP4tWDus,1085
15
+ poly_position_watcher-0.1.0.dist-info/METADATA,sha256=_xGsBYbowdlGwTFeBooKVgxnsECqxSemMmhyKyCPLLo,5447
16
+ poly_position_watcher-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ poly_position_watcher-0.1.0.dist-info/top_level.txt,sha256=UahKe-xC4_pgBU0_USUmMIweRzHaS5PTA9P-1TDK9AY,22
18
+ poly_position_watcher-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Donvink
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ poly_position_watcher