use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::broadcast;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SignalType {
Buy,
Sell,
Hold,
Close,
}
impl std::fmt::Display for SignalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignalType::Buy => write!(f, "BUY"),
SignalType::Sell => write!(f, "SELL"),
SignalType::Hold => write!(f, "HOLD"),
SignalType::Close => write!(f, "CLOSE"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SignalPriority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub id: String,
pub symbol: String,
pub signal_type: SignalType,
pub confidence: f64,
pub timestamp: DateTime<Utc>,
pub priority: SignalPriority,
pub source: String,
pub strategy_id: Option<String>,
pub target_price: Option<f64>,
pub stop_loss: Option<f64>,
pub take_profit: Option<f64>,
pub quantity: Option<f64>,
pub metadata: HashMap<String, String>,
}
impl Signal {
pub fn new(symbol: impl Into<String>, signal_type: SignalType, confidence: f64) -> Self {
Self {
id: Uuid::new_v4().to_string(),
symbol: symbol.into(),
signal_type,
confidence: confidence.clamp(0.0, 1.0),
timestamp: Utc::now(),
priority: SignalPriority::Normal,
source: "unknown".to_string(),
strategy_id: None,
target_price: None,
stop_loss: None,
take_profit: None,
quantity: None,
metadata: HashMap::new(),
}
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = source.into();
self
}
pub fn with_priority(mut self, priority: SignalPriority) -> Self {
self.priority = priority;
self
}
pub fn with_strategy(mut self, strategy_id: impl Into<String>) -> Self {
self.strategy_id = Some(strategy_id.into());
self
}
pub fn with_target_price(mut self, price: f64) -> Self {
self.target_price = Some(price);
self
}
pub fn with_stop_loss(mut self, price: f64) -> Self {
self.stop_loss = Some(price);
self
}
pub fn with_take_profit(mut self, price: f64) -> Self {
self.take_profit = Some(price);
self
}
pub fn with_quantity(mut self, quantity: f64) -> Self {
self.quantity = Some(quantity);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_actionable(&self) -> bool {
!matches!(self.signal_type, SignalType::Hold)
}
pub fn meets_threshold(&self, threshold: f64) -> bool {
self.confidence >= threshold
}
}
pub struct SignalBus {
tx: broadcast::Sender<Signal>,
capacity: usize,
}
impl SignalBus {
pub fn new(capacity: usize) -> Self {
let (tx, _rx) = broadcast::channel(capacity);
Self { tx, capacity }
}
pub fn publish(&self, signal: Signal) -> crate::Result<usize> {
let receivers = self.tx.send(signal)?;
Ok(receivers)
}
pub fn subscribe(&self) -> broadcast::Receiver<Signal> {
self.tx.subscribe()
}
pub fn subscriber_count(&self) -> usize {
self.tx.receiver_count()
}
pub fn capacity(&self) -> usize {
self.capacity
}
}
impl Default for SignalBus {
fn default() -> Self {
Self::new(1000)
}
}
impl Clone for SignalBus {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
capacity: self.capacity,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_creation() {
let signal = Signal::new("BTCUSD", SignalType::Buy, 0.85)
.with_source("forward")
.with_strategy("momentum_v1");
assert_eq!(signal.symbol, "BTCUSD");
assert_eq!(signal.signal_type, SignalType::Buy);
assert_eq!(signal.confidence, 0.85);
assert_eq!(signal.source, "forward");
assert!(signal.is_actionable());
}
#[test]
fn test_signal_bus() {
let bus = SignalBus::new(100);
let _rx = bus.subscribe();
let signal = Signal::new("ETHUSDT", SignalType::Sell, 0.9);
let _ = bus.publish(signal.clone());
assert_eq!(bus.subscriber_count(), 1);
}
#[test]
fn test_confidence_clamping() {
let signal = Signal::new("BTCUSD", SignalType::Buy, 1.5);
assert_eq!(signal.confidence, 1.0);
let signal2 = Signal::new("BTCUSD", SignalType::Buy, -0.5);
assert_eq!(signal2.confidence, 0.0);
}
}