use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SentimentIndicator {
pub sentiment_score: Decimal,
pub sentiment: Sentiment,
pub buy_pressure: Decimal,
pub sell_pressure: Decimal,
pub order_book_imbalance: Decimal,
pub price_momentum: Decimal,
pub calculated_at: DateTime<Utc>,
}
impl SentimentIndicator {
pub fn calculate(
buy_volume: Decimal,
sell_volume: Decimal,
bid_depth: Decimal,
ask_depth: Decimal,
price_changes: &[Decimal],
) -> Self {
let total_volume = buy_volume + sell_volume;
let buy_pressure = if total_volume.is_zero() {
dec!(50)
} else {
(buy_volume / total_volume) * dec!(100)
};
let sell_pressure = dec!(100) - buy_pressure;
let total_depth = bid_depth + ask_depth;
let order_book_imbalance = if total_depth.is_zero() {
dec!(0)
} else {
(bid_depth - ask_depth) / total_depth
};
let price_momentum = Self::calculate_momentum(price_changes);
let sentiment_score = (buy_pressure - sell_pressure) * dec!(0.4)
+ (order_book_imbalance * dec!(50))
+ (price_momentum * dec!(0.6));
let sentiment = Self::classify_sentiment(sentiment_score);
Self {
sentiment_score,
sentiment,
buy_pressure,
sell_pressure,
order_book_imbalance,
price_momentum,
calculated_at: Utc::now(),
}
}
fn calculate_momentum(price_changes: &[Decimal]) -> Decimal {
if price_changes.is_empty() {
return dec!(0);
}
let mut weighted_sum = dec!(0);
let mut weight_total = dec!(0);
for (i, &change) in price_changes.iter().enumerate() {
let weight = Decimal::from(i + 1); weighted_sum += change * weight;
weight_total += weight;
}
if weight_total.is_zero() {
dec!(0)
} else {
(weighted_sum / weight_total).max(dec!(-100)).min(dec!(100))
}
}
fn classify_sentiment(score: Decimal) -> Sentiment {
if score >= dec!(50) {
Sentiment::StronglyBullish
} else if score >= dec!(20) {
Sentiment::Bullish
} else if score >= dec!(-20) {
Sentiment::Neutral
} else if score >= dec!(-50) {
Sentiment::Bearish
} else {
Sentiment::StronglyBearish
}
}
pub fn is_bullish(&self) -> bool {
matches!(
self.sentiment,
Sentiment::Bullish | Sentiment::StronglyBullish
)
}
pub fn is_bearish(&self) -> bool {
matches!(
self.sentiment,
Sentiment::Bearish | Sentiment::StronglyBearish
)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Sentiment {
StronglyBearish,
Bearish,
Neutral,
Bullish,
StronglyBullish,
}
impl fmt::Display for Sentiment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Sentiment::StronglyBearish => write!(f, "strongly_bearish"),
Sentiment::Bearish => write!(f, "bearish"),
Sentiment::Neutral => write!(f, "neutral"),
Sentiment::Bullish => write!(f, "bullish"),
Sentiment::StronglyBullish => write!(f, "strongly_bullish"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FearGreedIndex {
pub value: Decimal,
pub classification: FearGreedClassification,
pub factors: FearGreedFactors,
pub calculated_at: DateTime<Utc>,
}
impl FearGreedIndex {
pub fn calculate(
price_momentum: Decimal,
volume_momentum: Decimal,
market_volatility: Decimal,
social_sentiment: Decimal,
) -> Self {
let price_component = ((price_momentum + dec!(100)) / dec!(2))
.max(dec!(0))
.min(dec!(100));
let volume_component = ((volume_momentum + dec!(100)) / dec!(2))
.max(dec!(0))
.min(dec!(100));
let volatility_component = (dec!(100) - market_volatility.min(dec!(100))).max(dec!(0));
let social_component = social_sentiment.max(dec!(0)).min(dec!(100));
let value = (price_component * dec!(0.25))
+ (volume_component * dec!(0.25))
+ (volatility_component * dec!(0.25))
+ (social_component * dec!(0.25));
let classification = Self::classify(value);
Self {
value,
classification,
factors: FearGreedFactors {
price_momentum: price_component,
volume_momentum: volume_component,
market_volatility: volatility_component,
social_sentiment: social_component,
},
calculated_at: Utc::now(),
}
}
fn classify(value: Decimal) -> FearGreedClassification {
if value >= dec!(75) {
FearGreedClassification::ExtremeGreed
} else if value >= dec!(55) {
FearGreedClassification::Greed
} else if value >= dec!(45) {
FearGreedClassification::Neutral
} else if value >= dec!(25) {
FearGreedClassification::Fear
} else {
FearGreedClassification::ExtremeFear
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FearGreedClassification {
ExtremeFear,
Fear,
Neutral,
Greed,
ExtremeGreed,
}
impl fmt::Display for FearGreedClassification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FearGreedClassification::ExtremeFear => write!(f, "extreme_fear"),
FearGreedClassification::Fear => write!(f, "fear"),
FearGreedClassification::Neutral => write!(f, "neutral"),
FearGreedClassification::Greed => write!(f, "greed"),
FearGreedClassification::ExtremeGreed => write!(f, "extreme_greed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FearGreedFactors {
pub price_momentum: Decimal,
pub volume_momentum: Decimal,
pub market_volatility: Decimal,
pub social_sentiment: Decimal,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sentiment_bullish() {
let sentiment = SentimentIndicator::calculate(
dec!(8000), dec!(2000), dec!(10000), dec!(3000), &[dec!(2), dec!(3), dec!(5)], );
assert!(sentiment.is_bullish());
assert!(!sentiment.is_bearish());
assert!(sentiment.buy_pressure > dec!(70));
}
#[test]
fn test_sentiment_bearish() {
let sentiment = SentimentIndicator::calculate(
dec!(2000), dec!(8000), dec!(3000), dec!(10000), &[dec!(-2), dec!(-3), dec!(-5)], );
assert!(sentiment.is_bearish());
assert!(!sentiment.is_bullish());
assert!(sentiment.sell_pressure > dec!(70));
}
#[test]
fn test_sentiment_neutral() {
let sentiment = SentimentIndicator::calculate(
dec!(5000),
dec!(5000),
dec!(5000),
dec!(5000),
&[dec!(0), dec!(1), dec!(-1)],
);
assert_eq!(sentiment.sentiment, Sentiment::Neutral);
}
#[test]
fn test_fear_greed_extreme_greed() {
let index = FearGreedIndex::calculate(
dec!(80), dec!(75), dec!(10), dec!(90), );
assert_eq!(index.classification, FearGreedClassification::ExtremeGreed);
assert!(index.value > dec!(70));
}
#[test]
fn test_fear_greed_extreme_fear() {
let index = FearGreedIndex::calculate(
dec!(-80), dec!(-75), dec!(90), dec!(10), );
assert_eq!(index.classification, FearGreedClassification::ExtremeFear);
assert!(index.value < dec!(30));
}
#[test]
fn test_fear_greed_neutral() {
let index = FearGreedIndex::calculate(
dec!(0), dec!(0), dec!(50), dec!(50), );
assert_eq!(index.classification, FearGreedClassification::Neutral);
}
#[test]
fn test_sentiment_momentum_empty() {
let sentiment =
SentimentIndicator::calculate(dec!(5000), dec!(5000), dec!(5000), dec!(5000), &[]);
assert_eq!(sentiment.price_momentum, dec!(0));
}
#[test]
fn test_sentiment_zero_volume() {
let sentiment =
SentimentIndicator::calculate(dec!(0), dec!(0), dec!(5000), dec!(5000), &[dec!(1)]);
assert_eq!(sentiment.buy_pressure, dec!(50));
assert_eq!(sentiment.sell_pressure, dec!(50));
}
}