scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
mod core_error;
pub mod market;
mod storage;

pub use core_error::{CoreError, CoreResult};
use market::{Balance, Market};

#[cfg(test)]
use crate::generics::order::SpecificOrderDetails;
use crate::{
    clock::Clock,
    core::{market::MarketFactory, storage::Storage},
    generics::order::{Crypto, Order, OrderDraft, OrderStatus, UpdateOrderOutput},
};

/// An interface with the market and different data sources.
pub struct Core<C: Clock, M: Market<_Clock = C>> {
    market: M,
    storage: Storage,
    clock: C,
}

impl<C: Clock, M: Market<_Clock = C>> Core<C, M> {
    pub async fn new<MF: MarketFactory<_Market = M>>(
        name: String,
        market_factory: MF,
        clock: C,
    ) -> CoreResult<Self> {
        let storage = Storage::new(&name).await?;
        let market = market_factory.with_clock(clock.clone()).build().await?;
        Ok(Self {
            market,
            storage,
            clock,
        })
    }

    /// Send an order to the market and save it to the database.
    ///
    /// First, it creates an `Order` from the given `OrderDraft` and sends it to the market.
    /// If the sending is successful, it sets the market's ID of the order and saves the order
    /// to the database. If the sending fails, it sets the status of the order to `Failed` and logs
    /// the error.
    ///
    /// # Arguments
    ///
    /// * `draft` - The draft of the order to be sent.
    ///
    /// | Order Type | Side | `price`     | `quantity::Asset` | `quantity::Quote`| Notes                         |
    /// | ---------- | ---- | ----------- | ----------------- | ---------------- | ----------------------------- |
    /// | Limit      | Buy  | ✅ required | ✅ required      | ❌ not allowed   | Buy at a specific price.      |
    /// | Limit      | Sell | ✅ required | ✅ required      | ❌ not allowed   | Sell at a specific price.     |
    /// | Market     | Buy  | ❌ ignored  | ❌ not allowed   | ✅ required      | Buy for a given quote amount. |
    /// | Market     | Sell | ❌ ignored  | ✅ required      | ❌ not allowed   | Sell a given asset quantity.  |
    ///
    /// # Returns
    ///
    /// * `Result<ORder, Error>` - On success, returns the order. On failure, returns an `Error`.
    pub async fn send_order(&self, draft: OrderDraft) -> CoreResult<Order<M::_OrderDetails>> {
        let mut order: Order<M::_OrderDetails> = Order::new(
            draft.asset,
            draft.quote,
            draft.side,
            draft.order_type,
            draft.qty_asset,
            draft.qty_quote,
            draft.price,
        )?;

        tracing::info!(
            "{} sending order {}, {:?} {} at {}",
            self.storage.get_name(),
            order.id(),
            order.side(),
            order.asset(),
            self.clock.now()
        );

        match self.market.send_order(&order).await {
            Ok(update) => {
                tracing::debug!("Order {} sent successfully", order.id());
                order.update(update)?;
                self.storage.add_order(order.clone()).await?;

                //TODO if order at market price : update soon
                Ok(order)
            }
            Err(e) => {
                let update = UpdateOrderOutput {
                    status: OrderStatus::Failed,
                    executed_qty_asset: *order.executed_qty_asset(),
                    executed_qty_quote: *order.executed_qty_quote(),
                    details: order.details().clone(),
                };

                tracing::warn!("Failed to send order {} : {}", order.id(), e);
                order.update(update)?;
                self.storage.add_order(order).await?;

                Err(e)
            }
        }
    }

    pub fn clock(&self) -> &C {
        &self.clock
    }

    /// Get the list of opened orders on the market and update their status with the information from the market.
    ///
    /// # Returns
    ///
    /// Returns the list of updated orders.
    pub async fn update_opened_orders(&self) -> CoreResult<Vec<Order<M::_OrderDetails>>> {
        tracing::debug!("Updating opened orders");
        let mut orders: Vec<Order<M::_OrderDetails>> = self.storage.opened_orders().await;
        for order in &mut orders {
            tracing::trace!("Updating order {}", order.id());
            self.update_order(order).await?;
        }
        Ok(orders)
    }

    /// Cancel all opened orders on the market and update their status accordingly.
    ///
    /// # Returns
    ///
    /// Returns the list of orders with their updated statuses after being canceled.
    pub async fn cancel_opened_orders(&self) -> CoreResult<Vec<Order<M::_OrderDetails>>> {
        let mut orders: Vec<Order<M::_OrderDetails>> = self.storage.opened_orders().await;
        for order in &mut orders {
            self.cancel_order(order).await?;
        }
        Ok(orders)
    }

    /// Get an order by its id from the database.
    ///
    /// # Arguments
    ///
    /// * `id` - The id of the order to retrieve.
    ///
    /// # Returns
    ///
    /// Returns the order with the given id if it exists
    pub async fn get_order(&self, id: &str) -> Option<Order<M::_OrderDetails>> {
        self.storage.get_order(id).await
    }

    /// Returns the list of opened orders from the database.
    ///
    /// # Returns
    ///
    /// Returns a vector of opened orders.
    pub async fn common_get_opened_orders(&self) -> Vec<Order<M::_OrderDetails>> {
        self.storage.opened_orders().await
    }

    /// Updates an order's status and details based on the market's response.
    ///
    /// This function sends an update request for the specified order to the market. If the order's
    /// status or details have changed after the update, it updates the local order accordingly and saves the changes in the
    /// database.
    ///
    /// # Arguments
    ///
    /// * `order` - A mutable reference to the `Order` object to be updated.
    ///
    /// # Returns
    ///
    /// Returns `Ok(true)` if the order was updated
    pub async fn update_order(&self, order: &mut Order<M::_OrderDetails>) -> CoreResult<bool> {
        tracing::debug!("Updating order {}", order.id());
        let update = self.market.update_order(order).await?;

        if order.update(update)? {
            self.storage.update_order(order.clone()).await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Cancels an order and updates its status and details based on the market's response.
    ///
    /// This function sends a cancellation request for the specified order to the market. If the order's
    /// status or details have changed after the cancellation, it updates the local order accordingly and saves the changes in the
    /// database.
    ///
    /// # Arguments
    ///
    /// * `order` - A mutable reference to the `Order` object to be canceled.
    ///
    /// # Returns
    ///
    /// Returns `Ok(true)` if the order was updated
    pub async fn cancel_order(&self, order: &mut Order<M::_OrderDetails>) -> CoreResult<bool> {
        tracing::debug!("Canceling order {}", order.id());
        let update = self.market.cancel_order(order).await?;

        if order.update(update)? {
            self.storage.update_order(order.clone()).await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Get the current balance of a crypto on the market.
    ///
    /// # Arguments
    ///
    /// * `crypto`: The crypto to get the balance of.
    ///
    /// # Returns
    ///
    /// Returns the balance of the given crypto.
    pub async fn get_balance(&self, crypto: Crypto) -> CoreResult<Balance> {
        self.market.get_balance(crypto).await
    }

    #[cfg(test)]
    pub async fn clear_storage<D: SpecificOrderDetails>(&self) -> CoreResult<()> {
        self.storage.clear::<D>().await
    }
}

#[cfg(test)]
mod tests {

    use std::future::ready;

    use chrono::Utc;
    use rust_decimal::Decimal;

    use crate::{
        clock::{ClockFactory as _, RunningCheatClockFactory},
        core::{
            Core,
            market::{MockMarket, MockMarketFactory},
        },
        generics::order::{
            Crypto, DEFAULT_EMPTY_DETAILS, EmptySpecificOrderDetails, OrderDraft, OrderSide,
            OrderStatus, OrderType, UpdateOrderOutput,
        },
    };

    const ASSET: Crypto = Crypto::BTC;
    const QUOTE: Crypto = Crypto::USDT;

    const DEFAULT_UPDATE: UpdateOrderOutput<EmptySpecificOrderDetails> = UpdateOrderOutput {
        executed_qty_asset: Decimal::ZERO,
        executed_qty_quote: Decimal::ZERO,
        status: OrderStatus::Open,
        details: DEFAULT_EMPTY_DETAILS,
    };

    #[tokio::test]
    async fn reload_and_update_orders() {
        let name = "core-reload_and_update_orders".to_string();

        let mut market1 = MockMarket::new();

        market1
            .expect_send_order()
            .returning(|_| Box::pin(ready(Ok(DEFAULT_UPDATE))));

        let mock_market_factory_1 = MockMarketFactory {
            mock_market: market1,
        };

        let mut market2 = MockMarket::new();

        market2
            .expect_update_order()
            .returning(|_| Box::pin(ready(Ok(DEFAULT_UPDATE))));

        let mock_market_factory_2 = MockMarketFactory {
            mock_market: market2,
        };

        let clock = RunningCheatClockFactory { date: Utc::now() }
            .build()
            .unwrap()
            .1;

        let order = {
            let core = Core::new(name.clone(), mock_market_factory_1, clock.clone())
                .await
                .unwrap();
            core.clear_storage::<EmptySpecificOrderDetails>()
                .await
                .unwrap();

            let order = core
                .send_order(OrderDraft {
                    asset: ASSET,
                    quote: QUOTE,
                    side: OrderSide::Buy,
                    order_type: OrderType::Limit,
                    qty_asset: Some(Decimal::ONE),
                    qty_quote: None,
                    price: Some(Decimal::from(10000)),
                })
                .await
                .unwrap();

            drop(core);
            order
        };

        // wait for db to fully close
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;

        let core = Core::new(name.clone(), mock_market_factory_2, clock.clone())
            .await
            .unwrap();

        let updated_orders = core.update_opened_orders().await.unwrap();

        assert_eq!(updated_orders.len(), 1);
        assert_eq!(core.get_order(order.id()).await.unwrap(), updated_orders[0]);
    }

    #[tokio::test]
    async fn cancel_orders() {
        let name = "core-cancel_orders".to_string();
        let mut market = MockMarket::new();
        market
            .expect_send_order()
            .returning(|_| Box::pin(ready(Ok(DEFAULT_UPDATE))));

        market.expect_cancel_order().returning(|_| {
            Box::pin(ready(Ok(UpdateOrderOutput {
                status: OrderStatus::Canceled,
                ..DEFAULT_UPDATE
            })))
        });

        let mock_market_factory = MockMarketFactory {
            mock_market: market,
        };

        let clock = RunningCheatClockFactory { date: Utc::now() }
            .build()
            .unwrap()
            .1;
        let core = Core::new(name, mock_market_factory, clock).await.unwrap();
        core.clear_storage::<EmptySpecificOrderDetails>()
            .await
            .unwrap();

        let mut order = core
            .send_order(OrderDraft {
                asset: ASSET,
                quote: QUOTE,
                side: OrderSide::Buy,
                order_type: OrderType::Limit,
                qty_asset: Some(Decimal::ONE),
                qty_quote: None,
                price: Some(Decimal::from(10000)),
            })
            .await
            .unwrap();

        let canceled_orders = core.cancel_opened_orders().await.unwrap();

        assert_eq!(canceled_orders.len(), 1);
        order = order.with_status(OrderStatus::Canceled);
        assert_eq!(order, canceled_orders[0]);
    }
}