Skip to main content

finance_query/backtesting/condition/
mod.rs

1//! Condition system for building strategy entry/exit rules.
2//!
3//! This module provides a composable way to define trading conditions
4//! using indicator references and comparison operations.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use finance_query::backtesting::refs::*;
10//! use finance_query::backtesting::condition::*;
11//!
12//! // Simple condition
13//! let oversold = rsi(14).below(30.0);
14//!
15//! // Compound conditions
16//! let entry = rsi(14).crosses_below(30.0)
17//!     .and(price().above_ref(sma(200)));
18//!
19//! let exit = rsi(14).crosses_above(70.0)
20//!     .or(stop_loss(0.05));
21//! ```
22
23mod comparison;
24mod composite;
25mod threshold;
26
27pub use comparison::*;
28pub use composite::*;
29pub use threshold::*;
30
31use crate::constants::Interval;
32use crate::indicators::Indicator;
33
34use super::strategy::StrategyContext;
35
36/// Describes an indicator that must be pre-computed on a resampled (HTF) candle series.
37///
38/// Returned by [`Condition::htf_requirements`] and processed by the engine to build
39/// stretched arrays stored in `StrategyContext::indicators` under `htf_key`.
40#[derive(Clone, Debug)]
41pub struct HtfIndicatorSpec {
42    /// Target higher timeframe interval.
43    pub interval: Interval,
44    /// Key under which the stretched value is stored (e.g. `"htf_1wk_sma_20"`).
45    pub htf_key: String,
46    /// Key the inner condition looks up (e.g. `"sma_20"`).
47    pub base_key: String,
48    /// Indicator to compute on the resampled HTF candles.
49    pub indicator: Indicator,
50    /// UTC offset in seconds for the exchange whose candles are being resampled.
51    ///
52    /// Passed to [`resample`] so that weekly/monthly bucket boundaries align with
53    /// the exchange's local calendar rather than UTC. Use
54    /// [`Region::utc_offset_secs`] to obtain the correct value, or `0` for UTC.
55    ///
56    /// [`resample`]: crate::backtesting::resample::resample
57    /// [`Region::utc_offset_secs`]: crate::constants::Region::utc_offset_secs
58    pub utc_offset_secs: i64,
59}
60
61/// A condition that can be evaluated on each candle.
62///
63/// Conditions are the building blocks of trading strategies.
64/// They can be combined using `and()`, `or()`, and `not()` operations.
65///
66/// # Example
67///
68/// ```ignore
69/// use finance_query::backtesting::condition::Condition;
70///
71/// fn my_custom_condition(ctx: &StrategyContext) -> bool {
72///     // Custom logic here
73///     true
74/// }
75/// ```
76pub trait Condition: Clone + Send + Sync + 'static {
77    /// Evaluate the condition with the current strategy context.
78    ///
79    /// Returns `true` if the condition is met, `false` otherwise.
80    fn evaluate(&self, ctx: &StrategyContext) -> bool;
81
82    /// Get the indicators required by this condition.
83    ///
84    /// The backtest engine will pre-compute these indicators
85    /// before running the strategy.
86    fn required_indicators(&self) -> Vec<(String, Indicator)>;
87
88    /// Get any higher-timeframe indicators required by this condition.
89    ///
90    /// The engine resamples candles to each unique interval, computes the
91    /// listed indicators on the resampled data, and stores stretched
92    /// (base-timeframe-length) arrays in `StrategyContext::indicators`
93    /// under the `htf_key` names.  [`HtfCondition`](crate::backtesting::refs::HtfCondition)
94    /// implements this automatically; all other conditions return `vec![]`.
95    fn htf_requirements(&self) -> Vec<HtfIndicatorSpec> {
96        vec![]
97    }
98
99    /// Get a human-readable description of this condition.
100    ///
101    /// This is used for logging, debugging, and signal reporting.
102    fn description(&self) -> String;
103
104    /// Combine this condition with another using AND logic.
105    ///
106    /// The resulting condition is true only when both conditions are true.
107    ///
108    /// # Example
109    ///
110    /// ```ignore
111    /// let entry = rsi(14).below(30.0).and(price().above_ref(sma(200)));
112    /// ```
113    fn and<C: Condition>(self, other: C) -> And<Self, C>
114    where
115        Self: Sized,
116    {
117        And::new(self, other)
118    }
119
120    /// Combine this condition with another using OR logic.
121    ///
122    /// The resulting condition is true when either condition is true.
123    ///
124    /// # Example
125    ///
126    /// ```ignore
127    /// let exit = rsi(14).above(70.0).or(stop_loss(0.05));
128    /// ```
129    fn or<C: Condition>(self, other: C) -> Or<Self, C>
130    where
131        Self: Sized,
132    {
133        Or::new(self, other)
134    }
135
136    /// Negate this condition.
137    ///
138    /// The resulting condition is true when this condition is false.
139    ///
140    /// # Example
141    ///
142    /// ```ignore
143    /// let not_overbought = rsi(14).above(70.0).not();
144    /// ```
145    fn not(self) -> Not<Self>
146    where
147        Self: Sized,
148    {
149        Not::new(self)
150    }
151}
152
153/// A condition that always returns the same value.
154///
155/// Useful for testing or as a placeholder.
156#[derive(Debug, Clone, Copy)]
157pub struct ConstantCondition(bool);
158
159impl ConstantCondition {
160    /// Create a condition that always returns true.
161    pub fn always_true() -> Self {
162        Self(true)
163    }
164
165    /// Create a condition that always returns false.
166    pub fn always_false() -> Self {
167        Self(false)
168    }
169}
170
171impl Condition for ConstantCondition {
172    fn evaluate(&self, _ctx: &StrategyContext) -> bool {
173        self.0
174    }
175
176    fn required_indicators(&self) -> Vec<(String, Indicator)> {
177        vec![]
178    }
179
180    fn description(&self) -> String {
181        if self.0 {
182            "always true".to_string()
183        } else {
184            "always false".to_string()
185        }
186    }
187}
188
189/// Convenience function to create a condition that always returns true.
190#[inline]
191pub fn always_true() -> ConstantCondition {
192    ConstantCondition::always_true()
193}
194
195/// Convenience function to create a condition that always returns false.
196#[inline]
197pub fn always_false() -> ConstantCondition {
198    ConstantCondition::always_false()
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_constant_conditions() {
207        assert_eq!(always_true().description(), "always true");
208        assert_eq!(always_false().description(), "always false");
209    }
210}