diff --git a/Cargo.lock b/Cargo.lock index 1528550..8cdcaa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -262,6 +271,20 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cmake" version = "0.1.58" @@ -337,6 +360,27 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -726,6 +770,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1578,6 +1646,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -1735,6 +1809,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "chrono", + "csv", "env_logger", "futures-util", "log", @@ -2270,6 +2346,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 4aab1a1..b0cdeae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ env_logger = "0.11.11" rust_decimal = { version = "1.42.1", features = ["macros"] } reqwest = { version = "0.13.4", features = ["json"] } async-trait = "0.1.91" +csv = "1.4.0" +chrono = { version = "0.4.45", features = ["serde"] } diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 0c915de..851ae1d 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -121,24 +121,10 @@ impl ExecutorWrapper { } pub async fn sell_all(&mut self, priority: Decimal, slippage: u16) -> anyhow::Result<()> { - let positions: Vec<(String, Decimal)> = self - .positions - .iter() - .map(|(mint, amount)| (mint.clone(), *amount)) - .collect(); - - for (mint, amount) in positions { + for (mint, _) in self.positions.drain() { self.executor - .sell(&mint, amount, priority, slippage) + .sell_percent(&mint, 100, priority, slippage) .await?; - - if let Some(position) = self.positions.get_mut(&mint) { - *position -= amount; - - if *position <= Decimal::ZERO { - self.positions.remove(&mint); - } - } } Ok(()) diff --git a/src/main.rs b/src/main.rs index a9acd57..a9b7604 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,22 +5,50 @@ pub mod strategy; pub mod tradelog; pub mod types; -use tokio::io::{self, AsyncBufReadExt, BufReader}; -use tokio::sync::watch; +use std::sync::Arc; + +use chrono::Local; +use tokio::{ + io::{self, AsyncBufReadExt, BufReader}, + sync::watch, +}; use crate::bot::Bot; -async fn shutdown_listener(tx: watch::Sender) { +async fn shutdown_listener(bot: Arc, tx: watch::Sender) -> anyhow::Result<()> { let stdin = BufReader::new(io::stdin()); let mut lines = stdin.lines(); while let Ok(Some(line)) = lines.next_line().await { - if line.trim().eq_ignore_ascii_case("exit") { - println!("Shutdown requested."); - let _ = tx.send(true); - break; + match line.to_lowercase().trim() { + "exit" | "shutdown" => { + let _ = tx.send(true); + } + + "save" => { + log::info!("Saving trades"); + + let now = Local::now(); + let formatted_time = now.format("%m-%d-%H-%M").to_string(); + + let filename = format!("sol-hun-{}.csv", formatted_time); + + let mut writer = csv::Writer::from_path(&filename)?; + + for trade in bot.trade_log.lock().await.iter() { + writer.serialize(trade)?; + } + + writer.flush()?; + + log::info!("Saved trades to {}", filename); + } + + _ => {} } } + + Ok(()) } #[tokio::main] @@ -31,9 +59,9 @@ async fn main() -> anyhow::Result<()> { let (shutdown_tx, shutdown_rx) = watch::channel(false); - tokio::spawn(shutdown_listener(shutdown_tx)); - let bot = Bot::new().await?; + tokio::spawn(shutdown_listener(bot.clone(), shutdown_tx)); + bot.start(shutdown_rx).await } diff --git a/src/tradelog.rs b/src/tradelog.rs index 2a39d2e..8634769 100644 --- a/src/tradelog.rs +++ b/src/tradelog.rs @@ -1,6 +1,6 @@ -use std::time::{Duration, Instant}; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ExitReason { TakeProfit, StopLoss, @@ -8,18 +8,18 @@ pub enum ExitReason { MomentumStalled, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct TradeLog { pub mint: String, - pub opened_at: Instant, - pub closed_at: Option, + pub opened_at: chrono::DateTime, + pub closed_at: Option>, pub entry_price_sol: f64, pub exit_price_sol: Option, pub pnl_percent: Option, - pub duration: Option, + pub duration: Option, pub exit_reason: Option, @@ -39,7 +39,7 @@ impl TradeLog { ) -> Self { Self { mint, - opened_at: Instant::now(), + opened_at: chrono::Utc::now(), closed_at: None, entry_price_sol, @@ -57,7 +57,7 @@ impl TradeLog { } pub fn close(&mut self, exit_price_sol: f64, reason: ExitReason) { - let now = Instant::now(); + let now = chrono::Utc::now(); let pnl = ((exit_price_sol - self.entry_price_sol) / self.entry_price_sol) * 100.0; @@ -65,7 +65,7 @@ impl TradeLog { self.pnl_percent = Some(pnl); self.closed_at = Some(now); - self.duration = Some(now.duration_since(self.opened_at)); + self.duration = Some(now.signed_duration_since(self.opened_at)); self.exit_reason = Some(reason); }