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}