scuriolus 0.3.0

Scuriolus is a modular trading bot platform.
Documentation
#![cfg(feature = "mexc")]

use anyhow::Error;
use chrono::{DateTime, TimeZone as _, Utc};
use dotenv::dotenv;
use futures::{
    FutureExt,
    future::{BoxFuture, join_all},
};
use rust_decimal::Decimal;
use std::time::Duration;
use surrealdb::{
    Surreal,
    engine::local::{Db, RocksDb},
};
use tokio::{select, time::sleep};

use scuriolus::{
    app::App,
    clock::{RunningCheatClock, RunningCheatClockFactory},
    core::market::simulated::{Simulated, SimulatedFactory},
    generics::order::Crypto,
    provider::{Interval, Provider, source::mexc_kline_source::MexcKlineSource},
    strategy::{
        Strategy, StrategyFactory, strict_buy::StrictBuyFactory, trend_tunnel::TrendTunnelFactory,
    },
};

const QUOTE: Crypto = Crypto::USDT;
const ASSET: Crypto = Crypto::BTC;
const AMOUNT: Decimal = Decimal::from_parts(1000, 0, 0, false, 0);

const SECS_PER_YEAR: Decimal = Decimal::from_parts(60 * 60 * 24 * 365, 0, 0, false, 0);

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenv().ok();

    let db = Surreal::new::<RocksDb>("db/perfo/".to_string())
        .await
        .expect("error creating db");
    db.use_ns("test")
        .use_db("test")
        .await
        .expect("error using db");

    let source = MexcKlineSource::new().await;
    let provider = Provider::new(source.clone(), db.clone());

    {
        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
        let end = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();

        let futures = vec![
            test_perf(
                "trend tunnel 5%".to_string(),
                TrendTunnelFactory::new(QUOTE, AMOUNT, ASSET, start, Interval::OneDay, 5)
                    .with_provider(provider.clone()),
                source.clone(),
                db.clone(),
                start,
                end,
            ),
            test_perf(
                "trend tunnel 10%".to_string(),
                TrendTunnelFactory::new(QUOTE, AMOUNT, ASSET, start, Interval::OneDay, 10)
                    .with_provider(provider.clone()),
                source.clone(),
                db.clone(),
                start,
                end,
            ),
            test_perf(
                "trend tunnel 20%".to_string(),
                TrendTunnelFactory::new(QUOTE, AMOUNT, ASSET, start, Interval::OneDay, 20)
                    .with_provider(provider.clone()),
                source.clone(),
                db.clone(),
                start,
                end,
            ),
            test_perf(
                "trend tunnel 30%".to_string(),
                TrendTunnelFactory::new(QUOTE, AMOUNT, ASSET, start, Interval::OneDay, 30)
                    .with_provider(provider.clone()),
                source.clone(),
                db.clone(),
                start,
                end,
            ),
            test_perf(
                "strict buy".to_string(),
                StrictBuyFactory::new(QUOTE, AMOUNT, ASSET),
                source.clone(),
                db.clone(),
                start,
                end,
            ),
        ];

        let results = join_all(futures)
            .await
            .into_iter()
            .map(|x| x.unwrap())
            .collect::<Vec<_>>();

        for (apy, name) in results {
            println!(
                "{}: {}%",
                name,
                (apy * SECS_PER_YEAR
                    / (Decimal::from(end.timestamp() - start.timestamp()) * AMOUNT)
                    * Decimal::ONE_HUNDRED)
                    .round_dp(2)
            );
        }
    }
    Ok(())
}

fn test_perf<
    'a,
    S: Strategy<
            _Market = Simulated<Provider<MexcKlineSource>, MexcKlineSource, RunningCheatClock>,
            _Clock = RunningCheatClock,
        >,
    ST: StrategyFactory<_Strategy = S> + 'a,
>(
    name: String,
    strat_factory: ST,
    source: MexcKlineSource,
    db: Surreal<Db>,
    start: DateTime<Utc>,
    end: DateTime<Utc>,
) -> BoxFuture<'a, Result<(Decimal, String), Error>> {
    async move {
    tracing::debug!("Building up market");
    let market_factory =
        SimulatedFactory::<Provider<MexcKlineSource>, MexcKlineSource, RunningCheatClock>::new(
            name.clone(),
        )
        .with_provider(Provider::new(source.clone(), db.clone()))
        .with_balance(QUOTE, AMOUNT);

    tracing::debug!("Creating kline store");

    let clock_factory = RunningCheatClockFactory { date: start };

    tracing::debug!("Creating app");
    let mut app = App::new(
        name.clone(),
        market_factory,
        strat_factory,
        clock_factory
    )
    .await
    .unwrap();

    let early_stat = app.get_stats().await?;

    app.start();

    app.advance_toward(end);

    {
        let mut future = app.wait_for_the_bot().fuse();
        loop {
            select! {
                _ = &mut future => break,
                _ = sleep(Duration::from_secs(3)) => {
                    let percents = ((app.get_time().timestamp() - start.timestamp()) as f64) / ((end.timestamp() - start.timestamp()) as f64) * 100.;
                    tracing::info!("time inside {} : {} ({}%)", app.get_name(),app.get_time(), percents.round());
                }
            }
        }
    }

    let final_stat = app.terminate().await?;
    Ok((final_stat.quote.free - early_stat.quote.free, name))
}.boxed()
}