1use crate::model::TimeframeUnit;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, HashSet};
13use std::ops::RangeInclusive;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum HandlePos {
22 Start,
24 End,
26 Middle,
28 TopLeft,
30 TopRight,
32 BottomLeft,
34 BottomRight,
36 Top,
38 Bottom,
40 Left,
42 Right,
44 Point(usize),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
51pub enum ArrowStyle {
52 None,
54 #[default]
56 Arrow,
57 OpenArrow,
59 Circle,
61 Square,
63 Diamond,
65}
66
67impl ArrowStyle {
68 #[inline]
70 pub fn is_visible(&self) -> bool {
71 !matches!(self, ArrowStyle::None)
72 }
73
74 #[inline]
76 pub fn size_multiplier(&self) -> f32 {
77 match self {
78 ArrowStyle::None => 0.0,
79 ArrowStyle::Arrow | ArrowStyle::OpenArrow => 4.0,
80 ArrowStyle::Circle => 3.0,
81 ArrowStyle::Square => 3.0,
82 ArrowStyle::Diamond => 3.5,
83 }
84 }
85
86 pub fn all() -> &'static [ArrowStyle] {
88 &[
89 ArrowStyle::None,
90 ArrowStyle::Arrow,
91 ArrowStyle::OpenArrow,
92 ArrowStyle::Circle,
93 ArrowStyle::Square,
94 ArrowStyle::Diamond,
95 ]
96 }
97}
98
99pub use crate::config::FontWeight;
100
101#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
109pub struct TimeframeVisibility {
110 pub show_on_all: bool,
112 #[serde(serialize_with = "serialize_sorted")]
114 pub visible_timeframes: HashSet<String>,
115 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
118 pub ranges: BTreeMap<TimeframeUnit, RangeInclusive<u64>>,
119}
120
121fn serialize_sorted<S: serde::Serializer>(
123 set: &HashSet<String>,
124 serializer: S,
125) -> Result<S::Ok, S::Error> {
126 serializer.collect_seq(set.iter().collect::<std::collections::BTreeSet<_>>())
127}
128
129fn interval_parts(label: &str) -> Option<(u64, TimeframeUnit)> {
131 let split = label.find(|c: char| !c.is_ascii_digit())?;
132 let unit = match &label[split..] {
133 "t" => TimeframeUnit::Tick,
134 "v" => TimeframeUnit::Volume,
135 "r" => TimeframeUnit::Range,
136 "delta" => TimeframeUnit::Delta,
137 "ms" => TimeframeUnit::Millisecond,
138 "s" => TimeframeUnit::Second,
139 "min" => TimeframeUnit::Minute,
140 "h" => TimeframeUnit::Hour,
141 "D" => TimeframeUnit::Day,
142 "W" => TimeframeUnit::Week,
143 "M" => TimeframeUnit::Month,
144 _ => return None,
145 };
146 Some((label[..split].parse().ok()?, unit))
147}
148
149impl TimeframeVisibility {
150 pub fn all() -> Self {
152 Self {
153 show_on_all: true,
154 visible_timeframes: HashSet::new(),
155 ranges: BTreeMap::new(),
156 }
157 }
158
159 pub fn specific(timeframes: &[&str]) -> Self {
161 Self {
162 show_on_all: false,
163 visible_timeframes: timeframes.iter().map(|s| (*s).to_string()).collect(),
164 ranges: BTreeMap::new(),
165 }
166 }
167
168 #[inline]
170 pub fn is_visible_on(&self, timeframe: &str) -> bool {
171 self.show_on_all
172 || self.visible_timeframes.contains(timeframe)
173 || (!self.ranges.is_empty()
174 && interval_parts(timeframe).is_some_and(|(value, unit)| {
175 self.ranges
176 .get(&unit)
177 .is_some_and(|range| range.contains(&value))
178 }))
179 }
180
181 pub fn range(&self, unit: TimeframeUnit) -> Option<RangeInclusive<u64>> {
184 self.visible_timeframes
185 .iter()
186 .filter_map(|label| interval_parts(label))
187 .filter(|&(_, of)| of == unit)
188 .map(|(value, _)| value..=value)
189 .chain(self.ranges.get(&unit).cloned())
190 .reduce(|a, b| *a.start().min(b.start())..=*a.end().max(b.end()))
191 }
192
193 pub fn set_range(&mut self, unit: TimeframeUnit, range: Option<RangeInclusive<u64>>) {
197 self.visible_timeframes
198 .retain(|label| interval_parts(label).is_none_or(|(_, of)| of != unit));
199 if let Some(range) = range {
200 self.show_on_all = false;
201 self.ranges.insert(unit, range);
202 } else {
203 self.ranges.remove(&unit);
204 }
205 }
206
207 pub fn add_timeframe(&mut self, timeframe: &str) {
209 self.show_on_all = false;
210 self.visible_timeframes.insert(timeframe.to_string());
211 }
212
213 pub fn remove_timeframe(&mut self, timeframe: &str) {
215 self.visible_timeframes.remove(timeframe);
216 if self.visible_timeframes.is_empty() && self.ranges.is_empty() {
217 self.show_on_all = true;
218 }
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct DrawingOptions {
225 pub snap_to_price: bool,
227 pub snap_to_time: bool,
229 pub snap_distance: f32,
231 pub magnet_mode: bool,
233 pub magnet_distance: f32,
235 pub show_handles: bool,
237 pub handle_size: f32,
239 pub handle_color: [u8; 4],
241 pub handle_sel_color: [u8; 4],
243}
244
245impl Default for DrawingOptions {
246 fn default() -> Self {
247 Self {
248 snap_to_price: true,
249 snap_to_time: true,
250 snap_distance: 10.0,
251 magnet_mode: false,
252 magnet_distance: 15.0,
253 show_handles: true,
254 handle_size: 6.0,
255 handle_color: [255, 255, 255, 200],
256 handle_sel_color: [242, 54, 69, 255], }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct FibonacciLevel {
264 pub value: f32,
266 #[serde(default)]
268 pub label: String,
269 pub color: [u8; 4],
271 pub visible: bool,
273 pub show_price: bool,
275}
276
277impl FibonacciLevel {
278 pub fn new(value: f32, label: &str, color: [u8; 4]) -> Self {
282 Self {
283 value,
284 label: label.to_string(),
285 color,
286 visible: true,
287 show_price: true,
288 }
289 }
290
291 pub fn label(&self, as_percent: bool) -> String {
293 let numeric = self
294 .label
295 .strip_suffix('%')
296 .is_some_and(|v| v.parse::<f32>().is_ok());
297 if as_percent && !self.label.is_empty() && !numeric {
298 return self.label.clone();
299 }
300 let text = if as_percent {
301 format!("{:.4}", f64::from(self.value) * 100.0)
302 } else {
303 format!("{:.6}", self.value)
304 };
305 let text = text.trim_end_matches('0').trim_end_matches('.');
306 if as_percent {
307 format!("{text}%")
308 } else {
309 text.to_owned()
310 }
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub struct FibonacciConfig {
317 pub levels: Vec<FibonacciLevel>,
319 #[serde(default = "fib_level_width")]
321 pub level_width: f32,
322 #[serde(default)]
324 pub level_style: crate::model::LineStyle,
325 pub show_background: bool,
327 pub background_opacity: u8,
329 pub extend_left: bool,
331 pub extend_right: bool,
333 pub reverse: bool,
335 #[serde(default)]
338 pub show_prices: Option<bool>,
339 #[serde(default)]
341 pub show_levels: Option<bool>,
342 #[serde(default = "percent_levels")]
344 pub levels_as_percent: bool,
345 #[serde(default)]
347 pub labels_on_left: bool,
348 #[serde(default)]
350 pub logarithmic: bool,
351}
352
353fn fib_level_width() -> f32 {
354 2.0
355}
356
357fn percent_levels() -> bool {
358 true
359}
360
361impl Default for FibonacciConfig {
362 fn default() -> Self {
364 Self {
365 levels: vec![
366 FibonacciLevel::new(0.0, "0%", [120, 123, 134, 255]),
367 FibonacciLevel::new(0.236, "23.6%", [242, 54, 69, 255]),
368 FibonacciLevel::new(0.382, "38.2%", [255, 152, 0, 255]),
369 FibonacciLevel::new(0.5, "50%", [76, 175, 80, 255]),
370 FibonacciLevel::new(0.618, "61.8%", [33, 150, 243, 255]),
371 FibonacciLevel::new(0.786, "78.6%", [156, 39, 176, 255]),
372 FibonacciLevel::new(1.0, "100%", [120, 123, 134, 255]),
373 ],
374 level_width: fib_level_width(),
375 level_style: crate::model::LineStyle::Solid,
376 show_background: false,
377 background_opacity: 30,
378 extend_left: false,
379 extend_right: false,
380 reverse: false,
381 logarithmic: false,
382 show_prices: None,
383 show_levels: None,
384 levels_as_percent: false,
385 labels_on_left: true,
386 }
387 }
388}
389
390impl FibonacciConfig {
391 pub fn extension_default() -> Self {
393 Self {
394 levels: vec![
395 FibonacciLevel::new(0.0, "0%", [120, 123, 134, 255]),
396 FibonacciLevel::new(0.618, "61.8%", [33, 150, 243, 255]),
397 FibonacciLevel::new(1.0, "100%", [120, 123, 134, 255]),
398 FibonacciLevel::new(1.272, "127.2%", [242, 54, 69, 255]),
399 FibonacciLevel::new(1.618, "161.8%", [255, 152, 0, 255]),
400 FibonacciLevel::new(2.0, "200%", [76, 175, 80, 255]),
401 FibonacciLevel::new(2.618, "261.8%", [156, 39, 176, 255]),
402 ],
403 level_width: fib_level_width(),
404 level_style: crate::model::LineStyle::Solid,
405 show_background: false,
406 background_opacity: 30,
407 extend_left: false,
408 extend_right: false,
409 reverse: false,
410 logarithmic: false,
411 show_prices: None,
412 show_levels: None,
413 levels_as_percent: false,
414 labels_on_left: true,
415 }
416 }
417
418 pub fn for_tool(tool: super::DrawingToolType) -> Self {
423 use super::DrawingToolType as T;
424 use super::drawing::{BLUE, CYAN, GREEN, GREY, ORANGE, PINK, PURPLE, RED, TEAL};
425 let table = match tool {
426 T::FibonacciTimeZones => [0.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0, 55.0, 89.0]
427 .map(|value| (value, if value == 0.0 { GREY } else { BLUE }))
428 .to_vec(),
429 T::TrendBasedFibTime => vec![
430 (0.0, GREY),
431 (0.382, RED),
432 (0.5, [129, 199, 132, 255]),
433 (0.618, GREEN),
434 (1.0, TEAL),
435 (1.382, CYAN),
436 (1.618, GREY),
437 (2.0, BLUE),
438 (2.382, PINK),
439 (2.618, PURPLE),
440 (3.0, [103, 58, 183, 255]),
441 ],
442 T::FibonacciSpeedFan => vec![
443 (0.0, GREY),
444 (0.25, ORANGE),
445 (0.382, CYAN),
446 (0.5, GREEN),
447 (0.618, TEAL),
448 (0.75, BLUE),
449 (1.0, GREY),
450 ],
451 T::FibonacciCircles | T::FibonacciSpeedResistanceArcs => vec![
452 (0.236, RED),
453 (0.382, ORANGE),
454 (0.5, TEAL),
455 (0.618, GREEN),
456 (0.786, CYAN),
457 (1.0, GREY),
458 (1.618, BLUE),
459 (2.618, PINK),
460 (3.618, BLUE),
461 (4.236, PINK),
462 (4.618, RED),
463 ],
464 T::FibonacciRetracement
465 | T::FibonacciExtension
466 | T::FibonacciChannel
467 | T::FibonacciWedge => {
468 let mut levels: Vec<(f32, _, _)> = [
469 (0.0, GREY),
470 (0.236, RED),
471 (0.382, ORANGE),
472 (0.5, GREEN),
473 (0.618, TEAL),
474 (0.786, CYAN),
475 (1.0, GREY),
476 (1.618, BLUE),
477 (2.618, RED),
478 (3.618, PURPLE),
479 (4.236, PINK),
480 ]
481 .map(|(value, color)| (value, color, true))
482 .to_vec();
483 if tool == T::FibonacciWedge {
484 levels.retain(|level| level.0 != 0.0);
485 levels.push((4.618, RED, true));
486 for level in &mut levels {
487 level.2 = level.0 <= 1.0;
488 }
489 } else {
490 let spare = [
492 1.272, 1.414, 2.0, 2.272, 2.414, 3.0, 3.272, 3.414, 4.0, 4.272, 4.414,
493 4.618, 4.764,
494 ];
495 let colors = [RED, ORANGE, GREEN, TEAL, CYAN, BLUE, PURPLE, PINK];
496 levels.extend(
497 (spare.into_iter().zip(colors.into_iter().cycle()))
498 .map(|(value, color)| (value, color, false)),
499 );
500 levels.sort_by(|a, b| a.0.total_cmp(&b.0));
501 }
502 return Self {
503 levels: levels
504 .into_iter()
505 .map(|(value, color, visible)| FibonacciLevel {
506 visible,
507 ..FibonacciLevel::new(value, "", color)
508 })
509 .collect(),
510 show_background: true,
511 background_opacity: 51,
512 show_levels: (tool == T::FibonacciWedge).then_some(true),
513 ..Self::default()
514 };
515 }
516 _ => return Self::default(),
517 };
518 Self {
519 levels: table
520 .into_iter()
521 .map(|(value, color)| {
522 let mut level = FibonacciLevel::new(value, "", color);
523 level.visible = tool != T::TrendBasedFibTime || value != 0.5;
524 level
525 })
526 .collect(),
527 show_background: tool != T::FibonacciTimeZones,
528 background_opacity: 51,
529 levels_as_percent: false,
530 ..Self::default()
531 }
532 }
533
534 pub(crate) fn label_text(&self, level: &FibonacciLevel, price: Option<&str>) -> String {
536 if self.show_prices.is_none()
537 && self.show_levels.is_none()
538 && !level.show_price
539 && level.label.is_empty()
540 {
541 return String::new();
542 }
543 let value = self
544 .show_levels
545 .unwrap_or(true)
546 .then(|| level.label(self.levels_as_percent));
547 let price = price.filter(|_| self.show_prices.unwrap_or(level.show_price));
548 match (value, price) {
549 (Some(value), Some(price)) => format!("{value} ({price})"),
550 (Some(value), None) => value,
551 (None, Some(price)) => price.to_owned(),
552 (None, None) => String::new(),
553 }
554 }
555
556 pub fn visible_levels(&self) -> impl Iterator<Item = &FibonacciLevel> {
558 self.levels.iter().filter(|l| l.visible)
559 }
560
561 pub(crate) fn retracement_position(&self, start: f64, end: f64, value: f64) -> f64 {
567 let (from, to) = if self.reverse {
568 (start, end)
569 } else {
570 (end, start)
571 };
572 if self.logarithmic && from > 0.0 && to > 0.0 {
573 (from.ln() + (to.ln() - from.ln()) * value).exp()
574 } else {
575 from + (to - from) * value
576 }
577 }
578
579 pub(crate) fn level_prices(
583 &self,
584 start: f64,
585 end: f64,
586 extension: bool,
587 origin: f64,
588 ) -> impl Iterator<Item = f64> + '_ {
589 self.visible_levels().map(move |level| {
590 let value = f64::from(level.value);
591 if extension {
592 let value = if self.reverse { -value } else { value };
593 if self.logarithmic && start > 0.0 && end > 0.0 && origin > 0.0 {
594 (origin.ln() + (end.ln() - start.ln()) * value).exp()
595 } else {
596 origin + (end - start) * value
597 }
598 } else {
599 self.retracement_position(start, end, value)
600 }
601 })
602 }
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
607#[serde(try_from = "RegressionTrendFields")]
608pub struct RegressionTrendConfig {
609 deviations: f64,
610}
611
612#[derive(Deserialize)]
614struct RegressionTrendFields {
615 deviations: f64,
616}
617
618impl TryFrom<RegressionTrendFields> for RegressionTrendConfig {
619 type Error = &'static str;
620
621 fn try_from(fields: RegressionTrendFields) -> Result<Self, Self::Error> {
622 Self::new(fields.deviations).ok_or("regression trend settings out of bounds")
623 }
624}
625
626impl RegressionTrendConfig {
627 pub fn new(deviations: f64) -> Option<Self> {
630 (deviations > 0.0 && deviations <= 10.0).then_some(Self { deviations })
631 }
632
633 pub fn deviations(&self) -> f64 {
635 self.deviations
636 }
637}
638
639impl Default for RegressionTrendConfig {
640 fn default() -> Self {
642 Self { deviations: 2.0 }
643 }
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
650#[serde(default)]
651pub struct PositionSizing {
652 pub point_value: f64,
654 pub lot_size: f64,
656 pub quantity_step: f64,
658 pub leverage: f64,
660}
661
662impl Default for PositionSizing {
663 fn default() -> Self {
664 Self {
665 point_value: 1.0,
666 lot_size: 1.0,
667 quantity_step: 0.0,
668 leverage: 1.0,
669 }
670 }
671}
672
673impl PositionSizing {
674 pub fn is_valid(&self) -> bool {
676 [
677 self.point_value,
678 self.lot_size,
679 self.leverage,
680 self.point_value * self.lot_size,
681 ]
682 .into_iter()
683 .all(|v| v.is_finite() && v > 0.0)
684 && self.quantity_step.is_finite()
685 && self.quantity_step >= 0.0
686 }
687
688 pub fn required_capital(&self, entry: f64, quantity: f64) -> Option<f64> {
691 if !self.is_valid() || !quantity.is_finite() || quantity <= 0.0 {
692 return None;
693 }
694 let capital = entry.abs() * self.point_value * self.lot_size * quantity / self.leverage;
695 capital.is_finite().then_some(capital)
696 }
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
701pub enum PositionSize {
702 Quantity(f64),
704 RiskAmount(f64),
706 RiskPercent {
708 account_size: f64,
710 percent: f64,
712 },
713}
714
715impl PositionSize {
716 pub fn quantity_with_sizing(
720 &self,
721 entry: f64,
722 stop: f64,
723 sizing: &PositionSizing,
724 ) -> Option<f64> {
725 if !sizing.is_valid() {
726 return None;
727 }
728 let mut quantity = self.quantity(entry, stop)?;
729 if !matches!(self, Self::Quantity(_)) {
730 quantity /= sizing.point_value * sizing.lot_size;
731 }
732 if sizing.quantity_step > 0.0 {
733 let steps = quantity / sizing.quantity_step;
734 let nearest = steps.round();
736 let steps = if (steps - nearest).abs() <= 4.0 * f64::EPSILON * steps.abs() {
737 nearest
738 } else {
739 steps.floor()
740 };
741 quantity = steps * sizing.quantity_step;
742 }
743 (quantity.is_finite() && quantity > 0.0).then_some(quantity)
744 }
745
746 pub fn quantity(&self, entry: f64, stop: f64) -> Option<f64> {
752 let positive = |value: f64| value.is_finite() && value > 0.0;
753 let risk = match *self {
754 Self::Quantity(quantity) => return positive(quantity).then_some(quantity),
755 Self::RiskAmount(amount) => amount,
756 Self::RiskPercent {
757 account_size,
758 percent,
759 } if positive(account_size) && positive(percent) && percent <= 100.0 => {
760 account_size * (percent / 100.0)
761 }
762 Self::RiskPercent { .. } => return None,
763 };
764 let quantity = risk / (entry - stop).abs();
765 positive(quantity).then_some(quantity)
766 }
767}
768
769impl Default for PositionSize {
770 fn default() -> Self {
772 Self::Quantity(1.0)
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use crate::data::VolumeProfileConfig;
780 use crate::model::Timeframe;
781
782 #[test]
786 fn position_size_quantity() {
787 let percent = |account_size, percent| PositionSize::RiskPercent {
788 account_size,
789 percent,
790 };
791 for (size, entry, stop, want) in [
792 (PositionSize::Quantity(3.0), 50.0, 48.0, Some(3.0)),
793 (PositionSize::RiskAmount(100.0), 50.0, 48.0, Some(50.0)),
794 (percent(10_000.0, 1.0), 50.0, 52.5, Some(40.0)),
795 (percent(2_000.0, 2.5), 1.1, 1.0975, Some(19999.99999999865)),
796 (PositionSize::RiskAmount(100.0), 50.0, 50.0, None),
797 (PositionSize::RiskAmount(-100.0), 50.0, 48.0, None),
798 (PositionSize::Quantity(f64::NAN), 50.0, 48.0, None),
799 (percent(10_000.0, 150.0), 50.0, 48.0, None),
800 (percent(-10_000.0, -1.0), 50.0, 48.0, None),
801 (percent(1e308, 100.0), 1.0, 0.0, Some(1e308)),
802 ] {
803 let got = size.quantity(entry, stop);
804 assert!(
805 got.zip(want)
806 .map_or(got == want, |(got, want)| (got - want).abs() < 1e-6),
807 "{size:?}: got {got:?}, want {want:?}"
808 );
809 }
810
811 let sizing = PositionSizing {
813 point_value: 50.0,
814 lot_size: 10.0,
815 quantity_step: 0.01,
816 leverage: 5.0,
817 };
818 for (size, stop, expected) in [
819 (PositionSize::RiskAmount(230.0), 97.5, Some(0.18)),
820 (PositionSize::RiskAmount(230.0), 102.5, Some(0.18)),
821 (
822 PositionSize::RiskPercent {
823 account_size: 23000.0,
824 percent: 1.0,
825 },
826 97.5,
827 Some(0.18),
828 ),
829 (PositionSize::Quantity(0.189), 97.5, Some(0.18)),
830 (PositionSize::Quantity(0.29), 97.5, Some(0.29)),
831 (PositionSize::Quantity(0.009), 97.5, None),
832 (PositionSize::RiskAmount(230.0), 100.0, None),
833 ] {
834 let quantity = size.quantity_with_sizing(100.0, stop, &sizing);
835 assert_eq!(quantity, expected, "{size:?}, stop {stop}");
836 let unleveraged = PositionSizing {
837 leverage: 1.0,
838 ..sizing
839 };
840 assert_eq!(
841 quantity,
842 size.quantity_with_sizing(100.0, stop, &unleveraged)
843 );
844 }
845 assert_eq!(sizing.required_capital(100.0, 0.18), Some(1800.0));
846 assert_eq!(
847 PositionSize::Quantity(2.13).quantity_with_sizing(
848 100.0,
849 97.5,
850 &PositionSizing {
851 quantity_step: 0.25,
852 ..sizing
853 }
854 ),
855 Some(2.0)
856 );
857 for sizing in [
858 PositionSizing {
859 point_value: 0.0,
860 ..sizing
861 },
862 PositionSizing {
863 lot_size: -1.0,
864 ..sizing
865 },
866 PositionSizing {
867 point_value: 1e-200,
868 lot_size: 1e-200,
869 ..sizing
870 },
871 PositionSizing {
872 quantity_step: -0.01,
873 ..sizing
874 },
875 PositionSizing {
876 leverage: f64::NAN,
877 ..sizing
878 },
879 ] {
880 assert!(
881 PositionSize::default()
882 .quantity_with_sizing(100.0, 97.5, &sizing)
883 .is_none()
884 );
885 assert!(sizing.required_capital(100.0, 0.18).is_none());
886 }
887 }
888
889 #[test]
890 fn drawing_settings_reject_out_of_bounds_values() {
891 assert!(VolumeProfileConfig::new(1, 1.0).is_some());
892 assert!(VolumeProfileConfig::new(1000, 0.01).is_some());
893 for (rows, value_area) in [(0, 0.7), (1001, 0.7), (24, 0.0), (24, 1.01), (24, f64::NAN)] {
894 assert!(VolumeProfileConfig::new(rows, value_area).is_none());
895 }
896 for height in [0.0, -1.0, f64::INFINITY, f64::NAN] {
897 assert!(
898 VolumeProfileConfig::default()
899 .with_row_size(Some(height))
900 .is_none()
901 );
902 }
903 assert!(RegressionTrendConfig::new(10.0).is_some());
904 for deviations in [0.0, -1.0, 10.5, f64::INFINITY, f64::NAN] {
905 assert!(RegressionTrendConfig::new(deviations).is_none());
906 }
907 }
908
909 #[test]
912 fn test_timeframe_visibility() {
913 let mut vis = TimeframeVisibility::all();
914 assert!(vis.is_visible_on("1h"));
915 assert!(vis.is_visible_on("1D"));
916
917 vis.add_timeframe("1h");
918 assert!(vis.is_visible_on("1h"));
919 assert!(!vis.is_visible_on("1D"));
920
921 let n = |v| std::num::NonZeroU64::new(v).unwrap();
922 let mut vis = TimeframeVisibility::all();
923 vis.set_range(TimeframeUnit::Hour, Some(1..=4));
924 vis.set_range(TimeframeUnit::Minute, Some(1..=59));
925 vis.set_range(TimeframeUnit::Second, Some(1..=600));
926 vis.set_range(TimeframeUnit::Tick, Some(100..=1000));
927 for (timeframe, shown) in [
928 (Timeframe::Hour1, true), (Timeframe::Custom(n(2 * 3600)), true), (Timeframe::Hour4, true), (Timeframe::Custom(n(5 * 3600)), false), (Timeframe::Day1, false), (Timeframe::Min30, true), (Timeframe::Custom(n(90 * 60)), false), (Timeframe::Month1, false), (Timeframe::Sec30, true), (Timeframe::Ms500, false), (Timeframe::Ticks(n(99)), false),
939 (Timeframe::Ticks(n(100)), true),
940 (Timeframe::Ticks(n(1000)), true),
941 (Timeframe::Ticks(n(1001)), false),
942 (Timeframe::Range(n(100)), false), (Timeframe::Delta(n(100)), false), ] {
945 assert_eq!(vis.is_visible_on(&timeframe.as_str()), shown, "{timeframe}");
946 }
947 vis.show_on_all = true;
948 assert!(vis.is_visible_on("1D"));
949
950 let mut saved = TimeframeVisibility::specific(&["1h", "4h", "1D"]);
951 assert_eq!(saved.range(TimeframeUnit::Hour), Some(1..=4));
952 assert!(!saved.is_visible_on("2h"));
953 saved.set_range(TimeframeUnit::Hour, Some(2..=3));
954 assert_eq!(saved.range(TimeframeUnit::Hour), Some(2..=3));
955 assert!(
956 !saved.is_visible_on("1h") && saved.is_visible_on("2h") && saved.is_visible_on("1D")
957 );
958 saved.remove_timeframe("1D");
959 assert!(!saved.is_visible_on("1D") && saved.is_visible_on("3h"));
960 saved.set_range(TimeframeUnit::Hour, None);
961 assert_eq!(saved.range(TimeframeUnit::Hour), None);
962
963 let legacy = r#"{"show_on_all":false,"visible_timeframes":["4h"]}"#;
964 let loaded: TimeframeVisibility = serde_json::from_str(legacy).unwrap();
965 assert_eq!(loaded, TimeframeVisibility::specific(&["4h"]));
966 assert_eq!(serde_json::to_string(&loaded).unwrap(), legacy);
967 let mut hours = TimeframeVisibility::default();
968 hours.set_range(TimeframeUnit::Hour, Some(1..=4));
969 let json = serde_json::to_string(&hours).unwrap();
970 assert_eq!(
971 json,
972 r#"{"show_on_all":false,"visible_timeframes":[],"ranges":{"Hour":{"start":1,"end":4}}}"#
973 );
974 assert_eq!(
975 serde_json::from_str::<TimeframeVisibility>(&json).unwrap(),
976 hours
977 );
978 }
979
980 #[test]
982 fn fibonacci_retracement_position() {
983 for (reverse, value, want) in [
984 (false, 0.0, 200.0),
985 (false, 0.236, 176.4),
986 (false, 0.618, 138.2),
987 (false, 1.0, 100.0),
988 (true, 0.236, 123.6),
989 (true, 0.786, 178.6),
990 ] {
991 let config = FibonacciConfig {
992 reverse,
993 ..Default::default()
994 };
995 let got = config.retracement_position(100.0, 200.0, value);
996 assert!(
997 (got - want).abs() < 1e-9,
998 "reverse {reverse}, level {value}: got {got}, want {want}"
999 );
1000 }
1001 for (logarithmic, low, ratio, want) in [
1004 (true, 10.0, 0.236, 29.64831389524342),
1005 (true, 10.0, 0.5, 100.0),
1006 (true, 10.0, 0.618, 172.1868574986006),
1007 (false, 10.0, 0.236, 243.64),
1008 (false, 10.0, 0.5, 505.0),
1009 (false, 10.0, 0.618, 621.82),
1010 (true, 0.0, 0.5, 500.0),
1011 (true, -10.0, 0.5, 495.0),
1012 ] {
1013 for reverse in [false, true] {
1014 let config = FibonacciConfig {
1015 logarithmic,
1016 reverse,
1017 ..Default::default()
1018 };
1019 let (start, end) = if reverse {
1020 (low, 1000.0)
1021 } else {
1022 (1000.0, low)
1023 };
1024 let got = config.retracement_position(start, end, ratio);
1025 assert!(
1026 (got - want).abs() < 1e-9,
1027 "log {logarithmic}, reverse {reverse}, ratio {ratio}: {got} != {want}"
1028 );
1029 }
1030 }
1031 }
1032}