├── .gitignore ├── Cargo.toml ├── readme.md └── src ├── config.json ├── lib.rs ├── main.rs ├── strategies ├── dummy.rs ├── dynamic_balance.rs ├── mod.rs ├── move_stoploss.rs └── turtle.rs ├── traits.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store 4 | .*.swp 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsquant" 3 | version = "0.1.0" 4 | authors = ["ccyanxyz "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [[bin]] 10 | name = "rsquant" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | env_logger = "0.7.1" 15 | ws = { version = "0.9.1", features = ["ssl"]} 16 | flate2 = "1.0" 17 | lazy_static = "1.4.0" 18 | hex = "0.4.2" 19 | base64 = "0.12.1" 20 | chrono = "0.4" 21 | percent-encoding = "1.0.1" 22 | serde = "1.0" 23 | serde_json = "1.0" 24 | serde_derive = "1.0" 25 | ring = "0.13.0-alpha" 26 | data-encoding = "2.1.2" 27 | reqwest = { version = "0.10", features = ["blocking", "json"] } 28 | url = "2.1" 29 | log = "0.4.8" 30 | rsex = { path = "../rsex" } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Cryptocurrency quant framework written in Rust. 2 | 3 | Using [rsex](https://github.com/ccyanxyz/rsex). 4 | 5 | 6 | 7 | ### TODO: 8 | 1. strategy framework/template 9 | 2. backtest framework 10 | 3. strategy/robot management system backend & frontend 11 | 12 | strategies: 13 | 14 | 1. move_stoploss √ 15 | 2. turtle 16 | 3. dynamic_balan 17 | 18 | ## Warn 19 | 20 | Still a very immature project. Use it at your own risk! 21 | 22 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "https://www.binance.com", 3 | "apikey": "YnbYI9ucqD0ZTeqeSItetzJaTf5IQ6rx9OXXkkDjJJiWGVpUjgCK4dok5ppXinKK", 4 | "secret_key": "BeMvtAFRBFztSmb70NKeYne5xrjYbEfLg7wH6c7wMoppSWckjCoBcDQkg6hlTedM", 5 | 6 | "strategy": "move_stoploss", 7 | 8 | "quote": "usdt", 9 | 10 | "ignore":["btc", "eth", "bnb"], 11 | 12 | "min_value": 10, 13 | "stoploss": -0.05, 14 | "start_threshold": 0.3, 15 | "withdraw_ratio": 0.5 16 | } 17 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate env_logger; 2 | extern crate log; 3 | extern crate rsex; 4 | extern crate serde_json; 5 | 6 | pub mod strategies; 7 | pub mod traits; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use log::{info, warn}; 2 | use rsquant::{ 3 | strategies::{Dummy, MoveStopLoss}, 4 | traits::Strategy, 5 | }; 6 | use serde_json::Value; 7 | use std::{env, fs}; 8 | 9 | fn construct_robot(config_path: &str) -> Box { 10 | let file = fs::File::open(config_path).expect("file should open read only"); 11 | let config: Value = serde_json::from_reader(file).expect("file should be proper json"); 12 | let strategy = config["strategy"].as_str().unwrap(); 13 | 14 | match strategy { 15 | "move_stoploss" => MoveStopLoss::new(config_path), 16 | _ => Dummy::new(config_path), 17 | } 18 | } 19 | 20 | fn main() { 21 | env_logger::init(); 22 | let args: Vec = env::args().collect(); 23 | let config_path = if args.len() > 1 { 24 | &args[1] 25 | } else { 26 | "./config.json" 27 | }; 28 | info!("config file: {}", config_path); 29 | 30 | let mut robot = construct_robot(&config_path); 31 | if robot.name() != "dummy" { 32 | info!("robot: {:?}", robot.stringify()); 33 | robot.run_forever(); 34 | } else { 35 | warn!("strategy not found!"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/strategies/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::traits::Strategy; 2 | use log::warn; 3 | 4 | #[derive(Debug)] 5 | pub struct Dummy {} 6 | 7 | impl Strategy for Dummy { 8 | fn new(_: &str) -> Box { 9 | Box::new(Dummy {}) 10 | } 11 | 12 | fn run_forever(&mut self) { 13 | warn!("dummy!"); 14 | } 15 | 16 | fn name(&self) -> String { 17 | "dummy".into() 18 | } 19 | 20 | fn stringify(&self) -> String { 21 | format!("{:?}", self) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/strategies/dynamic_balance.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, info, warn}; 2 | use std::{fs, {thread, time}}; 3 | use serde_json::Value; 4 | 5 | use rsex::{ 6 | binance::spot_rest::Binance, 7 | errors::APIResult, 8 | models::{SymbolInfo, Balance}, 9 | traits::SpotRest, 10 | constant::{ORDER_TYPE_LIMIT, ORDER_ACTION_SELL}, 11 | }; 12 | 13 | use crate::{ 14 | traits::Strategy, 15 | utils::{round_same, round_to}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | struct Position { 20 | symbol: String, 21 | amount: f64, 22 | price: f64, 23 | high: f64, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct MoveStopLoss { 28 | config: Value, 29 | client: Binance, 30 | watch: Vec, 31 | positions: Vec, 32 | balances: Vec, 33 | 34 | quote: String, 35 | min_value: f64, 36 | stoploss: f64, 37 | start_threshold: f64, 38 | withdraw_ratio: f64, 39 | } 40 | 41 | impl MoveStopLoss { 42 | fn get_symbols(&self) -> APIResult> { 43 | let symbol_info = self.client.get_symbols()?; 44 | debug!("client.get_symbols: {:?}", symbol_info); 45 | let symbol_info = symbol_info 46 | .into_iter() 47 | .filter(|symbol| symbol.quote.to_lowercase() == self.quote) 48 | .collect(); 49 | Ok(symbol_info) 50 | } 51 | 52 | fn init(&mut self) { 53 | 54 | } 55 | 56 | fn on_tick(&mut self) { 57 | let ret = self.client.get_all_balances(); 58 | if let Ok(balances) = ret { 59 | self.balances = balances; 60 | } else { 61 | warn!("get_all_balances error: {:?}", ret); 62 | return; 63 | } 64 | self.positions = self 65 | .positions 66 | .iter() 67 | .map(|pos| { 68 | let new_pos = self.refresh_position(&pos); 69 | //info!("new_pos: {:?}", new_pos); 70 | let new_pos = match new_pos { 71 | Ok(new_pos) => new_pos, 72 | Err(err) => { 73 | warn!("refresh_position error: {:?}", err); 74 | pos.clone() 75 | } 76 | }; 77 | if new_pos.amount > 0f64 { 78 | debug!("old_pos: {:?}, new_pos: {:?}", pos, new_pos); 79 | } 80 | let ret = self.check_move_stoploss(&new_pos); 81 | if let Err(err) = ret { 82 | warn!("check_move_stoploss error: {:?}", err); 83 | } 84 | new_pos 85 | }) 86 | .collect(); 87 | } 88 | } 89 | 90 | impl Strategy for MoveStopLoss { 91 | fn new(config_path: &str) -> Box { 92 | let file = fs::File::open(config_path).expect("file should open read only"); 93 | let config: Value = serde_json::from_reader(file).expect("file should be proper json"); 94 | let quote = config["quote"].as_str().unwrap(); 95 | let apikey = config["apikey"].as_str().unwrap(); 96 | let secret_key = config["secret_key"].as_str().unwrap(); 97 | let host = config["host"].as_str().unwrap(); 98 | let min_value = config["min_value"].as_f64().unwrap(); 99 | let stoploss = config["stoploss"].as_f64().unwrap(); 100 | let start_threshold = config["start_threshold"].as_f64().unwrap(); 101 | let withdraw_ratio = config["withdraw_ratio"].as_f64().unwrap(); 102 | 103 | Box::new(MoveStopLoss { 104 | config: config.clone(), 105 | client: Binance::new(Some(apikey.into()), Some(secret_key.into()), host.into()), 106 | watch: vec![], 107 | positions: vec![], 108 | balances: vec![], 109 | 110 | quote: quote.into(), 111 | min_value: min_value, 112 | stoploss: stoploss, 113 | start_threshold: start_threshold, 114 | withdraw_ratio: withdraw_ratio, 115 | }) 116 | } 117 | 118 | fn run_forever(&mut self) { 119 | self.init(); 120 | loop { 121 | self.on_tick(); 122 | thread::sleep(time::Duration::from_secs(60)); 123 | } 124 | } 125 | 126 | fn name(&self) -> String { 127 | "move_stoploss".into() 128 | } 129 | 130 | fn stringify(&self) -> String { 131 | format!("{:?}", self) 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use super::*; 138 | 139 | #[test] 140 | fn test_move_stoploss() { 141 | env_logger::init(); 142 | let config_path = "./config.json"; 143 | info!("config file: {}", config_path); 144 | 145 | let mut robot = MoveStopLoss::new(&config_path); 146 | info!("robot: {:?}", robot); 147 | robot.run_forever(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/strategies/mod.rs: -------------------------------------------------------------------------------- 1 | mod dummy; 2 | pub use dummy::Dummy; 3 | 4 | mod move_stoploss; 5 | pub use move_stoploss::MoveStopLoss; 6 | 7 | //mod turtle; 8 | //pub use turtle::Turtle; 9 | -------------------------------------------------------------------------------- /src/strategies/move_stoploss.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, info, warn}; 2 | use serde_json::Value; 3 | use std::{ 4 | fs, {thread, time}, 5 | }; 6 | use rsex::{ 7 | binance::spot_rest::Binance, 8 | constant::{ORDER_ACTION_SELL, ORDER_TYPE_LIMIT}, 9 | errors::APIResult, 10 | models::{Balance, SymbolInfo}, 11 | traits::SpotRest, 12 | }; 13 | use crate::{ 14 | traits::Strategy, 15 | utils::{round_same, round_to}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | struct Position { 20 | symbol: String, 21 | amount: f64, 22 | price: f64, 23 | high: f64, 24 | } 25 | 26 | #[derive(Debug)] 27 | struct Record { 28 | symbol: String, 29 | buy_price: f64, 30 | sell_price: f64, 31 | amount: f64, 32 | profit: f64, 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct MoveStopLoss { 37 | config: Value, 38 | client: Binance, 39 | watch: Vec, 40 | positions: Vec, 41 | balances: Vec, 42 | history: Vec, 43 | total_profit: f64, 44 | 45 | quote: String, 46 | min_value: f64, 47 | stoploss: f64, 48 | start_threshold: f64, 49 | withdraw_ratio: f64, 50 | } 51 | 52 | impl MoveStopLoss { 53 | fn get_symbols(&self) -> APIResult> { 54 | let symbol_info = self.client.get_symbols()?; 55 | debug!("client.get_symbols: {:?}", symbol_info); 56 | let symbol_info = symbol_info 57 | .into_iter() 58 | .filter(|symbol| symbol.quote.to_lowercase() == self.quote) 59 | .collect(); 60 | Ok(symbol_info) 61 | } 62 | 63 | fn init(&mut self) { 64 | // set watch_list 65 | let ret = self.get_symbols(); 66 | debug!("get_symbols: {:?}", ret); 67 | let symbol_info = match ret { 68 | Ok(symbols) => symbols, 69 | Err(err) => { 70 | warn!("get_symbols error: {:?}", err); 71 | vec![] 72 | } 73 | }; 74 | debug!("symbol_info: {:?}", symbol_info); 75 | 76 | // symbols - ignore 77 | let ignore: Vec = self.config["ignore"] 78 | .as_array() 79 | .unwrap() 80 | .into_iter() 81 | .map(|coin| coin.as_str().unwrap().to_owned() + &self.quote) 82 | .collect(); 83 | info!("ignore: {:?}", ignore); 84 | 85 | self.watch = symbol_info 86 | .into_iter() 87 | .filter(|info| !ignore.contains(&info.symbol.to_lowercase())) 88 | .collect(); 89 | debug!("watch_list: {:?}", self.watch); 90 | 91 | self.positions = self 92 | .watch 93 | .iter() 94 | .map(|info| Position { 95 | symbol: info.symbol.clone(), 96 | price: 0f64, 97 | amount: 0f64, 98 | high: 0f64, 99 | }) 100 | .collect(); 101 | } 102 | 103 | fn refresh_position(&self, pos: &Position) -> APIResult { 104 | let mut coin = pos.symbol.clone(); 105 | let len = self.quote.len(); 106 | for _ in 0..len { 107 | coin.pop(); 108 | } 109 | //let balance = self.client.get_balance(&coin)?; 110 | let ticker = self.client.get_ticker(&pos.symbol)?; 111 | let ret = self.balances.iter().find(|balance| balance.asset == coin); 112 | let balance = match ret { 113 | Some(balance) => balance, 114 | None => { 115 | warn!("{:?} not found in self.balances", coin); 116 | return Ok(pos.clone()); 117 | } 118 | }; 119 | if balance.free == pos.amount { 120 | if pos.amount * pos.price < self.min_value { 121 | return Ok(pos.clone()); 122 | } else { 123 | let high = if ticker.bid.price > pos.high { 124 | ticker.bid.price 125 | } else { 126 | pos.high 127 | }; 128 | return Ok(Position { 129 | symbol: pos.symbol.clone(), 130 | price: pos.price, 131 | amount: pos.amount, 132 | high: high, 133 | }); 134 | } 135 | } 136 | if balance.free * ticker.bid.price < self.min_value { 137 | return Ok(Position { 138 | symbol: pos.symbol.clone(), 139 | price: 0f64, 140 | amount: balance.free, 141 | high: 0f64, 142 | }); 143 | } 144 | // get avg_price 145 | let orders = self.client.get_history_orders(&pos.symbol)?; 146 | let mut amount = 0f64; 147 | let mut avg_price = 0f64; 148 | for order in &orders { 149 | if order.side == "BUY" { 150 | avg_price = 151 | (amount * avg_price + order.filled * order.price) / (amount + order.filled); 152 | amount += order.amount; 153 | } else if order.side == "SELL" { 154 | avg_price = 155 | (amount * avg_price - order.filled * order.price) / (amount - order.filled); 156 | amount -= order.amount; 157 | } 158 | 159 | if amount == balance.free { 160 | break; 161 | } 162 | } 163 | // ignore low value position 164 | if amount * avg_price < self.min_value { 165 | amount = 0f64; 166 | avg_price = 0f64; 167 | } 168 | // get highest price since hold 169 | let high = if ticker.bid.price > avg_price { 170 | ticker.bid.price 171 | } else { 172 | avg_price 173 | }; 174 | Ok(Position { 175 | symbol: pos.symbol.clone(), 176 | amount: amount, 177 | price: avg_price, 178 | high: high, 179 | }) 180 | } 181 | 182 | fn calc_withdraw_ratio(&self, profit_ratio: f64) -> f64 { 183 | if profit_ratio < self.start_threshold { 184 | return self.withdraw_ratio; 185 | } 186 | // y = (10*x-10*a+1)/10*x 187 | return (round_to(profit_ratio * 10f64, 0) - round_to(self.start_threshold * 10f64, 0) 188 | + 1f64) 189 | / round_to(profit_ratio * 10f64, 0); 190 | } 191 | 192 | fn check_move_stoploss(&mut self, pos: &Position) -> APIResult<()> { 193 | if pos.amount * pos.price < self.min_value { 194 | return Ok(()); 195 | } 196 | // get current price 197 | let ticker = self.client.get_ticker(&pos.symbol)?; 198 | let diff_ratio = (ticker.bid.price - pos.price) / pos.price; 199 | let high_ratio = (pos.high - pos.price) / pos.price; 200 | 201 | let stoploss_price = round_same(ticker.bid.price, pos.price * (1f64 + self.stoploss)); 202 | // calc withdraw ratio 203 | let withdraw_ratio = self.calc_withdraw_ratio(diff_ratio); 204 | let withdraw_price = round_same( 205 | ticker.bid.price, 206 | pos.price * (1f64 + withdraw_ratio * high_ratio), 207 | ); 208 | info!( 209 | "pos: {:?}, now_price: {:?}, profit_ratio: {:?}, stoploss_price: {:?}, withdraw_ratio: {:?}, withdraw_price: {:?}", 210 | pos, 211 | ticker.bid.price, 212 | round_to(diff_ratio, 4), 213 | stoploss_price, 214 | withdraw_ratio, 215 | withdraw_price 216 | ); 217 | info!( 218 | "total_profit: {:?}, history: {:?}", 219 | self.total_profit, self.history 220 | ); 221 | 222 | let profit = round_to((ticker.bid.price - pos.price) * pos.amount, 2); 223 | 224 | // stoploss 225 | if diff_ratio <= self.stoploss { 226 | // sell all 227 | let price = round_same(ticker.bid.price, ticker.bid.price * 0.95); 228 | let oid = self.client.create_order( 229 | &pos.symbol, 230 | price, 231 | pos.amount, 232 | ORDER_ACTION_SELL, 233 | ORDER_TYPE_LIMIT, 234 | ); 235 | info!( 236 | "{:?} stoploss triggered, sell {:?} at {:?}, order_id: {:?}", 237 | pos.symbol, price, pos.amount, oid 238 | ); 239 | self.history.push(Record { 240 | symbol: pos.symbol.clone(), 241 | buy_price: pos.price, 242 | sell_price: ticker.bid.price, 243 | amount: pos.amount, 244 | profit: profit, 245 | }); 246 | self.total_profit += profit; 247 | } 248 | if high_ratio >= self.start_threshold { 249 | if diff_ratio <= high_ratio * withdraw_ratio { 250 | // sell all 251 | let price = round_same(ticker.bid.price, ticker.bid.price * 0.95); 252 | let oid = self.client.create_order( 253 | &pos.symbol, 254 | price, 255 | pos.amount, 256 | ORDER_ACTION_SELL, 257 | ORDER_TYPE_LIMIT, 258 | ); 259 | info!( 260 | "{:?} profit withdraw triggered, sell {:?} at {:?}, order_id: {:?}", 261 | pos.symbol, price, pos.amount, oid 262 | ); 263 | self.history.push(Record { 264 | symbol: pos.symbol.clone(), 265 | buy_price: pos.price, 266 | sell_price: ticker.bid.price, 267 | amount: pos.amount, 268 | profit: profit, 269 | }); 270 | self.total_profit += profit; 271 | } 272 | } 273 | Ok(()) 274 | } 275 | 276 | fn on_tick(&mut self) { 277 | let ret = self.client.get_all_balances(); 278 | if let Ok(balances) = ret { 279 | self.balances = balances; 280 | } else { 281 | warn!("get_all_balances error: {:?}", ret); 282 | return; 283 | } 284 | self.positions = self 285 | .positions 286 | .clone() 287 | .iter() 288 | .map(|pos| { 289 | let new_pos = self.refresh_position(&pos); 290 | //info!("new_pos: {:?}", new_pos); 291 | let new_pos = match new_pos { 292 | Ok(new_pos) => new_pos, 293 | Err(err) => { 294 | warn!("refresh_position error: {:?}", err); 295 | pos.clone() 296 | } 297 | }; 298 | if new_pos.amount > 0f64 { 299 | debug!("old_pos: {:?}, new_pos: {:?}", pos, new_pos); 300 | } 301 | let ret = self.check_move_stoploss(&new_pos); 302 | if let Err(err) = ret { 303 | warn!("check_move_stoploss error: {:?}", err); 304 | } 305 | new_pos 306 | }) 307 | .collect(); 308 | } 309 | } 310 | 311 | impl Strategy for MoveStopLoss { 312 | fn new(config_path: &str) -> Box { 313 | let file = fs::File::open(config_path).expect("file should open read only"); 314 | let config: Value = serde_json::from_reader(file).expect("file should be proper json"); 315 | let quote = config["quote"].as_str().unwrap(); 316 | let apikey = config["apikey"].as_str().unwrap(); 317 | let secret_key = config["secret_key"].as_str().unwrap(); 318 | let host = config["host"].as_str().unwrap(); 319 | let min_value = config["min_value"].as_f64().unwrap(); 320 | let stoploss = config["stoploss"].as_f64().unwrap(); 321 | let start_threshold = config["start_threshold"].as_f64().unwrap(); 322 | let withdraw_ratio = config["withdraw_ratio"].as_f64().unwrap(); 323 | 324 | Box::new(MoveStopLoss { 325 | config: config.clone(), 326 | client: Binance::new(Some(apikey.into()), Some(secret_key.into()), host.into()), 327 | watch: vec![], 328 | positions: vec![], 329 | balances: vec![], 330 | history: vec![], 331 | 332 | total_profit: 0f64, 333 | quote: quote.into(), 334 | min_value: min_value, 335 | stoploss: stoploss, 336 | start_threshold: start_threshold, 337 | withdraw_ratio: withdraw_ratio, 338 | }) 339 | } 340 | 341 | fn run_forever(&mut self) { 342 | self.init(); 343 | loop { 344 | self.on_tick(); 345 | thread::sleep(time::Duration::from_secs(60)); 346 | } 347 | } 348 | 349 | fn name(&self) -> String { 350 | "move_stoploss".into() 351 | } 352 | 353 | fn stringify(&self) -> String { 354 | format!("{:?}", self) 355 | } 356 | } 357 | 358 | #[cfg(test)] 359 | mod test { 360 | use super::*; 361 | 362 | #[test] 363 | fn test_move_stoploss() { 364 | env_logger::init(); 365 | let config_path = "./config.json"; 366 | info!("config file: {}", config_path); 367 | 368 | let mut robot = MoveStopLoss::new(&config_path); 369 | info!("robot: {:?}", robot); 370 | robot.run_forever(); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/strategies/turtle.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, info, warn}; 2 | use std::{fs, {thread, time}}; 3 | use serde_json::Value; 4 | 5 | use rsex::{ 6 | binance::spot_rest::Binance, 7 | errors::APIResult, 8 | models::{SymbolInfo, Balance}, 9 | traits::SpotRest, 10 | constant::{ORDER_TYPE_LIMIT, ORDER_ACTION_SELL}, 11 | }; 12 | 13 | use crate::{ 14 | traits::Strategy, 15 | utils::{round_same, round_to}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | struct Position { 20 | symbol: String, 21 | amount: f64, 22 | price: f64, 23 | high: f64, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Turtle { 28 | config: Value, 29 | client: Binance, 30 | watch: Vec, 31 | positions: Vec, 32 | balances: Vec, 33 | } 34 | 35 | impl MoveStopLoss { 36 | fn get_symbols(&self) -> APIResult> { 37 | let symbol_info = self.client.get_symbols()?; 38 | debug!("client.get_symbols: {:?}", symbol_info); 39 | let symbol_info = symbol_info 40 | .into_iter() 41 | .filter(|symbol| symbol.quote.to_lowercase() == self.quote) 42 | .collect(); 43 | Ok(symbol_info) 44 | } 45 | 46 | fn init(&mut self) { 47 | 48 | } 49 | 50 | fn on_tick(&mut self) { 51 | let ret = self.client.get_all_balances(); 52 | if let Ok(balances) = ret { 53 | self.balances = balances; 54 | } else { 55 | warn!("get_all_balances error: {:?}", ret); 56 | return; 57 | } 58 | self.positions = self 59 | .positions 60 | .iter() 61 | .map(|pos| { 62 | let new_pos = self.refresh_position(&pos); 63 | //info!("new_pos: {:?}", new_pos); 64 | let new_pos = match new_pos { 65 | Ok(new_pos) => new_pos, 66 | Err(err) => { 67 | warn!("refresh_position error: {:?}", err); 68 | pos.clone() 69 | } 70 | }; 71 | if new_pos.amount > 0f64 { 72 | debug!("old_pos: {:?}, new_pos: {:?}", pos, new_pos); 73 | } 74 | let ret = self.check_move_stoploss(&new_pos); 75 | if let Err(err) = ret { 76 | warn!("check_move_stoploss error: {:?}", err); 77 | } 78 | new_pos 79 | }) 80 | .collect(); 81 | } 82 | } 83 | 84 | impl Strategy for MoveStopLoss { 85 | fn new(config_path: &str) -> Box { 86 | let file = fs::File::open(config_path).expect("file should open read only"); 87 | let config: Value = serde_json::from_reader(file).expect("file should be proper json"); 88 | let quote = config["quote"].as_str().unwrap(); 89 | let apikey = config["apikey"].as_str().unwrap(); 90 | let secret_key = config["secret_key"].as_str().unwrap(); 91 | let host = config["host"].as_str().unwrap(); 92 | let min_value = config["min_value"].as_f64().unwrap(); 93 | let stoploss = config["stoploss"].as_f64().unwrap(); 94 | let start_threshold = config["start_threshold"].as_f64().unwrap(); 95 | let withdraw_ratio = config["withdraw_ratio"].as_f64().unwrap(); 96 | 97 | Box::new(MoveStopLoss { 98 | config: config.clone(), 99 | client: Binance::new(Some(apikey.into()), Some(secret_key.into()), host.into()), 100 | watch: vec![], 101 | positions: vec![], 102 | balances: vec![], 103 | 104 | quote: quote.into(), 105 | min_value: min_value, 106 | stoploss: stoploss, 107 | start_threshold: start_threshold, 108 | withdraw_ratio: withdraw_ratio, 109 | }) 110 | } 111 | 112 | fn run_forever(&mut self) { 113 | self.init(); 114 | loop { 115 | self.on_tick(); 116 | thread::sleep(time::Duration::from_secs(60)); 117 | } 118 | } 119 | 120 | fn name(&self) -> String { 121 | "move_stoploss".into() 122 | } 123 | 124 | fn stringify(&self) -> String { 125 | format!("{:?}", self) 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod test { 131 | use super::*; 132 | 133 | #[test] 134 | fn test_move_stoploss() { 135 | env_logger::init(); 136 | let config_path = "./config.json"; 137 | info!("config file: {}", config_path); 138 | 139 | let mut robot = MoveStopLoss::new(&config_path); 140 | info!("robot: {:?}", robot); 141 | robot.run_forever(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | pub trait Strategy { 2 | fn new(config_path: &str) -> Box 3 | where 4 | Self: Sized; 5 | fn run_forever(&mut self); 6 | 7 | fn name(&self) -> String; 8 | fn stringify(&self) -> String; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn round_to(v: f64, len: u32) -> f64 { 2 | (v * 10i32.pow(len) as f64).floor() / 10i32.pow(len) as f64 3 | } 4 | 5 | pub fn round_same(a: f64, b: f64) -> f64 { 6 | let s = a.to_string(); 7 | let v: Vec<&str> = s.split(".").collect(); 8 | if v.len() == 1 { 9 | return b.floor(); 10 | } 11 | let len = v[1].len(); 12 | (b * 10i32.pow(len as u32) as f64).floor() / 10i32.pow(len as u32) as f64 13 | } 14 | 15 | #[cfg(test)] 16 | mod test { 17 | use super::*; 18 | 19 | //#[test] 20 | fn test_round_same() { 21 | let a = 1.23; 22 | let b = 1.4563; 23 | let ret = round_same(a, b); 24 | println!("ret: {:?}", ret); 25 | } 26 | } 27 | --------------------------------------------------------------------------------