Skip to main content

finance_query/backtesting/refs/
price.rs

1//! Price-based indicator references.
2//!
3//! This module provides references to OHLCV price data that can be used
4//! in trading conditions without requiring indicator computation.
5
6use crate::indicators::Indicator;
7
8use super::IndicatorRef;
9use crate::backtesting::strategy::StrategyContext;
10
11/// Reference to the close price.
12///
13/// # Example
14///
15/// ```ignore
16/// use finance_query::backtesting::refs::*;
17///
18/// let close_above_sma = close().above_ref(sma(200));
19/// ```
20#[derive(Debug, Clone, Copy)]
21pub struct ClosePrice;
22
23impl IndicatorRef for ClosePrice {
24    fn key(&self) -> String {
25        "close".to_string()
26    }
27
28    fn required_indicators(&self) -> Vec<(String, Indicator)> {
29        vec![] // Price doesn't need pre-computation
30    }
31
32    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
33        Some(ctx.close())
34    }
35
36    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
37        ctx.previous_candle().map(|c| c.close)
38    }
39}
40
41/// Reference to the open price.
42#[derive(Debug, Clone, Copy)]
43pub struct OpenPrice;
44
45impl IndicatorRef for OpenPrice {
46    fn key(&self) -> String {
47        "open".to_string()
48    }
49
50    fn required_indicators(&self) -> Vec<(String, Indicator)> {
51        vec![]
52    }
53
54    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
55        Some(ctx.open())
56    }
57
58    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
59        ctx.previous_candle().map(|c| c.open)
60    }
61}
62
63/// Reference to the high price.
64#[derive(Debug, Clone, Copy)]
65pub struct HighPrice;
66
67impl IndicatorRef for HighPrice {
68    fn key(&self) -> String {
69        "high".to_string()
70    }
71
72    fn required_indicators(&self) -> Vec<(String, Indicator)> {
73        vec![]
74    }
75
76    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
77        Some(ctx.high())
78    }
79
80    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
81        ctx.previous_candle().map(|c| c.high)
82    }
83}
84
85/// Reference to the low price.
86#[derive(Debug, Clone, Copy)]
87pub struct LowPrice;
88
89impl IndicatorRef for LowPrice {
90    fn key(&self) -> String {
91        "low".to_string()
92    }
93
94    fn required_indicators(&self) -> Vec<(String, Indicator)> {
95        vec![]
96    }
97
98    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
99        Some(ctx.low())
100    }
101
102    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
103        ctx.previous_candle().map(|c| c.low)
104    }
105}
106
107/// Reference to the volume.
108#[derive(Debug, Clone, Copy)]
109pub struct VolumeRef;
110
111impl IndicatorRef for VolumeRef {
112    fn key(&self) -> String {
113        "volume".to_string()
114    }
115
116    fn required_indicators(&self) -> Vec<(String, Indicator)> {
117        vec![]
118    }
119
120    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
121        Some(ctx.volume() as f64)
122    }
123
124    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
125        ctx.previous_candle().map(|c| c.volume as f64)
126    }
127}
128
129/// Reference to the typical price: (high + low + close) / 3
130#[derive(Debug, Clone, Copy)]
131pub struct TypicalPrice;
132
133impl IndicatorRef for TypicalPrice {
134    fn key(&self) -> String {
135        "typical_price".to_string()
136    }
137
138    fn required_indicators(&self) -> Vec<(String, Indicator)> {
139        vec![]
140    }
141
142    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
143        let candle = ctx.current_candle();
144        Some((candle.high + candle.low + candle.close) / 3.0)
145    }
146
147    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
148        ctx.previous_candle()
149            .map(|c| (c.high + c.low + c.close) / 3.0)
150    }
151}
152
153/// Reference to the median price: (high + low) / 2
154#[derive(Debug, Clone, Copy)]
155pub struct MedianPrice;
156
157impl IndicatorRef for MedianPrice {
158    fn key(&self) -> String {
159        "median_price".to_string()
160    }
161
162    fn required_indicators(&self) -> Vec<(String, Indicator)> {
163        vec![]
164    }
165
166    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
167        let candle = ctx.current_candle();
168        Some((candle.high + candle.low) / 2.0)
169    }
170
171    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
172        ctx.previous_candle().map(|c| (c.high + c.low) / 2.0)
173    }
174}
175
176// ============================================================================
177// DERIVED PRICE REFERENCES
178// ============================================================================
179
180/// Reference to the price change percentage from previous close.
181///
182/// Returns the percentage change: ((current_close - prev_close) / prev_close) * 100
183///
184/// # Example
185///
186/// ```ignore
187/// use finance_query::backtesting::refs::*;
188///
189/// // Enter on big moves (>2% change)
190/// let big_move = price_change_pct().above(2.0);
191/// // Exit on reversal
192/// let reversal = price_change_pct().below(-1.5);
193/// ```
194#[derive(Debug, Clone, Copy)]
195pub struct PriceChangePct;
196
197impl IndicatorRef for PriceChangePct {
198    fn key(&self) -> String {
199        "price_change_pct".to_string()
200    }
201
202    fn required_indicators(&self) -> Vec<(String, Indicator)> {
203        vec![]
204    }
205
206    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
207        let current = ctx.close();
208        ctx.previous_candle().map(|prev| {
209            if prev.close != 0.0 {
210                ((current - prev.close) / prev.close) * 100.0
211            } else {
212                0.0
213            }
214        })
215    }
216
217    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
218        // Need candle at idx-1 and idx-2
219        let candles = &ctx.candles;
220        let idx = ctx.index;
221        if idx >= 2 {
222            let prev = &candles[idx - 1];
223            let prev_prev = &candles[idx - 2];
224            if prev_prev.close != 0.0 {
225                Some(((prev.close - prev_prev.close) / prev_prev.close) * 100.0)
226            } else {
227                Some(0.0)
228            }
229        } else {
230            None
231        }
232    }
233}
234
235/// Reference to the gap percentage (open vs previous close).
236///
237/// Returns: ((current_open - prev_close) / prev_close) * 100
238///
239/// # Example
240///
241/// ```ignore
242/// use finance_query::backtesting::refs::*;
243///
244/// // Gap up strategy
245/// let gap_up = gap_pct().above(1.0);
246/// // Gap down reversal
247/// let gap_down = gap_pct().below(-2.0);
248/// ```
249#[derive(Debug, Clone, Copy)]
250pub struct GapPct;
251
252impl IndicatorRef for GapPct {
253    fn key(&self) -> String {
254        "gap_pct".to_string()
255    }
256
257    fn required_indicators(&self) -> Vec<(String, Indicator)> {
258        vec![]
259    }
260
261    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
262        let current_open = ctx.open();
263        ctx.previous_candle().map(|prev| {
264            if prev.close != 0.0 {
265                ((current_open - prev.close) / prev.close) * 100.0
266            } else {
267                0.0
268            }
269        })
270    }
271
272    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
273        let candles = &ctx.candles;
274        let idx = ctx.index;
275        if idx >= 2 {
276            let prev = &candles[idx - 1];
277            let prev_prev = &candles[idx - 2];
278            if prev_prev.close != 0.0 {
279                Some(((prev.open - prev_prev.close) / prev_prev.close) * 100.0)
280            } else {
281                Some(0.0)
282            }
283        } else {
284            None
285        }
286    }
287}
288
289/// Reference to the candle range (high - low).
290///
291/// # Example
292///
293/// ```ignore
294/// use finance_query::backtesting::refs::*;
295///
296/// // Filter for high volatility candles
297/// let wide_range = candle_range().above(5.0);
298/// ```
299#[derive(Debug, Clone, Copy)]
300pub struct CandleRange;
301
302impl IndicatorRef for CandleRange {
303    fn key(&self) -> String {
304        "candle_range".to_string()
305    }
306
307    fn required_indicators(&self) -> Vec<(String, Indicator)> {
308        vec![]
309    }
310
311    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
312        let candle = ctx.current_candle();
313        Some(candle.high - candle.low)
314    }
315
316    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
317        ctx.previous_candle().map(|c| c.high - c.low)
318    }
319}
320
321/// Reference to the candle body size (absolute difference between open and close).
322///
323/// # Example
324///
325/// ```ignore
326/// use finance_query::backtesting::refs::*;
327///
328/// // Strong candle filter
329/// let strong_body = candle_body().above(2.0);
330/// ```
331#[derive(Debug, Clone, Copy)]
332pub struct CandleBody;
333
334impl IndicatorRef for CandleBody {
335    fn key(&self) -> String {
336        "candle_body".to_string()
337    }
338
339    fn required_indicators(&self) -> Vec<(String, Indicator)> {
340        vec![]
341    }
342
343    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
344        let candle = ctx.current_candle();
345        Some((candle.close - candle.open).abs())
346    }
347
348    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
349        ctx.previous_candle().map(|c| (c.close - c.open).abs())
350    }
351}
352
353/// Reference to whether the current candle is bullish (close > open).
354///
355/// Returns 1.0 if bullish, 0.0 if bearish or doji.
356///
357/// # Example
358///
359/// ```ignore
360/// use finance_query::backtesting::refs::*;
361///
362/// // Only enter on bullish candles
363/// let bullish = is_bullish().above(0.5);
364/// ```
365#[derive(Debug, Clone, Copy)]
366pub struct IsBullish;
367
368impl IndicatorRef for IsBullish {
369    fn key(&self) -> String {
370        "is_bullish".to_string()
371    }
372
373    fn required_indicators(&self) -> Vec<(String, Indicator)> {
374        vec![]
375    }
376
377    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
378        let candle = ctx.current_candle();
379        Some(if candle.close > candle.open { 1.0 } else { 0.0 })
380    }
381
382    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
383        ctx.previous_candle()
384            .map(|c| if c.close > c.open { 1.0 } else { 0.0 })
385    }
386}
387
388/// Reference to whether the current candle is bearish (close < open).
389///
390/// Returns 1.0 if bearish, 0.0 if bullish or doji.
391#[derive(Debug, Clone, Copy)]
392pub struct IsBearish;
393
394impl IndicatorRef for IsBearish {
395    fn key(&self) -> String {
396        "is_bearish".to_string()
397    }
398
399    fn required_indicators(&self) -> Vec<(String, Indicator)> {
400        vec![]
401    }
402
403    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
404        let candle = ctx.current_candle();
405        Some(if candle.close < candle.open { 1.0 } else { 0.0 })
406    }
407
408    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
409        ctx.previous_candle()
410            .map(|c| if c.close < c.open { 1.0 } else { 0.0 })
411    }
412}
413
414// === Convenience Functions ===
415
416/// Get a reference to the close price.
417///
418/// # Example
419///
420/// ```ignore
421/// use finance_query::backtesting::refs::*;
422///
423/// let above_sma = price().above_ref(sma(200));
424/// let crosses_ema = close().crosses_above_ref(ema(50));
425/// ```
426#[inline]
427pub fn price() -> ClosePrice {
428    ClosePrice
429}
430
431/// Get a reference to the close price.
432///
433/// Alias for [`price()`].
434#[inline]
435pub fn close() -> ClosePrice {
436    ClosePrice
437}
438
439/// Get a reference to the open price.
440#[inline]
441pub fn open() -> OpenPrice {
442    OpenPrice
443}
444
445/// Get a reference to the high price.
446#[inline]
447pub fn high() -> HighPrice {
448    HighPrice
449}
450
451/// Get a reference to the low price.
452#[inline]
453pub fn low() -> LowPrice {
454    LowPrice
455}
456
457/// Get a reference to the volume.
458#[inline]
459pub fn volume() -> VolumeRef {
460    VolumeRef
461}
462
463/// Get a reference to the typical price: (high + low + close) / 3.
464#[inline]
465pub fn typical_price() -> TypicalPrice {
466    TypicalPrice
467}
468
469/// Get a reference to the median price: (high + low) / 2.
470#[inline]
471pub fn median_price() -> MedianPrice {
472    MedianPrice
473}
474
475/// Get a reference to the price change percentage from previous close.
476///
477/// # Example
478///
479/// ```ignore
480/// use finance_query::backtesting::refs::*;
481///
482/// // Big move filter (>2% change)
483/// let big_move = price_change_pct().above(2.0);
484/// ```
485#[inline]
486pub fn price_change_pct() -> PriceChangePct {
487    PriceChangePct
488}
489
490/// Get a reference to the gap percentage (open vs previous close).
491///
492/// # Example
493///
494/// ```ignore
495/// use finance_query::backtesting::refs::*;
496///
497/// // Gap up entry
498/// let gap_up = gap_pct().above(1.0);
499/// ```
500#[inline]
501pub fn gap_pct() -> GapPct {
502    GapPct
503}
504
505/// Get a reference to the candle range (high - low).
506#[inline]
507pub fn candle_range() -> CandleRange {
508    CandleRange
509}
510
511/// Get a reference to the candle body size.
512#[inline]
513pub fn candle_body() -> CandleBody {
514    CandleBody
515}
516
517/// Returns 1.0 if the candle is bullish (close > open), 0.0 otherwise.
518///
519/// Use with `.above(0.5)` to check for bullish candle.
520#[inline]
521pub fn is_bullish() -> IsBullish {
522    IsBullish
523}
524
525/// Returns 1.0 if the candle is bearish (close < open), 0.0 otherwise.
526///
527/// Use with `.above(0.5)` to check for bearish candle.
528#[inline]
529pub fn is_bearish() -> IsBearish {
530    IsBearish
531}
532
533/// Reference to relative volume (current volume / average volume over N periods).
534///
535/// Returns ratio: 1.0 = average, 2.0 = double average, etc.
536///
537/// # Example
538///
539/// ```ignore
540/// use finance_query::backtesting::refs::*;
541///
542/// // High volume breakout (volume > 1.5x average)
543/// let high_volume = relative_volume(20).above(1.5);
544/// ```
545#[derive(Debug, Clone, Copy)]
546pub struct RelativeVolume {
547    /// Number of periods for volume average
548    pub period: usize,
549}
550
551impl IndicatorRef for RelativeVolume {
552    fn key(&self) -> String {
553        format!("relative_volume_{}", self.period)
554    }
555
556    fn required_indicators(&self) -> Vec<(String, Indicator)> {
557        vec![] // We compute from candle history directly
558    }
559
560    fn value(&self, ctx: &StrategyContext) -> Option<f64> {
561        let candles = &ctx.candles;
562        let idx = ctx.index;
563
564        // Need at least `period` candles to compute average
565        if idx < self.period {
566            return None;
567        }
568
569        // Calculate average volume over the last N periods (not including current)
570        let avg_volume: f64 = candles[idx.saturating_sub(self.period)..idx]
571            .iter()
572            .map(|c| c.volume as f64)
573            .sum::<f64>()
574            / self.period as f64;
575
576        if avg_volume > 0.0 {
577            let current_volume = ctx.volume() as f64;
578            Some(current_volume / avg_volume)
579        } else {
580            None
581        }
582    }
583
584    fn prev_value(&self, ctx: &StrategyContext) -> Option<f64> {
585        let candles = &ctx.candles;
586        let idx = ctx.index;
587
588        if idx < self.period + 1 {
589            return None;
590        }
591
592        let prev_idx = idx - 1;
593        let avg_volume: f64 = candles[prev_idx.saturating_sub(self.period)..prev_idx]
594            .iter()
595            .map(|c| c.volume as f64)
596            .sum::<f64>()
597            / self.period as f64;
598
599        if avg_volume > 0.0 {
600            Some(candles[prev_idx].volume as f64 / avg_volume)
601        } else {
602            None
603        }
604    }
605}
606
607/// Get a reference to relative volume (current volume / N-period average volume).
608///
609/// # Arguments
610///
611/// * `period` - Number of periods for the volume average
612///
613/// # Example
614///
615/// ```ignore
616/// use finance_query::backtesting::refs::*;
617///
618/// // Volume spike filter
619/// let volume_spike = relative_volume(20).above(2.0);
620/// ```
621#[inline]
622pub fn relative_volume(period: usize) -> RelativeVolume {
623    RelativeVolume { period }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn test_price_keys() {
632        assert_eq!(price().key(), "close");
633        assert_eq!(close().key(), "close");
634        assert_eq!(open().key(), "open");
635        assert_eq!(high().key(), "high");
636        assert_eq!(low().key(), "low");
637        assert_eq!(volume().key(), "volume");
638        assert_eq!(typical_price().key(), "typical_price");
639        assert_eq!(median_price().key(), "median_price");
640    }
641
642    #[test]
643    fn test_price_no_indicators_required() {
644        assert!(price().required_indicators().is_empty());
645        assert!(open().required_indicators().is_empty());
646        assert!(high().required_indicators().is_empty());
647        assert!(low().required_indicators().is_empty());
648        assert!(volume().required_indicators().is_empty());
649    }
650
651    #[test]
652    fn test_derived_price_keys() {
653        assert_eq!(price_change_pct().key(), "price_change_pct");
654        assert_eq!(gap_pct().key(), "gap_pct");
655        assert_eq!(candle_range().key(), "candle_range");
656        assert_eq!(candle_body().key(), "candle_body");
657        assert_eq!(is_bullish().key(), "is_bullish");
658        assert_eq!(is_bearish().key(), "is_bearish");
659    }
660
661    #[test]
662    fn test_derived_price_no_indicators_required() {
663        assert!(price_change_pct().required_indicators().is_empty());
664        assert!(gap_pct().required_indicators().is_empty());
665        assert!(candle_range().required_indicators().is_empty());
666        assert!(candle_body().required_indicators().is_empty());
667        assert!(is_bullish().required_indicators().is_empty());
668        assert!(is_bearish().required_indicators().is_empty());
669    }
670
671    #[test]
672    fn test_relative_volume_key() {
673        assert_eq!(relative_volume(20).key(), "relative_volume_20");
674        assert_eq!(relative_volume(10).key(), "relative_volume_10");
675    }
676}