scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
use surrealdb::{
    Surreal,
    engine::local::{Db, RocksDb},
};
use tracing::{error, trace};

use super::core_error::{CoreError, CoreResult};
use crate::generics::order::{Order, SpecificOrderDetails};

const DB_NAMESPACE: &str = "scuriolus";
const DB_NAME: &str = "main";
#[cfg(not(test))]
const DB_ADDRESS_BASE: &str = "db/storage";
#[cfg(test)]
const DB_ADDRESS_BASE: &str = "db/test-storage";

const TABLE_ORDRES: &str = "orders";

#[derive(Debug)]
pub struct Storage {
    name: String,
    db: Surreal<Db>,
}

impl Storage {
    pub async fn new(name: &str) -> CoreResult<Self> {
        let db = Surreal::new::<RocksDb>(format!("{DB_ADDRESS_BASE}/{name}")).await?;
        db.use_ns(DB_NAMESPACE).use_db(DB_NAME).await?;

        Ok(Storage {
            name: name.to_string(),
            db,
        })
    }

    pub fn get_name(&self) -> &str {
        &self.name
    }

    pub async fn add_order<D: SpecificOrderDetails>(&self, order: Order<D>) -> CoreResult<()> {
        tracing::debug!("Adding order {} to storage", order.id());
        let previous: Option<Order<D>> = self.db.select((TABLE_ORDRES, order.id())).await?;
        if previous.is_some() {
            error!("Add order: {} already exists", order.id());
            trace!("Previous: {:#?}", previous.unwrap());
            trace!("Challenger: {:#?}", order);
            return Err(CoreError::param_error("id already occupied"));
        }

        let _: Option<Order<D>> = self
            .db
            .create((TABLE_ORDRES, order.id()))
            .content(order)
            .await?;

        Ok(())
    }

    pub async fn get_order<D: SpecificOrderDetails>(&self, id: &str) -> Option<Order<D>> {
        self.db.select((TABLE_ORDRES, id)).await.unwrap()
    }

    pub async fn update_order<D: SpecificOrderDetails>(&self, order: Order<D>) -> CoreResult<()> {
        let previous: Option<Order<D>> = self.db.select((TABLE_ORDRES, order.id())).await?;
        if previous.is_none() {
            error!("Update order: {} not found", order.id());
            trace!("Order: {:#?}", order);
            return Err(CoreError::param_error("order not found"));
        }

        let _updated: Option<Order<D>> = self
            .db
            .upsert((TABLE_ORDRES, order.id()))
            .content(order)
            .await?;
        Ok(())
    }

    pub async fn opened_orders<D: SpecificOrderDetails>(&self) -> Vec<Order<D>> {
        let query = "SELECT * from orders WHERE status IN ['Open', 'PartiallyFilled']";
        let mut res = self.db.query(query).await.unwrap();
        let orders: Vec<Order<D>> = res.take(0).expect("Corrupted DataBase");
        orders
    }

    #[cfg(test)]
    pub async fn clear<D: SpecificOrderDetails>(&self) -> CoreResult<()> {
        let _: Vec<Order<D>> = self.db.delete(TABLE_ORDRES).await?;
        Ok(())
    }
}
#[cfg(test)]
mod tests {

    use rust_decimal::Decimal;

    use crate::generics::order::{
        Crypto, EmptySpecificOrderDetails, OrderSide, OrderStatus, OrderType,
    };

    use super::*;

    #[tokio::test]
    async fn orders_basics() {
        let data = Storage::new("data-order_basic").await.unwrap();

        data.clear::<EmptySpecificOrderDetails>().await.unwrap();

        let mut order = Order::<EmptySpecificOrderDetails>::new(
            Crypto::BTC,
            Crypto::USDT,
            OrderSide::Buy,
            OrderType::Limit,
            Some(Decimal::ONE),
            None,
            Some(Decimal::from(10000)),
        )
        .unwrap();

        let mut order2 = Order::<EmptySpecificOrderDetails>::new(
            Crypto::BTC,
            Crypto::USDT,
            OrderSide::Buy,
            OrderType::Market,
            Some(Decimal::ONE),
            None,
            None,
        )
        .unwrap();

        data.add_order(order.clone()).await.unwrap();
        data.add_order(order2.clone()).await.unwrap();

        let recup = data.get_order(order.id()).await.unwrap();
        assert_eq!(recup, order);

        order = order.with_status(OrderStatus::Tested);
        data.update_order(order.clone()).await.unwrap();

        let recup = data.get_order(order.id()).await.unwrap();
        assert_eq!(recup, order);

        order2 = order2
            .with_status(OrderStatus::Tested)
            .with_price(Some(Decimal::from(20000)));
        data.update_order(order2.clone()).await.unwrap();

        let recup2 = data.get_order(order2.id()).await.unwrap();
        assert_eq!(recup2, order2);
    }

    #[tokio::test]
    async fn orders_get_opens() {
        let data = Storage::new("data-orders_get_opens").await.unwrap();

        data.clear::<EmptySpecificOrderDetails>().await.unwrap();

        let order_open = Order::<EmptySpecificOrderDetails>::new(
            Crypto::BTC,
            Crypto::USDT,
            OrderSide::Buy,
            OrderType::Limit,
            Some(Decimal::ONE),
            None,
            Some(Decimal::from(10000)),
        )
        .unwrap();

        let mut order_part = Order::<EmptySpecificOrderDetails>::new(
            Crypto::BTC,
            Crypto::USDT,
            OrderSide::Buy,
            OrderType::Limit,
            Some(Decimal::ONE),
            None,
            Some(Decimal::from(10000)),
        )
        .unwrap();

        order_part = order_part.with_status(OrderStatus::PartiallyFilled);

        let mut order_executed = Order::<EmptySpecificOrderDetails>::new(
            Crypto::BTC,
            Crypto::USDT,
            OrderSide::Buy,
            OrderType::Market,
            Some(Decimal::ONE),
            None,
            None,
        )
        .unwrap();

        order_executed = order_executed.with_status(OrderStatus::Filled);

        tracing::debug!("Order open: {:?}", order_open);

        data.add_order(order_open).await.unwrap();
        data.add_order(order_part).await.unwrap();
        data.add_order(order_executed).await.unwrap();

        let open_orders = data.opened_orders::<EmptySpecificOrderDetails>().await;
        assert_eq!(open_orders.len(), 2);
    }
}