diff --git a/README.md b/README.md index 570cf62..c5cfa1e 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,46 @@ # Orderbook Delta Bot -A trading bot written in Rust. +A trading bot written in Rust 🦀. -The strategy based on the concept of *mean reversion*. We look for large deviations in the volume delta of BTC-PERP on -FTX at a depth of 1. -These deviations could be caused by over-enthusiastic and over-leveraged market participants. +The strategy based on the concept of *mean reversion*. We look for large deviations in the volume delta of BTC-PERP on +FTX until a defined depth. +These deviations could be caused by over-enthusiastic and over-leveraged market participants (speculation). -We counter-trade those deviations, and enter short/long positions based on triggers given by a large deviation -(> 2 SDs) on the orderbook delta from a 20 period rolling bollinger band. +We counter-trade those deviations, and enter short/long positions based on triggers given by a large deviation +(> 2 SDs) on the orderbook delta from a 20 period rolling bollinger band. -We are testing this with BTC-PERP on FTX, which has good liquidity and small spreads (and FTX has the best API -in the business). In principle, the scheme could be modified for lower liquidity pairs too, perhaps by adjusting -the bollinger band length and standard deviation for generating triggers. +We are testing this with BTC-PERP on FTX, which has good liquidity and small spreads (and FTX has the best API +in the business). In principle, the scheme could be modified for lower liquidity pairs too, perhaps by adjusting +the sampling period and market depth for generating triggers. -We use the definitions: +We use the definitions: -| Name | Definition | -|--------------|----------------------------------------------------------------| -| `delta_perp` | Difference between bid and ask volume at depth = 1 on BTC-PERP | -| `bb_upper` | Upper bollinger band (L=20, SD=2) of `delta_perp` | -| `bb_lower` | Lower bollinger band (L=20, SD=2) of `delta_perp` | +| Name | Definition | +|-----------------|------------------------------------------------------------------------| +| `bid_ask_delta` | Difference between the sum of bid and ask volumes till a defined depth | +| `bb.upper` | Upper bollinger band (L=20, SD=2) of `bid_ask_delta` | +| `bb.lower` | Lower bollinger band (L=20, SD=2) of `bid_ask_delta` | -| Trigger | Position | -|-------------------------|----------| -| `delta_perp > bb_upper` | short | -| `delta_perp < bb_lower` | long | +| Trigger | Position | +|----------------------------|----------| +| `bid_ask_delta > bb.upper` | short | +| `bid_ask_delta < bb.lower` | long | - -A full analysis of this strategy along with it's limitations in +A full analysis of this strategy along with its limitations in [dineshpinto/market-analytics](https://github.com/dineshpinto/market-analytics). ## Installation + ### Clone the repository + #### With Git + ```shell git clone https://github.com/dineshpinto/orderbook-delta-bot.git ``` #### With GitHub CLI + ```shell gh repo clone dineshpinto/orderbook-delta-bot ``` @@ -45,40 +48,45 @@ gh repo clone dineshpinto/orderbook-delta-bot ### Set up bot #### Bot settings -Rename `settings-example.json` to `settings.json`. The default settings are given below. +Rename `settings-example.json` to `settings.json`. The default settings are given below. #### Place live orders (optional) + - Rename `.env.example` to `.env`, and enter in your FTX API keys - Set `"live" : true` in `settings.json` - ### Install all dependencies and build + ```shell cargo build ``` -### Run script +### 🫡 Run script + ```shell cargo run ``` -## Orderbook visualizer -You can use a live orderbook visualizer written in Python. The visualizer uses Dash and Plotly, and contains a set of configurable parameters and strategies. See `orderbook-delta-visualizer/` for more details. +## Orderbook Delta GUI (optional) -[GUI](https://user-images.githubusercontent.com/15251343/176155957-e6096eb1-a1ef-4373-b66e-7ebaa83b5b84.mov) +To visualize the orderbook delta live, use the orderbook-delta-visualizer. It's written in Python, with plotting +handled by Dash and Plotly, and it contains a set of configurable parameters and strategies. +See `orderbook-delta-visualizer/` for more details. +[GUI](https://user-images.githubusercontent.com/15251343/176155957-e6096eb1-a1ef-4373-b66e-7ebaa83b5b84.mov) ## Settings + `settings.json` contains all the configurable options: | Name | Explanation | |-------------------|------------------------------------------------------------------------| | `market_name` | Name of futures market on FTX (default: BTC-PERP) | -| `time_delta` | Delay in seconds between queries (default: 5) | +| `sampling_time` | Time (in seconds) to sample orderbook, each sample is 1s (default: 5) | | `bb_period` | Bollinger band period (default: 20) | | `bb_std_dev` | Bollinger band standard deviation (default: 2) | -| `orderbook_depth` | Depth of orderbook to query (default: 1) | +| `orderbook_depth` | Depth of orderbook to sum (default: 5) | | `live` | Place live orders on FTX, requires API keys in `.env` (default: false) | | `order_size` | Size of order to place (default: 0.1618 BTC) | | `tp_percent` | Percent move to take profit at (default: 0.2%) | @@ -86,16 +94,25 @@ You can use a live orderbook visualizer written in Python. The visualizer uses D | `write_to_file` | Store positions in a csv file for further analysis (default: true) | ## TODO + - [ ] Use Kelly criterion for order sizing (probabilities can be estimated from prior analysis) -- [ ] Use dynamic take profit and stop loss based on market movement (this is simply used as protection from getting rekt, not as actual exit points) -- [ ] Perform spectral analysis with wider timeframes to identify optimal -market conditions +- [ ] Use dynamic take profit and stop loss based on market movement (this is simply used as protection from getting + rekt, not as actual exit points) +- [ ] Perform spectral analysis with wider timeframes to identify optimal + market conditions - [ ] Switch to websockets API for reduced data query lag -- [ ] For more high frequency applications, switching to a library like [ccapi](https://github.com/crypto-chassis/ccapi/) is handy. Unfortunately this only exists for C++ right now. +- [ ] For more high frequency applications, switching to a library + like [ccapi](https://github.com/crypto-chassis/ccapi/) is handy. Unfortunately this only exists for C++ right now. ## Disclaimer -This project is for educational purposes only. You should not construe any such information or other material as legal, tax, investment, financial, or other advice. Nothing contained here constitutes a solicitation, recommendation, endorsement, or offer by me or any third party service provider to buy or sell any securities or other financial instruments in this or in any other jurisdiction in which such solicitation or offer would be unlawful under the securities laws of such jurisdiction. + +This project is for educational purposes only. You should not construe any such information or other material as legal, +tax, investment, financial, or other advice. Nothing contained here constitutes a solicitation, recommendation, +endorsement, or offer by me or any third party service provider to buy or sell any securities or other financial +instruments in this or in any other jurisdiction in which such solicitation or offer would be unlawful under the +securities laws of such jurisdiction. If you plan to use real money, use at your own risk. -Under no circumstances will I be held responsible or liable in any way for any claims, damages, losses, expenses, costs, or liabilities whatsoever, including, without limitation, any direct or indirect damages for loss of profits. +Under no circumstances will I be held responsible or liable in any way for any claims, damages, losses, expenses, costs, +or liabilities whatsoever, including, without limitation, any direct or indirect damages for loss of profits. diff --git a/settings-example.json b/settings-example.json index df4d2ca..e27d830 100644 --- a/settings-example.json +++ b/settings-example.json @@ -1,9 +1,9 @@ { "market_name": "BTC-PERP", - "time_delta": 5, + "sampling_time": 60, "bb_period": 20, "bb_std_dev": 2.0, - "orderbook_depth": 1, + "orderbook_depth": 5, "live": false, "order_size": 0.1618, "tp_percent": 0.2, diff --git a/src/helpers.rs b/src/helpers.rs index 4ff5eca..64dafdc 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -5,13 +5,13 @@ pub(crate) struct SettingsFile { /// Name of futures market on FTX pub(crate) market_name: String, - /// Time delay (in seconds) between queries - pub(crate) time_delta: u64, + /// Time (in seconds) to sample orderbook, each sample is 1s + pub(crate) sampling_time: u64, /// Period of bollinger band pub(crate) bb_period: usize, /// Standard deviation of bollinger band pub(crate) bb_std_dev: f64, - /// Depth of orderbook + /// Depth of orderbook to sum pub(crate) orderbook_depth: u32, /// Make live trades or not pub(crate) live: bool, diff --git a/src/main.rs b/src/main.rs index 8b448e1..2627057 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,7 +98,7 @@ async fn main() { log::info!("Order size and precision check...PASS"); // Set up bollinger bands - let mut bb = ta::indicators::BollingerBands::new( + let mut bollinger_bands = ta::indicators::BollingerBands::new( settings.bb_period, settings.bb_std_dev, ).unwrap(); @@ -109,41 +109,53 @@ async fn main() { let mut current_side: helpers::Side = helpers::Side::default(); let mut price = rust_decimal::Decimal::default(); - log::info!("Setting trigger in {:?} iterations (approx {:?}s)...", + log::info!("Setting trigger after {:?} samples, each sample is {:?}s (total = {:?}s)...", settings.bb_period, - settings.bb_period as u64 * settings.time_delta + settings.sampling_time, + settings.bb_period as u64 * settings.sampling_time ); loop { count += 1; - // Sleep before loop logic to handle continue statements - std::thread::sleep(std::time::Duration::from_secs(settings.time_delta)); - - // Get orderbook - let order_book = api.request( - ftx::rest::GetOrderBook { - market_name: String::from(&settings.market_name), - depth: Option::from(settings.orderbook_depth), - } - ).await; - let order_book = match order_book { - Err(e) => { - // Continue loop is getting orderbook fails - log::error!("Error: {:?}", e); - continue; + let mut bid_ask_delta = 0.0; + + for _ in 0..settings.sampling_time { + // Get orderbook + let order_book = api.request( + ftx::rest::GetOrderBook { + market_name: String::from(&settings.market_name), + depth: Option::from(settings.orderbook_depth), + } + ).await; + let order_book = match order_book { + Err(e) => { + // Continue loop if getting orderbook fails + log::error!("Error: {:?}", e); + continue; + } + Ok(o) => o + }; + + // Calculate values used for analysis + let mut total_bid_volume = rust_decimal::Decimal::from(0); + let mut total_ask_volume = rust_decimal::Decimal::from(0); + + for idx in 0..settings.orderbook_depth as usize { + total_bid_volume += order_book.bids[idx].1; + total_ask_volume += order_book.asks[idx].1; } - Ok(o) => o - }; + bid_ask_delta += rust_decimal::prelude::ToPrimitive::to_f64( + &(total_bid_volume - total_ask_volume)).unwrap(); + + log::debug!("bid_ask_delta={:.2}", &bid_ask_delta); + + std::thread::sleep(std::time::Duration::from_secs(1)); + } - // Calculate values used for analysis - let perp_delta = rust_decimal::prelude::ToPrimitive::to_f64( - &(order_book.bids[0].1 - order_book.asks[0].1)).unwrap(); - let out = ta::Next::next(&mut bb, perp_delta); - let bb_lower = out.lower; - let bb_upper = out.upper; + let bb = ta::Next::next(&mut bollinger_bands, bid_ask_delta); - log::debug!("perp_delta={:.2}, bb_lower={:.2}, bb_upper={:.2}", - perp_delta, bb_lower, bb_upper); + log::info!("bid_ask_delta={:.2}, bb_lower={:.2}, bb_upper={:.2}", + bid_ask_delta, bb.lower, bb.upper); // Only perform further calculation if bb_period is passed if count > settings.bb_period { @@ -152,7 +164,7 @@ async fn main() { } // Entry conditions - if perp_delta > bb_upper || perp_delta < bb_lower { + if bid_ask_delta > bb.upper || bid_ask_delta < bb.lower { // Get current price let price_result = api.request( ftx::rest::GetFuture { @@ -172,7 +184,7 @@ async fn main() { // Create local variables to handle side let mut _side: helpers::Side = helpers::Side::Buy; - if perp_delta > bb_upper { + if bid_ask_delta > bb.upper { // Enter short position _side = helpers::Side::Sell; price = bid_price; @@ -181,10 +193,10 @@ async fn main() { if _side == current_side { continue; } else { current_side = _side } log::info!( - "Perp delta above upper bb, {:?} at {:?}", + "Bid-ask delta above upper bb, {:?} at {:?}", _side, price ); - } else if perp_delta < bb_lower { + } else if bid_ask_delta < bb.lower { // Enter long position _side = helpers::Side::Buy; price = ask_price; @@ -193,7 +205,7 @@ async fn main() { if _side == current_side { continue; } else { current_side = _side } log::info!( - "Perp delta below lower bb, {:?} at {:?}", + "Bid-ask delta below lower bb, {:?} at {:?}", _side, price ); } @@ -231,14 +243,13 @@ async fn main() { &api, &settings.market_name, ) ); - futures::executor::block_on( - order_handler::cancel_all_trigger_orders( - &api, &settings.market_name, - ) - ); } + futures::executor::block_on( + order_handler::cancel_all_trigger_orders( + &api, &settings.market_name, + ) + ); - // TODO: Use Kelly criterion for order sizing // Place order on FTX let order_placed = futures::executor::block_on( order_handler::place_market_order( @@ -267,7 +278,6 @@ async fn main() { ); // If unable to place TP or SL, cancel all orders - // TODO: Market close position in event of failure if !triggers_placed { log::warn!("Cancelling all orders..."); let order_closed = futures::executor::block_on( diff --git a/src/order_handler.rs b/src/order_handler.rs index 9a960a6..41ff505 100644 --- a/src/order_handler.rs +++ b/src/order_handler.rs @@ -1,7 +1,6 @@ //! A set of functions to handle placing market or limit orders, //! trigger orders and canceling orders - /// Create a market order on FTX pub(crate) async fn place_market_order( api: &ftx::rest::Rest, @@ -41,7 +40,8 @@ pub(crate) async fn get_open_position(api: &ftx::rest::Rest, market_name: &str) let positions = api.request(ftx::rest::GetPositions {}).await.unwrap(); for position in positions { - if position.future == market_name { + if position.future == market_name && position.open_size != rust_decimal::Decimal::from(0) { + log::debug!("{:?}", position); return true; } } @@ -49,7 +49,7 @@ pub(crate) async fn get_open_position(api: &ftx::rest::Rest, market_name: &str) } -/// Close postion at market +/// Close position at market pub(crate) async fn market_close_order(api: &ftx::rest::Rest, market_name: &str) -> bool { let positions = api.request(ftx::rest::GetPositions {}).await.unwrap(); @@ -99,7 +99,7 @@ pub(crate) async fn cancel_all_trigger_orders(api: &ftx::rest::Rest, market_name true } Err(e) => { - log::error!("Unable to cancel orders Err: {:?}, panicking!", e); + log::error!("Unable to cancel orders Err: {:?}", e); false } };