Skip to main content

ccxt_exchanges/bitget/rest/futures/
positions.rs

1//! Position management methods for Bitget futures/swap.
2
3use super::super::super::{Bitget, parser, symbol::BitgetSymbolConverter};
4use ccxt_core::{Error, ParseError, Result, types::Position};
5use tracing::warn;
6
7impl Bitget {
8    /// Fetch a single position for a symbol.
9    ///
10    /// Uses Bitget GET `/api/v2/mix/position/single-position` endpoint.
11    pub async fn fetch_position_impl(&self, symbol: &str) -> Result<Position> {
12        let exchange_id = BitgetSymbolConverter::unified_to_exchange(symbol);
13        let product_type = BitgetSymbolConverter::product_type_from_symbol(symbol);
14
15        let data = self
16            .signed_request("/api/v2/mix/position/single-position")
17            .param("symbol", &exchange_id)
18            .param("productType", product_type)
19            .param("marginCoin", Self::margin_coin_from_symbol(symbol))
20            .execute()
21            .await?;
22
23        let positions_array = data["data"].as_array().ok_or_else(|| {
24            Error::from(ParseError::invalid_format("data", "Expected data array"))
25        })?;
26
27        if positions_array.is_empty() {
28            return Err(Error::from(ParseError::missing_field_owned(format!(
29                "No position found for symbol: {}",
30                symbol
31            ))));
32        }
33
34        parser::parse_position(&positions_array[0], symbol)
35    }
36
37    /// Fetch positions for specific symbols, or all positions if empty.
38    ///
39    /// Uses Bitget GET `/api/v2/mix/position/all-position` endpoint.
40    pub async fn fetch_positions_impl(&self, symbols: &[&str]) -> Result<Vec<Position>> {
41        let product_type = if symbols.is_empty() {
42            self.options().effective_product_type()
43        } else {
44            BitgetSymbolConverter::product_type_from_symbol(symbols[0])
45        };
46
47        let margin_coin = if symbols.is_empty() {
48            "USDT".to_string()
49        } else {
50            Self::margin_coin_from_symbol(symbols[0]).to_string()
51        };
52
53        let data = self
54            .signed_request("/api/v2/mix/position/all-position")
55            .param("productType", product_type)
56            .param("marginCoin", &margin_coin)
57            .execute()
58            .await?;
59
60        let positions_array = data["data"].as_array().ok_or_else(|| {
61            Error::from(ParseError::invalid_format("data", "Expected data array"))
62        })?;
63
64        let mut positions = Vec::new();
65        for position_data in positions_array {
66            let bitget_symbol = position_data["symbol"].as_str().unwrap_or_default();
67            let symbol_hint =
68                BitgetSymbolConverter::exchange_to_unified_hint(bitget_symbol, product_type);
69
70            match parser::parse_position(position_data, &symbol_hint) {
71                Ok(position) => {
72                    if symbols.is_empty() || symbols.contains(&position.symbol.as_str()) {
73                        positions.push(position);
74                    }
75                }
76                Err(e) => {
77                    warn!(error = %e, symbol = %bitget_symbol, "Failed to parse Bitget position");
78                }
79            }
80        }
81
82        Ok(positions)
83    }
84
85    /// Extract margin coin from a unified symbol.
86    ///
87    /// - "BTC/USDT:USDT" → "USDT"
88    /// - "BTC/USD:BTC" → "BTC"
89    /// - "BTC/USDT" → "USDT" (fallback to quote)
90    pub(crate) fn margin_coin_from_symbol(symbol: &str) -> &str {
91        if let Some(pos) = symbol.find(':') {
92            let settle_part = &symbol[pos + 1..];
93            // Strip expiry date if present
94            if let Some(dash_pos) = settle_part.find('-') {
95                &settle_part[..dash_pos]
96            } else {
97                settle_part
98            }
99        } else if let Some(pos) = symbol.find('/') {
100            &symbol[pos + 1..]
101        } else {
102            "USDT"
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_margin_coin_from_symbol() {
113        assert_eq!(Bitget::margin_coin_from_symbol("BTC/USDT:USDT"), "USDT");
114        assert_eq!(Bitget::margin_coin_from_symbol("BTC/USD:BTC"), "BTC");
115        assert_eq!(Bitget::margin_coin_from_symbol("BTC/USDT"), "USDT");
116        assert_eq!(
117            Bitget::margin_coin_from_symbol("BTC/USDT:USDT-241231"),
118            "USDT"
119        );
120    }
121}