scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
use anyhow::Error;
use chrono::{DateTime, Utc};
use std::{
    future::Future,
    pin::Pin,
    sync::{Arc, atomic::Ordering},
    time::Duration,
};
use tokio::{
    sync::{Notify, watch::Receiver},
    task::JoinHandle,
    time::sleep,
};

use crate::{
    clock::{Clock, ClockFactory, ClockRemote},
    core::{
        Core, CoreResult,
        market::{Market, MarketFactory},
    },
    strategy::{AtomicState, ErrorInStrat, State, StratStat, Strategy, StrategyFactory},
};

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

/// The [`App`] is responsible for running and managing the [`super::strategy::Strategy`]
pub struct App<Cl: Clock> {
    name: String,
    handle: Option<JoinHandle<()>>,
    remote: Option<Cl::Remote>,
    clock: Cl,
    strategy_state: Arc<AtomicState>,
    strategy_notify: Arc<Notify>,
    strategy_channel: Receiver<Result<StratStat, ErrorInStrat>>,
}

impl<Cl: Clock> App<Cl> {
    pub async fn new<
        ClockF: ClockFactory<_Clock = Cl>,
        M: Market<_Clock = Cl>,
        MF: MarketFactory<_Market = M>,
        S: Strategy<_Market = M, _Clock = Cl>,
        SF: StrategyFactory<_Strategy = S>,
    >(
        name: String,
        market_factory: MF,
        strategy_factory: SF,
        clock_factory: ClockF,
    ) -> CoreResult<Self> {
        let (remote, clock) = clock_factory.build()?;

        let core = Core::new(name.clone(), market_factory, clock.clone()).await?;

        let mut strategy = strategy_factory.with_core(core).build();
        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");
        }));

        Ok(Self {
            name,
            handle,
            remote,
            clock,
            strategy_state,
            strategy_notify,
            strategy_channel,
        })
    }

    /// Ask the strategy to end its pause and start running
    pub fn start(&self) {
        if self.handle.is_none() {
            tracing::warn!("App is not available");
            return;
        }
        self.send(State::Starting);
    }

    /// Manually send its next status to the strategy
    pub fn send(&self, state: State) {
        self.strategy_state.store(state, Ordering::Relaxed);
        self.strategy_notify.notify_waiters();
        tracing::debug!("New state sent : {}", state);
    }

    /// Ask the strategy to terminate its business and gracefully close
    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)
    }

    /// Ask the strategy to stop *ASAP*
    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();
    }

    /// Ask the strategy its economic statistics
    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)
    }

    /// Set the remote time allowing the strategy to reach it
    pub fn advance_toward(&self, date: DateTime<Utc>) -> bool {
        match self.remote.as_ref() {
            Some(remote) => {
                remote.advance_toward(date);
                true
            }
            None => false,
        }
    }

    /// Wait for the strategy to reach remote time
    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;
            }
        })
    }

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

    pub fn get_name(&self) -> &String {
        &self.name
    }
}