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}