use crate::client::{ApiError, Transport, info, info_request};
use crate::utils::dec::MAX_SCALE;
use crate::utils::{Address, Dec, Json, Round};
use serde::Deserialize;
pub const FUNDING_INTERVAL_MS: u64 = 3_600_000;
pub const FUNDING_WINDOW_MS: u64 = 480 * FUNDING_INTERVAL_MS;
pub const FUNDING_PAGE: usize = 500;
pub const HL_PERP: &str = "HlPerp";
#[must_use]
pub const fn next_funding(now_ms: u64) -> u64 {
(now_ms / FUNDING_INTERVAL_MS).saturating_add(1).saturating_mul(FUNDING_INTERVAL_MS)
}
#[must_use]
pub fn funding_history_request(coin: &str, start_ms: u64, end_ms: Option<u64>) -> String {
let fields = window(("coin", Json::String(coin.to_owned())), start_ms, end_ms);
info_request("fundingHistory", &fields)
}
fn window(first: (&'static str, Json), start_ms: u64, end_ms: Option<u64>) -> Vec<(&'static str, Json)> {
let time = |ms: u64| Json::Number(ms.to_string());
let mut fields = vec![first, ("startTime", time(start_ms))];
fields.extend(end_ms.map(|end| ("endTime", time(end))));
fields
}
pub async fn funding_history(
transport: &impl Transport,
coin: &str,
start_ms: u64,
end_ms: Option<u64>,
) -> Result<Vec<FundingRate>, ApiError> {
info(transport, funding_history_request(coin, start_ms, end_ms)).await
}
pub async fn funding_history_range(
transport: &impl Transport,
coin: &str,
start_ms: u64,
end_ms: u64,
) -> Result<Vec<FundingRate>, ApiError> {
let mut read = Vec::new();
for (from, to) in funding_windows(start_ms, end_ms) {
read.push(funding_history(transport, coin, from, Some(to)).await?);
}
Ok(merge_rates(start_ms, end_ms, read))
}
#[must_use]
pub fn funding_windows(start_ms: u64, end_ms: u64) -> Vec<(u64, u64)> {
let mut windows = Vec::new();
let mut from = start_ms;
while from <= end_ms {
let to = from.saturating_add(FUNDING_WINDOW_MS - 1).min(end_ms);
windows.push((from, to));
match to.checked_add(1) {
Some(next) => from = next,
None => break,
}
}
windows
}
#[must_use]
pub fn merge_rates(start_ms: u64, end_ms: u64, windows: Vec<Vec<FundingRate>>) -> Vec<FundingRate> {
let mut rates: Vec<FundingRate> =
windows.into_iter().flatten().filter(|rate| (start_ms..=end_ms).contains(&rate.time)).collect();
rates.sort_by_key(|rate| rate.time);
rates.dedup_by_key(|rate| rate.time);
rates
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingRate {
pub coin: String,
pub funding_rate: Dec,
pub premium: Dec,
pub time: u64,
}
#[must_use]
pub fn user_funding_request(user: Address, start_ms: u64, end_ms: Option<u64>) -> String {
let fields = window(("user", Json::String(user.to_hex())), start_ms, end_ms);
info_request("userFunding", &fields)
}
pub async fn user_funding(
transport: &impl Transport,
user: Address,
start_ms: u64,
end_ms: Option<u64>,
) -> Result<Vec<UserFunding>, ApiError> {
info(transport, user_funding_request(user, start_ms, end_ms)).await
}
pub async fn user_funding_range(
transport: &impl Transport,
user: Address,
start_ms: u64,
end_ms: u64,
pages: usize,
) -> Result<Vec<UserFunding>, ApiError> {
let mut rows: Vec<UserFunding> = Vec::new();
let mut from = start_ms;
for _ in 0..pages {
let page = user_funding(transport, user, from, Some(end_ms)).await?;
let next = next_page(from, &page);
for row in page {
if !rows.iter().any(|kept| kept.time == row.time && kept.delta.coin == row.delta.coin) {
rows.push(row);
}
}
match next {
Some(start) if start <= end_ms => from = start,
_ => break,
}
}
rows.sort_by_key(|row| row.time);
Ok(rows)
}
#[must_use]
pub fn next_page(start_ms: u64, page: &[UserFunding]) -> Option<u64> {
if page.len() < FUNDING_PAGE {
return None;
}
let last = page.iter().map(|row| row.time).max()?;
match last > start_ms {
true => Some(last),
false => start_ms.checked_add(1),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct UserFunding {
pub time: u64,
pub hash: String,
pub delta: FundingDelta,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingDelta {
#[serde(rename = "type")]
pub kind: String,
pub coin: String,
pub usdc: Dec,
pub szi: Dec,
pub funding_rate: Dec,
#[serde(default)]
pub n_samples: Option<u32>,
}
#[must_use]
pub fn predicted_fundings_request() -> String {
info_request("predictedFundings", &[])
}
pub async fn predicted_fundings(transport: &impl Transport) -> Result<PredictedFundings, ApiError> {
info(transport, predicted_fundings_request()).await
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(transparent)]
pub struct PredictedFundings(pub Vec<(String, Vec<VenueFunding>)>);
pub type VenueFunding = (String, Option<PredictedFunding>);
impl PredictedFundings {
pub fn coins(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(|(coin, _)| coin.as_str())
}
#[must_use]
pub fn venues(&self, coin: &str) -> Option<&[VenueFunding]> {
self.0.iter().find(|(listed, _)| listed == coin).map(|(_, venues)| venues.as_slice())
}
#[must_use]
pub fn get(&self, coin: &str, venue: &str) -> Option<&PredictedFunding> {
self.venues(coin)?.iter().find(|(listed, _)| listed == venue)?.1.as_ref()
}
#[must_use]
pub fn hyperliquid(&self, coin: &str) -> Option<&PredictedFunding> {
self.get(coin, HL_PERP)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PredictedFunding {
pub funding_rate: Dec,
pub next_funding_time: u64,
#[serde(default)]
pub funding_interval_hours: Option<u32>,
}
impl PredictedFunding {
#[must_use]
pub fn hourly(&self) -> Option<Dec> {
let hours = self.funding_interval_hours.filter(|hours| *hours > 0)?;
self.funding_rate.checked_div(Dec::from_int(i64::from(hours)), MAX_SCALE, Round::Nearest)
}
}