Skip to content

Commit

Permalink
Deepbook Summary Endpoint (#20536)
Browse files Browse the repository at this point in the history
## Description 

1. Deepbook Summary Endpoint: /summary

Sample response:

`[{"trading_pairs":"WUSDC_USDC","quote_currency":"USDC","base_volume":3675177.5,"last_price":1.0,"lowest_price_24h":0.9999,"base_currency":"WUSDC","highest_bid":1.0,"highest_price_24h":1.0002,"price_change_percent_24h":-0.009999000099991662,"quote_volume":3675165.11285,"lowest_ask":1.0001},{"highest_price_24h":4.4748,"base_volume":48790.4,"lowest_price_24h":0.0033,"highest_bid":4.2293,"lowest_ask":4.2361,"trading_pairs":"SUI_AUSD","quote_currency":"AUSD","price_change_percent_24h":0.6522306287503277,"quote_volume":207421.6792,"last_price":4.25,"base_currency":"SUI"}]`

2. Volume query defaults to using quote asset

## Test plan 

How did you test the new or updated feature?

Tested locally using production data

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [x] Indexer: Deepbook Indexer
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
- [ ] REST API:
  • Loading branch information
leecchh authored Dec 9, 2024
1 parent 24b4093 commit 1559f60
Showing 1 changed file with 225 additions and 3 deletions.
228 changes: 225 additions & 3 deletions crates/sui-deepbook-indexer/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub const GET_NET_DEPOSITS: &str = "/get_net_deposits/:asset_ids/:timestamp";
pub const TICKER_PATH: &str = "/ticker";
pub const TRADES_PATH: &str = "/trades/:pool_name";
pub const ASSETS_PATH: &str = "/assets";
pub const SUMMARY_PATH: &str = "/summary";
pub const LEVEL2_PATH: &str = "/orderbook/:pool_name";
pub const LEVEL2_MODULE: &str = "pool";
pub const LEVEL2_FUNCTION: &str = "get_level2_ticks_from_mid";
Expand Down Expand Up @@ -78,6 +79,7 @@ pub(crate) fn make_router(state: PgDeepbookPersistent) -> Router {
.route(TICKER_PATH, get(ticker))
.route(TRADES_PATH, get(trades))
.route(ASSETS_PATH, get(assets))
.route(SUMMARY_PATH, get(summary))
.with_state(state)
}

Expand Down Expand Up @@ -165,7 +167,7 @@ async fn historical_volume(
let volume_in_base = params
.get("volume_in_base")
.map(|v| v == "true")
.unwrap_or(true);
.unwrap_or(false);
let column_to_query = if volume_in_base {
sql::<diesel::sql_types::BigInt>("base_quantity")
} else {
Expand Down Expand Up @@ -242,7 +244,7 @@ async fn get_historical_volume_by_balance_manager_id(
let volume_in_base = params
.get("volume_in_base")
.map(|v| v == "true")
.unwrap_or(true);
.unwrap_or(false);
let column_to_query = if volume_in_base {
sql::<diesel::sql_types::BigInt>("base_quantity")
} else {
Expand Down Expand Up @@ -331,7 +333,7 @@ async fn get_historical_volume_by_balance_manager_id_with_interval(
let volume_in_base = params
.get("volume_in_base")
.map(|v| v == "true")
.unwrap_or(true);
.unwrap_or(false);
let column_to_query = if volume_in_base {
sql::<diesel::sql_types::BigInt>("base_quantity")
} else {
Expand Down Expand Up @@ -465,6 +467,226 @@ async fn fetch_historical_volume(
.map(|Json(volumes)| volumes)
}

#[allow(clippy::get_first)]
async fn summary(
State(state): State<PgDeepbookPersistent>,
) -> Result<Json<Vec<HashMap<String, Value>>>, DeepBookError> {
// Call the ticker function to get volumes and last price
let ticker_data = ticker(Query(HashMap::new()), State(state.clone())).await?;
let Json(ticker_map) = ticker_data;

// Prepare pool metadata (including decimals and pool_id <-> pool_name mapping)
let pools: Json<Vec<Pools>> = get_pools(State(state.clone())).await?;
let pool_metadata: HashMap<String, (String, (i16, i16))> = pools
.0
.into_iter()
.map(|pool| {
(
pool.pool_name.clone(),
(
pool.pool_id.clone(),
(pool.base_asset_decimals, pool.quote_asset_decimals),
),
)
})
.collect();

// Prepare pool decimals for scaling
let pool_decimals: HashMap<String, (i16, i16)> = pool_metadata
.iter()
.map(|(_, (pool_id, decimals))| (pool_id.clone(), *decimals))
.collect();

// Call the price_change_24h function to get price changes
let price_change_map = price_change_24h(&pool_metadata, State(state.clone())).await?;

// Call the high_low_prices_24h function to get the highest and lowest prices
let high_low_map = high_low_prices_24h(&pool_decimals, State(state.clone())).await?;

let mut response = Vec::new();

for (pool_name, ticker_info) in &ticker_map {
if let Some((pool_id, _)) = pool_metadata.get(pool_name) {
// Extract data from the ticker function response
let last_price = ticker_info
.get("last_price")
.and_then(|price| price.as_f64())
.unwrap_or(0.0);

let base_volume = ticker_info
.get("base_volume")
.and_then(|volume| volume.as_f64())
.unwrap_or(0.0);

let quote_volume = ticker_info
.get("quote_volume")
.and_then(|volume| volume.as_f64())
.unwrap_or(0.0);

// Fetch the 24-hour price change percent
let price_change_percent = price_change_map.get(pool_name).copied().unwrap_or(0.0);

// Fetch the highest and lowest prices in the last 24 hours
let (highest_price, lowest_price) =
high_low_map.get(pool_id).copied().unwrap_or((0.0, 0.0));

// Fetch the highest bid and lowest ask from the orderbook
let orderbook_data = orderbook(
Path(pool_name.clone()),
Query(HashMap::from([("level".to_string(), "1".to_string())])),
State(state.clone()),
)
.await
.ok()
.map(|Json(data)| data);

let highest_bid = orderbook_data
.as_ref()
.and_then(|data| data.get("bids"))
.and_then(|bids| bids.as_array())
.and_then(|bids| bids.get(0))
.and_then(|bid| bid.as_array())
.and_then(|bid| bid.get(0))
.and_then(|price| price.as_str()?.parse::<f64>().ok())
.unwrap_or(0.0);

let lowest_ask = orderbook_data
.as_ref()
.and_then(|data| data.get("asks"))
.and_then(|asks| asks.as_array())
.and_then(|asks| asks.get(0))
.and_then(|ask| ask.as_array())
.and_then(|ask| ask.get(0))
.and_then(|price| price.as_str()?.parse::<f64>().ok())
.unwrap_or(0.0);

let mut summary_data = HashMap::new();
summary_data.insert(
"trading_pairs".to_string(),
Value::String(pool_name.clone()),
);
let parts: Vec<&str> = pool_name.split('_').collect();
let base_currency = parts.get(0).unwrap_or(&"Unknown").to_string();
let quote_currency = parts.get(1).unwrap_or(&"Unknown").to_string();

summary_data.insert("base_currency".to_string(), Value::String(base_currency));
summary_data.insert("quote_currency".to_string(), Value::String(quote_currency));
summary_data.insert("last_price".to_string(), Value::from(last_price));
summary_data.insert("base_volume".to_string(), Value::from(base_volume));
summary_data.insert("quote_volume".to_string(), Value::from(quote_volume));
summary_data.insert(
"price_change_percent_24h".to_string(),
Value::from(price_change_percent),
);
summary_data.insert("highest_price_24h".to_string(), Value::from(highest_price));
summary_data.insert("lowest_price_24h".to_string(), Value::from(lowest_price));
summary_data.insert("highest_bid".to_string(), Value::from(highest_bid));
summary_data.insert("lowest_ask".to_string(), Value::from(lowest_ask));

response.push(summary_data);
}
}

Ok(Json(response))
}

async fn high_low_prices_24h(
pool_decimals: &HashMap<String, (i16, i16)>,
State(state): State<PgDeepbookPersistent>,
) -> Result<HashMap<String, (f64, f64)>, DeepBookError> {
// Get the current timestamp in milliseconds
let end_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
.as_millis() as i64;

// Calculate the start time for 24 hours ago
let start_time = end_time - (24 * 60 * 60 * 1000);

let connection = &mut state.pool.get().await?;

// Query for trades within the last 24 hours for all pools
let results: Vec<(String, i64)> = schema::order_fills::table
.select((schema::order_fills::pool_id, schema::order_fills::price))
.filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time))
.order_by(schema::order_fills::pool_id.asc())
.load(connection)
.await?;

// Aggregate the highest and lowest prices for each pool
let mut price_map: HashMap<String, (f64, f64)> = HashMap::new();

for (pool_id, price) in results {
if let Some((base_decimals, quote_decimals)) = pool_decimals.get(&pool_id) {
let scaling_factor = 10f64.powi((9 - base_decimals + quote_decimals) as i32);
let price_f64 = price as f64 / scaling_factor;

let entry = price_map.entry(pool_id).or_insert((f64::MIN, f64::MAX));
// Update the highest and lowest prices
entry.0 = entry.0.max(price_f64); // Highest price
entry.1 = entry.1.min(price_f64); // Lowest price
}
}

Ok(price_map)
}

async fn price_change_24h(
pool_metadata: &HashMap<String, (String, (i16, i16))>,
State(state): State<PgDeepbookPersistent>,
) -> Result<HashMap<String, f64>, DeepBookError> {
let connection = &mut state.pool.get().await?;

// Calculate the timestamp for 24 hours ago
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| DeepBookError::InternalError("System time error".to_string()))?
.as_millis() as i64;

let timestamp_24h_ago = now - (24 * 60 * 60 * 1000); // 24 hours in milliseconds

let mut response = HashMap::new();

for (pool_name, (pool_id, (base_decimals, quote_decimals))) in pool_metadata.iter() {
// Get the latest price <= 24 hours ago
let earliest_trade_24h = schema::order_fills::table
.filter(schema::order_fills::pool_id.eq(pool_id))
.filter(schema::order_fills::checkpoint_timestamp_ms.le(timestamp_24h_ago))
.order_by(schema::order_fills::checkpoint_timestamp_ms.desc())
.select(schema::order_fills::price)
.first::<i64>(connection)
.await;

// Get the most recent price
let most_recent_trade = schema::order_fills::table
.filter(schema::order_fills::pool_id.eq(pool_id))
.order_by(schema::order_fills::checkpoint_timestamp_ms.desc())
.select(schema::order_fills::price)
.first::<i64>(connection)
.await;

if let (Ok(earliest_price), Ok(most_recent_price)) = (earliest_trade_24h, most_recent_trade)
{
let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32);

// Scale the prices
let earliest_price_scaled = earliest_price as f64 / price_factor as f64;
let most_recent_price_scaled = most_recent_price as f64 / price_factor as f64;

// Calculate price change percentage
let price_change_percent =
((most_recent_price_scaled / earliest_price_scaled) - 1.0) * 100.0;

response.insert(pool_name.clone(), price_change_percent);
} else {
// If there's no price data for 24 hours or recent trades, insert 0.0 as price change
response.insert(pool_name.clone(), 0.0);
}
}

Ok(response)
}

async fn trades(
Path(pool_name): Path<String>,
State(state): State<PgDeepbookPersistent>,
Expand Down

0 comments on commit 1559f60

Please sign in to comment.