use chrono::{DateTime, Datelike as _, DurationRound as _, TimeDelta, Utc, Weekday};
use rust_decimal::{Decimal, prelude::FromPrimitive as _};
use surrealdb::{
Surreal,
engine::local::{Db, RocksDb},
};
use crate::{
clock::ClockBase,
core::{
CoreError, CoreResult,
market::{Balance, Market, MarketFactory, SpecificOrderDetails, UpdateOrderOutput},
},
generics::order::{
Crypto, EmptySpecificOrderDetails, Order, OrderSide, OrderStatus, OrderType,
},
provider::{
DataQuery, Interval, ProviderTrait, Source,
kline::{Kline, KlineSpecifier},
},
};
type SimpleOrder = Order<EmptySpecificOrderDetails>;
type SimpleUpdateOrderOutput = UpdateOrderOutput<EmptySpecificOrderDetails>;
#[cfg(not(test))]
const DB_ADDRESS_BASE: &str = "db/simulation";
#[cfg(test)]
const DB_ADDRESS_BASE: &str = "db/test-simulation";
const DB_NAMESPACE: &str = "scuriolus";
const DB_NAME: &str = "main";
const TABLE_STUB_ORDERS: &str = "stub_orders";
const TABLE_ACCOUNT_BALANCE: &str = "account_balance";
const DELTA_MIN_UPDATE_LIMIT: TimeDelta = TimeDelta::minutes(1);
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct AccountBalance {
pub asset: String,
pub free: Decimal,
pub locked: Decimal,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct StubbedOrder {
order: SimpleOrder,
time_creation: DateTime<Utc>,
time_update: DateTime<Utc>,
}
pub struct SimulatedFactory<P, S, C: ClockBase>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
provider: Option<P>,
clock: Option<C>,
name: String,
balances: Vec<(Crypto, Decimal)>,
}
impl<P, S, C: ClockBase> SimulatedFactory<P, S, C>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
pub fn new(name: String) -> Self {
Self {
name,
provider: None,
clock: None,
balances: vec![],
}
}
pub fn with_provider(self, provider: P) -> Self {
Self {
provider: Some(provider),
..self
}
}
pub fn with_balance(mut self, crypto: Crypto, amount: Decimal) -> Self {
self.balances.push((crypto, amount));
self
}
}
impl<P, S, C: ClockBase> MarketFactory for SimulatedFactory<P, S, C>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
type _Market = Simulated<P, S, C>;
fn with_clock(self, clock: <Self::_Market as Market>::_Clock) -> Self {
Self {
clock: Some(clock),
..self
}
}
async fn build(self) -> CoreResult<Self::_Market> {
let clock = self.clock.ok_or(CoreError::param_error("clock not set"))?;
let provider = self
.provider
.ok_or(CoreError::param_error("source not set"))?;
let simulated = Simulated::new(&self.name, provider, clock).await;
for (crypto, amount) in self.balances {
simulated.set_balance_to(&crypto, amount).await?;
}
Ok(simulated)
}
}
fn limit_quote_variation<D: SpecificOrderDetails>(order: &Order<D>) -> CoreResult<Decimal> {
let asset = order
.qty_asset()
.ok_or(CoreError::param_error("Limit order must specify an asset"))?;
let price = order
.price()
.ok_or(CoreError::param_error("Limit: price not set"))?;
let quote_variation = asset
.checked_mul(price)
.ok_or(CoreError::comput_error("Multiplication error"))?;
Ok(quote_variation)
}
#[derive(Debug)]
pub struct Simulated<P, S, C: ClockBase>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
provider: P,
simulation_db: Surreal<Db>,
clock: C,
}
impl<P, S, C: ClockBase> Simulated<P, S, C>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
async fn new(name: &str, provider: P, clock: C) -> Self {
let simulation_db =
Surreal::new::<RocksDb>(format!("{}/{}/{}", DB_ADDRESS_BASE, provider.name(), name))
.await
.unwrap();
simulation_db
.use_ns(DB_NAMESPACE)
.use_db(DB_NAME)
.await
.unwrap();
Self {
provider,
simulation_db,
clock,
}
}
async fn get_stubbed_order(&self, id: &String) -> Result<Option<StubbedOrder>, CoreError> {
Ok(self.simulation_db.select((TABLE_STUB_ORDERS, id)).await?)
}
async fn update_stubbed_order(
&self,
s_order: StubbedOrder,
) -> CoreResult<Option<StubbedOrder>> {
let so: Option<StubbedOrder> = self
.simulation_db
.upsert((TABLE_STUB_ORDERS, s_order.order.id()))
.content(s_order)
.await?;
Ok(so)
}
async fn passes_limit(
&self,
order: &SimpleOrder,
time: DateTime<Utc>,
time_ori: DateTime<Utc>,
) -> CoreResult<bool> {
let symbol = order.symbol();
let price = order
.price()
.as_ref()
.ok_or(CoreError::param_error("Limit: price not set"))?;
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(*order.asset(), *order.quote(), from, to)
.await?;
if klines.is_empty() {
return Ok(false);
}
#[cfg(debug_assertions)]
{
klines.sort_by(|a, b| a.basics.begin.cmp(&b.basics.begin));
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.basics.begin {
tracing::warn!(
"Hole in the klines, between {} and {}",
prev_close,
kline.basics.begin
);
}
kline.basics.end
});
if klines.last().unwrap().basics.end != to {
tracing::warn!(
"Hole in the klines, between {} and {}",
klines.last().unwrap().basics.end,
to
);
}
tracing::trace!("First Kline: {:#?}", klines.first());
tracing::trace!("Last Kline: {:#?}", klines.last());
let (maxi, moment) =
klines
.iter()
.fold((Decimal::MIN, &klines[0]), |acc, may_kline| {
if let Some(kline) = may_kline.content.as_ref()
&& acc.0 < kline.high
{
(kline.high, may_kline)
} else {
acc
}
});
tracing::debug!("Max of kandles is {}", maxi);
tracing::trace!("max is in {:#?}", moment);
let (mini, moment) =
klines
.iter()
.fold((Decimal::MAX, &klines[0]), |acc, may_kline| {
if let Some(kline) = may_kline.content.as_ref()
&& acc.0 > kline.low
{
(kline.low, may_kline)
} else {
acc
}
});
tracing::debug!("Min of kandles is {}", mini);
tracing::trace!("min is in {:#?}", moment);
}
Ok(match order.side() {
OrderSide::Buy => klines.iter().any(|kline| {
kline
.content
.as_ref()
.is_some_and(|kline| kline.close <= *price)
}),
OrderSide::Sell => klines.iter().any(|kline| {
kline
.content
.as_ref()
.is_some_and(|kline| kline.high >= *price)
}),
})
}
async fn get_all_klines(
&self,
asset: Crypto,
quote: Crypto,
mut from: DateTime<Utc>,
mut to: DateTime<Utc>,
) -> Result<Vec<Kline>, CoreError> {
let mut klines = vec![];
let mut precision = Interval::OneMinute;
while let Some(next_precision) = precision.next_interval_to_zoom_out() {
let (from_above, to_below) = get_inside_interval(from, to, next_precision)?;
if from_above >= to_below {
break;
}
let query1 = DataQuery::new(
KlineSpecifier::new(asset, quote),
precision,
from,
from_above,
);
let kline1 = self.provider.provide(&query1);
let query2 = DataQuery::new(KlineSpecifier::new(asset, quote), precision, to_below, to);
let kline2 = self.provider.provide(&query2);
klines.append(&mut kline1.await?);
klines.append(&mut kline2.await?);
from = from_above;
to = to_below;
precision = next_precision;
}
klines.append(
&mut self
.provider
.provide(&DataQuery::new(
KlineSpecifier::new(asset, quote),
precision,
from,
to,
))
.await?,
);
Ok(klines)
}
pub async fn set_balance_to(
&self,
crypto: &Crypto,
starting_sold: Decimal,
) -> Result<(), CoreError> {
let _: Vec<StubbedOrder> = self.simulation_db.delete(TABLE_STUB_ORDERS).await?;
let _: Vec<AccountBalance> = self.simulation_db.delete(TABLE_ACCOUNT_BALANCE).await?;
let _: Option<AccountBalance> = self
.simulation_db
.create((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(AccountBalance {
asset: crypto.to_string(),
free: starting_sold,
locked: Decimal::ZERO,
})
.await?;
Ok(())
}
async fn deal_order_market(
&self,
order: &SimpleOrder,
update: &mut SimpleUpdateOrderOutput,
) -> Result<(), CoreError> {
let price = self.actual_price(*order.asset(), *order.quote()).await?;
tracing::debug!(
"{} source price : {} at {}",
order.symbol(),
price,
self.clock.now()
);
match order.side() {
OrderSide::Buy => {
if let Some(quote_quantity) = order.qty_quote()
&& order.qty_asset().is_none()
{
let asset_quantity = quote_quantity / price;
self.exchange(order, update, asset_quantity, -quote_quantity)
.await?;
Ok(())
} else {
Err(CoreError::param_error(
"Market order buy : quantity should be in quote",
))
}
}
OrderSide::Sell => {
if let Some(asset_quantity) = order.qty_asset() {
let quote_quantity = asset_quantity * price;
self.exchange(order, update, -asset_quantity, quote_quantity)
.await?;
Ok(())
} else {
Err(CoreError::param_error(
"Market order sell : quantity should be in asset",
))
}
}
}
}
async fn actual_price(&self, asset: Crypto, quote: Crypto) -> Result<Decimal, CoreError> {
let now = self.clock.now().duration_trunc(TimeDelta::minutes(1))?;
let mut klines;
let mut interval = Interval::OneMinute;
let data = loop {
let (start_time, end_time) = interval.get_time_bounds(now)?;
klines = self
.provider
.provide_or_empty(&DataQuery::new(
KlineSpecifier::new(asset, quote),
interval,
start_time,
end_time,
))
.await?;
if let Some(first_kline) = klines.first()
&& let Some(data) = first_kline.content.as_ref()
{
break data;
};
interval = interval
.next_interval_to_zoom_out()
.ok_or(CoreError::comput_error(
"No klines on any available interval",
))?;
};
tracing::debug!("Selected price with precision {:?}", interval);
tracing::trace!("data for order source : {:#?}", data);
Ok((data.open + data.close) * Decimal::from_f32(0.5).unwrap())
}
async fn deal_order_limit(
&self,
order: &SimpleOrder,
update: &mut SimpleUpdateOrderOutput,
time: DateTime<Utc>,
time_ori: DateTime<Utc>,
) -> Result<(), CoreError> {
if !self.passes_limit(order, time, time_ori).await? {
tracing::debug!("Order {} didn't passed", order.id());
return Ok(());
};
let asset_variation = order.qty_asset().ok_or(CoreError::param_error(
"Limit order buy : quantity should be in asset",
))?;
let price = order
.price()
.ok_or(CoreError::param_error("Limit: price not set"))?;
let quote_variation = limit_quote_variation(order)?;
tracing::debug!("Order {} is passing at cost {}", order.id(), &price);
match order.side() {
OrderSide::Buy => {
self.unlock(*order.quote(), "e_variation).await?;
self.exchange(order, update, asset_variation, -quote_variation)
.await?;
}
OrderSide::Sell => {
self.unlock(*order.asset(), &asset_variation).await?;
self.exchange(order, update, -asset_variation, quote_variation)
.await?;
}
}
Ok(())
}
async fn _get_balance(&self, crypto: Crypto) -> Result<AccountBalance, CoreError> {
let balance: Option<AccountBalance> = self
.simulation_db
.select((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.await?;
Ok(balance.unwrap_or(AccountBalance {
asset: crypto.to_string(),
free: Decimal::ZERO,
locked: Decimal::ZERO,
}))
}
async fn exchange(
&self,
order: &SimpleOrder,
update: &mut SimpleUpdateOrderOutput,
asset_variation: Decimal,
quote_variation: Decimal,
) -> Result<(), CoreError> {
if asset_variation.is_sign_positive() == quote_variation.is_sign_positive() {
return Err(CoreError::param_error(
"Variations should have different signs",
));
}
let mut balance_asset = self._get_balance(*order.asset()).await?;
balance_asset.free += asset_variation;
if balance_asset.free < Decimal::ZERO {
return Err(CoreError::param_error("Not enough asset funds"));
}
let mut balance_quote = self._get_balance(*order.quote()).await?;
balance_quote.free += quote_variation;
if balance_quote.free < Decimal::ZERO {
return Err(CoreError::param_error("Not enough quote funds"));
}
let _: Option<AccountBalance> = self
.simulation_db
.upsert((TABLE_ACCOUNT_BALANCE, order.asset().to_string()))
.content(balance_asset)
.await?;
let _: Option<AccountBalance> = self
.simulation_db
.upsert((TABLE_ACCOUNT_BALANCE, order.quote().to_string()))
.content(balance_quote)
.await?;
update.executed_qty_asset = asset_variation;
update.executed_qty_quote = quote_variation;
update.status = OrderStatus::Filled;
Ok(())
}
async fn lock(&self, crypto: Crypto, amount: &Decimal) -> Result<(), CoreError> {
let mut balance = self._get_balance(crypto).await?;
if balance.free <= *amount {
return Err(CoreError::param_error("Not enough free funds"));
}
balance.free -= amount;
balance.locked += amount;
tracing::trace!("Locked {} of {}", amount, crypto);
let _: Option<AccountBalance> = self
.simulation_db
.upsert((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(balance)
.await?;
Ok(())
}
async fn unlock(&self, crypto: Crypto, amount: &Decimal) -> Result<(), CoreError> {
let mut balance = self._get_balance(crypto).await?;
if balance.locked < *amount {
return Err(CoreError::param_error("Not enough locked funds"));
}
balance.locked -= amount;
balance.free += amount;
tracing::trace!("Unlocked {} of {}", amount, crypto);
let _: Option<AccountBalance> = self
.simulation_db
.upsert((TABLE_ACCOUNT_BALANCE, crypto.to_string()))
.content(balance)
.await?;
Ok(())
}
async fn update_all_orders(&self) -> Result<(), CoreError> {
let stub_orders: Vec<StubbedOrder> = self.simulation_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: Interval,
) -> CoreResult<(DateTime<Utc>, DateTime<Utc>)> {
match inside_precision {
Interval::OneWeek => {
let mut from_above = from.duration_round_up(Interval::OneDay.time_delta())?;
while from_above.weekday() != Weekday::Mon {
from_above += TimeDelta::days(1);
}
let mut to_below = to.duration_trunc(Interval::OneDay.time_delta())?;
while to_below.weekday() != Weekday::Mon {
to_below -= TimeDelta::days(1);
}
Ok((from_above, to_below))
}
Interval::OneMonth => unimplemented!(),
_ => Ok((
from.duration_round_up(inside_precision.time_delta())?,
to.duration_trunc(inside_precision.time_delta())?,
)),
}
}
impl<P, S, C: ClockBase> Market for Simulated<P, S, C>
where
P: ProviderTrait<_Source = S>,
S: Source<_Data = Kline>,
{
type _Clock = C;
type _OrderDetails = EmptySpecificOrderDetails;
fn name(&self) -> String {
format!("Simulated-{}-{}", self.provider.name(), Self::_Clock::NAME)
}
async fn get_balance(&self, crypto: Crypto) -> CoreResult<Balance> {
if let Err(error) = self.update_all_orders().await {
tracing::error!("CoreError updating orders : {}", error);
};
let balance = self._get_balance(crypto).await?;
Ok(Balance {
free: balance.free,
locked: balance.locked,
})
}
async fn send_order(&self, order: &SimpleOrder) -> CoreResult<SimpleUpdateOrderOutput> {
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(CoreError::DbError("id already occupied".to_string()));
}
let mut output = UpdateOrderOutput::from(order);
output.status = OrderStatus::Open;
match order.order_type() {
OrderType::Limit => match order.side() {
OrderSide::Buy => {
let quote_variation = limit_quote_variation(order)?;
self.lock(*order.quote(), "e_variation).await?;
}
OrderSide::Sell => {
self.lock(
*order.asset(),
&order.qty_asset().ok_or(CoreError::param_error(
"Limit order must specify an asset quantity",
))?,
)
.await?
}
},
OrderType::Market => self.deal_order_market(order, &mut output).await?,
OrderType::LimitMaker => {
return Err(CoreError::param_error("LimitMaker order not implemented"));
}
OrderType::ImmediateOrCancel => {
return Err(CoreError::param_error(
"ImmediateOrCancel order not implemented",
));
}
OrderType::FillOrKill => {
return Err(CoreError::param_error("FillOrKill order not implemented"));
}
}
let mut simulated_order = order.clone();
simulated_order.update(output.clone()).unwrap();
let now = self.clock.now();
let _: Option<StubbedOrder> = self
.simulation_db
.create((TABLE_STUB_ORDERS, order.id()))
.content(StubbedOrder {
order: simulated_order,
time_creation: now,
time_update: now,
})
.await?;
Ok(output)
}
async fn update_order(&self, order: &SimpleOrder) -> CoreResult<SimpleUpdateOrderOutput> {
let previous = self.get_stubbed_order(order.id()).await?.ok_or_else(|| {
tracing::error!("Update order: {} doesn't exist", order.id());
tracing::trace!("Not updated order: {:#?}", order);
CoreError::param_error("Order not found")
})?;
let time_ori = previous.time_creation;
let mut local_order = previous.order;
let mut update = UpdateOrderOutput::from(&local_order);
let now = self.clock.now();
match local_order.status() {
OrderStatus::Open => match order.order_type() {
OrderType::Market => {
return Err(CoreError::comput_error(
"Market order should have been filled at send time",
));
}
OrderType::Limit => {
if previous.time_update + DELTA_MIN_UPDATE_LIMIT < now {
self.deal_order_limit(order, &mut update, now, time_ori)
.await?;
} else {
tracing::trace!("Order {} not updated : too soon", order.id());
}
}
_ => {
return Err(CoreError::param_error("Not implemented"));
}
},
OrderStatus::PartiallyFilled => {
return Err(CoreError::param_error("Not implemented"));
}
OrderStatus::Filled
| OrderStatus::Canceled
| OrderStatus::PartiallyCanceled
| OrderStatus::Tested
| OrderStatus::Failed => (),
}
local_order.update(update.clone()).unwrap();
self.update_stubbed_order(StubbedOrder {
order: local_order,
time_creation: time_ori,
time_update: self.clock.now(),
})
.await?;
Ok(update)
}
async fn cancel_order(&self, order: &SimpleOrder) -> CoreResult<SimpleUpdateOrderOutput> {
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!("Unexisting order: {:#?}", order);
CoreError::param_error("Order not found")
})?;
let mut local_order = previous.order;
let mut update = UpdateOrderOutput::from(&local_order);
match local_order.status() {
OrderStatus::PartiallyCanceled
| OrderStatus::Filled
| OrderStatus::Canceled
| OrderStatus::Tested
| OrderStatus::Failed => (),
OrderStatus::PartiallyFilled => {
update.status = OrderStatus::PartiallyCanceled;
Err(CoreError::param_error("Not Implemented"))?;
}
OrderStatus::Open => {
update.status = OrderStatus::Canceled;
if matches!(local_order.order_type(), OrderType::Limit) {
match order.side() {
OrderSide::Buy => {
let quote_variation = limit_quote_variation(order)?;
self.unlock(*order.quote(), "e_variation).await?
}
OrderSide::Sell => {
self.unlock(
*order.asset(),
&order.qty_asset().ok_or(CoreError::param_error(
"Limit order selling must specify an asset quantity",
))?,
)
.await?
}
};
}
}
}
local_order.update(update.clone()).unwrap();
self.update_stubbed_order(StubbedOrder {
order: local_order,
time_creation: previous.time_creation,
time_update: self.clock.now(),
})
.await?;
Ok(update)
}
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Datelike, Duration};
use rust_decimal::Decimal;
use std::str::FromStr;
use crate::{
clock::{Clock as _, ClockFactory as _, ClockRemote as _, StableCheatClockFactory},
core::{
CoreError,
market::{Market as _, simulated::Simulated},
},
generics::order::{Crypto, Order, OrderSide, OrderStatus, OrderType},
provider::{
DataBasics,
data::test::data_vec_over,
kline::{BaseKline, Kline},
test::MockProvider,
},
};
const FONDS: i32 = 100000000;
const BUY_PRICE: Decimal = Decimal::from_parts(25000, 0, 0, false, 0);
const TWO_BUY_PRICE: Decimal = Decimal::from_parts(50000, 0, 0, false, 0);
const SELL_PRICE: Decimal = Decimal::from_parts(50000, 0, 0, false, 0);
#[tokio::test]
async fn limit_buy_sell() {
let (_remote, clock) = StableCheatClockFactory {
date: DateTime::from_str("2010-01-01 12:00:00Z").unwrap(),
}
.build()
.unwrap();
let remote = _remote.unwrap();
remote.not_blocking();
let mock_provider = MockProvider::new(|query| {
let value = match query.begin().day() {
1 => BUY_PRICE + Decimal::ONE,
2 => BUY_PRICE + Decimal::ONE,
3 => BUY_PRICE,
4 => SELL_PRICE - Decimal::ONE,
5 => SELL_PRICE,
_ => return Err(CoreError::param_error("Year not implemented")),
};
data_vec_over(
*query.interval(),
*query.begin(),
*query.end(),
Kline {
content: Some(BaseKline {
open: value,
high: value,
low: value,
close: value,
volume: Decimal::ONE_HUNDRED,
quote_asset_volume: Decimal::ONE_HUNDRED,
}),
basics: Default::default(),
},
"MockedProvider".to_string(),
)
});
let market =
Simulated::new("simulation-limit_buy_sell", mock_provider, clock.clone()).await;
market
.set_balance_to(&Crypto::USDT, FONDS.into())
.await
.unwrap();
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
OrderSide::Buy,
OrderType::Limit,
Some(Decimal::TWO),
None,
Some(BUY_PRICE),
)
.unwrap();
market.send_order(&order).await.unwrap();
tracing::debug!("=== 02/01 ===");
clock
.sleep_until(DateTime::from_str("2010-01-02 12:00:00Z").unwrap())
.await;
let usdt = market.get_balance(Crypto::USDT).await.unwrap();
assert_eq!(usdt.locked, TWO_BUY_PRICE);
assert_eq!(usdt.free, Decimal::from(FONDS) - TWO_BUY_PRICE);
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Open);
tracing::debug!("=== 03/01 ===");
clock
.sleep_until(DateTime::from_str("2010-01-03 12:00:00Z").unwrap())
.await;
market.cancel_order(&order).await.unwrap();
assert_eq!(
market.get_balance(Crypto::USDT).await.unwrap().locked,
Decimal::ZERO
);
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Filled);
let usdt = market.get_balance(Crypto::USDT).await.unwrap();
assert!(usdt.free >= Decimal::from(FONDS) - TWO_BUY_PRICE);
assert!(usdt.free < FONDS.into());
assert_eq!(
market.get_balance(Crypto::BTC).await.unwrap().free,
Decimal::TWO
);
}
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
OrderSide::Sell,
OrderType::Limit,
Some(Decimal::ONE),
None,
Some(SELL_PRICE),
)
.unwrap();
market.send_order(&order).await.unwrap();
tracing::debug!("=== 04/01 ===");
clock
.sleep_until(DateTime::from_str("2010-01-04 12:00:00Z").unwrap())
.await;
let btc = market.get_balance(Crypto::BTC).await.unwrap();
assert_eq!(btc.locked, Decimal::ONE);
assert_eq!(btc.free, Decimal::TWO - Decimal::ONE);
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Open);
tracing::debug!("=== 05/01 ===");
clock
.sleep_until(DateTime::from_str("2010-01-05 12:00:00Z").unwrap())
.await;
market.cancel_order(&order).await.unwrap();
assert_eq!(
market.get_balance(Crypto::USDT).await.unwrap().locked,
Decimal::ZERO
);
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Filled);
let free_usdt = market.get_balance(Crypto::USDT).await.unwrap().free;
assert!(free_usdt >= Decimal::from(FONDS) - BUY_PRICE);
assert_eq!(
market.get_balance(Crypto::BTC).await.unwrap().free,
Decimal::ONE
);
}
}
#[tokio::test]
async fn cancel() {
let (_remote, clock) = StableCheatClockFactory {
date: DateTime::from_str("2022-01-01 00:30:30Z").unwrap(),
}
.build()
.unwrap();
let remote = _remote.unwrap();
remote.not_blocking();
let mock_provider = MockProvider::new(|query| {
data_vec_over(
*query.interval(),
*query.begin(),
*query.end(),
Kline {
content: Some(BaseKline {
open: TWO_BUY_PRICE,
high: TWO_BUY_PRICE,
low: TWO_BUY_PRICE,
close: TWO_BUY_PRICE,
volume: Decimal::ONE_HUNDRED,
quote_asset_volume: Decimal::ONE_HUNDRED,
}),
basics: DataBasics::default(),
},
"MockedProvider".to_string(),
)
});
let market = Simulated::new("simulated-cancel", mock_provider, clock.clone()).await;
market
.set_balance_to(&Crypto::USDT, FONDS.into())
.await
.unwrap();
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
OrderSide::Buy,
OrderType::Limit,
Some(Decimal::ONE),
None,
Some(BUY_PRICE),
)
.unwrap();
market.send_order(&order).await.unwrap();
clock.sleep(Duration::days(3)).await;
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Open);
market.cancel_order(&order).await.unwrap();
clock.sleep(Duration::days(5)).await;
let res = market.update_order(&order).await.unwrap();
assert_eq!(res.status, OrderStatus::Canceled);
let free_usdt = market.get_balance(Crypto::USDT).await.unwrap().free;
assert_eq!(free_usdt, Decimal::from(FONDS));
assert_eq!(
market.get_balance(Crypto::BTC).await.unwrap().free,
Decimal::ZERO
);
}
#[tokio::test]
async fn market_buy_sell() {
let (_remote, clock) = StableCheatClockFactory {
date: DateTime::from_str("2022-01-01 00:30:30Z").unwrap(),
}
.build()
.unwrap();
let remote = _remote.unwrap();
remote.not_blocking();
let mock_provider = MockProvider::new(|query| {
data_vec_over(
*query.interval(),
*query.begin(),
*query.end(),
Kline {
content: Some(BaseKline {
open: BUY_PRICE,
high: BUY_PRICE,
low: BUY_PRICE,
close: BUY_PRICE,
volume: Decimal::ONE_HUNDRED,
quote_asset_volume: Decimal::ONE_HUNDRED,
}),
basics: DataBasics::default(),
},
"MockedProvider".to_string(),
)
});
let market = Simulated::new("simulation-market_buy_sell", mock_provider, clock).await;
market
.set_balance_to(&Crypto::USDT, FONDS.into())
.await
.unwrap();
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
OrderSide::Buy,
OrderType::Market,
None,
Some(TWO_BUY_PRICE),
None,
)
.unwrap();
market.send_order(&order).await.unwrap();
let free_usdt = market.get_balance(Crypto::USDT).await.unwrap().free;
let free_btc = market.get_balance(Crypto::BTC).await.unwrap().free;
assert_eq!(free_usdt, Decimal::from(FONDS) - TWO_BUY_PRICE);
assert_eq!(free_btc, Decimal::TWO);
}
{
let order = Order::new(
Crypto::BTC,
Crypto::USDT,
OrderSide::Sell,
OrderType::Market,
Some(Decimal::ONE),
None,
None,
)
.unwrap();
market.send_order(&order).await.unwrap();
let free_usdt = market.get_balance(Crypto::USDT).await.unwrap().free;
let free_btc = market.get_balance(Crypto::BTC).await.unwrap().free;
assert_eq!(free_usdt, Decimal::from(FONDS) - BUY_PRICE);
assert_eq!(free_btc, Decimal::ONE);
}
}
}