digdigdig3/testing/suites/
positions.rs1use std::time::Instant;
10
11use crate::core::traits::{ExchangeIdentity, Positions};
12use crate::core::types::{AccountType, PositionQuery, Symbol};
13
14use super::{assert_position_sane, is_auth_error, is_unsupported, TestResult};
15
16pub async fn run_all(
27 connector: &(dyn PositionsConnector + Send + Sync),
28 symbol: Symbol,
29 account_type: AccountType,
30) -> Vec<TestResult> {
31 let mut results = Vec::new();
32
33 results.push(test_get_positions(connector, symbol.clone(), account_type).await);
34 results.push(test_get_funding_rate(connector, symbol.clone(), account_type).await);
35 results.push(test_get_mark_price(connector, symbol.clone()).await);
36 results.push(test_get_open_interest(connector, symbol.clone(), account_type).await);
37 results.push(test_get_long_short_ratio(connector, symbol.clone(), account_type).await);
38
39 results
40}
41
42pub trait PositionsConnector: Positions + ExchangeIdentity {}
48
49impl<T: Positions + ExchangeIdentity> PositionsConnector for T {}
50
51pub async fn test_get_positions(
60 connector: &(dyn PositionsConnector + Send + Sync),
61 symbol: Symbol,
62 account_type: AccountType,
63) -> TestResult {
64 const NAME: &str = "test_get_positions";
65 let exchange = connector.exchange_name();
66 let start = Instant::now();
67
68 let query = PositionQuery {
69 symbol: Some(symbol),
70 account_type,
71 };
72
73 match connector.get_positions(query).await {
74 Ok(positions) => {
75 for pos in &positions {
76 if let Err(reason) = assert_position_sane(pos) {
77 return TestResult::fail(
78 NAME, exchange,
79 start.elapsed().as_millis() as u64,
80 reason,
81 );
82 }
83 }
84 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
85 }
86 Err(err) if is_unsupported(&err) => {
87 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
88 format!("get_positions unsupported: {err}"))
89 }
90 Err(err) if is_auth_error(&err) => {
91 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
92 format!("auth error: {err}"))
93 }
94 Err(err) => {
95 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
96 format!("get_positions failed: {err}"))
97 }
98 }
99}
100
101pub async fn test_get_funding_rate(
111 connector: &(dyn PositionsConnector + Send + Sync),
112 symbol: Symbol,
113 account_type: AccountType,
114) -> TestResult {
115 const NAME: &str = "test_get_funding_rate";
116 let exchange = connector.exchange_name();
117 let start = Instant::now();
118
119 match connector.get_funding_rate(&symbol.to_concat(), account_type).await {
120 Ok(fr) => {
121 if fr.rate.is_nan() || fr.rate.is_infinite() {
122 return TestResult::fail(
123 NAME, exchange,
124 start.elapsed().as_millis() as u64,
125 format!("funding rate is NaN or infinite: {}", fr.rate),
126 );
127 }
128 if fr.rate <= -1.0 || fr.rate >= 1.0 {
129 return TestResult::fail(
130 NAME, exchange,
131 start.elapsed().as_millis() as u64,
132 format!("funding rate out of reasonable range: {}", fr.rate),
133 );
134 }
135 if let Some(nft) = fr.next_funding_time {
136 if nft <= 0 {
137 return TestResult::fail(
138 NAME, exchange,
139 start.elapsed().as_millis() as u64,
140 format!("next_funding_time must be positive, got {nft}"),
141 );
142 }
143 }
144 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
145 }
146 Err(err) if is_unsupported(&err) => {
147 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
148 format!("get_funding_rate unsupported: {err}"))
149 }
150 Err(err) if is_auth_error(&err) => {
151 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
152 format!("auth error: {err}"))
153 }
154 Err(err) => {
155 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
156 format!("get_funding_rate failed: {err}"))
157 }
158 }
159}
160
161pub async fn test_get_mark_price(
167 connector: &(dyn PositionsConnector + Send + Sync),
168 symbol: Symbol,
169) -> TestResult {
170 const NAME: &str = "test_get_mark_price";
171 let exchange = connector.exchange_name();
172 let start = Instant::now();
173
174 match connector.get_mark_price(&symbol.to_concat()).await {
175 Ok(mp) => {
176 if mp.mark_price.is_nan()
177 || mp.mark_price.is_infinite()
178 || mp.mark_price <= 0.0
179 {
180 return TestResult::fail(
181 NAME, exchange,
182 start.elapsed().as_millis() as u64,
183 format!("mark_price invalid: {}", mp.mark_price),
184 );
185 }
186 if let Some(idx) = mp.index_price {
188 if idx.is_nan() || idx.is_infinite() || idx <= 0.0 {
189 return TestResult::fail(
190 NAME, exchange,
191 start.elapsed().as_millis() as u64,
192 format!("index_price invalid: {idx}"),
193 );
194 }
195 }
196 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
197 }
198 Err(err) if is_unsupported(&err) => {
199 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
200 format!("get_mark_price unsupported: {err}"))
201 }
202 Err(err) if is_auth_error(&err) => {
203 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
204 format!("auth error: {err}"))
205 }
206 Err(err) => {
207 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
208 format!("get_mark_price failed: {err}"))
209 }
210 }
211}
212
213pub async fn test_get_open_interest(
221 connector: &(dyn PositionsConnector + Send + Sync),
222 symbol: Symbol,
223 account_type: AccountType,
224) -> TestResult {
225 const NAME: &str = "test_get_open_interest";
226 let exchange = connector.exchange_name();
227 let start = Instant::now();
228
229 match connector.get_open_interest(&symbol.to_concat(), account_type).await {
230 Ok(oi) => {
231 if oi.open_interest.is_nan()
232 || oi.open_interest.is_infinite()
233 || oi.open_interest < 0.0
234 {
235 return TestResult::fail(
236 NAME, exchange,
237 start.elapsed().as_millis() as u64,
238 format!("open_interest invalid: {}", oi.open_interest),
239 );
240 }
241 let base_upper = symbol.base.to_uppercase();
243 let is_major = matches!(base_upper.as_str(), "BTC" | "ETH" | "SOL" | "BNB");
244 if is_major && oi.open_interest == 0.0 {
245 return TestResult::fail(
246 NAME, exchange,
247 start.elapsed().as_millis() as u64,
248 format!("open_interest is 0 for major symbol {symbol}"),
249 );
250 }
251 if let Some(v) = oi.open_interest_value {
253 if v.is_nan() || v.is_infinite() || v < 0.0 {
254 return TestResult::fail(
255 NAME, exchange,
256 start.elapsed().as_millis() as u64,
257 format!("open_interest_value invalid: {v}"),
258 );
259 }
260 }
261 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
262 }
263 Err(err) if is_unsupported(&err) => {
264 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
265 format!("get_open_interest unsupported: {err}"))
266 }
267 Err(err) if is_auth_error(&err) => {
268 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
269 format!("auth error: {err}"))
270 }
271 Err(err) => {
272 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
273 format!("get_open_interest failed: {err}"))
274 }
275 }
276}
277
278pub async fn test_get_long_short_ratio(
284 connector: &(dyn PositionsConnector + Send + Sync),
285 symbol: Symbol,
286 account_type: AccountType,
287) -> TestResult {
288 const NAME: &str = "test_get_long_short_ratio";
289 let exchange = connector.exchange_name();
290 let start = Instant::now();
291
292 match connector.get_long_short_ratio(&symbol.to_concat(), account_type).await {
293 Ok(lsr) => {
294 if lsr.long_ratio < 0.0 || lsr.long_ratio.is_nan() {
295 return TestResult::fail(
296 NAME, exchange,
297 start.elapsed().as_millis() as u64,
298 format!("long_ratio invalid: {}", lsr.long_ratio),
299 );
300 }
301 if lsr.short_ratio < 0.0 || lsr.short_ratio.is_nan() {
302 return TestResult::fail(
303 NAME, exchange,
304 start.elapsed().as_millis() as u64,
305 format!("short_ratio invalid: {}", lsr.short_ratio),
306 );
307 }
308 let sum = lsr.long_ratio + lsr.short_ratio;
310 if (sum - 1.0).abs() > 0.01 {
311 return TestResult::fail(
312 NAME, exchange,
313 start.elapsed().as_millis() as u64,
314 format!(
315 "long_ratio + short_ratio = {sum:.4}, expected ~1.0 \
316 (long={}, short={})",
317 lsr.long_ratio, lsr.short_ratio
318 ),
319 );
320 }
321 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
322 }
323 Err(err) if is_unsupported(&err) => {
324 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
325 format!("get_long_short_ratio unsupported: {err}"))
326 }
327 Err(err) if is_auth_error(&err) => {
328 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
329 format!("auth error: {err}"))
330 }
331 Err(err) => {
332 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
333 format!("get_long_short_ratio failed: {err}"))
334 }
335 }
336}