#![allow(deprecated)]
#![allow(clippy::disallowed_methods)]
#![allow(deprecated)]
use ccxt_core::types::{Market, OrderSide, OrderStatus, OrderType};
use ccxt_core::{Amount, ExchangeConfig, Price};
use ccxt_exchanges::binance::Binance;
use ccxt_exchanges::binance::parser;
use rust_decimal::prelude::FromStr;
use std::env;
fn get_api_credentials() -> ExchangeConfig {
dotenvy::dotenv().ok();
let api_key = env::var("BINANCE_API_KEY").ok().map(|s| s.into());
let secret = env::var("BINANCE_API_SECRET").ok().map(|s| s.into());
let mut config = ExchangeConfig::default();
config.api_key = api_key;
config.secret = secret;
config
}
#[tokio::test]
async fn test_new_binance_instance() {
let config = ExchangeConfig {
id: "binance".to_string(),
name: "Binance".to_string(),
..Default::default()
};
let exchange = Binance::new(config).unwrap();
assert_eq!(exchange.id(), "binance");
assert_eq!(exchange.name(), "Binance");
}
#[tokio::test]
async fn test_get_urls() {
let config = ExchangeConfig::default();
let exchange = Binance::new(config).unwrap();
let urls = exchange.urls();
assert!(urls.public.contains("api.binance.com"));
assert!(urls.ws.contains("stream.binance.com"));
}
#[tokio::test]
#[ignore]
async fn test_fetch_markets_real() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange.fetch_markets().await;
assert!(
result.is_ok(),
"Failed to fetch markets: {:?}",
result.err()
);
let markets = result.unwrap();
assert!(
markets.len() > 100,
"Expected more than 100 markets, got {}",
markets.len()
);
let btc_usdt = markets.values().find(|m| m.symbol == "BTC/USDT");
assert!(btc_usdt.is_some(), "BTC/USDT market not found");
if let Some(market) = btc_usdt {
assert_eq!(market.base, "BTC");
assert_eq!(market.quote, "USDT");
assert!(market.active);
}
}
#[tokio::test]
#[ignore]
async fn test_fetch_ticker_real() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_ticker("BTC/USDT", ccxt_core::types::TickerParams::default())
.await;
assert!(result.is_ok(), "Failed to fetch ticker: {:?}", result.err());
let ticker = result.unwrap();
assert_eq!(ticker.symbol, "BTC/USDT");
assert!(ticker.last.is_some(), "Last price should be present");
assert!(ticker.bid.is_some(), "Bid price should be present");
assert!(ticker.ask.is_some(), "Ask price should be present");
assert!(ticker.high.is_some(), "High price should be present");
assert!(ticker.low.is_some(), "Low price should be present");
assert!(ticker.base_volume.is_some(), "Volume should be present");
}
#[tokio::test]
#[ignore]
async fn test_fetch_order_book_real() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange.fetch_order_book("BTC/USDT", Some(10)).await;
assert!(
result.is_ok(),
"Failed to fetch order book: {:?}",
result.err()
);
let order_book = result.unwrap();
assert_eq!(order_book.symbol, "BTC/USDT");
assert!(!order_book.bids.is_empty(), "Bids should not be empty");
assert!(!order_book.asks.is_empty(), "Asks should not be empty");
assert!(order_book.bids.len() <= 10, "Should have at most 10 bids");
assert!(order_book.asks.len() <= 10, "Should have at most 10 asks");
if let (Some(best_bid), Some(best_ask)) = (order_book.bids.first(), order_book.asks.first()) {
assert!(
best_bid.price < best_ask.price,
"Best bid ({}) should be less than best ask ({})",
best_bid.price,
best_ask.price
);
}
}
#[tokio::test]
#[ignore]
async fn test_fetch_trades_real() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange.fetch_trades("BTC/USDT", Some(5)).await;
assert!(result.is_ok(), "Failed to fetch trades: {:?}", result.err());
let trades = result.unwrap();
assert!(!trades.is_empty(), "Trades should not be empty");
assert!(trades.len() <= 5, "Should have at most 5 trades");
for trade in &trades {
assert_eq!(trade.symbol, "BTC/USDT");
assert!(
trade.price > Price::from(rust_decimal::Decimal::ZERO),
"Trade price should be positive"
);
assert!(
trade.amount > Amount::from(rust_decimal::Decimal::ZERO),
"Trade amount should be positive"
);
assert!(trade.timestamp > 0, "Trade should have timestamp");
}
}
#[tokio::test]
#[ignore]
async fn test_fetch_ohlcv_real() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_ohlcv("BTC/USDT", "1h", None, Some(5), None)
.await;
assert!(result.is_ok(), "Failed to fetch OHLCV: {:?}", result.err());
let candles = result.unwrap();
assert!(!candles.is_empty(), "Candles should not be empty");
assert!(candles.len() <= 5, "Should have at most 5 candles");
for candle in &candles {
assert!(candle.open > 0.0, "Open price should be positive");
assert!(candle.high >= candle.low, "High should be >= low");
assert!(candle.high >= candle.open, "High should be >= open");
assert!(candle.high >= candle.close, "High should be >= close");
assert!(candle.low <= candle.open, "Low should be <= open");
assert!(candle.low <= candle.close, "Low should be <= close");
assert!(candle.volume >= 0.0, "Volume should be non-negative");
}
}
#[tokio::test]
#[ignore]
async fn test_invalid_symbol() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_ticker("INVALID/SYMBOL", ccxt_core::types::TickerParams::default())
.await;
assert!(result.is_err(), "Should fail for invalid symbol");
}
#[tokio::test]
#[ignore]
async fn test_unsupported_timeframe() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_ohlcv("BTC/USDT", "2h", None, Some(5), None)
.await;
if result.is_err() {
println!(
"Expected error for unsupported timeframe: {:?}",
result.err()
);
}
}
#[tokio::test]
#[ignore = "fetch_time method not yet migrated to new modular structure"]
async fn test_fetch_time() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
}
#[tokio::test]
#[ignore]
async fn test_fetch_tickers() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_tickers(Some(vec!["BTC/USDT".to_string(), "ETH/USDT".to_string()]))
.await;
assert!(
result.is_ok(),
"Failed to fetch tickers: {:?}",
result.err()
);
let tickers = result.unwrap();
assert!(tickers.len() >= 2, "Should have at least 2 tickers");
assert!(tickers.iter().any(|t| t.symbol == "BTC/USDT"));
assert!(tickers.iter().any(|t| t.symbol == "ETH/USDT"));
}
#[tokio::test]
#[ignore]
async fn test_fetch_multiple_symbols() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let symbols = vec!["BTC/USDT", "ETH/USDT", "BNB/USDT"];
for symbol in symbols {
let result = exchange
.fetch_ticker(symbol, ccxt_core::types::TickerParams::default())
.await;
assert!(
result.is_ok(),
"Failed to fetch ticker for {}: {:?}",
symbol,
result.err()
);
let ticker = result.unwrap();
assert_eq!(ticker.symbol, symbol);
assert!(ticker.last.is_some());
}
}
#[tokio::test]
#[ignore]
async fn test_market_validation() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange.fetch_markets().await;
assert!(result.is_ok());
let markets = result.unwrap();
for market in markets.values().take(10) {
assert!(!market.symbol.is_empty(), "Symbol should not be empty");
assert!(!market.base.is_empty(), "Base currency should not be empty");
assert!(
!market.quote.is_empty(),
"Quote currency should not be empty"
);
}
}
#[tokio::test]
#[ignore]
async fn test_ticker_24h_change() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange
.fetch_ticker("BTC/USDT", ccxt_core::types::TickerParams::default())
.await;
assert!(result.is_ok());
let ticker = result.unwrap();
assert!(
ticker.percentage.is_some() || ticker.change.is_some(),
"Should have 24h change data"
);
assert!(ticker.high.is_some(), "Should have 24h high");
assert!(ticker.low.is_some(), "Should have 24h low");
}
#[tokio::test]
#[ignore]
async fn test_fetch_balance() {
let config = get_api_credentials();
if config.api_key.is_none() || config.secret.is_none() {
println!("⚠️ Skip test: API credentials not set");
return;
}
let exchange = Binance::new(config).unwrap();
let result = exchange.fetch_balance(None).await;
assert!(
result.is_ok(),
"Failed to fetch balance: {:?}",
result.err()
);
let balance = result.unwrap();
assert!(!balance.balances.is_empty(), "Should have balance data");
}
#[tokio::test]
#[ignore]
async fn test_fetch_open_orders() {
let config = get_api_credentials();
if config.api_key.is_none() || config.secret.is_none() {
println!("⚠️ Skip test: API credentials not set");
return;
}
let exchange = Binance::new(config).unwrap();
let result = exchange.fetch_open_orders(Some("BTC/USDT")).await;
assert!(
result.is_ok(),
"Failed to fetch open orders: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore]
async fn test_fetch_order() {
let config = get_api_credentials();
if config.api_key.is_none() || config.secret.is_none() {
println!("⚠️ Skip test: API credentials not set");
return;
}
let _exchange = Binance::new(config).unwrap();
println!("⚠️ Requires real order ID for complete testing");
}
#[tokio::test]
#[ignore]
async fn test_fetch_my_trades() {
let config = get_api_credentials();
if config.api_key.is_none() || config.secret.is_none() {
println!("⚠️ Skip test: API credentials not set");
return;
}
let exchange = Binance::new(config).unwrap();
let result = exchange.fetch_my_trades("BTC/USDT", None, None).await;
assert!(
result.is_ok(),
"Failed to fetch my trades: {:?}",
result.err()
);
}
#[tokio::test]
#[ignore]
async fn test_invalid_api_key() {
let config = ExchangeConfig {
api_key: Some("invalid_key".to_string().into()),
secret: Some("invalid_secret".to_string().into()),
..Default::default()
};
let exchange = Binance::new(config).unwrap();
let result = exchange.fetch_balance(None).await;
assert!(result.is_err(), "Should fail with invalid API key");
}
#[tokio::test]
#[ignore]
async fn test_rate_limit_handling() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
for i in 0..5 {
let result = exchange
.fetch_ticker("BTC/USDT", ccxt_core::types::TickerParams::default())
.await;
if result.is_err() {
println!("请求 {} 失败(可能触发速率限制): {:?}", i, result.err());
}
}
}
#[tokio::test]
#[ignore]
async fn test_network_error_retry() {
println!("⚠️ 网络错误重试测试需要mock框架支持");
}
#[tokio::test]
#[ignore]
async fn test_signature_error() {
let config = ExchangeConfig {
api_key: Some("test_key".to_string().into()),
secret: Some("wrong_secret".to_string().into()),
..Default::default()
};
let exchange = Binance::new(config).unwrap();
let result = exchange.fetch_balance(None).await;
assert!(result.is_err(), "Should fail with wrong signature");
}
#[tokio::test]
#[ignore]
async fn test_check_required_credentials_without_keys() {
let exchange = Binance::new(ExchangeConfig::default()).unwrap();
let result = exchange.fetch_balance(None).await;
assert!(result.is_err(), "Should fail without API credentials");
}
#[cfg(test)]
mod parser_tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_market() {
let data = json!({
"symbol": "BTCUSDT",
"status": "TRADING",
"baseAsset": "BTC",
"quoteAsset": "USDT",
"baseAssetPrecision": 8,
"quoteAssetPrecision": 8,
"orderTypes": ["LIMIT", "MARKET"],
"filters": [
{
"filterType": "PRICE_FILTER",
"minPrice": "0.01",
"maxPrice": "1000000.00",
"tickSize": "0.01"
},
{
"filterType": "LOT_SIZE",
"minQty": "0.00001",
"maxQty": "9000.00",
"stepSize": "0.00001"
}
]
});
let result = parser::parse_market(&data);
assert!(result.is_ok());
let market = result.unwrap();
assert_eq!(market.symbol, "BTC/USDT");
assert_eq!(market.base, "BTC");
assert_eq!(market.quote, "USDT");
assert!(market.active);
}
#[test]
fn test_parse_ticker() {
let data = json!({
"symbol": "BTCUSDT",
"lastPrice": "50000.00",
"bidPrice": "49999.00",
"askPrice": "50001.00",
"highPrice": "51000.00",
"lowPrice": "49000.00",
"volume": "1000.5",
"quoteVolume": "50000000.00"
});
let market = Market::new_spot(
"BTCUSDT".to_string(),
"BTC/USDT".to_string(),
"BTC".to_string(),
"USDT".to_string(),
);
let result = parser::parse_ticker(&data, Some(&market));
assert!(result.is_ok());
let ticker = result.unwrap();
assert_eq!(ticker.symbol, "BTC/USDT");
assert_eq!(
ticker.last,
Some(Price::from(rust_decimal::Decimal::new(500000, 1)))
); assert_eq!(
ticker.bid,
Some(Price::from(rust_decimal::Decimal::new(499990, 1)))
); assert_eq!(
ticker.ask,
Some(Price::from(rust_decimal::Decimal::new(500010, 1)))
); }
#[test]
fn test_parse_order() {
let data = json!({
"orderId": 12345,
"symbol": "BTCUSDT",
"price": "50000.00",
"origQty": "0.1",
"executedQty": "0.1",
"cummulativeQuoteQty": "5000.00",
"status": "FILLED",
"type": "LIMIT",
"side": "BUY",
"time": 1234567890000_i64
});
let market = Market::new_spot(
"BTCUSDT".to_string(),
"BTC/USDT".to_string(),
"BTC".to_string(),
"USDT".to_string(),
);
let result = parser::parse_order(&data, Some(&market));
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.id, "12345");
assert_eq!(order.symbol, "BTC/USDT");
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.order_type, OrderType::Limit);
assert_eq!(order.status, OrderStatus::Closed);
}
#[test]
fn test_parse_market_with_filters() {
let data = json!({
"symbol": "ETHUSDT",
"status": "TRADING",
"baseAsset": "ETH",
"quoteAsset": "USDT",
"baseAssetPrecision": 8,
"quoteAssetPrecision": 8,
"filters": [
{
"filterType": "PRICE_FILTER",
"minPrice": "0.01",
"maxPrice": "100000.00",
"tickSize": "0.01"
},
{
"filterType": "LOT_SIZE",
"minQty": "0.0001",
"maxQty": "10000.00",
"stepSize": "0.0001"
},
{
"filterType": "MIN_NOTIONAL",
"minNotional": "10.00"
}
]
});
let result = parser::parse_market(&data);
assert!(result.is_ok());
let market = result.unwrap();
assert_eq!(market.symbol, "ETH/USDT");
let limits = &market.limits;
assert!(limits.amount.is_some() || limits.price.is_some());
}
#[test]
fn test_parse_ticker_edge_cases() {
let data = json!({
"symbol": "BTCUSDT",
"lastPrice": "50000.00",
"volume": "1000.0"
});
let market = Market::new_spot(
"BTCUSDT".to_string(),
"BTC/USDT".to_string(),
"BTC".to_string(),
"USDT".to_string(),
);
let result = parser::parse_ticker(&data, Some(&market));
assert!(result.is_ok());
let ticker = result.unwrap();
assert_eq!(ticker.symbol, "BTC/USDT");
assert_eq!(
ticker.last,
Some(Price::from(rust_decimal::Decimal::new(500000, 1)))
); assert!(ticker.bid.is_none() || ticker.bid.is_some());
}
#[test]
fn test_parse_trade_timestamp() {
let data = json!({
"id": 12345,
"price": "50000.00",
"qty": "0.1",
"time": 1234567890000_i64,
"isBuyerMaker": false
});
let market = Market::new_spot(
"BTCUSDT".to_string(),
"BTC/USDT".to_string(),
"BTC".to_string(),
"USDT".to_string(),
);
let result = parser::parse_trade(&data, Some(&market));
assert!(result.is_ok());
let trade = result.unwrap();
assert_eq!(trade.symbol, "BTC/USDT");
let expected_price = rust_decimal::Decimal::from_str("50000.00").unwrap();
let expected_amount = rust_decimal::Decimal::from_str("0.1").unwrap();
assert!(
(trade.price.as_decimal() - expected_price).abs()
< rust_decimal::Decimal::from_str("0.0000001").unwrap()
);
assert!(
(trade.amount.as_decimal() - expected_amount).abs()
< rust_decimal::Decimal::from_str("0.0000001").unwrap()
);
assert_eq!(trade.timestamp, 1234567890000);
}
#[test]
fn test_parse_order_status() {
let market = Market::new_spot(
"BTCUSDT".to_string(),
"BTC/USDT".to_string(),
"BTC".to_string(),
"USDT".to_string(),
);
let statuses = vec![
("NEW", OrderStatus::Open),
("PARTIALLY_FILLED", OrderStatus::Open),
("FILLED", OrderStatus::Closed),
("CANCELED", OrderStatus::Cancelled),
("REJECTED", OrderStatus::Rejected),
("EXPIRED", OrderStatus::Expired),
];
for (binance_status, expected_status) in statuses {
let data = json!({
"orderId": 12345,
"symbol": "BTCUSDT",
"status": binance_status,
"type": "LIMIT",
"side": "BUY",
"origQty": "0.1",
"price": "50000.0",
"time": 1234567890000_i64
});
let result = parser::parse_order(&data, Some(&market));
assert!(result.is_ok(), "Failed to parse status: {}", binance_status);
let order = result.unwrap();
assert_eq!(order.status, expected_status);
}
}
#[test]
fn test_parse_balance_locked() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
let _data = json!({
"asset": "BTC",
"free": "1.5",
"locked": "0.5"
});
println!("Test balance parsing (requires parse_balance_entry method implementation)");
}
#[test]
fn test_parse_empty_response() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
println!("Test empty response parsing (requires parse_orders method implementation)");
}
#[test]
fn test_parse_malformed_data() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
let data = json!({
"symbol": "INVALID"
});
let result = parser::parse_market(&data);
assert!(result.is_err() || result.is_ok());
}
#[test]
fn test_currency_precision() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
let data = json!({
"symbol": "BTCUSDT",
"status": "TRADING",
"baseAsset": "BTC",
"quoteAsset": "USDT",
"baseAssetPrecision": 8,
"quotePrecision": 2,
"quoteAssetPrecision": 8,
"filters": [
{
"filterType": "PRICE_FILTER",
"tickSize": "0.01"
},
{
"filterType": "LOT_SIZE",
"stepSize": "0.00000001"
}
]
});
let result = parser::parse_market(&data);
assert!(result.is_ok());
let market = result.unwrap();
let precision = &market.precision;
assert!(precision.amount.is_some() || precision.price.is_some());
}
#[test]
fn test_market_limits() {
let _exchange = Binance::new(ExchangeConfig::default()).unwrap();
let data = json!({
"symbol": "BTCUSDT",
"status": "TRADING",
"baseAsset": "BTC",
"quoteAsset": "USDT",
"filters": [
{
"filterType": "PRICE_FILTER",
"minPrice": "0.01",
"maxPrice": "1000000.00"
},
{
"filterType": "LOT_SIZE",
"minQty": "0.00001",
"maxQty": "9000.00"
}
]
});
let result = parser::parse_market(&data);
assert!(result.is_ok());
let market = result.unwrap();
let limits = &market.limits;
assert!(
limits.amount.is_some()
|| limits.price.is_some()
|| limits.cost.is_some()
|| limits.leverage.is_some()
);
}
#[test]
fn test_fee_calculation() {
let _maker_fee = 0.001; let taker_fee = 0.001;
let trade_value = 1000.0;
let fee = trade_value * taker_fee;
assert_eq!(fee, 1.0);
}
#[test]
fn test_symbol_normalization() {
let binance_symbol = "BTCUSDT";
let normalized = format!("{}/{}", &binance_symbol[..3], &binance_symbol[3..]);
assert_eq!(normalized, "BTC/USDT");
}
#[test]
fn test_timeframe_conversion() {
let timeframes = vec![("1m", 60), ("5m", 300), ("1h", 3600), ("1d", 86400)];
for (tf, expected_seconds) in timeframes {
let seconds = match tf {
"1m" => 60,
"5m" => 300,
"1h" => 3600,
"1d" => 86400,
_ => 0,
};
assert_eq!(seconds, expected_seconds);
}
}
}