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}