1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use std::collections::HashMap;
use crate::model::Bar;
use super::smoothing::Ema;
use super::{Indicator, IndicatorOutput};
/// Elder's Force Index: the bar-to-bar price change weighted by the volume that moved it.
///
/// Raw value: `(close_t - close_{t-1}) * volume_t`. There is no output on the first bar of a
/// series, because a change needs a previous close — a first bar is not a zero-force bar.
/// The main line is that raw series smoothed with an [`Ema`] of `ema_len`.
///
/// Unit: price change times volume, i.e. the series' price unit multiplied by whatever the
/// series counts as volume (traded turnover, contracts, or — on a tick-volume series — update
/// counts, which makes the magnitude meaningless even though the sign still reads). Values from
/// different instruments or volume kinds are not comparable; the sign and the zero crossing are.
/// See [`crate::applicability::data_requirements`], which declares real traded volume for this
/// indicator.
///
/// A bar with zero volume, or one that closes exactly where the previous bar closed, has a raw
/// force of 0. That zero enters the average as an ordinary observation: it pulls the smoothed
/// line towards zero, it does not set it to zero.
///
/// Per-bar outputs:
/// - `value`: the smoothed force index.
/// - `extra["raw"]`: the unsmoothed force of this bar, in the same unit.
///
/// First output: with the `ema_len`-th price change, i.e. after `ema_len + 1` bars. The internal
/// EMA runs from the first change onward — seeded with that first real observation, not with an
/// invented starting value — but its seed-dominated early values are not published.
/// [`Indicator::reset`] clears the previous close and the average, so the next series starts
/// deterministically; a series switch must go through it rather than continuing the average
/// across the boundary.
#[derive(Debug, Clone)]
pub struct ElderForceIndex {
ema_len: usize,
prev_close: Option<f64>,
ema: Ema,
changes_seen: usize,
}
impl ElderForceIndex {
pub fn new(ema_len: usize) -> Self {
Self {
ema_len: ema_len.max(1),
prev_close: None,
ema: Ema::new(ema_len.max(1)),
changes_seen: 0,
}
}
pub fn with_defaults() -> Self {
Self::new(13)
}
}
impl Indicator for ElderForceIndex {
fn name(&self) -> &str {
"efi"
}
fn warmup_period(&self) -> usize {
self.ema_len + 1
}
fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
let prev_close = match self.prev_close {
None => {
self.prev_close = Some(bar.close);
return None;
}
Some(p) => p,
};
self.prev_close = Some(bar.close);
let raw = (bar.close - prev_close) * bar.volume;
let line = self.ema.update(raw)?;
self.changes_seen += 1;
if self.changes_seen < self.ema_len {
return None;
}
let mut extra = HashMap::new();
extra.insert("raw".to_string(), raw);
Some(IndicatorOutput::with_extra(line, extra))
}
fn reset(&mut self) {
self.prev_close = None;
self.ema.reset();
self.changes_seen = 0;
}
}