Skip to content

Commit

Permalink
Added sampling time, multi depth queries and code cleanup
Browse files Browse the repository at this point in the history
- The bid ask delta is now sampled over a period to reduce high frequency noise
- The orderbook can now be queried to a specified depth d. The orderbook bid/ask will be summed from 1 to d
- General cleanup of code, some typos fixed
  • Loading branch information
golden-lucky-monkey committed Aug 25, 2022
1 parent a5495a0 commit 6de1620
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 84 deletions.
87 changes: 52 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,118 @@
# 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
```

### 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%) |
| `sl_percent` | Percent move to stop loss at (default: 0.1%) |
| `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.
4 changes: 2 additions & 2 deletions settings-example.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
90 changes: 50 additions & 40 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
);
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions src/order_handler.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -41,15 +40,16 @@ 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;
}
}
return false;
}


/// 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();

Expand Down Expand Up @@ -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
}
};
Expand Down

0 comments on commit 6de1620

Please sign in to comment.