use std::fmt;
use anyhow::Error;
use chrono::{DateTime, Datelike, Duration, DurationRound, TimeDelta, Utc, Weekday};
use mexc_rs::spot::{
v3::{
cancel_order::CancelOrderOutput, enums::KlineInterval, klines::Kline, ping::PingEndpoint,
query_order::QueryOrderOutput,
},
MexcSpotApiClient, MexcSpotApiEndpoint,
};
use rust_decimal::{prelude::FromPrimitive, Decimal};
use surrealdb::{
engine::local::{Db, RocksDb},
Surreal,
};
use super::super::{super::clock::Clock, Balance};
use super::{
mexc_enums, Amount, Crypto, KlinesParams, Market, MarketError, MexcBase, MexcSymbol, Order,
OrderStatus, Quantity, KLINES_LIMIT,
};
const DB_ADDRESS_TEST_WITH_MEXC: &str = "db/test_with_mexc";
const DB_NAMESPACE: &str = "scuriolus";
const DB_NAME: &str = "test_with_mexc";
const TABLE_STUB_ORDERS: &str = "stub_orders";
const TABLE_ACCOUNT_BALANCE: &str = "account_balance";
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct AccountBalance {
pub asset: String,
pub free: Amount,
pub locked: Amount,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct StubbedOrder {
order: Order,
time_creation: DateTime<Utc>,
time_update: DateTime<Utc>,
}
pub struct TestWithMexc {
client: MexcSpotApiClient,
db: Surreal<Db>,
clock: Box<dyn Clock>,
}
impl fmt::Debug for TestWithMexc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TestWithMexc")
.field("client", &"local client")
.field("db", &self.db)
.field("clock", &self.clock)
.finish()
}
}
impl MexcBase for TestWithMexc {
fn client(&self) -> &MexcSpotApiClient {
&self.client
}
}
impl TestWithMexc {
pub async fn new(clock: Box<dyn Clock>) -> Self {
let client = MexcSpotApiClient::new(MexcSpotApiEndpoint::Base);
client.ping().await.expect("API injoignable");
let db = Surreal::new::<RocksDb>(DB_ADDRESS_TEST_WITH_MEXC)
.await
.expect("error creating test with mexc db");
db.use_ns(DB_NAMESPACE)
.use_db(DB_NAME)
.await
.expect("error using test with mexc db");
Self { client, db, clock }
}
#[allow(dead_code)]
async fn balances_to_string(&self) -> String {
let maybe_balances: Result<Vec<AccountBalance>, surrealdb::Error> =
self.db.select(TABLE_ACCOUNT_BALANCE).await;
match maybe_balances {
Err(error) => {
format!("error with db : {}", error)
}
Ok(balances) => balances.iter().fold("".to_string(), |acc, balance| {
format!(
"{}{} : {} free / {} blocked\n",
acc, balance.asset, balance.free, balance.locked
)
}),
}
}
async fn get_stubbed_order(&self, id: &String) -> Result<Option<StubbedOrder>, Error> {
Ok(self.db.select((TABLE_STUB_ORDERS, id)).await?)
}
async fn update_stubbed_order(
&self,
s_order: StubbedOrder,
) -> Result<Option<StubbedOrder>, Error> {
let so: Option<StubbedOrder> = self
.db
.upsert((TABLE_STUB_ORDERS, s_order.order.id()))
.content(s_order)
.await?;
Ok(so)
}
async fn passes_limit(
&self,
order: &Order,
time: DateTime<Utc>,
time_ori: DateTime<Utc>,
) -> Result<bool, Error> {
let symbol = order.symbol();
let price = if let Some(p) = order.price() {
p
} else {
tracing::error!("Limit : price not set");
return Ok(false);
};
let from = time_ori.duration_round_up(TimeDelta::minutes(1)).unwrap();
let to = time.duration_trunc(TimeDelta::minutes(1)).unwrap();
let mut klines = self.get_all_klines(symbol.clone(), from, to).await?;
if klines.is_empty() {
return Ok(false);
}
#[cfg(debug_assertions)]
{
klines.sort_by(|a, b| a.open_time.cmp(&b.open_time));
tracing::debug!(
"Klines for {} between {} as {} and {} as {}",
symbol,
time_ori,
from,
time,
to
);
klines.iter().fold(from, |prev_close, kline| {
if prev_close != kline.open_time {
tracing::warn!(
"Hole in the klines, between {} and {}",
prev_close,
kline.open_time
);
}
kline.close_time
});
if klines.last().unwrap().close_time != to {
tracing::warn!(
"Hole in the klines, between {} and {}",
klines.last().unwrap().close_time,
to
);
}
tracing::trace!("First Kline: {:#?}", klines.first());
tracing::trace!("Last Kline: {:#?}", klines.last());
let (maxi, moment) = klines
.iter()
.fold((Decimal::MIN, &klines[0]), |acc, kandle| {
if acc.0 < kandle.high {
(kandle.high, kandle)
} else {
acc
}
});
tracing::debug!("Max of kandles is {} at {:#?}", maxi, moment);
let (mini, moment) = klines
.iter()
.fold((Decimal::MAX, &klines[0]), |acc, kandle| {
if acc.0 > kandle.low {
(kandle.low, kandle)
} else {
acc
}
});
tracing::debug!("Min of kandles is {} at {:#?}", mini, moment);
}
Ok(match order.side() {
mexc_enums::OrderSide::Buy => klines.iter().any(|kandle| kandle.low <= *price),
mexc_enums::OrderSide::Sell => klines.iter().any(|kandle| kandle.high >= *price),
})
}
async fn get_all_klines(
&self,
symbol: String,
mut from: DateTime<Utc>,
mut to: DateTime<Utc>,
) -> Result<Vec<Kline>, Error> {
let mut klines = vec![];
let mut precision = mexc_enums::KlineInterval::OneMinute;
while let Some(next_precision) = precision.next_interval() {
let (from_above, to_below) = get_inside_interval(from, to, next_precision)?;
if from_above >= to_below {
break;
}
let kline1 = self.klines_from_to(symbol.clone(), from, from_above, precision);
let kline2 = self.klines_from_to(symbol.clone(), to_below, to, precision);
klines.append(&mut kline1.await?);
klines.append(&mut kline2.await?);
from = from_above;
to = to_below;
precision = next_precision;
}
if to - from > precision.time_delta() * KLINES_LIMIT.try_into().unwrap() {
return Err(Error::msg(format!(
"Not implemented for more than {} {:#?}",
KLINES_LIMIT, precision,
)));
}
klines.append(&mut self.klines_from_to(symbol, from, to, precision).await?);
Ok(klines)
}
async fn klines_from_to(
&self,
symbol: String,
from: DateTime<Utc>,
to: DateTime<Utc>,
interval: mexc_enums::KlineInterval,
) -> Result<Vec<Kline>, Error> {
if from == to {
return Ok(vec![]);
}
tracing::trace!("Getting klines from {} to {} in {:#?}", from, to, interval);
let result = self
.klines(KlinesParams {
symbol,
interval,
start_time: Some(from),
end_time: Some(to),
limit: Some(KLINES_LIMIT),
})
.await?;
if result.is_empty() {
tracing::warn!(
"No klines found between {} and {} in {:#?}. This may be because too old for Mexc.com",
from,
to,
interval
);
} else if result.first().unwrap().open_time != from
&& result.last().unwrap().close_time != to
{
return Err(Error::msg(format!(
"Got klines from {} to {}",
result.first().unwrap().open_time,
result.last().unwrap().close_time
)));
}
Ok(result)
}
pub async fn reset_to(&self, crypto: &Crypto, starting_sold: Amount) -> Result<(), Error> {
let _: Vec<StubbedOrder> = self.db.delete(TABLE_STUB_ORDERS).await?;
let _: Vec<AccountBalance> = self.db.delete(TABLE_ACCOUNT_BALANCE).await?;
let _: Option<AccountBalance> = self
.db
.create((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(AccountBalance {
asset: crypto.to_string(),
free: starting_sold,
locked: Amount::ZERO,
})
.await?;
Ok(())
}
async fn deal_order_market(&self, order: &mut Order) -> Result<(), Error> {
let price = self.actual_price(order.symbol()).await?;
tracing::debug!(
"{} market price : {} at {}",
order.symbol(),
price,
self.clock.now()
);
match order.side() {
mexc_enums::OrderSide::Buy => {
if let Quantity::Quote(quote_quantity) = order.quantity() {
let asset_quantity = quote_quantity / price;
self.exchange(order, asset_quantity, -quote_quantity)
.await?;
order.set_price(price)?;
order.set_asset_quantity(asset_quantity)?;
Ok(())
} else {
Err(Error::msg("Market order buy : quantity should be in quote"))
}
}
mexc_enums::OrderSide::Sell => {
if let Quantity::Asset(asset_quantity) = order.quantity() {
let quote_quantity = asset_quantity * price;
self.exchange(order, -asset_quantity, quote_quantity)
.await?;
order.set_price(price)?;
Ok(())
} else {
Err(Error::msg(
"Market order sell : quantity should be in asset",
))
}
}
}
}
async fn actual_price(&self, symbol: String) -> Result<Amount, Error> {
let now = self.clock.now().duration_trunc(TimeDelta::minutes(1))?;
let mut kline;
let mut klines;
let mut interval = KlineInterval::OneMinute;
loop {
let (start_time, end_time) = get_interval_around(now, interval)?;
let kline_output = self
.klines(KlinesParams {
symbol: symbol.clone(),
interval,
start_time: Some(start_time),
end_time: Some(end_time),
limit: None,
})
.await;
klines = kline_output?;
kline = klines.first();
if kline.is_some() {
break;
}
interval = interval
.next_interval()
.ok_or(Error::msg("No klines on any available interval"))?;
}
let kline = kline.unwrap();
tracing::debug!("Selected price with precision {:#?}", interval);
tracing::trace!("Kline for order market : {:#?}", kline);
Ok((kline.open + kline.close) * Decimal::from_f32(0.5).unwrap())
}
async fn deal_order_limit(
&self,
order: &mut Order,
time: DateTime<Utc>,
time_ori: DateTime<Utc>,
) -> Result<(), Error> {
let asset_variation = if let Quantity::Asset(qty) = order.quantity() {
*qty
} else {
return Err(Error::msg("Order limit : quantity should be in asset"));
};
if !self.passes_limit(order, time, time_ori).await? {
tracing::debug!("Order {} didn't passed", order.id());
return Ok(());
}
let quote_variation = order.quote_quantity()?;
tracing::debug!(
"Order {} is passing at cost {}",
order.id(),
quote_variation
);
match order.side() {
mexc_enums::OrderSide::Buy => {
self.unlock(*order.quote(), "e_variation).await?;
self.exchange(order, asset_variation, -quote_variation)
.await?;
}
mexc_enums::OrderSide::Sell => {
self.unlock(*order.asset(), &asset_variation).await?;
self.exchange(order, -asset_variation, quote_variation)
.await?;
}
}
Ok(())
}
async fn _get_balance(&self, crypto: Crypto) -> Result<AccountBalance, MarketError> {
let balance: Option<AccountBalance> = self
.db
.select((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.await?;
Ok(balance.unwrap_or(AccountBalance {
asset: crypto.to_string(),
free: Amount::ZERO,
locked: Amount::ZERO,
}))
}
async fn exchange(
&self,
order: &mut Order,
asset_variation: Amount,
quote_variation: Amount,
) -> Result<(), Error> {
if asset_variation * quote_variation > Amount::ZERO {
return Err(Error::msg("Both variation should have same sign"));
}
self.adjust_balance(order.asset(), asset_variation).await?;
order.set_executed(
*order.executed_qty() + asset_variation,
*order.cummulative_quote_qty() + quote_variation,
);
order.set_status(OrderStatus::Filled);
self.adjust_balance(order.quote(), quote_variation).await?;
Ok(())
}
async fn lock(&self, crypto: Crypto, amount: &Amount) -> Result<(), Error> {
let mut balance = self._get_balance(crypto).await?;
if balance.free <= *amount {
return Err(Error::msg("Not enough free funds"));
}
balance.free -= amount;
balance.locked += amount;
tracing::trace!("Locked {} of {}", amount, crypto);
let _: Option<AccountBalance> = self
.db
.upsert((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(balance)
.await?;
Ok(())
}
async fn unlock(&self, crypto: Crypto, amount: &Amount) -> Result<(), Error> {
let mut balance = self._get_balance(crypto).await?;
if balance.locked < *amount {
return Err(Error::msg("Not enough locked funds"));
}
balance.locked -= amount;
balance.free += amount;
tracing::trace!("Unlocked {} of {}", amount, crypto);
let _: Option<AccountBalance> = self
.db
.upsert((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(balance)
.await?;
Ok(())
}
async fn adjust_balance(&self, crypto: &Crypto, delta: Amount) -> Result<(), Error> {
let mut balance = self._get_balance(*crypto).await?;
balance.free += delta;
if balance.free < Amount::ZERO {
return Err(Error::msg("Not enough funds"));
}
let _: Option<AccountBalance> = self
.db
.upsert((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(balance)
.await?;
Ok(())
}
async fn update_all_orders(&self) -> Result<(), Error> {
let stub_orders: Vec<StubbedOrder> = self.db.select(TABLE_STUB_ORDERS).await?;
for stub_order in stub_orders {
if !stub_order.order.is_done()
&& stub_order.time_creation + TimeDelta::minutes(1) < self.clock.now()
{
self.update_order(&stub_order.order).await?;
}
}
Ok(())
}
}
fn get_inside_interval(
from: DateTime<Utc>,
to: DateTime<Utc>,
inside_precision: KlineInterval,
) -> Result<(DateTime<Utc>, DateTime<Utc>), Error> {
match inside_precision {
KlineInterval::OneWeek => {
let mut from_above = from
.duration_round_up(KlineInterval::OneDay.time_delta())
.map_err(|_| {
Error::msg(format!(
"error rounding up {} with precision {:#?}",
from, inside_precision
))
})?;
while from_above.weekday() != Weekday::Mon {
from_above += TimeDelta::days(1);
}
let mut to_below = to
.duration_trunc(KlineInterval::OneDay.time_delta())
.map_err(|_| {
Error::msg(format!(
"error truncating {} with precision {:#?}",
to, inside_precision
))
})?;
while to_below.weekday() != Weekday::Mon {
to_below -= TimeDelta::days(1);
}
Ok((from_above, to_below))
}
KlineInterval::OneMonth => unimplemented!(),
_ => Ok((
from.duration_round_up(inside_precision.time_delta())
.map_err(|_| {
Error::msg(format!(
"error rounding up {} with precision {:#?}",
from, inside_precision
))
})?,
to.duration_trunc(inside_precision.time_delta())
.map_err(|_| {
Error::msg(format!(
"error truncating {} with precision {:#?}",
to, inside_precision
))
})?,
)),
}
}
fn get_interval_around(
now: DateTime<Utc>,
interval: KlineInterval,
) -> Result<(DateTime<Utc>, DateTime<Utc>), Error> {
let before = now - Duration::seconds(1);
match interval {
KlineInterval::OneWeek => {
let mut start = before
.duration_trunc(KlineInterval::OneDay.time_delta())
.map_err(|_| {
Error::msg(format!(
"error truncating {} with precision {:#?}",
before, interval
))
})?;
let mut end = now
.duration_round_up(KlineInterval::OneDay.time_delta())
.map_err(|_| {
Error::msg(format!(
"error rounding up {} with precision {:#?}",
now, interval
))
})?;
while start.weekday() != Weekday::Mon {
start -= TimeDelta::days(1);
}
while end.weekday() != Weekday::Mon {
end += TimeDelta::days(1);
}
Ok((end, start))
}
KlineInterval::OneMonth => unimplemented!(),
_ => Ok((
before.duration_trunc(interval.time_delta()).map_err(|_| {
Error::msg(format!(
"error truncating {} with precision {:#?}",
before, interval
))
})?,
now.duration_round_up(interval.time_delta()).map_err(|_| {
Error::msg(format!(
"error rounding up {} with precision {:#?}",
now, interval
))
})?,
)),
}
}
#[allow(unused_variables)]
impl Market for TestWithMexc {
async fn klines(&self, params: KlinesParams) -> Result<Vec<Kline>, MarketError> {
self.base_klines(¶ms).await
}
async fn get_balance(&self, crypto: Crypto) -> Result<Balance, MarketError> {
if let Err(error) = self.update_all_orders().await {
tracing::error!("Error updating orders : {}", error);
};
let balance = self._get_balance(crypto).await?;
Ok(Balance {
free: balance.free,
locked: balance.locked,
})
}
async fn send_order(&self, order: &Order) -> Result<String, MarketError> {
if let Some(previous) = self.get_stubbed_order(order.id()).await? {
tracing::error!("Add order: {} already exists", order.id());
tracing::trace!("Previous: {:#?}", previous);
tracing::trace!("Challenger: {:#?}", order);
return Err(Error::msg("id already occupied").into());
}
let mut my_order = order.clone();
my_order.set_status(OrderStatus::Open);
match order.order_type() {
mexc_enums::OrderType::Limit => match order.side() {
mexc_enums::OrderSide::Buy => {
self.lock(*order.quote(), &order.quote_quantity()?).await?
}
mexc_enums::OrderSide::Sell => {
if let Quantity::Asset(asset_quantity) = order.quantity() {
self.lock(*order.asset(), asset_quantity).await?
} else {
return Err(Error::msg("Limit order must specify an asset quantity").into());
}
}
},
mexc_enums::OrderType::Market => self.deal_order_market(&mut my_order).await?,
mexc_enums::OrderType::LimitMaker => {
return Err(Error::msg("LimitMaker order not implemented").into())
}
mexc_enums::OrderType::ImmediateOrCancel => {
return Err(Error::msg("ImmediateOrCancel order not implemented").into())
}
mexc_enums::OrderType::FillOrKill => {
return Err(Error::msg("FillOrKill order not implemented").into())
}
}
let now = self.clock.now();
let _: Option<StubbedOrder> = self
.db
.create((TABLE_STUB_ORDERS, order.id()))
.content(StubbedOrder {
order: my_order,
time_creation: now,
time_update: now,
})
.await?;
Ok(order.id().clone())
}
async fn cancel_order(&self, order: &Order) -> Result<CancelOrderOutput, MarketError> {
self.update_order(order).await?;
let previous = self.get_stubbed_order(order.id()).await?.ok_or_else(|| {
tracing::error!("Update order: {} doesn't exist", order.id());
tracing::trace!("Order: {:#?}", order);
Error::msg("Order not found")
})?;
let mut local_order = previous.order;
match local_order.status() {
OrderStatus::PartiallyCanceled
| OrderStatus::Filled
| OrderStatus::Canceled
| OrderStatus::Tested
| OrderStatus::Failed => (),
OrderStatus::PartiallyFilled => {
local_order.set_status(OrderStatus::PartiallyCanceled);
Err(Error::msg("Not Implemented"))?;
}
OrderStatus::Open => {
local_order.set_status(OrderStatus::Canceled);
if matches!(local_order.order_type(), mexc_enums::OrderType::Limit) {
match order.side() {
mexc_enums::OrderSide::Buy => {
self.unlock(*order.quote(), &order.quote_quantity()?)
.await?
}
mexc_enums::OrderSide::Sell => {
if let Quantity::Asset(asset_quantity) = order.quantity() {
self.unlock(*order.asset(), asset_quantity).await?
} else {
return Err(Error::msg(
"Limit order must specify an asset quantity",
)
.into());
}
}
};
}
}
}
let original_quantity = if let Quantity::Asset(quantity) = local_order.quantity() {
*quantity
} else {
return Err(Error::msg(
"INTERNAL ERROR: Every order should have a quantity",
))?;
};
let output = CancelOrderOutput {
symbol: local_order.symbol().to_string(),
original_client_order_id: Some(local_order.id().clone()),
order_id: local_order.id().clone(),
client_order_id: Some(local_order.id().clone()),
price: local_order.price().expect("Price should be set"),
original_quantity,
executed_quantity: *local_order.executed_qty(),
cummulative_quote_quantity: *local_order.cummulative_quote_qty(),
status: mexc_enums::OrderStatus::from(*local_order.status()),
time_in_force: None,
order_type: *local_order.order_type(),
side: *local_order.side(),
};
self.update_stubbed_order(StubbedOrder {
order: local_order,
time_creation: previous.time_creation,
time_update: self.clock.now(),
})
.await?;
Ok(output)
}
async fn update_order(&self, order: &Order) -> Result<QueryOrderOutput, MarketError> {
let previous = self.get_stubbed_order(order.id()).await?.ok_or_else(|| {
tracing::error!("Update order: {} doesn't exist", order.id());
tracing::trace!("Order: {:#?}", order);
Error::msg("Order not found")
})?;
let time_ori = previous.time_creation;
let mut local_order = previous.order;
let now = self.clock.now();
match local_order.status() {
OrderStatus::Open => match order.order_type() {
mexc_enums::OrderType::Market => {
return Err(
Error::msg("Market order should have been filled at send time").into(),
);
}
mexc_enums::OrderType::Limit => {
if previous.time_update + TimeDelta::minutes(1) < now {
self.deal_order_limit(&mut local_order, now, time_ori)
.await?;
} else {
tracing::trace!("Order {} not updated : too soon", order.id());
}
}
_ => {
return Err(Error::msg("Not implemented").into());
}
},
OrderStatus::PartiallyFilled => {
return Err(Error::msg("Not implemented").into());
}
OrderStatus::Filled
| OrderStatus::Canceled
| OrderStatus::PartiallyCanceled
| OrderStatus::Tested
| OrderStatus::Failed => (),
}
let original_quantity = if let Quantity::Asset(quantity) = local_order.quantity() {
*quantity
} else {
return Err(Error::msg(
"INTERNAL ERROR: Every order should have a quantity",
))?;
};
let output = QueryOrderOutput {
symbol: local_order.symbol().to_string(),
original_client_order_id: Some(local_order.id().clone()),
order_id: local_order.id().clone(),
client_order_id: Some(local_order.id().clone()),
price: local_order.price().expect("Price should be set"),
original_quantity,
executed_quantity: *local_order.executed_qty(),
cummulative_quote_quantity: *local_order.cummulative_quote_qty(),
status: mexc_enums::OrderStatus::from(*local_order.status()),
time_in_force: None,
order_type: *local_order.order_type(),
side: *local_order.side(),
stop_price: order.price().unwrap(), time: time_ori,
update_time: self.clock.now(),
is_working: matches!(order.status(), OrderStatus::Open),
};
self.update_stubbed_order(StubbedOrder {
order: local_order,
time_creation: time_ori,
time_update: self.clock.now(),
})
.await?;
Ok(output)
}
fn clock(&self) -> Box<dyn Clock> {
self.clock.clone_box()
}
}
trait CrescendoInterval {
fn next_interval(&self) -> Option<KlineInterval>;
fn time_delta(&self) -> TimeDelta;
}
impl CrescendoInterval for KlineInterval {
fn next_interval(&self) -> Option<KlineInterval> {
match self {
KlineInterval::OneMinute => Some(KlineInterval::FiveMinutes),
KlineInterval::FiveMinutes => Some(KlineInterval::FifteenMinutes),
KlineInterval::FifteenMinutes => Some(KlineInterval::OneHour),
KlineInterval::ThirtyMinutes => Some(KlineInterval::OneHour),
KlineInterval::OneHour => Some(KlineInterval::OneDay),
KlineInterval::FourHours => Some(KlineInterval::OneDay),
KlineInterval::OneDay => Some(KlineInterval::OneWeek),
KlineInterval::OneWeek => None,
KlineInterval::OneMonth => None,
}
}
fn time_delta(&self) -> TimeDelta {
match self {
KlineInterval::OneMinute => TimeDelta::minutes(1),
KlineInterval::FiveMinutes => TimeDelta::minutes(5),
KlineInterval::FifteenMinutes => TimeDelta::minutes(15),
KlineInterval::ThirtyMinutes => TimeDelta::minutes(30),
KlineInterval::OneHour => TimeDelta::hours(1),
KlineInterval::FourHours => TimeDelta::hours(4),
KlineInterval::OneDay => TimeDelta::days(1),
KlineInterval::OneWeek => TimeDelta::weeks(1),
KlineInterval::OneMonth => TimeDelta::days(30),
}
}
}
impl From<surrealdb::Error> for MarketError {
fn from(value: surrealdb::Error) -> Self {
anyhow::Error::from(value).into()
}
}
#[cfg(all(test, feature = "mexc"))]
mod tests {
use serial_test::serial;
use std::str::FromStr;
use anyhow::Error;
use chrono::{DateTime, Duration};
use super::TestWithMexc;
use super::super::{
super::super::clock::{CheatClockFactory as _, StableCheatClockFactory},
mexc_enums, Amount, Crypto, Market, Order, Quantity,
};
const FONDS: i32 = 100000000;
#[tokio::test]
#[serial]
async fn limit_buy_sell() -> Result<(), Error> {
let (remote, clock) = StableCheatClockFactory {}
.get_clock(DateTime::from_str("2022-01-01 00:30:30Z").unwrap())
.unwrap();
remote.not_blocking();
let market = TestWithMexc::new(clock).await;
let buy_price = Amount::from(25000); let two_buy_price = Amount::from(50000);
let clock = market.clock();
market.reset_to(&Crypto::USDT, FONDS.into()).await?;
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
mexc_enums::OrderSide::Buy,
mexc_enums::OrderType::Limit,
Quantity::Asset(Amount::TWO),
Some(buy_price),
)?;
market.send_order(&order).await?;
tracing::debug!("=== Sleeping for 1 months ===");
clock.sleep(Duration::days(31)).await;
let usdt = market.get_balance(Crypto::USDT).await?;
assert_eq!(usdt.locked, two_buy_price);
assert_eq!(usdt.free, Amount::from(FONDS) - two_buy_price);
let status = market.update_order(&order).await?.status;
assert_eq!(status, mexc_enums::OrderStatus::New);
tracing::debug!("=== Sleeping for 6 months ===");
clock.sleep(Duration::days(6 * 30)).await;
market.cancel_order(&order).await?;
assert_eq!(market.get_balance(Crypto::USDT).await?.locked, Amount::ZERO);
let status_after_crash = market.update_order(&order).await?.status;
assert_eq!(status_after_crash, mexc_enums::OrderStatus::Filled);
let usdt = market.get_balance(Crypto::USDT).await?;
assert!(usdt.free >= Amount::from(FONDS) - two_buy_price);
assert!(usdt.free < FONDS.into());
assert_eq!(market.get_balance(Crypto::BTC).await?.free, Amount::TWO);
}
let sell_price = Amount::from(50000);
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
mexc_enums::OrderSide::Sell,
mexc_enums::OrderType::Limit,
Quantity::Asset(Amount::ONE),
Some(sell_price),
)?;
market.send_order(&order).await?;
tracing::debug!("=== Sleeping for 1 months ===");
clock.sleep(Duration::days(31)).await;
let btc = market.get_balance(Crypto::BTC).await?;
assert_eq!(btc.locked, Amount::ONE);
assert_eq!(btc.free, Amount::TWO - Amount::ONE);
let status = market.update_order(&order).await?.status;
assert_eq!(status, mexc_enums::OrderStatus::New);
tracing::debug!("=== Sleeping for 19 months ===");
clock.sleep(Duration::days(19 * 30)).await;
market.cancel_order(&order).await?;
assert_eq!(market.get_balance(Crypto::USDT).await?.locked, Amount::ZERO);
let status_after_crash = market.update_order(&order).await?.status;
assert_eq!(status_after_crash, mexc_enums::OrderStatus::Filled);
let free_usdt = market.get_balance(Crypto::USDT).await?.free;
assert!(free_usdt >= Amount::from(FONDS) - buy_price);
assert_eq!(market.get_balance(Crypto::BTC).await?.free, Amount::ONE);
}
Ok(())
}
#[tokio::test]
#[serial]
async fn cancel() -> Result<(), Error> {
let (remote, clock) = StableCheatClockFactory {}
.get_clock(DateTime::from_str("2022-01-01 00:30:30Z").unwrap())
.unwrap();
remote.not_blocking();
let market = TestWithMexc::new(clock).await;
let price = Amount::from(25000);
let clock = market.clock();
market.reset_to(&Crypto::USDT, FONDS.into()).await?;
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
mexc_enums::OrderSide::Buy,
mexc_enums::OrderType::Limit,
Quantity::Asset(Amount::ONE),
Some(price),
)?;
market.send_order(&order).await?;
clock.sleep(Duration::days(31)).await;
let status = market.update_order(&order).await?.status;
assert_eq!(status, mexc_enums::OrderStatus::New);
market.cancel_order(&order).await?;
clock.sleep(Duration::days(6 * 30)).await;
let status_after_crash = market.update_order(&order).await?.status;
assert_eq!(status_after_crash, mexc_enums::OrderStatus::Canceled);
let free_usdt = market.get_balance(Crypto::USDT).await?.free;
assert_eq!(free_usdt, Amount::from(FONDS));
assert_eq!(market.get_balance(Crypto::BTC).await?.free, Amount::ZERO);
Ok(())
}
#[tokio::test]
#[serial]
async fn market_buy_sell() -> Result<(), Error> {
let (remote, clock) = StableCheatClockFactory {}
.get_clock(DateTime::from_str("2025-04-21 18:46:00Z").unwrap())
.unwrap();
remote.not_blocking();
let market = TestWithMexc::new(clock).await;
let cost_for_one = Amount::from_str("86909.21")?;
let cost_for_two = Amount::from_str("173818.42")?;
market.reset_to(&Crypto::USDT, FONDS.into()).await?;
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
mexc_enums::OrderSide::Buy,
mexc_enums::OrderType::Market,
Quantity::Quote(cost_for_two),
None,
)?;
market.send_order(&order).await?;
let free_usdt = market.get_balance(Crypto::USDT).await?.free;
let free_btc = market.get_balance(Crypto::BTC).await?.free;
assert_eq!(free_usdt, Amount::from(FONDS) - cost_for_two);
assert_eq!(free_btc, Amount::TWO);
}
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
mexc_enums::OrderSide::Sell,
mexc_enums::OrderType::Market,
Quantity::Asset(Amount::ONE),
None,
)?;
market.send_order(&order).await?;
let free_usdt = market.get_balance(Crypto::USDT).await?.free;
let free_btc = market.get_balance(Crypto::BTC).await?.free;
assert_eq!(free_usdt, Amount::from(FONDS) - cost_for_one);
assert_eq!(free_btc, Amount::ONE);
}
Ok(())
}
}