scuriolus 0.2.0

Scuriolus is a modular trading bot platform.
Documentation
#[cfg(feature = "mexc")]
pub mod mexc;
pub mod simulated;

use chrono::{DateTime, Utc};
use derive_getters::Getters;
#[cfg(test)]
use mockall::automock;
use rust_decimal::Decimal;
use std::fmt;
use std::future::Future;

use crate::clock::Clock;
#[cfg(test)]
use crate::clock::RunningCheatClock;
#[cfg(test)]
use crate::core::CoreError;
use crate::core::CoreResult;
use crate::data::{Crypto, KlineInterval, Order, SpecificOrderDetails};
#[cfg(test)]
use crate::market::__mock_MockMarket_Market::__source_name::Context;

pub use mexc_rs::spot::v3::{
    cancel_order::CancelOrderOutput, enums as mexc_enums, query_order::QueryOrderOutput,
};

/// Cryptocurrency balance
#[derive(Debug, Clone, Copy)]
pub struct Balance {
    pub free: Decimal,
    pub locked: Decimal,
}

/// [`Kline`] params
#[derive(Debug, Clone, Getters, PartialEq)]
pub struct KlinesParams {
    asset: Crypto,
    quote: Crypto,
    interval: KlineInterval,
    start_time: DateTime<Utc>,
    end_time: DateTime<Utc>,
}

impl KlinesParams {
    pub fn new(
        asset: Crypto,
        quote: Crypto,
        interval: KlineInterval,
        start_time: DateTime<Utc>,
        end_time: DateTime<Utc>,
    ) -> Self {
        let (below_start, _) = interval.get_time_bounds(start_time).unwrap();
        let (below_end, above_end) = interval.get_time_bounds(end_time).unwrap();

        let final_end = if below_end == end_time {
            end_time
        } else {
            above_end
        };

        Self {
            asset,
            quote,
            interval,
            start_time: below_start,
            end_time: final_end,
        }
    }
}

/// [`Kline`]'s data
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
pub struct Kline {
    #[serde(with = "chrono::serde::ts_seconds")]
    pub open_time: DateTime<Utc>,
    pub open: Decimal,
    pub high: Decimal,
    pub low: Decimal,
    pub close: Decimal,
    pub volume: Decimal,
    #[serde(with = "chrono::serde::ts_seconds")]
    pub close_time: DateTime<Utc>,
    pub quote_asset_volume: Decimal,
}

impl Kline {
    #[cfg(test)]
    pub fn vec_over(
        interval: KlineInterval,
        mut start: DateTime<Utc>,
        end: DateTime<Utc>,
        kline_model: Kline,
    ) -> CoreResult<Vec<Kline>> {
        if interval > KlineInterval::OneDay {
            return Err(CoreError::param_error("interval not implemented"));
        }

        let mut vec = vec![];

        loop {
            let next_end = start + interval.time_delta();

            if next_end > end {
                break;
            }

            let kline = Kline {
                open_time: start,
                close_time: next_end,
                ..kline_model
            };

            vec.push(kline);
            start = next_end;
        }

        Ok(vec)
    }
}

pub trait MarketFactory: Send + Sync + 'static {
    type _Market: Market;
    fn with_clock(self, clock: <Self::_Market as Market>::_Clock) -> Self;
    fn build(self) -> impl Future<Output = CoreResult<Self::_Market>> + Send;
}

//TODO separate data source and exchange platform, both are mixed in Market
#[cfg_attr(test, automock(type _Clock = RunningCheatClock; type _OrderDetails = crate::data::EmptySpecificOrderDetails;))]
pub trait Market: Send + Sync + fmt::Debug + 'static {
    type _Clock: Clock;
    type _OrderDetails: SpecificOrderDetails;
    fn source_name() -> String;
    fn klines_limit(&self) -> i32;
    // doc => maximum of klines starting from `params.start_time`
    fn klines(&self, params: KlinesParams) -> impl Future<Output = CoreResult<Vec<Kline>>> + Send;
    fn get_balance(&self, crypto: Crypto) -> impl Future<Output = CoreResult<Balance>> + Send;
    fn send_order(
        &self,
        order: &mut Order<Self::_OrderDetails>,
    ) -> impl Future<Output = CoreResult<()>> + Send;
    fn cancel_order(
        &self,
        order: &Order<Self::_OrderDetails>,
    ) -> impl Future<Output = CoreResult<CancelOrderOutput>> + Send;
    fn update_order(
        &self,
        order: &Order<Self::_OrderDetails>,
    ) -> impl Future<Output = CoreResult<QueryOrderOutput>> + Send;
}

#[cfg(test)]
pub fn mock_context() -> Context {
    let ctx = MockMarket::source_name_context();
    ctx.expect().returning(|| "mock_market".to_string());
    ctx
}

#[cfg(test)]
mod tests {
    use chrono::{TimeZone as _, Utc};

    use crate::data::{Crypto, KlineInterval};

    use super::KlinesParams;

    #[test]
    fn create_klines_params() {
        let params = KlinesParams::new(
            Crypto::BTC,
            Crypto::USDT,
            KlineInterval::OneMinute,
            Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 59).unwrap(),
            Utc.with_ymd_and_hms(2022, 1, 1, 0, 1, 1).unwrap(),
        );

        assert_eq!(params.asset, Crypto::BTC);
        assert_eq!(params.quote, Crypto::USDT);
        assert_eq!(params.interval, KlineInterval::OneMinute);
        assert_eq!(
            params.start_time,
            Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap()
        );
        assert_eq!(
            params.end_time,
            Utc.with_ymd_and_hms(2022, 1, 1, 0, 2, 0).unwrap()
        );
    }
}