use crate::types::*;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderInput {
pub instrument: InstrumentId,
pub market_index: MarketIndex,
pub client_id: ClientOrderId,
pub side: OrderSide,
pub price: Price,
pub qty: Qty,
pub tif: TimeInForce,
pub post_only: bool,
pub reduce_only: bool,
}
#[derive(Debug, Error, Clone)]
pub enum ExchangeError {
#[error("HTTP error: {0}")]
Http(String),
#[error("Network error: {0}")]
Network(String),
#[error("Rate limited (429)")]
RateLimited,
#[error("Would exceed user action limit (needed {needed}, retry after {retry_after_ms}ms)")]
WouldExceedUserActionLimit {
retry_after_ms: u64,
needed: u32,
},
#[error("Exchange unavailable (502/503)")]
Unavailable,
#[error("Request timeout")]
Timeout,
#[error("Order rejected: {0}")]
Rejected(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Signing error: {0}")]
Signing(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
impl ExchangeError {
pub fn is_transient(&self) -> bool {
matches!(
self,
Self::RateLimited
| Self::WouldExceedUserActionLimit { .. }
| Self::Unavailable
| Self::Timeout
| Self::Network(_)
)
}
pub fn is_502(&self) -> bool {
matches!(self, Self::Unavailable)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlaceOrderResult {
Accepted {
exchange_order_id: Option<ExchangeOrderId>,
filled_qty: Option<Qty>,
avg_fill_px: Option<Price>,
},
Rejected {
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fill {
pub trade_id: TradeId,
pub client_id: Option<ClientOrderId>,
pub exchange_order_id: Option<ExchangeOrderId>,
pub instrument: InstrumentId,
pub side: OrderSide,
pub price: Price,
pub qty: Qty,
pub fee: Fee,
pub ts: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountBalance {
pub asset: AssetId,
pub total: Decimal,
pub available: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangePosition {
pub instrument: InstrumentId,
pub qty: Decimal, pub avg_entry_px: Option<Price>,
pub unrealized_pnl: Option<Decimal>,
}
#[async_trait::async_trait]
pub trait Exchange: Send + Sync + 'static {
fn exchange_id(&self) -> &ExchangeId;
fn environment(&self) -> Environment;
fn instance(&self) -> ExchangeInstance {
ExchangeInstance::new(self.exchange_id().clone(), self.environment())
}
async fn init(&self) -> Result<(), ExchangeError> {
Ok(()) }
async fn place_orders(
&self,
orders: &[OrderInput],
) -> Result<Vec<PlaceOrderResult>, ExchangeError>;
async fn cancel_order(
&self,
instrument: &InstrumentId,
market_index: &MarketIndex,
client_id: &ClientOrderId,
exchange_order_id: Option<&ExchangeOrderId>,
) -> Result<(), ExchangeError>;
async fn cancel_all_orders(
&self,
instrument: &InstrumentId,
market_index: &MarketIndex,
) -> Result<u32, ExchangeError>;
async fn poll_user_fills(&self, cursor: Option<&str>) -> Result<Vec<Fill>, ExchangeError>;
async fn poll_quotes(
&self,
instruments: &[InstrumentId],
) -> Result<Vec<crate::types::Quote>, ExchangeError>;
async fn poll_account_state(&self) -> Result<AccountState, ExchangeError>;
}