use crate::errors::Result;
use crate::data::HyperliquidData;
use crate::backtest::FundingPayment;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, FixedOffset};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingRatePoint {
pub timestamp: DateTime<FixedOffset>,
pub rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingDistribution {
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
pub skewness: f64,
pub kurtosis: f64,
pub percentiles: [f64; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingDirectionStats {
pub positive_count: usize,
pub negative_count: usize,
pub zero_count: usize,
pub positive_percentage: f64,
pub negative_percentage: f64,
pub avg_positive_rate: f64,
pub avg_negative_rate: f64,
pub longest_positive_streak: usize,
pub longest_negative_streak: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingPeriodMetric {
pub period_name: String,
pub avg_rate: f64,
pub total_pnl: f64,
pub volatility: f64,
pub sharpe: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingMetricsByPeriod {
pub daily: Vec<FundingPeriodMetric>,
pub weekly: Vec<FundingPeriodMetric>,
pub monthly: Vec<FundingPeriodMetric>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingReport {
pub symbol: String,
pub net_funding_pnl: f64,
pub total_funding_received: f64,
pub total_funding_paid: f64,
pub payment_count: usize,
pub average_rate: f64,
pub rate_volatility: f64,
pub distribution: FundingDistribution,
pub direction_stats: FundingDirectionStats,
pub payments: Vec<FundingPayment>,
pub rates: Vec<FundingRatePoint>,
pub metrics_by_period: FundingMetricsByPeriod,
}
impl FundingReport {
pub fn new(
symbol: &str,
data: &HyperliquidData,
position_sizes: &[f64],
payments: Vec<FundingPayment>,
net_funding_pnl: f64,
) -> Result<Self> {
let mut rates = Vec::new();
for (i, ×tamp) in data.datetime.iter().enumerate() {
let rate = data.funding_rates[i];
if !rate.is_nan() {
rates.push(FundingRatePoint {
timestamp,
rate,
});
}
}
let rate_values: Vec<f64> = rates.iter().map(|r| r.rate).collect();
let distribution = Self::calculate_funding_distribution(&rate_values)?;
let direction_stats = Self::calculate_direction_stats(&rates);
let mut total_received = 0.0;
let mut total_paid = 0.0;
for payment in &payments {
if payment.payment_amount > 0.0 {
total_received += payment.payment_amount;
} else {
total_paid += payment.payment_amount.abs();
}
}
let average_rate = if !rates.is_empty() {
rate_values.iter().sum::<f64>() / rate_values.len() as f64
} else {
0.0
};
let rate_volatility = if rate_values.len() > 1 {
let mean = average_rate;
let variance = rate_values.iter()
.map(|&r| (r - mean).powi(2))
.sum::<f64>() / (rate_values.len() - 1) as f64;
variance.sqrt()
} else {
0.0
};
let metrics_by_period = FundingMetricsByPeriod {
daily: vec![FundingPeriodMetric {
period_name: "Daily".to_string(),
avg_rate: average_rate,
total_pnl: net_funding_pnl / 30.0, volatility: rate_volatility,
sharpe: if rate_volatility > 0.0 { average_rate / rate_volatility } else { 0.0 },
}],
weekly: vec![FundingPeriodMetric {
period_name: "Weekly".to_string(),
avg_rate: average_rate,
total_pnl: net_funding_pnl / 4.0, volatility: rate_volatility,
sharpe: if rate_volatility > 0.0 { average_rate / rate_volatility } else { 0.0 },
}],
monthly: vec![FundingPeriodMetric {
period_name: "Monthly".to_string(),
avg_rate: average_rate,
total_pnl: net_funding_pnl,
volatility: rate_volatility,
sharpe: if rate_volatility > 0.0 { average_rate / rate_volatility } else { 0.0 },
}],
};
Ok(Self {
symbol: symbol.to_string(),
net_funding_pnl,
total_funding_received: total_received,
total_funding_paid: total_paid,
payment_count: payments.len(),
average_rate,
rate_volatility,
distribution,
direction_stats,
payments,
rates,
metrics_by_period,
})
}
fn calculate_funding_distribution(rates: &[f64]) -> Result<FundingDistribution> {
if rates.is_empty() {
return Ok(FundingDistribution {
mean: 0.0,
median: 0.0,
std_dev: 0.0,
min: 0.0,
max: 0.0,
skewness: 0.0,
kurtosis: 0.0,
percentiles: [0.0, 0.0, 0.0, 0.0],
});
}
let n = rates.len();
let mean = rates.iter().sum::<f64>() / n as f64;
let mut sorted_rates = rates.to_vec();
sorted_rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = if n % 2 == 0 {
(sorted_rates[n/2 - 1] + sorted_rates[n/2]) / 2.0
} else {
sorted_rates[n/2]
};
let min = sorted_rates[0];
let max = sorted_rates[n - 1];
let variance = rates.iter()
.map(|&r| (r - mean).powi(2))
.sum::<f64>() / (n - 1) as f64;
let std_dev = variance.sqrt();
let m3 = rates.iter()
.map(|&r| (r - mean).powi(3))
.sum::<f64>() / n as f64;
let m4 = rates.iter()
.map(|&r| (r - mean).powi(4))
.sum::<f64>() / n as f64;
let skewness = if std_dev > 0.0 { m3 / std_dev.powi(3) } else { 0.0 };
let kurtosis = if std_dev > 0.0 { m4 / std_dev.powi(4) - 3.0 } else { 0.0 };
let p10_idx = (n as f64 * 0.1).round() as usize;
let p25_idx = (n as f64 * 0.25).round() as usize;
let p75_idx = (n as f64 * 0.75).round() as usize;
let p90_idx = (n as f64 * 0.9).round() as usize;
let percentiles = [
sorted_rates[p10_idx.min(n - 1)],
sorted_rates[p25_idx.min(n - 1)],
sorted_rates[p75_idx.min(n - 1)],
sorted_rates[p90_idx.min(n - 1)],
];
Ok(FundingDistribution {
mean,
median,
std_dev,
min,
max,
skewness,
kurtosis,
percentiles,
})
}
fn calculate_direction_stats(rates: &[FundingRatePoint]) -> FundingDirectionStats {
let mut positive_count = 0;
let mut negative_count = 0;
let mut zero_count = 0;
let mut positive_sum = 0.0;
let mut negative_sum = 0.0;
let mut current_positive_streak = 0;
let mut current_negative_streak = 0;
let mut longest_positive_streak = 0;
let mut longest_negative_streak = 0;
for rate_point in rates {
let rate = rate_point.rate;
if rate > 0.0 {
positive_count += 1;
positive_sum += rate;
current_positive_streak += 1;
current_negative_streak = 0;
longest_positive_streak = longest_positive_streak.max(current_positive_streak);
} else if rate < 0.0 {
negative_count += 1;
negative_sum += rate;
current_negative_streak += 1;
current_positive_streak = 0;
longest_negative_streak = longest_negative_streak.max(current_negative_streak);
} else {
zero_count += 1;
current_positive_streak = 0;
current_negative_streak = 0;
}
}
let total_count = positive_count + negative_count + zero_count;
let positive_percentage = if total_count > 0 {
positive_count as f64 / total_count as f64
} else {
0.0
};
let negative_percentage = if total_count > 0 {
negative_count as f64 / total_count as f64
} else {
0.0
};
let avg_positive_rate = if positive_count > 0 {
positive_sum / positive_count as f64
} else {
0.0
};
let avg_negative_rate = if negative_count > 0 {
negative_sum / negative_count as f64
} else {
0.0
};
FundingDirectionStats {
positive_count,
negative_count,
zero_count,
positive_percentage,
negative_percentage,
avg_positive_rate,
avg_negative_rate,
longest_positive_streak,
longest_negative_streak,
}
}
}