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.
- poly_position_watcher/__init__.py +16 -0
- poly_position_watcher/_version.py +3 -0
- poly_position_watcher/api_worker.py +232 -0
- poly_position_watcher/common/__init__.py +6 -0
- poly_position_watcher/common/enums.py +20 -0
- poly_position_watcher/common/logger.py +49 -0
- poly_position_watcher/position_service.py +338 -0
- poly_position_watcher/schema/__init__.py +21 -0
- poly_position_watcher/schema/base.py +27 -0
- poly_position_watcher/schema/common_model.py +150 -0
- poly_position_watcher/schema/position_model.py +152 -0
- poly_position_watcher/trade_calculator.py +148 -0
- poly_position_watcher/wss_worker.py +460 -0
- poly_position_watcher-0.1.0.dist-info/METADATA +162 -0
- poly_position_watcher-0.1.0.dist-info/RECORD +18 -0
- poly_position_watcher-0.1.0.dist-info/WHEEL +5 -0
- poly_position_watcher-0.1.0.dist-info/licenses/LICENSE +21 -0
- poly_position_watcher-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|