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    /// Whether this condition reads [`StrategyContext::extremes`].
100    ///
101    /// The engine folds the running peak/trough per bar only when some condition
102    /// says it needs it, so a strategy without a trailing condition pays nothing.
103    /// A condition that returns `false` but reads `ctx.extremes` anyway still
104    /// gets a correct answer — the read falls back to scanning from the entry
105    /// bar — but pays the O(bars²) cost this flag exists to avoid.
106    ///
107    /// [`StrategyContext::extremes`]: crate::backtesting::StrategyContext::extremes
108    fn tracks_position_extremes(&self) -> bool {
109        false
110    }
111
112    /// Get a human-readable description of this condition.
113    ///
114    /// This is used for logging, debugging, and signal reporting.
115    fn description(&self) -> String;
116
117    /// Combine this condition with another using AND logic.
118    ///
119    /// The resulting condition is true only when both conditions are true.
120    ///
121    /// # Example
122    ///
123    /// ```ignore
124    /// let entry = rsi(14).below(30.0).and(price().above_ref(sma(200)));
125    /// ```
126    fn and<C: Condition>(self, other: C) -> And<Self, C>
127    where
128        Self: Sized,
129    {
130        And::new(self, other)
131    }
132
133    /// Combine this condition with another using OR logic.
134    ///
135    /// The resulting condition is true when either condition is true.
136    ///
137    /// # Example
138    ///
139    /// ```ignore
140    /// let exit = rsi(14).above(70.0).or(stop_loss(0.05));
141    /// ```
142    fn or<C: Condition>(self, other: C) -> Or<Self, C>
143    where
144        Self: Sized,
145    {
146        Or::new(self, other)
147    }
148
149    /// Negate this condition.
150    ///
151    /// The resulting condition is true when this condition is false.
152    ///
153    /// # Example
154    ///
155    /// ```ignore
156    /// let not_overbought = rsi(14).above(70.0).not();
157    /// ```
158    fn not(self) -> Not<Self>
159    where
160        Self: Sized,
161    {
162        Not::new(self)
163    }
164}
165
166/// A condition that always returns the same value.
167///
168/// Useful for testing or as a placeholder.
169#[derive(Debug, Clone, Copy)]
170pub struct ConstantCondition(bool);
171
172impl ConstantCondition {
173    /// Create a condition that always returns true.
174    pub fn always_true() -> Self {
175        Self(true)
176    }
177
178    /// Create a condition that always returns false.
179    pub fn always_false() -> Self {
180        Self(false)
181    }
182}
183
184impl Condition for ConstantCondition {
185    fn evaluate(&self, _ctx: &StrategyContext) -> bool {
186        self.0
187    }
188
189    fn required_indicators(&self) -> Vec<(String, Indicator)> {
190        vec![]
191    }
192
193    fn description(&self) -> String {
194        if self.0 {
195            "always true".to_string()
196        } else {
197            "always false".to_string()
198        }
199    }
200}
201
202/// Convenience function to create a condition that always returns true.
203#[inline]
204pub fn always_true() -> ConstantCondition {
205    ConstantCondition::always_true()
206}
207
208/// Convenience function to create a condition that always returns false.
209#[inline]
210pub fn always_false() -> ConstantCondition {
211    ConstantCondition::always_false()
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_constant_conditions() {
220        assert_eq!(always_true().description(), "always true");
221        assert_eq!(always_false().description(), "always false");
222    }
223}