bybit/models/closed_options_positions_request.rs
1use crate::prelude::*;
2
3/// Parameters for requesting closed options positions.
4///
5/// Used to construct a request to the `/v5/position/get-closed-positions` endpoint to retrieve closed options positions.
6/// Bots use this to analyze historical options trading performance, track P&L, and audit trading activity.
7///
8/// # Bybit API Reference
9/// According to the Bybit V5 API documentation:
10/// - Only supports querying closed options positions in the last 6 months.
11/// - Sorted by `closeTime` in descending order.
12/// - Fee and price are displayed with trailing zeroes up to 8 decimal places.
13///
14/// # Usage Example
15/// ```rust
16/// // Query closed options positions for a specific symbol
17/// let request = ClosedOptionsPositionsRequest::new(
18/// Category::Option,
19/// Some("BTC-12JUN25-104019-C-USDT"),
20/// None,
21/// None,
22/// Some(50),
23/// None,
24/// );
25///
26/// // Query all closed options positions from the last day
27/// let request = ClosedOptionsPositionsRequest::new(
28/// Category::Option,
29/// None,
30/// Some(1749730000000), // startTime in milliseconds
31/// None,
32/// None,
33/// None,
34/// );
35/// ```
36#[derive(Clone, Default)]
37pub struct ClosedOptionsPositionsRequest<'a> {
38 /// The product category (must be `option`).
39 ///
40 /// Specifies the instrument type. For this endpoint, must be `Category::Option`.
41 pub category: Category,
42
43 /// The options symbol name (e.g., "BTC-12JUN25-104019-C-USDT") (optional).
44 ///
45 /// Filters closed positions by symbol. If unset, all closed options positions are returned.
46 /// Bots should specify this for targeted analysis of specific options contracts.
47 pub symbol: Option<Cow<'a, str>>,
48
49 /// The start timestamp in milliseconds (optional).
50 ///
51 /// The beginning of the time range for querying closed positions.
52 /// According to Bybit API:
53 /// - If neither `start_time` nor `end_time` are provided, returns data from the last 1 day
54 /// - If only `start_time` is provided, returns range between `start_time` and `start_time + 1 day`
55 /// - If only `end_time` is provided, returns range between `end_time - 1 day` and `end_time`
56 /// - If both are provided, the rule is `end_time - start_time <= 7 days`
57 pub start_time: Option<u64>,
58
59 /// The end timestamp in milliseconds (optional).
60 ///
61 /// The end of the time range for querying closed positions.
62 pub end_time: Option<u64>,
63
64 /// The maximum number of records to return (optional).
65 ///
66 /// Limit for data size per page. Valid range: [`1`, `100`]. Default: `50`.
67 /// Bots should use pagination with the `cursor` field for large result sets.
68 pub limit: Option<usize>,
69
70 /// The cursor for pagination (optional).
71 ///
72 /// Use the `nextPageCursor` token from the response to retrieve the next page of results.
73 /// Bots use this to efficiently paginate through large sets of closed positions.
74 pub cursor: Option<Cow<'a, str>>,
75}
76
77impl<'a> ClosedOptionsPositionsRequest<'a> {
78 /// Constructs a new ClosedOptionsPositions request with specified parameters.
79 ///
80 /// Allows full customization of the closed options positions query.
81 /// Bots should use this to specify time ranges, symbols, and pagination parameters.
82 ///
83 /// # Arguments
84 /// * `category` - The product category (must be `Category::Option`)
85 /// * `symbol` - The options symbol name (optional)
86 /// * `start_time` - The start timestamp in milliseconds (optional)
87 /// * `end_time` - The end timestamp in milliseconds (optional)
88 /// * `limit` - The maximum number of records to return (optional, range: 1-100)
89 /// * `cursor` - The cursor for pagination (optional)
90 pub fn new(
91 category: Category,
92 symbol: Option<&'a str>,
93 start_time: Option<u64>,
94 end_time: Option<u64>,
95 limit: Option<usize>,
96 cursor: Option<&'a str>,
97 ) -> Self {
98 Self {
99 category,
100 symbol: symbol.map(Cow::Borrowed),
101 start_time,
102 end_time,
103 limit,
104 cursor: cursor.map(Cow::Borrowed),
105 }
106 }
107
108 /// Creates a default ClosedOptionsPositions request.
109 ///
110 /// Returns a request with `category` set to `Option`, no symbol filter,
111 /// no time range (returns last 1 day by default), limit set to `50`, and no cursor.
112 /// Suitable for testing but should be customized for production analysis.
113 pub fn default() -> ClosedOptionsPositionsRequest<'a> {
114 ClosedOptionsPositionsRequest::new(Category::Option, None, None, None, Some(50), None)
115 }
116
117 /// Validates the request parameters according to Bybit API constraints.
118 ///
119 /// Bots should call this method before sending the request to ensure compliance with API limits.
120 ///
121 /// # Returns
122 /// * `Ok(())` if validation passes
123 /// * `Err(String)` with error message if validation fails
124 pub fn validate(&self) -> Result<(), String> {
125 // Category must be Option
126 if !matches!(self.category, Category::Option) {
127 return Err("Category must be 'option' for closed options positions".to_string());
128 }
129
130 // Validate limit range
131 if let Some(limit) = self.limit {
132 if limit < 1 || limit > 100 {
133 return Err("Limit must be between 1 and 100".to_string());
134 }
135 }
136
137 // Validate time range if both start_time and end_time are provided
138 if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
139 if start >= end {
140 return Err("start_time must be less than end_time".to_string());
141 }
142
143 // Check if time range exceeds 7 days (604800000 milliseconds)
144 if end - start > 7 * 24 * 60 * 60 * 1000 {
145 return Err("Time range cannot exceed 7 days".to_string());
146 }
147 }
148
149 Ok(())
150 }
151
152 /// Sets the symbol filter for the request.
153 ///
154 /// Convenience method for updating the symbol filter.
155 ///
156 /// # Arguments
157 /// * `symbol` - The options symbol name
158 pub fn with_symbol(mut self, symbol: &'a str) -> Self {
159 self.symbol = Some(Cow::Borrowed(symbol));
160 self
161 }
162
163 /// Sets the time range for the request.
164 ///
165 /// Convenience method for updating both start and end times.
166 ///
167 /// # Arguments
168 /// * `start_time` - The start timestamp in milliseconds
169 /// * `end_time` - The end timestamp in milliseconds
170 pub fn with_time_range(mut self, start_time: u64, end_time: u64) -> Self {
171 self.start_time = Some(start_time);
172 self.end_time = Some(end_time);
173 self
174 }
175
176 /// Sets the limit for the request.
177 ///
178 /// Convenience method for updating the result limit.
179 ///
180 /// # Arguments
181 /// * `limit` - The maximum number of records to return (1-100)
182 pub fn with_limit(mut self, limit: usize) -> Self {
183 self.limit = Some(limit);
184 self
185 }
186
187 /// Sets the cursor for pagination.
188 ///
189 /// Convenience method for updating the pagination cursor.
190 ///
191 /// # Arguments
192 /// * `cursor` - The cursor string from previous response
193 pub fn with_cursor(mut self, cursor: &'a str) -> Self {
194 self.cursor = Some(Cow::Borrowed(cursor));
195 self
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_new() {
205 let request = ClosedOptionsPositionsRequest::new(
206 Category::Option,
207 Some("BTC-12JUN25-104019-C-USDT"),
208 Some(1749730000000),
209 Some(1749736000000),
210 Some(25),
211 Some("cursor_token"),
212 );
213
214 assert_eq!(request.category, Category::Option);
215 assert_eq!(request.symbol.unwrap(), "BTC-12JUN25-104019-C-USDT");
216 assert_eq!(request.start_time.unwrap(), 1749730000000);
217 assert_eq!(request.end_time.unwrap(), 1749736000000);
218 assert_eq!(request.limit.unwrap(), 25);
219 assert_eq!(request.cursor.unwrap(), "cursor_token");
220 }
221
222 #[test]
223 fn test_default() {
224 let request = ClosedOptionsPositionsRequest::default();
225 assert_eq!(request.category, Category::Option);
226 assert!(request.symbol.is_none());
227 assert!(request.start_time.is_none());
228 assert!(request.end_time.is_none());
229 assert_eq!(request.limit.unwrap(), 50);
230 assert!(request.cursor.is_none());
231 }
232
233 #[test]
234 fn test_validation() {
235 // Valid request
236 let valid_request =
237 ClosedOptionsPositionsRequest::new(Category::Option, None, None, None, Some(50), None);
238 assert!(valid_request.validate().is_ok());
239
240 // Invalid category
241 let invalid_category =
242 ClosedOptionsPositionsRequest::new(Category::Linear, None, None, None, Some(50), None);
243 assert!(invalid_category.validate().is_err());
244
245 // Invalid limit (too low)
246 let invalid_limit_low =
247 ClosedOptionsPositionsRequest::new(Category::Option, None, None, None, Some(0), None);
248 assert!(invalid_limit_low.validate().is_err());
249
250 // Invalid limit (too high)
251 let invalid_limit_high =
252 ClosedOptionsPositionsRequest::new(Category::Option, None, None, None, Some(101), None);
253 assert!(invalid_limit_high.validate().is_err());
254
255 // Invalid time range (start >= end)
256 let invalid_time_range = ClosedOptionsPositionsRequest::new(
257 Category::Option,
258 None,
259 Some(1749736000000),
260 Some(1749730000000),
261 Some(50),
262 None,
263 );
264 assert!(invalid_time_range.validate().is_err());
265
266 // Invalid time range (exceeds 7 days)
267 let invalid_time_range_long = ClosedOptionsPositionsRequest::new(
268 Category::Option,
269 None,
270 Some(1749730000000),
271 Some(1749730000000 + 8 * 24 * 60 * 60 * 1000), // 8 days later
272 Some(50),
273 None,
274 );
275 assert!(invalid_time_range_long.validate().is_err());
276 }
277
278 #[test]
279 fn test_builder_methods() {
280 let request = ClosedOptionsPositionsRequest::default()
281 .with_symbol("BTC-12JUN25-104019-C-USDT")
282 .with_time_range(1749730000000, 1749736000000)
283 .with_limit(25)
284 .with_cursor("cursor_token");
285
286 assert_eq!(request.category, Category::Option);
287 assert_eq!(request.symbol.unwrap(), "BTC-12JUN25-104019-C-USDT");
288 assert_eq!(request.start_time.unwrap(), 1749730000000);
289 assert_eq!(request.end_time.unwrap(), 1749736000000);
290 assert_eq!(request.limit.unwrap(), 25);
291 assert_eq!(request.cursor.unwrap(), "cursor_token");
292 }
293}