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 chrono::{DateTime, Utc};
use std::{
    future::Future,
    pin::Pin,
    sync::{atomic::Ordering, Arc},
    time::Duration,
};
use tokio::{
    sync::{watch::Receiver, Notify},
    task::JoinHandle,
    time::sleep,
};

use super::{
    clock::{CheatClockRemote, ClockBase, TrueClock},
    market::Market,
    strategy::{AtomicState, ErrorInStat, State, StratStat, StrategyFactory},
};

const KILL_TIMEOUT: Duration = Duration::from_secs(5);

/// The [`App`] is responsible for running and managing the [`super::strategy::Strategy`]
pub struct App {
    handle: Option<JoinHandle<()>>,
    remote: Option<Box<dyn CheatClockRemote>>,
    clock: Box<dyn ClockBase>,
    strategy_state: Arc<AtomicState>,
    strategy_notify: Arc<Notify>,
    strategy_channel: Receiver<Result<StratStat, ErrorInStat>>,
}

impl App {
    pub async fn new<M: Market>(
        mut strategy_factory: Box<dyn StrategyFactory<M>>,
        remote: Option<Box<dyn CheatClockRemote>>,
    ) -> Self {
        let mut strategy = strategy_factory.instanciate();
        let strategy_state = strategy.state().clone();
        let strategy_notify = strategy.notify().clone();
        let strategy_channel = strategy.stat_channel();

        let handle = Some(tokio::spawn(async move {
            tracing::debug!("Strategy created");
            strategy.run().await;
            tracing::debug!("Strategy is over");
        }));

        if remote.is_some() {
            let clock = remote.as_ref().unwrap().clone_box();
            Self {
                handle,
                remote,
                clock,
                strategy_state,
                strategy_notify,
                strategy_channel,
            }
        } else {
            Self {
                handle,
                remote,
                clock: Box::new(TrueClock::new()),
                strategy_state,
                strategy_notify,
                strategy_channel,
            }
        }
    }

    pub fn run(&self) {
        if self.handle.is_none() {
            tracing::warn!("App is not running");
            return;
        }
        self.send(State::Running);
    }

    pub fn send(&self, state: State) {
        self.strategy_state.store(state, Ordering::Relaxed);
        self.strategy_notify.notify_waiters();
        tracing::debug!("New state sent : {}", state);
    }

    pub async fn terminate(&mut self) -> Result<StratStat, Error> {
        if self.handle.is_none() {
            return Err(Error::msg("App is not running"));
        }
        self.send(State::Terminated);
        self.wait_for_the_bot().await;
        if let Err(err) = self.handle.take().unwrap().await {
            tracing::error!("Failed to stop the app properly: {}", err);
        }
        self.strategy_channel.changed().await?;
        let stat = self.strategy_channel.borrow_and_update().clone()?;
        Ok(stat)
    }

    pub async fn kill(&mut self) {
        if self.handle.is_none() {
            tracing::warn!("App is not running");
            return;
        }
        self.send(State::Killed);
        sleep(KILL_TIMEOUT).await;
        self.handle.take().unwrap().abort();
    }

    pub async fn get_stats(&mut self) -> Result<StratStat, Error> {
        self.send(State::Stats);
        self.strategy_channel.changed().await?;
        let stat = self.strategy_channel.borrow_and_update().clone()?;
        Ok(stat)
    }

    pub fn advance_toward(&self, date: DateTime<Utc>) -> bool {
        match self.remote.as_ref() {
            Some(remote) => {
                remote.advance_toward(date);
                true
            }
            None => false,
        }
    }

    pub fn wait_for_the_bot(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
        Box::pin(async move {
            if let Some(remote) = self.remote.as_ref() {
                remote.wait_for_the_clock().await;
            }
        })
    }

    pub fn get_time(&self) -> DateTime<Utc> {
        self.clock.now()
    }
}