1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CloseTrade {
11 #[serde(default)]
13 pub trade_seq: Option<u64>,
14 #[serde(default)]
16 pub trade_id: Option<String>,
17 #[serde(default)]
19 pub timestamp: Option<u64>,
20 #[serde(default)]
22 pub tick_direction: Option<i32>,
23 #[serde(default)]
25 pub state: Option<String>,
26 #[serde(default)]
28 pub reduce_only: Option<bool>,
29 #[serde(default)]
31 pub price: Option<f64>,
32 #[serde(default)]
34 pub post_only: Option<bool>,
35 #[serde(default)]
37 pub order_type: Option<String>,
38 #[serde(default)]
40 pub order_id: Option<String>,
41 #[serde(default)]
43 pub matching_id: Option<String>,
44 #[serde(default)]
46 pub mark_price: Option<f64>,
47 #[serde(default)]
49 pub liquidity: Option<String>,
50 #[serde(default)]
52 pub instrument_name: Option<String>,
53 #[serde(default)]
55 pub index_price: Option<f64>,
56 #[serde(default)]
58 pub fee_currency: Option<String>,
59 #[serde(default)]
61 pub fee: Option<f64>,
62 #[serde(default)]
64 pub direction: Option<String>,
65 #[serde(default)]
67 pub amount: Option<f64>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CloseOrder {
73 #[serde(default)]
75 pub web: Option<bool>,
76 #[serde(default)]
78 pub time_in_force: Option<String>,
79 #[serde(default)]
81 pub replaced: Option<bool>,
82 #[serde(default)]
84 pub reduce_only: Option<bool>,
85 #[serde(default)]
87 pub price: Option<f64>,
88 #[serde(default)]
90 pub post_only: Option<bool>,
91 #[serde(default)]
93 pub order_type: Option<String>,
94 #[serde(default)]
96 pub order_state: Option<String>,
97 #[serde(default)]
99 pub order_id: Option<String>,
100 #[serde(default)]
102 pub max_show: Option<f64>,
103 #[serde(default)]
105 pub last_update_timestamp: Option<u64>,
106 #[serde(default)]
108 pub label: Option<String>,
109 #[serde(default)]
111 pub is_rebalance: Option<bool>,
112 #[serde(default)]
114 pub is_liquidation: Option<bool>,
115 #[serde(default)]
117 pub instrument_name: Option<String>,
118 #[serde(default)]
120 pub filled_amount: Option<f64>,
121 #[serde(default)]
123 pub direction: Option<String>,
124 #[serde(default)]
126 pub creation_timestamp: Option<u64>,
127 #[serde(default)]
129 pub average_price: Option<f64>,
130 #[serde(default)]
132 pub api: Option<bool>,
133 #[serde(default)]
135 pub amount: Option<f64>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ClosePositionResponse {
141 #[serde(default)]
143 pub trades: Vec<CloseTrade>,
144 #[serde(default)]
146 pub order: Option<CloseOrder>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MovePositionTrade {
152 pub instrument_name: String,
154 pub amount: f64,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub price: Option<f64>,
159}
160
161impl MovePositionTrade {
162 #[must_use]
169 pub fn new(instrument_name: &str, amount: f64) -> Self {
170 Self {
171 instrument_name: instrument_name.to_string(),
172 amount,
173 price: None,
174 }
175 }
176
177 #[must_use]
179 pub fn with_price(mut self, price: f64) -> Self {
180 self.price = Some(price);
181 self
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct MovePositionResult {
188 pub target_uid: u64,
190 pub source_uid: u64,
192 pub price: f64,
194 pub instrument_name: String,
196 pub direction: String,
198 pub amount: f64,
200}
201
202#[cfg(test)]
203#[allow(clippy::unwrap_used, clippy::expect_used)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_move_position_trade_new() {
209 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
210 assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
211 assert_eq!(trade.amount, 100.0);
212 assert!(trade.price.is_none());
213 }
214
215 #[test]
216 fn test_move_position_trade_with_price() {
217 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0).with_price(50000.0);
218 assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
219 assert_eq!(trade.amount, 100.0);
220 assert_eq!(trade.price, Some(50000.0));
221 }
222
223 #[test]
224 fn test_move_position_trade_serialization() {
225 let trade = MovePositionTrade::new("ETH-PERPETUAL", 50.0).with_price(3000.0);
226 let json = serde_json::to_string(&trade).expect("serialize");
227 assert!(json.contains("ETH-PERPETUAL"));
228 assert!(json.contains("50"));
229 assert!(json.contains("3000"));
230 }
231
232 #[test]
233 fn test_move_position_trade_without_price_serialization() {
234 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
235 let json = serde_json::to_string(&trade).expect("serialize");
236 assert!(!json.contains("price"));
237 }
238
239 #[test]
240 fn test_move_position_result_deserialization() {
241 let json = r#"{
242 "target_uid": 23,
243 "source_uid": 3,
244 "price": 35800.0,
245 "instrument_name": "BTC-PERPETUAL",
246 "direction": "buy",
247 "amount": 110.0
248 }"#;
249
250 let result: MovePositionResult = serde_json::from_str(json).expect("deserialize");
251 assert_eq!(result.target_uid, 23);
252 assert_eq!(result.source_uid, 3);
253 assert_eq!(result.price, 35800.0);
254 assert_eq!(result.instrument_name, "BTC-PERPETUAL");
255 assert_eq!(result.direction, "buy");
256 assert_eq!(result.amount, 110.0);
257 }
258
259 #[test]
260 fn test_close_position_response_deserialization() {
261 let json = r#"{
262 "trades": [{
263 "trade_seq": 1966068,
264 "trade_id": "ETH-2696097",
265 "timestamp": 1590486335742,
266 "tick_direction": 0,
267 "state": "filled",
268 "reduce_only": true,
269 "price": 202.8,
270 "post_only": false,
271 "order_type": "limit",
272 "order_id": "ETH-584864807",
273 "mark_price": 202.79,
274 "liquidity": "T",
275 "instrument_name": "ETH-PERPETUAL",
276 "index_price": 202.86,
277 "fee_currency": "ETH",
278 "fee": 0.00007766,
279 "direction": "sell",
280 "amount": 21.0
281 }],
282 "order": {
283 "time_in_force": "good_til_cancelled",
284 "reduce_only": true,
285 "price": 198.75,
286 "post_only": false,
287 "order_type": "limit",
288 "order_state": "filled",
289 "order_id": "ETH-584864807",
290 "instrument_name": "ETH-PERPETUAL",
291 "filled_amount": 21.0,
292 "direction": "sell",
293 "creation_timestamp": 1590486335742,
294 "average_price": 202.8,
295 "api": true,
296 "amount": 21.0
297 }
298 }"#;
299
300 let response: ClosePositionResponse = serde_json::from_str(json).expect("deserialize");
301 assert_eq!(response.trades.len(), 1);
302 assert!(response.order.is_some());
303
304 let trade = &response.trades[0];
305 assert_eq!(trade.trade_id, Some("ETH-2696097".to_string()));
306 assert_eq!(trade.price, Some(202.8));
307 assert_eq!(trade.direction, Some("sell".to_string()));
308
309 let order = response.order.as_ref().expect("order");
310 assert_eq!(order.order_id, Some("ETH-584864807".to_string()));
311 assert_eq!(order.order_state, Some("filled".to_string()));
312 }
313
314 #[test]
315 fn test_close_trade_deserialization() {
316 let json = r#"{
317 "trade_seq": 12345,
318 "trade_id": "BTC-123456",
319 "price": 50000.0,
320 "amount": 1.0,
321 "direction": "buy"
322 }"#;
323
324 let trade: CloseTrade = serde_json::from_str(json).expect("deserialize");
325 assert_eq!(trade.trade_seq, Some(12345));
326 assert_eq!(trade.trade_id, Some("BTC-123456".to_string()));
327 assert_eq!(trade.price, Some(50000.0));
328 }
329
330 #[test]
331 fn test_close_order_deserialization() {
332 let json = r#"{
333 "order_id": "BTC-123",
334 "order_state": "open",
335 "order_type": "limit",
336 "price": 45000.0,
337 "amount": 100.0,
338 "direction": "sell"
339 }"#;
340
341 let order: CloseOrder = serde_json::from_str(json).expect("deserialize");
342 assert_eq!(order.order_id, Some("BTC-123".to_string()));
343 assert_eq!(order.order_state, Some("open".to_string()));
344 assert_eq!(order.price, Some(45000.0));
345 }
346}