egui_charts/widget/
trade_profile.rs1use 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 pub fn trade_profile_settings(&self) -> &TradeProfileSettings {
22 &self.trade_profiles.settings
23 }
24
25 pub fn set_trade_profile_settings(&mut self, settings: TradeProfileSettings) {
27 self.trade_profiles.settings = settings;
28 }
29
30 pub fn trade_profile_ranges(&self) -> Option<TradeProfileRanges> {
33 self.execution_profile_ranges(0..self.bars(SeriesId::MAIN).len())
34 }
35
36 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 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 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 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 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}