finance_query/backtesting/
signal.rs1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::error::{BacktestError, Result};
7
8#[non_exhaustive]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum SignalDirection {
12 Long,
14 Short,
16 Exit,
18 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
35pub struct SignalStrength(f64);
36
37impl SignalStrength {
38 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 pub fn clamped(value: f64) -> Self {
54 Self(value.clamp(0.0, 1.0))
55 }
56
57 pub fn value(&self) -> f64 {
59 self.0
60 }
61
62 pub fn strong() -> Self {
64 Self(1.0)
65 }
66
67 pub fn medium() -> Self {
69 Self(0.5)
70 }
71
72 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#[non_exhaustive]
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93pub struct SignalMetadata {
94 pub indicators: HashMap<String, f64>,
96}
97
98impl SignalMetadata {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 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#[non_exhaustive]
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Signal {
115 pub direction: SignalDirection,
117
118 pub strength: SignalStrength,
120
121 pub timestamp: i64,
123
124 pub price: f64,
126
127 pub reason: Option<String>,
129
130 pub metadata: Option<SignalMetadata>,
132}
133
134impl Signal {
135 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 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 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 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 pub fn is_hold(&self) -> bool {
185 matches!(self.direction, SignalDirection::Hold)
186 }
187
188 pub fn is_entry(&self) -> bool {
190 matches!(
191 self.direction,
192 SignalDirection::Long | SignalDirection::Short
193 )
194 }
195
196 pub fn is_exit(&self) -> bool {
198 matches!(self.direction, SignalDirection::Exit)
199 }
200
201 pub fn with_strength(mut self, strength: SignalStrength) -> Self {
203 self.strength = strength;
204 self
205 }
206
207 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
209 self.reason = Some(reason.into());
210 self
211 }
212
213 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}