From 0eca6971dbcc680fca71746908e1c336844fe937 Mon Sep 17 00:00:00 2001 From: Sik Yoon Date: Mon, 20 May 2024 03:16:21 +0900 Subject: [PATCH] Implement entry positioning --- src/future/mod.rs | 223 ++++++ src/future/order.rs | 1027 +++++++++++++++++++++++++ src/future/table_mgmt.rs | 314 ++++++++ src/initialization.rs | 109 +++ src/lib.rs | 4 +- src/main.rs | 216 +++++- src/strategy_team/future_strategy.rs | 540 +++---------- src/strategy_team/mod.rs | 45 +- src/strategy_team/strategy_manager.rs | 60 +- 9 files changed, 2081 insertions(+), 457 deletions(-) create mode 100644 src/future/mod.rs create mode 100644 src/future/order.rs create mode 100644 src/future/table_mgmt.rs diff --git a/src/future/mod.rs b/src/future/mod.rs new file mode 100644 index 0000000..41e074d --- /dev/null +++ b/src/future/mod.rs @@ -0,0 +1,223 @@ +pub mod order; +pub mod table_mgmt; + +use hmac_sha256::HMAC; +use crate::RunningMode::*; +use crate::{API_KEY, API_KEY_TESTNET, RUNNING_MODE, SECRET_KEY, SECRET_KEY_TESTNET, FUTURES_URL, FUTURES_URL_TEST}; +use crate::strategy_team::{AllData, TimeData}; +use crate::database_control::*; +use rust_decimal::Decimal; +use sqlx::FromRow; +use hex::ToHex; +use crate::future::table_mgmt::select_listuped_positions; + +#[derive(Debug, PartialEq, Clone, sqlx::Type)] +pub enum Position { + #[sqlx(rename = "long")] + Long, + #[sqlx(rename = "short")] + Short +} + +#[derive(Debug, PartialEq, Clone, sqlx::Type)] +pub enum Status { + #[sqlx(rename = "listup")] + Listup, + #[sqlx(rename = "filled")] + Filled, + #[sqlx(rename = "partially_filled")] + PartiallyFilled +} + +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +pub struct EntryCoinInfo { + pub symbol: String, + pub close_time: i64, + pub registered_server_epoch: i64, +} + +impl EntryCoinInfo { + fn new() -> EntryCoinInfo { + let a = EntryCoinInfo { + symbol: String::new(), + close_time: 0, + registered_server_epoch: 0 + }; + a + } +} + +#[derive(Debug, Clone)] +pub enum ContractType { + Perpetual, // this is the target + CurrentMonth, + NextMonth, + CurrentQuarter, + NextQuarter, + PerpetualDelivering, +} + +#[derive(Debug, Clone)] +pub enum ContractStatus { + PendingTrading, // this is the target + Trading, + PreDelivering, + Delivering, + Delivered, + PreSettle, + Settling, + Close, +} + +#[derive(Debug, FromRow, Clone)] +pub struct FuturesExchangeInfo { + pub stepsize: Decimal, + pub ticksize: Decimal, + pub contract_type: ContractType, + pub contract_status: ContractStatus, + pub base_asset_precision: u32, + pub quote_precision: u32, + pub notional: Decimal +} + +impl FuturesExchangeInfo { + fn new() -> FuturesExchangeInfo { + let a = FuturesExchangeInfo { + stepsize: Decimal::new(0, 8), + ticksize: Decimal::new(0, 8), + contract_type: ContractType::Perpetual, + contract_status: ContractStatus::Trading, + base_asset_precision: 0, + quote_precision: 0, + notional: Decimal::new(0, 8), + }; + a + } +} + +#[derive(Clone, Debug)] +pub struct FuturesTradeFee { + user_level: Option, + taker_fee_percent: Decimal, + maker_fee_percent: Decimal, +} + +impl FuturesTradeFee { + pub fn new() -> FuturesTradeFee { + let a = FuturesTradeFee { + user_level: None, + taker_fee_percent: Decimal::new(0, 8), + maker_fee_percent: Decimal::new(0, 8), + }; + a + } +} +#[derive(Debug, FromRow, Clone)] +pub struct PositionCoinList { + pub id: u64, + pub order_type: String, + pub status: String, + pub symbol: String, + pub order_id: u64, + pub position: Position, + pub registered_server_epoch: i64, + pub transact_time: i64, + pub close_time: i64, + pub used_usdt: Decimal, + pub expected_get_usdt: f64, + pub expected_usdt_profit: f64, + pub entry_price: Decimal, + pub current_price: Decimal, + pub base_qty_ordered: Decimal, + pub base_qty_fee_adjusted: Decimal, + pub pure_profit_percent: f64, + pub minimum_profit_percent: f64, + pub maximum_profit_percent: f64, +} + +impl PositionCoinList { + fn new() -> PositionCoinList { + let a = PositionCoinList{ + id: 0, + order_type: String::new(), + status: String::new(), + symbol: String::new(), + order_id: 0, + position: Position::Short, + registered_server_epoch: 0, + transact_time: 0, + close_time: 0, + used_usdt: Decimal::new(0, 8), + expected_get_usdt: 0.0, + expected_usdt_profit: 0.0, + entry_price: Decimal::new(0, 8), + current_price: Decimal::new(0, 8), + base_qty_ordered: Decimal::new(0, 8), + base_qty_fee_adjusted: Decimal::new(0, 8), + pure_profit_percent: 0.0, + minimum_profit_percent: 0.0, + maximum_profit_percent: 0.0, + }; + a + } +} + +async fn hmac_signature(query: &mut String) { + // fetch time information from [time] table + let table_name = String::from("time"); + let columns = String::from("*"); + let condition = None; + let mut time_info = TimeData { + server_epoch: 0, + local_epoch: 0, + epoch_difference: 0, + server_ymdhs: String::new(), + local_ymdhs: String::new(), + last_server_epoch: 0, + last_server_ymdhs: String::new(), + }; + + let select_result = select_record(&table_name, &columns, &condition, &time_info) + .await + .unwrap(); + let difference_epoch = select_result.first().unwrap().epoch_difference; + let server_epoch = select_result.first().unwrap().server_epoch; + + // build query message + // local 시간이 server 시간보다 너무 앞서거나 뒤쳐지면 USER_DATA를 요청할 수 없으므로 local과 server 의 시간 차이만큼을 local에서 빼어 보정한 뒤 + // 이를 timestamp로 사용한다. + let mut timestamp; + if difference_epoch >= 0 { + timestamp = (server_epoch as u128 + difference_epoch as u128).to_string(); + } else if difference_epoch < 0 { + timestamp = (server_epoch as u128 + (difference_epoch * -1) as u128).to_string(); + } else { + timestamp = server_epoch.to_string(); + } + let recv_window_size = "5000".to_string(); // default: 5,000ms, Max: 60,000ms + + let mut query_build = String::from("×tamp="); + query_build.push_str(×tamp); + query_build.push_str("&recvWindow="); + query_build.push_str(&recv_window_size); + + let mut secret_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + secret_key.push_str(SECRET_KEY_TESTNET); + } else { + secret_key.push_str(SECRET_KEY); + } + } + + query.push_str(&query_build); + let signature = HMAC::mac(&query.as_bytes(), secret_key.as_bytes()); + + query.push_str("&signature="); + query.push_str(signature.encode_hex::().as_str()); +} diff --git a/src/future/order.rs b/src/future/order.rs new file mode 100644 index 0000000..f599e04 --- /dev/null +++ b/src/future/order.rs @@ -0,0 +1,1027 @@ +// use crates +use crate::coex::assets_managing_team::*; +use crate::coex::exchange_team::*; +use crate::coin_health_check_team::request_others::TradeFee; +use crate::database_control::*; +use crate::decimal_funcs::*; +use crate::signal_association::signal_decision::*; +use crate::strategy_team::{AllData, TimeData}; +use crate::value_estimation_team::datapoints::price_data::RealtimePriceData; +use crate::value_estimation_team::indicators::ema::EmaData; +use crate::value_estimation_team::indicators::sma::SmaData; +use crate::value_estimation_team::indicators::stoch_rsi::StochRsiData; +use crate::value_estimation_team::indicators::supertrend::{supertrend, SupertrendData}; +use futures::future::try_join_all; +use log; +use reqwest::{Client, ClientBuilder}; +use rust_decimal::{prelude::FromPrimitive, prelude::ToPrimitive, Decimal, RoundingStrategy}; +use rust_decimal_macros::dec; +use serde_json::Value; +use sqlx::FromRow; +use std::collections::HashMap; +use tokio::time::*; +use super::{hmac_signature, REAL, SIMUL, TEST, RUNNING_MODE, FUTURES_URL, FUTURES_URL_TEST, API_KEY, API_KEY_TESTNET, Position, FuturesExchangeInfo, PositionCoinList, FuturesTradeFee, EntryCoinInfo, select_listuped_positions}; +use crate::strategy_team::future_strategy; + +pub enum TimeInForce { + Gtc, + Ioc, + Fok, +} + +#[derive(PartialEq)] +pub enum MarginType { + Isolated, + Crossed +} + +pub async fn entry_position( + price_map: &HashMap, + futures_exchange_info_map: &HashMap, + future_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + let mut available_usdt = get_future_available_usdt().await?; + let mut unit_trade_usdt = get_unit_trade_usdt().await; + + if !available_usdt.is_zero() && !unit_trade_usdt.is_zero() && available_usdt >= unit_trade_usdt && future_trade_fee.user_level.is_some() { + let listup_positions = select_listuped_positions().await?; + + let server_epoch = get_server_epoch().await; + let client = ClientBuilder::new() + .timeout(tokio::time::Duration::from_millis(3000)) + .build() + .unwrap(); + for element in &listup_positions { + available_usdt = get_future_available_usdt().await?; + unit_trade_usdt = get_unit_trade_usdt().await; + if !available_usdt.is_zero() && !unit_trade_usdt.is_zero() && available_usdt >= unit_trade_usdt && futures_exchange_info_map.contains_key(&element.symbol) && price_map.get(&element.symbol).is_some() { + let futures_exchange_info = futures_exchange_info_map.get(&element.symbol).unwrap(); + let lot_step_size = futures_exchange_info.stepsize; + let tick_size = futures_exchange_info.ticksize; + let base_asset_precision = futures_exchange_info.base_asset_precision; + let trade_fee = future_trade_fee.maker_fee_percent; + let entry_price = rust_decimal::prelude::FromPrimitive::from_f64(*price_map.get(&element.symbol).unwrap()).unwrap(); + + // calculate minimum order usdt >= unit_trade_usdt + let notional = futures_exchange_info.notional; + let mut minimum_quantity = decimal_div(notional, entry_price) + .round_dp_with_strategy(lot_step_size.normalize().scale(), RoundingStrategy::ToPositiveInfinity); + let mut minimum_order_usdt = decimal_mul(minimum_quantity, entry_price) + .round_dp_with_strategy(tick_size.normalize().scale(), RoundingStrategy::ToPositiveInfinity); + + if minimum_order_usdt <= unit_trade_usdt { + let mut base_qty_ordered = Decimal::new(0, 8); + base_qty_ordered = decimal_div(unit_trade_usdt, entry_price) + .round_dp_with_strategy( + lot_step_size.normalize().scale(), + RoundingStrategy::ToZero, + ); + + let mut base_qty_fee_adjusted = Decimal::new(0, 8); + base_qty_fee_adjusted = + decimal_mul(base_qty_ordered, decimal_sub(dec!(1), trade_fee)) + .round_dp_with_strategy( + base_asset_precision, + RoundingStrategy::ToZero, + ); + + let mut used_usdt = Decimal::new(0, 8); + used_usdt = decimal_mul(base_qty_ordered, entry_price) + .round_dp_with_strategy( + tick_size.normalize().scale(), + RoundingStrategy::ToZero, + ); + // let expected_pure_profit_percent: f64 = decimal_sub( + // decimal_div( + // decimal_mul(base_qty_fee_adjusted, entry_price) + // .round_dp_with_strategy( + // tick_size.normalize().scale(), + // RoundingStrategy::ToZero, + // ), + // used_usdt, + // ), + // dec!(1), + // ) + // .to_f64() + // .unwrap() + // * 100.0; + + // order the symbol based on base_qty_ordered and current_price + limit_order_entry( + &element.position, + &element, + futures_exchange_info_map, + trade_fee, + TimeInForce::Gtc, + entry_price, + base_qty_ordered, + used_usdt, + &base_qty_fee_adjusted.to_string(), + &client, + ) + .await; + } + } else { + break; + } + } + } + Ok(()) +} + +pub async fn limit_order_entry( + position: &Position, + entry_coin_info: &PositionCoinList, + exchange_info_map: &HashMap, + trade_fee: Decimal, + tif: TimeInForce, + order_price: Decimal, + order_quantity: Decimal, + used_usdt: Decimal, + simul_base_qty_fee_adjusted: &String, + client: &Client, +) -> Result<(), Box> { + let update_table_name = String::from("future_ordered_coin_list"); + let server_epoch = get_server_epoch().await; + unsafe { + if RUNNING_MODE == SIMUL { + let mut update_values = vec![ + (String::from("order_type"), entry_coin_info.order_type.clone()), + (String::from("status"), String::from("SIMUL")), + (String::from("transact_time"), server_epoch.to_string()), + (String::from("close_time"), entry_coin_info.close_time.to_string()), + (String::from("used_usdt"), used_usdt.to_string()), + (String::from("entry_price"), order_price.to_string()), + (String::from("current_price"), order_price.to_string()), + (String::from("base_qty_ordered"), order_quantity.to_string()), + (String::from("base_qty_fee_adjusted"), simul_base_qty_fee_adjusted.to_string()) + ]; + + if *position == Position::Long { + update_values.push((String::from("position"), String::from("LONG"))); + } else if *position == Position::Short { + update_values.push((String::from("position"), String::from("SHORT"))); + } + + let update_condition = vec![(String::from("id"), entry_coin_info.id.to_string())]; + update_record3(&update_table_name, &update_values, &update_condition) + .await + .unwrap(); + + sub_future_available_usdt(used_usdt).await; + + println!("SIMUL {} {}", position.to_string(), entry_coin_info.symbol); + } else { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + + let endpoint_url = "/fapi/v1/order?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + + // add parameters into URL + url_build.push_str("&symbol="); + url_build.push_str(&entry_coin_info.symbol); + if *position == Position::Long { + url_build.push_str("&side=BUY"); + } else { + url_build.push_str("&side=SELL"); + } + url_build.push_str("&type=LIMIT"); + url_build.push_str("&quantity="); + url_build.push_str(order_quantity.to_string().as_str()); + url_build.push_str("&price="); + url_build.push_str(order_price.to_string().as_str()); + match tif { + TimeInForce::Gtc => { + url_build.push_str("&timeInForce=GTC"); + } + TimeInForce::Ioc => { + url_build.push_str("&timeInForce=IOC"); + } + TimeInForce::Fok => { + url_build.push_str("&timeInForce=FOK"); + } + } + + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .post(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await + .unwrap(); + + let body = res.text_with_charset("utf-8").await.unwrap(); + // deserialize JSON and then insert record into table + let v = serde_json::from_str::(body.as_str()); + + match v { + Ok(T) => { + if T.get("code").is_some() { // when request failed + } else { + // when request succeed + let mut update_values = vec![]; + update_values.push((String::from("order_type"), entry_coin_info.order_type.clone())); + update_values.push((String::from("order_id"), T.get("orderId").unwrap().as_u64().unwrap().to_string())); + update_values.push((String::from("transact_time"), server_epoch.to_string())); + update_values.push((String::from("close_time"), entry_coin_info.close_time.to_string())); + + // status + if T.get("status").unwrap().as_str().unwrap() == "NEW" { + update_values.push((String::from("status"), String::from("NEW"))); + } else if T.get("status").unwrap().as_str().unwrap() == "FILLED" { + update_values.push((String::from("status"), String::from("FILLED"))); + println!("{} {}", position.to_string(), entry_coin_info.symbol); + } else if T.get("status").unwrap().as_str().unwrap() == "PARTIALLY_FILLED" { + update_values.push((String::from("status"), String::from("PARTIALLY_FILLED"))); + println!("Partially filled {} {}", position.to_string(), entry_coin_info.symbol); + } + + let base_qty_ordered = rust_decimal::prelude::FromStr::from_str( + T.get("origQty").unwrap().as_str().unwrap(), + ) + .unwrap(); + let base_qty_fee_adjusted = + decimal_mul(base_qty_ordered, decimal_sub(dec!(1), trade_fee)); + + let cummulative_quote_qty = rust_decimal::prelude::FromStr::from_str( + T.get("cumQuote").unwrap().as_str().unwrap(), + ) + .unwrap(); + // let base_asset_precision = exchange_info_vec + // .iter() + // .find(|exchange_info| exchange_info.symbol == element.symbol) + // .unwrap() + // .base_asset_precision; + let entry_price = decimal_div(cummulative_quote_qty, base_qty_ordered) + .round_dp_with_strategy(8, RoundingStrategy::ToZero); + + update_values.push((String::from("used_usdt"), cummulative_quote_qty.to_string())); + update_values.push((String::from("entry_price"), entry_price.to_string())); + update_values.push((String::from("base_qty_ordered"), base_qty_ordered.to_string())); + update_values.push((String::from("base_qty_fee_adjusted"), base_qty_fee_adjusted.to_string())); + + // reflect available_usdt in [asset_manage_announcement] + // sub_available_usdt(cummulative_quote_qty).await; + sub_future_available_usdt(cummulative_quote_qty).await; + + let update_condition = vec![(String::from("id"), entry_coin_info.id.to_string())]; + update_record3(&update_table_name, &update_values, &update_condition) + .await + .unwrap(); + } + }, + Err(e) => { + log::warn!("order failed!: {}", body); + } + } + } + } + Ok(()) +} + +pub async fn monitoring_unfilled_order( + client: &Client, + future_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + let open_positioning_orders = select_open_positioning_orders().await; + + if !open_positioning_orders.is_empty() { + let server_epoch = get_server_epoch().await; + let orders_outdated = open_positioning_orders + .iter() + .filter(|&element| server_epoch - element.transact_time >= 30_000) + .collect::>(); // wait up to 60 secs + let orders_to_be_queried = open_positioning_orders + .iter() + .filter(|&element| server_epoch - element.transact_time < 30_000) + .collect::>(); + + // cancel orders outdated over 3mins and delete the records in [buy_ordered_coin_list] + if !orders_outdated.is_empty() { + for element in orders_outdated { + cancel_open_positioning_order(element, &client, future_trade_fee).await; + sleep(Duration::from_millis(200)).await; + } + } + + if !orders_to_be_queried.is_empty() { + for element in orders_to_be_queried { + query_open_positioning_order(element, &client, future_trade_fee).await; + sleep(Duration::from_millis(200)).await; + } + } + } + + let open_closing_orders = select_open_closing_orders().await; + if !open_closing_orders.is_empty() { + } + + Ok(()) +} + +// Cancel an NEW or PARTIALLY FILLED order. (/api, Weight(IP) 1) +pub async fn cancel_open_positioning_order( + order: &PositionCoinList, + client: &Client, + future_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else if RUNNING_MODE == REAL { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/order?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + // add parameters into URL + url_build.push_str("&symbol="); + url_build.push_str(&order.symbol); + url_build.push_str("&orderId="); + url_build.push_str(order.order_id.to_string().as_str()); + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .delete(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await.unwrap(); + // println!("cancel_buy_order실행 body: {}", body); + // deserialize JSON and then insert record into table + let v = serde_json::from_str::(body.as_str()); + + match v { + Ok(T) => { + if T.get("status").is_some() { + // case that the order is canceled successfully + if T.get("status").unwrap().as_str().unwrap() == "CANCELED" { + let table_name = String::from("future_ordered_coin_list"); + let cummulative_quote_qty: Decimal = rust_decimal::prelude::FromStr::from_str( + T.get("cumQuote").unwrap().as_str().unwrap(), + ) + .unwrap(); + + if cummulative_quote_qty.is_zero() { + // NOT Filled case + // delete record in [future_ordered_coin_list] + let mut condition_build = String::from("WHERE order_id = "); + condition_build.push_str(order.order_id.to_string().as_str()); + condition_build.push_str(" AND symbol = \'"); + condition_build.push_str(&order.symbol); + condition_build.push('\''); + delete_record(&table_name, &condition_build).await; + } else { + // Patially Filled case + // update values in [future_ordered_coin_list] + + // calculate values to be updated + if future_trade_fee.user_level.is_some() { + let trade_fee = future_trade_fee.maker_fee_percent; + let base_qty_ordered = rust_decimal::prelude::FromStr::from_str( + T.get("executedQty").unwrap().as_str().unwrap(), + ) + .unwrap(); + + let base_qty_fee_adjusted = + decimal_mul(base_qty_ordered, decimal_sub(dec!(1), trade_fee)); + let buy_price = decimal_div(cummulative_quote_qty, base_qty_ordered) + .round_dp_with_strategy(8, RoundingStrategy::ToZero); + + let update_values = vec![ + (String::from("status"), String::from("FILLED")), // status + (String::from("used_usdt"), cummulative_quote_qty.to_string()), // used_usdt + (String::from("buy_price"), buy_price.to_string()), // buy_price + ( + String::from("base_qty_ordered"), + base_qty_ordered.to_string(), + ), // base_qty_ordered + ( + String::from("base_qty_fee_adjusted"), + base_qty_fee_adjusted.to_string(), + ), // base_qty_fee_adjusted + ]; + let update_condition = vec![ + (String::from("order_id"), order.order_id.to_string()), + (String::from("symbol"), order.symbol.clone()), + ]; + update_record3(&table_name, &update_values, &update_condition) + .await + .unwrap(); + + println!("partially Positioning {} {}", order.position, order.symbol); + } + } + } + } else if T.get("code").is_some() { + // case that the order isn't canceled because the order completes while canceling + // update record in ordered_coin_list + query_open_positioning_order(order, &client, future_trade_fee).await; + } + } + Err(e) => { + query_open_positioning_order(order, &client, future_trade_fee).await; + log::warn!("cancel order buy failed!: {}", body); + } + } + + Ok(()) +} + +// query order and update record in buy_ordered_coin_list (/api, Weight(IP) 1) +pub async fn query_open_positioning_order( + order: &PositionCoinList, + client: &Client, + future_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else if RUNNING_MODE == REAL { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/order?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + + // add parameters into URL + url_build.push_str("&symbol="); + url_build.push_str(&order.symbol); + url_build.push_str("&orderId="); + url_build.push_str(order.order_id.to_string().as_str()); + + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .get(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await.unwrap(); + // println!("query_buy_order: {}", body); + // deserialize JSON and then update record in table + let v = serde_json::from_str::(body.as_str()); + + match v { + Ok(T) => { + if T.get("status").is_some() { + if T.get("status").unwrap().as_str().unwrap() == "FILLED" + || T.get("status").unwrap().as_str().unwrap() == "PARTIALLY_FILLED" + { + // update values in [future_ordered_coin_list] + let table_name = String::from("future_ordered_coin_list"); + let mut value_build = String::from("\'"); + value_build.push_str(T.get("status").unwrap().as_str().unwrap()); + value_build.push('\''); + + // calculate values to be updated + if future_trade_fee.user_level.is_some() { + let trade_fee = future_trade_fee.maker_fee_percent; + let base_qty_ordered = rust_decimal::prelude::FromStr::from_str( + T.get("executedQty").unwrap().as_str().unwrap(), + ) + .unwrap(); + let base_qty_fee_adjusted = + decimal_mul(base_qty_ordered, decimal_sub(dec!(1), trade_fee)); + + let cummulative_quote_qty = rust_decimal::prelude::FromStr::from_str( + T.get("cumQuote").unwrap().as_str().unwrap(), + ) + .unwrap(); + + let buy_price = decimal_div(cummulative_quote_qty, base_qty_ordered) + .round_dp_with_strategy(8, RoundingStrategy::ToZero); + + let update_values = vec![ + (String::from("status"), value_build), // status + (String::from("used_usdt"), cummulative_quote_qty.to_string()), // used_usdt + (String::from("buy_price"), buy_price.to_string()), // buy_price + ( + String::from("base_qty_ordered"), + base_qty_ordered.to_string(), + ), // base_qty_ordered + ( + String::from("base_qty_fee_adjusted"), + base_qty_fee_adjusted.to_string(), + ), // base_qty_fee_adjusted + ]; + let update_condition = vec![ + (String::from("order_id"), order.order_id.to_string()), + (String::from("symbol"), order.symbol.clone()), + ]; + update_record3(&table_name, &update_values, &update_condition) + .await + .unwrap(); + + if T.get("status").unwrap().as_str().unwrap() == "FILLED" { + println!("Positioning {} {}", order.position, order.symbol); + // update available_usdt + if order.used_usdt > cummulative_quote_qty { + add_available_usdt(decimal_sub( + order.used_usdt, + cummulative_quote_qty, + )); + } else { + sub_available_usdt(decimal_sub( + cummulative_quote_qty, + order.used_usdt, + )); + } + } + } + } else if T.get("status").unwrap().as_str().unwrap() == "CANCELED" { + let update_table_name = String::from("future_ordered_coin_list"); + let update_values = vec![(String::from("status"), String::from("CANCELED"))]; + let update_condition = vec![ + (String::from("order_id"), order.order_id.to_string()), + (String::from("symbol"), order.symbol.clone()), + ]; + update_record3(&update_table_name, &update_values, &update_condition) + .await + .unwrap(); + } + } + } + Err(e) => { + log::warn!("query order failed!: {}", body); + } + } + Ok(()) +} + +// +pub async fn set_initial_leverage(symbol: &String, leverage_ratio: i32, client: &Client) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/leverage?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + url_build.push_str("&symbol="); + url_build.push_str(symbol); + url_build.push_str("&leverage="); + url_build.push_str(leverage_ratio.to_string().as_str()); + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .post(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v = serde_json::from_str::(body.as_str()); + match v { + Ok(T) => { + if let (Some(leverage), Some(symbol), Some(max_notional_value)) = (T.get("leverage"), T.get("symbol"), T.get("maxNotionalValue")) { + log::info!("symbol: {:?}. leverage: {:?}, max_notional_value: {:?}", symbol, leverage, max_notional_value); + } else { + log::error!("Endpoint(/fapi/v1/leverage?) output changed!"); + return Err("Err")?; + } + } + Err(e) => { + log::warn!("account information failed!: {}, {}", body, e); + } + } + Ok(()) +} + +pub async fn set_margin_type(symbol: &String, margin_type: MarginType, client: &Client) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/marginType?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + url_build.push_str("&symbol="); + url_build.push_str(symbol); + url_build.push_str("&marginType="); + if margin_type == MarginType::Isolated { + url_build.push_str("ISOLATED"); + } else { + url_build.push_str("CROSSED"); + } + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .post(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v = serde_json::from_str::(body.as_str()); + match v { + Ok(T) => { + if let (Some(code), Some(msg)) = (T.get("code"), T.get("msg")) { + log::info!("code: {:?}. msg: {:?}", code, msg); + } else { + log::error!("Endpoint(/fapi/v1/marginType?) output changed!"); + return Err("Err")?; + } + } + Err(e) => { + log::warn!("account information failed!: {}, {}", body, e); + } + } + Ok(()) +} + +// Basic: One-way Mode +pub async fn set_position_mode(client: &Client) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/positionSide/dual?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + url_build.push_str("&dualSidePosition="); + url_build.push_str("false"); // true: Hedge Mode, false: One-way Mode + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .post(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v = serde_json::from_str::(body.as_str()); + match v { + Ok(T) => { + if let (Some(code), Some(msg)) = (T.get("code"), T.get("msg")) { + log::info!("code: {:?}. msg: {:?}", code, msg); + } else { + log::error!("Endpoint(/fapi/v1/marginType?) output changed!"); + return Err("Err")?; + } + } + Err(e) => { + log::warn!("account information failed!: {}, {}", body, e); + } + } + Ok(()) +} + +// Basic: Single-Asset Mode +pub async fn set_asset_mode(client: &Client) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v1/multiAssetsMargin?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + url_build.push_str("&multiAssetsMargin="); + url_build.push_str("false"); // true: Multi-Assets Mode, false: Single-Asset Mode + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .post(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v = serde_json::from_str::(body.as_str()); + match v { + Ok(T) => { + if let (Some(code), Some(msg)) = (T.get("code"), T.get("msg")) { + log::info!("code: {:?}. msg: {:?}", code, msg); + } else { + log::error!("Endpoint(/fapi/v1/marginType?) output changed!"); + return Err("Err")?; + } + } + Err(e) => { + log::warn!("account information failed!: {}, {}", body, e); + } + } + Ok(()) +} + +pub async fn get_last_price(client: &Client, price_map: &mut HashMap,) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v2/ticker/price"; + url.push_str(endpoint_url); + + let res = client + .get(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v: Value = serde_json::from_str(body.as_str())?; + let into_vec = v.as_array(); + if into_vec == None { + return Err("Err")?; + } + let mut object_map = &serde_json::map::Map::new(); + + let mut symbol = String::new(); + let mut price = 0.0; + for element in into_vec.unwrap() { + object_map = element.as_object().unwrap(); + let mut object_map_iter = object_map.iter(); + + for element in object_map_iter { + match element.0.as_str() { + "symbol" => symbol = element.1.as_str().unwrap().to_string(), + "price" => price = element.1.as_str().unwrap().parse::().unwrap(), + _ => { + log::error!("Elements in body msg are changed. Please update both your coinprices table and vectors."); + } + } + price_map.insert(symbol.clone(), price); + } + } + Ok(()) +} + +// request exchange information. (/api, Weight(IP) 1) +pub async fn request_future_exchange_infomation( + client: &Client, + exchange_info_map: &mut HashMap, +) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + + url.push_str("https://fapi.binance.com"); + + let endpoint_url = "/fapi/v1/exchangeInfo"; + url.push_str(endpoint_url); + + let mut res = client.get(&url).send().await?; + + while !res.status().is_success() { + sleep(Duration::from_secs(5)).await; + res = client.get(&url).send().await?; + } + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v: Value = serde_json::from_str(body.as_str())?; + let mut into_vec = v.as_object(); + if into_vec == None { + return Err("Err")?; + } + + let mut symbol = String::new(); + let mut exchange_info = FuturesExchangeInfo::new(); + let mut data_map_temp: HashMap = HashMap::new(); + + for element in into_vec.unwrap() { + if element.0.contains("symbols") { + for element in element.1.as_array().unwrap() { + if element.is_object() { + if element + .get("symbol") + .unwrap() + .as_str() + .unwrap() + .ends_with("USDT") + { + if element.get("contractType").is_some_and(|a| a.as_str().is_some_and(|b| b == "PERPETUAL")) { + symbol = (element.get("symbol").unwrap().as_str().unwrap().to_string()); + exchange_info.base_asset_precision = + (element.get("baseAssetPrecision").unwrap().as_u64().unwrap()) as u32; + exchange_info.quote_precision = + (element.get("quotePrecision").unwrap().as_u64().unwrap()) as u32; + for element in element.get("filters").unwrap().as_array().unwrap() { + if element + .as_object() + .unwrap() + .get("filterType") + .unwrap() + .as_str() + .unwrap() + .starts_with("LOT_SIZE") + { + exchange_info.stepsize = rust_decimal::prelude::FromStr::from_str( + element.get("stepSize").unwrap().as_str().unwrap(), + ) + .unwrap(); + } else if element + .as_object() + .unwrap() + .get("filterType") + .unwrap() + .as_str() + .unwrap() + .starts_with("PRICE_FILTER") + { + exchange_info.ticksize = rust_decimal::prelude::FromStr::from_str( + element.get("tickSize").unwrap().as_str().unwrap(), + ) + .unwrap(); + } else if element + .as_object() + .unwrap() + .get("filterType") + .unwrap() + .as_str() + .unwrap() + .starts_with("MIN_NOTIONAL") + { + exchange_info.notional = rust_decimal::prelude::FromStr::from_str( + element.get("notional").unwrap().as_str().unwrap(), + ) + .unwrap(); + } + } + data_map_temp.insert(symbol.clone(), exchange_info.clone()); + } + } + } + } + } + } + + *exchange_info_map = data_map_temp; + + Ok(()) +} + +// subtract available USDT +pub async fn sub_future_available_usdt(sub_usdt: Decimal) { + let update_table_name = String::from("future_available_balance"); + let mut value_build = String::from("available_usdt - "); + value_build.push_str(sub_usdt.to_string().as_str()); + + let update_values = vec![(String::from("available_usdt"), value_build)]; + let update_condition = vec![(String::from("id"), String::from("1"))]; + update_record3(&update_table_name, &update_values, &update_condition) + .await + .unwrap(); +} + +// get available USDT +pub async fn get_future_available_usdt() -> Result> { + let select_table_name = String::from("future_available_balance"); + let select_columns = String::from("*"); + let select_condition = None; + + #[derive(FromRow)] + struct AvailableUsdt { + available_usdt: Decimal, + } + let data_struct = AvailableUsdt{ available_usdt: Decimal::new(0, 8) }; + + let select_result = try_select_record( + &select_table_name, + &select_columns, + &select_condition, + &data_struct, + ) + .await; + + if select_result.is_ok() { + Ok(select_result.unwrap().first().unwrap().available_usdt) + } else { + eprint!("get_future_available_usdt() error!"); + Err("error")? + } +} + +async fn select_open_positioning_orders() -> Vec { + let select_table_name = String::from("future_ordered_coin_list"); + let select_columns = String::from("*"); + let select_condition = Some(String::from( + "WHERE order_type = 'POSITIONING' AND (status = 'NEW' OR status = 'PARTIALLY_FILLED')", + )); + let data_struct = PositionCoinList::new(); + let select_result = try_select_record( + &select_table_name, + &select_columns, + &select_condition, + &data_struct, + ) + .await + .unwrap(); + + select_result +} + +async fn select_open_closing_orders() -> Vec { + let select_table_name = String::from("future_ordered_coin_list"); + let select_columns = String::from("*"); + let select_condition = Some(String::from( + "WHERE order_type = 'CLOSING' AND (status = 'NEW' OR status = 'PARTIALLY_FILLED')", + )); + let data_struct = PositionCoinList::new(); + let select_result = try_select_record( + &select_table_name, + &select_columns, + &select_condition, + &data_struct, + ) + .await + .unwrap(); + + select_result +} diff --git a/src/future/table_mgmt.rs b/src/future/table_mgmt.rs new file mode 100644 index 0000000..ea1196e --- /dev/null +++ b/src/future/table_mgmt.rs @@ -0,0 +1,314 @@ +use super::{hmac_signature, REAL, SIMUL, TEST, RUNNING_MODE, FUTURES_URL, FUTURES_URL_TEST, API_KEY, API_KEY_TESTNET, Position, FuturesExchangeInfo, PositionCoinList, FuturesTradeFee}; +use std::collections::HashMap; +use rust_decimal::{Decimal, RoundingStrategy}; +use crate::database_control::*; +use crate::decimal_funcs::*; +use rust_decimal_macros::dec; +use rust_decimal::prelude::ToPrimitive; +use serde_json::Value; +use reqwest::{Client, ClientBuilder}; + +pub async fn get_tradefee_balance(future_trade_fee: &mut FuturesTradeFee, client: &Client) -> Result<(), Box> { + // building URL and API-keys + let mut url = String::new(); + let mut api_key = String::new(); + unsafe { + if RUNNING_MODE == TEST { + url.push_str(FUTURES_URL_TEST); + api_key = API_KEY_TESTNET.to_string(); + } else { + url.push_str(FUTURES_URL); + api_key = API_KEY.to_string(); + } + } + + let endpoint_url = "/fapi/v2/account?"; + url.push_str(endpoint_url); + + let mut url_build = String::new(); + hmac_signature(&mut url_build).await; + url.push_str(&url_build); + + let res = client + .get(&url) + .header("X-MBX-APIKEY", api_key) + .send() + .await?; + + let body = res.text_with_charset("utf-8").await?; + + // deserialize JSON + let v = serde_json::from_str::(body.as_str()); + match v { + Ok(T) => { + if let (Some(fee_tier), Some(available_balance)) = (T.get("feeTier"), T.get("availableBalance")) { + let update_table_name = String::from("future_available_balance"); + let update_values = vec![ + (String::from("available_usdt"), available_balance.as_str().unwrap().to_string()), + ]; + let update_condition = vec![(String::from("id"), String::from("1"))]; + update_record2(&update_table_name, &update_values, &update_condition) + .await + .unwrap(); + + match fee_tier.as_number().unwrap().as_i64() { + Some(0) => { // Regular User + future_trade_fee.user_level = Some(0); + future_trade_fee.maker_fee_percent = dec!(0.0200); + future_trade_fee.taker_fee_percent = dec!(0.0500); + }, + Some(1) => { // VIP1 + future_trade_fee.user_level = Some(1); + future_trade_fee.maker_fee_percent = dec!(0.0160); + future_trade_fee.taker_fee_percent = dec!(0.0400); + }, + Some(2) => {// VIP 2 + future_trade_fee.user_level = Some(2); + future_trade_fee.maker_fee_percent = dec!(0.0140); + future_trade_fee.taker_fee_percent = dec!(0.0350); + }, + Some(3) => {// VIP 3 + future_trade_fee.user_level = Some(3); + future_trade_fee.maker_fee_percent = dec!(0.0120); + future_trade_fee.taker_fee_percent = dec!(0.0320); + }, + Some(4) => {// VIP 4 + future_trade_fee.user_level = Some(4); + future_trade_fee.maker_fee_percent = dec!(0.0100); + future_trade_fee.taker_fee_percent = dec!(0.0300); + }, + Some(5) => {// VIP 5 + future_trade_fee.user_level = Some(5); + future_trade_fee.maker_fee_percent = dec!(0.0080); + future_trade_fee.taker_fee_percent = dec!(0.0270); + }, + Some(6) => {// VIP 6 + future_trade_fee.user_level = Some(6); + future_trade_fee.maker_fee_percent = dec!(0.0060); + future_trade_fee.taker_fee_percent = dec!(0.0250); + }, + Some(7) => {// VIP 7 + future_trade_fee.user_level = Some(7); + future_trade_fee.maker_fee_percent = dec!(0.0040); + future_trade_fee.taker_fee_percent = dec!(0.0220); + }, + Some(8) => {// VIP 8 + future_trade_fee.user_level = Some(8); + future_trade_fee.maker_fee_percent = dec!(0.0020); + future_trade_fee.taker_fee_percent = dec!(0.0200); + }, + Some(9) => {// VIP 9 + future_trade_fee.user_level = Some(9); + future_trade_fee.maker_fee_percent = dec!(0.0000); + future_trade_fee.taker_fee_percent = dec!(0.0170); + }, + Some(_) => {}, + None => {} + } + log::info!("fee_tier: {:?}. available_balance: {:?}", fee_tier, available_balance); + } else { + log::error!("Endpoint(/fapi/v1/leverage?) output changed!"); + return Err("Err")?; + } + } + Err(e) => { + log::warn!("account information failed!: {}, {}", body, e); + } + } + + Ok(()) +} + +pub async fn update_price_of_filled_positions( + coin_price_map: &HashMap, + exchange_info_map: &HashMap, + futures_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + let filled_buy_orders = select_filled_positions().await?; + + if !filled_buy_orders.is_empty() { + // 심볼들을 5개씩 청스로 나누어 테스크를 생성하고 각 태스크 별로 병렬로 처리한다. + // update real-time current price to each record through chunks + let chunks: std::slice::Chunks<'_, PositionCoinList> = filled_buy_orders.chunks(5); + let mut task_vec = Vec::new(); + + for chunk in chunks { + let chunk_vec = chunk.to_vec(); + let coin_price_vec_c = coin_price_map.clone(); + let exchange_info_vec_c = exchange_info_map.clone(); + let futures_trade_fee_map_c = futures_trade_fee.clone(); + task_vec.push(tokio::spawn(async move { + update_repeat_task( + chunk_vec, + &coin_price_vec_c, + &exchange_info_vec_c, + &futures_trade_fee_map_c, + ) + .await; + })); + } + } + Ok(()) +} + +async fn update_repeat_task( + buy_ordered_coin_vec: Vec, + coin_price_map: &HashMap, + exchange_info_map: &HashMap, + futures_trade_fee: &FuturesTradeFee, +) -> Result<(), Box> { + let update_table_name = String::from("future_ordered_coin_list"); + let mut update_values: Vec<(String, String)> = Vec::new(); + let mut update_condition: Vec<(String, String)> = Vec::new(); + let mut price = Decimal::new(0, 8); + let mut profit_percent = 0.0; + + let update_colums = vec![ + "current_price", + "expected_get_usdt", + "expected_usdt_profit", + "pure_profit_percent", + "minimum_profit_percent", + "maximum_profit_percent", + ]; + let mut update_record_build: Vec = Vec::new(); + let mut update_record: Vec> = Vec::new(); + + // update current_price, expected__get_usdt, expected_usdt_profit, pure_profit_percent, minimum_profit_percent, maximum_profit_percent + for element in buy_ordered_coin_vec { + // build update values + update_record_build.clear(); + if coin_price_map.contains_key(&element.symbol) + && exchange_info_map.contains_key(&element.symbol) + && futures_trade_fee.user_level.is_some() + { + price = rust_decimal::prelude::FromPrimitive::from_f64( + *coin_price_map.get(&element.symbol).unwrap(), + ) + .unwrap(); + if !price.is_zero() { + // to get quote_commission_precision + let trade_fee = futures_trade_fee.maker_fee_percent; + let lot_step_size = exchange_info_map.get(&element.symbol).unwrap().stepsize; + let quote_precision = exchange_info_map + .get(&element.symbol) + .unwrap() + .quote_precision; + let base_qty_to_be_ordered = element.base_qty_fee_adjusted.round_dp_with_strategy( + lot_step_size.normalize().scale(), + RoundingStrategy::ToZero, + ); + let expected_get_usdt = decimal_mul( + decimal_mul(base_qty_to_be_ordered, price).round_dp_with_strategy( + quote_precision, + RoundingStrategy::ToZero, + ), + decimal_sub(dec!(1), trade_fee), + ); + + // TODO: sell_count >=1 이면 expected_get_usdt 는 한번만 tradefee만 적용하여 업데이트 할 것. 현재는 수수료를 2번 (매수,매도)를 계산함. 아래 변수에 든 값으로 업데이트 하면 됨 + // let expected_get_usdt = + // decimal_mul(base_qty_to_be_ordered, price).round_dp_with_strategy( + // quote_commission_precision, + // RoundingStrategy::ToZero, + // ); + let mut pure_profit_percent; + if element.position == Position::Long { + pure_profit_percent = ((expected_get_usdt.to_f64().unwrap() + / element.used_usdt.to_f64().unwrap()) + - 1.0) + * 100.0; + } else { + pure_profit_percent = ((expected_get_usdt.to_f64().unwrap() + / element.used_usdt.to_f64().unwrap()) + - 1.0) + * -100.0; + } + + pure_profit_percent = (pure_profit_percent * 100.0).round() / 100.0; // Rounding + update_record_build.push(element.id.to_string()); // id + update_record_build.push(price.to_string()); // current_price + update_record_build.push(expected_get_usdt.to_string()); //expected_get_usdt + update_record_build + .push(decimal_sub(expected_get_usdt, element.used_usdt).to_string()); // expected_usdt_profit + update_record_build.push(pure_profit_percent.to_string()); // pure_profit_percent + + if element.minimum_profit_percent > pure_profit_percent { + update_record_build.push(pure_profit_percent.to_string()); + // minimum_profit_percent + } else if pure_profit_percent >= 0.0 { + update_record_build.push(0.0.to_string()); // minimum_profit_percent + } else { + update_record_build.push(element.minimum_profit_percent.to_string()); + // minimum_profit_percent + } + + if element.maximum_profit_percent < pure_profit_percent { + update_record_build.push(pure_profit_percent.to_string()); + // maximum_profit_percent + } else if pure_profit_percent <= 0.0 { + update_record_build.push(0.0.to_string()); // maximum_profit_percent + } else { + update_record_build.push(element.maximum_profit_percent.to_string()); + // maximum_profit_percent + } + + update_record.push(update_record_build.clone()); + } + } + } + + update_records(&update_table_name, &update_record, &update_colums).await; + + Ok(()) +} + +pub async fn select_filled_positions() -> Result, Box> { + let select_table_name = String::from("future_ordered_coin_list"); + let select_columns = String::from("*"); + let mut select_condition_build = String::from("WHERE (status = 'FILLED' or status = 'SIMUL')"); + + let select_condition = Some(select_condition_build); + + let data_struct = PositionCoinList::new(); + + let select_result = try_select_record( + &select_table_name, + &select_columns, + &select_condition, + &data_struct, + ) + .await; + + if select_result.is_ok() { + Ok(select_result.unwrap()) + } else { + eprint!("select_filled_positions() error!"); + Err("error")? + } +} + +pub async fn select_listuped_positions() -> Result, Box> { + let select_table_name = String::from("future_ordered_coin_list"); + let select_columns = String::from("*"); + let mut select_condition_build = String::from("WHERE status = 'LISTUP'"); + + let select_condition = Some(select_condition_build); + + let data_struct = PositionCoinList::new(); + + let select_result = try_select_record( + &select_table_name, + &select_columns, + &select_condition, + &data_struct, + ) + .await; + + if select_result.is_ok() { + Ok(select_result.unwrap()) + } else { + eprint!("select_filled_positions() error!"); + Err("error")? + } +} diff --git a/src/initialization.rs b/src/initialization.rs index a031053..a8e4578 100644 --- a/src/initialization.rs +++ b/src/initialization.rs @@ -14,12 +14,16 @@ use rust_decimal_macros::dec; use sqlx::FromRow; use std::{io, io::Write, path::Path, process::Stdio}; use tokio::{fs::*, io::ErrorKind, process::Command, task::JoinHandle, time::*}; +use crate::future::order::*; +use crate::future::FuturesExchangeInfo; +use std::collections::HashMap; const STRATEGIST_NUMBER: u32 = 16; pub async fn initialization() { println!("- - - initialization start - - -"); // initialize_webdriver().await; + future_setup().await; initialize_database().await; println!("- - - initialization done - - -"); } @@ -65,6 +69,27 @@ async fn initialize_webdriver() { println!("Ok"); } +async fn future_setup() { + print!(">>> Check future setting..."); + + let client = ClientBuilder::new() + .timeout(tokio::time::Duration::from_millis(700)) + .build() + .unwrap(); + + let mut future_exchange_info_map: HashMap = HashMap::new(); + request_future_exchange_infomation(&client, &mut future_exchange_info_map).await; + for (symbol, futures_exchange_info) in future_exchange_info_map { + set_margin_type(&symbol, MarginType::Isolated, &client).await; // Default: Isolated + set_initial_leverage(&symbol, 1, &client).await; // Default: x1 + } + + set_position_mode(&client).await; // Default: One-way Mode + set_asset_mode(&client).await; // Default: Single-Asset Mode + print!("Ok"); + io::stdout().flush(); +} + async fn initialize_database() { let client = ClientBuilder::new() .timeout(tokio::time::Duration::from_millis(700)) @@ -1226,6 +1251,90 @@ async fn initialize_database() { println!("Ok"); } + { + // future_available_balance + print!("table 'future_available_balance'..."); + io::stdout().flush(); + + let table_name = String::from("future_available_balance"); + let exists_result = exists_table(&table_name).await; + let initial_table = vec![ + ("id", "integer", Some("PK, AI, UN")), + ("available_usdt", "decimal(16,8)", None), + ]; + let table_condition = None; + + if exists_result == false { + let mut result = new_table(&table_name, &initial_table, &table_condition).await; + if result.is_err() { + loop { + result = new_table(&table_name, &initial_table, &table_condition).await; + if result.is_ok() { + break; + } + sleep(Duration::from_millis(10)).await; + } + } + } else { + delete_all_rows(&table_name) + .await + .expect("Failed to delete rows!"); + } + + let initial_columns = vec!["available_usdt"]; + let initial_values = vec![String::from("0.0")]; + insert_one_record(&table_name, &initial_columns, &initial_values) + .await + .expect("Failed to insert initial record!"); + println!("Ok"); + } + + { + // future_ordered_coin_list + print!("table 'future_ordered_coin_list'..."); + io::stdout().flush(); + + let table_name = String::from("future_ordered_coin_list"); + let exists_result = exists_table(&table_name).await; + let initial_table = vec![ + ("id", "integer", Some("PK, AI, UN")), + ("order_type", "char(20)", None), // POSITIONING, CLOSING + ("status", "char(20)", None), // LISTUP, FILLED, PARTIALLY_FILLED + ("symbol", "char(20)", None), + ("order_id", "bigint", Some("UN")), + ("position", "char(20)", None), + ("registered_server_epoch", "bigint", None), + ("transact_time", "bigint", None), + ("close_time", "bigint", None), + ("used_usdt", "decimal(16,8)", None), + ("expected_get_usdt", "double", None), + ("expected_usdt_profit", "double", None), + ("entry_price", "decimal(16,8)", None), + ("current_price", "decimal(16,8)", None), + ("base_qty_ordered", "decimal(16,8)", None), + ("base_qty_fee_adjusted", "decimal(16,8)", None), + ("pure_profit_percent", "double", None), + ("minimum_profit_percent", "double", None), + ("maximum_profit_percent", "double", None), + ]; + let table_condition = None; + + if exists_result == false { + let mut result = new_table(&table_name, &initial_table, &table_condition).await; + if result.is_err() { + loop { + result = new_table(&table_name, &initial_table, &table_condition).await; + if result.is_ok() { + break; + } + sleep(Duration::from_millis(10)).await; + } + } + } + + println!("Ok"); + } + { // buy_ordered_coin_list print!("table 'buy_ordered_coin_list'..."); diff --git a/src/lib.rs b/src/lib.rs index 2e553b8..c41100e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,8 @@ pub const SECRET_KEY_TESTNET: &str = // URL pub const URL_TEST: &str = "https://testnet.binance.vision"; pub const URL: &str = "https://api1.binance.com"; -pub const FUTURE_URL_TEST: &str = "https://testnet.binancefuture.com"; -pub const FUTURE_URL: &str = "https://fapi.binance.com"; +pub const FUTURES_URL_TEST: &str = "https://testnet.binancefuture.com"; +pub const FUTURES_URL: &str = "https://fapi.binance.com"; // Select program mode #[derive(PartialEq)] diff --git a/src/main.rs b/src/main.rs index ed4c77d..d5733d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use reqwest::{Client, ClientBuilder}; use rust_decimal::Decimal; use simple_logger::set_up_color_terminal; use sqlx::{mysql::*, Connection, Executor, FromRow, Row}; +use tradingbot::future::{FuturesExchangeInfo, FuturesTradeFee}; use std::collections::{HashMap, HashSet}; use std::{ io::{self, Write}, @@ -24,6 +25,23 @@ use tradingbot::{RunningMode::*, *}; #[tokio::main] async fn main() -> Result<(), Box> { + + // future last price data + let mut future_price_map: HashMap = HashMap::new(); // + let (tx_future_price_map, mut rx_future_price_map) = watch::channel(future_price_map); + let mut rx2_future_price_map = rx_future_price_map.clone(); + + let mut futures_exchange_info_map: HashMap = HashMap::new(); + let (tx_futures_exchange_info, mut rx_futures_exchange_info) = watch::channel(futures_exchange_info_map); + let mut rx2_futures_exchange_info = rx_futures_exchange_info.clone(); + let mut rx3_futures_exchange_info = rx_futures_exchange_info.clone(); + + let mut futures_trade_fee = FuturesTradeFee::new(); + let (tx_futures_trade_fee, mut rx_futures_trade_fee) = watch::channel(futures_trade_fee); + let mut rx2_futures_trade_fee = rx_futures_trade_fee.clone(); + let mut rx3_futures_trade_fee = rx_futures_trade_fee.clone(); + + // parse argument and set program preference program_setting(); @@ -116,6 +134,8 @@ async fn main() -> Result<(), Box> { let mut rx5_price_map = rx_price_map.clone(); let mut price_map_capacity = rx_price_map.clone(); + + // candle data from endpoint and channels let mut candle_1m_map: HashMap> = HashMap::new(); // > let (tx_candle_1m_map, mut rx_candle_1m_map) = watch::channel(candle_1m_map); @@ -921,8 +941,10 @@ async fn main() -> Result<(), Box> { all_data.rt_price_1w_vec = rx3_rt_price_1w_map.borrow().clone(); all_data.rt_price_1mon_vec = rx3_rt_price_1mon_map.borrow().clone(); + // future exchange info + let futures_exchange_info = rx3_futures_exchange_info.borrow().clone(); let result = - strategy_team::strategy_manager::execute_list_up_for_buy(&all_data).await; + strategy_team::strategy_manager::execute_list_up_for_buy(&all_data, &futures_exchange_info).await; match result { Ok(T) => { @@ -1354,6 +1376,198 @@ async fn main() -> Result<(), Box> { } }); + + + + + // Futures Section + // Task#XX: get future last price + tokio::task::spawn(async move { + sleep(Duration::from_secs(20)).await; + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let client = ClientBuilder::new() + .timeout(tokio::time::Duration::from_millis(1000)) + .build() + .unwrap(); + let mut future_price_map_temp: HashMap = HashMap::new(); + future::order::get_last_price(&client, &mut future_price_map_temp).await; + + if future_price_map_temp.len() != 0 { + tx_future_price_map.send_modify(|vec| *vec = future_price_map_temp); + } + } + if 333 > elapsed_time { + sleep(Duration::from_millis((333 - elapsed_time) as u64)).await; + } + }); + + + // Task#XX: get future exchange information + tokio::task::spawn(async move { + sleep(Duration::from_secs(20)).await; + let client = ClientBuilder::new() + .timeout(Duration::from_millis(1000)) + .build() + .unwrap(); + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let mut futures_exchange_info_map_temp: HashMap = HashMap::new(); + let result = future::order::request_future_exchange_infomation( + &client, + &mut futures_exchange_info_map_temp, + ) + .await; + + if tx_futures_exchange_info.is_closed() { + log::error!("tx_futures_exchange_info has been closed!"); + } else { + if futures_exchange_info_map_temp.len() != 0 { + tx_futures_exchange_info.send_modify(|map| *map = futures_exchange_info_map_temp); + } + } + + // send Task#0 a message to notify running on + match result { + Ok(T) => { + + } + Err(E) => {} + } + + // sleep as much as the loop recurs per 10 second if all operation finished within 10 second. + elapsed_time = instant.elapsed().as_millis(); + + if 10000 > elapsed_time { + sleep(Duration::from_millis((10000 - elapsed_time) as u64)).await; + } + } + }); + + // Task#XX: get futures trade fee and available balance(USDT) + tokio::task::spawn(async move { + sleep(Duration::from_secs(3)).await; + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let client = ClientBuilder::new() + .timeout(tokio::time::Duration::from_millis(1000)) + .build() + .unwrap(); + let mut futures_trade_fee_temp = FuturesTradeFee::new(); + future::table_mgmt::get_tradefee_balance(&mut futures_trade_fee_temp, &client).await; + tx_futures_trade_fee.send_modify(|vec| *vec = futures_trade_fee_temp); + + } + + if 2000 > elapsed_time { + sleep(Duration::from_millis((2000 - elapsed_time) as u64)).await; + } + }); + + // Task#XX: update price of filled positions + tokio::task::spawn(async move { + sleep(Duration::from_secs(20)).await; + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let coin_price_map = rx_future_price_map.borrow().clone(); + let futures_exchange_info_map = rx_futures_exchange_info.borrow().clone(); + let future_trade_fee = rx_futures_trade_fee.borrow().clone(); + + let result = future::table_mgmt::update_price_of_filled_positions( + &coin_price_map, + &futures_exchange_info_map, + &future_trade_fee, + ) + .await; + + // send Task#0 a message to notify running on + match result { + Ok(T) => { + } + Err(E) => {} + } + + // sleep as much as the loop recurs per 1 second if all operation finished within 1 second. + elapsed_time = instant.elapsed().as_millis(); + + if 100 > elapsed_time { + sleep(Duration::from_millis((100 - elapsed_time) as u64)).await; + } + } + }); + + // Task#XX: monitoring ordered positions + tokio::task::spawn(async move { + sleep(Duration::from_secs(20)).await; + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let client = ClientBuilder::new() + .timeout(tokio::time::Duration::from_millis(1000)) + .build() + .unwrap(); + let future_trade_fee = rx2_futures_trade_fee.borrow().clone(); + + let result = future::order::monitoring_unfilled_order( + &client, + &future_trade_fee, + ) + .await; + + // send Task#0 a message to notify running on + match result { + Ok(T) => { + } + Err(E) => {} + } + + // sleep as much as the loop recurs per 1 second if all operation finished within 1 second. + elapsed_time = instant.elapsed().as_millis(); + + if 100 > elapsed_time { + sleep(Duration::from_millis((100 - elapsed_time) as u64)).await; + } + } + }); + + // Task#XX: monitoring ordered positions + tokio::task::spawn(async move { + sleep(Duration::from_secs(20)).await; + let mut elapsed_time = 0; + loop { + let instant = Instant::now(); + let coin_price_map = rx2_future_price_map.borrow().clone(); + let futures_exchange_info_map = rx2_futures_exchange_info.borrow().clone(); + let future_trade_fee = rx3_futures_trade_fee.borrow().clone(); + + let result = future::order::entry_position( + &coin_price_map, + &futures_exchange_info_map, + &future_trade_fee, + ) + .await; + + // send Task#0 a message to notify running on + match result { + Ok(T) => { + } + Err(E) => {} + } + + // sleep as much as the loop recurs per 1 second if all operation finished within 1 second. + elapsed_time = instant.elapsed().as_millis(); + + if 50 > elapsed_time { + sleep(Duration::from_millis((50 - elapsed_time) as u64)).await; + } + } + }); + + loop {} Ok(()) diff --git a/src/strategy_team/future_strategy.rs b/src/strategy_team/future_strategy.rs index 53aa064..53e648c 100644 --- a/src/strategy_team/future_strategy.rs +++ b/src/strategy_team/future_strategy.rs @@ -1,4 +1,4 @@ -use crate::value_estimation_team::indicators::wiliams_percent_r; +use crate::value_estimation_team::{datapoints::price_data::CandleType, indicators::wiliams_percent_r}; use super::{ adx, dec, decimal_add, decimal_div, decimal_mul, decimal_sub, dema, duplicate_filter, ema, @@ -8,12 +8,16 @@ use super::{ Arc, BollingerBandData, Client, ClientBuilder, Decimal, DemaData, EmaData, ExchangeInfo, FilteredDataValue, HashMap, HashSet, HeatMapLevel, HeatmapVolumeData, MacdData, Mutex, RealtimePriceData, RoundingStrategy, RsiData, StochRsiData, SuperTrendArea, SuperTrendSignal, - SupertrendData, TemaData, ToPrimitive, TradeFee, WiliamsPercentR + SupertrendData, TemaData, ToPrimitive, TradeFee, WiliamsPercentR, future_duplicate_filter, insert_future_coins }; +use crate::future::{Position, FuturesExchangeInfo}; +use crate::future::table_mgmt::select_filled_positions; + // BUY conditions pub async fn list_up_for_buy( alldata: &AllData, + future_exchange_info_map: &HashMap ) -> Result<(), Box> { // print rt_price for debugging // let a = alldata.rt_price_30m_vec.iter().position(|a| a.0 == "BTCUSDT"); @@ -25,481 +29,113 @@ pub async fn list_up_for_buy( filtered_data.insert(symbol.clone(), FilteredDataValue::new()); } - // current Tema(3) > current Tema(10) - // let mut keys_to_remove: HashSet = HashSet::new(); - // let tema_3 = tema(3, &alldata.rt_price_1d_vec, &filtered_data).await?; - // let tema_10 = tema(10, &alldata.rt_price_1d_vec, &filtered_data).await?; - // let server_epoch = get_server_epoch().await; - // for (symbol, values) in &mut filtered_data { - // if let (Some(tema5_vec), Some(tema10_vec)) = (tema_3.get(symbol), tema_10.get(symbol)) { - // if tema5_vec.len() > 2 - // && tema10_vec.len() > 2 - // && tema5_vec.last().unwrap().close_time == tema10_vec.last().unwrap().close_time - // && tema5_vec.last().unwrap().close_time > server_epoch - // && tema10_vec.last().unwrap().close_time > server_epoch - // { - // if tema5_vec.last().unwrap().tema_value > tema10_vec.last().unwrap().tema_value { - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } - // remove_keys(&mut filtered_data, keys_to_remove).await; - - // Wiliams %R(200) > -20.0 - // Wiliams %R(30) > -20.0 - let mut keys_to_remove: HashSet = HashSet::new(); - let mut wprs200 = wiliams_percent_r(200, &alldata.rt_price_30m_vec, &filtered_data).await?; - let mut wprs30 = wiliams_percent_r(30, &alldata.rt_price_30m_vec, &filtered_data).await?; - let server_epoch = get_server_epoch().await; - for (symbol, values) in &mut filtered_data { - if let (Some(wpr200_vec), Some(wpr30_vec)) = (wprs200.get(symbol), wprs30.get(symbol)) { - if wpr200_vec.len() > 15 - && wpr30_vec.len() > 15 - && wpr200_vec.last().unwrap().close_time > server_epoch - && wpr200_vec.last().unwrap().r_value > -20.0 - && wpr30_vec.last().unwrap().close_time > server_epoch - && wpr30_vec.last().unwrap().r_value > -20.0 - { - } else { - keys_to_remove.insert(symbol.clone()); - } - } else { - keys_to_remove.insert(symbol.clone()); - } - } - remove_keys(&mut filtered_data, keys_to_remove).await; - - // current Tema(300) > current Tema(200) > current Tema(100) - let mut keys_to_remove: HashSet = HashSet::new(); - let tema_100 = tema(100, &alldata.rt_price_30m_vec, &filtered_data).await?; - let tema_200 = tema(200, &alldata.rt_price_30m_vec, &filtered_data).await?; - let tema_300 = tema(300, &alldata.rt_price_30m_vec, &filtered_data).await?; - let server_epoch = get_server_epoch().await; - for (symbol, values) in &mut filtered_data { - if let (Some(tema100_vec), Some(tema200_vec), Some(tema300_vec)) = ( - tema_100.get(symbol), - tema_200.get(symbol), - tema_300.get(symbol), - ) { - if (tema100_vec.len() > 15 && tema200_vec.len() > 15 && tema300_vec.len() > 15) - && tema100_vec.last().unwrap().close_time == tema200_vec.last().unwrap().close_time - && tema200_vec.last().unwrap().close_time == tema300_vec.last().unwrap().close_time - && tema100_vec.last().unwrap().close_time > server_epoch - { - if tema100_vec.last().unwrap().tema_value > tema300_vec.last().unwrap().tema_value - && tema200_vec.last().unwrap().tema_value - < tema300_vec.last().unwrap().tema_value - && tema200_vec.last().unwrap().tema_value - > tema100_vec.last().unwrap().tema_value - { - } else { - keys_to_remove.insert(symbol.clone()); - } - } else { - keys_to_remove.insert(symbol.clone()); - } - } else { - keys_to_remove.insert(symbol.clone()); - } - } - remove_keys(&mut filtered_data, keys_to_remove).await; - - // supertrend(ATR period 10, multiplier: 3.0, 30m close price) - // let mut keys_to_remove: HashSet = HashSet::new(); - // let server_epoch = get_server_epoch().await; - // let supertrend_30m_map = - // supertrend(10, 3.0, true, &alldata.rt_price_30m_vec, &filtered_data).await?; - // for (symbol, values) in &mut filtered_data { - // if let (Some(supertrend_vec), Some(rt_price_vec)) = ( - // supertrend_30m_map.get(symbol), - // alldata.rt_price_30m_vec.get(symbol), - // ) { - // if supertrend_vec.last().unwrap().close_time == rt_price_vec.last().unwrap().close_time - // && rt_price_vec.last().unwrap().close_time > server_epoch - // { - // // input stoploss, target_price - // let band_value: Decimal = rust_decimal::prelude::FromPrimitive::from_f64( - // supertrend_vec.last().unwrap().band_value, - // ) - // .unwrap(); - // let open_price: Decimal = rust_decimal::prelude::FromPrimitive::from_f64( - // rt_price_vec.last().unwrap().open_price, - // ) - // .unwrap(); - // let current_price: Decimal = rust_decimal::prelude::FromPrimitive::from_f64( - // rt_price_vec.last().unwrap().close_price, - // ) - // .unwrap(); - // if supertrend_vec.last().unwrap().area == SuperTrendArea::UP - // && band_value < current_price - // && band_value < open_price - // { - // values.current_price = current_price; - // values.closetime = rt_price_vec.last().unwrap().close_time; - // values.stoploss = band_value; - // values.target_price = decimal_add( - // decimal_mul(decimal_sub(current_price, values.stoploss), dec!(10.0)), - // current_price, - // ); - // } else if supertrend_vec.last().unwrap().area == SuperTrendArea::DOWN - // && band_value > current_price - // && band_value > open_price - // { - // values.current_price = current_price; - // values.closetime = rt_price_vec.last().unwrap().close_time; - // values.stoploss = decimal_sub(open_price, decimal_sub(band_value, open_price)); - // values.target_price = decimal_add( - // decimal_mul(decimal_sub(open_price, values.stoploss), dec!(10.0)), - // current_price, - // ); - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } - // remove_keys(&mut filtered_data, keys_to_remove).await; - - // current ADX(15, 15) < 25 - // let mut keys_to_remove: HashSet = HashSet::new(); - // let adx_vec = adx(15, 15, &alldata.rt_price_30m_vec, &filtered_data).await?; - // for (symbol, values) in &mut filtered_data { - // if let Some(adx_vec) = adx_vec.get(symbol) { - // if let Some(last_idx) = adx_vec.iter().position(|elem| elem.close_time == values.closetime) { - // if adx_vec.len() > 10 && - // adx_vec[last_idx].adx < 25.0 { - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } - // remove_keys(&mut filtered_data, keys_to_remove).await; - - // StochRSI (RSI_len: 200, StochRSI_len: 200, K: 3, D: 3) K_current < 70, K_prev < 70, K_prev_1 < 70 - // let mut keys_to_remove: HashSet = HashSet::new(); - // let stoch_rsis = stoch_rsi(200, 200, 3, 3, &alldata.rt_price_30m_vec, &filtered_data).await?; - // for (symbol, values) in &mut filtered_data { - // if stoch_rsis.contains_key(symbol) { - // let stoch_rsi_vec = stoch_rsis.get(symbol).unwrap(); - // let search_result = stoch_rsi_vec - // .iter() - // .position(|x| x.close_time == values.closetime); - // if stoch_rsi_vec.len() > 10 - // && search_result.is_some_and(|a| { - // stoch_rsi_vec[a].k < 70.0 - // && stoch_rsi_vec[a - 1].k < 70.0 - // && stoch_rsi_vec[a - 2].k < 70.0 - // }) - // { - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } else { - // keys_to_remove.insert(symbol.clone()); - // } - // } - // remove_keys(&mut filtered_data, keys_to_remove).await; - // Heatmap volume: filtering close price with Extra High is over the previous candle from 30 previous candles let mut keys_to_remove: HashSet = HashSet::new(); let heatmap_volumes = heatmap_volume( - 30, - 30, + 60, + 60, 4.0, 2.5, 1.0, -0.5, &filtered_data, - &alldata.rt_price_30m_vec, + &alldata.rt_price_1m_vec, ) .await?; - for (symbol, values) in &mut filtered_data { - if let Some(heatmap_volume_vec) = heatmap_volumes.get(symbol) { - if heatmap_volume_vec.len() > 50 { - let heatmap_volume_trunc = heatmap_volume_vec - .get(heatmap_volume_vec.len() - 31..heatmap_volume_vec.len() - 1) - .unwrap(); - let windows = heatmap_volume_trunc.windows(2); - for slice in windows { - if slice[1].heatmap_level == HeatMapLevel::ExtraHigh { - if let (prev_candle_idx, current_candle_idx) = ( - (&alldata - .rt_price_30m_vec - .get(symbol) - .unwrap() - .iter() - .position(|x| x.close_time == slice[0].close_time)) - .unwrap(), - (&alldata - .rt_price_30m_vec - .get(symbol) - .unwrap() - .iter() - .position(|x| x.close_time == slice[1].close_time)) - .unwrap(), - ) { - let prev_candle = - &alldata.rt_price_30m_vec.get(symbol).unwrap()[prev_candle_idx]; - let current_candle = - &alldata.rt_price_30m_vec.get(symbol).unwrap()[current_candle_idx]; - if current_candle.close_price < prev_candle.close_price - || current_candle.close_price < prev_candle.open_price - || current_candle.close_price < prev_candle.high_price - || current_candle.close_price < prev_candle.low_price - { - keys_to_remove.insert(symbol.clone()); - } - } - } - } - } - } - } - remove_keys(&mut filtered_data, keys_to_remove).await; - - // limit buy price: 0.25 * abs(이전 5 개 중 최대값 제거 한 opclo 값 평균 - 현재 open 값) + 현재 open 값 > current_price - let mut keys_to_remove: HashSet = HashSet::new(); let server_epoch = get_server_epoch().await; for (symbol, values) in &mut filtered_data { - if let Some(rt_price_vec) = alldata.rt_price_30m_vec.get(symbol) { - if rt_price_vec.last().unwrap().close_time > server_epoch && rt_price_vec.len() >= 6 { - let mut opclo_vec: Vec = Vec::new(); - opclo_vec.push(rt_price_vec[rt_price_vec.len() - 2].opclo_price); - opclo_vec.push(rt_price_vec[rt_price_vec.len() - 3].opclo_price); - opclo_vec.push(rt_price_vec[rt_price_vec.len() - 4].opclo_price); - opclo_vec.push(rt_price_vec[rt_price_vec.len() - 5].opclo_price); - opclo_vec.push(rt_price_vec[rt_price_vec.len() - 6].opclo_price); - let max_idx = opclo_vec.iter().position(|&x| { - x == *opclo_vec - .iter() - .max_by(|&a, &b| a.partial_cmp(b).unwrap()) - .unwrap() - }); - opclo_vec.remove(max_idx.unwrap()); - - let mut mean = 0.0; - for element in &opclo_vec { - mean += element; - } - mean /= opclo_vec.len() as f64; - let current_price = rt_price_vec.last().unwrap().close_price; - let difference = (mean - rt_price_vec.last().unwrap().open_price).abs(); - - if current_price > rt_price_vec.last().unwrap().open_price + (0.5 * difference) { - } else { - keys_to_remove.insert(symbol.clone()); - } - } else { - keys_to_remove.insert(symbol.clone()); + let mut do_buy = false; + if let (Some(heatmap_volume_vec), Some(rt_price_vec), Some(rt_price_vec_30m)) = (heatmap_volumes.get(symbol), alldata.rt_price_1m_vec.get(symbol), alldata.rt_price_30m_vec.get(symbol)) { + if heatmap_volume_vec.len() > 100 + && heatmap_volume_vec.last().unwrap().close_time > server_epoch + && rt_price_vec.last().unwrap().close_time == heatmap_volume_vec.last().unwrap().close_time + && heatmap_volume_vec[heatmap_volume_vec.len()-2].heatmap_level == HeatMapLevel::ExtraHigh + && rt_price_vec[rt_price_vec.len()-2].candle_type == CandleType::DOWN { + let current_price: Decimal = rust_decimal::prelude::FromPrimitive::from_f64( + rt_price_vec_30m.last().unwrap().close_price, + ) + .unwrap(); + values.closetime = heatmap_volume_vec.last().unwrap().close_time; + values.current_price = current_price; + do_buy = true; } - } else { + } + + if do_buy == false { keys_to_remove.insert(symbol.clone()); } } remove_keys(&mut filtered_data, keys_to_remove).await; - if filtered_data.keys().len() != 0 { - let date_now = chrono::Local::now().to_rfc2822(); - println!("{} future coins: {:?}", date_now, filtered_data.keys()); - } - - // let final_filtered_data = duplicate_filter(8, &filtered_data).await?; - // insert_pre_suggested_coins(8, false, &final_filtered_data, &alldata).await; + + let final_filtered_data = future_duplicate_filter(&filtered_data, &future_exchange_info_map).await?; + insert_future_coins(Position::Short, server_epoch, &final_filtered_data).await?; Ok(()) } -pub async fn list_up_for_sell( - all_data: &AllData, - exchange_info_map: &HashMap, - trade_fee_map: &HashMap, -) -> Result<(), Box> { - let filled_buy_orders = select_filled_buy_orders(8).await?; +// pub async fn list_up_for_sell( +// all_data: &AllData, +// exchange_info_map: &HashMap, +// trade_fee_map: &HashMap, +// ) -> Result<(), Box> { +// let filled_positions = select_filled_positions().await?; - if !filled_buy_orders.is_empty() { - let client = ClientBuilder::new() - .timeout(tokio::time::Duration::from_millis(5000)) - .build() - .unwrap(); - let mut supertrend_vec: Vec = Vec::new(); - let server_epoch = get_server_epoch().await; - let mut filtered_symbols: HashMap = HashMap::new(); - for element in &filled_buy_orders { - filtered_symbols.insert(element.symbol.clone(), FilteredDataValue::new()); - } - let supertrend_30m = - supertrend(10, 2.0, true, &all_data.rt_price_30m_vec, &filtered_symbols).await?; - let tema_300 = tema(300, &all_data.rt_price_30m_vec, &filtered_symbols).await?; - let tema_200 = tema(200, &all_data.rt_price_30m_vec, &filtered_symbols).await?; - let tema_100 = tema(100, &all_data.rt_price_30m_vec, &filtered_symbols).await?; - for element in filled_buy_orders { - let mut is_sell = false; - let mut is_overturned = false; - if element.used_usdt >= dec!(10.0) { - if let (Some(tema300_vec), Some(tema200_vec), Some(tema100_vec)) = ( - tema_300.get(&element.symbol), - tema_200.get(&element.symbol), - tema_100.get(&element.symbol), - ) { - if tema200_vec.len() > 2 - && tema100_vec.len() > 2 - && tema300_vec.len() > 2 - && tema200_vec.last().unwrap().close_time - == tema100_vec.last().unwrap().close_time - && tema300_vec.last().unwrap().close_time - == tema100_vec.last().unwrap().close_time - && tema300_vec.last().unwrap().close_time > server_epoch - && tema200_vec.last().unwrap().close_time > server_epoch - && tema100_vec.last().unwrap().close_time > server_epoch - && ((tema200_vec.last().unwrap().tema_value - > tema100_vec.last().unwrap().tema_value - && tema200_vec[tema200_vec.len() - 2].tema_value - < tema100_vec[tema100_vec.len() - 2].tema_value) - || (tema200_vec.last().unwrap().tema_value - > tema300_vec.last().unwrap().tema_value - && tema200_vec[tema200_vec.len() - 2].tema_value - < tema300_vec[tema300_vec.len() - 2].tema_value)) - { - is_overturned = true; - } - } +// let client = ClientBuilder::new() +// .timeout(tokio::time::Duration::from_millis(5000)) +// .build() +// .unwrap(); +// let server_epoch = get_server_epoch().await; - if let (Some(exchange_info), Some(tradefee), Some(supertrend_vec)) = ( - exchange_info_map.get(&element.symbol), - trade_fee_map.get(&element.symbol), - supertrend_30m.get(&element.symbol), - ) { - // update stoploss - let band_value: Decimal = rust_decimal::prelude::FromPrimitive::from_f64( - supertrend_vec.last().unwrap().band_value, - ) - .unwrap(); - if supertrend_vec.last().unwrap().area == SuperTrendArea::UP - && band_value > element.stoploss - { - let update_table_name = String::from("buy_ordered_coin_list"); - let update_value = vec![(String::from("stoploss"), band_value.to_string())]; - let update_condition = vec![(String::from("id"), element.id.to_string())]; - update_record3(&update_table_name, &update_value, &update_condition) - .await - .unwrap(); - } +// for element in filled_positions { +// let mut is_sell = false; - let lot_step_size = exchange_info.stepsize; - let quote_commission_precision = exchange_info.quote_commission_precision; +// if element.used_usdt >= dec!(10.0) { +// if let (Some(exchange_info), Some(tradefee)) = ( +// exchange_info_map.get(&element.symbol), +// trade_fee_map.get(&element.symbol), +// ) { +// let lot_step_size = exchange_info.stepsize; +// let quote_commission_precision = exchange_info.quote_commission_precision; - // TODO: BNB 코인이 있으면 - // let base_qty_to_be_ordered = - // element.base_qty_ordered.round_dp_with_strategy( - // lot_step_size.normalize().scale(), - // RoundingStrategy::ToZero, - // ); - // TODO: BNB 코인이 없으면 - let base_qty_to_be_ordered = - element.base_qty_fee_adjusted.round_dp_with_strategy( - lot_step_size.normalize().scale(), - RoundingStrategy::ToZero, - ); - let target_profit_percent = decimal_mul( - decimal_div( - decimal_sub(element.target_price, element.buy_price), - element.buy_price, - ), - dec!(100), - ) - .to_f64() - .unwrap(); - if !element.current_price.is_zero() { - if element.current_price <= element.stoploss { - is_sell = true; - } else if element.current_price >= element.target_price { - is_sell = true; - } else if server_epoch - element.transact_time >= (1_800_000) * 5 - && is_overturned == true - { - is_sell = true; - } +// // TODO: BNB 코인이 있으면 +// // let base_qty_to_be_ordered = +// // element.base_qty_ordered.round_dp_with_strategy( +// // lot_step_size.normalize().scale(), +// // RoundingStrategy::ToZero, +// // ); +// // TODO: BNB 코인이 없으면 +// let base_qty_to_be_ordered = +// element.base_qty_fee_adjusted.round_dp_with_strategy( +// lot_step_size.normalize().scale(), +// RoundingStrategy::ToZero, +// ); - let minimum_candles = 5; - let maximum_candles = 240; - for count_candles in minimum_candles..=maximum_candles { - if count_candles < maximum_candles - && server_epoch - element.transact_time - > (1_800_000) * count_candles - && (target_profit_percent != 0.0 - && target_profit_percent.is_sign_positive() - && target_profit_percent - * ((maximum_candles - count_candles) as f64 - / (maximum_candles - minimum_candles + 1) as f64) - <= element.pure_profit_percent) - { - is_sell = true; - break; - } else { - break; - } - } +// if !element.current_price.is_zero() { +// if element.pure_profit_percent >= 1.0 { +// is_sell = true; +// } else if element.pure_profit_percent <= -0.8 { +// is_sell = true; +// } else if server_epoch - element.transact_time >= (1_800_000) * 1 { +// // time up selling +// is_sell = true; +// } + +// if is_sell == true { +// limit_order_sell( +// &element, +// element.current_price, +// base_qty_to_be_ordered, +// &client, +// &exchange_info_map, +// &trade_fee_map, +// ) +// .await; +// } +// } +// } +// } +// } - if server_epoch - element.transact_time >= (1_800_000) * maximum_candles { - // time up selling - is_sell = true; - } - - // TODO: sell_count가 1일 때 적용하기 - // else if (supertrend_vec - // .last() - // .unwrap() - // .signal - // .as_ref() - // .is_some_and(|x| x.contains("SELL")) - // || supertrend_vec.last().unwrap().area.contains("DOWN")) - // && (supertrend_vec.last().unwrap().close_time > element.close_time) - // { - // println!( - // "SELL signal selling {} {:.2}", - // element.symbol, element.pure_profit_percent - // ); - // limit_order_sell( - // &element, - // element.current_price, - // base_qty_to_be_ordered, - // &client, - // &exchange_info_vec, - // &trade_fee_vec, - // ) - // .await; - // } - if is_sell == true { - limit_order_sell( - &element, - element.current_price, - base_qty_to_be_ordered, - &client, - &exchange_info_map, - &trade_fee_map, - ) - .await; - } - } - } - } - } - } - - Ok(()) -} +// Ok(()) +// } diff --git a/src/strategy_team/mod.rs b/src/strategy_team/mod.rs index 7ffa9ca..0e996e5 100644 --- a/src/strategy_team/mod.rs +++ b/src/strategy_team/mod.rs @@ -42,8 +42,9 @@ use rust_decimal_macros::dec; use sqlx::FromRow; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use strategy_manager::insert_pre_suggested_coins; +use strategy_manager::{insert_pre_suggested_coins, insert_future_coins}; use tokio::sync::Mutex; +use crate::future::FuturesExchangeInfo; #[derive(Debug, Clone)] pub struct AllData { @@ -154,6 +155,48 @@ pub async fn duplicate_filter( Ok(filtered_data_c) } +pub async fn future_duplicate_filter( + original_filtered_data: &HashMap, + future_exchange_info_map: &HashMap +) -> Result, Box> { + let inspect_table_name_1 = String::from("future_ordered_coin_list"); + + let mut filtered_data: HashMap = HashMap::new(); + let mut filtered_data_arc: Arc>> = + Arc::new(Mutex::new(filtered_data)); + let mut task_vec = Vec::new(); + + for (symbol, filtered_data) in original_filtered_data { + + let mut exists_condition_build = String::from("symbol=\'"); + exists_condition_build.push_str(symbol.as_str()); + exists_condition_build.push_str("\' AND close_time="); + exists_condition_build.push_str(filtered_data.closetime.to_string().as_str()); + let exists_condition = Some(exists_condition_build); + let exists_condition_c = exists_condition.clone(); + let inspect_table_name_1_c = inspect_table_name_1.clone(); + let future_exchange_info_map_c = future_exchange_info_map.clone(); + let symbol_c = symbol.clone(); + let filtered_data_c = filtered_data.clone(); + let filtered_data_arc_c = Arc::clone(&filtered_data_arc); + task_vec.push(tokio::spawn(async move { + if future_exchange_info_map_c.contains_key(&symbol_c) { + let inspect_result_1 = + exists_record(&inspect_table_name_1_c, &exists_condition_c).await; + + if inspect_result_1 == false + { + let mut filtered_data_lock = filtered_data_arc_c.lock().await; + filtered_data_lock.insert(symbol_c, filtered_data_c); + } + } + })); + } + try_join_all(task_vec).await?; + let filtered_data_c = filtered_data_arc.lock().await.clone(); + Ok(filtered_data_c) +} + pub async fn remove_keys( filtered_data: &mut HashMap, keys_to_remove: HashSet, diff --git a/src/strategy_team/strategy_manager.rs b/src/strategy_team/strategy_manager.rs index 9bdb5c6..68905ed 100644 --- a/src/strategy_team/strategy_manager.rs +++ b/src/strategy_team/strategy_manager.rs @@ -1,9 +1,11 @@ use crate::coex::exchange_team::*; use crate::coex::order_team::*; +use crate::future::Position; use csv::{DeserializeRecordsIter, StringRecord}; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use serde::Deserialize; +use crate::future::FuturesExchangeInfo; // use super::strategy_test; use super::{ @@ -30,6 +32,7 @@ struct Record { pub async fn execute_list_up_for_buy( all_data: &AllData, + future_exchange_info_map: &HashMap ) -> Result<(), Box> { // crate::strategy_team::strategy_001::list_up_for_buy(all_data).await; // crate::strategy_team::strategy_002::list_up_for_buy(all_data).await; @@ -40,7 +43,7 @@ pub async fn execute_list_up_for_buy( // crate::strategy_team::strategy_007::list_up_for_buy(all_data).await; crate::strategy_team::strategy_008::list_up_for_buy(all_data).await; crate::strategy_team::strategy_009::list_up_for_buy(all_data).await; - crate::strategy_team::future_strategy::list_up_for_buy(all_data).await; + crate::strategy_team::future_strategy::list_up_for_buy(all_data, &future_exchange_info_map).await; Ok(()) } @@ -131,3 +134,58 @@ pub async fn insert_pre_suggested_coins( Ok(()) } + +pub async fn insert_future_coins( + position: Position, + server_epoch: i64, + filtered_coins: &HashMap +) -> Result<(), Box> { + let mut insert_table_name = String::from("future_ordered_coin_list"); + let insert_columns = vec![ + "order_type", + "status", + "symbol", + "order_id", + "position", + "registered_server_epoch", + "transact_time", + "close_time", + "used_usdt", + "expected_get_usdt", + "expected_usdt_profit", + "entry_price", + "current_price", + "base_qty_ordered", + "base_qty_fee_adjusted", + "pure_profit_percent", + "minimum_profit_percent", + "maximum_profit_percent", + ]; + + for (symbol, filtered_data) in filtered_coins { + let mut insert_values = vec![ + String::from("POSITIONING"), // order_type + String::from("LISTUP"), // status + symbol.clone(), // symbol + 0.to_string(), // order_id + position.to_string(), // position + server_epoch.to_string(), // registered_server_epoch + 0.to_string(), // transact_time + filtered_data.closetime.to_string(), // close_time + 0.0.to_string(), // used_usdt + 0.0.to_string(), // expected_get_usdt + 0.0.to_string(), // expected_usdt_profit + 0.0.to_string(), // entry_price + 0.0.to_string(), // current_price + 0.0.to_string(), // base_qty_ordered + 0.0.to_string(), // base_qty_fee_adjusted + 0.0.to_string(), // pure_profit_percent + 0.0.to_string(), // minimum_profit_percent + 0.0.to_string(), // maximum_profit_percent + ]; + + insert_one_record(&insert_table_name, &insert_columns, &insert_values).await; + } + + Ok(()) +}