scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
use anyhow::Error;
use rust_decimal::Decimal;
use std::{
    future::Future,
    pin::Pin,
    sync::{Arc, atomic::Ordering},
};
use tokio::sync::{
    Notify,
    watch::{Receiver, Sender, channel},
};

use crate::{
    clock::Clock,
    core::{Core, market::Market},
    generics::order::{Crypto, OrderDraft, OrderSide, OrderType},
};

use super::{AtomicState, ErrorInStrat, State, StratStat, Strategy, StrategyFactory};

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

impl<C: Clock, M: Market<_Clock = C>> StrictBuyFactory<C, M> {
    pub fn new(quote: Crypto, amount: Decimal, asset: Crypto) -> Self {
        Self {
            quote,
            amount,
            asset,
            core: None,
        }
    }
}

impl<C: Clock, M: Market<_Clock = C>> StrategyFactory for StrictBuyFactory<C, M> {
    type _Strategy = StrictBuy<C, M>;
    fn build(mut self) -> StrictBuy<C, M> {
        StrictBuy::new(
            self.core.take().expect("Core not set"),
            self.quote,
            self.amount,
            self.asset,
        )
    }

    fn with_core(self, core: Core<C, M>) -> Self {
        Self {
            core: Some(core),
            ..self
        }
    }
}

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

const DEFAULT_STATE: State = State::Paused;

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

        let (tx, rx) = channel(Err(ErrorInStrat::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| ErrorInStrat::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<C: Clock, M: Market<_Clock = C>> Strategy for StrictBuy<C, M> {
    type _Market = M;
    type _Clock = C;

    fn state(&self) -> &Arc<AtomicState> {
        &self.future_state
    }

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

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

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

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

            self.core
                .send_order(OrderDraft {
                    asset: self.asset,
                    quote: self.quote,
                    side: OrderSide::Buy,
                    order_type: OrderType::Market,
                    qty_asset: None,
                    qty_quote: Some(self.amount),
                    price: None,
                })
                .await
                .unwrap();

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

            while self.state == State::Starting {
                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,
                    quote: self.quote,
                    side: OrderSide::Sell,
                    order_type: OrderType::Market,
                    qty_asset: Some(asset_balance.free),
                    qty_quote: None,
                    price: None,
                })
                .await
                .unwrap();

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