ccxt_exchanges/okx/rest/futures/
positions.rs1use super::super::super::{Okx, parser};
4use ccxt_core::{Error, ParseError, Result, types::Position};
5use tracing::warn;
6
7impl Okx {
8 pub(crate) fn inst_type_from_symbol(symbol: &str) -> &'static str {
15 if symbol.contains(':') {
16 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 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 let expiry = &settle_part[dash_pos + 1..];
47 format!("{}-{}", base_quote_dash, expiry)
48 } else {
49 format!("{}-SWAP", base_quote_dash)
51 }
52 } else {
53 symbol.replace('/', "-")
55 }
56 }
57
58 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 pub async fn fetch_positions_impl(&self, symbols: &[&str]) -> Result<Vec<Position>> {
90 let inst_type = if symbols.is_empty() {
91 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 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 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 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 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 format!("{}/{}", parts[0], parts[1])
149 }
150 3 if parts[2] == "SWAP" => {
151 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 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}