Skip to main content

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}