Skip to main content

egui_charts/widget/
trade_profile.rs

1//! Display settings and timestamp-based edits for execution profiles.
2
3use super::frame::session_ranges;
4use super::{Chart, SeriesId};
5use crate::chart::overlays::TradeProfileSettings;
6use crate::model::{BarClock, SessionProvider};
7use crate::studies::TradeProfileRanges;
8use chrono::{DateTime, Utc};
9use std::ops::{Range, RangeInclusive};
10
11#[derive(Clone, Default)]
12pub(super) struct TradeProfiles {
13    settings: TradeProfileSettings,
14    context: Option<(String, BarClock)>,
15    spans: Vec<RangeInclusive<DateTime<Utc>>>,
16    splits: Vec<DateTime<Utc>>,
17}
18
19impl Chart {
20    /// Settings used when drawing [`crate::model::ChartType::SessionVolumeProfile`].
21    pub fn trade_profile_settings(&self) -> &TradeProfileSettings {
22        &self.trade_profiles.settings
23    }
24
25    /// Changes the session profile display without changing its executions or ranges.
26    pub fn set_trade_profile_settings(&mut self, settings: TradeProfileSettings) {
27        self.trade_profiles.settings = settings;
28    }
29
30    /// Current execution-profile ranges, in time order, over the loaded main bars.
31    /// Merge and split edits retain UTC boundaries across history eviction and prepending.
32    pub fn trade_profile_ranges(&self) -> Option<TradeProfileRanges> {
33        self.execution_profile_ranges(0..self.bars(SeriesId::MAIN).len())
34    }
35
36    /// Merges the indexed execution profile with its next neighbour. Returns false if absent.
37    pub fn merge_trade_profile(&mut self, index: usize) -> bool {
38        let Some(ranges) = self.trade_profile_ranges() else {
39            return false;
40        };
41        let Some(pair) = ranges.ranges().get(index..).and_then(|r| r.get(..2)) else {
42            return false;
43        };
44        let bars = self.bars(SeriesId::MAIN);
45        let end = bars.partition_point(|bar| bar.time < pair[1].end);
46        let Some(last) = end.checked_sub(1).and_then(|i| bars.get(i)) else {
47            return false;
48        };
49        // End at a held bar, so future sessions cannot join this composite accidentally.
50        let merged = pair[0].start..=last.time;
51        self.prepare_profile_edits();
52        self.trade_profiles
53            .splits
54            .retain(|t| !(merged.start() < t && merged.contains(t)));
55        self.retain_profile_span(merged);
56        true
57    }
58
59    /// Splits the indexed execution profile at a loaded bar's timestamp or clock-slot open.
60    /// Endpoints and times between bars leave the ranges unchanged.
61    pub fn split_trade_profile(&mut self, index: usize, time: DateTime<Utc>) -> bool {
62        let Some(ranges) = self.trade_profile_ranges() else {
63            return false;
64        };
65        let bars = self.bars(SeriesId::MAIN);
66        let Some(bar) = bars.get(bars.partition_point(|bar| bar.time < time)) else {
67            return false;
68        };
69        let Some(range) = ranges.ranges().get(index) else {
70            return false;
71        };
72        let open = self.profile_clock().bar_open(bar.time);
73        if (time != bar.time && time != open) || !(range.start < open && open < range.end) {
74            return false;
75        }
76        let end = bars.partition_point(|bar| bar.time < range.end);
77        let span = range.start..=bars[end - 1].time;
78        self.prepare_profile_edits();
79        // A split needs its original session start even after the first candles are evicted.
80        self.retain_profile_span(span);
81        self.trade_profiles.splits.push(open);
82        self.trade_profiles.splits.sort_unstable();
83        self.trade_profiles.splits.dedup();
84        true
85    }
86
87    /// Restores the natural session boundaries for execution profiles.
88    pub fn reset_trade_profile_ranges(&mut self) {
89        self.trade_profiles.spans.clear();
90        self.trade_profiles.splits.clear();
91    }
92
93    pub(crate) fn profile_clock(&self) -> BarClock {
94        let mut clock = self.bar_clock();
95        if let Some(zone) = self
96            .instrument()
97            .filter(|i| i.session.is_none())
98            .and_then(|i| i.time_zone.as_ref())
99        {
100            clock = clock.with_timezone(zone.clone());
101        }
102        clock
103    }
104
105    fn profile_edits_current(&self) -> bool {
106        self.trade_profiles
107            .context
108            .as_ref()
109            .is_some_and(|(symbol, clock)| {
110                symbol == self.symbol() && *clock == self.profile_clock()
111            })
112    }
113
114    fn prepare_profile_edits(&mut self) {
115        if !self.profile_edits_current() {
116            self.reset_trade_profile_ranges();
117            self.trade_profiles.context = Some((self.symbol().to_owned(), self.profile_clock()));
118        }
119    }
120
121    fn retain_profile_span(&mut self, mut span: RangeInclusive<DateTime<Utc>>) {
122        for range in &self.trade_profiles.spans {
123            if range.end() >= span.start() && range.start() <= span.end() {
124                span = (*range.start()).min(*span.start())..=(*range.end()).max(*span.end());
125            }
126        }
127        self.trade_profiles
128            .spans
129            .retain(|r| r.end() < span.start() || r.start() > span.end());
130        self.trade_profiles.spans.push(span);
131    }
132
133    pub(crate) fn execution_profile_ranges(
134        &self,
135        mut shown: Range<usize>,
136    ) -> Option<TradeProfileRanges> {
137        let bars = self.bars(SeriesId::MAIN);
138        let clock = self.profile_clock();
139        let sessions = clock.sessions();
140        let mut natural = session_ranges(bars, shown.clone(), &clock)?;
141        let current = self.profile_edits_current();
142        if current {
143            let first = natural.ranges().first()?.start;
144            let end = natural.ranges().last()?.end;
145            let original = shown.clone();
146            for merged in &self.trade_profiles.spans {
147                if *merged.start() < end
148                    && (*merged.end() >= first
149                        || sessions
150                            .session_break_between(merged.end(), &first)
151                            .is_none())
152                {
153                    shown.start = shown
154                        .start
155                        .min(bars.partition_point(|b| b.time < *merged.start()));
156                    shown.end = shown
157                        .end
158                        .max(bars.partition_point(|b| b.time <= *merged.end()));
159                }
160            }
161            if shown != original {
162                natural = session_ranges(bars, shown, &clock)?;
163            }
164        }
165        if !current {
166            return Some(natural);
167        }
168        let mut ranges: Vec<Range<DateTime<Utc>>> = Vec::new();
169        for range in natural.ranges() {
170            let merged = self.trade_profiles.spans.iter().find(|r| {
171                *r.start() < range.end
172                    && (*r.end() >= range.start
173                        || sessions
174                            .session_break_between(r.end(), &range.start)
175                            .is_none())
176            });
177            if let Some(last) = ranges.last_mut()
178                && merged.is_some_and(|r| last.end > *r.start() && last.start <= *r.end())
179            {
180                last.end = range.end;
181            } else {
182                ranges.push(merged.map_or(range.start, |r| range.start.min(*r.start()))..range.end);
183            }
184        }
185        let ranges = ranges
186            .into_iter()
187            .flat_map(|range| {
188                let mut start = range.start;
189                self.trade_profiles
190                    .splits
191                    .iter()
192                    .copied()
193                    .filter(move |t| range.start < *t && *t < range.end)
194                    .chain([range.end])
195                    .map(move |end| {
196                        let part = start..end;
197                        start = end;
198                        part
199                    })
200            })
201            .filter(|range| range.end > clock.bar_open(bars[0].time))
202            .collect();
203        TradeProfileRanges::new(ranges).ok()
204    }
205}