diff --git a/rustybot/Cargo.lock b/rustybot/Cargo.lock index 1acf9c5..c51ed09 100644 --- a/rustybot/Cargo.lock +++ b/rustybot/Cargo.lock @@ -223,6 +223,12 @@ dependencies = [ "generic-array 0.14.4", ] +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + [[package]] name = "encoding_rs" version = "0.8.26" @@ -1056,6 +1062,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bitfinex", + "dyn-clone", "futures-util", "regex", "tokio 0.2.24", diff --git a/rustybot/Cargo.toml b/rustybot/Cargo.toml index 4f37fe1..3985630 100644 --- a/rustybot/Cargo.toml +++ b/rustybot/Cargo.toml @@ -12,4 +12,5 @@ tokio = { version = "0.2", features=["full"]} tokio-tungstenite = "*" futures-util = { version = "0.3", default-features = false, features = ["async-await", "sink", "std"] } async-trait = "0.1" -regex = "1" \ No newline at end of file +regex = "1" +dyn-clone = "1" \ No newline at end of file diff --git a/rustybot/src/bot.rs b/rustybot/src/bot.rs index 1be1429..d7bba5a 100644 --- a/rustybot/src/bot.rs +++ b/rustybot/src/bot.rs @@ -9,17 +9,16 @@ use tokio::time::delay_for; use crate::connectors::Connector; use crate::currency::{Symbol, SymbolPair}; use crate::pairs::PairStatus; +use crate::strategy::Strategy; use crate::ticker::Ticker; use crate::BoxError; pub struct BfxBot<'a> { connector: Box, ticker: Ticker, - pair_status: Vec, + pair_statuses: Vec>, quote: Symbol, trading_symbols: Vec, - // account_info: String, - // ledger: String, } impl<'a> BfxBot<'a> { @@ -32,18 +31,24 @@ impl<'a> BfxBot<'a> { BfxBot { connector: Box::new(connector), ticker: Ticker::new(tick_duration), - pair_status: trading_symbols + pair_statuses: trading_symbols .iter() .map(|x| SymbolPair::new(quote.clone(), x.clone())) .map(|x| PairStatus::new(x, 1, None)) .collect(), quote, - // account_info: String::new(), - // ledger: String::new(), trading_symbols, } } + pub fn with_strategy(mut self, strategy: Box) -> Self { + self.pair_statuses + .iter_mut() + .for_each(|x| x.set_strategy(dyn_clone::clone_box(&*strategy))); + + self + } + pub async fn current_prices(&self, symbol: Symbol) -> Result { let trading_pair = SymbolPair::new(self.quote.clone(), symbol.clone()); @@ -54,17 +59,35 @@ impl<'a> BfxBot<'a> { self.connector.current_prices(&trading_pair).await } - pub async fn update(&mut self) { - println!("Updating..."); - delay_for(self.ticker.duration()).await; - self.ticker.inc(); - // self.update_pairs().await; - println!("Done!"); + pub async fn start_loop(&mut self) -> Result<(), BoxError> { + if let Err(e) = self.update_pair_statuses().await { + println!("Error while updating pairs at first start: {}", e); + } + + loop { + self.update().await; + } } - // async fn update_pairs(&mut self) { - // let active_positions = self.bfx.positions.active_positions().await?; - // - // for p in active_positions {} - // } + async fn update(&mut self) { + delay_for(self.ticker.duration()).await; + self.ticker.inc(); + + if let Err(e) = self.update_pair_statuses().await { + println!("Error while updating pairs: {}", e); + } + } + + async fn update_pair_statuses(&mut self) -> Result<(), BoxError> { + for status in &mut self.pair_statuses { + // add positions for each pair + self.connector + .active_positions(status.pair()) + .await? + .into_iter() + .for_each(|x| status.add_position(x)); + } + + Ok(()) + } } diff --git a/rustybot/src/currency.rs b/rustybot/src/currency.rs index 97a8594..b5b0fd3 100644 --- a/rustybot/src/currency.rs +++ b/rustybot/src/currency.rs @@ -7,7 +7,7 @@ use regex::Regex; use crate::BoxError; -#[derive(Clone, PartialEq, Hash)] +#[derive(Clone, PartialEq, Hash, Debug, Eq)] pub struct Symbol { name: Cow<'static, str>, } @@ -26,6 +26,8 @@ impl Symbol { pub const BTC: Symbol = Symbol::new_static("BTC"); pub const ETH: Symbol = Symbol::new_static("ETH"); pub const LTC: Symbol = Symbol::new_static("LTC"); + pub const TESTBTC: Symbol = Symbol::new_static("TESTBTC"); + pub const TESTUSD: Symbol = Symbol::new_static("TESTUSD"); pub const USD: Symbol = Symbol::new_static("USD"); pub const GBP: Symbol = Symbol::new_static("GBP"); pub const EUR: Symbol = Symbol::new_static("EUR"); @@ -53,7 +55,7 @@ impl Display for Symbol { } } -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct SymbolPair { quote: Symbol, base: Symbol, @@ -79,7 +81,7 @@ impl SymbolPair { impl Into for SymbolPair { fn into(self) -> String { - format!("{}{}", self.base, self.quote) + format!("{}/{}", self.base, self.quote) } } @@ -87,7 +89,7 @@ impl TryFrom<&str> for SymbolPair { type Error = BoxError; fn try_from(value: &str) -> Result { - const REGEX: &str = r"^[t|f](?P\w{3,4}):?(?P\w{3,4})"; + const REGEX: &str = r"^[t|f](?P\w{3,7}):?(?P\w{3,7})"; let captures = Regex::new(REGEX)?.captures(&value).ok_or("Invalid input")?; let quote = captures.name("quote").ok_or("Quote not found")?.as_str(); @@ -102,7 +104,7 @@ impl TryFrom<&str> for SymbolPair { impl Display for SymbolPair { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}{}", self.base, self.quote) + write!(f, "{}/{}", self.base, self.quote) } } diff --git a/rustybot/src/events.rs b/rustybot/src/events.rs index 9cb3bd0..83cbf17 100644 --- a/rustybot/src/events.rs +++ b/rustybot/src/events.rs @@ -8,13 +8,13 @@ use crate::pairs::PairStatus; use crate::positions::{Position, PositionProfitState, PositionState}; use crate::BoxError; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub enum SignalKind { ClosePosition, OpenPosition, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct EventMetadata { position_id: Option, order_id: Option, @@ -29,7 +29,7 @@ impl EventMetadata { } } -#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub enum EventKind { NewMinimum, NewMaximum, @@ -45,7 +45,7 @@ pub enum EventKind { Any, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Event { kind: EventKind, tick: u64, diff --git a/rustybot/src/main.rs b/rustybot/src/main.rs index 5885bdd..9025d11 100644 --- a/rustybot/src/main.rs +++ b/rustybot/src/main.rs @@ -1,7 +1,9 @@ use tokio::time::{delay_for, Duration}; use crate::bot::BfxBot; +use crate::connectors::BfxWrapper; use crate::currency::{Symbol, SymbolPair}; +use crate::strategy::TrailingStop; mod bot; mod connectors; @@ -17,24 +19,17 @@ pub type BoxError = Box; #[tokio::main] async fn main() -> Result<(), BoxError> { - // let test_api_key = "P1EVE68DJByDAkGQvpIkTwfrbYXd2Vo2ZaIhTYb9vx2"; - // let test_api_secret = "1nicg8z0zKVEt5Rb7ZDpIYjVYVTgvCaCPMZqB0niFli"; + let test_api_key = "P1EVE68DJByDAkGQvpIkTwfrbYXd2Vo2ZaIhTYb9vx2"; + let test_api_secret = "1nicg8z0zKVEt5Rb7ZDpIYjVYVTgvCaCPMZqB0niFli"; // - // let mut bot = BfxBot::new( - // test_api_key, - // test_api_secret, - // vec![Symbol::BTC, Symbol::ETH, Symbol::XMR], - // Symbol::USD, - // Duration::new(20, 0), - // ); - // - // loop { - // let ticker = bot.current_prices("ETH".into()).await?; - // bot.update().await; - // - // // let ticker = bot.current_prices("ETH".into()).await?; - // println!("{:?}", ticker); - // } + let bfx = BfxWrapper::new(test_api_key, test_api_secret); + let mut bot = BfxBot::new( + bfx, + vec![Symbol::TESTBTC], + Symbol::TESTUSD, + Duration::new(20, 0), + ) + .with_strategy(Box::new(TrailingStop::new())); - Ok(()) + Ok(bot.start_loop().await?) } diff --git a/rustybot/src/pairs.rs b/rustybot/src/pairs.rs index b2e986b..e5f779e 100644 --- a/rustybot/src/pairs.rs +++ b/rustybot/src/pairs.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; use crate::currency::SymbolPair; -use crate::events::{Event, EventDispatcher}; +use crate::events::{Event, EventDispatcher, SignalKind}; use crate::orders::Order; use crate::positions::Position; use crate::strategy::Strategy; -pub struct PairStatus { +pub struct PairStatus<'a> { pair: SymbolPair, dispatcher: EventDispatcher, prices: HashMap, @@ -14,10 +14,11 @@ pub struct PairStatus { orders: HashMap>, positions: HashMap>, current_tick: u64, - strategy: Option>, + strategy: Option>, + signals: HashMap, } -impl PairStatus { +impl<'a> PairStatus<'a> { pub fn new(pair: SymbolPair, current_tick: u64, strategy: Option>) -> Self { PairStatus { pair, @@ -25,52 +26,82 @@ impl PairStatus { prices: HashMap::new(), events: Vec::new(), positions: HashMap::new(), + orders: HashMap::new(), + signals: HashMap::new(), current_tick, strategy, - orders: HashMap::new(), } } pub fn add_position(&mut self, position: Position) { - let (pw, events, signals) = { + let (new_position, events, signals) = { match &self.strategy { Some(strategy) => strategy.position_on_new_tick(&position, &self), - None => ( - position, - // PositionWrapper::new(position.clone(), position.pl(), position.pl_perc(), None), - vec![], - vec![], - ), + None => (position, vec![], vec![]), } }; self.positions .entry(self.current_tick) .or_default() - .push(pw.clone()); + .push(new_position.clone()); // calling position state callbacks - self.dispatcher.call_position_state_handlers(&pw, &self); + self.dispatcher + .call_position_state_handlers(&new_position, &self); // adding events and calling callbacks for e in events { self.add_event(e); } + + // adding signals to current tick vector + for s in signals { + self.add_signal(s); + } + + println!( + "EVENTS: {:?} | SIGNALS: {:?} | POSITION: {:?}", + self.events, self.signals, new_position + ); } - pub fn add_event(&mut self, event: Event) { + fn add_event(&mut self, event: Event) { self.events.push(event); self.dispatcher.call_event_handlers(&event, &self); } + fn add_signal(&mut self, signal: SignalKind) { + self.signals.insert(self.current_tick(), signal); + } + pub fn current_tick(&self) -> u64 { self.current_tick } - pub fn previous_pw(&self, id: u64) -> Option<&Position> { + pub fn set_strategy(&mut self, strategy: Box) { + self.strategy = Some(strategy); + } + + pub fn pair(&self) -> &SymbolPair { + &self.pair + } + + pub fn position_previous_tick(&self, id: u64, tick: Option) -> Option<&Position> { + let tick = match tick { + Some(tick) => { + if tick < 1 { + 1 + } else { + tick + } + } + None => self.current_tick() - 1, + }; + self.positions - .get(&(self.current_tick - 1)) + .get(&tick) .and_then(|x| x.iter().find(|x| x.position_id() == id)) } } diff --git a/rustybot/src/positions.rs b/rustybot/src/positions.rs index 444ad94..cbbd0af 100644 --- a/rustybot/src/positions.rs +++ b/rustybot/src/positions.rs @@ -1,6 +1,6 @@ use crate::currency::{Symbol, SymbolPair}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Position { pair: SymbolPair, state: PositionState, @@ -91,7 +91,7 @@ impl Position { } } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum PositionProfitState { Critical, Loss, @@ -111,7 +111,7 @@ impl PositionProfitState { } } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub enum PositionState { Closed, Open, diff --git a/rustybot/src/strategy.rs b/rustybot/src/strategy.rs index 24f3fdb..eeaab7d 100644 --- a/rustybot/src/strategy.rs +++ b/rustybot/src/strategy.rs @@ -3,8 +3,9 @@ use std::collections::HashMap; use crate::events::{Event, EventKind, EventMetadata, SignalKind}; use crate::pairs::PairStatus; use crate::positions::{Position, PositionProfitState, PositionState}; +use dyn_clone::DynClone; -pub trait Strategy { +pub trait Strategy: DynClone { fn position_on_new_tick( &self, position: &Position, @@ -12,7 +13,8 @@ pub trait Strategy { ) -> (Position, Vec, Vec); } -struct TrailingStop { +#[derive(Clone, Debug)] +pub struct TrailingStop { stop_percentages: HashMap, } @@ -62,18 +64,17 @@ impl Strategy for TrailingStop { } }; - let opt_pre_pw = status.previous_pw(position.position_id()); + let opt_pre_pw = status.position_previous_tick(position.position_id(), None); let event_metadata = EventMetadata::new(Some(position.position_id()), None); - // let pw = PositionWrapper::new(position.clone(), position.pl(), pl_perc, Some(state)); - let pw = position.clone(); + let new_position = position.clone().with_profit_state(Some(state)); match opt_pre_pw { Some(prev) => { if prev.profit_state() == Some(state) { - return (pw, events, signals); + return (new_position, events, signals); } } - None => return (pw, events, signals), + None => return (new_position, events, signals), }; let events = { @@ -114,6 +115,6 @@ impl Strategy for TrailingStop { events }; - return (pw, events, signals); + return (new_position, events, signals); } }