use mexc_rs::spot::{
MexcSpotApiClientWithAuthentication, MexcSpotApiEndpoint,
v3::{
ApiError,
account_information::AccountInformationEndpoint,
cancel_order::{CancelOrderEndpoint, CancelOrderParams},
enums::{
OrderSide as MexcOrderSide, OrderStatus as MexcOrderStatus, OrderType as MexcOrderType,
},
order::{OrderEndpoint, OrderParams},
ping::PingEndpoint,
query_order::{QueryOrderEndpoint, QueryOrderParams},
},
};
use std::fmt;
use tracing::{debug, trace};
use super::{Balance, Crypto, Market, MarketFactory, Order};
use crate::{
clock::TrueClock,
core::{CoreError, CoreResult},
generics::order::{OrderSide, OrderStatus, OrderType, SpecificOrderDetails, UpdateOrderOutput},
};
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone, Default)]
pub struct MexcOrderDetails {
pub id: Option<String>,
}
impl MexcOrderDetails {
pub fn set_id(&mut self, id: String) {
self.id = Some(id);
}
}
impl fmt::Display for MexcOrderDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.id)
}
}
impl SpecificOrderDetails for MexcOrderDetails {
const NAME: &'static str = "Mexc";
fn try_update(&mut self, item: Self) -> CoreResult<()> {
match item.id {
None => tracing::warn!("Update with no id"),
Some(id) => match self.id.clone() {
None => self.set_id(id),
Some(old_id) => {
if old_id != id {
return Err(CoreError::ApiError(format!(
"Id changed from {old_id} to {id}"
)));
}
}
},
};
Ok(())
}
}
pub struct MexcMarketFactory;
impl MarketFactory for MexcMarketFactory {
type _Market = MexcMarket;
fn with_clock(self, _: <Self::_Market as Market>::_Clock) -> Self {
self
}
async fn build(self) -> CoreResult<Self::_Market> {
Ok(MexcMarket::new().await)
}
}
pub struct MexcMarket {
client_auth: MexcSpotApiClientWithAuthentication,
}
impl MexcMarket {
pub async fn new() -> Self {
let api_key = std::env::var("MEXC_API_KEY").expect("MEXC_API_KEY not set");
let secret_key = std::env::var("MEXC_SECRET_KEY").expect("MEXC_SECRET_KEY not set");
if std::env::var("SERIOUS").is_ok_and(|v| v == "true") {
tracing::info!("Careful, this is not an exercise, and you're about to use real money.");
} else {
tracing::info!("SERIOUS isn't `true`, so this is a dry run. Your money is safe.");
}
let client_auth = MexcSpotApiClientWithAuthentication::new(
MexcSpotApiEndpoint::Base,
api_key,
secret_key,
);
client_auth.ping().await.expect("Connexion impossible");
trace!("Mymexc initialised");
MexcMarket { client_auth }
}
}
impl fmt::Debug for MexcMarket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Mexc")
.field("client", &"local client")
.field("client_auth", &"local client auth")
.finish()
}
}
impl Market for MexcMarket {
type _Clock = TrueClock;
type _OrderDetails = MexcOrderDetails;
fn name(&self) -> String {
"Mexc".to_string()
}
async fn get_balance(&self, crypto: Crypto) -> CoreResult<Balance> {
let account_info = self.client_auth.account_information().await?;
let mexc_balance = account_info
.balances
.iter()
.find(|balance| balance.asset == crypto.to_string())
.expect("Balance not found");
Ok(Balance {
free: mexc_balance.free,
locked: mexc_balance.locked,
})
}
async fn send_order(
&self,
order: &Order<MexcOrderDetails>,
) -> CoreResult<UpdateOrderOutput<MexcOrderDetails>> {
if std::env::var("SERIOUS").unwrap_or("false".to_string()) != "true" {
Err(CoreError::SafetyError)
} else {
let params = OrderParams {
symbol: &order.mexc_symbol(),
side: (*order.side()).into(),
order_type: (*order.order_type()).into(),
quantity: *order.qty_asset(),
price: *order.price(),
quote_order_quantity: *order.qty_quote(),
new_client_order_id: None,
};
debug!("Order to Mexc");
trace!("params: {:?}", params);
let ret = self.client_auth.order(params).await?;
Ok(UpdateOrderOutput {
status: *order.status(),
executed_qty_asset: *order.executed_qty_asset(),
executed_qty_quote: *order.executed_qty_quote(),
details: MexcOrderDetails {
id: Some(ret.order_id),
},
})
}
}
async fn cancel_order(
&self,
order: &Order<MexcOrderDetails>,
) -> CoreResult<UpdateOrderOutput<MexcOrderDetails>> {
let ret = self
.client_auth
.cancel_order(CancelOrderParams {
symbol: &order.mexc_symbol(),
order_id: Some(&order.details().id.clone().expect("Order ID should be set")),
new_client_order_id: None,
original_client_order_id: None,
})
.await?;
Ok(UpdateOrderOutput {
status: ret.status.into(),
executed_qty_asset: ret.executed_quantity,
executed_qty_quote: ret.cummulative_quote_quantity,
details: MexcOrderDetails {
id: Some(ret.order_id),
},
})
}
async fn update_order(
&self,
order: &Order<MexcOrderDetails>,
) -> CoreResult<UpdateOrderOutput<MexcOrderDetails>> {
let ret = self
.client_auth
.query_order(QueryOrderParams {
symbol: &order.mexc_symbol(),
order_id: Some(&order.details().id.clone().expect("Order ID should be set")),
original_client_order_id: None,
})
.await?;
Ok(UpdateOrderOutput {
status: ret.status.into(),
executed_qty_asset: ret.executed_quantity,
executed_qty_quote: ret.cummulative_quote_quantity,
details: MexcOrderDetails {
id: Some(ret.order_id),
},
})
}
}
fn mexc_symbol(asset: &Crypto, quote: &Crypto) -> String {
format!("{asset}{quote}")
}
trait MexcSymbol {
fn mexc_symbol(&self) -> String;
}
impl MexcSymbol for Order<MexcOrderDetails> {
fn mexc_symbol(&self) -> String {
mexc_symbol(self.asset(), self.quote())
}
}
impl From<OrderStatus> for MexcOrderStatus {
fn from(status: OrderStatus) -> Self {
match status {
OrderStatus::Open => MexcOrderStatus::New,
OrderStatus::Filled => MexcOrderStatus::Filled,
OrderStatus::Canceled => MexcOrderStatus::Canceled,
OrderStatus::PartiallyFilled => MexcOrderStatus::PartiallyFilled,
OrderStatus::PartiallyCanceled => MexcOrderStatus::PartiallyCanceled,
OrderStatus::Tested => MexcOrderStatus::Canceled,
OrderStatus::Failed => MexcOrderStatus::Canceled,
}
}
}
impl From<MexcOrderStatus> for OrderStatus {
fn from(status: MexcOrderStatus) -> Self {
match status {
MexcOrderStatus::New => OrderStatus::Open,
MexcOrderStatus::Filled => OrderStatus::Filled,
MexcOrderStatus::Canceled => OrderStatus::Canceled,
MexcOrderStatus::PartiallyFilled => OrderStatus::PartiallyFilled,
MexcOrderStatus::PartiallyCanceled => OrderStatus::PartiallyCanceled,
}
}
}
impl From<OrderSide> for MexcOrderSide {
fn from(side: OrderSide) -> Self {
match side {
OrderSide::Buy => MexcOrderSide::Buy,
OrderSide::Sell => MexcOrderSide::Sell,
}
}
}
impl From<OrderType> for MexcOrderType {
fn from(order_type: OrderType) -> Self {
match order_type {
OrderType::Limit => MexcOrderType::Limit,
OrderType::Market => MexcOrderType::Market,
OrderType::LimitMaker => MexcOrderType::LimitMaker,
OrderType::ImmediateOrCancel => MexcOrderType::ImmediateOrCancel,
OrderType::FillOrKill => MexcOrderType::FillOrKill,
}
}
}
impl From<ApiError> for CoreError {
fn from(value: ApiError) -> Self {
CoreError::ApiError(value.to_string())
}
}