use serde::Deserialize;
use crate::client::http::{ApiError, Call, Transport, read};
use crate::markets::listed;
use crate::markets::stats::stat_value;
use crate::utils::MarketId;
use crate::utils::dec::{Dec, MAX_SCALE, Round};
use crate::utils::figure::Figure;
use crate::utils::ids::whole;
use crate::utils::time::{HOUR_MS, ms_to_seconds, seconds_to_ms};
pub const FUNDING_INTERVAL_MS: u64 = HOUR_MS;
pub const FUNDINGS_PER_CALL: u64 = 750;
#[must_use]
pub const fn next_funding(now_ms: u64) -> u64 {
(now_ms / FUNDING_INTERVAL_MS).saturating_add(1).saturating_mul(FUNDING_INTERVAL_MS)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FundingResolution {
#[default]
Hour,
Day,
}
impl FundingResolution {
#[must_use]
pub const fn wire(self) -> &'static str {
match self {
FundingResolution::Hour => "1h",
FundingResolution::Day => "1d",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Payer {
Long,
Short,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Funding {
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub value: Dec,
pub rate: Dec,
#[serde(rename = "direction")]
pub payer: Payer,
}
impl Funding {
#[must_use]
pub const fn at_ms(&self) -> u64 {
seconds_to_ms(self.timestamp)
}
#[must_use]
pub fn fraction(&self) -> Option<Dec> {
let fraction = self.rate.checked_div(Dec::from_int(100), MAX_SCALE, Round::Nearest)?;
match self.payer {
Payer::Long => Some(fraction),
Payer::Short => Some(fraction.negate()),
Payer::Other => None,
}
}
}
#[derive(Deserialize)]
struct Fundings {
#[serde(default, deserialize_with = "listed")]
fundings: Vec<Funding>,
}
#[must_use]
pub fn fundings_call(
market: MarketId,
resolution: FundingResolution,
start_s: u64,
end_s: u64,
count_back: u64,
) -> Call {
Call::get("fundings")
.field("market_id", market)
.field("resolution", resolution.wire())
.field("start_timestamp", start_s)
.field("end_timestamp", end_s)
.field("count_back", count_back.min(FUNDINGS_PER_CALL))
}
pub async fn fundings(
transport: &impl Transport,
market: MarketId,
resolution: FundingResolution,
start_s: u64,
end_s: u64,
count_back: u64,
) -> Result<Vec<Funding>, ApiError> {
Ok(read::<Fundings>(transport, fundings_call(market, resolution, start_s, end_s, count_back)).await?.fundings)
}
#[must_use]
pub fn funding_count_back(start_ms: u64, end_ms: u64) -> u64 {
(end_ms.saturating_sub(start_ms) / FUNDING_INTERVAL_MS).saturating_add(2).clamp(1, FUNDINGS_PER_CALL)
}
pub async fn funding_history(
transport: &impl Transport,
market: MarketId,
start_ms: u64,
end_ms: u64,
) -> Result<Vec<Funding>, ApiError> {
let (start_s, end_s) = (ms_to_seconds(start_ms), ms_to_seconds(end_ms));
let count = funding_count_back(start_ms, end_ms);
let mut rows = fundings(transport, market, FundingResolution::Hour, start_s, end_s, count).await?;
rows.retain(|row| (start_ms..=end_ms).contains(&row.at_ms()));
rows.sort_by_key(|row| row.timestamp);
Ok(rows)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Exchange {
Lighter,
Binance,
Bybit,
Hyperliquid,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct FundingRate {
pub market_id: MarketId,
pub exchange: Exchange,
pub symbol: String,
pub rate: Figure,
}
impl FundingRate {
#[must_use]
pub fn eight_hour(&self) -> Option<Dec> {
stat_value(&self.rate)
}
#[must_use]
pub fn hourly(&self) -> Option<Dec> {
self.eight_hour()?.checked_div(Dec::from_int(8), MAX_SCALE, Round::Nearest)
}
}
#[derive(Deserialize)]
struct FundingRates {
#[serde(default, deserialize_with = "listed")]
funding_rates: Vec<FundingRate>,
}
#[must_use]
pub fn funding_rates_call() -> Call {
Call::get("funding-rates")
}
pub async fn funding_rates(transport: &impl Transport) -> Result<Vec<FundingRate>, ApiError> {
Ok(read::<FundingRates>(transport, funding_rates_call()).await?.funding_rates)
}
#[cfg(test)]
#[path = "tests/funding.rs"]
mod tests;