Skip to main content

rustrade_core/
signal.rs

1//! Trading signals — the output of a [`crate::Brain`].
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A trading signal direction.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SignalType {
10    /// Enter long (or flip from short to long).
11    Buy,
12    /// Enter short (or flip from long to short).
13    Sell,
14    /// No new action. Existing position may or may not be held depending on
15    /// other gates (stops, max-hold, etc.).
16    Hold,
17    /// Close the existing position without reversing.
18    Close,
19}
20
21impl std::fmt::Display for SignalType {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::Buy => write!(f, "BUY"),
25            Self::Sell => write!(f, "SELL"),
26            Self::Hold => write!(f, "HOLD"),
27            Self::Close => write!(f, "CLOSE"),
28        }
29    }
30}
31
32/// A richer signal carrying confidence, source, and arbitrary metadata for logging.
33///
34/// The framework's execution layer doesn't interpret `metadata` — it's there so
35/// a brain can record its rationale for post-hoc analysis.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Signal {
38    /// Unique identifier for this signal (typically `{brain_name}-{counter}`).
39    pub id: String,
40    /// Symbol the signal is for, as a free-form string.
41    pub symbol: String,
42    /// Buy / Sell / Hold / Close.
43    pub kind: SignalType,
44    /// Confidence in [0.0, 1.0]. A brain producing a `Buy` with confidence
45    /// 0.2 is saying "I'm barely sure about this" — the risk layer can choose
46    /// to size down or reject.
47    pub confidence: f64,
48    /// Time the brain emitted this signal.
49    pub timestamp: DateTime<Utc>,
50    /// Brain name that emitted the signal — useful for routing in
51    /// multi-brain bots.
52    pub source: String,
53    /// Free-form. Use `serde_json::json!({...})` to populate.
54    #[serde(default)]
55    pub metadata: serde_json::Value,
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn signal_type_display() {
64        assert_eq!(SignalType::Buy.to_string(), "BUY");
65        assert_eq!(SignalType::Sell.to_string(), "SELL");
66        assert_eq!(SignalType::Hold.to_string(), "HOLD");
67        assert_eq!(SignalType::Close.to_string(), "CLOSE");
68    }
69
70    #[test]
71    fn signal_serde_roundtrip() {
72        let s = Signal {
73            id: "sig-1".into(),
74            symbol: "BTCUSDT".into(),
75            kind: SignalType::Buy,
76            confidence: 0.8,
77            timestamp: chrono::Utc::now(),
78            source: "ema-cross".into(),
79            metadata: serde_json::json!({"fast": 9, "slow": 21}),
80        };
81        let json = serde_json::to_string(&s).unwrap();
82        let back: Signal = serde_json::from_str(&json).unwrap();
83        assert_eq!(back.id, s.id);
84        assert!(matches!(back.kind, SignalType::Buy));
85        assert_eq!(back.metadata["fast"], 9);
86    }
87}