use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct FundingRate {
pub rate: f64,
pub timestamp: u64,
pub interval_hours: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FundingPayment {
pub position_size: f64,
pub funding_rate: f64,
pub payment: f64,
pub is_long: bool,
}
pub struct FundingRateCalculator;
impl FundingRateCalculator {
pub fn premium_index(mark_price: f64, spot_price: f64) -> f64 {
if spot_price == 0.0 {
return 0.0;
}
(mark_price - spot_price) / spot_price
}
pub fn compute_funding_rate(premium: f64, interest_rate: f64, clamp: f64) -> f64 {
let raw = premium + interest_rate;
raw.clamp(-clamp, clamp)
}
pub fn compute_payment(
position_size: f64,
funding_rate: f64,
is_long: bool,
) -> FundingPayment {
let raw_payment = position_size * funding_rate;
let payment = if is_long { raw_payment } else { -raw_payment };
FundingPayment {
position_size,
funding_rate,
payment,
is_long,
}
}
pub fn annualize_rate(funding_rate: f64, intervals_per_day: f64) -> f64 {
funding_rate * intervals_per_day * 365.0
}
pub fn predicted_next_rate(rates: &[FundingRate]) -> f64 {
if rates.is_empty() {
return 0.0;
}
let len = rates.len();
let window: Vec<f64> = rates[len.saturating_sub(3)..]
.iter()
.map(|r| r.rate)
.collect();
match window.len() {
1 => window[0],
2 => 0.6 * window[1] + 0.4 * window[0],
_ => 0.5 * window[2] + 0.3 * window[1] + 0.2 * window[0],
}
}
}
pub struct FundingHistory {
records: VecDeque<FundingRate>,
capacity: usize,
}
impl FundingHistory {
pub fn new(capacity: usize) -> Self {
Self {
records: VecDeque::new(),
capacity: capacity.max(1),
}
}
pub fn add(&mut self, rate: FundingRate) {
if self.records.len() >= self.capacity {
self.records.pop_front();
}
self.records.push_back(rate);
}
pub fn avg_rate(&self, window: usize) -> Option<f64> {
let len = self.records.len();
if len == 0 {
return None;
}
let take = window.min(len);
let sum: f64 = self.records.iter().rev().take(take).map(|r| r.rate).sum();
Some(sum / take as f64)
}
pub fn cumulative_payment(
&self,
position_size: f64,
is_long: bool,
window: usize,
) -> f64 {
let len = self.records.len();
let take = window.min(len);
self.records
.iter()
.rev()
.take(take)
.map(|r| FundingRateCalculator::compute_payment(position_size, r.rate, is_long).payment)
.sum()
}
pub fn rate_volatility(&self, window: usize) -> Option<f64> {
let len = self.records.len();
if len < 2 {
return None;
}
let take = window.min(len);
if take < 2 {
return None;
}
let rates: Vec<f64> = self
.records
.iter()
.rev()
.take(take)
.map(|r| r.rate)
.collect();
let mean = rates.iter().sum::<f64>() / rates.len() as f64;
let variance =
rates.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (rates.len() - 1) as f64;
Some(variance.sqrt())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rate(r: f64) -> FundingRate {
FundingRate { rate: r, timestamp: 0, interval_hours: 8 }
}
#[test]
fn premium_index_normal() {
let pi = FundingRateCalculator::premium_index(101.0, 100.0);
assert!((pi - 0.01).abs() < 1e-12);
}
#[test]
fn premium_index_zero_spot() {
assert_eq!(FundingRateCalculator::premium_index(100.0, 0.0), 0.0);
}
#[test]
fn compute_funding_rate_clamped_high() {
let clamp = 0.0005;
let result = FundingRateCalculator::compute_funding_rate(0.01, 0.0001, clamp);
assert!((result - clamp).abs() < 1e-12);
}
#[test]
fn compute_funding_rate_clamped_low() {
let clamp = 0.0005;
let result = FundingRateCalculator::compute_funding_rate(-0.01, 0.0001, clamp);
assert!((result + clamp).abs() < 1e-12);
}
#[test]
fn compute_funding_rate_within_clamp() {
let result =
FundingRateCalculator::compute_funding_rate(0.0002, 0.0001, 0.0005);
assert!((result - 0.0003).abs() < 1e-12);
}
#[test]
fn compute_payment_long_positive_rate() {
let p = FundingRateCalculator::compute_payment(10_000.0, 0.001, true);
assert!((p.payment - 10.0).abs() < 1e-9);
assert!(p.is_long);
}
#[test]
fn compute_payment_short_positive_rate() {
let p = FundingRateCalculator::compute_payment(10_000.0, 0.001, false);
assert!((p.payment + 10.0).abs() < 1e-9);
assert!(!p.is_long);
}
#[test]
fn annualize_rate_8h_intervals() {
let ann = FundingRateCalculator::annualize_rate(0.0001, 3.0);
assert!((ann - 0.1095).abs() < 1e-9);
}
#[test]
fn predicted_next_rate_empty() {
assert_eq!(FundingRateCalculator::predicted_next_rate(&[]), 0.0);
}
#[test]
fn predicted_next_rate_one() {
let rates = vec![rate(0.001)];
assert!((FundingRateCalculator::predicted_next_rate(&rates) - 0.001).abs() < 1e-12);
}
#[test]
fn predicted_next_rate_three() {
let rates = vec![rate(0.002), rate(0.001), rate(0.003)];
let pred = FundingRateCalculator::predicted_next_rate(&rates);
assert!((pred - 0.0022).abs() < 1e-12);
}
#[test]
fn history_avg_rate_empty() {
let h = FundingHistory::new(10);
assert!(h.avg_rate(5).is_none());
}
#[test]
fn history_avg_rate() {
let mut h = FundingHistory::new(10);
h.add(rate(0.001));
h.add(rate(0.003));
h.add(rate(0.002));
let avg = h.avg_rate(3).unwrap();
assert!((avg - 0.002).abs() < 1e-12);
}
#[test]
fn history_capacity_eviction() {
let mut h = FundingHistory::new(3);
for i in 0..5u64 {
h.add(FundingRate { rate: i as f64, timestamp: i, interval_hours: 8 });
}
assert_eq!(h.records.len(), 3);
let avg = h.avg_rate(3).unwrap();
assert!((avg - 3.0).abs() < 1e-12);
}
#[test]
fn history_cumulative_payment() {
let mut h = FundingHistory::new(10);
h.add(rate(0.001));
h.add(rate(0.002));
let total = h.cumulative_payment(10_000.0, true, 10);
assert!((total - 30.0).abs() < 1e-9);
}
#[test]
fn history_rate_volatility_insufficient() {
let mut h = FundingHistory::new(10);
h.add(rate(0.001));
assert!(h.rate_volatility(5).is_none());
}
#[test]
fn history_rate_volatility_computed() {
let mut h = FundingHistory::new(10);
h.add(rate(0.001));
h.add(rate(0.003));
let vol = h.rate_volatility(5).unwrap();
let expected = (2.0_f64 * 1e-6_f64).sqrt();
assert!((vol - expected).abs() < 1e-12);
}
}