use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialSentiment {
pub token_symbol: String,
pub score: i32,
pub mention_count: u64,
pub positive_ratio: f64,
pub negative_ratio: f64,
pub neutral_ratio: f64,
pub trending_score: u32,
pub updated_at: DateTime<Utc>,
}
impl SocialSentiment {
pub fn new(token_symbol: String) -> Self {
Self {
token_symbol,
score: 0,
mention_count: 0,
positive_ratio: 0.0,
negative_ratio: 0.0,
neutral_ratio: 0.0,
trending_score: 0,
updated_at: Utc::now(),
}
}
pub fn update(&mut self, positive: u64, negative: u64, neutral: u64) {
let total = positive + negative + neutral;
if total == 0 {
return;
}
self.mention_count += total;
self.positive_ratio = positive as f64 / total as f64;
self.negative_ratio = negative as f64 / total as f64;
self.neutral_ratio = neutral as f64 / total as f64;
self.score = ((self.positive_ratio - self.negative_ratio) * 100.0) as i32;
self.trending_score = ((total as f64 / 1000.0) * 100.0).min(100.0) as u32;
self.updated_at = Utc::now();
}
pub fn is_bullish(&self) -> bool {
self.score > 20
}
pub fn is_bearish(&self) -> bool {
self.score < -20
}
pub fn category(&self) -> SentimentCategory {
match self.score {
s if s > 50 => SentimentCategory::ExtremelyBullish,
s if s > 20 => SentimentCategory::Bullish,
s if s > -20 => SentimentCategory::Neutral,
s if s > -50 => SentimentCategory::Bearish,
_ => SentimentCategory::ExtremelyBearish,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SentimentCategory {
ExtremelyBullish,
Bullish,
Neutral,
Bearish,
ExtremelyBearish,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FearAndGreedIndex {
pub value: u32,
pub components: FearGreedComponents,
pub category: FearGreedCategory,
pub change_24h: i32,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FearGreedComponents {
pub volatility: u32,
pub volume: u32,
pub social: u32,
pub dominance: u32,
pub trends: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FearGreedCategory {
ExtremeFear,
Fear,
Neutral,
Greed,
ExtremeGreed,
}
impl FearAndGreedIndex {
pub fn new() -> Self {
Self {
value: 50,
components: FearGreedComponents {
volatility: 50,
volume: 50,
social: 50,
dominance: 50,
trends: 50,
},
category: FearGreedCategory::Neutral,
change_24h: 0,
updated_at: Utc::now(),
}
}
pub fn calculate(&mut self) {
let weighted = (self.components.volatility * 25
+ self.components.volume * 25
+ self.components.social * 20
+ self.components.dominance * 15
+ self.components.trends * 15)
/ 100;
let old_value = self.value;
self.value = weighted;
self.change_24h = self.value as i32 - old_value as i32;
self.category = match self.value {
0..=24 => FearGreedCategory::ExtremeFear,
25..=49 => FearGreedCategory::Fear,
50 => FearGreedCategory::Neutral,
51..=75 => FearGreedCategory::Greed,
76..=100 => FearGreedCategory::ExtremeGreed,
_ => FearGreedCategory::Neutral,
};
self.updated_at = Utc::now();
}
pub fn update_volatility(&mut self, volatility_pct: Decimal) {
let normalized = (dec!(100) - (volatility_pct / dec!(2)).min(dec!(100)))
.to_u32()
.unwrap_or(50);
self.components.volatility = normalized;
}
pub fn update_volume(&mut self, volume_change_pct: Decimal) {
let normalized = (dec!(50) + volume_change_pct / dec!(2))
.clamp(dec!(0), dec!(100))
.to_u32()
.unwrap_or(50);
self.components.volume = normalized;
}
pub fn update_social(&mut self, social_score: i32) {
self.components.social = ((social_score + 100) / 2).clamp(0, 100) as u32;
}
pub fn is_extreme_fear(&self) -> bool {
matches!(self.category, FearGreedCategory::ExtremeFear)
}
pub fn is_extreme_greed(&self) -> bool {
matches!(self.category, FearGreedCategory::ExtremeGreed)
}
}
impl Default for FearAndGreedIndex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundingRateAnalysis {
pub token_symbol: String,
pub current_rate: Decimal,
pub avg_24h: Decimal,
pub avg_7d: Decimal,
pub history: Vec<(DateTime<Utc>, Decimal)>,
pub trend: FundingTrend,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FundingTrend {
Increasing,
Decreasing,
Stable,
}
impl FundingRateAnalysis {
pub fn new(token_symbol: String) -> Self {
Self {
token_symbol,
current_rate: Decimal::ZERO,
avg_24h: Decimal::ZERO,
avg_7d: Decimal::ZERO,
history: Vec::new(),
trend: FundingTrend::Stable,
updated_at: Utc::now(),
}
}
pub fn add_rate(&mut self, rate: Decimal) {
self.current_rate = rate;
self.history.push((Utc::now(), rate));
let cutoff = Utc::now() - chrono::Duration::days(7);
self.history.retain(|(timestamp, _)| *timestamp > cutoff);
self.calculate_averages();
self.determine_trend();
self.updated_at = Utc::now();
}
fn calculate_averages(&mut self) {
let now = Utc::now();
let day_ago = now - chrono::Duration::hours(24);
let week_ago = now - chrono::Duration::days(7);
let rates_24h: Vec<_> = self
.history
.iter()
.filter(|(timestamp, _)| *timestamp > day_ago)
.map(|(_, rate)| rate)
.collect();
if !rates_24h.is_empty() {
self.avg_24h =
rates_24h.iter().copied().sum::<Decimal>() / Decimal::from(rates_24h.len());
}
let rates_7d: Vec<_> = self
.history
.iter()
.filter(|(timestamp, _)| *timestamp > week_ago)
.map(|(_, rate)| rate)
.collect();
if !rates_7d.is_empty() {
self.avg_7d = rates_7d.iter().copied().sum::<Decimal>() / Decimal::from(rates_7d.len());
}
}
fn determine_trend(&mut self) {
if self.history.len() < 10 {
self.trend = FundingTrend::Stable;
return;
}
let recent_avg: Decimal = self
.history
.iter()
.rev()
.take(5)
.map(|(_, rate)| rate)
.sum::<Decimal>()
/ dec!(5);
let older_avg: Decimal = self
.history
.iter()
.rev()
.skip(5)
.take(5)
.map(|(_, rate)| rate)
.sum::<Decimal>()
/ dec!(5);
let diff = recent_avg - older_avg;
self.trend = if diff > dec!(0.0001) {
FundingTrend::Increasing
} else if diff < dec!(-0.0001) {
FundingTrend::Decreasing
} else {
FundingTrend::Stable
};
}
pub fn is_extremely_positive(&self) -> bool {
self.current_rate > dec!(0.001) }
pub fn is_extremely_negative(&self) -> bool {
self.current_rate < dec!(-0.001) }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LongShortRatio {
pub token_symbol: String,
pub ratio: Decimal,
pub long_percentage: Decimal,
pub short_percentage: Decimal,
pub history: Vec<(DateTime<Utc>, Decimal)>,
pub sentiment: RatioSentiment,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RatioSentiment {
ExtremelyBullish,
Bullish,
Neutral,
Bearish,
ExtremelyBearish,
}
impl LongShortRatio {
pub fn new(token_symbol: String) -> Self {
Self {
token_symbol,
ratio: dec!(1.0),
long_percentage: dec!(50),
short_percentage: dec!(50),
history: Vec::new(),
sentiment: RatioSentiment::Neutral,
updated_at: Utc::now(),
}
}
pub fn update(&mut self, long_count: u64, short_count: u64) {
if short_count == 0 {
self.ratio = dec!(10); } else {
self.ratio = (Decimal::from(long_count) / Decimal::from(short_count)).min(dec!(10));
}
let total = long_count + short_count;
if total > 0 {
self.long_percentage = Decimal::from(long_count) / Decimal::from(total) * dec!(100);
self.short_percentage = Decimal::from(short_count) / Decimal::from(total) * dec!(100);
}
self.history.push((Utc::now(), self.ratio));
let cutoff = Utc::now() - chrono::Duration::days(7);
self.history.retain(|(timestamp, _)| *timestamp > cutoff);
self.sentiment = if self.ratio > dec!(2.0) {
RatioSentiment::ExtremelyBullish
} else if self.ratio > dec!(1.3) {
RatioSentiment::Bullish
} else if self.ratio > dec!(0.7) {
RatioSentiment::Neutral
} else if self.ratio > dec!(0.5) {
RatioSentiment::Bearish
} else {
RatioSentiment::ExtremelyBearish
};
self.updated_at = Utc::now();
}
pub fn longs_dominant(&self) -> bool {
self.ratio > dec!(1.3)
}
pub fn shorts_dominant(&self) -> bool {
self.ratio < dec!(0.7)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketSentimentAggregator {
pub social_sentiments: HashMap<String, SocialSentiment>,
pub fear_greed: FearAndGreedIndex,
pub funding_rates: HashMap<String, FundingRateAnalysis>,
pub long_short_ratios: HashMap<String, LongShortRatio>,
}
impl MarketSentimentAggregator {
pub fn new() -> Self {
Self {
social_sentiments: HashMap::new(),
fear_greed: FearAndGreedIndex::new(),
funding_rates: HashMap::new(),
long_short_ratios: HashMap::new(),
}
}
pub fn get_overall_sentiment(&self, token: &str) -> OverallSentiment {
let mut bullish_signals = 0;
let mut bearish_signals = 0;
if let Some(social) = self.social_sentiments.get(token) {
if social.is_bullish() {
bullish_signals += 1;
} else if social.is_bearish() {
bearish_signals += 1;
}
}
if let Some(funding) = self.funding_rates.get(token) {
if funding.is_extremely_positive() {
bullish_signals += 1;
} else if funding.is_extremely_negative() {
bearish_signals += 1;
}
}
if let Some(ratio) = self.long_short_ratios.get(token) {
if ratio.longs_dominant() {
bullish_signals += 1;
} else if ratio.shorts_dominant() {
bearish_signals += 1;
}
}
OverallSentiment {
token: token.to_string(),
bullish_signals,
bearish_signals,
category: if bullish_signals > bearish_signals * 2 {
SentimentCategory::ExtremelyBullish
} else if bullish_signals > bearish_signals {
SentimentCategory::Bullish
} else if bearish_signals > bullish_signals * 2 {
SentimentCategory::ExtremelyBearish
} else if bearish_signals > bullish_signals {
SentimentCategory::Bearish
} else {
SentimentCategory::Neutral
},
}
}
}
impl Default for MarketSentimentAggregator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OverallSentiment {
pub token: String,
pub bullish_signals: u32,
pub bearish_signals: u32,
pub category: SentimentCategory,
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_social_sentiment() {
let mut sentiment = SocialSentiment::new("BTC".to_string());
sentiment.update(70, 20, 10);
assert!(sentiment.is_bullish());
assert_eq!(sentiment.category(), SentimentCategory::Bullish);
}
#[test]
fn test_fear_and_greed() {
let mut index = FearAndGreedIndex::new();
index.components.volatility = 20; index.components.volume = 30;
index.components.social = 25;
index.components.dominance = 20;
index.components.trends = 25;
index.calculate();
assert!(index.value < 50);
assert!(matches!(
index.category,
FearGreedCategory::Fear | FearGreedCategory::ExtremeFear
));
}
#[test]
fn test_funding_rate() {
let mut funding = FundingRateAnalysis::new("BTC".to_string());
for i in 1..=15 {
funding.add_rate(dec!(0.0005) + Decimal::from(i) * dec!(0.0001));
}
assert!(funding.is_extremely_positive());
assert_eq!(funding.trend, FundingTrend::Increasing);
}
#[test]
fn test_long_short_ratio() {
let mut ratio = LongShortRatio::new("BTC".to_string());
ratio.update(700, 300);
assert!(ratio.longs_dominant());
assert!(matches!(
ratio.sentiment,
RatioSentiment::Bullish | RatioSentiment::ExtremelyBullish
));
}
#[test]
fn test_market_sentiment_aggregator() {
let mut aggregator = MarketSentimentAggregator::new();
let mut social = SocialSentiment::new("BTC".to_string());
social.update(80, 20, 0);
aggregator
.social_sentiments
.insert("BTC".to_string(), social);
let mut ratio = LongShortRatio::new("BTC".to_string());
ratio.update(700, 300);
aggregator
.long_short_ratios
.insert("BTC".to_string(), ratio);
let sentiment = aggregator.get_overall_sentiment("BTC");
assert!(sentiment.bullish_signals > 0);
}
}