pub trait Trading: ExchangeIdentity {
// Required methods
fn place_order<'life0, 'async_trait>(
&'life0 self,
req: OrderRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<PlaceOrderResponse>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn cancel_order<'life0, 'async_trait>(
&'life0 self,
req: CancelRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn get_order<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
symbol: &'life1 str,
order_id: &'life2 str,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait;
fn get_open_orders<'life0, 'life1, 'async_trait>(
&'life0 self,
symbol: Option<&'life1 str>,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn get_order_history<'life0, 'async_trait>(
&'life0 self,
filter: OrderHistoryFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
// Provided methods
fn get_user_trades<'life0, 'async_trait>(
&'life0 self,
filter: UserTradeFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<UserTrade>>> + Send + 'async_trait>>
where Self: Sync + 'async_trait,
'life0: 'async_trait { ... }
fn trading_capabilities(
&self,
account_type: AccountType,
) -> TradingCapabilities { ... }
}Expand description
Core trading — 24/24 exchanges.
All order types go through place_order via OrderType enum.
Connectors match the variants they support; unmatched variants
return ExchangeError::UnsupportedOperation.
§Strict Non-Composition Rule
Connectors MUST NOT simulate unsupported variants by composing base methods.
- A connector without native batch cancel MUST NOT loop
cancel_order. - A connector without native Bracket MUST NOT submit 3 separate orders.
If the exchange has no endpoint for it, return
UnsupportedOperation.
Required Methods§
Sourcefn place_order<'life0, 'async_trait>(
&'life0 self,
req: OrderRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<PlaceOrderResponse>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn place_order<'life0, 'async_trait>(
&'life0 self,
req: OrderRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<PlaceOrderResponse>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Place an order of any type.
Connectors inspect req.order_type and match the variants they support.
Returns PlaceOrderResponse::Simple for scalar orders, or the
appropriate composite variant for Bracket/OCO/Algo orders.
Sourcefn cancel_order<'life0, 'async_trait>(
&'life0 self,
req: CancelRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn cancel_order<'life0, 'async_trait>(
&'life0 self,
req: CancelRequest,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Cancel one order, a batch, all orders, or all orders for a symbol.
The scope is determined by req.scope (CancelScope enum).
Connectors that only support single-cancel MUST return
UnsupportedOperation for Batch/All/BySymbol scopes — never loop.
Sourcefn get_order<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
symbol: &'life1 str,
order_id: &'life2 str,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn get_order<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
symbol: &'life1 str,
order_id: &'life2 str,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Order>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Get the current state of a single order by ID.
symbol is required by most exchanges; provide it when available.
Sourcefn get_open_orders<'life0, 'life1, 'async_trait>(
&'life0 self,
symbol: Option<&'life1 str>,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_open_orders<'life0, 'life1, 'async_trait>(
&'life0 self,
symbol: Option<&'life1 str>,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Get all currently open orders, optionally filtered to a single symbol.
symbol = None fetches open orders across all symbols.
Not all exchanges support symbol-less open order queries — those that
don’t MUST return UnsupportedOperation for None, not an empty vec.
Sourcefn get_order_history<'life0, 'async_trait>(
&'life0 self,
filter: OrderHistoryFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_order_history<'life0, 'async_trait>(
&'life0 self,
filter: OrderHistoryFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<Order>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Get closed / filled / cancelled order history with filtering.
Provided Methods§
Sourcefn get_user_trades<'life0, 'async_trait>(
&'life0 self,
filter: UserTradeFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<UserTrade>>> + Send + 'async_trait>>where
Self: Sync + 'async_trait,
'life0: 'async_trait,
fn get_user_trades<'life0, 'async_trait>(
&'life0 self,
filter: UserTradeFilter,
account_type: AccountType,
) -> Pin<Box<dyn Future<Output = ExchangeResult<Vec<UserTrade>>> + Send + 'async_trait>>where
Self: Sync + 'async_trait,
'life0: 'async_trait,
Get the user’s own trade fills (executions).
Returns individual fill records for completed orders, optionally filtered by symbol, order ID, or time range.
Default implementation returns UnsupportedOperation — connectors
that expose a native fills/trades endpoint should override this.
~20/24: all major CEX exchanges. DEX connectors without native trade records return
UnsupportedOperation.
Examples found in repository?
1118async fn test_trading(id: ExchangeId) -> TradingRow {
1119 let harness = TestHarness::new();
1120 let conn = match harness.create_authenticated(id).await {
1121 None => {
1122 // Also check direct ENV vars as fallback
1123 match trading::load_credentials(id) {
1124 None => {
1125 return TradingRow {
1126 exchange: format!("{:?}", id),
1127 balance: MethodResult::Skipped,
1128 account_info: MethodResult::Skipped,
1129 open_orders: MethodResult::Skipped,
1130 user_trades: MethodResult::Skipped,
1131 positions: MethodResult::Skipped,
1132 fees: MethodResult::Skipped,
1133 issues: vec!["no_credentials_in_env".into()],
1134 };
1135 }
1136 Some(_) => {
1137 // Has ENV creds but TestHarness didn't pick them up (.env file missing)
1138 return TradingRow {
1139 exchange: format!("{:?}", id),
1140 balance: MethodResult::Skipped,
1141 account_info: MethodResult::Skipped,
1142 open_orders: MethodResult::Skipped,
1143 user_trades: MethodResult::Skipped,
1144 positions: MethodResult::Skipped,
1145 fees: MethodResult::Skipped,
1146 issues: vec!["creds_in_env_but_not_dotenv".into()],
1147 };
1148 }
1149 }
1150 }
1151 Some(Err(e)) => {
1152 let msg = truncate(&e.to_string(), 70);
1153 return TradingRow {
1154 exchange: format!("{:?}", id),
1155 balance: MethodResult::Err(format!("auth_connect_fail: {}", msg)),
1156 account_info: MethodResult::Skipped,
1157 open_orders: MethodResult::Skipped,
1158 user_trades: MethodResult::Skipped,
1159 positions: MethodResult::Skipped,
1160 fees: MethodResult::Skipped,
1161 issues: vec![format!("auth_fail: {}", msg)],
1162 };
1163 }
1164 Some(Ok(c)) => c,
1165 };
1166
1167 let (_, raw_str, account_type) = raw_symbol_for(id);
1168 let futures_at = if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
1169 account_type
1170 } else {
1171 AccountType::FuturesCross
1172 };
1173
1174 // balance
1175 let balance = {
1176 let conn = conn.clone();
1177 match timeout(Duration::from_secs(10),
1178 conn.get_balance(BalanceQuery { asset: None, account_type })).await {
1179 Ok(Ok(bs)) => MethodResult::Ok(format!("assets={}", bs.len())),
1180 Ok(Err(e)) => {
1181 let msg = e.to_string();
1182 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1183 else { MethodResult::Err(truncate(&msg, 60)) }
1184 }
1185 Err(_) => MethodResult::Timeout,
1186 }
1187 };
1188
1189 // account_info
1190 let account_info = {
1191 let conn = conn.clone();
1192 match timeout(Duration::from_secs(10),
1193 conn.get_account_info(account_type)).await {
1194 Ok(Ok(_)) => MethodResult::Ok("ok".into()),
1195 Ok(Err(e)) => {
1196 let msg = e.to_string();
1197 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1198 else { MethodResult::Err(truncate(&msg, 60)) }
1199 }
1200 Err(_) => MethodResult::Timeout,
1201 }
1202 };
1203
1204 // open_orders
1205 let open_orders = {
1206 let conn = conn.clone();
1207 let s = raw_str.clone();
1208 match timeout(Duration::from_secs(10),
1209 conn.get_open_orders(Some(&s), account_type)).await {
1210 Ok(Ok(os)) => MethodResult::Ok(format!("count={}", os.len())),
1211 Ok(Err(e)) => {
1212 let msg = e.to_string();
1213 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1214 else { MethodResult::Err(truncate(&msg, 60)) }
1215 }
1216 Err(_) => MethodResult::Timeout,
1217 }
1218 };
1219
1220 // user_trades
1221 let user_trades = {
1222 let conn = conn.clone();
1223 let s = raw_str.clone();
1224 match timeout(Duration::from_secs(10),
1225 conn.get_user_trades(
1226 UserTradeFilter { symbol: Some(s), order_id: None, start_time: None, end_time: None, limit: Some(5) },
1227 account_type)).await {
1228 Ok(Ok(ts)) => MethodResult::Ok(format!("count={}", ts.len())),
1229 Ok(Err(e)) => {
1230 let msg = e.to_string();
1231 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1232 else { MethodResult::Err(truncate(&msg, 60)) }
1233 }
1234 Err(_) => MethodResult::Timeout,
1235 }
1236 };
1237
1238 // positions (futures)
1239 let positions = if !is_futures(id, account_type) {
1240 MethodResult::Skipped
1241 } else {
1242 let conn = conn.clone();
1243 match timeout(Duration::from_secs(10),
1244 conn.get_positions(PositionQuery { symbol: None, account_type: futures_at })).await {
1245 Ok(Ok(ps)) => MethodResult::Ok(format!("count={}", ps.len())),
1246 Ok(Err(e)) => {
1247 let msg = e.to_string();
1248 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1249 else { MethodResult::Err(truncate(&msg, 60)) }
1250 }
1251 Err(_) => MethodResult::Timeout,
1252 }
1253 };
1254
1255 // fees
1256 let fees = {
1257 let conn = conn.clone();
1258 let s = raw_str.clone();
1259 match timeout(Duration::from_secs(10), conn.get_fees(Some(&s))).await {
1260 Ok(Ok(f)) => MethodResult::Ok(format!("maker={:.6} taker={:.6}", f.maker_rate, f.taker_rate)),
1261 Ok(Err(e)) => {
1262 let msg = e.to_string();
1263 if msg.contains("UnsupportedOperation") || msg.contains("Not supported:") { MethodResult::Unsupported(truncate(&msg, 50)) }
1264 else { MethodResult::Err(truncate(&msg, 60)) }
1265 }
1266 Err(_) => MethodResult::Timeout,
1267 }
1268 };
1269
1270 let mut issues: Vec<String> = Vec::new();
1271 for (name, result) in [
1272 ("balance", &balance), ("account_info", &account_info),
1273 ("open_orders", &open_orders), ("user_trades", &user_trades),
1274 ("positions", &positions), ("fees", &fees),
1275 ] {
1276 if result.is_issue() {
1277 if let Some(d) = result.detail() {
1278 issues.push(format!("{}: {}", name, d));
1279 } else {
1280 issues.push(format!("{}: {:?}", name, result));
1281 }
1282 }
1283 }
1284
1285 TradingRow {
1286 exchange: format!("{:?}", id),
1287 balance, account_info, open_orders, user_trades, positions, fees,
1288 issues,
1289 }
1290}Sourcefn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities
fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities
Returns the connector’s trading capabilities.
The default implementation returns permissive defaults. Connectors with specific limitations should override this method.