scuriolus 0.1.0

Scuriolus is a modular trading bot platform. It can apply different strategies to various markets, as described below.
Documentation
use std::fmt;

use anyhow::Error;
use derive_getters::Getters;
use rust_decimal::Decimal;
use strum_macros::Display;

use crate::trade_log;

use super::market::{mexc_base::mexc_enums, MarketKind};

/// Cryptocurrency
#[derive(Debug, Clone, Copy, Display, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum Crypto {
    /// Bitcoin
    BTC,
    /// Dogecoin
    DOGE,
    /// Ethereum
    ETH,
    /// Solana
    SOL,
    /// USD Coin
    USDC,
    /// Tether
    USDT,
    /// Xelis
    XEL,
}

/// The status of an [`Order`].
#[derive(Debug, Clone, Copy, Display, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum OrderStatus {
    Open,
    PartiallyFilled,
    PartiallyCanceled,
    Filled,
    Canceled,
    Tested,
    Failed,
}

pub type Amount = Decimal;

/// Represents the quantity of an [`Order`].
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Display)]
pub enum Quantity {
    /// The Quantity is in the asset currency.
    Asset(Amount), // in BTCUSDT: BTC
    /// The Quantity is in the quote currency.
    Quote(Amount), // in BTCUSDT: USDT
}

impl Quantity {
    pub fn get_amount(&self) -> Amount {
        match self {
            Quantity::Asset(amount) => *amount,
            Quantity::Quote(amount) => *amount,
        }
    }

    /// Returns a tuple representing the quantity in either asset or quote currency.
    ///
    /// This method extracts the value stored in a `Quantity` instance and returns it
    /// as a tuple of `(Option<Amount>, Option<Amount>)`.
    ///
    /// - If the variant is `Quantity::Asset`, it returns `(Some(amount), None)`.
    /// - If the variant is `Quantity::Quote`, it returns `(None, Some(amount))`.
    ///
    /// # Example
    /// ```rust
    /// use crate::scuriolus::order::{Quantity, Amount};
    /// let qty = Quantity::Asset(Amount::TWO);
    /// let (asset, quote) = qty.get_amounts();
    /// assert_eq!(asset, Some(Amount::TWO));
    /// assert_eq!(quote, None);
    /// ```
    ///
    /// # Returns
    /// A tuple where:
    /// - The first element is `Some(amount)` if it's an asset quantity, otherwise `None`.
    /// - The second element is `Some(amount)` if it's a quote quantity, otherwise `None`.
    pub fn get_amounts(&self) -> (Option<Amount>, Option<Amount>) {
        match self {
            Quantity::Asset(amount) => (Some(*amount), None),
            Quantity::Quote(amount) => (None, Some(*amount)),
        }
    }
}

/// Represents a market order.
#[derive(Debug, Clone, Getters, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct Order {
    #[getter(skip)]
    _id: String, // "id" cannot be used because of surrealDB read system
    market: MarketKind,
    market_id: Option<String>,
    asset: Crypto,
    quote: Crypto,
    side: mexc_enums::OrderSide,
    order_type: mexc_enums::OrderType,
    status: OrderStatus,
    quantity: Quantity,
    price: Option<Amount>,
    executed_qty: Amount,
    cummulative_quote_qty: Amount,
}

impl Order {
    pub fn new(
        buy: Crypto,
        with: Crypto,
        side: mexc_enums::OrderSide,
        order_type: mexc_enums::OrderType,
        quantity: Quantity,
        price: Option<Amount>,
    ) -> Result<Self, Error> {
        match order_type {
            mexc_enums::OrderType::Limit => {
                if price.is_none() {
                    return Err(Error::msg("Limit order should have a price"));
                }
            }
            mexc_enums::OrderType::Market => {
                if price.is_some() {
                    return Err(Error::msg("Market order should not have a price"));
                }
            }
            mexc_enums::OrderType::LimitMaker
            | mexc_enums::OrderType::ImmediateOrCancel
            | mexc_enums::OrderType::FillOrKill => {
                return Err(Error::msg("Unimplemented order type"))
            }
        }

        if quantity.get_amount() <= Amount::from(0) {
            return Err(Error::msg("Amount should be greater than 0"));
        }

        if price.is_some() && price.unwrap() <= Amount::from(0) {
            return Err(Error::msg("Price should be greater than 0"));
        }

        Ok(Order {
            asset: buy,
            quote: with,
            side,
            order_type,
            quantity,
            price,
            market: MarketKind::None,
            status: OrderStatus::Open,
            _id: uuid::Uuid::new_v4().to_string(),
            market_id: None,
            executed_qty: Amount::from(0),
            cummulative_quote_qty: Amount::from(0),
        })
    }

    pub fn id(&self) -> &String {
        &self._id
    }

    pub fn set_status(&mut self, status: OrderStatus) {
        self.status = status;
        trade_log!("Update on order {}: status now {}", self._id, status);
    }

    pub fn set_executed(&mut self, executed_qty: Amount, cummulative_quote_qty: Amount) {
        self.executed_qty = executed_qty;
        self.cummulative_quote_qty = cummulative_quote_qty;
        tracing::debug!(
            "Order {} executed: {} - {}",
            self._id,
            executed_qty,
            cummulative_quote_qty
        );
    }

    pub fn set_asset_quantity(&mut self, asset_quantity: Amount) -> Result<(), Error> {
        if self.order_type != mexc_enums::OrderType::Market
            && self.side != mexc_enums::OrderSide::Buy
        {
            return Err(Error::msg(
                "Asset quantity can only be modified for buy market orders",
            ));
        }
        self.quantity = Quantity::Asset(asset_quantity);
        Ok(())
    }

    pub fn set_market_id(&mut self, id: String) {
        self.market_id = Some(id);
    }

    pub fn quote_quantity(&self) -> Result<Amount, Error> {
        match self.quantity {
            Quantity::Asset(amount) => {
                Ok(self.price.ok_or_else(|| Error::msg("Price not set"))? * amount)
            }
            Quantity::Quote(amount) => Ok(amount),
        }
    }

    pub fn is_done(&self) -> bool {
        matches!(
            self.status,
            OrderStatus::Filled
                | OrderStatus::Canceled
                | OrderStatus::PartiallyCanceled
                | OrderStatus::Tested
                | OrderStatus::Failed
        )
    }

    pub fn set_price(&mut self, price: Amount) -> Result<(), Error> {
        if self.order_type != mexc_enums::OrderType::Market && self.status != OrderStatus::Filled {
            return Err(Error::msg(
                "Price can only be modified except for filled market orders",
            ));
        }

        self.price = Some(price);
        Ok(())
    }
}

impl fmt::Display for Order {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "Buy: {}, With: {}, Side: {:?}, Type: {:?},Amount: {}, Price: {:?}, Market: {}, Status: {}",
            self.asset, self.quote, self.side, self.order_type, self.quantity, self.price, self.market, self.status
        )
    }
}

/// An order draft to be sent to the market.
#[derive(Debug, Clone, Copy)]
pub struct OrderDraft {
    pub asset: Crypto,
    pub currency: Crypto,
    pub side: mexc_enums::OrderSide,
    pub order_type: mexc_enums::OrderType,
    pub amount: Quantity,
    pub price: Option<Amount>,
}