use mexc_rs::spot::{
v3::{
account_information::AccountInformationEndpoint,
cancel_order::{CancelOrderEndpoint, CancelOrderOutput, CancelOrderParams},
enums::KlineInterval as MexcKlineInterval,
klines::{Kline as MexcKline, KlinesEndpoint as _, KlinesParams as MexcKlinesParams},
order::{OrderEndpoint, OrderParams},
ping::PingEndpoint,
query_order::{QueryOrderEndpoint, QueryOrderOutput, QueryOrderParams},
ApiError,
},
MexcSpotApiClient, MexcSpotApiClientWithAuthentication, MexcSpotApiEndpoint,
};
use std::fmt;
use tracing::{debug, trace};
use super::{Balance, Crypto, Kline, KlinesParams, Market, MarketFactory, Order};
use crate::{
clock::TrueClock,
core::{CoreError, CoreResult},
data::{KlineInterval, SpecificOrderDetails},
};
pub const KLINES_LIMIT: i32 = 500;
#[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";
}
pub struct MexcFactory;
impl MarketFactory for MexcFactory {
type _Market = Mexc;
fn with_clock(self, _: <Self::_Market as Market>::_Clock) -> Self {
self
}
async fn build(self) -> CoreResult<Self::_Market> {
Ok(Mexc::new().await)
}
}
pub struct Mexc {
client_auth: MexcSpotApiClientWithAuthentication,
client: MexcSpotApiClient,
}
impl Mexc {
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 = MexcSpotApiClient::new(MexcSpotApiEndpoint::Base);
client.ping().await.expect("API injoignable");
let client_auth = MexcSpotApiClientWithAuthentication::new(
MexcSpotApiEndpoint::Base,
api_key,
secret_key,
);
client_auth.ping().await.expect("Connexion impossible");
trace!("Mymexc initialised");
Mexc {
client_auth,
client,
}
}
}
impl fmt::Debug for Mexc {
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 Mexc {
type _Clock = TrueClock;
type _OrderDetails = MexcOrderDetails;
fn source_name() -> String {
"Mexc".to_string()
}
async fn klines(&self, params: KlinesParams) -> CoreResult<Vec<Kline>> {
let mexc_params = MexcKlinesParams {
symbol: &mexc_symbol(¶ms.asset, ¶ms.quote),
interval: params.interval.into(),
start_time: Some(params.start_time),
end_time: Some(params.end_time),
limit: None,
};
let klines: Vec<_> = self
.client
.klines(mexc_params)
.await?
.klines
.into_iter()
.map(Kline::from)
.collect();
trace!("Fetched {} klines from Mexc API", klines.len());
Ok(klines)
}
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: &mut Order<MexcOrderDetails>) -> CoreResult<()> {
if std::env::var("SERIOUS").unwrap_or("false".to_string()) != "true" {
Err(CoreError::SafetyError)
} else {
let (quantity, quote_order_quantity) = order.quantity().get_amounts();
let params = OrderParams {
symbol: &order.mexc_symbol(),
side: *order.side(),
order_type: *order.order_type(),
quantity,
price: *order.price(),
quote_order_quantity,
new_client_order_id: None,
};
debug!("Order to Mexc, [{:#?}]", params);
let ret = self.client_auth.order(params).await?;
order.details_mut().set_id(ret.order_id);
Ok(())
}
}
async fn cancel_order(&self, order: &Order<MexcOrderDetails>) -> CoreResult<CancelOrderOutput> {
Ok(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?)
}
async fn update_order(&self, order: &Order<MexcOrderDetails>) -> CoreResult<QueryOrderOutput> {
Ok(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?)
}
fn klines_limit(&self) -> i32 {
KLINES_LIMIT
}
}
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<ApiError> for CoreError {
fn from(value: ApiError) -> Self {
CoreError::ApiError(value.to_string())
}
}
impl From<MexcKline> for Kline {
fn from(kline: MexcKline) -> Self {
Kline {
open_time: kline.open_time,
close_time: kline.close_time,
open: kline.open,
high: kline.high,
low: kline.low,
close: kline.close,
volume: kline.volume,
quote_asset_volume: kline.quote_asset_volume,
}
}
}
impl From<KlineInterval> for MexcKlineInterval {
fn from(value: KlineInterval) -> Self {
match value {
KlineInterval::OneMinute => MexcKlineInterval::OneMinute,
KlineInterval::FiveMinutes => MexcKlineInterval::FiveMinutes,
KlineInterval::FifteenMinutes => MexcKlineInterval::FifteenMinutes,
KlineInterval::ThirtyMinutes => MexcKlineInterval::ThirtyMinutes,
KlineInterval::OneHour => MexcKlineInterval::OneHour,
KlineInterval::FourHours => MexcKlineInterval::FourHours,
KlineInterval::OneDay => MexcKlineInterval::OneDay,
KlineInterval::OneWeek => MexcKlineInterval::OneWeek,
KlineInterval::OneMonth => MexcKlineInterval::OneMonth,
}
}
}