feat: add /orderbook route
This commit is contained in:
parent
be1424b1e1
commit
037afe36dd
|
@ -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 =
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue