#[cfg(feature = "serde")]
use serde::Serialize;
use super::{
FearGaugeReading, FearGaugeState, PriceSummary, RegimeReading, RegimeState,
TrendPersistenceReading,
};
use crate::Bar;
const FLOW_WINDOW: usize = 34;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum FearGreedState {
ExtremeFear,
Fear,
Neutral,
Greed,
ExtremeGreed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum FearGreedDriver {
FearGauge,
Volatility,
Flow,
Persistence,
Regime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct FearGreedReading {
pub score: f64,
pub state: FearGreedState,
pub fear_gauge_score: f64,
pub volatility_score: f64,
pub flow_score: f64,
pub persistence_score: f64,
pub regime_score: f64,
pub driver: FearGreedDriver,
pub drag: FearGreedDriver,
}
pub fn fear_greed_reading(
bars: &[Bar],
regime: Option<&RegimeReading>,
price: Option<&PriceSummary>,
fear_gauge: Option<&FearGaugeReading>,
trend_persistence: Option<&TrendPersistenceReading>,
) -> Option<FearGreedReading> {
if bars.len() < 2 {
return None;
}
let fear_gauge_score = fear_gauge.map_or(50.0, fear_gauge_component);
let volatility_score = price.map_or(50.0, |p| 100.0 * (1.0 - p.atr_percentile));
let flow_score = flow_component(bars)?;
let persistence_score = trend_persistence.map_or(50.0, |t| t.score);
let regime_score = regime.map_or(50.0, regime_component);
let score = (fear_gauge_score * 0.30
+ volatility_score * 0.20
+ flow_score * 0.20
+ persistence_score * 0.20
+ regime_score * 0.10)
.clamp(0.0, 100.0);
let state = classify(score);
let (driver, drag) = driver_and_drag(
fear_gauge_score,
volatility_score,
flow_score,
persistence_score,
regime_score,
);
Some(FearGreedReading {
score,
state,
fear_gauge_score,
volatility_score,
flow_score,
persistence_score,
regime_score,
driver,
drag,
})
}
fn fear_gauge_component(f: &FearGaugeReading) -> f64 {
let base = match f.state {
FearGaugeState::FearSpike => 15.0,
FearGaugeState::ComplacencySpike => 85.0,
FearGaugeState::Neutral => {
let spread = (f.bwvf - f.wvf).clamp(-20.0, 20.0);
50.0 + spread * 1.25
}
};
if f.absorbed {
(base + 50.0) / 2.0
} else {
base
}
}
fn regime_component(r: &RegimeReading) -> f64 {
let trendiness = r.trend_votes as f64 / 3.0;
match r.state {
RegimeState::Trending => 55.0 + trendiness * 25.0,
RegimeState::Ranging => 45.0 + trendiness * 15.0,
}
}
fn flow_component(bars: &[Bar]) -> Option<f64> {
let start = bars.len().saturating_sub(FLOW_WINDOW);
let window = &bars[start..];
if window.is_empty() {
return None;
}
let use_volume = window.iter().any(|b| b.volume > 0.0);
let mut weighted = 0.0;
let mut weight_sum = 0.0;
for bar in window {
let span = bar.high - bar.low;
if span <= 0.0 {
continue;
}
let clv = (((bar.close - bar.low) - (bar.high - bar.close)) / span).clamp(-1.0, 1.0);
let weight = if use_volume { bar.volume.max(0.0) } else { 1.0 };
if weight == 0.0 {
continue;
}
weighted += clv * weight;
weight_sum += weight;
}
if weight_sum == 0.0 {
return Some(50.0);
}
Some(((weighted / weight_sum) + 1.0) * 50.0)
}
fn classify(score: f64) -> FearGreedState {
if score < 25.0 {
FearGreedState::ExtremeFear
} else if score < 45.0 {
FearGreedState::Fear
} else if score < 55.0 {
FearGreedState::Neutral
} else if score < 75.0 {
FearGreedState::Greed
} else {
FearGreedState::ExtremeGreed
}
}
fn driver_and_drag(
fear_gauge: f64,
volatility: f64,
flow: f64,
persistence: f64,
regime: f64,
) -> (FearGreedDriver, FearGreedDriver) {
use FearGreedDriver::*;
let scores = [
(FearGauge, fear_gauge),
(Volatility, volatility),
(Flow, flow),
(Persistence, persistence),
(Regime, regime),
];
let driver = scores
.iter()
.max_by(|a, b| a.1.total_cmp(&b.1))
.expect("scores is non-empty")
.0;
let drag = scores
.iter()
.min_by(|a, b| a.1.total_cmp(&b.1))
.expect("scores is non-empty")
.0;
(driver, drag)
}
#[cfg(test)]
mod tests {
use super::*;
fn bar(c: f64, h: f64, l: f64, volume: f64) -> Bar {
Bar {
timestamp: 0,
open: c,
high: h,
low: l,
close: c,
volume,
}
}
#[test]
fn close_near_high_pushes_flow_toward_greed() {
let bars: Vec<Bar> = (0..40).map(|_| bar(109.0, 110.0, 100.0, 10.0)).collect();
let r = fear_greed_reading(&bars, None, None, None, None).expect("reading");
assert!(r.flow_score > 80.0);
assert!(r.score > 50.0);
}
#[test]
fn fear_spike_pulls_score_down() {
let bars: Vec<Bar> = (0..40).map(|_| bar(101.0, 110.0, 100.0, 10.0)).collect();
let fear = FearGaugeReading {
state: FearGaugeState::FearSpike,
wvf: 20.0,
bwvf: 1.0,
absorbed: false,
};
let r = fear_greed_reading(&bars, None, None, Some(&fear), None).expect("reading");
assert!(r.fear_gauge_score < 20.0);
assert!(r.score < 50.0);
}
}