digdigdig3/testing/suites/
trading.rs1use std::time::Instant;
10
11use crate::core::traits::{ExchangeIdentity, MarketData, Trading};
12use crate::core::types::{
13 AccountType, CancelRequest, CancelScope, OrderHistoryFilter, OrderRequest,
14 OrderSide, OrderType, PlaceOrderResponse, Symbol, SymbolInput, TimeInForce, UserTradeFilter,
15};
16
17use super::{is_auth_error, is_unsupported, TestResult};
18
19pub async fn run_all(
30 connector: &(dyn TradingWithMarketData + Send + Sync),
31 symbol: Symbol,
32 account_type: AccountType,
33) -> Vec<TestResult> {
34 let mut results = Vec::new();
35
36 results.push(
37 test_place_cancel_roundtrip(connector, symbol.clone(), account_type).await,
38 );
39 results.push(test_get_open_orders(connector, symbol.clone(), account_type).await);
40 results.push(test_get_order_history(connector, symbol.clone(), account_type).await);
41 results.push(test_get_user_trades(connector, symbol.clone(), account_type).await);
42
43 results
44}
45
46pub trait TradingWithMarketData: Trading + MarketData + ExchangeIdentity {}
56
57impl<T: Trading + MarketData + ExchangeIdentity> TradingWithMarketData for T {}
58
59pub async fn test_place_cancel_roundtrip(
79 connector: &(dyn TradingWithMarketData + Send + Sync),
80 symbol: Symbol,
81 account_type: AccountType,
82) -> TestResult {
83 const NAME: &str = "test_place_cancel_roundtrip";
84 let exchange = connector.exchange_name();
85 let start = Instant::now();
86
87 let raw_sym = symbol.to_concat();
89 let price = match connector.get_price(SymbolInput::Raw(&raw_sym), account_type).await {
90 Ok(p) => p,
91 Err(err) if is_unsupported(&err) => {
92 return TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
93 format!("get_price unsupported: {err}"));
94 }
95 Err(err) if is_auth_error(&err) => {
96 return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
97 format!("auth error fetching price: {err}"));
98 }
99 Err(err) => {
100 return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
101 format!("failed to get price: {err}"));
102 }
103 };
104
105 let far_price = (price * 0.3 * 100.0).round() / 100.0;
106
107 let req = OrderRequest {
109 symbol: symbol.clone(),
110 side: OrderSide::Buy,
111 order_type: OrderType::Limit { price: far_price },
112 quantity: 0.001,
113 time_in_force: TimeInForce::Gtc,
114 account_type,
115 client_order_id: None,
116 reduce_only: false,
117 };
118
119 let place_resp = match connector.place_order(req).await {
120 Ok(r) => r,
121 Err(err) if is_unsupported(&err) => {
122 return TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
123 format!("place_order unsupported: {err}"));
124 }
125 Err(err) if is_auth_error(&err) => {
126 return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
127 format!("auth error placing order: {err}"));
128 }
129 Err(err) => {
130 return TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
131 format!("place_order failed: {err}"));
132 }
133 };
134
135 let order_id = match &place_resp {
137 PlaceOrderResponse::Simple(order) => order.id.clone(),
138 PlaceOrderResponse::Bracket(br) => br.entry_order.id.clone(),
139 PlaceOrderResponse::Oco(oco) => oco.first_order.id.clone(),
140 PlaceOrderResponse::Algo(algo) => algo.algo_id.clone(),
141 };
142
143 match connector.get_order(
145 &symbol.to_concat(),
146 &order_id,
147 account_type,
148 ).await {
149 Ok(_) => {} Err(err) if is_unsupported(&err) => {
151 }
153 Err(err) => {
154 let _ = cancel_single(connector, &symbol, &order_id, account_type).await;
156 return TestResult::error(
157 NAME, exchange,
158 start.elapsed().as_millis() as u64,
159 format!("get_order failed for id={order_id}: {err}"),
160 );
161 }
162 }
163
164 match cancel_single(connector, &symbol, &order_id, account_type).await {
166 Ok(_) => {
167 TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64)
168 }
169 Err(err) if is_unsupported(&err) => {
170 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
171 format!("cancel_order unsupported: {err}"))
172 }
173 Err(err) => {
174 TestResult::error(
175 NAME, exchange,
176 start.elapsed().as_millis() as u64,
177 format!(
178 "cancel failed — MANUAL CLEANUP REQUIRED order_id={order_id}: {err}"
179 ),
180 )
181 }
182 }
183}
184
185async fn cancel_single(
187 connector: &dyn TradingWithMarketData,
188 symbol: &Symbol,
189 order_id: &str,
190 account_type: AccountType,
191) -> Result<(), crate::core::types::ExchangeError> {
192 let cancel_req = CancelRequest {
193 scope: CancelScope::Single {
194 order_id: order_id.to_string(),
195 },
196 symbol: Some(symbol.clone()),
197 account_type,
198 };
199 connector.cancel_order(cancel_req).await.map(|_| ())
200}
201
202pub async fn test_get_open_orders(
211 connector: &(dyn TradingWithMarketData + Send + Sync),
212 symbol: Symbol,
213 account_type: AccountType,
214) -> TestResult {
215 const NAME: &str = "test_get_open_orders";
216 let exchange = connector.exchange_name();
217 let start = Instant::now();
218
219 match connector
220 .get_open_orders(Some(&symbol.to_concat()), account_type)
221 .await
222 {
223 Ok(_orders) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
224 Err(err) if is_unsupported(&err) => {
225 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
226 format!("get_open_orders unsupported: {err}"))
227 }
228 Err(err) if is_auth_error(&err) => {
229 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
230 format!("auth error: {err}"))
231 }
232 Err(err) => {
233 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
234 format!("get_open_orders failed: {err}"))
235 }
236 }
237}
238
239pub async fn test_get_order_history(
248 connector: &(dyn TradingWithMarketData + Send + Sync),
249 symbol: Symbol,
250 account_type: AccountType,
251) -> TestResult {
252 const NAME: &str = "test_get_order_history";
253 let exchange = connector.exchange_name();
254 let start = Instant::now();
255
256 let filter = OrderHistoryFilter {
257 symbol: Some(symbol.clone()),
258 start_time: None,
259 end_time: None,
260 limit: Some(10),
261 status: None,
262 };
263
264 match connector.get_order_history(filter, account_type).await {
265 Ok(_orders) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
266 Err(err) if is_unsupported(&err) => {
267 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
268 format!("get_order_history unsupported: {err}"))
269 }
270 Err(err) if is_auth_error(&err) => {
271 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
272 format!("auth error: {err}"))
273 }
274 Err(err) => {
275 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
276 format!("get_order_history failed: {err}"))
277 }
278 }
279}
280
281pub async fn test_get_user_trades(
290 connector: &(dyn TradingWithMarketData + Send + Sync),
291 symbol: Symbol,
292 account_type: AccountType,
293) -> TestResult {
294 const NAME: &str = "test_get_user_trades";
295 let exchange = connector.exchange_name();
296 let start = Instant::now();
297
298 let filter = UserTradeFilter {
299 symbol: Some(symbol.to_concat()),
300 order_id: None,
301 start_time: None,
302 end_time: None,
303 limit: Some(10),
304 };
305
306 match connector.get_user_trades(filter, account_type).await {
307 Ok(_trades) => TestResult::pass(NAME, exchange, start.elapsed().as_millis() as u64),
308 Err(err) if is_unsupported(&err) => {
309 TestResult::skip(NAME, exchange, start.elapsed().as_millis() as u64,
310 format!("get_user_trades unsupported: {err}"))
311 }
312 Err(err) if is_auth_error(&err) => {
313 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
314 format!("auth error: {err}"))
315 }
316 Err(err) => {
317 TestResult::error(NAME, exchange, start.elapsed().as_millis() as u64,
318 format!("get_user_trades failed: {err}"))
319 }
320 }
321}