Skip to main content

finance_query/backtesting/
signal.rs

1//! Signal types for trading signals generated by strategies.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::error::{BacktestError, Result};
7
8/// Trading signal direction
9#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum SignalDirection {
12    /// Buy / Go Long
13    Long,
14    /// Sell / Go Short
15    Short,
16    /// Exit current position
17    Exit,
18    /// No action
19    Hold,
20}
21
22impl std::fmt::Display for SignalDirection {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Self::Long => write!(f, "LONG"),
26            Self::Short => write!(f, "SHORT"),
27            Self::Exit => write!(f, "EXIT"),
28            Self::Hold => write!(f, "HOLD"),
29        }
30    }
31}
32
33/// Signal strength/confidence (0.0 to 1.0)
34#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
35pub struct SignalStrength(f64);
36
37impl SignalStrength {
38    /// Create a new signal strength value
39    ///
40    /// # Errors
41    /// Returns error if value is not between 0.0 and 1.0
42    pub fn new(value: f64) -> Result<Self> {
43        if !(0.0..=1.0).contains(&value) {
44            return Err(BacktestError::invalid_param(
45                "signal_strength",
46                "must be between 0.0 and 1.0",
47            ));
48        }
49        Ok(Self(value))
50    }
51
52    /// Create a signal strength without validation (clamped to [0.0, 1.0])
53    pub fn clamped(value: f64) -> Self {
54        Self(value.clamp(0.0, 1.0))
55    }
56
57    /// Get the strength value
58    pub fn value(&self) -> f64 {
59        self.0
60    }
61
62    /// Strong signal (1.0)
63    pub fn strong() -> Self {
64        Self(1.0)
65    }
66
67    /// Medium signal (0.5)
68    pub fn medium() -> Self {
69        Self(0.5)
70    }
71
72    /// Weak signal (0.3)
73    pub fn weak() -> Self {
74        Self(0.3)
75    }
76}
77
78impl Default for SignalStrength {
79    fn default() -> Self {
80        Self(1.0)
81    }
82}
83
84impl std::fmt::Display for SignalStrength {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{:.2}", self.0)
87    }
88}
89
90/// Metadata attached to signals for analysis
91#[non_exhaustive]
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93pub struct SignalMetadata {
94    /// Indicator values at signal time
95    pub indicators: HashMap<String, f64>,
96}
97
98impl SignalMetadata {
99    /// Create empty metadata
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Add an indicator value
105    pub fn with_indicator(mut self, name: impl Into<String>, value: f64) -> Self {
106        self.indicators.insert(name.into(), value);
107        self
108    }
109}
110
111/// A trading signal generated by a strategy
112#[non_exhaustive]
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Signal {
115    /// Signal direction
116    pub direction: SignalDirection,
117
118    /// Signal strength/confidence
119    pub strength: SignalStrength,
120
121    /// Timestamp when signal was generated
122    pub timestamp: i64,
123
124    /// Price at signal generation
125    pub price: f64,
126
127    /// Optional reason/description
128    pub reason: Option<String>,
129
130    /// Strategy-specific metadata (indicator values, etc.)
131    pub metadata: Option<SignalMetadata>,
132}
133
134impl Signal {
135    /// Create a long signal
136    pub fn long(timestamp: i64, price: f64) -> Self {
137        Self {
138            direction: SignalDirection::Long,
139            strength: SignalStrength::default(),
140            timestamp,
141            price,
142            reason: None,
143            metadata: None,
144        }
145    }
146
147    /// Create a short signal
148    pub fn short(timestamp: i64, price: f64) -> Self {
149        Self {
150            direction: SignalDirection::Short,
151            strength: SignalStrength::default(),
152            timestamp,
153            price,
154            reason: None,
155            metadata: None,
156        }
157    }
158
159    /// Create an exit signal
160    pub fn exit(timestamp: i64, price: f64) -> Self {
161        Self {
162            direction: SignalDirection::Exit,
163            strength: SignalStrength::default(),
164            timestamp,
165            price,
166            reason: None,
167            metadata: None,
168        }
169    }
170
171    /// Create a hold signal (no action)
172    pub fn hold() -> Self {
173        Self {
174            direction: SignalDirection::Hold,
175            strength: SignalStrength::default(),
176            timestamp: 0,
177            price: 0.0,
178            reason: None,
179            metadata: None,
180        }
181    }
182
183    /// Check if this is a hold signal
184    pub fn is_hold(&self) -> bool {
185        matches!(self.direction, SignalDirection::Hold)
186    }
187
188    /// Check if this is an entry signal (Long or Short)
189    pub fn is_entry(&self) -> bool {
190        matches!(
191            self.direction,
192            SignalDirection::Long | SignalDirection::Short
193        )
194    }
195
196    /// Check if this is an exit signal
197    pub fn is_exit(&self) -> bool {
198        matches!(self.direction, SignalDirection::Exit)
199    }
200
201    /// Set signal strength
202    pub fn with_strength(mut self, strength: SignalStrength) -> Self {
203        self.strength = strength;
204        self
205    }
206
207    /// Set reason/description
208    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
209        self.reason = Some(reason.into());
210        self
211    }
212
213    /// Set metadata
214    pub fn with_metadata(mut self, metadata: SignalMetadata) -> Self {
215        self.metadata = Some(metadata);
216        self
217    }
218}
219
220impl Default for Signal {
221    fn default() -> Self {
222        Self::hold()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_signal_strength_bounds() {
232        assert!(SignalStrength::new(0.5).is_ok());
233        assert!(SignalStrength::new(0.0).is_ok());
234        assert!(SignalStrength::new(1.0).is_ok());
235        assert!(SignalStrength::new(-0.1).is_err());
236        assert!(SignalStrength::new(1.1).is_err());
237    }
238
239    #[test]
240    fn test_signal_strength_clamped() {
241        assert_eq!(SignalStrength::clamped(1.5).value(), 1.0);
242        assert_eq!(SignalStrength::clamped(-0.5).value(), 0.0);
243        assert_eq!(SignalStrength::clamped(0.7).value(), 0.7);
244    }
245
246    #[test]
247    fn test_signal_creation() {
248        let sig = Signal::long(1234567890, 150.0).with_reason("test signal");
249        assert_eq!(sig.direction, SignalDirection::Long);
250        assert_eq!(sig.timestamp, 1234567890);
251        assert_eq!(sig.price, 150.0);
252        assert_eq!(sig.reason, Some("test signal".to_string()));
253        assert!(sig.is_entry());
254        assert!(!sig.is_hold());
255        assert!(!sig.is_exit());
256    }
257
258    #[test]
259    fn test_signal_hold() {
260        let sig = Signal::hold();
261        assert!(sig.is_hold());
262        assert!(!sig.is_entry());
263        assert!(!sig.is_exit());
264    }
265
266    #[test]
267    fn test_signal_metadata() {
268        let metadata = SignalMetadata::new()
269            .with_indicator("rsi", 30.0)
270            .with_indicator("sma_20", 150.0);
271
272        let sig = Signal::long(0, 0.0).with_metadata(metadata);
273        let meta = sig.metadata.unwrap();
274        assert_eq!(meta.indicators.get("rsi"), Some(&30.0));
275        assert_eq!(meta.indicators.get("sma_20"), Some(&150.0));
276    }
277}