egui_charts/chart/series/
trade_delta.rs1use crate::chart::coords::ChartMapping;
4use crate::studies::TradeDeltaValue;
5use egui::{Color32, Painter, Rect, Stroke, pos2};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum TradeDeltaStyle {
10 Histogram,
12 CumulativeCandles,
14 BarCandles,
17}
18
19pub fn render_trade_delta(
25 painter: &Painter,
26 mapping: &ChartMapping,
27 values: &[TradeDeltaValue],
28 style: TradeDeltaStyle,
29 buy: Color32,
30 sell: Color32,
31) {
32 if mapping.bar_spacing <= 0.0 || !mapping.bar_spacing.is_finite() {
33 return;
34 }
35 let painter = painter.with_clip_rect(mapping.rect.intersect(painter.clip_rect()));
36 let first = mapping
37 .x_to_idx(mapping.rect.left())
38 .saturating_sub(1)
39 .min(values.len());
40 let end = mapping
41 .x_to_idx(mapping.rect.right())
42 .saturating_add(2)
43 .min(values.len());
44 let half_width = (mapping.bar_spacing * 0.35).max(0.5);
45 for (i, value) in values.iter().enumerate().take(end).skip(first) {
46 let x = mapping.idx_to_x(i);
47 let color = if value.delta >= 0.0 { buy } else { sell };
48 let (open, close, wick) = match style {
49 TradeDeltaStyle::Histogram => (0.0, value.delta, None),
50 TradeDeltaStyle::BarCandles => {
51 (0.0, value.delta, Some((value.delta_high, value.delta_low)))
52 }
53 TradeDeltaStyle::CumulativeCandles => {
54 (value.open, value.close, Some((value.high, value.low)))
55 }
56 };
57 let y1 = mapping.price_to_y(open);
58 let y2 = mapping.price_to_y(close);
59 if !y1.is_finite() || !y2.is_finite() {
60 continue;
61 }
62 if let Some((high, low)) = wick {
63 let (high, low) = (mapping.price_to_y(high), mapping.price_to_y(low));
64 if high.is_finite() && low.is_finite() {
65 painter.line_segment([pos2(x, high), pos2(x, low)], Stroke::new(1.0, color));
66 }
67 }
68 painter.rect_filled(
69 Rect::from_min_max(
70 pos2(x - half_width, y1.min(y2)),
71 pos2(x + half_width, y1.max(y2).max(y1.min(y2) + 1.0)),
72 ),
73 0,
74 color,
75 );
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::data::Footprint;
83 use crate::model::{Bar, BarClock, OrderSide, Tick, Timeframe};
84 use crate::scales::{PriceMap, PriceRange, PriceTransform};
85 use crate::studies::trade_delta;
86 use chrono::{DateTime, TimeDelta, Utc};
87 use egui::{Context, LayerId, Pos2, Shape, vec2};
88
89 #[test]
92 fn bar_candles_span_the_bar_range_from_zero_beside_histogram_and_cumulative_candles() {
93 let start: DateTime<Utc> = "2026-01-05T10:00:00Z".parse().unwrap();
94 let bars: Vec<_> = (0..2)
95 .map(|i| {
96 Bar::new(
97 start + TimeDelta::minutes(i),
98 100.0,
99 100.0,
100 100.0,
101 100.0,
102 0.0,
103 )
104 })
105 .collect();
106 let mut footprint = Footprint::new(BarClock::new(Timeframe::Min1), 1.0, 1).unwrap();
107 for (second, size, side) in [
108 (0, 4.0, OrderSide::Buy),
109 (60, 5.0, OrderSide::Buy),
110 (61, 8.0, OrderSide::Sell),
111 (62, 5.0, OrderSide::Buy),
112 ] {
113 footprint
114 .push(&Tick::new(
115 start + TimeDelta::seconds(second),
116 100.0,
117 size,
118 Some(side),
119 ))
120 .unwrap();
121 }
122 let values = trade_delta(&footprint, &bars, 1, |_, _| false);
123 let rect = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 128.0));
124 let price = PriceMap::new(
125 PriceTransform::default(),
126 PriceRange::new(-16.0, 16.0),
127 0.0,
128 128.0,
129 false,
130 );
131 let mapping = ChartMapping::new(rect, 20.0, 0, 1, 0.0, price);
132 for (style, wicks, bodies) in [
133 (
134 TradeDeltaStyle::Histogram,
135 vec![],
136 vec![(48.0, 64.0), (56.0, 64.0)],
137 ),
138 (
140 TradeDeltaStyle::BarCandles,
141 vec![(48.0, 64.0), (44.0, 76.0)],
142 vec![(48.0, 64.0), (56.0, 64.0)],
143 ),
144 (
146 TradeDeltaStyle::CumulativeCandles,
147 vec![(48.0, 64.0), (28.0, 60.0)],
148 vec![(48.0, 64.0), (40.0, 48.0)],
149 ),
150 ] {
151 let ctx = Context::default();
152 let output = ctx.run_ui(Default::default(), |_| {
153 let painter = Painter::new(ctx.clone(), LayerId::background(), rect);
154 render_trade_delta(
155 &painter,
156 &mapping,
157 &values,
158 style,
159 Color32::GREEN,
160 Color32::RED,
161 );
162 });
163 let (mut drawn_wicks, mut drawn_bodies) = (Vec::new(), Vec::new());
164 for shape in &output.shapes {
165 match &shape.shape {
166 Shape::LineSegment { points, .. } => {
167 drawn_wicks.push((points[0].y, points[1].y))
168 }
169 Shape::Rect(body) => drawn_bodies.push((body.rect.top(), body.rect.bottom())),
170 _ => {}
171 }
172 }
173 output.drop_without_applying_deltas();
174 assert_eq!((drawn_wicks, drawn_bodies), (wicks, bodies), "{style:?}");
175 }
176 }
177}