From 4abadc9589f707097727e601c9bcd7b8e0dadbd0 Mon Sep 17 00:00:00 2001 From: dboures Date: Mon, 13 Mar 2023 16:18:18 -0500 Subject: [PATCH] feat: can query traders by quote volume --- sqlx-data.json | 102 ++++++++++++++++++++++++++-------------- src/database/fetch.rs | 49 +++++++++++++++---- src/server/main.rs | 9 ++-- src/server/traders.rs | 72 +++++++++++++++++++++++----- src/structs/mod.rs | 1 + src/structs/openbook.rs | 29 +----------- src/structs/trader.rs | 52 ++++++++++++++++++++ 7 files changed, 228 insertions(+), 86 deletions(-) create mode 100644 src/structs/trader.rs diff --git a/sqlx-data.json b/sqlx-data.json index d14a6f9..08a5120 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,5 +1,39 @@ { "db": "PostgreSQL", + "21b633d5aec33394129b051ea1df0ee9ab097626d74d8943f6323f9fb42723b5": { + "describe": { + "columns": [ + { + "name": "open_orders_owner", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "raw_ask_size!", + "ordinal": 1, + "type_info": "Numeric" + }, + { + "name": "raw_bid_size!", + "ordinal": 2, + "type_info": "Numeric" + } + ], + "nullable": [ + false, + null, + null + ], + "parameters": { + "Left": [ + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "SELECT \n open_orders_owner, \n sum(\n native_qty_received * CASE bid WHEN true THEN 0 WHEN false THEN 1 END\n ) as \"raw_ask_size!\",\n sum(\n native_qty_paid * CASE bid WHEN true THEN 1 WHEN false THEN 0 END\n ) as \"raw_bid_size!\"\n FROM fills\n WHERE market = $1\n AND time >= $2\n AND time < $3\n GROUP BY open_orders_owner\n ORDER BY \n sum(native_qty_received * CASE bid WHEN true THEN 0 WHEN false THEN 1 END) \n + \n sum(native_qty_paid * CASE bid WHEN true THEN 1 WHEN false THEN 0 END) \nDESC \nLIMIT 10000" + }, "35e8220c601aca620da1cfcb978c8b7a64dcbf15550521b418cf509015cd88d8": { "describe": { "columns": [], @@ -269,6 +303,40 @@ }, "query": "SELECT \n start_time as \"start_time!\",\n end_time as \"end_time!\",\n resolution as \"resolution!\",\n market_name as \"market_name!\",\n open as \"open!\",\n close as \"close!\",\n high as \"high!\",\n low as \"low!\",\n volume as \"volume!\",\n complete as \"complete!\"\n from candles\n where market_name = $1\n and resolution = $2\n and start_time >= $3\n and end_time <= $4\n and complete = true\n ORDER BY start_time asc" }, + "aee3a3e04f837bd62e263452bfbaf5d7dff271799c80d5efd22a54954ac212c4": { + "describe": { + "columns": [ + { + "name": "open_orders_owner", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "raw_ask_size!", + "ordinal": 1, + "type_info": "Numeric" + }, + { + "name": "raw_bid_size!", + "ordinal": 2, + "type_info": "Numeric" + } + ], + "nullable": [ + false, + null, + null + ], + "parameters": { + "Left": [ + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "SELECT \n open_orders_owner, \n sum(\n native_qty_paid * CASE bid WHEN true THEN 0 WHEN false THEN 1 END\n ) as \"raw_ask_size!\",\n sum(\n native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END\n ) as \"raw_bid_size!\"\n FROM fills\n WHERE market = $1\n AND time >= $2\n AND time < $3\n GROUP BY open_orders_owner\n ORDER BY \n sum(native_qty_paid * CASE bid WHEN true THEN 0 WHEN false THEN 1 END) \n + \n sum(native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END) \nDESC \nLIMIT 10000" + }, "b259d64b388eb675b727ee511529472177b59ea616041360217afc2d928f33ed": { "describe": { "columns": [], @@ -331,40 +399,6 @@ }, "query": "SELECT \n time as \"time!\",\n bid as \"bid!\",\n maker as \"maker!\",\n native_qty_paid as \"native_qty_paid!\",\n native_qty_received as \"native_qty_received!\",\n native_fee_or_rebate as \"native_fee_or_rebate!\" \n from fills \n where market = $1\n and time >= $2\n and time < $3 \n and maker = true\n ORDER BY time asc" }, - "dc367af7bb8da9d57033833da06567f3f3cfdcff72e5b140ccd3355f91b4c5a7": { - "describe": { - "columns": [ - { - "name": "open_orders_owner", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "raw_ask_size!", - "ordinal": 1, - "type_info": "Numeric" - }, - { - "name": "raw_bid_size!", - "ordinal": 2, - "type_info": "Numeric" - } - ], - "nullable": [ - false, - null, - null - ], - "parameters": { - "Left": [ - "Text", - "Timestamptz", - "Timestamptz" - ] - } - }, - "query": "SELECT \n open_orders_owner, \n sum(\n native_qty_paid * CASE bid WHEN true THEN 0 WHEN false THEN 1 END\n ) as \"raw_ask_size!\",\n sum(\n native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END\n ) as \"raw_bid_size!\"\n FROM public.\"fills\"\n WHERE market = $1\n AND time >= $2\n AND time < $3\n GROUP BY open_orders_owner\n ORDER BY \n sum(native_qty_paid * CASE bid WHEN true THEN 0 WHEN false THEN 1 END) \n + \n sum(native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END) \nDESC " - }, "dc7c7c04b6870b9617e1e869aa4b7027baddaeeb22f2792f2e9c40f643f863c7": { "describe": { "columns": [ diff --git a/src/database/fetch.rs b/src/database/fetch.rs index 9d41282..6896d55 100644 --- a/src/database/fetch.rs +++ b/src/database/fetch.rs @@ -2,11 +2,7 @@ use chrono::{DateTime, Utc}; use sqlx::{Pool, Postgres}; use crate::{ - structs::{ - candle::Candle, - openbook::{PgOpenBookFill, PgTrader}, - resolution::Resolution, - }, + structs::{candle::Candle, openbook::PgOpenBookFill, resolution::Resolution, trader::PgTrader}, utils::AnyhowWrap, }; @@ -198,7 +194,7 @@ pub async fn fetch_tradingview_candles( .map_err_anyhow() } -pub async fn fetch_top_traders_by_volume_from( +pub async fn fetch_top_traders_by_base_volume_from( pool: &Pool, market_address_string: &str, start_time: DateTime, @@ -214,7 +210,7 @@ pub async fn fetch_top_traders_by_volume_from( sum( native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END ) as "raw_bid_size!" - FROM public."fills" + FROM fills WHERE market = $1 AND time >= $2 AND time < $3 @@ -223,7 +219,8 @@ pub async fn fetch_top_traders_by_volume_from( sum(native_qty_paid * CASE bid WHEN true THEN 0 WHEN false THEN 1 END) + sum(native_qty_received * CASE bid WHEN true THEN 1 WHEN false THEN 0 END) -DESC "#, +DESC +LIMIT 10000"#, market_address_string, start_time, end_time @@ -233,4 +230,38 @@ DESC "#, .map_err_anyhow() } -// pub async fn fetch_traders_above_x_dollars +pub async fn fetch_top_traders_by_quote_volume_from( + pool: &Pool, + market_address_string: &str, + start_time: DateTime, + end_time: DateTime, +) -> anyhow::Result> { + sqlx::query_as!( + PgTrader, + r#"SELECT + open_orders_owner, + sum( + native_qty_received * CASE bid WHEN true THEN 0 WHEN false THEN 1 END + ) as "raw_ask_size!", + sum( + native_qty_paid * CASE bid WHEN true THEN 1 WHEN false THEN 0 END + ) as "raw_bid_size!" + FROM fills + WHERE market = $1 + AND time >= $2 + AND time < $3 + GROUP BY open_orders_owner + ORDER BY + sum(native_qty_received * CASE bid WHEN true THEN 0 WHEN false THEN 1 END) + + + sum(native_qty_paid * CASE bid WHEN true THEN 1 WHEN false THEN 0 END) +DESC +LIMIT 10000"#, + market_address_string, + start_time, + end_time + ) + .fetch_all(pool) + .await + .map_err_anyhow() +} diff --git a/src/server/main.rs b/src/server/main.rs index 367522f..cd11197 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -1,8 +1,7 @@ use actix_web::{ - get, middleware::Logger, web::{self, Data}, - App, HttpResponse, HttpServer, Responder, + App, HttpServer, }; use candles::get_candles; use dotenv; @@ -12,14 +11,14 @@ use openbook_candles::{ structs::markets::load_markets, utils::{Config, WebContext}, }; -use sqlx::{Pool, Postgres}; use traders::get_top_traders_by_base_volume; +use crate::traders::get_top_traders_by_quote_volume; + mod candles; mod server_error; mod traders; - #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); @@ -56,7 +55,7 @@ async fn main() -> std::io::Result<()> { web::scope("/api") .service(get_candles) .service(get_top_traders_by_base_volume) - // .service(get_top_traders_by_quote_volume) + .service(get_top_traders_by_quote_volume), ) }) .bind(("127.0.0.1", 8080))? diff --git a/src/server/traders.rs b/src/server/traders.rs index 512a674..17618d3 100644 --- a/src/server/traders.rs +++ b/src/server/traders.rs @@ -1,7 +1,9 @@ use crate::server_error::ServerError; use openbook_candles::{ - database::fetch::fetch_top_traders_by_volume_from, - structs::openbook::{calculate_trader_volume, Trader}, + database::fetch::{ + fetch_top_traders_by_base_volume_from, fetch_top_traders_by_quote_volume_from, + }, + structs::trader::{calculate_trader_volume, Trader, TraderResponse, VolumeType}, utils::{to_timestampz, WebContext}, }; use { @@ -29,19 +31,67 @@ pub async fn get_top_traders_by_base_volume( let from = to_timestampz(info.from); let to = to_timestampz(info.to); - let raw_traders = - match fetch_top_traders_by_volume_from(&context.pool, &selected_market.address, from, to) - .await - { - Ok(c) => c, - Err(_) => return Err(ServerError::DbQueryError), - }; + let raw_traders = match fetch_top_traders_by_base_volume_from( + &context.pool, + &selected_market.address, + from, + to, + ) + .await + { + Ok(c) => c, + Err(_) => return Err(ServerError::DbQueryError), + }; let traders = raw_traders .into_iter() .map(|t| calculate_trader_volume(t, selected_market.base_decimals)) .collect::>(); - // TODO: add start and end in response? - Ok(HttpResponse::Ok().json(traders)) + let response = TraderResponse { + start_time: info.from, + end_time: info.to, + traders: traders, + volume_type: VolumeType::Base.to_string(), + }; + Ok(HttpResponse::Ok().json(response)) +} + +#[get("/traders/quote-volume")] +pub async fn get_top_traders_by_quote_volume( + info: web::Query, + context: web::Data, +) -> Result { + let selected_market = context.markets.iter().find(|x| x.name == info.market_name); + if selected_market.is_none() { + return Err(ServerError::MarketNotFound); + } + let selected_market = selected_market.unwrap(); + let from = to_timestampz(info.from); + let to = to_timestampz(info.to); + + let raw_traders = match fetch_top_traders_by_quote_volume_from( + &context.pool, + &selected_market.address, + from, + to, + ) + .await + { + Ok(c) => c, + Err(_) => return Err(ServerError::DbQueryError), + }; + + let traders = raw_traders + .into_iter() + .map(|t| calculate_trader_volume(t, selected_market.quote_decimals)) + .collect::>(); + + let response = TraderResponse { + start_time: info.from, + end_time: info.to, + traders: traders, + volume_type: VolumeType::Quote.to_string(), + }; + Ok(HttpResponse::Ok().json(response)) } diff --git a/src/structs/mod.rs b/src/structs/mod.rs index a1fd60e..eaa38b1 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -2,4 +2,5 @@ pub mod candle; pub mod markets; pub mod openbook; pub mod resolution; +pub mod trader; pub mod tradingview; diff --git a/src/structs/openbook.rs b/src/structs/openbook.rs index b3a7abf..cd5c6ab 100644 --- a/src/structs/openbook.rs +++ b/src/structs/openbook.rs @@ -1,7 +1,6 @@ use anchor_lang::{event, AnchorDeserialize, AnchorSerialize}; use chrono::{DateTime, Utc}; -use num_traits::{FromPrimitive, ToPrimitive}; -use serde::Serialize; +use num_traits::FromPrimitive; use solana_sdk::pubkey::Pubkey; use sqlx::types::Decimal; @@ -33,19 +32,6 @@ pub struct PgOpenBookFill { pub native_fee_or_rebate: Decimal, } -#[derive(Clone, Debug, PartialEq)] -pub struct PgTrader { - pub open_orders_owner: String, - pub raw_ask_size: Decimal, - pub raw_bid_size: Decimal, -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -pub struct Trader { - pub pubkey: String, - pub volume_base_units: f64, -} - #[derive(Copy, Clone, AnchorDeserialize)] #[cfg_attr(target_endian = "little", derive(Debug))] #[repr(packed)] @@ -129,17 +115,6 @@ pub fn calculate_fill_price_and_size( } } -pub fn calculate_trader_volume(trader: PgTrader, base_decimals: u8) -> Trader { - let bid_size = trader.raw_bid_size / token_factor(base_decimals); - let ask_size = trader.raw_ask_size / token_factor(base_decimals); - - Trader { - pubkey: trader.open_orders_owner, - volume_base_units: (bid_size + ask_size).to_f64().unwrap(), - // TODO: quote volume - } -} - -fn token_factor(decimals: u8) -> Decimal { +pub fn token_factor(decimals: u8) -> Decimal { Decimal::from_u64(10u64.pow(decimals as u32)).unwrap() } diff --git a/src/structs/trader.rs b/src/structs/trader.rs new file mode 100644 index 0000000..8963a9d --- /dev/null +++ b/src/structs/trader.rs @@ -0,0 +1,52 @@ +use std::fmt; + +use num_traits::ToPrimitive; +use serde::Serialize; +use sqlx::types::Decimal; + +use super::openbook::token_factor; + +#[derive(Clone, Debug, PartialEq)] +pub struct PgTrader { + pub open_orders_owner: String, + pub raw_ask_size: Decimal, + pub raw_bid_size: Decimal, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub enum VolumeType { + Base, + Quote, +} +impl fmt::Display for VolumeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + VolumeType::Base => write!(f, "Base"), + VolumeType::Quote => write!(f, "Quote"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct Trader { + pub pubkey: String, + pub volume: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct TraderResponse { + pub start_time: u64, + pub end_time: u64, + pub volume_type: String, + pub traders: Vec, +} + +pub fn calculate_trader_volume(trader: PgTrader, decimals: u8) -> Trader { + let bid_size = trader.raw_bid_size / token_factor(decimals); + let ask_size = trader.raw_ask_size / token_factor(decimals); + + Trader { + pubkey: trader.open_orders_owner, + volume: (bid_size + ask_size).to_f64().unwrap(), + } +}