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 mexc_rs::spot::v3::account_information::AccountInformationEndpoint;
use mexc_rs::spot::v3::cancel_order::{CancelOrderEndpoint, CancelOrderOutput, CancelOrderParams};
use mexc_rs::spot::v3::klines::Kline;
use mexc_rs::spot::v3::order::{OrderEndpoint, OrderParams};
use mexc_rs::spot::v3::ping::PingEndpoint;
use mexc_rs::spot::v3::query_order::{QueryOrderEndpoint, QueryOrderOutput, QueryOrderParams};
use mexc_rs::spot::{MexcSpotApiClient, MexcSpotApiClientWithAuthentication, MexcSpotApiEndpoint};
use tracing::{debug, trace};

use super::super::{
    super::clock::{Clock, TrueClock},
    Balance,
};
use super::{Crypto, KlinesParams, Market, MarketError, MexcBase, MexcSymbol as _, Order};

/// API to real **Mexc** website.
pub struct Mexc {
    client: MexcSpotApiClient,
    client_auth: MexcSpotApiClientWithAuthentication,
}

impl Mexc {
    pub async fn new() -> Self {
        let client = MexcSpotApiClient::new(MexcSpotApiEndpoint::Base);

        client.ping().await.expect("API injoignable");

        let api_key = std::env::var("MEXC_API_KEY").expect("MEXC_API_KEY not set");
        let secret_key = std::env::var("MEXC_SECRET_KEY").expect("MEXC_SECRET_KEY not set");

        let client_auth = MexcSpotApiClientWithAuthentication::new(
            MexcSpotApiEndpoint::Base,
            api_key,
            secret_key,
        );
        client_auth.ping().await.expect("Connexion impossible");

        trace!("Mymexc initialised");

        Mexc {
            client,
            client_auth,
        }
    }
}

impl fmt::Debug for Mexc {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Mexc")
            .field("client", &"local client")
            .field("client_auth", &"local client auth")
            .finish()
    }
}

impl MexcBase for Mexc {
    fn client(&self) -> &MexcSpotApiClient {
        &self.client
    }
}

impl Market for Mexc {
    async fn klines(&self, params: KlinesParams) -> Result<Vec<Kline>, MarketError> {
        self.base_klines(&params).await
    }

    async fn get_balance(&self, crypto: Crypto) -> Result<Balance, MarketError> {
        let account_info = self.client_auth.account_information().await?;
        let mexc_balance = account_info
            .balances
            .iter()
            .find(|balance| balance.asset == crypto.to_string())
            .expect("Balance not found");
        Ok(Balance {
            free: mexc_balance.free,
            locked: mexc_balance.locked,
        })
    }

    /// Sends an order to the Mexc exchange using the authenticated client.
    ///
    /// # Arguments
    ///
    /// * `order` - A reference to an `Order` struct containing details 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<String, Error>` - On success, returns the order ID as a `String`. On failure, returns an `Error`.
    ///
    /// # Remarks
    ///
    /// This function constructs an `OrderParams` object from the provided `Order` and sends it to the Mexc exchange.
    /// It requires the client to be authenticated with the necessary API keys.
    async fn send_order(&self, order: &Order) -> Result<String, MarketError> {
        if std::env::var("SERIOUS").unwrap_or("false".to_string()) != "true" {
            Err(MarketError::SafetyError)
        } else {
            let (quantity, quote_order_quantity) = order.quantity().get_amounts();

            let params = OrderParams {
                symbol: &format!("{}{}", order.asset(), order.quote()),
                side: *order.side(),
                order_type: *order.order_type(),
                quantity,
                price: *order.price(),
                quote_order_quantity,
                new_client_order_id: None,
            };
            debug!("Order to Mexc, [{:#?}]", params);

            Ok(self.client_auth.order(params).await?.order_id)
        }
    }

    async fn cancel_order(&self, order: &Order) -> Result<CancelOrderOutput, MarketError> {
        Ok(self
            .client_auth
            .cancel_order(CancelOrderParams {
                symbol: &format!("{}{}", order.asset(), order.quote()),
                order_id: Some(&order.market_id().clone().expect("Order ID should be set")),
                new_client_order_id: None,
                original_client_order_id: None,
            })
            .await?)
    }

    async fn update_order(&self, order: &Order) -> Result<QueryOrderOutput, MarketError> {
        Ok(self
            .client_auth
            .query_order(QueryOrderParams {
                symbol: &order.symbol(),
                order_id: Some(&order.market_id().clone().expect("Order ID should be set")),
                original_client_order_id: None,
            })
            .await?)
    }

    fn clock(&self) -> Box<dyn Clock> {
        Box::new(TrueClock::new())
    }
}