1use crate::model::Bar;
10use crate::studies::palette;
11use crate::studies::{
12 Indicator, IndicatorValues, InputKind, InputValue, StudyData, StudyDataRequest, StudyInput,
13};
14use egui::Color32;
15
16pub type AdvanceDeclineRatio = MarketBreadth<true, false>;
18pub type AdvanceDeclineLine = MarketBreadth<false, false>;
20pub type CumulativeVolumeIndex = MarketBreadth<false, true>;
22
23#[derive(Clone)]
29pub struct MarketBreadth<const RATIO: bool, const VOLUME: bool> {
30 advancing_symbol: String,
31 declining_symbol: String,
32 timeframe: Option<crate::model::Timeframe>,
33 clock: Option<crate::model::BarClock>,
34 wait_for_close: bool,
35 values: IndicatorValues,
36 source_values: Vec<Option<f64>>,
37 color: Color32,
38 visible: bool,
39}
40
41impl<const RATIO: bool, const VOLUME: bool> Default for MarketBreadth<RATIO, VOLUME> {
42 fn default() -> Self {
43 Self {
44 advancing_symbol: if VOLUME {
45 "ADVANCING_VOLUME"
46 } else {
47 "ADVANCING_ISSUES"
48 }
49 .into(),
50 declining_symbol: if VOLUME {
51 "DECLINING_VOLUME"
52 } else {
53 "DECLINING_ISSUES"
54 }
55 .into(),
56 timeframe: None,
57 clock: None,
58 wait_for_close: true,
59 values: IndicatorValues::new(),
60 source_values: Vec::new(),
61 color: if !RATIO && !VOLUME {
62 palette::LIGHT_BLUE
63 } else {
64 palette::BLUE
65 },
66 visible: true,
67 }
68 }
69}
70
71impl<const RATIO: bool, const VOLUME: bool> Indicator for MarketBreadth<RATIO, VOLUME> {
72 fn name(&self) -> &str {
73 if RATIO {
74 "Advance/Decline Ratio"
75 } else if VOLUME {
76 "Cumulative Volume Index"
77 } else {
78 "Advance/Decline Line"
79 }
80 }
81 fn desc(&self) -> &str {
82 "Market breadth from host-supplied advancing and declining series"
83 }
84 fn inputs(&self) -> Vec<StudyInput> {
85 let defaults = Self::default();
86 vec![
87 StudyInput::new(
88 "advancing_symbol",
89 "Advancing symbol",
90 InputValue::Symbol(self.advancing_symbol.clone()),
91 InputKind::Symbol,
92 )
93 .with_default(InputValue::Symbol(defaults.advancing_symbol)),
94 StudyInput::new(
95 "declining_symbol",
96 "Declining symbol",
97 InputValue::Symbol(self.declining_symbol.clone()),
98 InputKind::Symbol,
99 )
100 .with_default(InputValue::Symbol(defaults.declining_symbol)),
101 StudyInput::new(
102 "timeframe",
103 "Interval",
104 InputValue::Timeframe(self.timeframe),
105 InputKind::Timeframe,
106 )
107 .with_default(InputValue::Timeframe(None)),
108 StudyInput::new(
109 "wait_for_close",
110 "Wait for interval close",
111 InputValue::Toggle(self.wait_for_close),
112 InputKind::Toggle,
113 )
114 .with_default(InputValue::Toggle(true)),
115 ]
116 }
117 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
118 let schema = self
119 .inputs()
120 .into_iter()
121 .find(|input| input.id == id)
122 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
123 if !schema.accepts(&value) {
124 return Err(crate::studies::StudyError::invalid_input(id));
125 }
126 match (id, value) {
127 ("advancing_symbol", InputValue::Symbol(value)) => self.advancing_symbol = value,
128 ("declining_symbol", InputValue::Symbol(value)) => self.declining_symbol = value,
129 ("timeframe", InputValue::Timeframe(value)) => self.timeframe = value,
130 ("wait_for_close", InputValue::Toggle(value)) => self.wait_for_close = value,
131 _ => return Err(crate::studies::StudyError::invalid_input(id)),
132 }
133 self.values.clear();
134 Ok(())
135 }
136 fn data_requests(&self) -> Vec<StudyDataRequest> {
137 [&self.advancing_symbol, &self.declining_symbol]
138 .into_iter()
139 .map(|symbol| StudyDataRequest {
140 metric: None,
141 symbol: symbol.clone(),
142 timeframe: self.timeframe,
143 })
144 .collect()
145 }
146 fn set_bar_clock(&mut self, clock: &crate::model::BarClock) -> bool {
147 let changed = self.clock.as_ref() != Some(clock);
148 self.clock = Some(clock.clone());
149 changed
150 }
151 fn calculate(&mut self, data: &[Bar]) {
152 self.values = IndicatorValues::missing(data.len());
153 self.source_values.clear();
154 }
155 fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
156 let mut from = from.min(self.values.len()).min(data.len());
157 let requests = self.data_requests();
158 let Some(advancing) = sources.iter().find(|source| source.request == requests[0]) else {
159 self.calculate(data);
160 return;
161 };
162 let Some(declining) = sources.iter().find(|source| source.request == requests[1]) else {
163 self.calculate(data);
164 return;
165 };
166 if from == 0 || self.source_values.len() != advancing.bars.len() {
167 from = 0;
168 let mut total = 0.0;
169 self.source_values = advancing
170 .bars
171 .iter()
172 .map(|bar| {
173 let other = declining
174 .bar_at(bar.time)
175 .map(|at| declining.bars[at].close)?;
176 if !bar.close.is_finite() || !other.is_finite() || (RATIO && other == 0.0) {
177 return None;
178 }
179 let value = if RATIO {
180 bar.close / other
181 } else {
182 total + bar.close - other
183 };
184 if !value.is_finite() {
185 return None;
186 }
187 if !RATIO {
188 total = value;
189 }
190 Some(value)
191 })
192 .collect();
193 }
194 let from = from.saturating_sub(1);
196 self.values.truncate(from);
197 let clock = self.clock.as_ref().unwrap_or(&advancing.clock);
198 for (at, bar) in data.iter().enumerate().skip(from) {
199 let value = advancing
200 .bar_at(bar.time)
201 .and_then(|_| {
202 let forming_higher = at + 1 == data.len()
203 && self.wait_for_close
204 && clock
205 .timeframe()
206 .is_some_and(|tf| advancing.timeframe.duration_ms() > tf.duration_ms());
207 let close = if forming_higher {
208 bar.time
209 } else {
210 clock
211 .next_open(clock.bar_open(bar.time))
212 .unwrap_or(bar.time)
213 };
214 advancing.output_at(close, !self.wait_for_close && at + 1 == data.len())
215 })
216 .and_then(|at| self.source_values[at])
217 .unwrap_or(f64::NAN);
218 self.values.push_lines(&[value]);
219 }
220 }
221 fn is_incremental(&self) -> bool {
222 true
223 }
224 fn values(&self) -> &IndicatorValues {
225 &self.values
226 }
227 fn colors(&self) -> Vec<Color32> {
228 vec![self.color]
229 }
230 fn set_colors(&mut self, colors: Vec<Color32>) {
231 if let Some(color) = colors.first() {
232 self.color = *color;
233 }
234 }
235 fn is_overlay(&self) -> bool {
236 false
237 }
238 fn is_visible(&self) -> bool {
239 self.visible
240 }
241 fn set_visible(&mut self, visible: bool) {
242 self.visible = visible;
243 }
244 fn clone_box(&self) -> Box<dyn Indicator> {
245 Box::new(self.clone())
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::model::{BarClock, Timeframe};
253 use chrono::{DateTime, Duration};
254
255 #[test]
256 fn breadth_uses_paired_market_values_and_skips_missing_observations() {
257 let bars = |values: &[Option<f64>]| {
258 values
259 .iter()
260 .enumerate()
261 .filter_map(|(at, value)| {
262 let value = (*value)?;
263 Some(Bar::new(
264 DateTime::UNIX_EPOCH + Duration::days(at as i64),
265 value,
266 value,
267 value,
268 value,
269 999.0,
270 ))
271 })
272 .collect::<Vec<_>>()
273 };
274 let chart = bars(&[Some(1.0); 5]);
275 for (name, advancing, declining, expected) in [
276 (
277 "Advance/Decline Ratio",
278 [Some(10.0), Some(14.0), Some(8.0), None, Some(5.0)],
279 [Some(4.0), Some(7.0), Some(0.0), Some(3.0), Some(10.0)],
280 [Some(2.5), Some(2.0), None, None, Some(0.5)],
281 ),
282 (
283 "Advance/Decline Line",
284 [Some(10.0), Some(14.0), Some(8.0), None, Some(5.0)],
285 [Some(4.0), Some(7.0), Some(0.0), Some(3.0), Some(10.0)],
286 [Some(6.0), Some(13.0), Some(21.0), None, Some(16.0)],
287 ),
288 (
289 "Cumulative Volume Index",
290 [Some(30.0), Some(70.0), Some(50.0), None, Some(20.0)],
291 [Some(10.0), Some(20.0), Some(0.0), Some(5.0), Some(5.0)],
292 [Some(20.0), Some(70.0), Some(120.0), None, Some(135.0)],
293 ),
294 ] {
295 for stamp in [0, 6] {
296 let mut study = crate::studies::IndicatorFactory::new()
297 .create(name)
298 .unwrap();
299 let requests = study.data_requests();
300 let mut advancing = bars(&advancing);
301 let mut declining = bars(&declining);
302 for bar in &mut advancing {
303 bar.time += Duration::hours(stamp);
304 }
305 for bar in &mut declining {
306 bar.time += Duration::hours(stamp * 2);
307 }
308 let sources: Vec<_> = requests
309 .into_iter()
310 .zip([&advancing, &declining])
311 .map(|(request, bars)| StudyData {
312 request,
313 bars,
314 clock: BarClock::new(Timeframe::Day1),
315 instrument: None,
316 timeframe: Timeframe::Day1,
317 })
318 .collect();
319 study.calculate(&chart);
320 assert!((0..chart.len()).all(|at| study.values().value(0, at).is_none()));
321 study.set_bar_clock(&BarClock::new(Timeframe::Day1));
322 study.update_data(&chart, 0, &sources);
323 let actual: Vec<_> = (0..chart.len())
324 .map(|at| study.values().value(0, at))
325 .collect();
326 assert_eq!(actual, expected, "{name}");
327 let half_days: Vec<_> = (0..11)
328 .map(|at| {
329 Bar::new(
330 DateTime::UNIX_EPOCH + Duration::hours(at * 12),
331 1.0,
332 1.0,
333 1.0,
334 1.0,
335 1.0,
336 )
337 })
338 .collect();
339 study.set_bar_clock(&BarClock::new("12h".parse().unwrap()));
340 study.update_data(&half_days, 0, &sources);
341 let aligned = [
343 None,
344 expected[0],
345 expected[0],
346 expected[1],
347 expected[1],
348 expected[2],
349 None,
350 None,
351 expected[2],
352 expected[4],
353 None,
354 ];
355 let actual: Vec<_> = (0..11).map(|at| study.values().value(0, at)).collect();
356 assert_eq!(actual, aligned, "{name} confirmed intervals");
357 study.update_data(&half_days[..10], 0, &sources);
358 assert_eq!(
359 study.values().value(0, 9),
360 expected[2],
361 "{name} forming chart bar"
362 );
363 study
364 .set_input("wait_for_close", InputValue::Toggle(false))
365 .unwrap();
366 study.update_data(&half_days[..9], 0, &sources);
367 assert_eq!(
368 study.values().value(0, 8),
369 expected[4],
370 "{name} developing interval"
371 );
372 assert_eq!(study.values().value(0, 0), None);
373 }
374 }
375 }
376}