Skip to main content

deribit_websocket/model/
position.rs

1//! Position management model definitions for Deribit WebSocket API
2//!
3//! This module provides types for position management operations including
4//! closing positions and moving positions between subaccounts.
5
6use serde::{Deserialize, Serialize};
7
8/// Trade information from a close_position response
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CloseTrade {
11    /// Trade sequence number
12    #[serde(default)]
13    pub trade_seq: Option<u64>,
14    /// Unique trade identifier
15    #[serde(default)]
16    pub trade_id: Option<String>,
17    /// Trade timestamp in milliseconds
18    #[serde(default)]
19    pub timestamp: Option<u64>,
20    /// Tick direction (0, 1, 2, 3)
21    #[serde(default)]
22    pub tick_direction: Option<i32>,
23    /// Trade state (filled, etc.)
24    #[serde(default)]
25    pub state: Option<String>,
26    /// Whether this was a reduce-only trade
27    #[serde(default)]
28    pub reduce_only: Option<bool>,
29    /// Trade price
30    #[serde(default)]
31    pub price: Option<f64>,
32    /// Whether this was a post-only order
33    #[serde(default)]
34    pub post_only: Option<bool>,
35    /// Order type (limit, market)
36    #[serde(default)]
37    pub order_type: Option<String>,
38    /// Order ID
39    #[serde(default)]
40    pub order_id: Option<String>,
41    /// Matching ID
42    #[serde(default)]
43    pub matching_id: Option<String>,
44    /// Mark price at time of trade
45    #[serde(default)]
46    pub mark_price: Option<f64>,
47    /// Liquidity type (T = taker, M = maker)
48    #[serde(default)]
49    pub liquidity: Option<String>,
50    /// Instrument name
51    #[serde(default)]
52    pub instrument_name: Option<String>,
53    /// Index price at time of trade
54    #[serde(default)]
55    pub index_price: Option<f64>,
56    /// Fee currency
57    #[serde(default)]
58    pub fee_currency: Option<String>,
59    /// Fee amount
60    #[serde(default)]
61    pub fee: Option<f64>,
62    /// Trade direction (buy/sell)
63    #[serde(default)]
64    pub direction: Option<String>,
65    /// Trade amount
66    #[serde(default)]
67    pub amount: Option<f64>,
68}
69
70/// Order information from a close_position response
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CloseOrder {
73    /// Whether the order was created via web
74    #[serde(default)]
75    pub web: Option<bool>,
76    /// Time in force
77    #[serde(default)]
78    pub time_in_force: Option<String>,
79    /// Whether the order was replaced
80    #[serde(default)]
81    pub replaced: Option<bool>,
82    /// Whether this is a reduce-only order
83    #[serde(default)]
84    pub reduce_only: Option<bool>,
85    /// Order price
86    #[serde(default)]
87    pub price: Option<f64>,
88    /// Whether this is a post-only order
89    #[serde(default)]
90    pub post_only: Option<bool>,
91    /// Order type (limit, market)
92    #[serde(default)]
93    pub order_type: Option<String>,
94    /// Order state (open, filled, cancelled)
95    #[serde(default)]
96    pub order_state: Option<String>,
97    /// Order ID
98    #[serde(default)]
99    pub order_id: Option<String>,
100    /// Maximum display amount
101    #[serde(default)]
102    pub max_show: Option<f64>,
103    /// Last update timestamp
104    #[serde(default)]
105    pub last_update_timestamp: Option<u64>,
106    /// Order label
107    #[serde(default)]
108    pub label: Option<String>,
109    /// Whether this is a rebalance order
110    #[serde(default)]
111    pub is_rebalance: Option<bool>,
112    /// Whether this is a liquidation order
113    #[serde(default)]
114    pub is_liquidation: Option<bool>,
115    /// Instrument name
116    #[serde(default)]
117    pub instrument_name: Option<String>,
118    /// Filled amount
119    #[serde(default)]
120    pub filled_amount: Option<f64>,
121    /// Order direction (buy/sell)
122    #[serde(default)]
123    pub direction: Option<String>,
124    /// Creation timestamp
125    #[serde(default)]
126    pub creation_timestamp: Option<u64>,
127    /// Average fill price
128    #[serde(default)]
129    pub average_price: Option<f64>,
130    /// Whether created via API
131    #[serde(default)]
132    pub api: Option<bool>,
133    /// Order amount
134    #[serde(default)]
135    pub amount: Option<f64>,
136}
137
138/// Response from close_position API call
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ClosePositionResponse {
141    /// List of trades executed to close the position
142    #[serde(default)]
143    pub trades: Vec<CloseTrade>,
144    /// The order placed to close the position
145    #[serde(default)]
146    pub order: Option<CloseOrder>,
147}
148
149/// Trade specification for move_positions request
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MovePositionTrade {
152    /// Instrument name (e.g., "BTC-PERPETUAL")
153    pub instrument_name: String,
154    /// Amount to move
155    pub amount: f64,
156    /// Optional price at which to move the position
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub price: Option<f64>,
159}
160
161impl MovePositionTrade {
162    /// Create a new move position trade
163    ///
164    /// # Arguments
165    ///
166    /// * `instrument_name` - The instrument name
167    /// * `amount` - The amount to move
168    #[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    /// Set the price for the position move
178    #[must_use]
179    pub fn with_price(mut self, price: f64) -> Self {
180        self.price = Some(price);
181        self
182    }
183}
184
185/// Result of a single position move
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct MovePositionResult {
188    /// Target subaccount ID
189    pub target_uid: u64,
190    /// Source subaccount ID
191    pub source_uid: u64,
192    /// Price at which the position was moved
193    pub price: f64,
194    /// Instrument name
195    pub instrument_name: String,
196    /// Direction of the position (buy/sell)
197    pub direction: String,
198    /// Amount that was moved
199    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}