scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
use std::fmt::{self, Display};

use derive_getters::Getters;
#[cfg(test)]
use derive_setters::Setters;
use rust_decimal::Decimal;
use strum_macros::Display;

use crate::{core::CoreResult, trade_log};

/// An error related to an [`Order`].
#[derive(Debug, thiserror::Error)]
#[error("Order error: {0}")]
pub struct OrderError(pub String);

impl OrderError {
    pub fn new(msg: &str) -> Self {
        OrderError(msg.to_string())
    }
}

/// Cryptocurrency
#[derive(
    Debug, Clone, Copy, Display, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash,
)]
#[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, Eq)]
#[serde(rename_all = "PascalCase")]
pub enum OrderStatus {
    /// The order is open
    Open,

    /// The order is partially filled
    PartiallyFilled,

    /// The order is partially canceled
    PartiallyCanceled,

    /// The order is filled
    Filled,

    /// The order is canceled
    Canceled,

    /// The order is a test and has not been executed
    Tested,

    /// The order failed
    Failed,
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)]
pub enum OrderType {
    /// Limit order
    Limit,

    /// Market order
    Market,

    /// Limit maker order
    LimitMaker,

    /// Immediate or cancel order
    ImmediateOrCancel,

    /// Fill or kill order
    FillOrKill,
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)]
pub enum OrderSide {
    /// Buy
    Buy,

    /// Sell
    Sell,
}

/// A trait for specific details of an [`Order`], depending on a market
pub trait SpecificOrderDetails:
    Clone
    + Default
    + fmt::Debug
    + Display
    + serde::Serialize
    + PartialEq
    + 'static
    + Send
    + Sync
    + for<'de> serde::Deserialize<'de>
{
    const NAME: &'static str;

    fn try_update(&mut self, item: Self) -> CoreResult<()>;
}

/// Empty [`SpecificOrderDetails`], for testing mainly.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct EmptySpecificOrderDetails {
    content: Option<()>,
}

impl SpecificOrderDetails for EmptySpecificOrderDetails {
    const NAME: &'static str = "EmptySpecificOrderDetails";

    fn try_update(&mut self, _: Self) -> CoreResult<()> {
        Ok(())
    }
}

impl Display for EmptySpecificOrderDetails {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "<EmptySpecificOrderDetails>")
    }
}

pub const DEFAULT_EMPTY_DETAILS: EmptySpecificOrderDetails =
    EmptySpecificOrderDetails { content: None };

/// Represents a market order.

#[derive(Debug, Clone, Getters, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(test, derive(Setters))]
#[cfg_attr(test, setters(prefix = "with_"))]
#[serde(bound = "D: SpecificOrderDetails")]
pub struct Order<D: SpecificOrderDetails> {
    #[getter(skip)]
    #[cfg_attr(test, setters(skip))]
    _id: String, // "id" cannot be used because of surrealDB read system

    asset: Crypto,
    quote: Crypto,
    side: OrderSide,
    order_type: OrderType,
    status: OrderStatus,
    qty_asset: Option<Decimal>,
    qty_quote: Option<Decimal>,
    price: Option<Decimal>,
    executed_qty_asset: Decimal,
    executed_qty_quote: Decimal,
    details: D,
}

impl<D: SpecificOrderDetails> Order<D> {
    pub fn new(
        asset: Crypto,
        quote: Crypto,
        side: OrderSide,
        order_type: OrderType,
        qty_asset: Option<Decimal>,
        qty_quote: Option<Decimal>,
        price: Option<Decimal>,
    ) -> Result<Self, OrderError> {
        //TODO test here or in Core ??
        match order_type {
            OrderType::Limit => {
                if price.is_none() {
                    return Err(OrderError::new("Limit order should have a price"));
                }
            }
            OrderType::Market => {
                if price.is_some() {
                    return Err(OrderError::new("Market order should not have a price"));
                }
            }
            OrderType::LimitMaker | OrderType::ImmediateOrCancel | OrderType::FillOrKill => {
                return Err(OrderError::new("Unimplemented order type"));
            }
        }

        // if let Some(quantity) = quantity_asset && quantity <= Decimal::from(0) {
        //     return Err(OrderError::new("Amount should be greater than 0"));
        // }

        // if let Some(_price) = price && _price <= Decimal::from(0) {
        //     return Err(OrderError::new("Price should be greater than 0"));
        // }

        tracing::trace!(
            "Creating order: asset: {}, quote: {}, side: {:?}, type: {:?}, quantity_asset: {:?}, quantity_quote: {:?}, price: {:?}",
            asset,
            quote,
            side,
            order_type,
            qty_asset,
            qty_quote,
            price
        );

        Ok(Self {
            asset,
            quote,
            side,
            order_type,
            qty_asset,
            qty_quote,
            price,
            status: OrderStatus::Open,
            _id: uuid::Uuid::new_v4().to_string(),
            executed_qty_asset: Decimal::from(0),
            executed_qty_quote: Decimal::from(0),
            details: D::default(),
        })
    }

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

    pub fn update(&mut self, update: UpdateOrderOutput<D>) -> CoreResult<bool> {
        if self.status == update.status
            && self.executed_qty_asset == update.executed_qty_asset
            && self.executed_qty_quote == update.executed_qty_quote
            && self.details == update.details
        {
            return Ok(false);
        }

        self.status = update.status;
        self.executed_qty_asset = update.executed_qty_asset;
        self.executed_qty_quote = update.executed_qty_quote;
        self.details.try_update(update.details)?;

        trade_log!(
            "Order updated: id: {}, status: {:?}, executed_asset: {}, executed_quote: {}, details: {}",
            self.id(),
            self.status,
            self.executed_qty_asset,
            self.executed_qty_quote,
            self.details
        );

        Ok(true)
    }

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

    pub fn symbol(&self) -> String {
        format!("{}{}", self.asset(), self.quote())
    }
}

/// An order draft to be sent to the market.
#[derive(Debug, Clone)]
pub struct OrderDraft {
    pub asset: Crypto,
    pub quote: Crypto,
    pub side: OrderSide,
    pub order_type: OrderType,
    pub qty_asset: Option<Decimal>,
    pub qty_quote: Option<Decimal>,
    pub price: Option<Decimal>,
}

/// The output of an order update.
#[derive(Debug, Clone)]
pub struct UpdateOrderOutput<OrderDetails: SpecificOrderDetails> {
    pub status: OrderStatus,
    pub executed_qty_asset: Decimal,
    pub executed_qty_quote: Decimal,
    pub details: OrderDetails,
}

impl<D: SpecificOrderDetails> From<&Order<D>> for UpdateOrderOutput<D> {
    fn from(order: &Order<D>) -> Self {
        Self {
            status: order.status,
            executed_qty_asset: order.executed_qty_asset,
            executed_qty_quote: order.executed_qty_quote,
            details: order.details.clone(),
        }
    }
}