greed 0.6.0

A rust tool to automate trades 📈
use crate::asset::AssetSymbol;
use crate::config::strategy::StrategyProperties;
use crate::config::Config;
use crate::platform::FinancialPlatform;
use crate::tactic::TacticRunner;
use async_trait::async_trait;
use duckdb::Connection;
use log::{info, warn};
#[cfg(test)]
use std::any::Any;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;

#[async_trait(?Send)]
pub trait StrategyRunner {
    async fn run(&self, config_assets: &[AssetSymbol]);
    #[cfg(test)]
    fn as_any(&self) -> &dyn Any;
}

pub struct TacticStrategyRunner {
    loop_interval: Duration,
    strategy_properties: StrategyProperties,
    tactic_runners: Vec<TacticRunner>,
}

impl TacticStrategyRunner {
    pub fn new(
        loop_interval: Duration,
        strategy_properties: StrategyProperties,
        tactic_runners: Vec<TacticRunner>,
    ) -> Self {
        Self {
            loop_interval,
            strategy_properties,
            tactic_runners,
        }
    }

    pub fn from_config(
        config: &Config,
        platform: &Arc<dyn FinancialPlatform>,
        connection: &Arc<Mutex<Connection>>,
    ) -> Self {
        let loop_interval = Duration::from_secs(config.interval);

        let tactic_runners = config
            .tactics
            .clone()
            .into_iter()
            .map(|tactic_config| {
                TacticRunner::new(
                    tactic_config.clone(),
                    platform.clone(),
                    Default::default(),
                    connection.clone(),
                )
            })
            .collect();
        Self::new(loop_interval, Default::default(), tactic_runners)
    }

    #[cfg(test)]
    pub fn tactic_runner_count(&self) -> usize {
        self.tactic_runners.len()
    }
}

#[async_trait(?Send)]
impl StrategyRunner for TacticStrategyRunner {
    async fn run(&self, config_assets: &[AssetSymbol]) {
        let name = self.strategy_properties.name.clone();
        if !name.is_empty() {
            info!("🚀 running strategy: {}", self.strategy_properties.name);
        }
        for tactic_runner in &self.tactic_runners {
            let _ = tactic_runner
                .run(config_assets)
                .await
                .inspect_err(|e| warn!("{e}"));
            sleep(self.loop_interval).await;
        }
    }

    #[cfg(test)]
    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fixture;
    use crate::platform::noop::NoOpPlatform;

    #[tokio::test]
    async fn from_config_empty_tactics() {
        let config = fixture::config("config_minimal.toml").await;
        let platform = NoOpPlatform::arc();
        let connection = fixture::db_connection();
        let runner = TacticStrategyRunner::from_config(&config, &platform, &connection);
        assert_eq!(runner.loop_interval, Duration::from_secs(60));
        assert_eq!(runner.tactic_runner_count(), 0);
    }

    #[tokio::test]
    async fn from_config_with_tactics() {
        let config = fixture::config("config_single_tactic.toml").await;
        let platform = NoOpPlatform::arc();
        let connection = fixture::db_connection();
        let runner = TacticStrategyRunner::from_config(&config, &platform, &connection);
        assert_eq!(runner.loop_interval, Duration::from_secs(300));
        assert_eq!(runner.tactic_runner_count(), 1);
    }
}