use std::time::Instant;
use crate::core::traits::{ExchangeIdentity, MarketData, Trading};
use crate::core::types::{
AccountType, CancelRequest, CancelScope, OrderHistoryFilter, OrderRequest,
OrderSide, OrderType, PlaceOrderResponse, Symbol, SymbolInput, TimeInForce, UserTradeFilter,
};
use super::{is_auth_error, is_unsupported, TestResult};
pub async fn run_all(
connector: &(dyn TradingWithMarketData + Send + Sync),
symbol: Symbol,
account_type: AccountType,
) -> Vec<TestResult> {
let mut results = Vec::new();
results.push(
test_place_cancel_roundtrip(connector, symbol.clone(), account_type).await,
);
results.push(test_get_open_orders(connector, symbol.clone(), account_type).await);
results.push(test_get_order_history(connector, symbol.clone(), account_type).await);
results.push(test_get_user_trades(connector, symbol.clone(), account_type).await);
results
}
pub trait TradingWithMarketData: Trading + MarketData + ExchangeIdentity {}
impl<T: Trading + MarketData + ExchangeIdentity> TradingWithMarketData for T {}
pub async fn test_place_cancel_roundtrip(
connector: &(dyn TradingWithMarketData + Send + Sync),
symbol: Symbol,
account_type: AccountType,
) -> TestResult {
const NAME: &str = "test_place_cancel_roundtrip";
let exchange = connector.exchange_name();
let start = Instant::now();
let raw_sym = symbol.to_concat();
let price = match connector.get_price(SymbolInput::Raw(&raw_sym), account_type).await {
Ok(p) => p,
Err(err) if is_unsupported(&err) => {
return TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_price unsupported: {err}"));
}
Err(err) if is_auth_error(&err) => {
return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("auth error fetching price: {err}"));
}
Err(err) => {
return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("failed to get price: {err}"));
}
};
let far_price = (price * 0.3 * 100.0).round() / 100.0;
let req = OrderRequest {
symbol: symbol.clone(),
side: OrderSide::Buy,
order_type: OrderType::Limit { price: far_price },
quantity: 0.001,
time_in_force: TimeInForce::Gtc,
account_type,
client_order_id: None,
reduce_only: false,
};
let place_resp = match connector.place_order(req).await {
Ok(r) => r,
Err(err) if is_unsupported(&err) => {
return TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("place_order unsupported: {err}"));
}
Err(err) if is_auth_error(&err) => {
return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("auth error placing order: {err}"));
}
Err(err) => {
return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("place_order failed: {err}"));
}
};
let order_id = match &place_resp {
PlaceOrderResponse::Simple(order) => order.id.clone(),
PlaceOrderResponse::Bracket(br) => br.entry_order.id.clone(),
PlaceOrderResponse::Oco(oco) => oco.first_order.id.clone(),
PlaceOrderResponse::Algo(algo) => algo.algo_id.clone(),
};
match connector.get_order(
&symbol.to_concat(),
&order_id,
account_type,
).await {
Ok(_) => {} Err(err) if is_unsupported(&err) => {
}
Err(err) => {
let _ = cancel_single(connector, &symbol, &order_id, account_type).await;
return TestResult::error(
NAME, exchange,
start.elapsed().as_millis() as u64,
format!("get_order failed for id={order_id}: {err}"),
);
}
}
match cancel_single(connector, &symbol, &order_id, account_type).await {
Ok(_) => {
TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
}
Err(err) if is_unsupported(&err) => {
TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("cancel_order unsupported: {err}"))
}
Err(err) => {
TestResult::error(
NAME, exchange,
start.elapsed().as_millis() as u64,
format!(
"cancel failed — MANUAL CLEANUP REQUIRED order_id={order_id}: {err}"
),
)
}
}
}
async fn cancel_single(
connector: &dyn TradingWithMarketData,
symbol: &Symbol,
order_id: &str,
account_type: AccountType,
) -> Result<(), crate::core::types::ExchangeError> {
let cancel_req = CancelRequest {
scope: CancelScope::Single {
order_id: order_id.to_string(),
},
symbol: Some(symbol.clone()),
account_type,
};
connector.cancel_order(cancel_req).await.map(|_| ())
}
pub async fn test_get_open_orders(
connector: &(dyn TradingWithMarketData + Send + Sync),
symbol: Symbol,
account_type: AccountType,
) -> TestResult {
const NAME: &str = "test_get_open_orders";
let exchange = connector.exchange_name();
let start = Instant::now();
match connector
.get_open_orders(Some(&symbol.to_concat()), account_type)
.await
{
Ok(_orders) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
Err(err) if is_unsupported(&err) => {
TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_open_orders unsupported: {err}"))
}
Err(err) if is_auth_error(&err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("auth error: {err}"))
}
Err(err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_open_orders failed: {err}"))
}
}
}
pub async fn test_get_order_history(
connector: &(dyn TradingWithMarketData + Send + Sync),
symbol: Symbol,
account_type: AccountType,
) -> TestResult {
const NAME: &str = "test_get_order_history";
let exchange = connector.exchange_name();
let start = Instant::now();
let filter = OrderHistoryFilter {
symbol: Some(symbol.clone()),
start_time: None,
end_time: None,
limit: Some(10),
status: None,
};
match connector.get_order_history(filter, account_type).await {
Ok(_orders) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
Err(err) if is_unsupported(&err) => {
TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_order_history unsupported: {err}"))
}
Err(err) if is_auth_error(&err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("auth error: {err}"))
}
Err(err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_order_history failed: {err}"))
}
}
}
pub async fn test_get_user_trades(
connector: &(dyn TradingWithMarketData + Send + Sync),
symbol: Symbol,
account_type: AccountType,
) -> TestResult {
const NAME: &str = "test_get_user_trades";
let exchange = connector.exchange_name();
let start = Instant::now();
let filter = UserTradeFilter {
symbol: Some(symbol.to_concat()),
order_id: None,
start_time: None,
end_time: None,
limit: Some(10),
};
match connector.get_user_trades(filter, account_type).await {
Ok(_trades) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
Err(err) if is_unsupported(&err) => {
TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_user_trades unsupported: {err}"))
}
Err(err) if is_auth_error(&err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("auth error: {err}"))
}
Err(err) => {
TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
format!("get_user_trades failed: {err}"))
}
}
}