use serde::{Deserialize, Serialize};
pub const EIGHT_HOURS_MS: i64 = 8 * 3_600_000;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub enum FundingModel {
#[default]
None,
Constant {
rate: f64,
interval_ms: i64,
},
Series(Vec<(i64, f64)>),
}
impl FundingModel {
pub fn settlements_between(&self, after: i64, until: i64) -> Vec<(i64, f64)> {
if until <= after {
return Vec::new();
}
match self {
Self::None => Vec::new(),
Self::Constant { rate, interval_ms } => {
let interval = *interval_ms;
if interval <= 0 {
return Vec::new();
}
let mut t = (after.div_euclid(interval) + 1) * interval;
let mut out = Vec::new();
while t <= until {
out.push((t, *rate));
t += interval;
}
out
}
Self::Series(points) => {
debug_assert!(
points.windows(2).all(|w| w[0].0 <= w[1].0),
"settlements_between requires an ascending series; call FundingModel::validated first"
);
let start = points.partition_point(|(t, _)| *t <= after);
let end = points.partition_point(|(t, _)| *t <= until);
points[start..end].to_vec()
}
}
}
pub(crate) fn validated(mut self) -> std::result::Result<Self, String> {
match &mut self {
Self::None => {}
Self::Constant { rate, interval_ms } => {
if !rate.is_finite() {
return Err(format!("funding rate must be finite, got {rate}"));
}
if *interval_ms <= 0 {
return Err(format!(
"funding interval_ms must be > 0, got {interval_ms}"
));
}
}
Self::Series(points) => {
for (t, rate) in points.iter() {
if !rate.is_finite() {
return Err(format!("funding rate at t={t} must be finite, got {rate}"));
}
}
points.sort_by_key(|(t, _)| *t);
if points.windows(2).any(|w| w[0].0 == w[1].0) {
return Err(
"funding series has duplicate settlement timestamps (would double-book)"
.into(),
);
}
}
}
Ok(self)
}
pub(crate) fn is_enabled(&self) -> bool {
!matches!(self, Self::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_yields_no_settlements() {
assert!(
FundingModel::None
.settlements_between(0, i64::MAX)
.is_empty()
);
}
#[test]
fn constant_grid_is_entry_exclusive_exit_inclusive() {
let m = FundingModel::Constant {
rate: 0.0001,
interval_ms: 100,
};
assert_eq!(m.settlements_between(100, 250), vec![(200, 0.0001)]);
assert_eq!(
m.settlements_between(50, 200),
vec![(100, 0.0001), (200, 0.0001)]
);
assert!(m.settlements_between(100, 100).is_empty());
assert!(m.settlements_between(200, 100).is_empty());
}
#[test]
fn constant_grid_stays_epoch_aligned_across_zero() {
let m = FundingModel::Constant {
rate: 0.5,
interval_ms: 100,
};
assert_eq!(
m.settlements_between(-150, 100),
vec![(-100, 0.5), (0, 0.5), (100, 0.5)]
);
}
#[test]
fn series_window_matches_constant_window_semantics() {
let m = FundingModel::Series(vec![(100, 0.1), (200, 0.2), (300, 0.3)]);
assert_eq!(
m.settlements_between(100, 300),
vec![(200, 0.2), (300, 0.3)]
);
assert!(m.settlements_between(300, 1_000).is_empty());
}
#[test]
fn validated_sorts_series_and_rejects_duplicates() {
let m = FundingModel::Series(vec![(300, 0.3), (100, 0.1)])
.validated()
.unwrap();
assert_eq!(m, FundingModel::Series(vec![(100, 0.1), (300, 0.3)]));
assert!(
FundingModel::Series(vec![(100, 0.1), (100, 0.2)])
.validated()
.is_err()
);
}
#[test]
fn validated_rejects_bad_constant() {
assert!(
FundingModel::Constant {
rate: f64::NAN,
interval_ms: 100
}
.validated()
.is_err()
);
assert!(
FundingModel::Constant {
rate: 0.0001,
interval_ms: 0
}
.validated()
.is_err()
);
}
#[test]
fn validated_rejects_non_finite_series_rate() {
assert!(
FundingModel::Series(vec![(100, f64::INFINITY)])
.validated()
.is_err()
);
}
}