diff --git a/Cargo.toml b/Cargo.toml index 956076c..e5e5a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -binance-rs-async = { git = "https://github.com/Igosuki/binance-rs-async.git", features = ["margin_api"] } +binance-rs-async = { git = "https://github.com/Igosuki/binance-rs-async.git", features = ["futures_api"] } serde = { version = "1.0.167", features = ["derive"] } serde_yaml = "0.9.22" chrono = "0.4.26" diff --git a/src/algorithm.rs b/src/algorithm.rs index d7bbf43..4d27b4c 100644 --- a/src/algorithm.rs +++ b/src/algorithm.rs @@ -1,39 +1,36 @@ -use std::{error::Error, sync::Arc, time::Duration}; +use std::{error::Error, time::Duration}; use binance::rest_model::OrderSide; use chrono::Utc; -use tokio::sync::Mutex; use crate::{ config::Config, historical_data::{calculate_technicals, load, TickerData, MINS_TO_MILLIS}, - krypto_account::KryptoAccount, - math::percentage_change, - order_event::{OrderDetails, OrderEvent}, + krypto_account::{KryptoAccount, Order}, + math::{format_number, percentage_change}, relationships::{compute_relationships, predict, Relationship}, testing::{test_headers, TestData}, }; -const MARGIN: f32 = 0.05; +const MARGIN: f32 = 0.2; const STARTING_CASH: f32 = 1000.0; -const WAIT_WINDOW: i64 = 20000; -const ENTRY_TIME_PERCENT: f64 = 0.2; -const EXIT_TIME_PERCENT: f64 = 0.075; +const WAIT_WINDOW: i64 = 15000; pub async fn backtest( candles: &[TickerData], relationships: &[Relationship], config: &Config, ) -> TestData { - let mut test = TestData::new(STARTING_CASH); + let mut test = &mut TestData::new(STARTING_CASH); for i in *config.depth()..*config.periods() - *config.depth() { let (index, score) = predict(relationships, i, candles, config).await; - if score > config.min_score().unwrap_or_default() { + if score.abs() > config.min_score().unwrap_or_default() { let current_price = candles[index].candles()[i].close(); let exit_price = candles[index].candles()[i + *config.depth()].close(); let change = percentage_change(*current_price, *exit_price); + let change = score.signum() * change.abs(); let fee_change = test.cash() * config.fee().unwrap_or_default() * MARGIN; test.add_cash(-fee_change); @@ -46,12 +43,12 @@ pub async fn backtest( } if *test.cash() <= 0.0 { - test.set_cash(0.0); + test = test.set_cash(0.0); break; } } } - test + test.clone() } pub async fn livetest(config: &Config) -> Result<(), Box> { @@ -99,6 +96,7 @@ pub async fn livetest(config: &Config) -> Result<(), Box> { let li = last_index.unwrap(); let current_price = lc[li].candles()[999].close(); let change = percentage_change(ep, *current_price); + let change = last_score.unwrap().signum() * change.abs(); let fee_change = test.cash() * fee * MARGIN; test.add_cash(-fee_change); @@ -149,7 +147,7 @@ pub async fn livetest(config: &Config) -> Result<(), Box> { } let (index, score) = predict(&relationships, 999, &lc, config).await; - if score > min_score { + if score.abs() > min_score { let current_price = lc[index].candles()[999].close(); enter_price = Some(*current_price); last_index = Some(index); @@ -162,7 +160,7 @@ pub async fn livetest(config: &Config) -> Result<(), Box> { enter_price = None; last_index = None; last_score = None; - println!("No trade ({:.5} < {})", score, min_score); + println!("No trade ({:.5} < {})", score.abs(), min_score); } } @@ -172,14 +170,13 @@ pub async fn livetest(config: &Config) -> Result<(), Box> { pub async fn run(config: &Config) -> Result<(), Box> { let depth = *config.depth(); let min_score = config.min_score().unwrap_or_default(); - let interval_mins = config.interval_minutes()?; - let order_len = depth as i64 * interval_mins; - - let kr = Arc::new(Mutex::new(KryptoAccount::new(config))); - kr.lock().await.update_exchange_info().await?; - let mut test = TestData::new(kr.lock().await.get_balance().await? as f32); + let mut account = KryptoAccount::new(config); + let balance = account.get_balance("BUSD").await?; + let mut test = TestData::new(balance); + println!("Starting balance: ${}", format_number(balance)); + account.set_default_leverages(config).await?; - let mut file = csv::Writer::from_path("live.csv")?; + let mut file = csv::Writer::from_path("run.csv")?; let headers = test_headers(); file.write_record(headers)?; let starting_records = vec![ @@ -199,10 +196,9 @@ pub async fn run(config: &Config) -> Result<(), Box> { let mut candles = load_new_data(config, 2).await?; candles = calculate_technicals(candles); let mut relationships = compute_relationships(&candles, config).await; + wait(config, 1).await?; loop { - wait(config, 1).await?; - let mut c_clone = config.clone(); c_clone.set_periods(1000); let lc = load_new_data(&c_clone, 3).await; @@ -216,41 +212,49 @@ pub async fn run(config: &Config) -> Result<(), Box> { let lc = calculate_technicals(lc); let (index, score) = predict(&relationships, 999, &lc, config).await; - if score > min_score { + if score.abs() > min_score { + println!("Predicted {} ({:.5})", lc[index].ticker(), score); let ticker = lc[index].ticker(); - let (max_entry_time, min_exit_time) = get_entry_and_exit_times(order_len); - let order_details = OrderDetails { - ticker: ticker.to_string(), - side: OrderSide::Buy, - quantity: None, - max_time: Some(max_entry_time), + let price = lc[index].candles()[999].close(); + let order = Order { + symbol: ticker.to_string(), + side: match score.signum() as i32 { + 1 => OrderSide::Buy, + -1 => OrderSide::Sell, + _ => panic!("Invalid score"), + }, + quantity: *config.trade_size() * test.cash() * *config.leverage() as f32 / price, }; - println!("Buying {} ({:.5})", ticker, score); - let order = OrderEvent::new(order_details, kr.lock().await.to_owned()).await; - - if order.is_err() { - println!("Error: {}", order.unwrap_err()); - continue; - } - - let order = order?; - let enter_price = order.current_order_price().unwrap(); - let qty = order.details().quantity.unwrap(); - let update_time = (min_exit_time - Utc::now().timestamp_millis()) / 2; - tokio::time::sleep(Duration::from_millis(update_time as u64)).await; + let order = account.order(order).await; + let quantity = match order { + Ok(order) => order, + Err(err) => { + println!("Error: {}", err); + continue; + } + }; + let direction_string = match score.signum() as i32 { + 1 => "Long", + -1 => "Short", + _ => panic!("Invalid score"), + }; + let enter_price = *price; + println!( + "Entered {} for {} of {} at ${:.5}", + direction_string, ticker, quantity, enter_price + ); + wait(config, depth - 1).await?; let c_result = load_new_data(config, 1).await; - - if c_result.is_err() { - println!("Error: {}", c_result.unwrap_err()); - continue; - } - - candles = c_result.unwrap(); - candles = calculate_technicals(candles); - relationships = compute_relationships(&candles, config).await; - kr.lock().await.update_exchange_info().await?; - let update_time = min_exit_time - Utc::now().timestamp_millis(); - tokio::time::sleep(Duration::from_millis(update_time as u64)).await; + match c_result { + Ok(c_result) => { + candles = calculate_technicals(c_result); + relationships = compute_relationships(&candles, config).await; + } + Err(err) => { + println!("Error: {}", err); + } + }; + wait(config, 1).await?; loop { let lc = load_new_data(&c_clone, 1).await; @@ -261,46 +265,45 @@ pub async fn run(config: &Config) -> Result<(), Box> { let c = lc.unwrap(); let c = calculate_technicals(c); let (index_2, score_2) = predict(&relationships, 999, &c, config).await; - if score_2 > 0.0 && index_2 == index { - let (_, min_exit_time_2) = get_entry_and_exit_times(order_len); + if score_2.abs() > min_score && index_2 == index && score_2 * score > 0.0 { println!("Continuing to hold {} ({:.5})", ticker, score_2); - let update_time_2 = min_exit_time_2 - Utc::now().timestamp_millis(); - tokio::time::sleep(Duration::from_millis(update_time_2 as u64)).await; + wait(config, depth).await?; } else { break; } } } - let details = OrderDetails { - ticker: ticker.to_string(), - side: OrderSide::Sell, - quantity: Some(qty), - max_time: None, + let order = Order { + symbol: ticker.to_string(), + side: match score.signum() as i32 { + 1 => OrderSide::Sell, + -1 => OrderSide::Buy, + _ => panic!("Invalid score"), + }, + quantity: quantity as f32, }; - - let order = OrderEvent::new(details, kr.lock().await.to_owned()).await; - - if order.is_err() { - println!("Error loading order event"); - println!("This could be an issue! Check your account!"); - continue; - } - - let order = order?; - let exit_price = order.current_order_price().unwrap(); + match account.order(order).await { + Ok(_) => {} + Err(err) => { + println!("Error: {}", err); + continue; + } + }; + let exit_price = *lc[index].candles()[999].close(); + println!( + "Exited {} for {} of {} at ${:.5}", + direction_string, ticker, quantity, exit_price + ); let change = percentage_change(enter_price as f32, exit_price as f32); - test.set_cash(kr.lock().await.get_balance().await? as f32); + let change = score.signum() * change.abs(); match change { - x if x > 0.0 => { - test.add_correct(); - } - x if x < 0.0 => { - test.add_incorrect(); - } + x if x > 0.0 => test.add_correct(), + x if x < 0.0 => test.add_incorrect(), _ => (), } - + let balance = account.get_balance("BUSD").await?; + test.set_cash(balance); let record = vec![ test.cash().to_string(), test.get_accuracy().to_string(), @@ -316,28 +319,29 @@ pub async fn run(config: &Config) -> Result<(), Box> { change.to_string(), chrono::Utc::now().to_rfc3339(), ]; - file.write_record(&record).unwrap_or_else(|err| { println!("Error writing record: {}", err); }); - file.flush().unwrap_or_else(|err| { println!("Error flushing file: {}", err); }); } else { - println!("No trade ({:.5} < {})", score, min_score); + println!("No trade ({:.5} < {})", score.abs(), min_score); + let c_result = load_new_data(config, 1).await; + match c_result { + Ok(c_result) => { + candles = calculate_technicals(c_result); + relationships = compute_relationships(&candles, config).await; + } + Err(err) => { + println!("Error: {}", err); + } + }; + wait(config, 1).await?; } } } -fn get_entry_and_exit_times(order_length: i64) -> (i64, i64) { - let entry_amount = ENTRY_TIME_PERCENT * order_length as f64; - let max_entry_time = Utc::now().timestamp_millis() + (entry_amount as i64 * MINS_TO_MILLIS); - let exit_amount = (1.0 - EXIT_TIME_PERCENT) * order_length as f64; - let min_exit_time = Utc::now().timestamp_millis() + (exit_amount as i64 * MINS_TO_MILLIS); - (max_entry_time, min_exit_time) -} - const MAX_REPEATS: usize = 5; async fn load_new_data( @@ -383,7 +387,10 @@ async fn wait(config: &Config, periods: usize) -> Result<(), Box> { #[cfg(test)] pub mod tests { - use crate::{historical_data::{calculate_technicals, load}, candlestick::TECHNICAL_COUNT}; + use crate::{ + candlestick::TECHNICAL_COUNT, + historical_data::{calculate_technicals, load}, + }; use super::*; diff --git a/src/config.rs b/src/config.rs index 4e92ad4..799e80b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,8 @@ const DEFAULT_DATA: &str = r#" periods: 2000 interval: "15m" depth: 3 +leverage: 5 +trade-size: 0.75 tickers: - "BTCBUSD" - "ETHBUSD" @@ -24,6 +26,9 @@ pub struct Config { periods: usize, interval: String, depth: usize, + leverage: u8, + #[serde(rename = "trade-size")] + trade_size: f32, fee: Option, #[serde(rename = "min-score")] min_score: Option, @@ -41,6 +46,8 @@ impl Default for Config { periods: 2000, interval: "15m".to_string(), depth: 3, + leverage: 5, + trade_size: 0.75, tickers: vec!["BTCBUSD".to_string(), "ETHBUSD".to_string()], fee: None, min_score: None, @@ -52,7 +59,6 @@ impl Default for Config { } impl Config { - #[inline] pub async fn read_config(filename: Option<&str>) -> Result, Box> { let path = match filename { diff --git a/src/historical_data.rs b/src/historical_data.rs index 3b17fd7..82a6d54 100644 --- a/src/historical_data.rs +++ b/src/historical_data.rs @@ -1,6 +1,6 @@ use std::error::Error; -use binance::{api::Binance, market::Market, rest_model::KlineSummaries}; +use binance::{api::Binance, futures::market::FuturesMarket, rest_model::KlineSummaries}; use getset::{Getters, MutGetters}; use ta::{indicators, Next}; @@ -53,7 +53,7 @@ async fn load_ticker( config: &Config, ) -> Result> { let mut candlesticks = Vec::new(); - let market: Market = Binance::new(config.api_key().clone(), config.api_secret().clone()); + let market: FuturesMarket = Binance::new(config.api_key().clone(), config.api_secret().clone()); let addition = MINS_TO_MILLIS * 1000 * config.interval_minutes()?; let mut start_time = start_time; let mut start_times = Vec::new(); @@ -89,13 +89,13 @@ async fn load_chunk( start_time: u64, end_time: u64, config: &Config, - market: &Market, + market: &FuturesMarket, ) -> Result, Box> { let summaries = market .get_klines( ticker.clone(), config.interval(), - 1000, + 1000u16, Some(start_time), Some(end_time), ) diff --git a/src/krypto_account.rs b/src/krypto_account.rs index 18ec9ef..44fd19d 100644 --- a/src/krypto_account.rs +++ b/src/krypto_account.rs @@ -1,25 +1,24 @@ -use std::{error::Error, sync::Arc}; +use std::error::Error; use binance::{ - account::Account, api::Binance, - general::General, - margin::Margin, - market::Market, - rest_model::{ExchangeInformation, Filters, MarginOrdersQuery}, + futures::{ + account::FuturesAccount, + general::FuturesGeneral, + market::FuturesMarket, + rest_model::{AccountBalance, ExchangeInformation, Filters, Symbol}, + }, + rest_model::OrderSide, }; use getset::Getters; -use tokio::sync::Mutex; use crate::config::Config; -#[derive(Clone)] pub struct KryptoAccount { - pub margin: Margin, - pub general: General, - pub market: Market, - pub account: Account, - exchange_info: Arc>>, + pub account: FuturesAccount, + pub general: FuturesGeneral, + pub market: FuturesMarket, + pub exchange_info: Option, } #[derive(Debug, Clone, Getters)] @@ -32,38 +31,30 @@ pub struct PrecisionData { step_precision: usize, } +pub struct Order { + pub symbol: String, + pub side: OrderSide, + pub quantity: f32, +} + impl KryptoAccount { pub fn new(config: &Config) -> Self { - let mut margin: Margin = + let market: FuturesMarket = + Binance::new(config.api_key().clone(), config.api_secret().clone()); + let account: FuturesAccount = Binance::new(config.api_key().clone(), config.api_secret().clone()); - margin.recv_window = 10000; - let general: General = Binance::new(config.api_key().clone(), config.api_secret().clone()); - let market: Market = Binance::new(config.api_key().clone(), config.api_secret().clone()); - let account: Account = Binance::new(config.api_key().clone(), config.api_secret().clone()); - KryptoAccount { - margin, + let general: FuturesGeneral = + Binance::new(config.api_key().clone(), config.api_secret().clone()); + Self { + account, general, market, - account, - exchange_info: Arc::new(Mutex::new(None)), + exchange_info: None, } } - pub async fn get_precision_data( - &mut self, - ticker: String, - ) -> Result, Box> { - if self.exchange_info.lock().await.is_none() { - self.update_exchange_info().await?; - } - let ei = self.exchange_info.lock().await; - let symbol = ei - .as_ref() - .unwrap() - .symbols - .iter() - .find(|symbol| symbol.symbol == ticker) - .unwrap(); + pub async fn precision(&mut self, ticker: String) -> Result> { + let symbol = self.get_symbol(ticker.clone()).await?; let mut ts = None; let mut ss = None; for filter in &symbol.filters { @@ -74,75 +65,138 @@ impl KryptoAccount { ss = Some(step_size); } } - let tick_size = *ts.unwrap(); - let step_size = *ss.unwrap(); - let tick_precision = tick_size.log10().abs() as usize; - let step_precision = step_size.log10().abs() as usize; - Ok(Box::new(PrecisionData { + Ok(PrecisionData { ticker, - tick_size, - step_size, - tick_precision, - step_precision, - })) + tick_size: *ts.unwrap(), + step_size: *ss.unwrap(), + tick_precision: ts.unwrap().log10().abs() as usize, + step_precision: ss.unwrap().log10().abs() as usize, + }) } - pub async fn extract_base_asset(&mut self, ticker: &str) -> Result> { - if self.exchange_info.lock().await.is_none() { - self.update_exchange_info().await?; + async fn get_symbol(&mut self, ticker: String) -> Result> { + if self.exchange_info.is_none() { + self.exchange_info = Some( + self.general + .exchange_info() + .await + .map_err(|e| Box::new(KryptoError::ExchangeInfoError(e.to_string())))?, + ); } - let ei = self.exchange_info.lock().await; - let symbol = ei + let symbol = self + .exchange_info .as_ref() .unwrap() .symbols .iter() - .find(|symbol| symbol.symbol == ticker) - .unwrap(); - Ok(symbol.base_asset.clone()) + .find(|symbol| symbol.symbol == ticker); + if symbol.is_none() { + return Err(Box::new(KryptoError::InvalidSymbol(ticker))); + } + Ok(symbol.unwrap().clone()) } - pub async fn max_borrowable(&mut self, ticker: &str) -> Result> { - let base_asset = self.extract_base_asset(ticker).await?; - let max_borrowable = self.margin.max_borrowable(base_asset, None).await?.amount; - Ok(max_borrowable) + pub async fn extract_base(&mut self, ticker: String) -> Result> { + let symbol = self.get_symbol(ticker).await?; + Ok(symbol.base_asset) } - pub async fn update_exchange_info(&mut self) -> Result<(), Box> { - self.exchange_info - .lock() + pub async fn extract_quote(&mut self, ticker: String) -> Result> { + let symbol = self.get_symbol(ticker).await?; + Ok(symbol.quote_asset) + } + + pub async fn get_price(&mut self, ticker: String) -> Result> { + let symbol = self.get_symbol(ticker).await?; + let price = self + .market + .get_price(&symbol.symbol) .await - .replace(self.general.exchange_info().await?); - Ok(()) + .map_err(|e| Box::new(KryptoError::PriceError(e.to_string())))?; + Ok(price.price as f32) } - pub async fn get_balance(&mut self) -> Result> { - let account = self.margin.details().await?; - let total_balance = account.total_net_asset_of_btc; - let btc_price = self.market.get_price("BTCUSDT").await?.price; - let total_balance = total_balance * btc_price; - Ok(total_balance) + pub async fn order(&mut self, order: Order) -> Result> { + let symbol = self.get_symbol(order.symbol.clone()).await?; + let qty = self + .precision(order.symbol.clone()) + .await? + .fmt_quantity(order.quantity as f64)?; + let result = match order.side { + OrderSide::Buy => self.account.market_buy(symbol.symbol, qty).await, + OrderSide::Sell => self.account.market_sell(symbol.symbol, qty).await, + }; + if result.is_err() { + return Err(Box::new(KryptoError::OrderError( + result.err().unwrap().to_string(), + ))); + } + Ok(qty) } - pub async fn close_all_orders(&self, config: &Config) -> Result<(), Box> { + pub async fn set_default_leverages(&mut self, config: &Config) -> Result<(), Box> { + let blacklist = config.blacklist().clone().unwrap_or_default(); for ticker in config.tickers() { - let orders = self - .margin - .orders(MarginOrdersQuery { - symbol: ticker.clone(), - ..Default::default() - }) - .await?; - if !orders.is_empty() { - println!("{} has {} open orders", ticker, orders.len()); - let result = self.margin.cancel_all_orders(ticker, None).await; - if result.is_err() { - println!("Error (Could not cancel order): {}", result.err().unwrap()); - } + if blacklist.contains(ticker) { + continue; } + self.set_leverage(ticker, *config.leverage()).await?; } Ok(()) } + + pub async fn set_leverage( + &mut self, + ticker: &str, + leverage: u8, + ) -> Result<(), Box> { + let symbol = self.get_symbol(ticker.to_owned()).await?; + self.account + .change_initial_leverage(symbol.symbol, leverage) + .await + .map_err(|e| Box::new(KryptoError::LeverageError(e.to_string())))?; + Ok(()) + } + + pub async fn get_total_balance_in(&mut self, asset: &str) -> Result> { + let balances: Vec = self + .account + .account_balance() + .await + .map_err(|e| Box::new(KryptoError::BalanceError(e.to_string())))?; + let mut total = 0.0_f32; + for balance in balances { + if balance.available_balance == 0.0 { + continue; + } + if balance.asset == asset { + total += balance.available_balance as f32; + } else { + let price = self + .get_price(format!("{}{}", balance.asset, asset)) + .await?; + total += balance.available_balance as f32 * price; + } + } + Ok(total) + } + + pub async fn get_balance(&mut self, asset: &str) -> Result> { + let balances: Vec = self + .account + .account_balance() + .await + .map_err(|e| Box::new(KryptoError::BalanceError(e.to_string())))?; + let balance = balances.iter().find(|balance| balance.asset == asset); + if balance.is_none() { + return Err(Box::new(KryptoError::BalanceError(format!( + "Unable to find balance for {}", + asset + )))); + } + let balance = balance.unwrap(); + Ok(balance.available_balance as f32) + } } impl PrecisionData { @@ -162,3 +216,19 @@ impl PrecisionData { Ok(format!("{:.1$}", quantity, self.step_precision).parse::()?) } } + +#[derive(thiserror::Error, Debug)] +pub enum KryptoError { + #[error("Invalid symbol: {0}")] + InvalidSymbol(String), + #[error("Unable to retrieve exchange information: {0}")] + ExchangeInfoError(String), + #[error("Unable to retrieve price: {0}")] + PriceError(String), + #[error("Error sending order: {0}")] + OrderError(String), + #[error("Error updating default leverage: {0}")] + LeverageError(String), + #[error("Error retrieving balance: {0}")] + BalanceError(String), +} diff --git a/src/lib.rs b/src/lib.rs index 85292ac..29233c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,5 @@ pub mod config; pub mod historical_data; pub mod krypto_account; pub mod math; -pub mod order_event; pub mod relationships; pub mod testing; diff --git a/src/main.rs b/src/main.rs index 7574a1f..0018961 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use krypto::{ args::Args, config::Config, historical_data::{calculate_technicals, load, TickerData}, - krypto_account::KryptoAccount, relationships::compute_relationships, testing::PerPeriod, }; @@ -36,25 +35,23 @@ pub async fn main() -> Result<(), Box> { let result = run(&config).await; if result.is_err() { println!("Error: {}", result.err().unwrap()); - let account = KryptoAccount::new(&config); - account.close_all_orders(&config).await?; } } Ok(()) } async fn find_best_parameters(config: &mut Config, candles: &[TickerData]) -> Box { - let mut best_return = 0.0; + let mut best_return = f32::NEG_INFINITY; let mut best_config = config.clone(); let interval_num = config.interval_minutes().unwrap() as usize; let mut results_file = csv::Writer::from_path("results.csv").unwrap(); let headers = vec!["min_score", "depth", "cash", "accuracy", "return"]; results_file.write_record(&headers).unwrap(); - for depth in 3..=10 { + for depth in 2..=10 { let config = config.set_depth(depth); let relationships = compute_relationships(candles, config).await; - for i in 0..=75 { - let min_score = i as f32 / 25.0; + for i in 0..=100 { + let min_score = i as f32 / 10.0; let config = config.set_min_score(Some(min_score)); let test = backtest(candles, &relationships, config).await; let test_return = test.compute_average_return( diff --git a/src/order_event.rs b/src/order_event.rs deleted file mode 100644 index 380608b..0000000 --- a/src/order_event.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::error::Error; - -use binance::rest_model::{ - MarginOrder, MarginOrderQuery, MarginOrderState, OrderResponse, OrderSide, OrderStatus, - OrderType, SideEffectType, TimeInForce, -}; -use chrono::Utc; -use getset::Getters; - -use crate::krypto_account::{KryptoAccount, PrecisionData}; - -const BUY_TICK_SIZE_MULTIPLIER: f64 = 4.0; -const SELL_TICK_SIZE_MULTIPLIER: f64 = 3.0; -const TRADE_PERCENTAGE: f64 = 1.5; - -#[derive(Debug, Clone)] -pub struct OrderDetails { - pub ticker: String, - pub side: OrderSide, - pub quantity: Option, - pub max_time: Option, -} - -#[derive(Getters, Clone)] -#[getset(get = "pub")] -pub struct OrderEvent { - details: OrderDetails, - precision: Box, - account: KryptoAccount, - latest_price: f64, - current_order_price: Option, - current_order_id: Option, - mutable_quantity: f64, -} - -impl OrderEvent { - pub async fn new( - details: OrderDetails, - mut account: KryptoAccount, - ) -> Result> { - let max_borrow = account.max_borrowable(details.ticker.as_str()).await?; - let quantity = details.quantity.unwrap_or(max_borrow * TRADE_PERCENTAGE); - let latest_price = account - .market - .get_price(details.ticker.clone()) - .await? - .price; - let precision = account.get_precision_data(details.ticker.clone()).await?; - let mut event = Self { - details: OrderDetails { - quantity: Some(quantity), - ..details - }, - precision, - account, - latest_price, - current_order_price: None, - current_order_id: None, - mutable_quantity: quantity, - }; - println!("OrderEvent created: {:?}", event); - event.run().await?; - Ok(event) - } - - async fn run(&mut self) -> Result<(), Box> { - self.update_latest_price().await?; - self.place_order_catch().await?; - loop { - let order = self.query_order().await?; - self.update_latest_price().await?; - match order.status { - OrderStatus::New => { - if self.should_update() { - self.cancel_order().await; - println!("Order canceled"); - self.place_order_catch().await?; - } - } - OrderStatus::Filled => { - println!("Order filled"); - break; - } - OrderStatus::PartiallyFilled => { - if self.should_update() { - self.mutable_quantity -= order.executed_qty; - self.cancel_order().await; - println!("Order partially filled but canceled"); - self.place_order_catch().await?; - } - } - _ => { - return Err(Box::new(OrderError::OrderCanceled(format!( - "Unknown order status: {:?}. (You may have canceled an order)", - order.status - )))); - } - } - let wait_time = match self.details.side { - OrderSide::Buy => 200, - OrderSide::Sell => 500, - }; - tokio::time::sleep(tokio::time::Duration::from_millis(wait_time)).await; - if let Some(max_time) = self.details.max_time { - if Utc::now().timestamp() - max_time > 0 { - return Err(Box::new(OrderError::OrderTimeout)); - } - } - } - Ok(()) - } - - async fn place_order_catch(&mut self) -> Result<(), Box> { - let result = self.place_order().await; - if result.is_err() { - self.cancel_order().await; - self.update_latest_price().await?; - self.place_order().await?; - } - Ok(()) - } - - async fn update_latest_price(&mut self) -> Result<(), Box> { - let last_price = self.latest_price; - self.latest_price = self - .account - .market - .get_price(self.details.ticker.clone()) - .await? - .price; - if last_price != self.latest_price { - println!( - "Updated latest price: {} -> {}", - last_price, self.latest_price - ); - } - Ok(()) - } - - fn get_order(&self) -> Result> { - let client_order_id = format!("{}-{}", self.details.ticker, Utc::now().timestamp()); - let quantity = self.precision.fmt_quantity(self.mutable_quantity)?; - let price = self.precision.fmt_price(self.get_enter_price())?; - Ok(MarginOrder { - symbol: self.details.ticker.clone(), - side: self.details.side.clone(), - order_type: OrderType::Limit, - quantity: Some(quantity), - quote_order_qty: None, - price: Some(price), - stop_price: None, - new_client_order_id: Some(client_order_id), - iceberg_qty: None, - new_order_resp_type: OrderResponse::Full, - time_in_force: Some(TimeInForce::GTC), - is_isolated: None, - side_effect_type: match self.details.side { - OrderSide::Buy => SideEffectType::MarginBuy, - OrderSide::Sell => SideEffectType::AutoRepay, - }, - }) - } - - async fn place_order(&mut self) -> Result<(), Box> { - let order = self.get_order()?; - let order = self.account.margin.new_order(order).await.map_err(|e| { - Box::new(OrderError::OrderError(format!( - "Error placing order: {}", - e - ))) - })?; - self.current_order_id = Some(order.order_id); - self.current_order_price = Some(order.price); - println!( - "Placed LIMIT order: {} {:?} {} at ${}", - order.symbol, order.side, order.orig_qty, order.price - ); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - Ok(()) - } - - async fn cancel_order(&mut self) { - let result = self - .account - .margin - .cancel_all_orders(self.details.ticker.clone(), None) - .await; - match result { - Ok(_) => {} - Err(e) => { - println!("Error canceling order: {} (This may be an issue with the API and the order may be cancelled).", e); - } - } - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - } - - async fn query_order(&mut self) -> Result> { - let order_id = self.current_order_id.unwrap().to_string(); - let query = MarginOrderQuery { - symbol: self.details.ticker.clone(), - order_id: Some(order_id), - ..Default::default() - }; - let order = self.account.margin.order(query).await.map_err(|e| { - Box::new(OrderError::OrderQueryError(format!( - "Error querying order: {}", - e - ))) - })?; - Ok(order) - } - - #[inline] - fn get_difference(&self) -> f64 { - match self.details.side { - OrderSide::Buy => -self.precision.tick_size() * BUY_TICK_SIZE_MULTIPLIER, - OrderSide::Sell => *self.precision.tick_size() * SELL_TICK_SIZE_MULTIPLIER, - } - } - - #[inline] - fn get_enter_price(&self) -> f64 { - self.latest_price + self.get_difference() - } - - #[inline] - fn should_update(&self) -> bool { - let price_to_order_dif = self.current_order_price.unwrap() - self.latest_price; - let buffer = self.get_difference(); - match self.details.side { - OrderSide::Buy => price_to_order_dif < buffer - self.precision.tick_size(), - OrderSide::Sell => price_to_order_dif > buffer + self.precision.tick_size(), - } - } -} - -impl core::fmt::Debug for OrderEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OrderEvent") - .field("details", &self.details) - .field("precision", &self.precision) - .field("latest_price", &self.latest_price) - .field("current_order_price", &self.current_order_price) - .field("current_order_id", &self.current_order_id) - .field("mutable_quantity", &self.mutable_quantity) - .finish() - } -} - -#[derive(thiserror::Error, Debug)] -pub enum OrderError { - #[error("Order timed out")] - OrderTimeout, - #[error("Order error: {0}")] - OrderError(String), - #[error("Order status error: {0}")] - OrderQueryError(String), - #[error("Order canceled unexpectedly: {0}")] - OrderCanceled(String), -} diff --git a/src/relationships.rs b/src/relationships.rs index 55b6da6..e17ca60 100644 --- a/src/relationships.rs +++ b/src/relationships.rs @@ -102,14 +102,16 @@ pub async fn predict( let blacklist_indexes = blacklist_indexes_task.await.unwrap(); let mut max_index = None; - let mut max = None; + let mut max: Option = None; for (i, score) in scores.iter().enumerate().skip(1) { - if (max_index.is_none() || score > max.unwrap()) && !blacklist_indexes.contains(&i) { + if (max_index.is_none() || score.abs() > max.unwrap().abs()) + && !blacklist_indexes.contains(&i) + { max_index = Some(i); - max = Some(score); + max = Some(*score); } } - (max_index.unwrap(), *max.unwrap()) + (max_index.unwrap(), max.unwrap()) } async fn get_blacklist_indexes(tickers: Vec, blacklist: Vec) -> Vec { diff --git a/src/testing.rs b/src/testing.rs index 71c7d22..ea0496b 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -149,4 +149,4 @@ pub const fn test_headers() -> [&'static str; 9] { "Change (%)", "Time", ] -} \ No newline at end of file +}