feat: add /orderbook route

This commit is contained in:
dboures 2023-05-07 16:43:55 -05:00
parent be1424b1e1
commit 037afe36dd
No known key found for this signature in database
GPG Key ID: AB3790129D478852
5 changed files with 175 additions and 101 deletions

View File

@ -12,7 +12,7 @@ use {
};
#[derive(Debug, Deserialize)]
pub struct Params {
pub struct CandleParams {
pub market_name: String,
pub from: u64,
pub to: u64,
@ -21,7 +21,7 @@ pub struct Params {
#[get("/candles")]
pub async fn get_candles(
info: web::Query<Params>,
info: web::Query<CandleParams>,
context: web::Data<WebContext>,
) -> Result<HttpResponse, ServerError> {
let resolution =

View File

@ -1,15 +1,20 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::server_error::ServerError;
use actix_web::{get, web, HttpResponse, Scope};
use futures::join;
use num_traits::ToPrimitive;
use openbook_candles::{
database::fetch::{fetch_coingecko_24h_high_low, fetch_coingecko_24h_volume},
structs::{
coingecko::{CoinGecko24HourVolume, CoinGeckoPair, CoinGeckoTicker, PgCoinGecko24HighLow},
slab::{get_best_bids_and_asks},
coingecko::{
CoinGecko24HourVolume, CoinGeckoOrderBook, CoinGeckoPair, CoinGeckoTicker,
PgCoinGecko24HighLow,
},
slab::{get_best_bids_and_asks, get_orderbooks_with_depth},
},
utils::WebContext,
};
use serde::Deserialize;
use solana_client::nonblocking::rpc_client::RpcClient;
pub fn service() -> Scope {
@ -19,6 +24,12 @@ pub fn service() -> Scope {
.service(orderbook)
}
#[derive(Debug, Deserialize)]
pub struct OrderBookParams {
pub ticker_id: String, // market_name
pub depth: usize,
}
#[get("/pairs")]
pub async fn pairs(context: web::Data<WebContext>) -> Result<HttpResponse, ServerError> {
let markets = context.markets.clone();
@ -47,11 +58,8 @@ pub async fn tickers(context: web::Data<WebContext>) -> Result<HttpResponse, Ser
let volume_fut = fetch_coingecko_24h_volume(&mut c1);
let high_low_fut = fetch_coingecko_24h_high_low(&mut c2);
let ((best_bids, best_asks), volume_query, high_low_quey) = join!(
bba_fut,
volume_fut,
high_low_fut,
);
let ((best_bids, best_asks), volume_query, high_low_quey) =
join!(bba_fut, volume_fut, high_low_fut,);
let raw_volumes = match volume_query {
Ok(c) => c,
@ -85,13 +93,13 @@ pub async fn tickers(context: web::Data<WebContext>) -> Result<HttpResponse, Ser
ticker_id: m.name.clone(),
base_currency: m.base_mint_key.clone(),
target_currency: m.quote_mint_key.clone(),
last_price: high_low.close.to_f64().unwrap(),
base_volume: volume.base_volume.to_f64().unwrap(),
target_volume: volume.target_volume.to_f64().unwrap(),
bid: best_bids[index].to_f64().unwrap(),
ask: best_asks[index].to_f64().unwrap(),
high: high_low.high.to_f64().unwrap(),
low: high_low.low.to_f64().unwrap(),
last_price: high_low.close.to_string(),
base_volume: volume.base_volume.to_string(),
target_volume: volume.target_volume.to_string(),
bid: best_bids[index].to_string(),
ask: best_asks[index].to_string(),
high: high_low.high.to_string(),
low: high_low.low.to_string(),
}
})
.collect::<Vec<CoinGeckoTicker>>();
@ -100,23 +108,27 @@ pub async fn tickers(context: web::Data<WebContext>) -> Result<HttpResponse, Ser
}
#[get("/orderbook")]
pub async fn orderbook(context: web::Data<WebContext>) -> Result<HttpResponse, ServerError> {
pub async fn orderbook(
info: web::Query<OrderBookParams>,
context: web::Data<WebContext>,
) -> Result<HttpResponse, ServerError> {
let client = RpcClient::new(context.rpc_url.clone());
let market_name = &info.ticker_id;
let market = context
.markets
.iter()
.find(|m| m.name == *market_name)
.ok_or(ServerError::MarketNotFound)?;
let depth = info.depth;
let markets = &context.markets;
let (best_bids, best_asks) = get_best_bids_and_asks(client, markets).await;
// let bids = bid_bytes.into_iter().map(|mut x| Slab::new(x.as_mut_slice())).collect::<Vec<_>>();
// Slab::new(&mut x.data)
// let mut bb = bid_bytes[0].clone();
// let data_end = bb.len() - 7;
// let goo = Slab::new(&mut bb[13..data_end]);
// decode results
let markets = context.markets.clone();
Ok(HttpResponse::Ok().json(markets))
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap().as_millis();
let (bid_levels, ask_levels) = get_orderbooks_with_depth(client, market, depth).await;
let result = CoinGeckoOrderBook {
timestamp: timestamp.to_string(),
ticker_id: market.name.clone(),
bids: bid_levels,
asks: ask_levels,
};
Ok(HttpResponse::Ok().json(result))
}

View File

@ -12,7 +12,7 @@ use {
};
#[derive(Debug, Deserialize)]
pub struct Params {
pub struct TraderParams {
pub market_name: String,
pub from: u64,
pub to: u64,
@ -20,7 +20,7 @@ pub struct Params {
#[get("/traders/base-volume")]
pub async fn get_top_traders_by_base_volume(
info: web::Query<Params>,
info: web::Query<TraderParams>,
context: web::Data<WebContext>,
) -> Result<HttpResponse, ServerError> {
let selected_market = context.markets.iter().find(|x| x.name == info.market_name);
@ -56,7 +56,7 @@ pub async fn get_top_traders_by_base_volume(
#[get("/traders/quote-volume")]
pub async fn get_top_traders_by_quote_volume(
info: web::Query<Params>,
info: web::Query<TraderParams>,
context: web::Data<WebContext>,
) -> Result<HttpResponse, ServerError> {
let selected_market = context.markets.iter().find(|x| x.name == info.market_name);

View File

@ -1,9 +1,16 @@
use num_traits::ToPrimitive;
use serde::Serialize;
use sqlx::types::Decimal;
use super::{markets::MarketInfo, openbook::token_factor};
#[derive(Debug, Clone, Serialize)]
pub struct CoinGeckoOrderBook {
pub ticker_id: String,
pub timestamp: String, //as milliseconds
pub bids: Vec<(String, String)>,
pub asks: Vec<(String, String)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CoinGeckoPair {
pub ticker_id: String,
@ -17,13 +24,13 @@ pub struct CoinGeckoTicker {
pub ticker_id: String,
pub base_currency: String,
pub target_currency: String,
pub last_price: f64,
pub base_volume: f64,
pub target_volume: f64,
pub bid: f64,
pub ask: f64,
pub high: f64,
pub low: f64,
pub last_price: String,
pub base_volume: String,
pub target_volume: String,
pub bid: String,
pub ask: String,
pub high: String,
pub low: String,
}
pub struct PgCoinGecko24HourVolume {
@ -34,12 +41,8 @@ pub struct PgCoinGecko24HourVolume {
impl PgCoinGecko24HourVolume {
pub fn convert_to_readable(&self, markets: &Vec<MarketInfo>) -> CoinGecko24HourVolume {
let market = markets.iter().find(|m| m.address == self.address).unwrap();
let base_volume = (self.raw_base_size / token_factor(market.base_decimals))
.to_f64()
.unwrap();
let target_volume = (self.raw_quote_size / token_factor(market.quote_decimals))
.to_f64()
.unwrap();
let base_volume = (self.raw_base_size / token_factor(market.base_decimals)).to_string();
let target_volume = (self.raw_quote_size / token_factor(market.quote_decimals)).to_string();
CoinGecko24HourVolume {
market_name: market.name.clone(),
base_volume,
@ -51,8 +54,8 @@ impl PgCoinGecko24HourVolume {
#[derive(Debug, Default)]
pub struct CoinGecko24HourVolume {
pub market_name: String,
pub base_volume: f64,
pub target_volume: f64,
pub base_volume: String,
pub target_volume: String,
}
#[derive(Debug, Default)]

View File

@ -1,14 +1,16 @@
use anchor_lang::prelude::Pubkey;
use arrayref::{array_refs, mut_array_refs};
use bytemuck::{cast, cast_mut, cast_ref, cast_slice, cast_slice_mut, Pod, Zeroable};
use arrayref::array_refs;
use bytemuck::{cast_mut, cast_ref, cast_slice, Pod, Zeroable};
use futures::join;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use num_traits::ToPrimitive;
use solana_client::nonblocking::rpc_client::RpcClient;
use sqlx::types::Decimal;
use std::{
convert::{identity, TryFrom},
convert::TryFrom,
mem::{align_of, size_of},
num::NonZeroU64, str::FromStr,
num::NonZeroU64,
str::FromStr,
};
use crate::structs::openbook::token_factor;
@ -53,14 +55,6 @@ struct InnerNode {
unsafe impl Zeroable for InnerNode {}
unsafe impl Pod for InnerNode {}
impl InnerNode {
fn walk_down(&self, search_key: u128) -> (NodeHandle, bool) {
let crit_bit_mask = (1u128 << 127) >> self.prefix_len;
let crit_bit = (search_key & crit_bit_mask) != 0;
(self.children[crit_bit as usize], crit_bit)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(packed)]
pub struct LeafNode {
@ -108,6 +102,21 @@ impl LeafNode {
NonZeroU64::new((self.key >> 64) as u64).unwrap()
}
pub fn readable_price(&self, market: &MarketInfo) -> Decimal {
let price_lots = Decimal::from((self.key >> 64) as u64);
let base_multiplier = token_factor(market.base_decimals);
let quote_multiplier = token_factor(market.quote_decimals);
let base_lot_size = Decimal::from(market.base_lot_size);
let quote_lot_size = Decimal::from(market.quote_lot_size);
(price_lots * quote_lot_size * base_multiplier) / (base_lot_size * quote_multiplier)
}
pub fn readable_quantity(&self, market: &MarketInfo) -> Decimal {
let base_lot_size = Decimal::from(market.base_lot_size);
let base_multiplier = token_factor(market.base_decimals);
Decimal::from(self.quantity) * base_lot_size / base_multiplier
}
#[inline]
pub fn order_id(&self) -> u128 {
self.key
@ -186,27 +195,6 @@ enum NodeRefMut<'a> {
}
impl AnyNode {
fn key(&self) -> Option<u128> {
match self.case()? {
NodeRef::Inner(inner) => Some(inner.key),
NodeRef::Leaf(leaf) => Some(leaf.key),
}
}
fn prefix_len(&self) -> u32 {
match self.case().unwrap() {
NodeRef::Inner(&InnerNode { prefix_len, .. }) => prefix_len,
NodeRef::Leaf(_) => 128,
}
}
fn children(&self) -> Option<[u32; 2]> {
match self.case().unwrap() {
NodeRef::Inner(&InnerNode { children, .. }) => Some(children),
NodeRef::Leaf(_) => None,
}
}
fn case(&self) -> Option<NodeRef> {
match NodeTag::try_from(self.tag) {
Ok(NodeTag::InnerNode) => Some(NodeRef::Inner(cast_ref(self))),
@ -275,14 +263,6 @@ unsafe fn invariant(check: bool) {
}
}
#[cfg(not(debug_assertions))]
#[inline(always)]
unsafe fn invariant(check: bool) {
if check {
std::hint::unreachable_unchecked();
}
}
/// Mainly copied from the original code, slightly modified to make working with it easier.
#[repr(transparent)]
pub struct Slab([u8]);
@ -369,6 +349,37 @@ impl Slab {
}
}
pub fn traverse(&self, descending: bool) -> Vec<&LeafNode> {
fn walk_rec<'a>(
slab: &'a Slab,
sub_root: NodeHandle,
buf: &mut Vec<&'a LeafNode>,
descending: bool,
) {
match slab.get(sub_root).unwrap().case().unwrap() {
NodeRef::Leaf(leaf) => {
buf.push(leaf);
}
NodeRef::Inner(inner) => {
if descending {
walk_rec(slab, inner.children[1], buf, descending);
walk_rec(slab, inner.children[0], buf, descending);
} else {
walk_rec(slab, inner.children[0], buf, descending);
walk_rec(slab, inner.children[1], buf, descending);
}
}
}
}
let mut buf = Vec::with_capacity(self.header().leaf_count as usize);
if let Some(r) = self.root() {
walk_rec(self, r, &mut buf, descending);
}
assert_eq!(buf.len(), buf.capacity());
buf
}
#[inline]
pub fn find_min(&self) -> Option<&LeafNode> {
let handle = self.find_min_max(false).unwrap();
@ -393,13 +404,7 @@ impl Slab {
} else {
self.find_min()
};
let price_lots = Decimal::from(min.unwrap().key >> 64);
let base_multiplier = token_factor(market.base_decimals);
let quote_multiplier = token_factor(market.quote_decimals);
let base_lot_size = Decimal::from(market.base_lot_size);
let quote_lot_size = Decimal::from(market.quote_lot_size);
(price_lots * quote_lot_size * base_multiplier) / (base_lot_size * quote_multiplier)
min.unwrap().readable_price(market)
}
}
@ -452,3 +457,57 @@ pub async fn get_best_bids_and_asks(
.collect::<Vec<_>>();
(best_bids, best_asks)
}
pub async fn get_orderbooks_with_depth(
client: RpcClient,
market: &MarketInfo,
depth: usize,
) -> (Vec<(String, String)>, Vec<(String, String)>) {
let keys = vec![
Pubkey::from_str(&market.bids_key).unwrap(),
Pubkey::from_str(&market.asks_key).unwrap(),
];
// let start = Instant::now();
let mut results = client.get_multiple_accounts(&keys).await.unwrap();
// let duration = start.elapsed();
// println!("Time elapsed in rpc call is: {:?}", duration);
let mut ask_acc = results.pop().unwrap().unwrap();
let mut bid_acc = results.pop().unwrap().unwrap();
let bids = Slab::new(&mut bid_acc.data);
let asks = Slab::new(&mut ask_acc.data);
let bid_leaves = bids.traverse(true);
let ask_leaves = asks.traverse(false);
let bid_levels = construct_levels(bid_leaves, market, depth);
let ask_levels = construct_levels(ask_leaves, market, depth);
(bid_levels, ask_levels)
}
fn construct_levels(
leaves: Vec<&LeafNode>,
market: &MarketInfo,
depth: usize,
) -> Vec<(String, String)> {
let mut levels: Vec<(f64, f64)> = vec![];
for x in leaves {
let len = levels.len();
if len > 0 && levels[len - 1].0 == x.readable_price(market).to_f64().unwrap() {
let q = x.readable_quantity(market).to_f64().unwrap();
levels[len - 1].1 += q;
} else if len == depth {
break;
} else {
levels.push((
x.readable_price(market).to_f64().unwrap(),
x.readable_quantity(market).to_f64().unwrap(),
));
}
}
levels
.into_iter()
.map(|x| (x.0.to_string(), x.1.to_string()))
.collect()
}