scuriolus 0.1.0

Scuriolus is a modular trading bot platform. It can apply different strategies to various markets, as described below.
Documentation
use anyhow::Error;
use std::{
    future::Future,
    pin::Pin,
    sync::{atomic::Ordering, Arc},
};
use tokio::sync::{
    watch::{channel, Receiver, Sender},
    Notify,
};

use super::{
    super::{
        core::Core,
        market::{mexc_base::mexc_enums, Market},
        order::{Amount, Crypto, OrderDraft, Quantity},
    },
    AtomicState, ErrorInStat, State, StratStat, Strategy, StrategyFactory,
};


/// A [`StrategyFactory`] for [`StrictBuy`].
pub struct StrictBuyFactory<M: Market> {
    quote: Crypto,
    amount: Amount,
    asset: Crypto,
    core: Option<Core<M>>,
}

impl<M: Market> StrictBuyFactory<M> {
    pub fn new(quote: Crypto, amount: Amount, asset: Crypto) -> Self {
        Self {
            quote,
            amount,
            asset,
            core: None,
        }
    }
}

impl<M: Market> StrategyFactory<M> for StrictBuyFactory<M> {
    fn instanciate(&mut self) -> Box<dyn Strategy> {
        Box::new(StrictBuy::new(
            self.core.take().expect("Core not set"),
            self.quote,
            self.amount,
            self.asset,
        ))
    }

    fn set_core(&mut self, core: Core<M>) {
        self.core = Some(core);
    }
}

/// A [`Strategy`] that simply buys a given asset at market price and sell it last moment.
pub struct StrictBuy<M: Market> {
    core: Core<M>,
    future_state: Arc<AtomicState>,
    state: State,
    last_state: State,
    notify: Arc<Notify>,
    quote: Crypto,
    amount: Amount,
    asset: Crypto,
    tx: Sender<Result<StratStat, ErrorInStat>>,
    rx: Receiver<Result<StratStat, ErrorInStat>>,
}

const DEFAULT_STATE: State = State::Paused;

impl<M: Market> StrictBuy<M> {
    pub fn new(core: Core<M>, quote: Crypto, amount: Amount, asset: Crypto) -> Self {
        let future_state = Arc::new(AtomicState::new(DEFAULT_STATE));
        let notify = Arc::new(Notify::new());

        let (tx, rx) = channel(Err(ErrorInStat::new(
            "Not initialized".to_string(),
            core.clock().now(),
        )));

        Self {
            core,
            future_state,
            state: DEFAULT_STATE,
            last_state: DEFAULT_STATE,
            notify,
            asset,
            amount,
            quote,
            tx,
            rx,
        }
    }

    async fn get_stats(&self) -> Result<StratStat, Error> {
        let asset = self.core.get_balance(self.asset).await?;
        let quote = self.core.get_balance(self.quote).await?;

        Ok(StratStat {
            asset,
            quote,
            time: self.core.clock().now(),
        })
    }

    async fn send_stats(&self) {
        let stat = self
            .get_stats()
            .await
            .map_err(|err| ErrorInStat::new(err.to_string(), self.core.clock().now()));

        if let Err(err) = self.tx.send(stat) {
            tracing::error!("Failed to send stat: {}", err);
        }
    }

    async fn await_state_change(&mut self) {
        self.last_state = self.state;
        loop {
            self.core.clock().synchronize();
            loop {
                self.state = self.future_state.load(Ordering::Relaxed);

                if self.state != self.last_state {
                    break;
                }
                self.notify.notified().await;
            }
            if self.state == State::Stats {
                self.future_state.store(self.last_state, Ordering::Relaxed);
                self.send_stats().await;
            } else {
                break;
            }
        }
    }
}

impl<M: Market> Strategy for StrictBuy<M> {
    fn state(&self) -> &Arc<AtomicState> {
        &self.future_state
    }

    fn notify(&self) -> &Arc<Notify> {
        &self.notify
    }

    fn stat_channel(&self) -> Receiver<Result<StratStat, ErrorInStat>> {
        self.rx.clone()
    }

    fn run(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
        Box::pin(async move {
            while self.state != State::Running {
                self.await_state_change().await;
            }

            tracing::info!("Strict_buy running");

            self.core
                .send_order(OrderDraft {
                    asset: self.asset,
                    currency: self.quote,
                    side: mexc_enums::OrderSide::Buy,
                    order_type: mexc_enums::OrderType::Market,
                    amount: Quantity::Quote(self.amount),
                    price: None,
                })
                .await
                .unwrap();

            tracing::info!("Strict buy waiting");

            while self.state == State::Running {
                self.await_state_change().await;
            }

            tracing::info!("Strict buy closing");

            let asset_balance = self.core.get_balance(self.asset).await.unwrap();

            self.core
                .send_order(OrderDraft {
                    asset: self.asset,
                    currency: self.quote,
                    side: mexc_enums::OrderSide::Sell,
                    order_type: mexc_enums::OrderType::Market,
                    amount: Quantity::Asset(asset_balance.free),
                    price: None,
                })
                .await
                .unwrap();

            self.send_stats().await;
        })
    }
}