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,16 @@
1
+ """High level service for monitoring Polymarket positions in real time."""
2
+
3
+ from .position_service import PositionWatcherService
4
+ from .schema.position_model import UserPosition, TradeMessage, OrderMessage
5
+ from .trade_calculator import calculate_position_from_trades, calculate_position_with_price
6
+ from ._version import __version__
7
+
8
+ __all__ = [
9
+ "PositionWatcherService",
10
+ "calculate_position_from_trades",
11
+ "calculate_position_with_price",
12
+ "UserPosition",
13
+ "TradeMessage",
14
+ "OrderMessage",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,232 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/1 16:17
3
+ # @Author: pinbar
4
+ # @Site:
5
+ # @File: api_worker.py
6
+ # @Software: PyCharm
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from typing import List, Optional, TYPE_CHECKING
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+
13
+ from py_clob_client.client import ClobClient
14
+ from py_clob_client.clob_types import TradeParams
15
+
16
+ from poly_position_watcher.schema.position_model import TradeMessage, OrderMessage
17
+ from poly_position_watcher.common.logger import logger
18
+
19
+ if TYPE_CHECKING:
20
+ from poly_position_watcher.position_service import PositionWatcherService
21
+
22
+ executor = ThreadPoolExecutor(max_workers=3)
23
+
24
+
25
+ class APIWorker:
26
+ """
27
+ Pulls trade history through the CLOB HTTP API using the official SDK.
28
+ The worker normalizes responses into TradeMessage instances so
29
+ downstream consumers can share the same data model as the WebSocket stream.
30
+ """
31
+
32
+ def __init__(self, client: ClobClient, maker_address: str):
33
+ self.client = client
34
+ self.maker_address = maker_address
35
+
36
+ def fetch_order(self, order_id: str) -> OrderMessage | None:
37
+ if order := self.client.get_order(order_id):
38
+ return self._parse_order(order)
39
+
40
+ def fetch_trades(
41
+ self,
42
+ market: Optional[str] = None,
43
+ after: Optional[int] = None,
44
+ before: Optional[int] = None,
45
+ ) -> List[TradeMessage]:
46
+ """
47
+ Fetches historical trades for this maker.
48
+
49
+ :param market: Optional condition_id filter.
50
+ :param after: Only return trades updated after this timestamp (ms).
51
+ :param before: Only return trades updated before this timestamp (ms).
52
+ """
53
+ params_kwargs = {"maker_address": self.maker_address}
54
+ if market:
55
+ params_kwargs["market"] = market
56
+ if after:
57
+ params_kwargs["after"] = after
58
+ if before:
59
+ params_kwargs["before"] = before
60
+
61
+ params = TradeParams(**params_kwargs)
62
+ raw_trades = self.client.get_trades(params)
63
+
64
+ trades: List[TradeMessage] = []
65
+ for raw in raw_trades:
66
+ trades.append(self._parse_trade(raw))
67
+ return trades
68
+
69
+ @staticmethod
70
+ def _parse_trade(payload: dict) -> TradeMessage:
71
+ normalized = dict(payload)
72
+ normalized.setdefault("type", "TRADE")
73
+ normalized.setdefault("event_type", "trade")
74
+ return TradeMessage(**normalized)
75
+
76
+ @staticmethod
77
+ def _parse_order(payload: dict) -> OrderMessage:
78
+ normalized = dict(payload)
79
+ normalized.setdefault("type", "update")
80
+ normalized.setdefault("event_type", "order")
81
+ normalized.setdefault("timestamp", 0)
82
+ normalized["owner"] = ""
83
+ return OrderMessage(**normalized)
84
+
85
+
86
+ class HttpListenerContext:
87
+ """
88
+ Thread-safe context manager that:
89
+ - applies temporary HTTP listen lists
90
+ - starts HTTP trade/order polling threads on enter
91
+ - stops threads on exit
92
+ - restores previous listening state
93
+ """
94
+
95
+ def __init__(
96
+ self,
97
+ service: "PositionWatcherService",
98
+ markets=None,
99
+ orders=None,
100
+ http_poll_interval: float = 3,
101
+ bootstrap_http: bool = False,
102
+ ):
103
+ self.service = service
104
+ self._lock = threading.RLock()
105
+ self.markets: set[str] = set(markets) if markets else set()
106
+ self.orders: set[str] = set(orders) if orders else set()
107
+ self.http_poll_interval = http_poll_interval
108
+ self.bootstrap_http = bootstrap_http
109
+ self.api_worker = APIWorker(self.service.client, self.service.user_address)
110
+
111
+ # Local thread control
112
+ self._stop_event = threading.Event()
113
+ self._trade_thread = None
114
+ self._order_thread = None
115
+
116
+ # -------------------------
117
+ # Context enter
118
+ # -------------------------
119
+ def __enter__(self):
120
+ if self.bootstrap_http:
121
+ self.sync_trade_from_http(is_init=True)
122
+ self.sync_order_from_http()
123
+
124
+ # Start HTTP polling threads
125
+ self._start_threads()
126
+ return self
127
+
128
+ # -------------------------
129
+ # add markets/orders safely
130
+ # -------------------------
131
+ def add(self, markets=None, orders=None):
132
+ with self._lock:
133
+ if markets:
134
+ self.markets.update(markets)
135
+ if orders:
136
+ self.orders.update(orders)
137
+
138
+ def reset(self, markets: list[str] = None, orders: list[str] = None):
139
+ with self._lock:
140
+ if markets:
141
+ self.markets = set(markets)
142
+ if orders:
143
+ self.orders = set(orders)
144
+
145
+ def clear(self):
146
+ with self._lock:
147
+ self.markets = set()
148
+ self.orders = set()
149
+
150
+ def __exit__(self, exc_type, exc_val, exc_tb):
151
+ self._stop_threads()
152
+
153
+ return False
154
+
155
+ # -------------------------
156
+ # internal: start threads
157
+ # -------------------------
158
+ def _start_threads(self):
159
+ self._stop_event.clear()
160
+
161
+ self._trade_thread = threading.Thread(
162
+ target=self._trade_loop,
163
+ daemon=True,
164
+ )
165
+ self._order_thread = threading.Thread(
166
+ target=self._order_loop,
167
+ daemon=True,
168
+ )
169
+
170
+ self._trade_thread.start()
171
+ self._order_thread.start()
172
+
173
+ # -------------------------
174
+ # internal: stop threads
175
+ # -------------------------
176
+ def _stop_threads(self):
177
+ self._stop_event.set()
178
+
179
+ def _trade_loop(self):
180
+ while not self._stop_event.wait(self.http_poll_interval):
181
+ self.sync_trade_from_http()
182
+ logger.info(f"{self.markets}, trade loop is stopped")
183
+
184
+ def _order_loop(self):
185
+ while not self._stop_event.wait(self.http_poll_interval):
186
+ self.sync_order_from_http()
187
+ logger.info(f"{self.orders}, order loop is stopped")
188
+
189
+ # -------------------------------------------------------------------------
190
+ # HTTP sync (manual)
191
+ # -------------------------------------------------------------------------
192
+ def sync_trade_from_http(self, is_init: bool = False):
193
+ with self._lock:
194
+ markets = list(self.markets)
195
+ tasks = []
196
+ for market in markets:
197
+ task = executor.submit(self.api_worker.fetch_trades, market)
198
+ task._market_id = market
199
+ tasks.append(task)
200
+ for task in as_completed(tasks):
201
+ try:
202
+ trades = task.result()
203
+ except Exception as e:
204
+ logger.error(f"Failed to http fetch trades market {task._market_id}: {e}")
205
+ continue
206
+ if is_init:
207
+ self.service._init_trades(sorted(trades, key=lambda x: x.match_time))
208
+ else:
209
+ for trade in sorted(trades, key=lambda x: x.match_time):
210
+ self.service._ingest_trade(trade)
211
+
212
+ def sync_order_from_http(self):
213
+ with self._lock:
214
+ order_ids = list(self.orders)
215
+ tasks = []
216
+ for order_id in order_ids:
217
+ task = executor.submit(self.api_worker.fetch_order, order_id)
218
+ task._order_id = order_id
219
+ tasks.append(task)
220
+ for task in as_completed(tasks):
221
+ try:
222
+ order = task.result()
223
+ except Exception as e:
224
+ logger.error(f"Failed to fetch order {task._order_id}: {e}")
225
+ continue
226
+ if order is None:
227
+ exists = self.service.position_store.orders.get(task._order_id)
228
+ if exists:
229
+ exists.status = "canceled"
230
+ self.service._ingest_order(exists)
231
+ else:
232
+ self.service._ingest_order(order)
@@ -0,0 +1,6 @@
1
+ """Shared enums and utilities for the position watcher package."""
2
+
3
+ from .enums import Side, MarketEvent
4
+ from .logger import logger
5
+
6
+ __all__ = ["Side", "MarketEvent", "logger"]
@@ -0,0 +1,20 @@
1
+ """Enum definitions used across the position watcher package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class Side(str, Enum):
9
+ """Order side in Polymarket's CLOB."""
10
+
11
+ BUY = "BUY"
12
+ SELL = "SELL"
13
+
14
+
15
+ class MarketEvent(str, Enum):
16
+ """Supported market level websocket event names."""
17
+
18
+ PRICE_CHANGE = "price_change"
19
+ TICK_SIZE_CHANGE = "tick_size_change"
20
+ BOOK = "book"
@@ -0,0 +1,49 @@
1
+ """Lightweight logger used throughout the position watcher package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from typing import Any
8
+
9
+ _LOG_LEVEL = os.getenv("poly_position_watcher_LOG_LEVEL", "INFO").upper()
10
+
11
+
12
+ class _BraceAdapter:
13
+ """Simple adapter that supports ``logger.info("foo {}", bar)`` formatting."""
14
+
15
+ def __init__(self) -> None:
16
+ logging.basicConfig(
17
+ level=getattr(logging, _LOG_LEVEL, logging.INFO),
18
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
19
+ )
20
+ self._logger = logging.getLogger("poly_position_watcher")
21
+
22
+ def _log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None:
23
+ if args or kwargs:
24
+ try:
25
+ msg = msg.format(*args, **kwargs)
26
+ except Exception: # pragma: no cover - fallback to raw message
27
+ msg = " ".join([msg, *map(str, args)])
28
+ self._logger.log(level, msg, **kwargs)
29
+
30
+ def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
31
+ self._log(logging.DEBUG, msg, *args, **kwargs)
32
+
33
+ def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
34
+ self._log(logging.INFO, msg, *args, **kwargs)
35
+
36
+ def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
37
+ self._log(logging.WARNING, msg, *args, **kwargs)
38
+
39
+ def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
40
+ self._log(logging.ERROR, msg, *args, **kwargs)
41
+
42
+ def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
43
+ kwargs.setdefault("exc_info", True)
44
+ self._log(logging.ERROR, msg, *args, **kwargs)
45
+
46
+
47
+ logger = _BraceAdapter()
48
+
49
+ __all__ = ["logger"]
@@ -0,0 +1,338 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/3 15:35
3
+ # @Author: pinbar
4
+ # @Site:
5
+ # @File: position_service.py
6
+ # @Software: PyCharm
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from datetime import datetime
11
+ from queue import Queue, Empty
12
+ from collections import defaultdict
13
+ from typing import Dict
14
+
15
+ from poly_position_watcher.common.enums import Side
16
+ from poly_position_watcher.common.logger import logger
17
+ from poly_position_watcher.api_worker import HttpListenerContext
18
+
19
+ from poly_position_watcher.schema.position_model import (
20
+ UserPosition,
21
+ TradeMessage,
22
+ OrderMessage,
23
+ )
24
+ from poly_position_watcher.trade_calculator import calculate_position_from_trades
25
+ from poly_position_watcher.wss_worker import PolymarketUserWS
26
+
27
+
28
+ class PositionStore:
29
+ """
30
+ Keeps an in-memory view of per-market trades and exposes aggregated positions.
31
+ """
32
+
33
+ def __init__(self, user_address: str):
34
+ self.user_address = user_address
35
+ self.trades_by_token: Dict[str, Dict[str, TradeMessage]] = defaultdict(dict)
36
+ self.positions: Dict[str, UserPosition] = {}
37
+ self.orders: Dict[str, OrderMessage] = {}
38
+ self._lock = threading.RLock()
39
+ self.queue_dict: Dict[str, Queue] = {}
40
+
41
+ def get_token_id_from_trade(self, trade: TradeMessage) -> tuple[str, str] | None:
42
+ if trade.maker_address == self.user_address:
43
+ return trade.outcome, trade.asset_id
44
+ for order in trade.maker_orders:
45
+ if order.maker_address == self.user_address:
46
+ return order.outcome, order.asset_id
47
+
48
+ def _put(self, _id: str, item: UserPosition | OrderMessage) -> None:
49
+ if _id not in self.queue_dict:
50
+ self.queue_dict[_id] = Queue()
51
+ self.queue_dict[_id].put(item)
52
+
53
+ def _clear_q(self, _id: str) -> None:
54
+ if _id in self.queue_dict:
55
+ self.queue_dict[_id] = Queue()
56
+
57
+ def _get(self, _id: str, timeout=None) -> OrderMessage | UserPosition | None:
58
+ with self._lock:
59
+ if _id not in self.queue_dict:
60
+ self.queue_dict[_id] = Queue()
61
+ return self.queue_dict[_id].get(timeout=timeout)
62
+
63
+ def append_trade(self, trade: TradeMessage):
64
+ """
65
+ Stores a new trade snapshot.
66
+ If duplicate trade ids arrive, the payload with the latest update timestamp wins.
67
+ """
68
+ with self._lock:
69
+ result = self.get_token_id_from_trade(trade)
70
+ if result is None:
71
+ logger.debug(
72
+ "Skip trade without matching maker/taker context: {}", trade.id
73
+ )
74
+ return
75
+ outcome, token_id = result
76
+ trades_map = self.trades_by_token[token_id]
77
+ existing = trades_map.get(trade.id)
78
+ if existing and trade.match_time <= existing.match_time:
79
+ return
80
+ else:
81
+ trades_map[trade.id] = trade
82
+ user_pos = self.build_position(
83
+ trades=list(trades_map.values()), token_id=token_id, outcome=outcome
84
+ )
85
+ self.positions[token_id] = user_pos
86
+ self._put(token_id, user_pos)
87
+
88
+ def init_trades(self, trades: list[TradeMessage]):
89
+ with self._lock:
90
+ if not trades:
91
+ return
92
+ result = self.get_token_id_from_trade(trades[0])
93
+ if result is None:
94
+ logger.debug(
95
+ "Skip trade without matching maker/taker context: {}", trades[0].id
96
+ )
97
+ return
98
+ outcome, token_id = result
99
+ trades_map = self.trades_by_token[token_id]
100
+ for trade in trades:
101
+ trades_map[trade.id] = trade
102
+ user_pos = self.build_position(
103
+ trades=list(trades_map.values()), token_id=token_id, outcome=outcome
104
+ )
105
+ self._clear_q(token_id)
106
+ self.positions[token_id] = user_pos
107
+ self._put(token_id, user_pos)
108
+
109
+ def append_order(self, order: OrderMessage | None):
110
+ """
111
+ Stores a new order snapshot.
112
+ If duplicate trade ids arrive, the payload with the latest update timestamp wins.
113
+ """
114
+ with self._lock:
115
+ existing = self.orders.get(order.id)
116
+ if (
117
+ existing
118
+ and order.size_matched <= existing.size_matched
119
+ and order.status == existing.status
120
+ ):
121
+ return
122
+ if abs(order.size_matched - order.original_size) < 0.5:
123
+ order.filled = True
124
+ self.orders[order.id] = order
125
+ self._put(order.id, order)
126
+
127
+ def build_position(
128
+ self, trades: list[TradeMessage], token_id, outcome: str
129
+ ) -> UserPosition | None:
130
+ position_result = calculate_position_from_trades(
131
+ trades, user_address=self.user_address
132
+ )
133
+ current = UserPosition(
134
+ price=position_result.avg_price,
135
+ size=position_result.size,
136
+ volume=position_result.amount,
137
+ token_id=token_id,
138
+ last_update=position_result.last_update,
139
+ market_id=trades[0].market,
140
+ outcome=outcome,
141
+ created_at=datetime.fromtimestamp(position_result.last_update),
142
+ )
143
+ return current
144
+ # if exists_pos := self.positions.get(token_id):
145
+ # if exists_pos.last_update < current.last_update:
146
+ # return current
147
+ # else:
148
+ # return current
149
+
150
+ def get_token_position(self, token_id: str) -> UserPosition:
151
+ return self.positions.get(token_id)
152
+
153
+ def get_token_order(self, token_id: str) -> list[OrderMessage]:
154
+ orders = []
155
+ for key, value in self.orders.items():
156
+ if value.asset_id == token_id:
157
+ orders.append(value)
158
+ return orders
159
+
160
+ def get_order_by_id(self, order_id: str) -> OrderMessage:
161
+ return self.orders.get(order_id)
162
+
163
+ def blocking_get_token_position(
164
+ self, token_id: str, timeout: float = None
165
+ ) -> UserPosition:
166
+ return self._get(token_id, timeout)
167
+
168
+ def blocking_get_order_by_id(
169
+ self, order_id: str, timeout: float = None
170
+ ) -> OrderMessage:
171
+ return self._get(order_id, timeout)
172
+
173
+ @staticmethod
174
+ def _calculate_size(order, size: float, volume: float):
175
+ if order.side == Side.BUY:
176
+ size += order.size
177
+ volume += order.size * order.price
178
+ elif order.side == Side.SELL:
179
+ size -= order.size
180
+ volume -= order.size * order.price
181
+ return size, volume
182
+
183
+
184
+ class PositionWatcherService:
185
+ """
186
+ High level service:
187
+ - Bootstraps positions via HTTP
188
+ - Maintains updates via WebSocket
189
+ - Provides context-based HTTP listener for temporary polling threads
190
+ """
191
+
192
+ def __init__(
193
+ self,
194
+ client,
195
+ ws_idle_timeout=60 * 60,
196
+ wss_proxies: dict | None = None,
197
+ ):
198
+ """
199
+ wss_proxies example: {
200
+ "http_proxy_host": "127.0.0.1",
201
+ "http_proxy_port": 8118,
202
+ "proxy_type": "http",
203
+ }
204
+ """
205
+ self.client = client
206
+ self.user_address = self._resolve_user_address()
207
+ self.position_store = PositionStore(self.user_address)
208
+ self._wss_proxies = wss_proxies or {}
209
+ # Setup WS client
210
+ creds = self.client.creds or self.client.create_or_derive_api_creds()
211
+ self.ws_client = PolymarketUserWS(
212
+ api_key=creds.api_key,
213
+ api_secret=creds.api_secret,
214
+ api_passphrase=creds.api_passphrase,
215
+ idle_timeout=ws_idle_timeout,
216
+ on_message_callback=self._handle_ws_message,
217
+ wss_proxies=self._wss_proxies,
218
+ )
219
+
220
+ self._ws_thread = None
221
+
222
+ # -------------------------------------------------------------------------
223
+ # Context: start/stop entire service
224
+ # -------------------------------------------------------------------------
225
+ def __enter__(self):
226
+ self.start()
227
+ return self
228
+
229
+ def __exit__(self, exc_type, exc_val, exc_tb):
230
+ self.stop()
231
+ return False
232
+
233
+ # -------------------------------------------------------------------------
234
+ # Public factory: returns the context manager
235
+ # -------------------------------------------------------------------------
236
+ def http_listen(
237
+ self,
238
+ markets=None,
239
+ orders=None,
240
+ http_poll_interval: float = 3,
241
+ bootstrap_http: bool = False,
242
+ ):
243
+ """
244
+ 如果在启动前已经有历史仓位需要: bootstrap_http=True
245
+ """
246
+ return HttpListenerContext(
247
+ self,
248
+ markets,
249
+ orders,
250
+ http_poll_interval=http_poll_interval,
251
+ bootstrap_http=bootstrap_http,
252
+ )
253
+
254
+ # -------------------------------------------------------------------------
255
+ # Start / Stop
256
+ # -------------------------------------------------------------------------
257
+ def start(self, bootstrap_http=True):
258
+ # Start WebSocket
259
+ if not self._ws_thread or not self._ws_thread.is_alive():
260
+ self._ws_thread = threading.Thread(target=self.ws_client.start, daemon=True)
261
+ self._ws_thread.start()
262
+ logger.info("Started WebSocket worker.")
263
+
264
+ def stop(self):
265
+ self.ws_client.stop()
266
+ # Join WS
267
+ if self._ws_thread:
268
+ self._ws_thread.join(timeout=1)
269
+
270
+ logger.info("Position watcher stopped.")
271
+
272
+ # -------------------------------------------------------------------------
273
+ # WS handler
274
+ # -------------------------------------------------------------------------
275
+ def _handle_ws_message(self, payload):
276
+ logger.info(f"WS message: {payload.get('type')}")
277
+ if payload.get("type") == "TRADE":
278
+ self._ingest_trade(TradeMessage(**payload))
279
+ else:
280
+ self._ingest_order(OrderMessage(**payload))
281
+
282
+ # -------------------------------------------------------------------------
283
+ # Ingestion
284
+ # -------------------------------------------------------------------------
285
+ def _ingest_trade(self, trade):
286
+ self.position_store.append_trade(trade)
287
+
288
+ def _init_trades(self, trades: list[TradeMessage]):
289
+ self.position_store.init_trades(trades)
290
+
291
+ def _ingest_order(self, order):
292
+ self.position_store.append_order(order)
293
+
294
+ # -------------------------------------------------------------------------
295
+ # Helpers
296
+ # -------------------------------------------------------------------------
297
+ def _resolve_user_address(self):
298
+ funder = getattr(getattr(self.client, "builder", None), "funder", None)
299
+ if funder:
300
+ return funder
301
+ return self.client.get_address()
302
+
303
+ def get_position(self, token_id: str) -> UserPosition:
304
+ if position := self.position_store.get_token_position(token_id):
305
+ return position
306
+ return UserPosition(token_id=token_id, price=0, size=0, volume=0, last_update=0)
307
+
308
+ def get_order_by_token(self, token_id: str) -> list[OrderMessage]:
309
+ return self.position_store.get_token_order(token_id)
310
+
311
+ def get_order(self, order_id: str) -> OrderMessage:
312
+ return self.position_store.get_order_by_id(order_id)
313
+
314
+ def blocking_get_position(
315
+ self, token_id: str, timeout: float = None
316
+ ) -> UserPosition | None:
317
+ """
318
+ 超时返回None;若无仓位则返回 size=0 的占位 UserPosition
319
+ """
320
+ try:
321
+ return self.position_store.blocking_get_token_position(token_id, timeout)
322
+ except Empty:
323
+ # 若超时未收到任何仓位更新,则返回 size=0 的占位对象,方便上层统一处理
324
+ return UserPosition(
325
+ token_id=token_id, price=0, size=0, volume=0, last_update=0
326
+ )
327
+
328
+ def blocking_get_order(
329
+ self, order_id: str, timeout: float = None
330
+ ) -> OrderMessage | None:
331
+ """
332
+ 超时返回None(即返回 None,表示没有订单更新)
333
+ """
334
+ try:
335
+ return self.position_store.blocking_get_order_by_id(order_id, timeout)
336
+ except Empty:
337
+ # 超时未拿到订单更新时直接返回 None,由调用方自行判断
338
+ return None
@@ -0,0 +1,21 @@
1
+ """Pydantic models used by the position watcher service."""
2
+
3
+ from .position_model import (
4
+ MakerOrder,
5
+ TradeMessage,
6
+ OrderMessage,
7
+ UserPosition,
8
+ PositionDetails,
9
+ PositionResult,
10
+ )
11
+ from .common_model import OrderBookSummary
12
+
13
+ __all__ = [
14
+ "MakerOrder",
15
+ "TradeMessage",
16
+ "OrderMessage",
17
+ "UserPosition",
18
+ "PositionDetails",
19
+ "PositionResult",
20
+ "OrderBookSummary",
21
+ ]