Skip to main content

ccxt_exchanges/okx/rest/futures/
positions.rs

1//! Position management methods for OKX futures/swap.
2
3use super::super::super::{Okx, parser};
4use ccxt_core::{Error, ParseError, Result, types::Position};
5use tracing::warn;
6
7impl Okx {
8    /// Map a unified symbol to OKX instType parameter.
9    ///
10    /// Returns the appropriate instType for the given symbol:
11    /// - Symbols with `:` and expiry date → "FUTURES"
12    /// - Symbols with `:` (perpetual) → "SWAP"
13    /// - Otherwise → "SWAP" (default for contract queries)
14    pub(crate) fn inst_type_from_symbol(symbol: &str) -> &'static str {
15        if symbol.contains(':') {
16            // Check if it has an expiry date (e.g., BTC/USDT:USDT-241231)
17            if symbol.contains('-')
18                && symbol
19                    .rsplit('-')
20                    .next()
21                    .is_some_and(|s| s.len() == 6 && s.chars().all(|c| c.is_ascii_digit()))
22            {
23                "FUTURES"
24            } else {
25                "SWAP"
26            }
27        } else {
28            "SWAP"
29        }
30    }
31
32    /// Convert a unified symbol to OKX instId.
33    ///
34    /// Examples:
35    /// - "BTC/USDT:USDT" → "BTC-USDT-SWAP"
36    /// - "BTC/USD:BTC" → "BTC-USD-SWAP"
37    /// - "BTC/USDT" → "BTC-USDT"
38    pub(crate) fn symbol_to_inst_id(symbol: &str) -> String {
39        if let Some(pos) = symbol.find(':') {
40            let base_quote = &symbol[..pos];
41            let settle_part = &symbol[pos + 1..];
42            let base_quote_dash = base_quote.replace('/', "-");
43
44            if let Some(dash_pos) = settle_part.find('-') {
45                // Futures with expiry: BTC/USDT:USDT-241231 → BTC-USDT-241231
46                let expiry = &settle_part[dash_pos + 1..];
47                format!("{}-{}", base_quote_dash, expiry)
48            } else {
49                // Perpetual swap: BTC/USDT:USDT → BTC-USDT-SWAP
50                format!("{}-SWAP", base_quote_dash)
51            }
52        } else {
53            // Spot: BTC/USDT → BTC-USDT
54            symbol.replace('/', "-")
55        }
56    }
57
58    /// Fetch a single position for a symbol.
59    ///
60    /// Uses OKX GET `/api/v5/account/positions` endpoint.
61    pub async fn fetch_position_impl(&self, symbol: &str) -> Result<Position> {
62        let inst_id = Self::symbol_to_inst_id(symbol);
63        let inst_type = Self::inst_type_from_symbol(symbol);
64
65        let data = self
66            .signed_request("/api/v5/account/positions")
67            .param("instId", &inst_id)
68            .param("instType", inst_type)
69            .execute()
70            .await?;
71
72        let positions_array = data["data"].as_array().ok_or_else(|| {
73            Error::from(ParseError::invalid_format("data", "Expected data array"))
74        })?;
75
76        if positions_array.is_empty() {
77            return Err(Error::from(ParseError::missing_field_owned(format!(
78                "No position found for symbol: {}",
79                symbol
80            ))));
81        }
82
83        parser::parse_position(&positions_array[0], symbol)
84    }
85
86    /// Fetch positions for specific symbols, or all positions if empty.
87    ///
88    /// Uses OKX GET `/api/v5/account/positions` endpoint.
89    pub async fn fetch_positions_impl(&self, symbols: &[&str]) -> Result<Vec<Position>> {
90        let inst_type = if symbols.is_empty() {
91            // Fetch all — use the configured default type
92            self.get_inst_type()
93        } else {
94            Self::inst_type_from_symbol(symbols[0])
95        };
96
97        let mut builder = self
98            .signed_request("/api/v5/account/positions")
99            .param("instType", inst_type);
100
101        // OKX supports filtering by instId (comma-separated for up to 10)
102        if !symbols.is_empty() && symbols.len() <= 10 {
103            let inst_ids: Vec<String> =
104                symbols.iter().map(|s| Self::symbol_to_inst_id(s)).collect();
105            builder = builder.param("instId", inst_ids.join(","));
106        }
107
108        let data = builder.execute().await?;
109
110        let positions_array = data["data"].as_array().ok_or_else(|| {
111            Error::from(ParseError::invalid_format("data", "Expected data array"))
112        })?;
113
114        let mut positions = Vec::new();
115        for position_data in positions_array {
116            let inst_id = position_data["instId"].as_str().unwrap_or_default();
117            // Derive symbol from instId for parsing
118            let symbol_hint = Self::inst_id_to_symbol_hint(inst_id);
119
120            match parser::parse_position(position_data, &symbol_hint) {
121                Ok(position) => {
122                    // Filter by requested symbols if specified
123                    if symbols.is_empty() || symbols.contains(&position.symbol.as_str()) {
124                        positions.push(position);
125                    }
126                }
127                Err(e) => {
128                    warn!(error = %e, inst_id = %inst_id, "Failed to parse OKX position");
129                }
130            }
131        }
132
133        Ok(positions)
134    }
135
136    /// Convert an OKX instId back to a rough unified symbol hint.
137    ///
138    /// This is a best-effort conversion used when market data isn't loaded.
139    /// - "BTC-USDT-SWAP" → "BTC/USDT:USDT"
140    /// - "BTC-USD-SWAP" → "BTC/USD:BTC"
141    /// - "BTC-USDT-241231" → "BTC/USDT:USDT-241231"
142    /// - "BTC-USDT" → "BTC/USDT"
143    fn inst_id_to_symbol_hint(inst_id: &str) -> String {
144        let parts: Vec<&str> = inst_id.split('-').collect();
145        match parts.len() {
146            2 => {
147                // Spot: BTC-USDT → BTC/USDT
148                format!("{}/{}", parts[0], parts[1])
149            }
150            3 if parts[2] == "SWAP" => {
151                // Perpetual: BTC-USDT-SWAP → BTC/USDT:USDT or BTC/USD:BTC
152                let settle = if parts[1] == "USD" {
153                    parts[0]
154                } else {
155                    parts[1]
156                };
157                format!("{}/{}:{}", parts[0], parts[1], settle)
158            }
159            3 => {
160                // Futures with expiry: BTC-USDT-241231 → BTC/USDT:USDT-241231
161                let settle = if parts[1] == "USD" {
162                    parts[0]
163                } else {
164                    parts[1]
165                };
166                format!("{}/{}:{}-{}", parts[0], parts[1], settle, parts[2])
167            }
168            _ => inst_id.to_string(),
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_inst_type_from_symbol() {
179        assert_eq!(Okx::inst_type_from_symbol("BTC/USDT:USDT"), "SWAP");
180        assert_eq!(Okx::inst_type_from_symbol("BTC/USD:BTC"), "SWAP");
181        assert_eq!(
182            Okx::inst_type_from_symbol("BTC/USDT:USDT-241231"),
183            "FUTURES"
184        );
185        assert_eq!(Okx::inst_type_from_symbol("BTC/USDT"), "SWAP");
186    }
187
188    #[test]
189    fn test_symbol_to_inst_id() {
190        assert_eq!(Okx::symbol_to_inst_id("BTC/USDT:USDT"), "BTC-USDT-SWAP");
191        assert_eq!(Okx::symbol_to_inst_id("BTC/USD:BTC"), "BTC-USD-SWAP");
192        assert_eq!(
193            Okx::symbol_to_inst_id("BTC/USDT:USDT-241231"),
194            "BTC-USDT-241231"
195        );
196        assert_eq!(Okx::symbol_to_inst_id("BTC/USDT"), "BTC-USDT");
197        assert_eq!(Okx::symbol_to_inst_id("ETH/USD:ETH"), "ETH-USD-SWAP");
198    }
199
200    #[test]
201    fn test_inst_id_to_symbol_hint() {
202        assert_eq!(
203            Okx::inst_id_to_symbol_hint("BTC-USDT-SWAP"),
204            "BTC/USDT:USDT"
205        );
206        assert_eq!(Okx::inst_id_to_symbol_hint("BTC-USD-SWAP"), "BTC/USD:BTC");
207        assert_eq!(
208            Okx::inst_id_to_symbol_hint("BTC-USDT-241231"),
209            "BTC/USDT:USDT-241231"
210        );
211        assert_eq!(Okx::inst_id_to_symbol_hint("BTC-USDT"), "BTC/USDT");
212    }
213}