1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
11pub struct VolumePriceTrend {
12 values: IndicatorValues,
13 stepper: Stepper<(Option<f64>, f64)>,
14 color: Color32,
15 visible: bool,
16}
17
18impl VolumePriceTrend {
19 pub fn new() -> Self {
21 Self {
22 values: IndicatorValues::new(),
23 stepper: Stepper::default(),
24 color: palette::BLUE, visible: true,
26 }
27 }
28
29 pub fn with_color(mut self, color: Color32) -> Self {
31 self.color = color;
32 self
33 }
34}
35
36impl Default for VolumePriceTrend {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl Indicator for VolumePriceTrend {
43 fn source(&self) -> Option<crate::studies::StudySource> {
44 Some(crate::studies::StudySource::Price(
45 crate::model::PriceSource::Close,
46 ))
47 }
48
49 fn value_format(&self) -> crate::scales::StudyFormat {
50 crate::scales::StudyFormat::Volume
51 }
52
53 fn name(&self) -> &str {
54 "VPT"
55 }
56
57 fn desc(&self) -> &str {
58 "Volume Price Trend - Volume weighted by price change"
59 }
60
61 fn calculate(&mut self, data: &[Bar]) {
62 self.update(data, 0);
63 }
64
65 fn update(&mut self, data: &[Bar], from: usize) {
66 self.stepper
67 .run(data, from, &mut self.values, |state, bar| {
68 if !bar.close.is_finite() || !bar.volume.is_finite() {
69 return IndicatorValue::None;
70 }
71 if let Some(previous) = state.0.filter(|p| *p != 0.0) {
72 let change = (bar.close - previous) / previous;
73 state.1 += bar.volume * change;
74 }
75 state.0 = Some(bar.close);
76 IndicatorValue::Single(state.1)
77 });
78 }
79
80 fn is_incremental(&self) -> bool {
81 true
82 }
83
84 fn values(&self) -> &IndicatorValues {
85 &self.values
86 }
87
88 fn colors(&self) -> Vec<Color32> {
89 vec![self.color]
90 }
91
92 fn set_colors(&mut self, colors: Vec<Color32>) {
93 if !colors.is_empty() {
94 self.color = colors[0];
95 }
96 }
97
98 fn is_overlay(&self) -> bool {
99 false
100 }
101
102 fn is_visible(&self) -> bool {
103 self.visible
104 }
105
106 fn set_visible(&mut self, visible: bool) {
107 self.visible = visible;
108 }
109
110 fn clone_box(&self) -> Box<dyn Indicator> {
111 Box::new(self.clone())
112 }
113
114 fn line_names(&self) -> Vec<String> {
115 vec!["VPT".to_string()]
116 }
117}
118
119const SIGNAL_LEN: f64 = 255.0;
121
122fn volume_index(
124 stepper: &mut Stepper<(Option<[f64; 2]>, f64, f64)>,
125 values: &mut IndicatorValues,
126 data: &[Bar],
127 from: usize,
128 counts: fn(f64, f64) -> bool,
129) {
130 stepper.run(data, from, values, |state, bar| {
131 if !bar.close.is_finite() || !bar.volume.is_finite() {
132 return IndicatorValue::None;
133 }
134 if let Some([close, volume]) = state.0 {
135 if counts(bar.volume, volume) && close != 0.0 {
136 state.1 += state.1 * ((bar.close - close) / close);
137 }
138 state.2 += (state.1 - state.2) * (2.0 / (SIGNAL_LEN + 1.0));
139 } else {
140 state.1 = 1000.0;
141 state.2 = 1000.0;
142 }
143 state.0 = Some([bar.close, bar.volume]);
144 IndicatorValue::Multiple(vec![state.1, state.2])
145 });
146}
147
148#[derive(Clone)]
154pub struct NegativeVolumeIndex {
155 values: IndicatorValues,
156 stepper: Stepper<(Option<[f64; 2]>, f64, f64)>,
157 color: Color32,
158 signal_color: Color32,
159 visible: bool,
160}
161
162impl NegativeVolumeIndex {
163 pub fn new() -> Self {
165 Self {
166 values: IndicatorValues::new(),
167 stepper: Stepper::default(),
168 color: palette::BLUE,
169 signal_color: palette::WARM_ORANGE,
170 visible: true,
171 }
172 }
173
174 pub fn with_color(mut self, color: Color32) -> Self {
176 self.color = color;
177 self
178 }
179}
180
181impl Default for NegativeVolumeIndex {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl Indicator for NegativeVolumeIndex {
188 fn source(&self) -> Option<crate::studies::StudySource> {
189 Some(crate::studies::StudySource::Price(
190 crate::model::PriceSource::Close,
191 ))
192 }
193
194 fn name(&self) -> &str {
195 "NVI"
196 }
197
198 fn desc(&self) -> &str {
199 "Negative Volume Index - Tracks price on low volume days"
200 }
201
202 fn calculate(&mut self, data: &[Bar]) {
203 self.update(data, 0);
204 }
205
206 fn update(&mut self, data: &[Bar], from: usize) {
207 volume_index(
208 &mut self.stepper,
209 &mut self.values,
210 data,
211 from,
212 |volume, prev| volume < prev,
213 );
214 }
215
216 fn is_incremental(&self) -> bool {
217 true
218 }
219
220 fn values(&self) -> &IndicatorValues {
221 &self.values
222 }
223
224 fn colors(&self) -> Vec<Color32> {
225 vec![self.color, self.signal_color]
226 }
227
228 fn set_colors(&mut self, colors: Vec<Color32>) {
229 if let Some(&color) = colors.first() {
230 self.color = color;
231 }
232 if let Some(&color) = colors.get(1) {
233 self.signal_color = color;
234 }
235 }
236
237 fn is_overlay(&self) -> bool {
238 false
239 }
240
241 fn is_visible(&self) -> bool {
242 self.visible
243 }
244
245 fn set_visible(&mut self, visible: bool) {
246 self.visible = visible;
247 }
248
249 fn clone_box(&self) -> Box<dyn Indicator> {
250 Box::new(self.clone())
251 }
252
253 fn line_count(&self) -> usize {
254 2
255 }
256
257 fn line_names(&self) -> Vec<String> {
258 vec!["NVI".to_string(), "Signal".to_string()]
259 }
260}
261
262#[derive(Clone)]
268pub struct PositiveVolumeIndex {
269 values: IndicatorValues,
270 stepper: Stepper<(Option<[f64; 2]>, f64, f64)>,
271 color: Color32,
272 signal_color: Color32,
273 visible: bool,
274}
275
276impl PositiveVolumeIndex {
277 pub fn new() -> Self {
279 Self {
280 values: IndicatorValues::new(),
281 stepper: Stepper::default(),
282 color: palette::BLUE,
283 signal_color: palette::WARM_ORANGE,
284 visible: true,
285 }
286 }
287
288 pub fn with_color(mut self, color: Color32) -> Self {
290 self.color = color;
291 self
292 }
293}
294
295impl Default for PositiveVolumeIndex {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl Indicator for PositiveVolumeIndex {
302 fn source(&self) -> Option<crate::studies::StudySource> {
303 Some(crate::studies::StudySource::Price(
304 crate::model::PriceSource::Close,
305 ))
306 }
307
308 fn name(&self) -> &str {
309 "PVI"
310 }
311
312 fn desc(&self) -> &str {
313 "Positive Volume Index - Tracks price on high volume days"
314 }
315
316 fn calculate(&mut self, data: &[Bar]) {
317 self.update(data, 0);
318 }
319
320 fn update(&mut self, data: &[Bar], from: usize) {
321 volume_index(
322 &mut self.stepper,
323 &mut self.values,
324 data,
325 from,
326 |volume, prev| volume > prev,
327 );
328 }
329
330 fn is_incremental(&self) -> bool {
331 true
332 }
333
334 fn values(&self) -> &IndicatorValues {
335 &self.values
336 }
337
338 fn colors(&self) -> Vec<Color32> {
339 vec![self.color, self.signal_color]
340 }
341
342 fn set_colors(&mut self, colors: Vec<Color32>) {
343 if let Some(&color) = colors.first() {
344 self.color = color;
345 }
346 if let Some(&color) = colors.get(1) {
347 self.signal_color = color;
348 }
349 }
350
351 fn is_overlay(&self) -> bool {
352 false
353 }
354
355 fn is_visible(&self) -> bool {
356 self.visible
357 }
358
359 fn set_visible(&mut self, visible: bool) {
360 self.visible = visible;
361 }
362
363 fn clone_box(&self) -> Box<dyn Indicator> {
364 Box::new(self.clone())
365 }
366
367 fn line_count(&self) -> usize {
368 2
369 }
370
371 fn line_names(&self) -> Vec<String> {
372 vec!["PVI".to_string(), "Signal".to_string()]
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use chrono::Utc;
380
381 fn make_bar(close: f64, volume: f64) -> Bar {
382 Bar {
383 time: Utc::now(),
384 open: close,
385 high: close,
386 low: close,
387 close,
388 volume,
389 colors: None,
390 payload: None,
391 }
392 }
393
394 #[test]
395 fn test_vpt() {
396 let mut vpt = VolumePriceTrend::new();
397
398 let data = vec![
399 make_bar(100.0, 1000.0),
400 make_bar(105.0, 1200.0), make_bar(102.0, 800.0), ];
403
404 vpt.calculate(&data);
405
406 assert_eq!(vpt.values.len(), 3);
407
408 let IndicatorValue::Single(v) = vpt.values.to_rows()[0] else {
410 panic!("no value: {:?}", vpt.values.to_rows()[0]);
411 };
412 assert_eq!(v, 0.0);
413 }
414
415 #[test]
422 fn nvi_pvi_match_published_definition() {
423 let data: Vec<Bar> = [
426 (100.0, 1000.0),
427 (105.0, 800.0),
428 (108.0, 1200.0),
429 (0.0, 1200.0),
430 (50.0, 900.0),
431 (0.0, 900.0),
432 (40.0, 1100.0),
433 (44.0, 700.0),
434 (33.0, 1400.0),
435 ]
436 .iter()
437 .map(|&(close, volume)| make_bar(close, volume))
438 .collect();
439 let nvi = [
440 (1000.0, 1000.0),
441 (1050.0, 1000.390625),
442 (1050.0, 1000.7781982421875),
443 (1050.0, 1001.1627435684204),
444 (1050.0, 1001.5442846342921),
445 (1050.0, 1001.9228449105867),
446 (1050.0, 1002.2984476847228),
447 (1155.0, 1003.4914285621859),
448 (1155.0, 1004.6750892765438),
449 ];
450 let pvi = [
451 (1000.0, 1000.0),
452 (1000.0, 1000.0),
453 (1028.5714285714287, 1000.2232142857143),
454 (1028.5714285714287, 1000.4446847098214),
455 (1028.5714285714287, 1000.6644248962402),
456 (1028.5714285714287, 1000.8824483624527),
457 (1028.5714285714287, 1001.0987685203353),
458 (1028.5714285714287, 1001.3133986769844),
459 (771.4285714285714, 999.5174234641062),
460 ];
461 let cases = [
462 (
463 Box::new(NegativeVolumeIndex::new()) as Box<dyn Indicator>,
464 nvi,
465 ),
466 (Box::new(PositiveVolumeIndex::new()), pvi),
467 ];
468 for (mut study, expected) in cases {
469 study.calculate(&data);
470 assert_eq!(study.line_count(), 2, "{}", study.name());
471 assert_eq!(study.values().len(), expected.len(), "{}", study.name());
472 for (bar, (value, &(index, signal))) in
473 study.values().to_rows().iter().zip(&expected).enumerate()
474 {
475 let IndicatorValue::Multiple(lines) = value else {
476 panic!(
477 "{} bar {bar}: expected index and signal, got {value:?}",
478 study.name()
479 );
480 };
481 assert!(
482 (lines[0] - index).abs() < 1e-12 && (lines[1] - signal).abs() < 1e-12,
483 "{} bar {bar}: got {lines:?}, expected [{index}, {signal}]",
484 study.name()
485 );
486 }
487 }
488 }
489}