#![allow(clippy::comparison_to_empty)]
#![allow(clippy::get_first)]
#![allow(clippy::len_zero)] #![feature(trait_alias)]
#![feature(type_changing_struct_update)]
#![feature(stmt_expr_attributes)]
pub mod config;
pub mod exchange_apis;
pub mod positions;
pub mod protocols;
pub mod utils;
use std::sync::{atomic::AtomicU32, Arc};
use clap::{Args, Parser, Subcommand};
use color_eyre::eyre::{bail, Context, Result};
use config::AppConfig;
use exchange_apis::{exchanges::Exchanges, hub, hub::PositionToHub};
use positions::*;
use tokio::{sync::mpsc, task::JoinSet};
use tracing::{info, instrument};
use v_utils::{
io::ExpandedPath,
trades::{Side, Timeframe},
};
pub static MAX_CONNECTION_FAILURES: u32 = 10;
pub static MUT_CURRENT_CONNECTION_FAILURES: AtomicU32 = AtomicU32::new(0);
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, default_value = "~/.config/discretionary_engine.toml")]
config: ExpandedPath,
#[arg(short, long, action = clap::ArgAction::SetTrue)]
noconfirm: bool,
#[arg(long, default_value = "~/.discretionary_engine")]
artifacts: ExpandedPath,
}
#[derive(Subcommand)]
enum Commands {
New(PositionArgs),
}
#[derive(Args, Debug, Clone)]
struct PositionArgs {
#[arg(short, long)]
size_usdt: f64,
#[arg(short, long)]
tf: Option<Timeframe>,
#[arg(short, long)]
coin: String,
#[arg(short, long)]
acquisition_protocols: Vec<String>,
#[arg(short, long)]
followup_protocols: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
let cli = Cli::parse();
let config = match AppConfig::new(cli.config) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("Loading config failed: {}", e);
std::process::exit(1);
}
};
let config_arc = Arc::new(config);
match std::fs::create_dir_all(&cli.artifacts) {
Ok(_) => {}
Err(e) => {
eprintln!("Failed to create artifacts directory: {}", e);
std::process::exit(1);
}
}
let log_path = match std::env::var("TEST_LOG") {
Ok(_) => None,
Err(_) => Some(cli.artifacts.0.join(".log").clone().into_boxed_path()),
};
utils::init_subscriber(log_path);
let mut js = JoinSet::new();
let exchanges_arc = Arc::new(
Exchanges::init(config_arc.clone())
.await
.wrap_err_with(|| "Error initializing Exchanges, likely indicative of bad internet connection")?,
);
let tx = hub::init_hub(config_arc.clone(), &mut js, exchanges_arc.clone());
match cli.command {
Commands::New(position_args) => match command_new(position_args, config_arc.clone(), tx, exchanges_arc).await {
Ok(_) => {}
Err(e) => {
eprintln!("{}", utils::format_eyre_chain_for_user(e));
std::process::exit(1);
}
},
}
Ok(())
}
#[instrument(skip(config_arc, tx, exchanges_arc))]
async fn command_new(position_args: PositionArgs, config_arc: Arc<AppConfig>, tx: mpsc::Sender<PositionToHub>, exchanges_arc: Arc<Exchanges>) -> Result<()> {
let balance = match Exchanges::compile_total_balance(exchanges_arc.clone(), config_arc.clone()).await {
Ok(b) => b,
Err(e) => {
eprintln!("Failed to get balance: {}", e);
std::process::exit(1);
}
};
info!("Total balance: {}", balance);
println!("Current total available balance: {}", balance);
let (side, target_size) = match position_args.size_usdt {
s if s > 0.0 => (Side::Buy, s),
s if s < 0.0 => (Side::Sell, -s),
_ => {
bail!("Size must be non-zero");
}
};
let followup_protocols = protocols::interpret_protocol_specs(position_args.followup_protocols).wrap_err("Failed to interpret followup protocols")?;
let acquisition_protocols = protocols::interpret_protocol_specs(position_args.acquisition_protocols).wrap_err("Failed to interpret acquisition protocols")?;
let spec = PositionSpec::new(position_args.coin, side, target_size);
let acquired = PositionAcquisition::do_acquisition(spec, acquisition_protocols, tx.clone(), exchanges_arc.clone()).await?;
let _followed = PositionFollowup::do_followup(acquired, followup_protocols, tx.clone(), exchanges_arc.clone()).await?;
Ok(())
}