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,27 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025-12-06 19:09:43
3
+ # @Author: Donvink wuwukai
4
+ # @Site:
5
+ # @File: base.py
6
+ # @Software: PyCharm
7
+ from datetime import datetime
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+
12
+ def datetime_to_gmt_str(dt: datetime) -> str:
13
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
14
+
15
+
16
+ class PrettyPrintBaseModel(BaseModel):
17
+ def __str__(self):
18
+ # 将每个字段和值分行显示
19
+ lines = [f"{name}: {value!r}" for name, value in self.__dict__.items()]
20
+ return f"{self.__class__.__name__}(\n " + ",\n ".join(lines) + "\n)"
21
+
22
+ # 可选:让 repr() 打印同样效果
23
+ __repr__ = __str__
24
+ model_config = ConfigDict(
25
+ json_encoders={datetime: datetime_to_gmt_str},
26
+ populate_by_name=True,
27
+ )
@@ -0,0 +1,150 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/10/13 10:03
3
+ # @Author: pinbar
4
+ # @Site:
5
+ # @File: schema.py
6
+ # @Software: PyCharm
7
+ import time
8
+ from typing import Optional
9
+ from pydantic import BaseModel, model_validator, Field
10
+
11
+ from poly_position_watcher.common.enums import Side
12
+ from poly_position_watcher.schema.base import PrettyPrintBaseModel
13
+
14
+
15
+ class OrderSummary(BaseModel):
16
+ price: float = None
17
+ size: float = None
18
+ size_cumsum: float = None
19
+
20
+
21
+ class OrderBookSummary(BaseModel):
22
+ market: str = None
23
+ period: Optional[str] = None
24
+ asset_id: str = None
25
+ timestamp: float = None
26
+ bids: Optional[list[OrderSummary]] = None
27
+ asks: Optional[list[OrderSummary]] = None
28
+ min_order_size: str = None
29
+ neg_risk: bool = None
30
+ tick_size: str = None
31
+ hash: str = None
32
+
33
+ def flush_cumsum(self):
34
+ tick_size = len(self.tick_size) - 2
35
+ for item in [[*self.bids], [*self.asks]]:
36
+ cumsum = 0
37
+ for row in item[::-1]:
38
+ row.size = float(row.size)
39
+ row.price = round(float(row.price), tick_size)
40
+ cumsum += row.size
41
+ row.size_cumsum = cumsum
42
+
43
+ def set_price(self, item: dict, timestamp):
44
+ tick_size = len(self.tick_size) - 2
45
+ new_price = round(float(item["price"]), tick_size)
46
+ for row in [*self.asks, *self.bids]:
47
+ if row.price == new_price:
48
+ row.size = float(item["size"])
49
+ self.timestamp = timestamp
50
+ self.flush_cumsum()
51
+
52
+ @model_validator(mode="before")
53
+ @classmethod
54
+ def validate_fields(cls, values: dict):
55
+ if isinstance(values, dict):
56
+ if timestamp := values.get("timestamp"):
57
+ values["timestamp"] = int(timestamp) / 1000
58
+ tick_size = len(values.get("tick_size")) - 2
59
+ for item in [values.get("bids", []), values.get("asks", [])]:
60
+ cumsum = 0
61
+ for row in item[::-1]:
62
+ row["size"] = float(row["size"])
63
+ row["price"] = round(float(row["price"]), tick_size)
64
+ cumsum += row["size"]
65
+ row["size_cumsum"] = cumsum
66
+ return values
67
+
68
+ def print_order_book(
69
+ self,
70
+ ) -> str:
71
+ """漂亮打印盘口,不依赖第三方库,返回字符串"""
72
+ depth = 8
73
+ bids = self.bids[-depth:]
74
+ asks = self.asks[-depth:]
75
+
76
+ lines = []
77
+ lines.append("\n📊 Order Book Summary")
78
+ lines.append(f"Min Order Size: {self.min_order_size}")
79
+ lines.append(f"Tick Size: {self.tick_size}")
80
+ lines.append(f"{'Price':>10} | {'Size':>12} | {'Size_Cumsum':>12}")
81
+ lines.append("-" * 30)
82
+
83
+ # 卖单从高到低打印(视觉上上方高价)
84
+ for ask in asks:
85
+ lines.append(
86
+ f"{ask.price:>10.3f} | {ask.size:>12,.2f} | {ask.size_cumsum:>12,.2f}"
87
+ )
88
+
89
+ lines.append("-" * 30)
90
+
91
+ for bid in reversed(bids):
92
+ lines.append(
93
+ f"{bid.price:>10.3f} | {bid.size:>12,.2f} | {bid.size_cumsum:>12,.2f}"
94
+ )
95
+
96
+ lines.append("-" * 30)
97
+ lines.append("")
98
+
99
+ return "\n".join(lines)
100
+
101
+
102
+ class UserPosition(PrettyPrintBaseModel):
103
+ buy_price: Optional[float] = 0
104
+ sell_price: Optional[float] = 0
105
+ original_size: Optional[float] = 0
106
+ buy_size_matched: Optional[float] = 0
107
+ status: Optional[str] = None
108
+ remaining_size: Optional[float] = 0
109
+ sell_size_matched: Optional[float] = 0
110
+
111
+
112
+ class MarketOrder(BaseModel):
113
+ order_id: Optional[str] = None
114
+ slug: str = None
115
+ token_id: str = None
116
+ shares: float = None
117
+ side: Side = None
118
+ amount: float = None
119
+ take_profit: float = 0
120
+ price: float = None
121
+ tick_size: str = None
122
+ neg_risk: bool = None
123
+
124
+
125
+ class StreakPosition(PrettyPrintBaseModel):
126
+ shares: float = None
127
+ origin_shares: float = None
128
+ price: float = None
129
+ real_price: float = None
130
+ volume: float = None
131
+ real_volume: float = None
132
+ combine_now: str = ""
133
+ side: str = ""
134
+ market: str = ""
135
+ streak_len: int = 2
136
+
137
+
138
+ class PeakData(BaseModel):
139
+ """峰值数据模型"""
140
+
141
+ is_peak: bool
142
+ peak_idx: int = None
143
+ peak_value: float = None
144
+ left: float = None
145
+ center: float = None
146
+ right: float = None
147
+ center_token_id: Optional[str] = Field(None, description="中心token ID")
148
+ right_token_id: Optional[str] = Field(None, description="右侧token ID")
149
+ left_token_id: Optional[str] = Field(None, description="左侧token ID")
150
+ last_update: float = Field(default_factory=time.time)
@@ -0,0 +1,152 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025/12/1 16:15
3
+ # @Author: pinbar
4
+ # @Site:
5
+ # @File: model.py
6
+ # @Software: PyCharm
7
+ from datetime import datetime
8
+ from typing import List, Optional, Literal
9
+
10
+ from pydantic import BaseModel, field_validator, model_validator
11
+
12
+ from poly_position_watcher.common.enums import Side
13
+ from poly_position_watcher.schema.base import PrettyPrintBaseModel
14
+
15
+
16
+ class MakerOrder(BaseModel):
17
+ asset_id: str
18
+ matched_amount: float
19
+ size: float = 0
20
+ order_id: str
21
+ outcome: str
22
+ owner: str
23
+ price: float
24
+ fee_rate_bps: float
25
+ outcome_index: int | None = None
26
+ maker_address: str
27
+ side: Side
28
+
29
+ @field_validator("fee_rate_bps", "price", "matched_amount", "size", mode="before")
30
+ @classmethod
31
+ def validate_float(cls, v: str):
32
+ return float(v)
33
+
34
+ @model_validator(mode="after")
35
+ def validate_size(self):
36
+ self.size = self.matched_amount
37
+ return self
38
+
39
+
40
+ class TradeMessage(BaseModel):
41
+ type: Literal["TRADE"] = "Trade"
42
+ event_type: Literal["trade"] = "trade"
43
+
44
+ asset_id: str
45
+ id: str
46
+ maker_orders: List[MakerOrder]
47
+ transaction_hash: str
48
+ market: str
49
+ maker_address: str
50
+ outcome: str
51
+ owner: str
52
+ price: float
53
+ side: Side
54
+ size: float
55
+ status: str
56
+ taker_order_id: str
57
+ timestamp: int | None = None
58
+ match_time: int | None = None
59
+ last_update: int | None = None
60
+ trade_owner: Optional[str] = None
61
+ fee_rate_bps: float
62
+ created_at: datetime | None = None
63
+
64
+ @model_validator(mode="after")
65
+ def validate_datetime(self):
66
+ base_ts = self.match_time or self.last_update
67
+ if base_ts:
68
+ self.created_at = datetime.fromtimestamp(base_ts)
69
+ return self
70
+
71
+ @field_validator("timestamp", "last_update", "match_time", mode="before")
72
+ @classmethod
73
+ def validate_int(cls, v: str):
74
+ if v in (None, ""):
75
+ return None
76
+ return int(v)
77
+
78
+ @field_validator("fee_rate_bps", "price", "size", mode="before")
79
+ @classmethod
80
+ def validate_float(cls, v: str):
81
+ return float(v)
82
+
83
+
84
+ class OrderMessage(PrettyPrintBaseModel):
85
+ type: Optional[str] = None # Literal["PLACEMENT", "UPDATE", "CANCELLATION"]
86
+ event_type: Optional[str] = None # Literal["order"]
87
+
88
+ asset_id: Optional[str] = None
89
+ associate_trades: Optional[List[str]] = None
90
+ id: Optional[str]
91
+ market: Optional[str] = None
92
+ order_owner: Optional[str] = None
93
+ original_size: float | None = None
94
+ outcome: Optional[str] = None
95
+ owner: Optional[str] = None
96
+ price: float
97
+ side: Literal["BUY", "SELL"]
98
+ size_matched: float
99
+ timestamp: float
100
+ filled: bool = False
101
+ status: Optional[str] = None
102
+ created_at: datetime | None = None
103
+
104
+ @field_validator(
105
+ "size_matched", "price", "original_size", "timestamp", mode="before"
106
+ )
107
+ @classmethod
108
+ def validate_float(cls, v: str):
109
+ return float(v)
110
+
111
+ @model_validator(mode="after")
112
+ def validate_datetime(self):
113
+ base_ts = self.timestamp
114
+ if base_ts:
115
+ self.created_at = datetime.fromtimestamp(base_ts / 1000)
116
+ return self
117
+
118
+
119
+ class UserPosition(PrettyPrintBaseModel):
120
+ price: float
121
+ size: float
122
+ volume: float
123
+ token_id: Optional[str] = None
124
+ last_update: float
125
+ market_id: Optional[str] = None
126
+ outcome: Optional[str] = None
127
+ created_at: datetime | None = None
128
+
129
+
130
+ class PositionDetails(BaseModel):
131
+ """仓位详细信息"""
132
+
133
+ buy_events: int
134
+ sell_events: int
135
+ total_trades: int
136
+
137
+
138
+ class PositionResult(PrettyPrintBaseModel):
139
+ """仓位计算结果"""
140
+
141
+ size: float
142
+ avg_price: float
143
+ realized_pnl: float
144
+ amount: float
145
+ position_value: Optional[float] = None
146
+ unrealized_pnl: Optional[float] = None
147
+ total_pnl: Optional[float] = None
148
+ profit_rate: Optional[float] = None
149
+ is_long: bool
150
+ is_short: bool
151
+ details: PositionDetails
152
+ last_update: float
@@ -0,0 +1,148 @@
1
+ # -*- coding = utf-8 -*-
2
+ # @Time: 2025-12-06 15:50:02
3
+ # @Author: Donvink wuwukai
4
+ # @Site:
5
+ # @File: trade_calculator.py
6
+ # @Software: PyCharm
7
+ from collections import deque
8
+ from typing import List
9
+
10
+ from poly_position_watcher.common.enums import Side
11
+ from poly_position_watcher.schema.position_model import (
12
+ PositionResult,
13
+ PositionDetails,
14
+ TradeMessage,
15
+ )
16
+
17
+
18
+ def calculate_position_from_trades(
19
+ trades: List[TradeMessage], user_address: str
20
+ ) -> PositionResult:
21
+ """
22
+ 根据交易记录直接计算用户仓位(带浮点误差修正)
23
+ """
24
+
25
+ # ==== 新增:统一误差阈值 ====
26
+ EPS = 0.01
27
+
28
+ def clean(x: float) -> float:
29
+ return 0.0 if abs(x) < EPS else x
30
+
31
+ buy_events = []
32
+ sell_events = []
33
+
34
+ # --- 1. 解析所有交易 ---
35
+ for trade in trades:
36
+ is_maker_order = False
37
+
38
+ # maker 部分
39
+ for order in trade.maker_orders:
40
+ if order.maker_address != user_address:
41
+ continue
42
+ is_maker_order = True
43
+
44
+ if order.side == Side.BUY:
45
+ buy_events.append((order.size, order.price, trade.match_time))
46
+ else:
47
+ sell_events.append((-order.size, order.price, trade.match_time))
48
+
49
+ # taker 部分
50
+ if not is_maker_order and trade.maker_address == user_address:
51
+ if trade.side == Side.BUY:
52
+ buy_events.append((trade.size, trade.price, trade.match_time))
53
+ else:
54
+ sell_events.append((-trade.size, trade.price, trade.match_time))
55
+
56
+ # --- 2. 时间排序 ---
57
+ all_events = sorted(buy_events + sell_events, key=lambda x: x[2])
58
+
59
+ # --- 3. FIFO 撮合 ---
60
+ buy_queue = deque()
61
+ realized_pnl = 0.0
62
+
63
+ for size, price, _ in all_events:
64
+ size = clean(size) # ← 新增:修正
65
+
66
+ if size > 0:
67
+ # 买入加入队列
68
+ buy_queue.append([size, price])
69
+
70
+ else:
71
+ # 卖出(size 为负)
72
+ sell_size = clean(-size)
73
+
74
+ while sell_size > EPS and buy_queue:
75
+ lot_size, lot_price = buy_queue[0]
76
+
77
+ if lot_size <= sell_size + EPS:
78
+ # 完全消耗
79
+ realized_pnl += (price - lot_price) * lot_size
80
+ sell_size -= lot_size
81
+ buy_queue.popleft()
82
+ else:
83
+ # 部分消耗
84
+ realized_pnl += (price - lot_price) * sell_size
85
+ buy_queue[0][0] = clean(lot_size - sell_size)
86
+ sell_size = 0.0
87
+
88
+ # 如果还有卖不完 → 变成空头
89
+ if sell_size > EPS:
90
+ buy_queue.appendleft([-sell_size, price])
91
+
92
+ # --- 4. 计算最终持仓 ---
93
+ total_size = clean(sum(q[0] for q in buy_queue))
94
+ cost_basis = clean(sum(clean(q[0]) * q[1] for q in buy_queue))
95
+
96
+ # 若因误差产生 0.0000003 的 ghost position → 完全清掉
97
+ if abs(total_size) < EPS:
98
+ total_size = 0.0
99
+ cost_basis = 0.0
100
+
101
+ avg_price = cost_basis / total_size if total_size != 0 else 0.0
102
+
103
+ return PositionResult(
104
+ size=total_size,
105
+ avg_price=avg_price,
106
+ realized_pnl=realized_pnl,
107
+ amount=cost_basis,
108
+ is_long=total_size > 0,
109
+ is_short=total_size < 0,
110
+ details=PositionDetails(
111
+ buy_events=len(buy_events),
112
+ sell_events=len(sell_events),
113
+ total_trades=len(all_events),
114
+ ),
115
+ last_update=max([i.match_time for i in trades]) if trades else None,
116
+ )
117
+
118
+
119
+ def calculate_position_with_price(
120
+ trades: List[TradeMessage], user_address: str, market_price: float
121
+ ) -> PositionResult:
122
+ """
123
+ 包含市场价格的仓位计算
124
+
125
+ Args:
126
+ market_price: 当前市场价格
127
+
128
+ Returns:
129
+ 额外包含:
130
+ - position_value: 持仓市值
131
+ - unrealized_pnl: 未实现盈亏
132
+ - total_pnl: 总盈亏
133
+ """
134
+ position = calculate_position_from_trades(trades, user_address)
135
+ size = position.size
136
+
137
+ if size != 0:
138
+ position_value = size * market_price
139
+ position.position_value = position_value
140
+ cost_basis = position.amount
141
+ unrealized = position_value - cost_basis
142
+ position.unrealized_pnl = unrealized
143
+ position.total_pnl = position.realized_pnl + unrealized
144
+ position.profit_rate = (
145
+ position.total_pnl / cost_basis * 100 if cost_basis else None
146
+ )
147
+
148
+ return position