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)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_move_position_trade_new() {
208 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
209 assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
210 assert_eq!(trade.amount, 100.0);
211 assert!(trade.price.is_none());
212 }
213
214 #[test]
215 fn test_move_position_trade_with_price() {
216 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0).with_price(50000.0);
217 assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
218 assert_eq!(trade.amount, 100.0);
219 assert_eq!(trade.price, Some(50000.0));
220 }
221
222 #[test]
223 fn test_move_position_trade_serialization() {
224 let trade = MovePositionTrade::new("ETH-PERPETUAL", 50.0).with_price(3000.0);
225 let json = serde_json::to_string(&trade).expect("serialize");
226 assert!(json.contains("ETH-PERPETUAL"));
227 assert!(json.contains("50"));
228 assert!(json.contains("3000"));
229 }
230
231 #[test]
232 fn test_move_position_trade_without_price_serialization() {
233 let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
234 let json = serde_json::to_string(&trade).expect("serialize");
235 assert!(!json.contains("price"));
236 }
237
238 #[test]
239 fn test_move_position_result_deserialization() {
240 let json = r#"{
241 "target_uid": 23,
242 "source_uid": 3,
243 "price": 35800.0,
244 "instrument_name": "BTC-PERPETUAL",
245 "direction": "buy",
246 "amount": 110.0
247 }"#;
248
249 let result: MovePositionResult = serde_json::from_str(json).expect("deserialize");
250 assert_eq!(result.target_uid, 23);
251 assert_eq!(result.source_uid, 3);
252 assert_eq!(result.price, 35800.0);
253 assert_eq!(result.instrument_name, "BTC-PERPETUAL");
254 assert_eq!(result.direction, "buy");
255 assert_eq!(result.amount, 110.0);
256 }
257
258 #[test]
259 fn test_close_position_response_deserialization() {
260 let json = r#"{
261 "trades": [{
262 "trade_seq": 1966068,
263 "trade_id": "ETH-2696097",
264 "timestamp": 1590486335742,
265 "tick_direction": 0,
266 "state": "filled",
267 "reduce_only": true,
268 "price": 202.8,
269 "post_only": false,
270 "order_type": "limit",
271 "order_id": "ETH-584864807",
272 "mark_price": 202.79,
273 "liquidity": "T",
274 "instrument_name": "ETH-PERPETUAL",
275 "index_price": 202.86,
276 "fee_currency": "ETH",
277 "fee": 0.00007766,
278 "direction": "sell",
279 "amount": 21.0
280 }],
281 "order": {
282 "time_in_force": "good_til_cancelled",
283 "reduce_only": true,
284 "price": 198.75,
285 "post_only": false,
286 "order_type": "limit",
287 "order_state": "filled",
288 "order_id": "ETH-584864807",
289 "instrument_name": "ETH-PERPETUAL",
290 "filled_amount": 21.0,
291 "direction": "sell",
292 "creation_timestamp": 1590486335742,
293 "average_price": 202.8,
294 "api": true,
295 "amount": 21.0
296 }
297 }"#;
298
299 let response: ClosePositionResponse = serde_json::from_str(json).expect("deserialize");
300 assert_eq!(response.trades.len(), 1);
301 assert!(response.order.is_some());
302
303 let trade = &response.trades[0];
304 assert_eq!(trade.trade_id, Some("ETH-2696097".to_string()));
305 assert_eq!(trade.price, Some(202.8));
306 assert_eq!(trade.direction, Some("sell".to_string()));
307
308 let order = response.order.as_ref().expect("order");
309 assert_eq!(order.order_id, Some("ETH-584864807".to_string()));
310 assert_eq!(order.order_state, Some("filled".to_string()));
311 }
312
313 #[test]
314 fn test_close_trade_deserialization() {
315 let json = r#"{
316 "trade_seq": 12345,
317 "trade_id": "BTC-123456",
318 "price": 50000.0,
319 "amount": 1.0,
320 "direction": "buy"
321 }"#;
322
323 let trade: CloseTrade = serde_json::from_str(json).expect("deserialize");
324 assert_eq!(trade.trade_seq, Some(12345));
325 assert_eq!(trade.trade_id, Some("BTC-123456".to_string()));
326 assert_eq!(trade.price, Some(50000.0));
327 }
328
329 #[test]
330 fn test_close_order_deserialization() {
331 let json = r#"{
332 "order_id": "BTC-123",
333 "order_state": "open",
334 "order_type": "limit",
335 "price": 45000.0,
336 "amount": 100.0,
337 "direction": "sell"
338 }"#;
339
340 let order: CloseOrder = serde_json::from_str(json).expect("deserialize");
341 assert_eq!(order.order_id, Some("BTC-123".to_string()));
342 assert_eq!(order.order_state, Some("open".to_string()));
343 assert_eq!(order.price, Some(45000.0));
344 }
345}