Skip to main content

quantoxide/signal/
core.rs

1use std::{
2    fmt,
3    panic::{self, AssertUnwindSafe},
4};
5
6use async_trait::async_trait;
7use futures::FutureExt;
8
9use crate::{
10    db::models::OhlcCandleRow, error::Result, shared::Lookback, shared::MinIterationInterval,
11};
12
13use super::error::{SignalEvaluatorError, SignalEvaluatorResult};
14
15/// Marker trait for signal types that can be used with the signal framework.
16///
17/// This trait bundles the common constraints required for signal types:
18/// - `Send + Sync`: Safe to share across threads
19/// - `Clone`: Can be duplicated for broadcasting
20/// - `Display`: Can be formatted for logging
21/// - `'static`: No borrowed references
22///
23/// A blanket implementation is provided for all types meeting these constraints, so this trait
24/// doesn't need to be implemented manually.
25///
26/// # Example
27///
28/// ```
29/// # use std::fmt;
30/// # use chrono::{DateTime, Utc};
31/// #[derive(Debug, Clone)]
32/// pub struct MySignal {
33///     pub time: DateTime<Utc>,
34///     // ...
35/// }
36///
37/// impl fmt::Display for MySignal {
38///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39///         write!(f, "{}", self.time)
40///     }
41/// }
42///
43/// // MySignal: Signal is satisfied automatically
44/// ```
45pub trait Signal: Send + Sync + Clone + fmt::Display + 'static {}
46
47impl<T> Signal for T where T: Send + Sync + Clone + fmt::Display + 'static {}
48
49/// Trait for implementing custom signal evaluation logic.
50///
51/// Signal evaluators analyze candlestick data to produce trading signals of type `S`. Evaluators
52/// are designed to be reusable building blocks that can be composed into different operators.
53///
54/// # Type Parameter
55///
56/// * `S` - The signal type this evaluator produces. For reusable evaluators, this is typically
57///   constrained with `where YourSignal: Into<S>` to allow conversion to any target type.
58///
59/// # Example
60///
61/// ```
62/// # use std::fmt;
63/// # use chrono::{DateTime, Utc};
64/// use quantoxide::{
65///     error::Result,
66///     models::{
67///         Lookback, MinIterationInterval, OhlcCandleRow, OhlcResolution
68///     },
69///     signal::{Signal, SignalEvaluator},
70/// };
71///
72/// // Define the evaluator's native signal type
73/// #[derive(Debug, Clone)]
74/// pub struct MaCrossSignal {
75///     pub time: DateTime<Utc>,
76///     pub fast_ma: f64,
77///     pub slow_ma: f64,
78/// }
79///
80/// impl fmt::Display for MaCrossSignal {
81///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82///         write!(f, "MaCross at {}: fast={:.2}, slow={:.2}", self.time, self.fast_ma, self.slow_ma)
83///     }
84/// }
85///
86/// // Evaluator struct is not generic, only the trait impl is
87/// pub struct MaCrossEvaluator {
88///     fast_period: usize,
89///     slow_period: usize,
90/// }
91///
92/// impl MaCrossEvaluator {
93///     pub fn new(fast_period: usize, slow_period: usize) -> Box<Self> {
94///         Box::new(Self { fast_period, slow_period })
95///     }
96/// }
97///
98/// #[async_trait::async_trait]
99/// impl<S: Signal> SignalEvaluator<S> for MaCrossEvaluator
100/// where
101///     MaCrossSignal: Into<S>,
102/// {
103///     fn lookback(&self) -> Option<Lookback> {
104///         Some(Lookback::new(OhlcResolution::FifteenMinutes, self.slow_period as u64)
105///             .expect("valid lookback"))
106///     }
107///
108///     fn min_iteration_interval(&self) -> MinIterationInterval {
109///         MinIterationInterval::MIN
110///     }
111///
112///     async fn evaluate(&self, candles: &[OhlcCandleRow]) -> Result<S> {
113///         let signal = MaCrossSignal {
114///             time: Utc::now(),
115///             fast_ma: 20.0, // Calculate actual MA
116///             slow_ma: 100.0,
117///         };
118///
119///         Ok(signal.into()) // Convert to target type
120///     }
121/// }
122/// ```
123#[async_trait]
124pub trait SignalEvaluator<S: Signal>: Send + Sync {
125    /// Returns the candle resolution and count needed for evaluation, or `None` if no historical
126    /// candle data is required.
127    ///
128    /// The framework uses this to fetch the appropriate historical candles before calling
129    /// [`evaluate`](Self::evaluate). When `None` is returned, an empty slice is provided to
130    /// `evaluate`.
131    fn lookback(&self) -> Option<Lookback>;
132
133    /// Returns the minimum interval between successive evaluations.
134    ///
135    /// The framework will not call [`evaluate`](Self::evaluate) more frequently than this interval.
136    fn min_iteration_interval(&self) -> MinIterationInterval;
137
138    /// Evaluates a series of OHLC candlesticks and returns a signal.
139    ///
140    /// The candlestick slice is ordered chronologically, with the most recent candle last.
141    /// The number of candles provided is determined by the [`lookback`](Self::lookback)
142    /// configuration.
143    async fn evaluate(&self, candles: &[OhlcCandleRow]) -> Result<S>;
144}
145
146/// Internal wrapper that provides panic protection for signal evaluators.
147pub(crate) struct WrappedSignalEvaluator<S: Signal>(Box<dyn SignalEvaluator<S>>);
148
149impl<S: Signal> WrappedSignalEvaluator<S> {
150    pub fn new(evaluator: Box<dyn SignalEvaluator<S>>) -> Self {
151        Self(evaluator)
152    }
153
154    /// Returns the lookback configuration with panic protection.
155    pub fn lookback(&self) -> SignalEvaluatorResult<Option<Lookback>> {
156        panic::catch_unwind(AssertUnwindSafe(|| self.0.lookback()))
157            .map_err(|e| SignalEvaluatorError::LookbackPanicked(e.into()))
158    }
159
160    /// Returns the minimum iteration interval with panic protection.
161    pub fn min_iteration_interval(&self) -> SignalEvaluatorResult<MinIterationInterval> {
162        panic::catch_unwind(AssertUnwindSafe(|| self.0.min_iteration_interval()))
163            .map_err(|e| SignalEvaluatorError::MinIterationIntervalPanicked(e.into()))
164    }
165
166    /// Evaluates candlestick data with panic protection.
167    pub async fn evaluate(&self, candles: &[OhlcCandleRow]) -> SignalEvaluatorResult<S> {
168        FutureExt::catch_unwind(AssertUnwindSafe(self.0.evaluate(candles)))
169            .await
170            .map_err(|e| SignalEvaluatorError::EvaluatePanicked(e.into()))?
171            .map_err(|e| SignalEvaluatorError::EvaluateError(e.to_string()))
172    }
173}