Skip to main content

egui_charts/studies/builtin/
time_price_opportunity.rs

1//! Market Profile letters of each session drawn beside the session's bars.
2
3use crate::model::{Bar, Instrument, TPOConfig, TPODisplayMode};
4use crate::studies::{Indicator, IndicatorValues, InputKind, InputValue, StudyInput};
5use egui::Color32;
6
7/// Display choices of the study, in input order. Saved choices are indices, so new ones go last.
8const DISPLAYS: [TPODisplayMode; 3] = [
9    TPODisplayMode::Letters,
10    TPODisplayMode::Blocks,
11    TPODisplayMode::Both,
12];
13
14/// Index of `mode` among [`DISPLAYS`].
15fn display_choice(mode: TPODisplayMode) -> InputValue {
16    InputValue::Choice(DISPLAYS.iter().position(|d| *d == mode).unwrap())
17}
18
19/// Each session's TPO profile on the price pane, starting at the session's first bar, with its
20/// point of control, value area and initial balance.
21///
22/// The chart builds the profiles of the whole sessions it shows from its bars and calendar with
23/// [`to_tpo_profiles`](crate::model::to_tpo_profiles), through the same code and cache as the
24/// TPO chart type, so the study's profile of a session equals the chart type's under the same
25/// settings. [`Indicator::tpo`] exposes those settings; the study has no plotted values.
26#[derive(Clone)]
27pub struct TimePriceOpportunity {
28    row_ticks: usize,
29    instrument_tick: Option<f64>,
30    config: TPOConfig,
31    values: IndicatorValues,
32    visible: bool,
33}
34
35impl TimePriceOpportunity {
36    /// Profiles of `period_minutes` periods, raised to the bar interval when shorter.
37    pub fn new(period_minutes: u32) -> Self {
38        Self {
39            row_ticks: 0,
40            instrument_tick: None,
41            config: TPOConfig {
42                // A zero row lets the chart fit the rows to the visible range.
43                tick_size: 0.0,
44                period_minutes: period_minutes.max(1),
45                ..Default::default()
46            },
47            values: IndicatorValues::new(),
48            visible: true,
49        }
50    }
51
52    fn update_tick_size(&mut self) {
53        self.config.tick_size = self
54            .instrument_tick
55            .map_or(0.0, |tick| tick * self.row_ticks as f64);
56        self.config.instrument_tick = self.instrument_tick;
57    }
58}
59
60impl Default for TimePriceOpportunity {
61    fn default() -> Self {
62        Self::new(30)
63    }
64}
65
66impl Indicator for TimePriceOpportunity {
67    fn name(&self) -> &str {
68        "Time Price Opportunity"
69    }
70    fn desc(&self) -> &str {
71        "Market Profile of each session beside its bars"
72    }
73    fn inputs(&self) -> Vec<StudyInput> {
74        let defaults = TPOConfig::default();
75        vec![
76            StudyInput::new(
77                "period",
78                "Period (minutes)",
79                InputValue::Length(self.config.period_minutes as usize),
80                InputKind::Length(1, 1440),
81            )
82            .with_default(InputValue::Length(30)),
83            StudyInput::new(
84                "row_ticks",
85                "Ticks per row (0 for auto)",
86                InputValue::Length(self.row_ticks),
87                InputKind::Length(0, 100_000),
88            )
89            .with_default(InputValue::Length(0)),
90            StudyInput::new(
91                "value_area",
92                "Value area (%)",
93                InputValue::Number(self.config.value_area_pct * 100.0),
94                InputKind::Number(1.0, 100.0),
95            )
96            .with_default(InputValue::Number(defaults.value_area_pct * 100.0)),
97            StudyInput::new(
98                "split_sessions",
99                "Split extended hours",
100                InputValue::Toggle(self.config.split_sessions),
101                InputKind::Toggle,
102            )
103            .with_default(InputValue::Toggle(defaults.split_sessions)),
104            StudyInput::new(
105                "single_prints",
106                "Single prints",
107                InputValue::Toggle(self.config.show_single_prints),
108                InputKind::Toggle,
109            )
110            .with_default(InputValue::Toggle(defaults.show_single_prints)),
111            StudyInput::new(
112                "display",
113                "Display",
114                display_choice(self.config.display_mode),
115                InputKind::Choice(vec![
116                    "Letters".into(),
117                    "Blocks".into(),
118                    "Letters and blocks".into(),
119                ]),
120            )
121            .with_default(display_choice(defaults.display_mode)),
122            StudyInput::new(
123                "sessions",
124                "Sessions",
125                InputValue::Choice(usize::from(self.config.latest_session_only)),
126                InputKind::Choice(vec!["Visible".into(), "Latest".into()]),
127            )
128            .with_default(InputValue::Choice(usize::from(
129                defaults.latest_session_only,
130            ))),
131        ]
132    }
133    fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
134        match (id, value) {
135            ("period", InputValue::Length(minutes @ 1..=1440)) => {
136                self.config.period_minutes = minutes as u32
137            }
138            ("row_ticks", InputValue::Length(ticks @ 0..=100_000)) => {
139                self.row_ticks = ticks;
140                self.update_tick_size();
141            }
142            ("value_area", InputValue::Number(percent)) if (1.0..=100.0).contains(&percent) => {
143                self.config.value_area_pct = percent / 100.0
144            }
145            ("split_sessions", InputValue::Toggle(split)) => self.config.split_sessions = split,
146            ("single_prints", InputValue::Toggle(show)) => self.config.show_single_prints = show,
147            ("display", InputValue::Choice(choice @ 0..=2)) => {
148                self.config.display_mode = DISPLAYS[choice]
149            }
150            ("sessions", InputValue::Choice(choice @ 0..=1)) => {
151                self.config.latest_session_only = choice == 1
152            }
153            _ => return Err(crate::studies::StudyError::rejected(&self.inputs(), id)),
154        }
155        Ok(())
156    }
157    fn set_instrument(&mut self, instrument: Option<&Instrument>) -> bool {
158        self.instrument_tick = instrument.and_then(|instrument| instrument.tick_size);
159        self.update_tick_size();
160        false
161    }
162    fn tpo(&self) -> Option<&TPOConfig> {
163        Some(&self.config)
164    }
165    fn calculate(&mut self, data: &[Bar]) {
166        self.values = IndicatorValues::missing(data.len());
167    }
168
169    fn update(&mut self, data: &[Bar], _from: usize) {
170        self.calculate(data);
171    }
172    fn is_incremental(&self) -> bool {
173        true
174    }
175
176    fn values(&self) -> &IndicatorValues {
177        &self.values
178    }
179    fn line_names(&self) -> Vec<String> {
180        Vec::new()
181    }
182    fn colors(&self) -> Vec<Color32> {
183        Vec::new()
184    }
185    fn set_colors(&mut self, _colors: Vec<Color32>) {}
186    fn is_visible(&self) -> bool {
187        self.visible
188    }
189    fn set_visible(&mut self, visible: bool) {
190        self.visible = visible;
191    }
192    fn clone_box(&self) -> Box<dyn Indicator> {
193        Box::new(self.clone())
194    }
195}