bybit/models/closed_options_positions_response.rs
1use crate::prelude::*;
2
3/// Represents a single closed options position item.
4///
5/// Contains detailed information about a closed options position, including entry/exit prices,
6/// fees, P&L, and timing. Bots use this to analyze options trading performance, calculate
7/// profitability metrics, and audit trading history.
8///
9/// # Bybit API Reference
10/// According to the Bybit V5 API documentation:
11/// - Fee and price are displayed with trailing zeroes up to 8 decimal places.
12/// - Positions are sorted by `closeTime` in descending order.
13/// - Only supports querying closed options positions in the last 6 months.
14#[derive(Debug, Serialize, Deserialize, Clone)]
15#[serde(rename_all = "camelCase")]
16pub struct ClosedOptionsPositionItem {
17 /// The options symbol name (e.g., "BTC-12JUN25-104019-C-USDT").
18 ///
19 /// Identifies the specific options contract. Bots use this to track performance
20 /// by symbol and analyze specific options strategies.
21 pub symbol: String,
22
23 /// The position side ("Buy" or "Sell").
24 ///
25 /// Indicates whether the position was long (Buy) or short (Sell) the option.
26 /// Bots use this to calculate directional exposure and analyze strategy performance.
27 pub side: Side,
28
29 /// The total open fee paid for the position.
30 ///
31 /// The cumulative fee paid when opening the position. Bots use this to calculate
32 /// net profitability and optimize for fee efficiency.
33 #[serde(with = "string_to_float")]
34 pub total_open_fee: f64,
35
36 /// The delivery fee (if applicable).
37 ///
38 /// The fee charged for options delivery at expiration. Bots use this to account
39 /// for expiration costs in options strategies.
40 #[serde(with = "string_to_float")]
41 pub delivery_fee: f64,
42
43 /// The total close fee paid for the position.
44 ///
45 /// The cumulative fee paid when closing the position. Bots use this to calculate
46 /// total transaction costs and net P&L.
47 #[serde(with = "string_to_float")]
48 pub total_close_fee: f64,
49
50 /// The position quantity.
51 ///
52 /// The number of options contracts in the position. Bots use this to calculate
53 /// position size and exposure.
54 #[serde(with = "string_to_float")]
55 pub qty: f64,
56
57 /// The timestamp when the position was closed (in milliseconds).
58 ///
59 /// Indicates when the position was closed. Bots use this for time-series analysis
60 /// and to correlate position closures with market events.
61 #[serde(with = "string_to_u64")]
62 pub close_time: u64,
63
64 /// The average exit price.
65 ///
66 /// The average price at which the position was closed. Bots use this to calculate
67 /// exit efficiency and compare against target exit prices.
68 #[serde(with = "string_to_float")]
69 pub avg_exit_price: f64,
70
71 /// The delivery price (if applicable).
72 ///
73 /// The settlement price at options expiration. Bots use this to analyze
74 /// expiration outcomes for options held to maturity.
75 #[serde(with = "string_to_float")]
76 pub delivery_price: f64,
77
78 /// The timestamp when the position was opened (in milliseconds).
79 ///
80 /// Indicates when the position was initially opened. Bots use this to calculate
81 /// position duration and analyze holding period returns.
82 #[serde(with = "string_to_u64")]
83 pub open_time: u64,
84
85 /// The average entry price.
86 ///
87 /// The average price at which the position was opened. Bots use this to calculate
88 /// entry efficiency and compare against target entry prices.
89 #[serde(with = "string_to_float")]
90 pub avg_entry_price: f64,
91
92 /// The total profit and loss for the position.
93 ///
94 /// The net P&L including all fees. Bots use this as the primary performance metric
95 /// for options trading strategies.
96 #[serde(with = "string_to_float")]
97 pub total_pnl: f64,
98}
99
100/// Represents the response from the `/v5/position/get-closed-positions` endpoint.
101///
102/// Contains a paginated list of closed options positions with metadata for continued
103/// pagination. Bots use this to retrieve historical options trading data for analysis,
104/// reporting, and strategy optimization.
105#[derive(Debug, Serialize, Deserialize, Clone)]
106#[serde(rename_all = "camelCase")]
107pub struct ClosedOptionsPositionsResponse {
108 /// The pagination cursor for retrieving the next page of results.
109 ///
110 /// Use this token in subsequent requests to retrieve the next page of closed positions.
111 /// Returns an empty string if there are no more results. Bots use this for efficient
112 /// pagination through large result sets.
113 pub next_page_cursor: String,
114
115 /// The product category (always "option" for this endpoint).
116 ///
117 /// Confirms the instrument type. Bots should validate this matches the request category.
118 pub category: String,
119
120 /// The list of closed options positions.
121 ///
122 /// Contains detailed information for each closed position, sorted by `closeTime`
123 /// in descending order. Bots iterate through this list to analyze trading history.
124 pub list: Vec<ClosedOptionsPositionItem>,
125}
126
127/// Type alias for the API response containing closed options positions.
128///
129/// Standardized response type that includes retCode, retMsg, result, retExtInfo, and time.
130/// Bots use this to handle both successful responses and errors uniformly.
131pub type ClosedOptionsPositionsResult = BybitApiResponse<ClosedOptionsPositionsResponse>;
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use serde_json::json;
137
138 #[test]
139 fn test_deserialize_closed_options_position_item() {
140 let json_data = json!({
141 "symbol": "BTC-12JUN25-104019-C-USDT",
142 "side": "Sell",
143 "totalOpenFee": "0.94506647",
144 "deliveryFee": "0.32184533",
145 "totalCloseFee": "0.00000000",
146 "qty": "0.02",
147 "closeTime": "1749726002161",
148 "avgExitPrice": "107281.77405000",
149 "deliveryPrice": "107281.77405031",
150 "openTime": "1749722990063",
151 "avgEntryPrice": "3371.50000000",
152 "totalPnl": "0.90760719"
153 });
154
155 let item: ClosedOptionsPositionItem =
156 serde_json::from_value(json_data).expect("Failed to deserialize");
157
158 assert_eq!(item.symbol, "BTC-12JUN25-104019-C-USDT");
159 assert_eq!(item.side, Side::Sell);
160 assert_eq!(item.total_open_fee, 0.94506647);
161 assert_eq!(item.delivery_fee, 0.32184533);
162 assert_eq!(item.total_close_fee, 0.0);
163 assert_eq!(item.qty, 0.02);
164 assert_eq!(item.close_time, 1749726002161);
165 assert_eq!(item.avg_exit_price, 107281.77405);
166 assert_eq!(item.delivery_price, 107281.77405031);
167 assert_eq!(item.open_time, 1749722990063);
168 assert_eq!(item.avg_entry_price, 3371.5);
169 assert_eq!(item.total_pnl, 0.90760719);
170 }
171
172 #[test]
173 fn test_deserialize_closed_options_positions_response() {
174 let json_data = json!({
175 "nextPageCursor": "1749726002161%3A0%2C1749715220240%3A1",
176 "category": "option",
177 "list": [
178 {
179 "symbol": "BTC-12JUN25-104019-C-USDT",
180 "side": "Sell",
181 "totalOpenFee": "0.94506647",
182 "deliveryFee": "0.32184533",
183 "totalCloseFee": "0.00000000",
184 "qty": "0.02",
185 "closeTime": "1749726002161",
186 "avgExitPrice": "107281.77405000",
187 "deliveryPrice": "107281.77405031",
188 "openTime": "1749722990063",
189 "avgEntryPrice": "3371.50000000",
190 "totalPnl": "0.90760719"
191 },
192 {
193 "symbol": "BTC-12JUN25-104000-C-USDT",
194 "side": "Buy",
195 "totalOpenFee": "0.86379999",
196 "deliveryFee": "0.32287622",
197 "totalCloseFee": "0.00000000",
198 "qty": "0.02",
199 "closeTime": "1749715220240",
200 "avgExitPrice": "107625.40470150",
201 "deliveryPrice": "107625.40470159",
202 "openTime": "1749710568608",
203 "avgEntryPrice": "3946.50000000",
204 "totalPnl": "-7.60858218"
205 }
206 ]
207 });
208
209 let response: ClosedOptionsPositionsResponse =
210 serde_json::from_value(json_data).expect("Failed to deserialize");
211
212 assert_eq!(
213 response.next_page_cursor,
214 "1749726002161%3A0%2C1749715220240%3A1"
215 );
216 assert_eq!(response.category, "option");
217 assert_eq!(response.list.len(), 2);
218
219 let first_item = &response.list[0];
220 assert_eq!(first_item.symbol, "BTC-12JUN25-104019-C-USDT");
221 assert_eq!(first_item.side, Side::Sell);
222 assert_eq!(first_item.total_pnl, 0.90760719);
223
224 let second_item = &response.list[1];
225 assert_eq!(second_item.symbol, "BTC-12JUN25-104000-C-USDT");
226 assert_eq!(second_item.side, Side::Buy);
227 assert_eq!(second_item.total_pnl, -7.60858218);
228 }
229
230 #[test]
231 fn test_deserialize_closed_options_positions_result() {
232 let json_data = json!({
233 "retCode": 0,
234 "retMsg": "OK",
235 "result": {
236 "nextPageCursor": "1749726002161%3A0%2C1749715220240%3A1",
237 "category": "option",
238 "list": [
239 {
240 "symbol": "BTC-12JUN25-104019-C-USDT",
241 "side": "Sell",
242 "totalOpenFee": "0.94506647",
243 "deliveryFee": "0.32184533",
244 "totalCloseFee": "0.00000000",
245 "qty": "0.02",
246 "closeTime": "1749726002161",
247 "avgExitPrice": "107281.77405000",
248 "deliveryPrice": "107281.77405031",
249 "openTime": "1749722990063",
250 "avgEntryPrice": "3371.50000000",
251 "totalPnl": "0.90760719"
252 }
253 ]
254 },
255 "retExtInfo": {},
256 "time": 1672284129
257 });
258
259 let result: ClosedOptionsPositionsResult =
260 serde_json::from_value(json_data).expect("Failed to deserialize");
261
262 assert_eq!(result.ret_code, 0);
263 assert_eq!(result.ret_msg, "OK");
264 assert_eq!(result.result.category, "option");
265 assert_eq!(result.result.list.len(), 1);
266 assert_eq!(result.time, 1672284129);
267 }
268
269 #[test]
270 fn test_serialize_closed_options_position_item() {
271 let item = ClosedOptionsPositionItem {
272 symbol: "BTC-12JUN25-104019-C-USDT".to_string(),
273 side: Side::Sell,
274 total_open_fee: 0.94506647,
275 delivery_fee: 0.32184533,
276 total_close_fee: 0.0,
277 qty: 0.02,
278 close_time: 1749726002161,
279 avg_exit_price: 107281.77405,
280 delivery_price: 107281.77405031,
281 open_time: 1749722990063,
282 avg_entry_price: 3371.5,
283 total_pnl: 0.90760719,
284 };
285
286 let json_string = serde_json::to_string(&item).expect("Failed to serialize");
287 let parsed: serde_json::Value = serde_json::from_str(&json_string).unwrap();
288
289 assert_eq!(parsed["symbol"], "BTC-12JUN25-104019-C-USDT");
290 assert_eq!(parsed["side"], "Sell");
291 assert_eq!(parsed["totalOpenFee"], "0.94506647");
292 assert_eq!(parsed["totalPnl"], "0.90760719");
293 }
294}